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:
parent
cffe41c238
commit
70530f9a66
@ -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)
|
||||
|
@ -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)
|
||||
|
@ -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',
|
||||
|
@ -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.
|
Loading…
Reference in New Issue
Block a user