from __future__ import absolute_import import collections import os import charms_openstack.charm import charms_openstack.adapters from charms_openstack.adapters import ( RabbitMQRelationAdapter, DatabaseRelationAdapter, OpenStackRelationAdapters, ) from charmhelpers.contrib.openstack.utils import os_release from charmhelpers.contrib.openstack import templating from charmhelpers.core import host import charm.openstack.ironic.controller_utils as controller_utils import charms_openstack.adapters as adapters import charmhelpers.contrib.network.ip as ch_ip import charmhelpers.contrib.openstack.utils as ch_utils import charms.leadership as leadership import charms.reactive as reactive PACKAGES = [ 'ironic-conductor', 'python3-dracclient', 'python3-keystoneauth1', 'python3-keystoneclient', 'python3-glanceclient', 'python3-sushy', 'python3-swiftclient', 'python3-mysqldb', 'python3-ironicclient', 'shellinabox', 'openssl', 'socat', 'open-iscsi', 'qemu-utils', 'ipmitool'] IRONIC_DIR = "/etc/ironic/" IRONIC_CONF = os.path.join(IRONIC_DIR, "ironic.conf") IRONIC_CONF_D = os.path.join(IRONIC_DIR, "conf.d") IRONIC_CONF_HW_ENABLEMENT = os.path.join(IRONIC_CONF_D, "90-hardware-enablement.conf") IRONIC_DEFAULT = "/etc/default/ironic-conductor" ROOTWRAP_CONF = os.path.join(IRONIC_DIR, "rootwrap.conf") FILTERS_DIR = os.path.join(IRONIC_DIR, "rootwrap.d") IRONIC_LIB_FILTERS = os.path.join( FILTERS_DIR, "ironic-lib.filters") IRONIC_UTILS_FILTERS = os.path.join( FILTERS_DIR, "ironic-utils.filters") TFTP_CONF = "/etc/default/tftpd-hpa" HTTP_SERVER_CONF = "/etc/nginx/nginx.conf" VALID_NETWORK_INTERFACES = ["neutron", "flat", "noop"] VALID_DEPLOY_INTERFACES = ["direct", "iscsi"] DEFAULT_DEPLOY_IFACE = "flat" DEFAULT_NET_IFACE = "direct" # The IPMI HW type requires only ipmitool to function. This HW type # remains pretty much unchanged across OpenStack releases and *should* # work _NOOP_INTERFACES = { 'enabled_bios_interfaces': 'no-bios', 'enabled_management_interfaces': 'noop', 'enabled_inspect_interfaces': 'no-inspect', 'enabled_console_interfaces': 'no-console', 'enabled_raid_interfaces': 'no-raid', 'enabled_vendor_interfaces': 'no-vendor', } _IPMI_HARDWARE_TYPE = { 'needed_packages': ['ipmitool', 'shellinabox', 'socat'], 'config_options': { 'enabled_hardware_types': ['ipmi', 'intel-ipmi'], 'enabled_management_interfaces': [ 'ipmitool', 'intel-ipmitool'], 'enabled_inspect_interfaces': [], 'enabled_power_interfaces': ['ipmitool'], 'enabled_console_interfaces': [ 'ipmitool-socat', 'ipmitool-shellinabox'], 'enabled_raid_interfaces': [], 'enabled_vendor_interfaces': ['ipmitool'], 'enabled_boot_interfaces': ['pxe'], 'enabled_bios_interfaces': [] } } _HW_TYPES_MAP = collections.OrderedDict([ ('train', { 'ipmi': _IPMI_HARDWARE_TYPE, 'redfish': { 'needed_packages': ['python3-sushy'], 'config_options': { 'enabled_hardware_types': ['redfish'], 'enabled_management_interfaces': ['redfish'], 'enabled_inspect_interfaces': ['redfish'], 'enabled_power_interfaces': ['redfish'], 'enabled_console_interfaces': [], 'enabled_raid_interfaces': [], 'enabled_vendor_interfaces': [], 'enabled_boot_interfaces': ['pxe'], 'enabled_bios_interfaces': [] } }, 'idrac': { 'needed_packages': ['python-dracclient', 'python3-sushy'], 'config_options': { 'enabled_hardware_types': ['idrac'], 'enabled_management_interfaces': ['idrac-redfish'], 'enabled_inspect_interfaces': ['idrac-redfish'], 'enabled_power_interfaces': ['idrac-redfish'], 'enabled_console_interfaces': [], 'enabled_raid_interfaces': ['idrac-wsman'], 'enabled_vendor_interfaces': ['idrac-wsman'], 'enabled_boot_interfaces': ['pxe'], 'enabled_bios_interfaces': [] } } }), ('ussuri', { 'ipmi': _IPMI_HARDWARE_TYPE, 'redfish': { 'needed_packages': ['python3-sushy'], 'config_options': { 'enabled_hardware_types': ['redfish'], 'enabled_management_interfaces': ['redfish'], 'enabled_inspect_interfaces': ['redfish'], 'enabled_power_interfaces': ['redfish'], 'enabled_console_interfaces': [], 'enabled_raid_interfaces': [], 'enabled_vendor_interfaces': [], 'enabled_boot_interfaces': ['pxe', 'redfish-virtual-media'], 'enabled_bios_interfaces': [], } }, 'idrac': { 'needed_packages': ['python-dracclient', 'python3-sushy'], 'config_options': { 'enabled_hardware_types': ['idrac'], 'enabled_management_interfaces': ['idrac-redfish'], 'enabled_inspect_interfaces': ['idrac-redfish'], 'enabled_power_interfaces': ['idrac-redfish'], 'enabled_console_interfaces': [], 'enabled_raid_interfaces': ['idrac-wsman'], 'enabled_vendor_interfaces': ['idrac-wsman'], 'enabled_boot_interfaces': ['pxe'], 'enabled_bios_interfaces': ['idrac-wsman'] } } }) ]) OPENSTACK_RELEASE_KEY = 'ironic-charm.openstack-release-version' # select the default release function charms_openstack.charm.use_defaults('charm.default-select-release') @adapters.config_property def deployment_interface_ip(args): return ch_ip.get_relation_ip("deployment") @adapters.config_property def internal_interface_ip(args): return ch_ip.get_relation_ip("internal") @adapters.config_property def temp_url_secret(args): url_secret = leadership.leader_get("temp_url_secret") return url_secret class IronicAdapters(OpenStackRelationAdapters): relation_adapters = { 'amqp': RabbitMQRelationAdapter, 'shared_db': DatabaseRelationAdapter, } class IronicConductorCharm(charms_openstack.charm.OpenStackCharm): adapters_class = IronicAdapters abstract_class = False release = 'train' name = 'ironic' packages = PACKAGES python_version = 3 service_type = 'ironic' default_service = 'ironic-conductor' services = ['ironic-conductor', 'tftpd-hpa'] required_relations = [ 'shared-db', 'amqp', 'identity-credentials', 'ironic-api'] restart_map = { IRONIC_CONF: ['ironic-conductor', ], IRONIC_DEFAULT: ['ironic-conductor', ], IRONIC_CONF_HW_ENABLEMENT: ['ironic-conductor', ], IRONIC_UTILS_FILTERS: ['ironic-conductor', ], IRONIC_LIB_FILTERS: ['ironic-conductor', ], ROOTWRAP_CONF: ['ironic-conductor', ], } # Package for release version detection release_pkg = 'ironic-common' # Package codename map for ironic-common package_codenames = { 'ironic-common': collections.OrderedDict([ ('13', 'train'), ('15', 'ussuri'), ('16', 'victoria'), ]), } group = "ironic" mandatory_config = [ "enabled-network-interfaces", "enabled-deploy-interfaces", ] def __init__(self, **kw): super().__init__(**kw) self.pxe_config = controller_utils.get_pxe_config_class( self.config) self._setup_pxe_config(self.pxe_config) self._setup_power_adapter_config() self._configure_defaults() if "neutron" in self.enabled_network_interfaces: self.mandatory_config.extend([ "provisioning-network", "cleaning-network"]) def _configure_defaults(self): net_iface = self.config.get("default-network-interface", None) if not net_iface: self.config["default-network-interface"] = DEFAULT_NET_IFACE iface = self.config.get("default-deploy-interface", None) if not iface: self.config["default-deploy-interface"] = DEFAULT_DEPLOY_IFACE def _get_hw_type_map(self): release = os_release(self.release_pkg) supported = list(_HW_TYPES_MAP.keys()) latest = supported[-1] hw_type_map = _HW_TYPES_MAP.get( release, _HW_TYPES_MAP[latest]) return hw_type_map def _get_power_adapter_packages(self): pkgs = [] hw_type_map = self._get_hw_type_map() for hw_type in self.enabled_hw_types: needed_pkgs = hw_type_map.get( hw_type, {}).get("needed_packages", []) pkgs.extend(needed_pkgs) return list(set(pkgs)) def _get_hardware_types_config(self): hw_type_map = self._get_hw_type_map() configs = {} for hw_type in self.enabled_hw_types: details = hw_type_map.get(hw_type, None) if details is None: # Not a valid hardware type. No need to raise here, # we will let the operator know when we validate the # config in custom_assess_status_check() continue driver_cfg = details['config_options'] for cfg_opt in driver_cfg.items(): if not configs.get(cfg_opt[0], None): configs[cfg_opt[0]] = cfg_opt[1] else: configs[cfg_opt[0]].extend(cfg_opt[1]) opt_list = list(set(configs[cfg_opt[0]])) opt_list.sort() configs[cfg_opt[0]] = opt_list if self.config.get('use-ipxe', None): configs["enabled_boot_interfaces"].append('ipxe') # append the noop interfaces at the end for noop in _NOOP_INTERFACES: if configs.get(noop, None) is not None: configs[noop].append(_NOOP_INTERFACES[noop]) for opt in configs: if len(configs[opt]) > 0: configs[opt] = ", ".join(configs[opt]) else: configs[opt] = "" return configs def _setup_power_adapter_config(self): pkgs = self._get_power_adapter_packages() config = self._get_hardware_types_config() self.packages.extend(pkgs) self.packages = list(set(self.packages)) self.config["hardware_type_cfg"] = config def _setup_pxe_config(self, cfg): self.packages.extend(cfg.determine_packages()) self.packages = list(set(self.packages)) self.config["tftpboot"] = cfg.TFTP_ROOT self.config["httpboot"] = cfg.HTTP_ROOT self.config["ironic_user"] = cfg.IRONIC_USER self.config["ironic_group"] = cfg.IRONIC_GROUP self.restart_map.update(cfg.get_restart_map()) self.permission_override_map.update( cfg.get_permission_override_map()) if cfg.HTTPD_SERVICE_NAME not in self.services: self.services.append( cfg.HTTPD_SERVICE_NAME) def install(self): self.configure_source() super().install() self._reconfigure_ironic_conductor() self.pxe_config._copy_resources() self.assess_status() def get_amqp_credentials(self): """Provide the default amqp username and vhost as a tuple. :returns (username, host): two strings to send to the amqp provider. """ return (self.config['rabbit-user'], self.config['rabbit-vhost']) def get_database_setup(self): return [ dict( database=self.config['database'], username=self.config['database-user'], ) ] def _validate_network_interfaces(self, interfaces): valid_interfaces = VALID_NETWORK_INTERFACES for interface in interfaces: if interface not in valid_interfaces: raise ValueError( 'Network interface %s is not valid. Valid ' 'interfaces are: %s' % ( interface, ", ".join(valid_interfaces))) def _validate_default_net_interface(self): net_iface = self.config["default-network-interface"] if net_iface not in self.enabled_network_interfaces: raise ValueError( "default-network-interface (%s) is not enabled " "in enabled-network-interfaces: %s" % ( net_iface, ", ".join(self.enabled_network_interfaces))) def _validate_deploy_interfaces(self, interfaces): valid_interfaces = VALID_DEPLOY_INTERFACES has_secret = reactive.is_flag_set("leadership.set.temp_url_secret") for interface in interfaces: if interface not in valid_interfaces: raise ValueError( 'Deploy interface %s is not valid. Valid ' 'interfaces are: %s' % ( interface, ", ".join(valid_interfaces))) if reactive.is_flag_set("config.complete"): if "direct" in interfaces and has_secret is False: raise ValueError( 'run set-temp-url-secret action on leader to ' 'enable direct deploy method') def _validate_default_deploy_interface(self): iface = self.config["default-deploy-interface"] if iface not in self.enabled_deploy_interfaces: raise ValueError( "default-deploy-interface (%s) is not enabled " "in enabled-deploy-interfaces: %s" % ( iface, ", ".join( self.enabled_deploy_interfaces))) def _validate_enabled_hw_type(self): hw_types = self._get_hw_type_map() unsupported = [] for hw_type in self.enabled_hw_types: if hw_types.get(hw_type, None) is None: unsupported.append(hw_type) if len(unsupported) > 0: raise ValueError( 'hardware type(s) %s not supported at ' 'this time' % ", ".join(unsupported)) @property def enabled_network_interfaces(self): network_interfaces = self.config.get( 'enabled-network-interfaces', "").replace(" ", "") return network_interfaces.split(",") @property def enabled_hw_types(self): hw_types = self.config.get( 'enabled-hw-types', "ipmi").replace(" ", "") return hw_types.split(",") @property def enabled_deploy_interfaces(self): network_interfaces = self.config.get( 'enabled-deploy-interfaces', "").replace(" ", "") return network_interfaces.split(",") def custom_assess_status_check(self): try: self._validate_network_interfaces(self.enabled_network_interfaces) except Exception as err: msg = ("invalid enabled-network-interfaces config, %s" % err) return ('blocked', msg) try: self._validate_default_net_interface() except Exception as err: msg = ("invalid default-network-interface config, %s" % err) return ('blocked', msg) try: self._validate_deploy_interfaces( self.enabled_deploy_interfaces) except Exception as err: msg = ("invalid enabled-deploy-interfaces config, %s" % err) return ('blocked', msg) try: self._validate_default_deploy_interface() except Exception as err: msg = ("invalid default-deploy-interface config, %s" % err) return ('blocked', msg) try: self._validate_enabled_hw_type() except Exception as err: msg = ("invalid enabled-hw-types config, %s" % err) return ('blocked', msg) return (None, None) def upgrade_charm(self): """Custom upgrade charm. Side effects: - Create /etc/ironic/conf.d/ directory. - Reconfigure ironic-conductor service to use the previously created directory. """ self._reconfigure_ironic_conductor() super().upgrade_charm() def _reconfigure_ironic_conductor(self): """Reconfigure ironic-conductor daemon. Set /etc/default/ironic-conductor to pass --config-dir in DAEMON_ARGS. """ if not os.path.isdir(IRONIC_CONF_D): host.mkdir(IRONIC_CONF_D) # reconfigure ironic-conductor to run it with --conf-dir release = ch_utils.os_release('ironic-common') configs = templating.OSConfigRenderer(templates_dir='templates/', openstack_release=release) configs.register(config_file=IRONIC_DEFAULT, contexts=[]) configs.write_all()