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
This commit is contained in:
Hironori Shiina 2017-05-31 13:13:52 +09:00
parent cffe41c238
commit 70530f9a66
4 changed files with 869 additions and 2 deletions

View File

@ -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)

View File

@ -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)

View File

@ -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',

View File

@ -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: ``<Card Type><Slot No>-<Port No>``
``<Card Type>``
LAN, FC or CNA
``<Slot No>``
0 indicates onboard slot. Use 1 to 9 for addon slots.
``<Port No>``
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.