# Copyright 2015 Red Hat, Inc. # # Licensed under the Apache License, Version 2.0 (the "License"); you may # not use this file except in compliance with the License. You may obtain # a copy of the License at # # http://www.apache.org/licenses/LICENSE-2.0 # # Unless required by applicable law or agreed to in writing, software # distributed under the License is distributed on an "AS IS" BASIS, WITHOUT # WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the # License for the specific language governing permissions and limitations # under the License. # """Plugin action implementation""" import copy import jinja2 import json import logging import netaddr import os import yaml from cryptography import x509 from cryptography.hazmat.backends import default_backend from cryptography.hazmat.primitives import serialization from oslo_config import cfg from tripleo_common.image import kolla_builder from tripleoclient import constants from tripleoclient import exceptions from tripleoclient import utils from tripleoclient.v1 import undercloud_preflight NETCONFIG_TAGS_EXAMPLE = """ "network_config": [ { "type": "ovs_bridge", "name": "br-ctlplane", "ovs_extra": [ "br-set-external-id br-ctlplane bridge-id br-ctlplane" ], "members": [ { "type": "interface", "name": "{{LOCAL_INTERFACE}}", "primary": "true", "mtu": {{LOCAL_MTU}}, "dns_servers": {{UNDERCLOUD_NAMESERVERS}} } ], "addresses": [ { "ip_netmask": "{{PUBLIC_INTERFACE_IP}}" } ], "routes": {{SUBNETS_STATIC_ROUTES}}, "mtu": {{LOCAL_MTU}} } ] """ # Provides mappings for some of the instack_env tags to undercloud heat # params or undercloud.conf opts known here (as a fallback), needed to maintain # feature parity with instack net config override templates. # TODO(bogdando): all of the needed mappings should be wired-in, eventually INSTACK_NETCONF_MAPPING = { 'LOCAL_INTERFACE': 'local_interface', 'LOCAL_IP': 'local_ip', 'LOCAL_MTU': 'UndercloudLocalMtu', 'PUBLIC_INTERFACE_IP': 'undercloud_public_host', # can't be 'CloudName' 'UNDERCLOUD_NAMESERVERS': 'undercloud_nameservers', 'SUBNETS_STATIC_ROUTES': 'ControlPlaneStaticRoutes', } PARAMETER_MAPPING = { 'inspection_interface': 'IronicInspectorInterface', 'undercloud_debug': 'Debug', 'ipxe_enabled': 'IronicInspectorIPXEEnabled', 'certificate_generation_ca': 'CertmongerCA', 'undercloud_public_host': 'CloudName', 'scheduler_max_attempts': 'NovaSchedulerMaxAttempts', 'local_mtu': 'UndercloudLocalMtu', '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', "/usr/share/openstack-tripleo-heat-templates/") USER_HOME = os.environ.get('HOME', '') TELEMETRY_DOCKER_ENV_YAML = [ 'environments/services/undercloud-gnocchi.yaml', 'environments/services/undercloud-aodh.yaml', 'environments/services/undercloud-panko.yaml', 'environments/services/undercloud-ceilometer.yaml'] # Control plane network name SUBNETS_DEFAULT = ['ctlplane-subnet'] class Paths(object): @property def CONF_PATH(self): return os.path.expanduser('~/undercloud.conf') CONF = cfg.CONF 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 # sample config by running "tox -e genconfig" in the project root. ci_defaults = kolla_builder.container_images_prepare_defaults() _opts = [ cfg.StrOpt('output_dir', default=constants.UNDERCLOUD_OUTPUT_DIR, help=('Directory to output state, processed heat templates, ' 'ansible deployment files.'), ), cfg.BoolOpt('cleanup', default=False, help=('Cleanup temporary files'), ), cfg.StrOpt('deployment_user', help=('User used to run openstack undercloud install command ' 'which will be used to add the user to the docker group, ' 'required to upload containers'), ), cfg.StrOpt('undercloud_hostname', help=('Fully qualified hostname (including domain) to set on ' 'the Undercloud. If left unset, the ' 'current hostname will be used, but the user is ' 'responsible for configuring all system hostname ' 'settings appropriately. If set, the undercloud install ' 'will configure all system hostname settings.'), ), cfg.StrOpt('local_ip', default='192.168.24.1/24', help=('IP information for the interface on the Undercloud ' 'that will be handling the PXE boots and DHCP for ' 'Overcloud instances. The IP portion of the value will ' 'be assigned to the network interface defined by ' 'local_interface, with the netmask defined by the ' 'prefix portion of the value.') ), cfg.StrOpt('undercloud_public_host', deprecated_name='undercloud_public_vip', default='192.168.24.2', help=('Virtual IP or DNS address to use for the public ' 'endpoints of Undercloud services. Only used with SSL.') ), cfg.StrOpt('undercloud_admin_host', deprecated_name='undercloud_admin_vip', default='192.168.24.3', help=('Virtual IP or DNS address to use for the admin ' 'endpoints of Undercloud services. Only used with SSL.') ), cfg.ListOpt('undercloud_nameservers', default=[], help=('DNS nameserver(s) to use for the undercloud node.'), ), cfg.ListOpt('undercloud_ntp_servers', default=[], help=('List of ntp servers to use.')), cfg.StrOpt('overcloud_domain_name', default='localdomain', help=('DNS domain name to use when deploying the overcloud. ' 'The overcloud parameter "CloudDomain" must be set to a ' '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', default='', help=('Certificate file to use for OpenStack service SSL ' 'connections. Setting this enables SSL for the ' 'OpenStack API endpoints, leaving it unset disables SSL.') ), cfg.BoolOpt('generate_service_certificate', default=True, help=('When set to True, an SSL certificate will be generated ' 'as part of the undercloud install and this certificate ' 'will be used in place of the value for ' 'undercloud_service_certificate. The resulting ' 'certificate will be written to ' '/etc/pki/tls/certs/undercloud-[undercloud_public_host].' 'pem. This certificate is signed by CA selected by the ' '"certificate_generation_ca" option.') ), cfg.StrOpt('certificate_generation_ca', default='local', help=('The certmonger nickname of the CA from which the ' 'certificate will be requested. This is used only if ' 'the generate_service_certificate option is set. ' 'Note that if the "local" CA is selected the ' 'certmonger\'s local CA certificate will be extracted to ' '/etc/pki/ca-trust/source/anchors/cm-local-ca.pem and ' 'subsequently added to the trust chain.') ), cfg.StrOpt('service_principal', default='', help=('The kerberos principal for the service that will use ' 'the certificate. This is only needed if your CA ' 'requires a kerberos principal. e.g. with FreeIPA.') ), cfg.StrOpt('local_interface', default='eth1', help=('Network interface on the Undercloud that will be ' 'handling the PXE boots and DHCP for Overcloud ' 'instances.') ), cfg.IntOpt('local_mtu', default=1500, help=('MTU to use for the local_interface.') ), cfg.StrOpt('hieradata_override', default='', help=('Path to hieradata override file. Relative paths get ' 'computed inside of $HOME. When it points to a heat ' 'env file, it is passed in t-h-t via "-e ", as is. ' 'When the file contains legacy instack data, ' 'it is wrapped with UndercloudExtraConfig and also ' 'passed in for t-h-t as a temp file created in ' 'output_dir. Note, instack hiera data may be ' 'not t-h-t compatible and will highly likely require a ' 'manual revision.') ), cfg.StrOpt('net_config_override', default='', help=('Path to network config override template. Relative ' 'paths get computed inside of the given heat templates ' 'directory. Must be in json format. ' 'Its content overrides anything in t-h-t ' 'UndercloudNetConfigOverride. The processed template ' 'is then passed in Heat via the top scope ' 'undercloud_parameters.yaml file created in ' 'output_dir and used to configure the networking ' 'via run-os-net-config. If you wish to disable ' 'you can set this location to an empty file.' 'Templated for instack j2 tags ' 'may be used, for example:\n%s ') % NETCONFIG_TAGS_EXAMPLE ), cfg.StrOpt('inspection_interface', default='br-ctlplane', deprecated_name='discovery_interface', help=('Network interface on which inspection dnsmasq will ' 'listen. If in doubt, use the default value.') ), cfg.BoolOpt('inspection_extras', default=True, help=('Whether to enable extra hardware collection during ' 'the inspection process. Requires python-hardware or ' 'python-hardware-detect package on the introspection ' 'image.')), cfg.BoolOpt('inspection_runbench', default=False, deprecated_name='discovery_runbench', help=('Whether to run benchmarks when inspecting nodes. ' 'Requires inspection_extras set to True.') ), cfg.BoolOpt('enable_node_discovery', default=False, help=('Makes ironic-inspector enroll any unknown node that ' 'PXE-boots introspection ramdisk in Ironic. By default, ' 'the "fake" driver is used for new nodes (it is ' 'automatically enabled when this option is set to True).' ' Set discovery_default_driver to override. ' 'Introspection rules can also be used to specify driver ' 'information for newly enrolled nodes.') ), cfg.StrOpt('discovery_default_driver', default='ipmi', help=('The default driver or hardware type to use for newly ' 'discovered nodes (requires enable_node_discovery set to ' 'True). It is automatically added to ' 'enabled_hardware_types.') ), cfg.BoolOpt('undercloud_debug', default=True, help=('Whether to enable the debug log level for Undercloud ' 'OpenStack services.') ), cfg.BoolOpt('undercloud_update_packages', default=False, help=('Whether to update packages during the Undercloud ' 'install. This is a no-op for containerized undercloud.') ), cfg.BoolOpt('enable_tempest', default=True, help=('Whether to install Tempest in the Undercloud.' 'This is a no-op for containerized undercloud.') ), cfg.BoolOpt('enable_telemetry', default=False, help=('Whether to install Telemetry services ' '(ceilometer, gnocchi, aodh, panko ) in the Undercloud.') ), cfg.BoolOpt('enable_ui', default=True, help=('Whether to install the TripleO UI.') ), cfg.BoolOpt('enable_validations', default=True, help=('Whether to install requirements to run the TripleO ' 'validations.') ), cfg.BoolOpt('enable_cinder', default=False, help=('Whether to install the Volume service. It is not ' 'currently used in the undercloud.')), cfg.BoolOpt('enable_novajoin', default=False, help=('Whether to install novajoin metadata service in ' 'the Undercloud.') ), cfg.BoolOpt('enable_container_images_build', default=True, help=('Whether to enable docker container images to be build ' 'on the undercloud.') ), cfg.StrOpt('ipa_otp', default='', help=('One Time Password to register Undercloud node with ' 'an IPA server. ' 'Required when enable_novajoin = True.') ), cfg.BoolOpt('ipxe_enabled', default=True, help=('Whether to use iPXE for deploy and inspection.'), deprecated_name='ipxe_deploy', ), cfg.IntOpt('scheduler_max_attempts', default=30, min=1, help=('Maximum number of attempts the scheduler will make ' 'when deploying the instance. You should keep it ' 'greater or equal to the number of bare metal nodes ' 'you expect to deploy at once to work around ' 'potential race condition when scheduling.')), cfg.BoolOpt('clean_nodes', default=False, help=('Whether to clean overcloud nodes (wipe the hard drive) ' 'between deployments and after the introspection.')), cfg.ListOpt('enabled_hardware_types', default=['ipmi', 'redfish', 'ilo', 'idrac'], help=('List of enabled bare metal hardware types (next ' 'generation drivers).')), cfg.StrOpt('docker_registry_mirror', default='', help=('An optional docker \'registry-mirror\' that will be' 'configured in /etc/docker/daemon.json.') ), cfg.ListOpt('docker_insecure_registries', default=[], help=('Used to add custom insecure registries in ' '/etc/sysconfig/docker.') ), cfg.StrOpt('templates', default='', help=('heat templates file to override.') ), cfg.StrOpt('roles_file', default=None, help=('Roles file to override for heat. ' 'The file path is related to the templates path') ), cfg.BoolOpt('heat_native', default=True, help=('Use native heat templates.')), cfg.StrOpt('heat_container_image', default='', help=('URL for the heat container image to use.') ), cfg.StrOpt('container_images_file', default='', help=('Heat environment file with parameters for all required ' 'container images. Or alternatively, parameter ' '"ContainerImagePrepare" to drive the required image ' 'preparation.')), cfg.BoolOpt('enable_ironic', default=True, help=('Whether to enable the ironic service.')), cfg.BoolOpt('enable_ironic_inspector', default=True, help=('Whether to enable the ironic inspector service.')), cfg.BoolOpt('enable_mistral', default=True, help=('Whether to enable the mistral service.')), cfg.BoolOpt('enable_zaqar', default=True, help=('Whether to enable the zaqar service.')), cfg.ListOpt('custom_env_files', default=[], help=('List of any custom environment yaml files to use')), cfg.BoolOpt('enable_routed_networks', default=False, help=('Enable support for routed ctlplane networks.')), cfg.BoolOpt('enable_swift_encryption', default=False, help=('Whether to enable Swift encryption at-rest or not.')), ] # 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) 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") def list_opts(): return [(None, copy.deepcopy(_opts)), (SUBNETS_DEFAULT[0], copy.deepcopy(_subnets_opts))] def _load_config(): conf_params = [] if os.path.isfile(PATHS.CONF_PATH): conf_params += ['--config-file', PATHS.CONF_PATH] else: LOG.warning('%s does not exist. Using defaults.' % PATHS.CONF_PATH) CONF(conf_params) def _get_jinja_env_source(f): path, filename = os.path.split(f) env = jinja2.Environment(loader=jinja2.FileSystemLoader(path)) src = env.loader.get_source(env, filename)[0] return (env, src) def _get_unknown_instack_tags(env, src): found_tags = set(jinja2.meta.find_undeclared_variables(env.parse(src))) known_tags = set(INSTACK_NETCONF_MAPPING.keys()) if found_tags <= known_tags: return (', ').join(found_tags - known_tags) else: return None def _process_drivers_and_hardware_types(conf, env): """Populate the environment with ironic driver information.""" # Ensure correct rendering of the list and uniqueness of the items enabled_hardware_types = set(conf.enabled_hardware_types) if conf.enable_node_discovery: if conf.discovery_default_driver not in enabled_hardware_types: enabled_hardware_types.add(conf.discovery_default_driver) env['IronicInspectorEnableNodeDiscovery'] = True env['IronicInspectorDiscoveryDefaultDriver'] = ( conf.discovery_default_driver) # In most cases power and management interfaces are called the same, so we # use one variable for them. mgmt_interfaces = {'fake', 'ipmitool'} # TODO(dtantsur): can we somehow avoid hardcoding hardware types here? for hw_type in ('redfish', 'idrac', 'ilo', 'irmc', 'staging-ovirt'): if hw_type in enabled_hardware_types: mgmt_interfaces.add(hw_type) for (hw_type, iface) in [('cisco-ucs-managed', 'ucsm'), ('cisco-ucs-standalone', 'cimc')]: if hw_type in enabled_hardware_types: mgmt_interfaces.add(iface) # Two hardware types use non-default boot interfaces. boot_interfaces = {'pxe'} for hw_type in ('ilo', 'irmc'): if hw_type in enabled_hardware_types: boot_interfaces.add('%s-pxe' % hw_type) raid_interfaces = {'no-raid'} if 'idrac' in enabled_hardware_types: raid_interfaces.add('idrac') vendor_interfaces = {'no-vendor'} for (hw_type, iface) in [('ipmi', 'ipmitool'), ('idrac', 'idrac')]: if hw_type in enabled_hardware_types: vendor_interfaces.add(iface) env['IronicEnabledHardwareTypes'] = sorted(enabled_hardware_types) env['IronicEnabledBootInterfaces'] = sorted(boot_interfaces) env['IronicEnabledManagementInterfaces'] = sorted(mgmt_interfaces) env['IronicEnabledRaidInterfaces'] = sorted(raid_interfaces) env['IronicEnabledVendorInterfaces'] = sorted(vendor_interfaces) # The snmp hardware type uses fake management and snmp power if 'snmp' in enabled_hardware_types: mgmt_interfaces.add('snmp') env['IronicEnabledPowerInterfaces'] = sorted(mgmt_interfaces) def _process_ipa_args(conf, env): """Populate the environment with IPA kernal args .""" inspection_kernel_args = [] if conf.undercloud_debug: inspection_kernel_args.append('ipa-debug=1') if conf.inspection_runbench: inspection_kernel_args.append('ipa-inspection-benchmarks=cpu,mem,disk') if conf.inspection_extras: inspection_kernel_args.append('ipa-inspection-dhcp-all-interfaces=1') inspection_kernel_args.append('ipa-collect-lldp=1') env['IronicInspectorCollectors'] = ('default,extra-hardware,' 'numa-topology,logs') else: env['IronicInspectorCollectors'] = 'default,logs' 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 prepare_undercloud_deploy(upgrade=False, no_validations=False, verbose_level=1): """Prepare Undercloud deploy command based on undercloud.conf""" env_data = {} registry_overwrites = {} deploy_args = [] _load_config() _load_subnets_config_groups() # Set the undercloud home dir parameter so that stackrc is produced in # the users home directory. env_data['UndercloudHomeDir'] = USER_HOME for param_key, param_value in PARAMETER_MAPPING.items(): if param_key in CONF.keys(): 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 # yaml files for undercloud deploy command if CONF.get('undercloud_ntp_servers', None): env_data['NtpServer'] = CONF['undercloud_ntp_servers'][0] if CONF.get('enable_validations', False) and not no_validations: env_data['EnableValidations'] = CONF['enable_validations'] if CONF.get('overcloud_domain_name', None): env_data['NeutronDnsDomain'] = CONF['overcloud_domain_name'] deploy_args.append('--local-domain=%s' % CONF['overcloud_domain_name']) # FIXME need to add admin VIP as well env_data['DockerInsecureRegistryAddress'] = [ '%s:8787' % CONF['local_ip'].split('/')[0]] env_data['DockerInsecureRegistryAddress'].extend( CONF['docker_insecure_registries']) if CONF.get('docker_registry_mirror', None): env_data['DockerRegistryMirror'] = CONF['docker_registry_mirror'] if CONF.get('local_ip', None): deploy_args.append('--local-ip=%s' % CONF['local_ip']) if CONF.get('templates', None): tht_templates = CONF['templates'] deploy_args.append('--templates=%s' % tht_templates) else: tht_templates = THT_HOME deploy_args.append('--templates=%s' % THT_HOME) if CONF.get('roles_file', None): deploy_args.append('--roles-file=%s' % CONF['roles_file']) if upgrade: deploy_args += ['-e', os.path.join( tht_templates, "environments/lifecycle/undercloud-upgrade-prepare.yaml")] if CONF.get('heat_native', None): deploy_args.append('--heat-native') if CONF.get('heat_container_image'): deploy_args.append('--heat-container-image=%s' % CONF['heat_container_image']) _container_images_config(CONF, deploy_args, env_data) if env_data['MasqueradeNetworks']: deploy_args += ['-e', os.path.join( tht_templates, "environments/services/masquerade-networks.yaml")] if CONF.get('enable_ironic'): deploy_args += ['-e', os.path.join( tht_templates, "environments/services/ironic.yaml")] # ironic-inspector can only work if ironic is enabled if CONF.get('enable_ironic_inspector'): deploy_args += ['-e', os.path.join( tht_templates, "environments/services/ironic-inspector.yaml")] _process_drivers_and_hardware_types(CONF, env_data) _process_ipa_args(CONF, env_data) if CONF.get('enable_mistral'): deploy_args += ['-e', os.path.join( tht_templates, "environments/services/mistral.yaml")] if CONF.get('enable_novajoin'): deploy_args += ['-e', os.path.join( tht_templates, "environments/services/novajoin.yaml")] env_data['NovajoinIpaOtp'] = CONF['ipa_otp'] if CONF.get('enable_zaqar'): deploy_args += ['-e', os.path.join( tht_templates, "environments/services/zaqar.yaml")] if CONF.get('enable_telemetry'): for env_file in TELEMETRY_DOCKER_ENV_YAML: deploy_args += ['-e', os.path.join(tht_templates, env_file)] if CONF.get('enable_ui'): deploy_args += ['-e', os.path.join( tht_templates, "environments/services/tripleo-ui.yaml")] if CONF.get('enable_cinder'): deploy_args += ['-e', os.path.join( tht_templates, "environments/services/undercloud-cinder.yaml")] if CONF.get('enable_tempest'): deploy_args += ['-e', os.path.join( tht_templates, "environments/services/tempest.yaml")] if CONF.get('enable_swift_encryption'): deploy_args += [ '-e', os.path.join(tht_templates, "environments/services/barbican.yaml"), '-e', os.path.join( tht_templates, "environments/barbican-backend-simple-crypto.yaml") ] env_data['BarbicanSimpleCryptoGlobalDefault'] = True env_data['SwiftEncryptionEnabled'] = True if CONF.get('generate_service_certificate'): deploy_args += ['-e', os.path.join( tht_templates, "environments/public-tls-undercloud.yaml")] elif CONF.get('undercloud_service_certificate'): enable_tls_yaml_path = os.path.join(tht_templates, "environments/ssl/enable-tls.yaml") env_data.update( _get_public_tls_parameters( CONF.get('undercloud_service_certificate'))) registry_overwrites.update( _get_public_tls_resource_registry_overwrites(enable_tls_yaml_path)) deploy_args += [ '-e', os.path.join(tht_templates, 'environments/services/' 'undercloud-haproxy.yaml'), '-e', os.path.join(tht_templates, 'environments/services/' 'undercloud-keepalived.yaml')] else: deploy_args += ['-e', os.path.join( tht_templates, "environments/no-tls-endpoints-public-ip.yaml")] if (CONF.get('generate_service_certificate') or CONF.get('undercloud_service_certificate')): endpoint_environment = _get_tls_endpoint_environment( CONF.get('undercloud_public_host'), tht_templates) try: public_host = CONF.get('undercloud_public_host') netaddr.IPAddress(public_host) deploy_args += ['--public-virtual-ip', public_host] admin_host = CONF.get('undercloud_admin_host') netaddr.IPAddress(admin_host) deploy_args += ['--control-virtual-ip', admin_host] except netaddr.core.AddrFormatError: # TODO(jaosorior): We could do a reverse lookup for the hostnames # if the *_host variables are DNS names and not IPs. pass deploy_args += [ '-e', endpoint_environment, '-e', os.path.join( tht_templates, 'environments/use-dns-for-vips.yaml'), '-e', os.path.join( tht_templates, 'environments/services/undercloud-haproxy.yaml'), '-e', os.path.join( tht_templates, 'environments/services/undercloud-keepalived.yaml')] u = CONF.get('deployment_user') or utils.get_deployment_user() env_data['DeploymentUser'] = u deploy_args += [ "-e", os.path.join(tht_templates, "environments/docker.yaml"), "-e", os.path.join(tht_templates, "environments/undercloud.yaml")] params_file = os.path.abspath(os.path.join(CONF['output_dir'], 'undercloud_parameters.yaml')) deploy_args += ['--output-dir=%s' % CONF['output_dir']] if not os.path.isdir(CONF['output_dir']): os.mkdir(CONF['output_dir']) if CONF.get('cleanup'): deploy_args.append('--cleanup') if CONF.get('custom_env_files'): for custom_file in CONF['custom_env_files']: deploy_args += ['-e', custom_file] if CONF.get('net_config_override', None): data_file = CONF['net_config_override'] if os.path.abspath(data_file) != data_file: data_file = os.path.join(tht_templates, data_file) if not os.path.exists(data_file): msg = "Could not find net_config_override file '%s'" % data_file LOG.error(msg) raise RuntimeError(msg) # NOTE(bogdando): Process templated net config override data: # * get a list of used instack_env j2 tags (j2 vars, like {{foo}}), # * fetch values for the tags from the known mappins, # * raise, if there is unmatched tags left # * render the template into a JSON dict net_config_env, template_source = _get_jinja_env_source(data_file) unknown_tags = _get_unknown_instack_tags(net_config_env, template_source) if unknown_tags: msg = ('Can not render net_config_override file %s contains ' 'unknown instack_env j2 tags: %s' % ( data_file, unknown_tags)) LOG.error(msg) raise exceptions.DeploymentError(msg) # Create rendering context from the known to be present mappings for # identified instack_env tags to generated in env_data undercloud heat # params. Fall back to config opts, when env_data misses a param. context = {} for tag in INSTACK_NETCONF_MAPPING.keys(): mapped_value = INSTACK_NETCONF_MAPPING[tag] if mapped_value in env_data.keys() or mapped_value in CONF.keys(): context[tag] = env_data.get( mapped_value, None) or CONF[mapped_value] # this returns a unicode string, convert it in into json net_config_str = net_config_env.get_template( os.path.split(data_file)[-1]).render(context).replace( "'", '"').replace('"', '"') try: net_config_json = json.loads(net_config_str) except ValueError: net_config_json = json.loads("{%s}" % net_config_str) if 'network_config' not in net_config_json: msg = ('Unsupported data format in net_config_override ' 'file %s: %s' % (data_file, net_config_str)) LOG(msg) raise exceptions.DeploymentError(msg) env_data['UndercloudNetConfigOverride'] = net_config_json deploy_args += ['-e', params_file] utils.write_env_file(env_data, params_file, registry_overwrites) if CONF.get('hieradata_override', None): data_file = CONF['hieradata_override'] if os.path.abspath(data_file) != data_file: data_file = os.path.join(USER_HOME, data_file) if not os.path.exists(data_file): msg = "Could not find hieradata_override file '%s'" % data_file LOG.error(msg) raise RuntimeError(msg) deploy_args += ['--hieradata-override=%s' % data_file] if CONF.get('enable_validations') and not no_validations: undercloud_preflight.check() if verbose_level > 1: deploy_args.append('--debug') LOG_FILE = os.path.join(os.getcwd() + '/install-undercloud.log') deploy_args.append('--log-file=' + LOG_FILE) cmd = ["sudo", "openstack", "tripleo", "deploy", "--standalone"] cmd += deploy_args[:] return cmd def _get_tls_endpoint_environment(public_host, tht_templates): try: netaddr.IPAddress(public_host) return os.path.join(tht_templates, "environments/ssl/tls-endpoints-public-ip.yaml") except netaddr.core.AddrFormatError: return os.path.join(tht_templates, "environments/ssl/tls-endpoints-public-dns.yaml") def _get_public_tls_parameters(service_certificate_path): with open(service_certificate_path, "rb") as pem_file: pem_data = pem_file.read() cert = x509.load_pem_x509_certificate(pem_data, default_backend()) private_key = serialization.load_pem_private_key( pem_data, password=None, backend=default_backend()) key_pem = private_key.private_bytes( encoding=serialization.Encoding.PEM, format=serialization.PrivateFormat.TraditionalOpenSSL, encryption_algorithm=serialization.NoEncryption()) cert_pem = cert.public_bytes(serialization.Encoding.PEM) return { 'SSLCertificate': cert_pem, 'SSLKey': key_pem } def _get_public_tls_resource_registry_overwrites(enable_tls_yaml_path): with open(enable_tls_yaml_path, 'rb') as enable_tls_file: enable_tls_dict = yaml.load(enable_tls_file.read()) try: return enable_tls_dict['resource_registry'] except KeyError: raise RuntimeError('%s is malformed and is missing the resource ' 'registry.' % enable_tls_yaml_path) def _container_images_config(conf, deploy_args, env_data): if conf.container_images_file: deploy_args += ['-e', conf.container_images_file] else: # no images file was provided. Set a default ContainerImagePrepare # parameter to trigger the preparation of the required container list cip = kolla_builder.CONTAINER_IMAGE_PREPARE_PARAM env_data['ContainerImagePrepare'] = cip