diff options
author | Hironori Shiina <shiina.hironori@jp.fujitsu.com> | 2017-05-31 13:13:52 +0900 |
---|---|---|
committer | Hironori Shiina <shiina.hironori@jp.fujitsu.com> | 2017-08-10 22:21:55 +0900 |
commit | 70530f9a668af6d59a3e72c230592a4b0dc2a1b8 (patch) | |
tree | a449914cde31108105d406e7fd930137ee5aa12c /ironic/drivers/modules | |
parent | cffe41c238555bef5d2f9836a4145392754f17be (diff) | |
download | ironic-70530f9a668af6d59a3e72c230592a4b0dc2a1b8.tar.gz |
iRMC: Support volume boot for iRMC virtual media boot interface
This patch enables iRMC virtual media boot interface to configure
remote boot. When a node is set up for boot-from-volume, this
interface registers volume boot information via out-of-band network.
This interface supports iSCSI and FibreChannel.
For the configuration, some extra parameters of volume connectors are
required by python-scciclient. These parameters should be set to
node's driver_info by an operator.
Partial-Bug: #1677436
Change-Id: I387ae9382ebc561bc721dcfed6416b25f4809183
Diffstat (limited to 'ironic/drivers/modules')
-rw-r--r-- | ironic/drivers/modules/irmc/boot.py | 367 |
1 files changed, 365 insertions, 2 deletions
diff --git a/ironic/drivers/modules/irmc/boot.py b/ironic/drivers/modules/irmc/boot.py index e8a93aba7..ff552fd07 100644 --- a/ironic/drivers/modules/irmc/boot.py +++ b/ironic/drivers/modules/irmc/boot.py @@ -41,6 +41,7 @@ from ironic.drivers.modules import pxe scci = importutils.try_import('scciclient.irmc.scci') +viom = importutils.try_import('scciclient.irmc.viom.client') try: if CONF.debug: @@ -57,7 +58,22 @@ REQUIRED_PROPERTIES = { "Required."), } -COMMON_PROPERTIES = REQUIRED_PROPERTIES +OPTIONAL_PROPERTIES = { + 'irmc_pci_physical_ids': + _("Physical IDs of PCI cards. A dictionary of pairs of resource UUID " + "and its physical ID like '<UUID>:<Physical ID>,...'. The resources " + "are Ports and Volume connectors. The Physical ID consists of card " + "type, slot No, and port No. The format is " + "{LAN|FC|CNA}<slot-No>-<Port-No>. This parameter is necessary for " + "booting a node from a remote volume. Optional."), + 'irmc_storage_network_size': + _("Size of the network for iSCSI storage network. It should be a " + "positive integer. This is necessary for booting a node from a " + "remote iSCSI volume. Optional."), +} + +COMMON_PROPERTIES = REQUIRED_PROPERTIES.copy() +COMMON_PROPERTIES.update(OPTIONAL_PROPERTIES) def _parse_config_option(): @@ -522,7 +538,329 @@ def check_share_fs_mounted(): share=CONF.irmc.remote_image_share_root) -class IRMCVirtualMediaBoot(base.BootInterface): +class IRMCVolumeBootMixIn(object): + """Mix-in class for volume boot configuration to iRMC + + iRMC has a feature to set up remote boot to a server. This feature can be + used by VIOM (Virtual I/O Manager) library of SCCI client. + """ + + def _validate_volume_boot(self, task): + """Validate information for volume boot with this interface. + + This interface requires physical information of connectors to + configure remote boot to iRMC. Physical information of LAN ports + is also required since VIOM feature manages all adapters. + + :param task: a TaskManager instance containing the node to act on. + :raises: InvalidParameterValue: If invalid value is set to resources. + :raises: MissingParameterValue: If some value is not set to resources. + """ + + if not deploy_utils.get_remote_boot_volume(task): + # No boot volume. Nothing to validate. + return + + irmc_common.parse_driver_info(task.node) + + for port in task.ports: + self._validate_lan_port(task.node, port) + + for vt in task.volume_targets: + if vt.volume_type == 'iscsi': + self._validate_iscsi_connectors(task) + elif vt.volume_type == 'fibre_channel': + self._validate_fc_connectors(task) + # Unknown volume type is filtered in storage interface validation. + + def _get_connector_physical_id(self, task, types): + """Get physical ID of volume connector. + + A physical ID of volume connector required by iRMC is registered in + "irmc_pci_physical_ids" of a Node's driver_info as a pair of resource + UUID and its physical ID. This method gets this ID from the parameter. + + :param task: a TaskManager instance containing the node to act on. + :param types: a list of types of volume connectors required for the + target volume. One of connectors must have a physical ID. + :raises InvalidParameterValue if a physical ID is invalid. + :returns: A physical ID of a volume connector. + """ + for vc in task.volume_connectors: + if vc.type not in types: + continue + pid = task.node.driver_info['irmc_pci_physical_ids'].get(vc.uuid) + if pid: + try: + viom.validate_physical_port_id(pid) + except scci.SCCIInvalidInputError as e: + raise exception.InvalidParameterValue( + _('Physical port information of volume connector ' + '%(connector)s is invalid: %(error)') % + {'connector': vc.uuid, 'error': e}) + return pid + return None + + def _validate_iscsi_connectors(self, task): + """Validate if volume connectors are properly registered for iSCSI. + + For connecting a node to a iSCSI volume, volume connectors containing + an IQNN and an IP address are necessary. One of connectors must have + a physical ID of the PCI card. Network size of a storage network is + also required by iRMC. which should be registered in the node's + driver_info. + + :param task: a TaskManager instance containing the node to act on. + :raises: InvalidParameterValue if a volume connector with a required + type is not registered. + :raises: InvalidParameterValue if a physical ID is not registered in + any volume connectors. + :raises: InvalidParameterValue if a physical ID is invalid. + """ + vc_dict = self._get_volume_connectors_by_type(task) + node = task.node + missing_types = [] + for vc_type in ('iqn', 'ip'): + vc = vc_dict.get(vc_type) + if not vc: + missing_types.append(vc_type) + continue + + if missing_types: + raise exception.MissingParameterValue( + _('Failed to validate for node %(node)s because of missing ' + 'volume connector(s) with type(s) %(types)s') % + {'node': node.uuid, + 'types': ', '.join(missing_types)}) + + if not self._get_connector_physical_id(task, ['iqn', 'ip']): + raise exception.MissingParameterValue( + _('Failed to validate for node %(node)s because of missing ' + 'physical port information for iSCSI connector. This ' + 'information must be set in "pci_physical_ids" parameter of ' + 'node\'s driver_info as <connector uuid>:<physical id>.') % + {'node': node.uuid}) + self._get_network_size(node) + + def _validate_fc_connectors(self, task): + """Validate if volume connectors are properly registered for FC. + + For connecting a node to a FC volume, one of connectors representing + wwnn and wwpn must have a physical ID of the PCI card. + + :param task: a TaskManager instance containing the node to act on. + :raises: InvalidParameterValue if a physical ID is not registered in + any volume connectors. + :raises: InvalidParameterValue if a physical ID is invalid. + """ + node = task.node + if not self._get_connector_physical_id(task, ['wwnn', 'wwpn']): + raise exception.MissingParameterValue( + _('Failed to validate for node %(node)s because of missing ' + 'physical port information for FC connector. This ' + 'information must be set in "pci_physical_ids" parameter of ' + 'node\'s driver_info as <connector uuid>:<physical id>.') % + {'node': node.uuid}) + + def _validate_lan_port(self, node, port): + """Validate ports for VIOM configuration. + + Physical information of LAN ports must be registered to VIOM + configuration to activate them under VIOM management. The information + has to be set to "irmc_pci_physical_id" parameter in a nodes + driver_info. + + :param node: an ironic node object + :param port: a port to be validated + :raises: MissingParameterValue if a physical ID of the port is not set. + :raises: InvalidParameterValue if a physical ID is invalid. + """ + physical_id = node.driver_info['irmc_pci_physical_ids'].get(port.uuid) + if not physical_id: + raise exception.MissingParameterValue( + _('Failed to validate for node %(node)s because of ' + 'missing physical port information of port %(port)s. ' + 'This information should be contained in ' + '"pci_physical_ids" parameter of node\'s driver_info.') % + {'node': node.uuid, + 'port': port.uuid}) + try: + viom.validate_physical_port_id(physical_id) + except scci.SCCIInvalidInputError as e: + raise exception.InvalidParameterValue( + _('Failed to validate for node %(node)s because ' + 'the physical port ID for port %(port)s in node\'s' + ' driver_info is invalid: %(reason)s') % + {'node': node.uuid, + 'port': port.uuid, + 'reason': e}) + + def _get_network_size(self, node): + """Get network size of a storage network. + + The network size of iSCSI network is required by iRMC for connecting + a node to an iSCSI volume. This network size is set to node's + driver_info as "irmc_storage_network_size" parameter in the form of + positive integer. + + :param node: an ironic node object. + :raises: MissingParameterValue if the network size parameter is not + set. + :raises: InvalidParameterValue the network size is invalid. + """ + network_size = node.driver_info.get('irmc_storage_network_size') + if network_size is None: + raise exception.MissingParameterValue( + _('Failed to validate for node %(node)s because of ' + 'missing "irmc_storage_network_size" parameter in the ' + 'node\'s driver_info. This should be a positive integer ' + 'smaller than 32.') % + {'node': node.uuid}) + try: + network_size = int(network_size) + except (ValueError, TypeError): + raise exception.InvalidParameterValue( + _('Failed to validate for node %(node)s because ' + '"irmc_storage_network_size" parameter in the node\'s ' + 'driver_info is invalid. This should be a ' + 'positive integer smaller than 32.') % + {'node': node.uuid}) + + if network_size not in range(1, 32): + raise exception.InvalidParameterValue( + _('Failed to validate for node %(node)s because ' + '"irmc_storage_network_size" parameter in the node\'s ' + 'driver_info is invalid. This should be a ' + 'positive integer smaller than 32.') % + {'node': node.uuid}) + + return network_size + + def _get_volume_connectors_by_type(self, task): + """Create a dictionary of volume connectors by types. + + :param task: a TaskManager. + :returns: a volume connector dictionary whose key is a connector type. + """ + connectors = {} + for vc in task.volume_connectors: + if vc.type in ('ip', 'iqn', 'wwnn', 'wwpn'): + connectors[vc.type] = vc + else: + LOG.warning('Node %(node)s has a volume_connector (%(uuid)s) ' + 'defined with an unsupported type: %(type)s.', + {'node': task.node.uuid, + 'uuid': vc.uuid, + 'type': vc.type}) + return connectors + + def _register_lan_ports(self, viom_conf, task): + """Register ports to VIOM configuration. + + LAN ports information must be registered for VIOM configuration to + activate them under VIOM management. + + :param viom_conf: a configurator for iRMC + :param task: a TaskManager instance containing the node to act on. + """ + for port in task.ports: + viom_conf.set_lan_port( + task.node.driver_info['irmc_pci_physical_ids'].get(port.uuid)) + + def _configure_boot_from_volume(self, task): + """Set information for booting from a remote volume to iRMC. + + :param task: a TaskManager instance containing the node to act on. + :raises: IRMCOperationError if iRMC operation failed + """ + + irmc_info = irmc_common.parse_driver_info(task.node) + viom_conf = viom.VIOMConfiguration(irmc_info, + identification=task.node.uuid) + + self._register_lan_ports(viom_conf, task) + + for vt in task.volume_targets: + if vt.volume_type == 'iscsi': + self._set_iscsi_target(task, viom_conf, vt) + elif vt.volume_type == 'fibre_channel': + self._set_fc_target(task, viom_conf, vt) + + try: + LOG.debug('Set VIOM configuration for node %(node)s: %(table)s', + {'node': task.node.uuid, + 'table': viom_conf.dump_json()}) + viom_conf.apply() + except scci.SCCIError as e: + LOG.error('iRMC failed to set VIOM configuration for node ' + '%(node)s: %(error)s', + {'node': task.node.uuid, + 'error': e}) + raise exception.IRMCOperationError( + operation='Configure VIOM', error=e) + + def _set_iscsi_target(self, task, viom_conf, target): + """Set information for iSCSI boot to VIOM configuration.""" + connectors = self._get_volume_connectors_by_type(task) + target_portal = target.properties['target_portal'] + if ':' in target_portal: + target_host, target_port = target_portal.split(':') + else: + target_host = target_portal + target_port = None + if target.properties.get('auth_method') == 'CHAP': + chap_user = target.properties.get('auth_username') + chap_secret = target.properties.get('auth_password') + else: + chap_user = None + chap_secret = None + + viom_conf.set_iscsi_volume( + self._get_connector_physical_id(task, ['iqn', 'ip']), + connectors['iqn'].connector_id, + initiator_ip=connectors['ip'].connector_id, + initiator_netmask=self._get_network_size(task.node), + target_iqn=target.properties['target_iqn'], + target_ip=target_host, + target_port=target_port, + target_lun=target.properties.get('target_lun'), + # Boot priority starts from 1 in the library. + boot_prio=target.boot_index + 1, + chap_user=chap_user, + chap_secret=chap_secret) + + def _set_fc_target(self, task, viom_conf, target): + """Set information for FC boot to VIOM configuration.""" + wwn = target.properties['target_wwn'] + if isinstance(wwn, list): + wwn = wwn[0] + viom_conf.set_fc_volume( + self._get_connector_physical_id(task, ['wwnn', 'wwpn']), + wwn, + target.properties['target_lun'], + # Boot priority starts from 1 in the library. + boot_prio=target.boot_index + 1) + + def _cleanup_boot_from_volume(self, task, reboot=False): + """Clear remote boot configuration. + + :param task: a task from TaskManager. + :param reboot: True if reboot node soon + :raises: IRMCOperationError if iRMC operation failed + """ + irmc_info = irmc_common.parse_driver_info(task.node) + try: + viom_conf = viom.VIOMConfiguration(irmc_info, task.node.uuid) + viom_conf.terminate(reboot=reboot) + except scci.SCCIError as e: + LOG.error('iRMC failed to terminate VIOM configuration from ' + 'node %(node)s: %(error)s', {'node': task.node.uuid, + 'error': e}) + raise exception.IRMCOperationError(operation='Terminate VIOM', + error=e) + + +class IRMCVirtualMediaBoot(base.BootInterface, IRMCVolumeBootMixIn): """iRMC Virtual Media boot-related actions.""" def __init__(self): @@ -533,6 +871,7 @@ class IRMCVirtualMediaBoot(base.BootInterface): :raises: InvalidParameterValue, if config option has invalid value. """ check_share_fs_mounted() + self.capabilities = ['iscsi_volume_boot', 'fc_volume_boot'] super(IRMCVirtualMediaBoot, self).__init__() def get_properties(self): @@ -553,6 +892,13 @@ class IRMCVirtualMediaBoot(base.BootInterface): """ check_share_fs_mounted() + self._validate_volume_boot(task) + if not task.driver.storage.should_write_image(task): + LOG.debug('Node %(node) skips image validation because of booting ' + 'from a remote volume.', + {'node': task.node.uuid}) + return + d_info = _parse_deploy_info(task.node) if task.node.driver_internal_info.get('is_whole_disk_image'): props = [] @@ -594,6 +940,12 @@ class IRMCVirtualMediaBoot(base.BootInterface): if task.node.provision_state == states.DEPLOYING: irmc_management.backup_bios_config(task) + if not task.driver.storage.should_write_image(task): + LOG.debug('Node %(node) skips ramdisk preparation because of ' + 'booting from a remote volume.', + {'node': task.node.uuid}) + return + deploy_nic_mac = deploy_utils.get_single_nic_with_vif_port_id(task) ramdisk_params['BOOTIF'] = deploy_nic_mac @@ -622,6 +974,13 @@ class IRMCVirtualMediaBoot(base.BootInterface): :param task: a task from TaskManager. :returns: None """ + if task.node.driver_internal_info.get('boot_from_volume'): + LOG.debug('Node %(node) is configured for booting from a remote ' + 'volume.', + {'node': task.node.uuid}) + self._configure_boot_from_volume(task) + return + _cleanup_vmedia_boot(task) node = task.node @@ -645,6 +1004,10 @@ class IRMCVirtualMediaBoot(base.BootInterface): :returns: None :raises: IRMCOperationError if iRMC operation failed. """ + if task.node.driver_internal_info.get('boot_from_volume'): + self._cleanup_boot_from_volume(task) + return + _remove_share_file(_get_boot_iso_name(task.node)) driver_internal_info = task.node.driver_internal_info driver_internal_info.pop('irmc_boot_iso', None) |