From 70530f9a668af6d59a3e72c230592a4b0dc2a1b8 Mon Sep 17 00:00:00 2001 From: Hironori Shiina Date: Wed, 31 May 2017 13:13:52 +0900 Subject: [PATCH] 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 --- ironic/drivers/modules/irmc/boot.py | 367 +++++++++++++- .../unit/drivers/modules/irmc/test_boot.py | 452 ++++++++++++++++++ .../drivers/third_party_driver_mock_specs.py | 15 + ...rmc-boot-from-volume-4bc5d20a0a780669.yaml | 37 ++ 4 files changed, 869 insertions(+), 2 deletions(-) create mode 100644 releasenotes/notes/irmc-boot-from-volume-4bc5d20a0a780669.yaml diff --git a/ironic/drivers/modules/irmc/boot.py b/ironic/drivers/modules/irmc/boot.py index e8a93aba78..ff552fd07d 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 ':,...'. 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}-. 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 :.') % + {'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 :.') % + {'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) diff --git a/ironic/tests/unit/drivers/modules/irmc/test_boot.py b/ironic/tests/unit/drivers/modules/irmc/test_boot.py index 0e03218481..bfc3b3b23d 100644 --- a/ironic/tests/unit/drivers/modules/irmc/test_boot.py +++ b/ironic/tests/unit/drivers/modules/irmc/test_boot.py @@ -23,6 +23,7 @@ import tempfile from ironic_lib import utils as ironic_utils import mock from oslo_config import cfg +from oslo_utils import uuidutils import six from ironic.common import boot_devices @@ -41,6 +42,8 @@ from ironic.drivers.modules import pxe from ironic.tests.unit.conductor import mgr_utils from ironic.tests.unit.db import base as db_base from ironic.tests.unit.db import utils as db_utils +from ironic.tests.unit.drivers import third_party_driver_mock_specs \ + as mock_specs from ironic.tests.unit.objects import utils as obj_utils if six.PY3: @@ -50,6 +53,19 @@ if six.PY3: INFO_DICT = db_utils.get_test_irmc_info() CONF = cfg.CONF +PARSED_IFNO = { + 'irmc_address': '1.2.3.4', + 'irmc_port': 80, + 'irmc_username': 'admin0', + 'irmc_password': 'fake0', + 'irmc_auth_method': 'digest', + 'irmc_client_timeout': 60, + 'irmc_snmp_community': 'public', + 'irmc_snmp_port': 161, + 'irmc_snmp_version': 'v2c', + 'irmc_snmp_security': None, + 'irmc_sensor_method': 'ipmitool', +} class IRMCDeployPrivateMethodsTestCase(db_base.DbTestCase): @@ -1215,3 +1231,439 @@ class IRMCPXEBootTestCase(db_base.DbTestCase): self.assertFalse(mock_set_secure_boot_mode.called) mock_clean_up_instance.assert_called_once_with( task.driver.boot, task) + + +@mock.patch.object(irmc_boot, 'viom', + spec_set=mock_specs.SCCICLIENT_VIOM_SPEC) +class IRMCVirtualMediaBootWithVolumeTestCase(db_base.DbTestCase): + + def setUp(self): + super(IRMCVirtualMediaBootWithVolumeTestCase, self).setUp() + irmc_boot.check_share_fs_mounted_patcher.start() + self.addCleanup(irmc_boot.check_share_fs_mounted_patcher.stop) + self.config(enabled_hardware_types=['irmc'], + enabled_boot_interfaces=['irmc-virtual-media'], + enabled_deploy_interfaces=['direct'], + enabled_power_interfaces=['irmc'], + enabled_management_interfaces=['irmc'], + enabled_storage_interfaces=['cinder']) + driver_info = INFO_DICT + d_in_info = dict(boot_from_volume='volume-uuid') + self.node = obj_utils.create_test_node(self.context, + driver='irmc', + driver_info=driver_info, + storage_interface='cinder', + driver_internal_info=d_in_info) + + def _create_mock_conf(self, mock_viom): + mock_conf = mock.Mock(spec_set=mock_specs.SCCICLIENT_VIOM_CONF_SPEC) + mock_viom.VIOMConfiguration.return_value = mock_conf + return mock_conf + + def _add_pci_physical_id(self, uuid, physical_id): + driver_info = self.node.driver_info + ids = driver_info.get('irmc_pci_physical_ids', {}) + ids[uuid] = physical_id + driver_info['irmc_pci_physical_ids'] = ids + self.node.driver_info = driver_info + self.node.save() + + def _create_port(self, physical_id='LAN0-1', **kwargs): + uuid = uuidutils.generate_uuid() + obj_utils.create_test_port(self.context, + uuid=uuid, + node_id=self.node.id, + **kwargs) + if physical_id: + self._add_pci_physical_id(uuid, physical_id) + + def _create_iscsi_iqn_connector(self, physical_id='CNA1-1'): + uuid = uuidutils.generate_uuid() + obj_utils.create_test_volume_connector( + self.context, + uuid=uuid, + type='iqn', + node_id=self.node.id, + connector_id='iqn.initiator') + if physical_id: + self._add_pci_physical_id(uuid, physical_id) + + def _create_iscsi_ip_connector(self, physical_id=None, network_size='24'): + uuid = uuidutils.generate_uuid() + obj_utils.create_test_volume_connector( + self.context, + uuid=uuid, + type='ip', + node_id=self.node.id, + connector_id='192.168.11.11') + if physical_id: + self._add_pci_physical_id(uuid, physical_id) + if network_size: + driver_info = self.node.driver_info + driver_info['irmc_storage_network_size'] = network_size + self.node.driver_info = driver_info + self.node.save() + + def _create_iscsi_target(self, target_info=None, boot_index=0, **kwargs): + target_properties = { + 'target_portal': '192.168.22.22:3260', + 'target_iqn': 'iqn.target', + 'target_lun': 1, + } + if target_info: + target_properties.update(target_info) + obj_utils.create_test_volume_target( + self.context, + volume_type='iscsi', + node_id=self.node.id, + boot_index=boot_index, + properties=target_properties, + **kwargs) + + def _create_iscsi_resources(self): + self._create_iscsi_iqn_connector() + self._create_iscsi_ip_connector() + self._create_iscsi_target() + + def _create_fc_connector(self): + uuid = uuidutils.generate_uuid() + obj_utils.create_test_volume_connector( + self.context, + uuid=uuid, + type='wwnn', + node_id=self.node.id, + connector_id='11:22:33:44:55') + self._add_pci_physical_id(uuid, 'FC2-1') + obj_utils.create_test_volume_connector( + self.context, + uuid=uuidutils.generate_uuid(), + type='wwpn', + node_id=self.node.id, + connector_id='11:22:33:44:56') + + def _create_fc_target(self): + target_properties = { + 'target_wwn': 'aa:bb:cc:dd:ee', + 'target_lun': 2, + } + obj_utils.create_test_volume_target( + self.context, + volume_type='fibre_channel', + node_id=self.node.id, + boot_index=0, + properties=target_properties) + + def _create_fc_resources(self): + self._create_fc_connector() + self._create_fc_target() + + def _call_validate(self): + with task_manager.acquire(self.context, self.node.uuid) as task: + task.driver.boot.validate(task) + + def test_validate_iscsi(self, mock_viom): + self._create_port() + self._create_iscsi_resources() + self._call_validate() + self.assertEqual([mock.call('LAN0-1'), mock.call('CNA1-1')], + mock_viom.validate_physical_port_id.call_args_list) + + def test_validate_no_physical_id_in_lan_port(self, mock_viom): + self._create_port(physical_id=None) + self._create_iscsi_resources() + self.assertRaises(exception.MissingParameterValue, + self._call_validate) + + @mock.patch.object(irmc_boot, 'scci', + spec_set=mock_specs.SCCICLIENT_IRMC_SCCI_SPEC) + def test_validate_invalid_physical_id_in_lan_port(self, mock_scci, + mock_viom): + self._create_port(physical_id='wrong-id') + self._create_iscsi_resources() + + mock_viom.validate_physical_port_id.side_effect = ( + Exception('fake error')) + mock_scci.SCCIInvalidInputError = Exception + self.assertRaises(exception.InvalidParameterValue, + self._call_validate) + + def test_validate_iscsi_connector_no_ip(self, mock_viom): + self._create_port() + self._create_iscsi_iqn_connector() + self._create_iscsi_target() + + self.assertRaises(exception.MissingParameterValue, + self._call_validate) + + def test_validate_iscsi_connector_no_iqn(self, mock_viom): + self._create_port() + self._create_iscsi_ip_connector(physical_id='CNA1-1') + self._create_iscsi_target() + + self.assertRaises(exception.MissingParameterValue, + self._call_validate) + + def test_validate_iscsi_connector_no_netmask(self, mock_viom): + self._create_port() + self._create_iscsi_iqn_connector() + self._create_iscsi_ip_connector(network_size=None) + self._create_iscsi_target() + + self.assertRaises(exception.MissingParameterValue, + self._call_validate) + + def test_validate_iscsi_connector_invalid_netmask(self, mock_viom): + self._create_port() + self._create_iscsi_iqn_connector() + self._create_iscsi_ip_connector(network_size='worng-netmask') + self._create_iscsi_target() + + self.assertRaises(exception.InvalidParameterValue, + self._call_validate) + + def test_validate_iscsi_connector_too_small_netmask(self, mock_viom): + self._create_port() + self._create_iscsi_iqn_connector() + self._create_iscsi_ip_connector(network_size='0') + self._create_iscsi_target() + + self.assertRaises(exception.InvalidParameterValue, + self._call_validate) + + def test_validate_iscsi_connector_too_large_netmask(self, mock_viom): + self._create_port() + self._create_iscsi_iqn_connector() + self._create_iscsi_ip_connector(network_size='32') + self._create_iscsi_target() + + self.assertRaises(exception.InvalidParameterValue, + self._call_validate) + + def test_validate_iscsi_connector_no_physical_id(self, mock_viom): + self._create_port() + self._create_iscsi_iqn_connector(physical_id=None) + self._create_iscsi_ip_connector() + self._create_iscsi_target() + + self.assertRaises(exception.MissingParameterValue, + self._call_validate) + + @mock.patch.object(deploy_utils, 'get_single_nic_with_vif_port_id') + def test_prepare_ramdisk_skip(self, mock_nic, mock_viom): + self._create_iscsi_resources() + with task_manager.acquire(self.context, self.node.uuid) as task: + task.node.provision_state = states.DEPLOYING + task.driver.boot.prepare_ramdisk(task, {}) + mock_nic.assert_not_called() + + @mock.patch.object(irmc_boot, '_cleanup_vmedia_boot') + def test_prepare_instance(self, mock_clean, mock_viom): + mock_conf = self._create_mock_conf(mock_viom) + self._create_port() + self._create_iscsi_resources() + + with task_manager.acquire(self.context, self.node.uuid) as task: + task.driver.boot.prepare_instance(task) + mock_clean.assert_not_called() + + mock_conf.set_iscsi_volume.assert_called_once_with( + 'CNA1-1', + 'iqn.initiator', + initiator_ip='192.168.11.11', + initiator_netmask=24, + target_iqn='iqn.target', + target_ip='192.168.22.22', + target_port='3260', + target_lun=1, + boot_prio=1, + chap_user=None, + chap_secret=None) + mock_conf.set_lan_port.assert_called_once_with('LAN0-1') + mock_viom.validate_physical_port_id.assert_called_once_with('CNA1-1') + self._assert_viom_apply(mock_viom, mock_conf) + + def _call__configure_boot_from_volume(self): + with task_manager.acquire(self.context, self.node.uuid) as task: + task.driver.boot._configure_boot_from_volume(task) + + def _assert_viom_apply(self, mock_viom, mock_conf): + mock_conf.apply.assert_called_once_with() + mock_conf.dump_json.assert_called_once_with() + mock_viom.VIOMConfiguration.assert_called_once_with( + PARSED_IFNO, identification=self.node.uuid) + + def test__configure_boot_from_volume_iscsi(self, mock_viom): + mock_conf = self._create_mock_conf(mock_viom) + self._create_port() + self._create_iscsi_resources() + + self._call__configure_boot_from_volume() + + mock_conf.set_iscsi_volume.assert_called_once_with( + 'CNA1-1', + 'iqn.initiator', + initiator_ip='192.168.11.11', + initiator_netmask=24, + target_iqn='iqn.target', + target_ip='192.168.22.22', + target_port='3260', + target_lun=1, + boot_prio=1, + chap_user=None, + chap_secret=None) + mock_conf.set_lan_port.assert_called_once_with('LAN0-1') + mock_viom.validate_physical_port_id.assert_called_once_with('CNA1-1') + self._assert_viom_apply(mock_viom, mock_conf) + + def test__configure_boot_from_volume_multi_lan_ports(self, mock_viom): + mock_conf = self._create_mock_conf(mock_viom) + self._create_port() + self._create_port(physical_id='LAN0-2', + address='52:54:00:cf:2d:32') + self._create_iscsi_resources() + + self._call__configure_boot_from_volume() + + mock_conf.set_iscsi_volume.assert_called_once_with( + 'CNA1-1', + 'iqn.initiator', + initiator_ip='192.168.11.11', + initiator_netmask=24, + target_iqn='iqn.target', + target_ip='192.168.22.22', + target_port='3260', + target_lun=1, + boot_prio=1, + chap_user=None, + chap_secret=None) + self.assertEqual([mock.call('LAN0-1'), mock.call('LAN0-2')], + mock_conf.set_lan_port.call_args_list) + mock_viom.validate_physical_port_id.assert_called_once_with('CNA1-1') + self._assert_viom_apply(mock_viom, mock_conf) + + def test__configure_boot_from_volume_iscsi_no_portal_port(self, mock_viom): + mock_conf = self._create_mock_conf(mock_viom) + self._create_port() + self._create_iscsi_iqn_connector() + self._create_iscsi_ip_connector() + self._create_iscsi_target( + target_info=dict(target_portal='192.168.22.23')) + + self._call__configure_boot_from_volume() + + mock_conf.set_iscsi_volume.assert_called_once_with( + 'CNA1-1', + 'iqn.initiator', + initiator_ip='192.168.11.11', + initiator_netmask=24, + target_iqn='iqn.target', + target_ip='192.168.22.23', + target_port=None, + target_lun=1, + boot_prio=1, + chap_user=None, + chap_secret=None) + mock_conf.set_lan_port.assert_called_once_with('LAN0-1') + mock_viom.validate_physical_port_id.assert_called_once_with('CNA1-1') + self._assert_viom_apply(mock_viom, mock_conf) + + def test__configure_boot_from_volume_iscsi_chap(self, mock_viom): + mock_conf = self._create_mock_conf(mock_viom) + self._create_port() + self._create_iscsi_iqn_connector() + self._create_iscsi_ip_connector() + self._create_iscsi_target( + target_info=dict(auth_method='CHAP', + auth_username='chapuser', + auth_password='chappass')) + + self._call__configure_boot_from_volume() + + mock_conf.set_iscsi_volume.assert_called_once_with( + 'CNA1-1', + 'iqn.initiator', + initiator_ip='192.168.11.11', + initiator_netmask=24, + target_iqn='iqn.target', + target_ip='192.168.22.22', + target_port='3260', + target_lun=1, + boot_prio=1, + chap_user='chapuser', + chap_secret='chappass') + mock_conf.set_lan_port.assert_called_once_with('LAN0-1') + mock_viom.validate_physical_port_id.assert_called_once_with('CNA1-1') + self._assert_viom_apply(mock_viom, mock_conf) + + def test__configure_boot_from_volume_fc(self, mock_viom): + mock_conf = self._create_mock_conf(mock_viom) + self._create_port() + self._create_fc_connector() + self._create_fc_target() + + self._call__configure_boot_from_volume() + + mock_conf.set_fc_volume.assert_called_once_with( + 'FC2-1', + 'aa:bb:cc:dd:ee', + 2, + boot_prio=1) + mock_conf.set_lan_port.assert_called_once_with('LAN0-1') + mock_viom.validate_physical_port_id.assert_called_once_with('FC2-1') + self._assert_viom_apply(mock_viom, mock_conf) + + @mock.patch.object(irmc_boot, 'scci', + spec_set=mock_specs.SCCICLIENT_IRMC_SCCI_SPEC) + def test__configure_boot_from_volume_apply_error(self, mock_scci, + mock_viom): + mock_conf = self._create_mock_conf(mock_viom) + self._create_port() + self._create_fc_connector() + self._create_fc_target() + mock_conf.apply.side_effect = Exception('fake scci error') + mock_scci.SCCIError = Exception + + with task_manager.acquire(self.context, self.node.uuid) as task: + self.assertRaises(exception.IRMCOperationError, + task.driver.boot._configure_boot_from_volume, + task) + + mock_conf.set_fc_volume.assert_called_once_with( + 'FC2-1', + 'aa:bb:cc:dd:ee', + 2, + boot_prio=1) + mock_conf.set_lan_port.assert_called_once_with('LAN0-1') + mock_viom.validate_physical_port_id.assert_called_once_with('FC2-1') + self._assert_viom_apply(mock_viom, mock_conf) + + def test_clean_up_instance(self, mock_viom): + mock_conf = self._create_mock_conf(mock_viom) + with task_manager.acquire(self.context, self.node.uuid) as task: + task.driver.boot.clean_up_instance(task) + + mock_viom.VIOMConfiguration.assert_called_once_with(PARSED_IFNO, + self.node.uuid) + mock_conf.terminate.assert_called_once_with(reboot=False) + + def test_clean_up_instance_error(self, mock_viom): + mock_conf = self._create_mock_conf(mock_viom) + mock_conf.terminate.side_effect = Exception('fake error') + irmc_boot.scci.SCCIError = Exception + with task_manager.acquire(self.context, self.node.uuid) as task: + self.assertRaises(exception.IRMCOperationError, + task.driver.boot.clean_up_instance, + task) + + mock_viom.VIOMConfiguration.assert_called_once_with(PARSED_IFNO, + self.node.uuid) + mock_conf.terminate.assert_called_once_with(reboot=False) + + def test__cleanup_boot_from_volume(self, mock_viom): + mock_conf = self._create_mock_conf(mock_viom) + with task_manager.acquire(self.context, self.node.uuid) as task: + task.driver.boot._cleanup_boot_from_volume(task) + + mock_viom.VIOMConfiguration.assert_called_once_with(PARSED_IFNO, + self.node.uuid) + mock_conf.terminate.assert_called_once_with(reboot=False) diff --git a/ironic/tests/unit/drivers/third_party_driver_mock_specs.py b/ironic/tests/unit/drivers/third_party_driver_mock_specs.py index f07f20f096..8f59f10720 100644 --- a/ironic/tests/unit/drivers/third_party_driver_mock_specs.py +++ b/ironic/tests/unit/drivers/third_party_driver_mock_specs.py @@ -93,6 +93,7 @@ SCCICLIENT_IRMC_SCCI_SPEC = ( 'UNMOUNT_FD', 'SCCIError', 'SCCIClientError', + 'SCCIError', 'SCCIInvalidInputError', 'get_share_type', 'get_client', @@ -108,6 +109,20 @@ SCCICLIENT_IRMC_ELCM_SPEC = ( 'set_secure_boot_mode', ) +SCCICLIENT_VIOM_SPEC = ( + 'validate_physical_port_id', + 'VIOMConfiguration', +) + +SCCICLIENT_VIOM_CONF_SPEC = ( + 'set_lan_port', + 'set_iscsi_volume', + 'set_fc_volume', + 'apply', + 'dump_json', + 'terminate', +) + ONEVIEWCLIENT_SPEC = ( 'client', 'states', diff --git a/releasenotes/notes/irmc-boot-from-volume-4bc5d20a0a780669.yaml b/releasenotes/notes/irmc-boot-from-volume-4bc5d20a0a780669.yaml new file mode 100644 index 0000000000..9f8b1ef0d5 --- /dev/null +++ b/releasenotes/notes/irmc-boot-from-volume-4bc5d20a0a780669.yaml @@ -0,0 +1,37 @@ +--- +features: + - | + Adds support for booting from remote volumes to ``irmc-virtual-media`` + boot interface. It enables boot configuration for iSCSI or FibreChannel + via out-of-band network. + + In addition to the same configuration as generic boot-from-volume, this + interface requires the following settings. + + * It is necessary to set a physical port ID to network ports and volume + connectors. All cards including those not used for volume boot should be + registered. + + * A physical ID format is: ``-`` + + ```` + LAN, FC or CNA + ```` + 0 indicates onboard slot. Use 1 to 9 for addon slots. + ```` + A port number starting from 1. + + * Set the IDs to ``node.driver_info.pci_physical_ids``. This parameter + is a list of pair of UUID of a resource (Port or Volume connector) + and a physical ID like: + + pci_physical_ids = 1ecd14ee-c191-4007-8413-16bb5d5a73a2:LAN0-1,1ecd14ee-c191-4007-8413-16bb5d5a73a2:CNA1-1 + + * For iSCSI, volume connectors with both type ``iqn`` and ``ip`` are + required. The configuration with DHCP is not supported yet. + + * For iSCSI, a subnet mask of the storage network is necessary. It should + be set to ``node.driver_info.storage_network_size`` as integer. + + This feature requires specific FC cards or CNAs (Converged Network Adapter). + See the documentation of iRMC driver for details.