Add docker proxy configuration for kubernetes

Current config_controller for containerization pulls kubernetes
images from public container registry. This requires controller
to access internet. If a host network is behind a proxy, there
is no chance to setup docker proxy configuration during config_
controller, therefore, kubernetes images are not accessible.

Docker proxy configuration questions were added to require users
input http/https proxy and no proxy settings. The docker proxy
configurations are added to service_parameter table in sysinv.

http-proxy.conf is the proxy info file required by docker daemon,
generated by docker puppet manifest. It consists of the user
input docker proxy configuration.

Tests:
AIO-SX: public k8s images accessible
AIO-DX: public k8s images accessible
AIO-SX without k8s config: config_controller successfully

Story: 2004710
Task: 28741

Change-Id: Ie273ad77338cdec496c5d05bf3e05baa83166626
Signed-off-by: Mingyuan Qi <mingyuan.qi@intel.com>
This commit is contained in:
Mingyuan Qi 2019-01-25 13:22:49 +08:00
parent 4d62be583e
commit 3a30da9a88
12 changed files with 410 additions and 1 deletions

View File

@ -26,6 +26,8 @@ from configutilities.common.utils import is_mtu_valid # noqa: F401
from configutilities.common.utils import validate_network_str # noqa: F401
from configutilities.common.utils import validate_address_str # noqa: F401
from configutilities.common.utils import validate_address # noqa: F401
from configutilities.common.utils import is_valid_url # noqa: F401
from configutilities.common.utils import is_valid_domain_or_ip # noqa: F401
from configutilities.common.utils import ip_version_to_string # noqa: F401
from configutilities.common.utils import lag_mode_to_str # noqa: F401
from configutilities.common.utils import validate_openstack_password # noqa: F401

View File

@ -11,6 +11,8 @@ import six
from netaddr import IPNetwork
from netaddr import IPAddress
from netaddr import AddrFormatError
from netaddr import valid_ipv4
from netaddr import valid_ipv6
from configutilities.common.exceptions import ValidateFail
@ -131,6 +133,65 @@ def is_valid_by_path(filename):
return "/dev/disk/by-path" in filename and "-part" not in filename
def is_valid_url(url_str):
# Django URL validation patterns
r = re.compile(
r'^(?:http|ftp)s?://' # http:// or https://
r'(?:(?:[A-Z0-9](?:[A-Z0-9-]{0,61}[A-Z0-9])?\.)' # domain...
r'+(?:[A-Z]{2,6}\.?|[A-Z0-9-]{2,}\.?)|'
r'localhost|' # localhost...
r'\d{1,3}\.\d{1,3}\.\d{1,3}\.\d{1,3})' # ...or ip
r'(?::\d+)?' # optional port
r'(?:/?|[/?]\S+)$', re.IGNORECASE)
url = r.match(url_str)
if url:
return True
else:
return False
def is_valid_domain(url_str):
r = re.compile(
r'^(?:(?:[A-Z0-9](?:[A-Z0-9-]{0,61}[A-Z0-9])?\.)' # domain...
r'+(?:[A-Z]{2,6}\.?|[A-Z0-9-]{2,}\.?)|'
r'[A-Za-z0-9-_]*)' # localhost, hostname
r'(?::\d+)?' # optional port
r'(?:/?|[/?]\S+)$', re.IGNORECASE)
url = r.match(url_str)
if url:
return True
else:
return False
def is_valid_ipv4(address):
"""Verify that address represents a valid IPv4 address."""
try:
return valid_ipv4(address)
except Exception:
return False
def is_valid_ipv6(address):
try:
return valid_ipv6(address)
except Exception:
return False
def is_valid_domain_or_ip(url_str):
if is_valid_domain(url_str):
return True
elif is_valid_ipv4(url_str):
return True
elif is_valid_ipv6(url_str):
return True
else:
return False
def validate_address_str(ip_address_str, network):
"""Determine whether an address is valid."""
try:

View File

@ -24,6 +24,8 @@ from configutilities.common.utils import get_service
from configutilities.common.utils import get_optional
from configutilities.common.utils import validate_address_str
from configutilities.common.utils import validate_nameserver_address_str
from configutilities.common.utils import is_valid_url
from configutilities.common.utils import is_valid_domain_or_ip
from configutilities.common.exceptions import ConfigFail
from configutilities.common.exceptions import ValidateFail
@ -965,6 +967,48 @@ class ConfigValidator(object):
"Invalid DNS NAMESERVER value of %s.\nReason: %s" %
(dns_address_str, e))
def validate_docker_proxy(self):
if not self.conf.has_section('DOCKER_PROXY'):
return
if self.cgcs_conf is not None:
self.cgcs_conf.add_section('cDOCKER_PROXY')
# check http_proxy
if self.conf.has_option('DOCKER_PROXY', 'DOCKER_HTTP_PROXY'):
docker_http_proxy_str = self.conf.get(
'DOCKER_PROXY', 'DOCKER_HTTP_PROXY')
if is_valid_url(docker_http_proxy_str):
if self.cgcs_conf is not None:
self.cgcs_conf.set('cDOCKER_PROXY', 'DOCKER_HTTP_PROXY',
docker_http_proxy_str)
else:
raise ConfigFail(
"Invalid DOCKER_HTTP_PROXY value of %s." %
docker_http_proxy_str)
# check https_proxy
if self.conf.has_option('DOCKER_PROXY', 'DOCKER_HTTPS_PROXY'):
docker_https_proxy_str = self.conf.get(
'DOCKER_PROXY', 'DOCKER_HTTPS_PROXY')
if is_valid_url(docker_https_proxy_str):
if self.cgcs_conf is not None:
self.cgcs_conf.set('cDOCKER_PROXY', 'DOCKER_HTTPS_PROXY',
docker_https_proxy_str)
else:
raise ConfigFail(
"Invalid DOCKER_HTTPS_PROXY value of %s." %
docker_https_proxy_str)
# check no_proxy
if self.conf.has_option('DOCKER_PROXY', 'DOCKER_NO_PROXY'):
docker_no_proxy_list_str = self.conf.get(
'DOCKER_PROXY', 'DOCKER_NO_PROXY').split(',')
for no_proxy_str in docker_no_proxy_list_str:
if not is_valid_domain_or_ip(no_proxy_str):
raise ConfigFail(
"Invalid DOCKER_NO_PROXY value of %s." %
no_proxy_str)
if self.cgcs_conf is not None:
self.cgcs_conf.set('cDOCKER_PROXY', 'DOCKER_NO_PROXY',
docker_no_proxy_list_str)
def validate_ntp(self):
if self.conf.has_section('NTP'):
raise ConfigFail("NTP Configuration is no longer supported")
@ -1421,6 +1465,8 @@ def validate(system_config, config_type=REGION_CONFIG, cgcs_config=None,
# Neutron configuration - leave blank to use defaults
# DNS configuration
validator.validate_dns()
# Docker Proxy configuration
validator.validate_docker_proxy()
# NTP configuration
validator.validate_ntp()
# Network configuration

View File

@ -29,6 +29,8 @@ from configutilities import validate_address_str
from configutilities import validate_address
from configutilities import ip_version_to_string
from configutilities import validate_nameserver_address_str
from configutilities import is_valid_url
from configutilities import is_valid_domain_or_ip
from configutilities import validate_openstack_password
from configutilities import DEFAULT_DOMAIN_NAME
from netaddr import IPNetwork
@ -462,6 +464,12 @@ class ConfigAssistant():
self.cluster_pod_subnet = IPNetwork("172.16.0.0/16")
self.cluster_service_subnet = IPNetwork("10.96.0.0/12")
# Docker Proxy config
self.enable_docker_proxy = False
self.docker_http_proxy = ""
self.docker_https_proxy = ""
self.docker_no_proxy = ""
# SDN config
self.enable_sdn = False
@ -2729,6 +2737,118 @@ class ConfigAssistant():
print("Invalid address - please enter a valid IPv4 "
"address")
def input_docker_proxy_config(self):
"""Allow user to input docker proxy config."""
print("\nDocker Proxy:")
print("-------------------------\n")
print(textwrap.fill(
"Docker proxy is needed if host OAM network is behind a proxy.",
80))
print('')
while True:
user_input = input(
"Configure docker proxy [y/N]: ")
if user_input.lower() == 'q':
raise UserQuit
elif user_input.lower() == 'y':
while True:
user_input = input(
"HTTP proxy (http://example.proxy:port): ")
if user_input.lower() == 'q':
raise UserQuit
if user_input:
if is_valid_url(user_input):
self.docker_http_proxy = user_input
break
else:
print("Please input a valid url")
continue
else:
self.docker_http_proxy = ""
break
while True:
user_input = input(
"HTTPS proxy (https://example.proxy:port): ")
if user_input.lower() == 'q':
raise UserQuit
if user_input:
if is_valid_url(user_input):
self.docker_https_proxy = user_input
break
else:
print("Please input a valid url")
continue
else:
self.docker_https_proxy = ""
break
if not self.docker_http_proxy and not self.docker_https_proxy:
print("At least one proxy required")
continue
else:
self.enable_docker_proxy = True
while True:
# TODO: Current Docker version 18.03.1-ce utilizes go-lang
# net library for proxy setting. The go-lang net lib
# doesn't support CIDR notation until this commit:
#
# https://github.com/golang/net/commit/
# c21de06aaf072cea07f3a65d6970e5c7d8b6cd6d
#
# After docker upgrades to a version that CIDR notation
# supported pre_set_no_proxy will be simplified to subnets
if self.system_mode == \
sysinv_constants.SYSTEM_MODE_SIMPLEX:
pre_set_no_proxy = "localhost,127.0.0.1," + \
str(self.controller_floating_address) + "," + \
str(self.controller_address_0) + "," + \
str(self.controller_address_1) + "," + \
str(self.external_oam_address_0)
else:
pre_set_no_proxy = "localhost,127.0.0.1," + \
str(self.controller_floating_address) + "," + \
str(self.controller_address_0) + "," + \
str(self.controller_address_1) + "," + \
str(self.external_oam_floating_address) + "," + \
str(self.external_oam_address_0) + "," + \
str(self.external_oam_address_1)
user_input = input(
"Additional NO proxy besides '" +
pre_set_no_proxy +
"'\n(Comma-separated addresses, " +
"wildcard/subnet not allowed)\n:")
if user_input.lower() == 'q':
raise UserQuit
if user_input:
input_addr_list = user_input.split(",")
valid_address = True
for input_addr in input_addr_list:
if not is_valid_domain_or_ip(input_addr):
print("Input address '%s' is invalid" %
input_addr)
valid_address = False
break
if valid_address:
self.docker_no_proxy = pre_set_no_proxy + \
"," + user_input
break
else:
continue
else:
self.docker_no_proxy = pre_set_no_proxy
break
break
elif user_input.lower() in ('n', ''):
self.enable_docker_proxy = False
break
else:
print("Invalid choice")
continue
def input_authentication_config(self):
"""Allow user to input authentication config and perform validation.
"""
@ -2814,6 +2934,8 @@ class ConfigAssistant():
self.input_external_oam_config()
if self.kubernetes:
self.input_dns_config()
# Docker proxy is only used in kubernetes config
self.input_docker_proxy_config()
self.input_authentication_config()
def is_valid_management_multicast_subnet(self, ip_subnet):
@ -3159,6 +3281,22 @@ class ConfigAssistant():
self.nameserver_addresses[x] = \
IPAddress(cvalue)
# Docker Proxy Configuration
if config.has_section('cDOCKER_PROXY'):
self.enable_docker_proxy = True
if config.has_option('cDOCKER_PROXY',
'DOCKER_HTTP_PROXY'):
self.docker_http_proxy = config.get(
'cDOCKER_PROXY', 'DOCKER_HTTP_PROXY')
if config.has_option('cDOCKER_PROXY',
'DOCKER_HTTPS_PROXY'):
self.docker_https_proxy = config.get(
'cDOCKER_PROXY', 'DOCKER_HTTPS_PROXY')
if config.has_option('cDOCKER_PROXY',
'DOCKER_NO_PROXY'):
self.docker_no_proxy = config.get(
'cDOCKER_PROXY', 'DOCKER_NO_PROXY')
# SDN Network configuration
if config.has_option('cSDN', 'ENABLE_SDN'):
raise ConfigFail("The option ENABLE_SDN is no longer "
@ -3602,6 +3740,15 @@ class ConfigAssistant():
dns_config = True
if not dns_config:
print("External DNS servers not configured")
if self.enable_docker_proxy:
print("\nDocker Proxy Configuraton")
print("----------------------")
if self.docker_http_proxy:
print("Docker HTTP proxy: " + self.docker_http_proxy)
if self.docker_https_proxy:
print("Docker HTTPS proxy: " + self.docker_https_proxy)
if self.docker_no_proxy:
print("Docker NO proxy: " + self.docker_no_proxy)
if self.region_config:
print("\nRegion Configuration")
@ -3899,6 +4046,23 @@ class ConfigAssistant():
else:
f.write("NAMESERVER_" + str(x + 1) + "=NC" + "\n")
# Docker proxy configuration
if self.enable_docker_proxy:
f.write("\n[cDOCKER_PROXY]")
f.write("\n# Docker Proxy Configuration\n")
if self.docker_http_proxy:
f.write(
"DOCKER_HTTP_PROXY=" +
str(self.docker_http_proxy) + "\n")
if self.docker_https_proxy:
f.write(
"DOCKER_HTTPS_PROXY=" +
str(self.docker_https_proxy) + "\n")
if self.docker_no_proxy:
f.write(
"DOCKER_NO_PROXY=" +
str(self.docker_no_proxy) + "\n")
# Network configuration
f.write("\n[cNETWORK]")
f.write("\n# Data Network Configuration\n")
@ -5302,6 +5466,24 @@ class ConfigAssistant():
patch = sysinv.dict_to_patch(values)
client.sysinv.idns.update(dns_record.uuid, patch)
def _populate_docker_config(self, client):
parameter = {}
if self.docker_http_proxy:
parameter['http_proxy'] = self.docker_http_proxy
if self.docker_https_proxy:
parameter['https_proxy'] = self.docker_https_proxy
if self.docker_no_proxy:
parameter['no_proxy'] = self.docker_no_proxy
if parameter:
client.sysinv.service_parameter.create(
sysinv_constants.SERVICE_TYPE_DOCKER,
sysinv_constants.SERVICE_PARAM_SECTION_DOCKER_PROXY,
None,
None,
parameter
)
def populate_initial_config(self):
"""Populate initial system inventory configuration"""
try:
@ -5311,6 +5493,8 @@ class ConfigAssistant():
self._populate_network_config(client)
if self.kubernetes:
self._populate_dns_config(client)
if self.enable_docker_proxy:
self._populate_docker_config(client)
controller = self._populate_controller_config(client)
# ceph_mon config requires controller host to be created
self._inventory_config_complete_wait(client, controller)

View File

@ -73,6 +73,12 @@ NAMESERVER_1=1.2.3.4
NAMESERVER_2=5.6.7.8
NAMESERVER_3=NC
[cDOCKER_PROXY]
# Docker Proxy Configuration
DOCKER_HTTP_PROXY=http://proxy.com:123
DOCKER_HTTPS_PROXY=https://proxy.com:123
DOCKER_NO_PROXY=localhost,127.0.0.1,192.168.204.2
[cNETWORK]
# Data Network Configuration
VSWITCH_TYPE=ovs-dpdk

View File

@ -49,6 +49,12 @@ LOGICAL_INTERFACE=LOGICAL_INTERFACE_2
NAMESERVER_1=1.2.3.4
NAMESERVER_2=5.6.7.8
[DOCKER_PROXY]
# Docker Proxy Configuration
DOCKER_HTTP_PROXY=http://proxy.com:123
DOCKER_HTTPS_PROXY=https://proxy.com:123
DOCKER_NO_PROXY=localhost,127.0.0.1,192.168.204.2
;[PXEBOOT_NETWORK]
;PXEBOOT_CIDR=192.168.203.0/24

View File

@ -647,3 +647,10 @@ def test_kubernetes():
cr.create_cgcs_config_file(None, system_config, None, None, None, 0,
validate_only=True)
validate(system_config, DEFAULT_CONFIG, None, False)
# Test absence of optional docker proxy configuration
system_config = cr.parse_system_config(systemfile)
system_config.remove_section('DOCKER_PROXY')
cr.create_cgcs_config_file(None, system_config, None, None, None, 0,
validate_only=True)
validate(system_config, DEFAULT_CONFIG, None, False)

View File

@ -1,5 +1,8 @@
class platform::docker::params (
$package_name = 'docker-ce',
$package_name = 'docker-ce',
$http_proxy = undef,
$https_proxy = undef,
$no_proxy = undef,
) { }
class platform::docker::config
@ -9,6 +12,22 @@ class platform::docker::config
if $::platform::kubernetes::params::enabled {
if $http_proxy or $https_proxy {
file { '/etc/systemd/system/docker.service.d':
ensure => 'directory',
owner => 'root',
group => 'root',
mode => '0755',
}
-> file { '/etc/systemd/system/docker.service.d/http-proxy.conf':
ensure => present,
owner => 'root',
group => 'root',
mode => '0644',
content => template('platform/dockerproxy.conf.erb'),
}
}
Class['::platform::filesystem::docker'] ~> Class[$name]
service { 'docker':

View File

@ -0,0 +1,8 @@
[Service]
<%- if @http_proxy -%>
Environment="HTTP_PROXY=<%= @http_proxy %>"
<%- end -%>
<%- if @https_proxy -%>
Environment="HTTPS_PROXY=<%= @https_proxy %>"
<%- end -%>
Environment="NO_PROXY=<%= @no_proxy %>"

View File

@ -874,6 +874,7 @@ SERVICE_TYPE_PANKO = 'panko'
SERVICE_TYPE_AODH = 'aodh'
SERVICE_TYPE_GLANCE = 'glance'
SERVICE_TYPE_BARBICAN = 'barbican'
SERVICE_TYPE_DOCKER = 'docker'
SERVICE_PARAM_SECTION_MURANO_RABBITMQ = 'rabbitmq'
SERVICE_PARAM_SECTION_MURANO_ENGINE = 'engine'
@ -1064,6 +1065,13 @@ SERVICE_PARAM_AODH_DATABASE_ALARM_HISTORY_TIME_TO_LIVE_DEFAULT = PM_TTL_DEFAULT
SERVICE_PARAM_SECTION_SWIFT_CONFIG = 'config'
SERVICE_PARAM_NAME_SWIFT_SERVICE_ENABLED = 'service_enabled'
SERVICE_PARAM_NAME_SWIFT_FS_SIZE_MB = 'fs_size_mb'
# docker parameters
SERVICE_PARAM_SECTION_DOCKER_PROXY = 'proxy'
SERVICE_PARAM_NAME_DOCKER_HTTP_PROXY = 'http_proxy'
SERVICE_PARAM_NAME_DOCKER_HTTPS_PROXY = 'https_proxy'
SERVICE_PARAM_NAME_DOCKER_NO_PROXY = 'no_proxy'
# default filesystem size to 25 MB
SERVICE_PARAM_SWIFT_FS_SIZE_MB_DEFAULT = 25

View File

@ -648,6 +648,25 @@ def _validate_swift_enabled(name, value):
"Swift API is already supported by Ceph Object Gateway."))
def _validate_docker_proxy_address(name, value):
"""Check if proxy value is valid"""
if not cutils.is_url(value):
raise wsme.exc.ClientSideError(_(
"Parameter '%s' must be a valid address." % name))
def _validate_docker_no_proxy_address(name, value):
"""Check if no proxy value is valid"""
values = value.split(',')
for item in values:
# will extend to more cases if CIDR notation is supported
if not cutils.is_valid_domain(item):
if not cutils.is_valid_ip(item):
raise wsme.exc.ClientSideError(_(
"Parameter '%s' includes an invalid address '%s'." %
(name, item)))
# LDAP Identity Service Parameters (mandatory)
SERVICE_PARAM_IDENTITY_LDAP_URL = 'url'
@ -1476,6 +1495,27 @@ SWIFT_CONFIG_PARAMETER_DATA_FORMAT = {
constants.SERVICE_PARAM_NAME_SWIFT_SERVICE_ENABLED: SERVICE_PARAMETER_DATA_FORMAT_BOOLEAN,
}
DOCKER_PROXY_PARAMETER_OPTIONAL = [
constants.SERVICE_PARAM_NAME_DOCKER_HTTP_PROXY,
constants.SERVICE_PARAM_NAME_DOCKER_HTTPS_PROXY,
constants.SERVICE_PARAM_NAME_DOCKER_NO_PROXY,
]
DOCKER_PROXY_PARAMETER_VALIDATOR = {
constants.SERVICE_PARAM_NAME_DOCKER_HTTP_PROXY: _validate_docker_proxy_address,
constants.SERVICE_PARAM_NAME_DOCKER_HTTPS_PROXY: _validate_docker_proxy_address,
constants.SERVICE_PARAM_NAME_DOCKER_NO_PROXY: _validate_docker_no_proxy_address,
}
DOCKER_PROXY_PARAMETER_RESOURCE = {
constants.SERVICE_PARAM_NAME_DOCKER_HTTP_PROXY:
'platform::docker::params::http_proxy',
constants.SERVICE_PARAM_NAME_DOCKER_HTTPS_PROXY:
'platform::docker::params::https_proxy',
constants.SERVICE_PARAM_NAME_DOCKER_NO_PROXY:
'platform::docker::params::no_proxy',
}
# Service Parameter Schema
SERVICE_PARAM_MANDATORY = 'mandatory'
SERVICE_PARAM_OPTIONAL = 'optional'
@ -1652,6 +1692,13 @@ SERVICE_PARAMETER_SCHEMA = {
SERVICE_PARAM_DATA_FORMAT: SWIFT_CONFIG_PARAMETER_DATA_FORMAT,
},
},
constants.SERVICE_TYPE_DOCKER: {
constants.SERVICE_PARAM_SECTION_DOCKER_PROXY: {
SERVICE_PARAM_OPTIONAL: DOCKER_PROXY_PARAMETER_OPTIONAL,
SERVICE_PARAM_VALIDATOR: DOCKER_PROXY_PARAMETER_VALIDATOR,
SERVICE_PARAM_RESOURCE: DOCKER_PROXY_PARAMETER_RESOURCE,
},
},
}
SERVICE_PARAMETER_MAX_LENGTH = 255

View File

@ -1797,6 +1797,21 @@ def is_url(url_str):
return False
def is_valid_domain(url_str):
r = re.compile(
r'^(?:(?:[A-Z0-9](?:[A-Z0-9-]{0,61}[A-Z0-9])?\.)' # domain...
r'+(?:[A-Z]{2,6}\.?|[A-Z0-9-]{2,}\.?)|'
r'[A-Za-z0-9-_]*)' # localhost, hostname
r'(?::\d+)?' # optional port
r'(?:/?|[/?]\S+)$', re.IGNORECASE)
url = r.match(url_str)
if url:
return True
else:
return False
def verify_checksum(path):
""" Find and validate the checksum file in a given directory. """
rc = True