Contanerized Undercloud - Routed Spine-Leaf

* Update config to use the same options used in
  instack-undercloud for routed ctlplane network.
* Update pre-flight validations to validate all
  the networks. (Also fix and re-enabled validations
  that was disabled)
* Create input for OS::TripleO::Services::MasqueradeNetworks

Depends-On: Ide1267bfd9cc60d837dc823e4e106ac70dd2e5e6
Change-Id: I5fbac0c4a75ad2fb719bfd10887778c8eaeacfd6
This commit is contained in:
Harald Jensas 2018-02-12 14:12:07 +01:00 committed by Harald Jensås
parent 644cfd38c8
commit 81c64c6798
2 changed files with 220 additions and 98 deletions

View File

@ -35,21 +35,24 @@ from tripleoclient.v1 import undercloud_preflight
PARAMETER_MAPPING = { PARAMETER_MAPPING = {
'network_gateway': 'UndercloudNetworkGateway',
'enabled_drivers': 'IronicEnabledDrivers',
'inspection_iprange': 'IronicInspectorIpRange',
'inspection_interface': 'IronicInspectorInterface', 'inspection_interface': 'IronicInspectorInterface',
'dhcp_start': 'UndercloudDhcpRangeStart', 'enabled_drivers': 'IronicEnabledDrivers',
'dhcp_end': 'UndercloudDhcpRangeEnd',
'network_cidr': 'UndercloudNetworkCidr',
'undercloud_debug': 'Debug', 'undercloud_debug': 'Debug',
'ipxe_enabled': 'IronicInspectorIPXEEnabled', 'ipxe_enabled': 'IronicInspectorIPXEEnabled',
'certificate_generation_ca': 'CertmongerCA', 'certificate_generation_ca': 'CertmongerCA',
'undercloud_public_host': 'CloudName', 'undercloud_public_host': 'CloudName',
'scheduler_max_attempts': 'NovaSchedulerMaxAttempts', 'scheduler_max_attempts': 'NovaSchedulerMaxAttempts',
'local_mtu': 'UndercloudLocalMtu', 'local_mtu': 'UndercloudLocalMtu',
'undercloud_nameservers': 'DnsServers',
'clean_nodes': 'IronicAutomatedClean', 'clean_nodes': 'IronicAutomatedClean',
'local_subnet': 'UndercloudCtlplaneLocalSubnet',
'enable_routed_networks': 'UndercloudEnableRoutedNetworks'
}
SUBNET_PARAMETER_MAPPING = {
'cidr': 'NetworkCidr',
'gateway': 'NetworkGateway',
'dhcp_start': 'DhcpRangeStart',
'dhcp_end': 'DhcpRangeEnd',
} }
THT_HOME = os.environ.get('THT_HOME', THT_HOME = os.environ.get('THT_HOME',
@ -61,6 +64,9 @@ TELEMETRY_DOCKER_ENV_YAML = [
'environments/services-docker/undercloud-panko.yaml', 'environments/services-docker/undercloud-panko.yaml',
'environments/services-docker/undercloud-ceilometer.yaml'] 'environments/services-docker/undercloud-ceilometer.yaml']
# Control plane network name
SUBNETS_DEFAULT = ['ctlplane-subnet']
class Paths(object): class Paths(object):
@property @property
@ -71,6 +77,16 @@ class Paths(object):
CONF = cfg.CONF CONF = cfg.CONF
PATHS = Paths() PATHS = Paths()
# Deprecated options
_deprecated_opt_network_gateway = [cfg.DeprecatedOpt(
'network_gateway', group='DEFAULT')]
_deprecated_opt_network_cidr = [cfg.DeprecatedOpt(
'network_cidr', group='DEFAULT')]
_deprecated_opt_dhcp_start = [cfg.DeprecatedOpt(
'dhcp_start', group='DEFAULT')]
_deprecated_opt_dhcp_end = [cfg.DeprecatedOpt('dhcp_end', group='DEFAULT')]
_deprecated_opt_inspection_iprange = [cfg.DeprecatedOpt(
'inspection_iprange', group='DEFAULT')]
# When adding new options to the lists below, make sure to regenerate the # When adding new options to the lists below, make sure to regenerate the
# sample config by running "tox -e genconfig" in the project root. # sample config by running "tox -e genconfig" in the project root.
@ -107,12 +123,6 @@ _opts = [
'local_interface, with the netmask defined by the ' 'local_interface, with the netmask defined by the '
'prefix portion of the value.') 'prefix portion of the value.')
), ),
cfg.StrOpt('network_gateway',
default='192.168.24.1',
help=('Network gateway for the Neutron-managed network for '
'Overcloud instances. This should match the local_ip '
'above when using masquerading.')
),
cfg.StrOpt('undercloud_public_host', cfg.StrOpt('undercloud_public_host',
deprecated_name='undercloud_public_vip', deprecated_name='undercloud_public_vip',
default='192.168.24.2', default='192.168.24.2',
@ -138,6 +148,36 @@ _opts = [
'The overcloud parameter "CloudDomain" must be set to a ' 'The overcloud parameter "CloudDomain" must be set to a '
'matching value.') 'matching value.')
), ),
cfg.ListOpt('subnets',
default=SUBNETS_DEFAULT,
help=('List of routed network subnets for provisioning '
'and introspection. Comma separated list of names/tags. '
'For each network a section/group needs to be added to '
'the configuration file with these parameters set: '
'cidr, dhcp_start, dhcp_end, inspection_iprange, '
'gateway and masquerade_network.'
'\n\n'
'Example:\n\n'
'subnets = subnet1,subnet2\n'
'\n'
'An example section/group in config file:\n'
'\n'
'[subnet1]\n'
'cidr = 192.168.10.0/24\n'
'dhcp_start = 192.168.10.100\n'
'dhcp_end = 192.168.10.200\n'
'inspection_iprange = 192.168.10.20,192.168.10.90\n'
'gateway = 192.168.10.254\n'
'masquerade = True'
'\n'
'[subnet2]\n'
'. . .\n')),
cfg.StrOpt('local_subnet',
default=SUBNETS_DEFAULT[0],
help=('Name of the local subnet, where the PXE boot and DHCP '
'interfaces for overcloud instances is located. The IP '
'address of the local_ip/local_interface should reside '
'in this subnet.')),
cfg.StrOpt('undercloud_service_certificate', cfg.StrOpt('undercloud_service_certificate',
default='', default='',
help=('Certificate file to use for OpenStack service SSL ' help=('Certificate file to use for OpenStack service SSL '
@ -182,22 +222,6 @@ _opts = [
default=1500, default=1500,
help=('MTU to use for the local_interface.') help=('MTU to use for the local_interface.')
), ),
cfg.StrOpt('network_cidr',
default='192.168.24.0/24',
help=('Network CIDR for the Neutron-managed network for '
'Overcloud instances. This should be the subnet used '
'for PXE booting.')
),
cfg.StrOpt('dhcp_start',
default='192.168.24.5',
help=('Start of DHCP allocation range for PXE and DHCP of '
'Overcloud instances.')
),
cfg.StrOpt('dhcp_end',
default='192.168.24.24',
help=('End of DHCP allocation range for PXE and DHCP of '
'Overcloud instances.')
),
cfg.StrOpt('hieradata_override', cfg.StrOpt('hieradata_override',
default='', default='',
help=('Path to hieradata override file. If set, the file will ' help=('Path to hieradata override file. If set, the file will '
@ -222,14 +246,6 @@ _opts = [
help=('Network interface on which inspection dnsmasq will ' help=('Network interface on which inspection dnsmasq will '
'listen. If in doubt, use the default value.') 'listen. If in doubt, use the default value.')
), ),
cfg.StrOpt('inspection_iprange',
default='192.168.24.100,192.168.24.120',
deprecated_name='discovery_iprange',
help=('Temporary IP range that will be given to nodes during '
'the inspection process. Should not overlap with the '
'range defined by dhcp_start and dhcp_end, but should '
'be in the same network.')
),
cfg.BoolOpt('inspection_extras', cfg.BoolOpt('inspection_extras',
default=True, default=True,
help=('Whether to enable extra hardware collection during ' help=('Whether to enable extra hardware collection during '
@ -398,15 +414,60 @@ _opts = [
cfg.ListOpt('custom_env_files', cfg.ListOpt('custom_env_files',
default=[], default=[],
help=('List of any custom environment yaml files to use')), help=('List of any custom environment yaml files to use')),
cfg.BoolOpt('enable_routed_networks',
default=False,
help=('Enable support for routed ctlplane networks.')),
]
# Routed subnets
_subnets_opts = [
cfg.StrOpt('cidr',
default='192.168.24.0/24',
deprecated_opts=_deprecated_opt_network_cidr,
help=('Network CIDR for the Neutron-managed subnet for '
'Overcloud instances.')),
cfg.StrOpt('dhcp_start',
default='192.168.24.5',
deprecated_opts=_deprecated_opt_dhcp_start,
help=('Start of DHCP allocation range for PXE and DHCP of '
'Overcloud instances on this network.')),
cfg.StrOpt('dhcp_end',
default='192.168.24.24',
deprecated_opts=_deprecated_opt_dhcp_end,
help=('End of DHCP allocation range for PXE and DHCP of '
'Overcloud instances on this network.')),
cfg.StrOpt('inspection_iprange',
default='192.168.24.100,192.168.24.120',
deprecated_opts=_deprecated_opt_inspection_iprange,
help=('Temporary IP range that will be given to nodes on this '
'network during the inspection process. Should not '
'overlap with the range defined by dhcp_start and '
'dhcp_end, but should be in the same ip subnet.')),
cfg.StrOpt('gateway',
default='192.168.24.1',
deprecated_opts=_deprecated_opt_network_gateway,
help=('Network gateway for the Neutron-managed network for '
'Overcloud instances on this network.')),
cfg.BoolOpt('masquerade',
default=False,
help=('The network will be masqueraded for external access.')),
] ]
CONF.register_opts(_opts) CONF.register_opts(_opts)
def _load_subnets_config_groups():
for group in CONF.subnets:
g = cfg.OptGroup(name=group, title=group)
CONF.register_opts(_subnets_opts, group=g)
LOG = logging.getLogger(__name__ + ".undercloud_config") LOG = logging.getLogger(__name__ + ".undercloud_config")
def list_opts(): def list_opts():
return [(None, copy.deepcopy(_opts))] return [(None, copy.deepcopy(_opts)),
(SUBNETS_DEFAULT[0], copy.deepcopy(_subnets_opts))]
def _load_config(): def _load_config():
@ -505,6 +566,66 @@ def _process_ipa_args(conf, env):
env['IronicInspectorKernelArgs'] = ' '.join(inspection_kernel_args) env['IronicInspectorKernelArgs'] = ' '.join(inspection_kernel_args)
def _generate_inspection_subnets():
env_list = []
for subnet in CONF.subnets:
env_dict = {}
s = CONF.get(subnet)
env_dict['tag'] = subnet
env_dict['ip_range'] = s.inspection_iprange
env_dict['netmask'] = str(netaddr.IPNetwork(s.cidr).netmask)
env_dict['gateway'] = s.gateway
env_list.append(env_dict)
return env_list
def _generate_subnets_static_routes():
env_list = []
local_router = CONF.get(CONF.local_subnet).gateway
for subnet in CONF.subnets:
if subnet == str(CONF.local_subnet):
continue
s = CONF.get(subnet)
env_list.append({'ip_netmask': s.cidr, 'next_hop': local_router})
return env_list
def _generate_masquerade_networks():
"""Create input for OS::TripleO::Services::MasqueradeNetworks
The service use parameter MasqueradeNetworks with the following
formating:
{'source_cidr_A': ['destination_cidr_A', 'destination_cidr_B'],
'source_cidr_B': ['destination_cidr_A', 'destination_cidr_B']}
"""
network_cidrs = []
for subnet in CONF.subnets:
s = CONF.get(subnet)
network_cidrs.append(s.cidr)
masqurade_networks = {}
for subnet in CONF.subnets:
s = CONF.get(subnet)
if s.masquerade:
masqurade_networks.update({s.cidr: network_cidrs})
return masqurade_networks
# def _generate_subnets_cidr_nat_rules():
# env_list = []
# for subnet in CONF.subnets:
# env_dict = {}
# s = CONF.get(subnet)
# env_dict['140 ' + subnet + ' cidr nat'] = {
# 'chain': 'FORWARD',
# 'destination': s.cidr
# }
# # NOTE(hjensas): sort_keys=True because unit test reference is static
# env_list.append(json.dumps(env_dict, sort_keys=True)[1:-1])
# # Whitespace after newline required for indentation in templated yaml
# return '\n '.join(env_list)
def prepare_undercloud_deploy(upgrade=False, no_validations=False): def prepare_undercloud_deploy(upgrade=False, no_validations=False):
"""Prepare Undercloud deploy command based on undercloud.conf""" """Prepare Undercloud deploy command based on undercloud.conf"""
@ -512,6 +633,7 @@ def prepare_undercloud_deploy(upgrade=False, no_validations=False):
registry_overwrites = {} registry_overwrites = {}
deploy_args = [] deploy_args = []
_load_config() _load_config()
_load_subnets_config_groups()
# Set the undercloud home dir parameter so that stackrc is produced in # Set the undercloud home dir parameter so that stackrc is produced in
# the users home directory. # the users home directory.
@ -521,14 +643,22 @@ def prepare_undercloud_deploy(upgrade=False, no_validations=False):
if param_key in CONF.keys(): if param_key in CONF.keys():
env_data[param_value] = CONF[param_key] env_data[param_value] = CONF[param_key]
# Set up parameters for undercloud networking
env_data['IronicInspectorSubnets'] = _generate_inspection_subnets()
env_data['ControlPlaneStaticRoutes'] = _generate_subnets_static_routes()
env_data['UndercloudCtlplaneSubnets'] = {}
for subnet in CONF.subnets:
s = CONF.get(subnet)
env_data['UndercloudCtlplaneSubnets'][subnet] = {}
for param_key, param_value in SUBNET_PARAMETER_MAPPING.items():
env_data['UndercloudCtlplaneSubnets'][subnet].update(
{param_value: s[param_key]})
env_data['MasqueradeNetworks'] = _generate_masquerade_networks()
env_data['DnsServers'] = ','.join(CONF['undercloud_nameservers'])
# Parse the undercloud.conf options to include necessary args and # Parse the undercloud.conf options to include necessary args and
# yaml files for undercloud deploy command # yaml files for undercloud deploy command
# we use this to set --dns-nameserver for the ctlplane network
# so just pick the first entry
if CONF.get('undercloud_nameservers', None):
env_data['UndercloudNameserver'] = CONF['undercloud_nameservers'][0]
if CONF.get('undercloud_ntp_servers', None): if CONF.get('undercloud_ntp_servers', None):
env_data['NtpServer'] = CONF['undercloud_ntp_servers'][0] env_data['NtpServer'] = CONF['undercloud_ntp_servers'][0]
@ -757,7 +887,9 @@ def _write_env_file(env_data,
env_file = os.path.abspath(env_file) env_file = os.path.abspath(env_file)
with open(env_file, "w") as f: with open(env_file, "w") as f:
try: try:
yaml.dump(data, f, default_flow_style=False) dumper = yaml.dumper.SafeDumper
dumper.ignore_aliases = lambda self, data: True
yaml.dump(data, f, default_flow_style=False, Dumper=dumper)
except yaml.YAMLError as exc: except yaml.YAMLError as exc:
raise exc raise exc
return env_file return env_file

View File

@ -194,7 +194,7 @@ def _validate_ips():
msg = '%s "%s" must be a valid IP address' % \ msg = '%s "%s" must be a valid IP address' % \
(param_name, value) (param_name, value)
raise FailedValidation(msg) raise FailedValidation(msg)
for ip in CONF['undercloud_nameservers']: for ip in CONF.undercloud_nameservers:
is_ip(ip, 'undercloud_nameservers') is_ip(ip, 'undercloud_nameservers')
@ -206,7 +206,7 @@ def _validate_value_formats():
hostname must be a FQDN. hostname must be a FQDN.
""" """
try: try:
local_ip = netaddr.IPNetwork(CONF['local_ip']) local_ip = netaddr.IPNetwork(CONF.local_ip)
if local_ip.prefixlen == 32: if local_ip.prefixlen == 32:
raise netaddr.AddrFormatError('Invalid netmask') raise netaddr.AddrFormatError('Invalid netmask')
# If IPv6 the ctlplane network uses the EUI-64 address format, # If IPv6 the ctlplane network uses the EUI-64 address format,
@ -216,7 +216,7 @@ def _validate_value_formats():
except netaddr.core.AddrFormatError as e: except netaddr.core.AddrFormatError as e:
message = ('local_ip "%s" not valid: "%s" ' message = ('local_ip "%s" not valid: "%s" '
'Value must be in CIDR format.' % 'Value must be in CIDR format.' %
(CONF['local_ip'], str(e))) (CONF.local_ip, str(e)))
raise FailedValidation(message) raise FailedValidation(message)
hostname = CONF['undercloud_hostname'] hostname = CONF['undercloud_hostname']
if hostname is not None and '.' not in hostname: if hostname is not None and '.' not in hostname:
@ -224,8 +224,8 @@ def _validate_value_formats():
raise FailedValidation(message) raise FailedValidation(message)
def _validate_in_cidr(): def _validate_in_cidr(subnet_props, subnet_name):
cidr = netaddr.IPNetwork(CONF['network_cidr']) cidr = netaddr.IPNetwork(subnet_props.cidr)
def validate_addr_in_cidr(addr, pretty_name=None, require_ip=True): def validate_addr_in_cidr(addr, pretty_name=None, require_ip=True):
try: try:
@ -238,79 +238,67 @@ def _validate_in_cidr():
message = 'Invalid IP address: %s' % addr message = 'Invalid IP address: %s' % addr
raise FailedValidation(message) raise FailedValidation(message)
just_local_ip = CONF['local_ip'].split('/')[0] if subnet_name == CONF.local_subnet:
# What is this about? They have invalidated the configuration validate_addr_in_cidr(str(netaddr.IPNetwork(CONF.local_ip).ip),
# specification here.. - imain 'local_ip')
# validate_addr_in_cidr(subnet_props.gateway, 'gateway')
# undercloud.conf uses inspection_iprange, the configuration wizard
# tool passes the values separately.
# if 'inspection_iprange' in CONF:
# inspection_iprange = CONF['inspection_iprange'].split(',')
# CONF['inspection_start'] = inspection_iprange[0]
# CONF['inspection_end'] = inspection_iprange[1]
validate_addr_in_cidr(just_local_ip, 'local_ip')
validate_addr_in_cidr(CONF['network_gateway'], 'network_gateway')
# NOTE(bnemec): The ui needs to be externally accessible, which means in # NOTE(bnemec): The ui needs to be externally accessible, which means in
# many cases we can't have the public vip on the provisioning network. # many cases we can't have the public vip on the provisioning network.
# In that case users are on their own to ensure they've picked valid # In that case users are on their own to ensure they've picked valid
# values for the VIP hosts. # values for the VIP hosts.
if ((CONF['undercloud_service_certificate'] or if ((CONF.undercloud_service_certificate or
CONF['generate_service_certificate']) and CONF.generate_service_certificate) and
not CONF['enable_ui']): not CONF.enable_ui):
validate_addr_in_cidr(CONF['undercloud_public_host'], validate_addr_in_cidr(CONF['undercloud_public_host'],
'undercloud_public_host', 'undercloud_public_host',
require_ip=False) require_ip=False)
validate_addr_in_cidr(CONF['undercloud_admin_host'], validate_addr_in_cidr(CONF['undercloud_admin_host'],
'undercloud_admin_host', 'undercloud_admin_host',
require_ip=False) require_ip=False)
validate_addr_in_cidr(CONF['dhcp_start'], 'dhcp_start') validate_addr_in_cidr(subnet_props.dhcp_start, 'dhcp_start')
validate_addr_in_cidr(CONF['dhcp_end'], 'dhcp_end') validate_addr_in_cidr(subnet_props.dhcp_end, 'dhcp_end')
# validate_addr_in_cidr(CONF, 'inspection_start', 'Inspection range start')
# validate_addr_in_cidr(CONF, 'inspection_end', 'Inspection range end')
def _validate_dhcp_range(): def _validate_dhcp_range(subnet_props):
dhcp_start = netaddr.IPAddress(CONF['dhcp_start']) start = netaddr.IPAddress(subnet_props.dhcp_start)
dhcp_end = netaddr.IPAddress(CONF['dhcp_end']) end = netaddr.IPAddress(subnet_props.dhcp_end)
if dhcp_start >= dhcp_end: if start >= end:
message = ('Invalid dhcp range specified, dhcp_start "%s" does ' message = ('Invalid dhcp range specified, dhcp_start "%s" does '
'not come before dhcp_end "%s"' % 'not come before dhcp_end "%s"' % (start, end))
(dhcp_start, dhcp_end))
raise FailedValidation(message) raise FailedValidation(message)
def _validate_inspection_range(): def _validate_inspection_range(subnet_props):
inspection_start = netaddr.IPAddress(CONF['inspection_start']) start = netaddr.IPAddress(subnet_props.inspection_iprange.split(',')[0])
inspection_end = netaddr.IPAddress(CONF['inspection_end']) end = netaddr.IPAddress(subnet_props.inspection_iprange.split(',')[1])
if inspection_start >= inspection_end: if start >= end:
message = ('Invalid inspection range specified, inspection_start ' message = ('Invalid inspection range specified, inspection_iprange '
'"%s" does not come before inspection_end "%s"' % '"%s" does not come before "%s"' % (start, end))
(inspection_start, inspection_end))
raise FailedValidation(message) raise FailedValidation(message)
def _validate_no_overlap(): def _validate_no_overlap(subnet_props):
"""Validate the provisioning and inspection ip ranges do not overlap""" """Validate the provisioning and inspection ip ranges do not overlap"""
dhcp_set = netaddr.IPSet(netaddr.IPRange(CONF['dhcp_start'], dhcp_set = netaddr.IPSet(netaddr.IPRange(subnet_props.dhcp_start,
CONF['dhcp_end'])) subnet_props.dhcp_end))
inspection_set = netaddr.IPSet(netaddr.IPRange(CONF['inspection_start'], inspection_set = netaddr.IPSet(netaddr.IPRange(
CONF['inspection_end'])) subnet_props.inspection_iprange.split(',')[0],
# If there is any intersection of the two sets then we have a problem subnet_props.inspection_iprange.split(',')[1]))
if dhcp_set & inspection_set: if dhcp_set.intersection(inspection_set):
message = ('Inspection DHCP range "%s-%s" overlaps provisioning ' message = ('Inspection DHCP range "%s-%s" overlaps provisioning '
'DHCP range "%s-%s".' % 'DHCP range "%s-%s".' %
(CONF['inspection_start'], CONF['inspection_end'], (subnet_props.inspection_iprange.split(',')[0],
CONF['dhcp_start'], CONF['dhcp_end'])) subnet_props.inspection_iprange.split(',')[1],
subnet_props.dhcp_start, subnet_props.dhcp_end))
raise FailedValidation(message) raise FailedValidation(message)
def _validate_interface_exists(): def _validate_interface_exists():
"""Validate the provided local interface exists""" """Validate the provided local interface exists"""
local_interface = CONF['local_interface'] if (not CONF.net_config_override
net_override = CONF['net_config_override'] and CONF.local_interface not in netifaces.interfaces()):
if not net_override and local_interface not in netifaces.interfaces():
message = ('Invalid local_interface specified. %s is not available.' % message = ('Invalid local_interface specified. %s is not available.' %
local_interface) CONF.local_interface)
raise FailedValidation(message) raise FailedValidation(message)
@ -384,10 +372,12 @@ def check():
_validate_passwords_file() _validate_passwords_file()
# Networking validations # Networking validations
_validate_value_formats() _validate_value_formats()
_validate_in_cidr() for subnet in CONF.subnets:
_validate_dhcp_range() s = CONF.get(subnet)
# _validate_inspection_range() _validate_in_cidr(s, subnet)
# _validate_no_overlap() _validate_dhcp_range(s)
_validate_inspection_range(s)
_validate_no_overlap(s)
_validate_ips() _validate_ips()
_validate_interface_exists() _validate_interface_exists()
_validate_no_ip_change() _validate_no_ip_change()