diff --git a/doc/source/admin/drivers/irmc.rst b/doc/source/admin/drivers/irmc.rst index 964ce7c51b..769f285c46 100644 --- a/doc/source/admin/drivers/irmc.rst +++ b/doc/source/admin/drivers/irmc.rst @@ -18,7 +18,7 @@ Prerequisites * Install `python-scciclient `_ and `pysnmp `_ packages:: - $ pip install "python-scciclient>=0.6.0" pysnmp + $ pip install "python-scciclient>=0.7.0" pysnmp Hardware Type ============= @@ -65,6 +65,10 @@ hardware interfaces: Supports ``irmc``, which enables power control via ServerView Common Command Interface (SCCI), by default. Also supports ``ipmitool``. +* raid + Supports ``irmc``, ``no-raid`` and ``agent``. + The default is ``no-raid``. + For other hardware interfaces, ``irmc`` hardware type supports the Bare Metal reference interfaces. For more details about the hardware interfaces and how to enable the desired ones, see @@ -84,7 +88,7 @@ interfaces enabled for ``irmc`` hardware type. enabled_management_interfaces = irmc enabled_network_interfaces = flat,neutron enabled_power_interfaces = irmc - enabled_raid_interfaces = no-raid + enabled_raid_interfaces = no-raid,irmc enabled_storage_interfaces = noop,cinder enabled_vendor_interfaces = no-vendor,ipmitool @@ -93,10 +97,10 @@ Here is a command example to enroll a node with ``irmc`` hardware type. .. code-block:: console openstack baremetal node create --os-baremetal-api-version=1.31 \ - --driver irmc \ --boot-interface irmc-pxe \ --deploy-interface direct \ - --inspect-interface irmc + --inspect-interface irmc \ + --raid-interface irmc Node configuration ^^^^^^^^^^^^^^^^^^ @@ -384,6 +388,99 @@ example:: See :ref:`capabilities-discovery` for more details and examples. +RAID configuration Support +^^^^^^^^^^^^^^^^^^^^^^^^^^ + +The ``irmc`` hardware type provides the iRMC RAID configuration with ``irmc`` +raid interface. + +.. note:: + + * RAID implementation for ``irmc`` hardware type is based on eLCM license + and SDCard. Otherwise, SP(Service Platform) in lifecycle management + must be available. + * RAID implementation only supported for RAIDAdapter 0 in Fujitsu Servers. + +Configuration +~~~~~~~~~~~~~ + +The RAID configuration Support in the iRMC drivers requires the following +configuration: + +* It is necessary to set ironic configuration into Node with + JSON file option:: + + $ openstack baremetal node set \ + --target-raid-config + + Here is some sample values for JSON file:: + + { + "logical_disks": [ + { + "size_gb": 1000, + "raid_level": "1" + ] + } + + or:: + + { + "logical_disks": [ + { + "size_gb": 1000, + "raid_level": "1", + "controller": "FTS RAID Ctrl SAS 6G 1GB (D3116C) (0)", + "physical_disks": [ + "0", + "1" + ] + } + ] + } + +.. note:: + + RAID 1+0 and 5+0 in iRMC driver does not support property ``physical_disks`` + in ``target_raid_config`` during create raid configuration yet. See + following example:: + + { + "logical_disks": + [ + { + "size_gb": "Max", + "raid_level": "1+0" + } + ] + } + +See :ref:`raid` for more details and examples. + +Supported properties +~~~~~~~~~~~~~~~~~~~~ + +The RAID configuration using iRMC driver supports following parameters in +JSON file: + +* ``size_gb``: is mandatory properties in Ironic. +* ``raid_level``: is mandatory properties in Ironic. Currently, iRMC Server + supports following RAID levels: 0, 1, 5, 6, 1+0 and 5+0. +* ``controller``: is name of the controller as read by the RAID interface. +* ``physical_disks``: are specific values for each raid array in + LogicalDrive which operator want to set them along with ``raid_level``. + +The RAID configuration is supported as a manual cleaning step. + +.. note:: + + * iRMC server will power-on after create/delete raid configuration is + applied, FGI (Foreground Initialize) will process raid configuration in + iRMC server, thus the operation will completed upon power-on and power-off + when created RAID on iRMC server. + +See :ref:`raid` for more details and examples. + Supported platforms =================== This driver supports FUJITSU PRIMERGY BX S4 or RX S8 servers and above. @@ -397,3 +494,8 @@ Power Off (Graceful Power Off) are only available if `ServerView agents `_ are installed. See `iRMC S4 Manual `_ for more details. + +RAID configuration feature supports FUJITSU PRIMERGY servers with +RAID-Ctrl-SAS-6G-1GB(D3116C) controller and above. +For detail supported controller with OOB-RAID configuration, please see +`the whitepaper for iRMC RAID configuration `_. diff --git a/driver-requirements.txt b/driver-requirements.txt index 6435463334..5b4c7e22eb 100644 --- a/driver-requirements.txt +++ b/driver-requirements.txt @@ -8,7 +8,7 @@ proliantutils>=2.5.0 pysnmp python-ironic-inspector-client>=1.5.0 python-oneviewclient<3.0.0,>=2.5.2 -python-scciclient>=0.6.0 +python-scciclient>=0.7.0 python-ilorest-library>=2.1.0 hpOneView>=4.4.0 UcsSdk==0.8.2.2 diff --git a/ironic/conf/irmc.py b/ironic/conf/irmc.py index c4f9bacd23..8a4e000159 100644 --- a/ironic/conf/irmc.py +++ b/ironic/conf/irmc.py @@ -89,6 +89,14 @@ opts = [ 'this option is not defined, then leave out ' 'pci_gpu_devices in capabilities property. ' 'Sample gpu_ids value: 0x1000/0x0079,0x2100/0x0080')), + cfg.IntOpt('query_raid_config_fgi_status_interval', + min=1, + default=300, + help=_('Interval (in seconds) between periodic RAID status ' + 'checks to determine whether the asynchronous RAID ' + 'configuration was successfully finished or not. ' + 'Foreground Initialization (FGI) will start 5 minutes ' + 'after creating virtual drives.')), ] diff --git a/ironic/drivers/irmc.py b/ironic/drivers/irmc.py index 45da922620..d35ba4e2b4 100644 --- a/ironic/drivers/irmc.py +++ b/ironic/drivers/irmc.py @@ -17,12 +17,14 @@ of FUJITSU PRIMERGY servers, and above servers. """ from ironic.drivers import generic +from ironic.drivers.modules import agent from ironic.drivers.modules import inspector from ironic.drivers.modules import ipmitool from ironic.drivers.modules.irmc import boot from ironic.drivers.modules.irmc import inspect from ironic.drivers.modules.irmc import management from ironic.drivers.modules.irmc import power +from ironic.drivers.modules.irmc import raid from ironic.drivers.modules import noop from ironic.drivers.modules import pxe @@ -63,3 +65,8 @@ class IRMCHardware(generic.GenericHardware): def supported_power_interfaces(self): """List of supported power interfaces.""" return [power.IRMCPower, ipmitool.IPMIPower] + + @property + def supported_raid_interfaces(self): + """List of supported raid interfaces.""" + return [noop.NoRAID, raid.IRMCRAID, agent.AgentRAID] diff --git a/ironic/drivers/modules/irmc/common.py b/ironic/drivers/modules/irmc/common.py index 9f04d74e1e..59fe954500 100644 --- a/ironic/drivers/modules/irmc/common.py +++ b/ironic/drivers/modules/irmc/common.py @@ -21,6 +21,8 @@ import six from ironic.common import exception from ironic.common.i18n import _ +from ironic.common import raid as raid_common +from ironic.conductor import utils as manager_utils from ironic.conf import CONF scci = importutils.try_import('scciclient.irmc.scci') @@ -219,3 +221,9 @@ def set_secure_boot_mode(node, enable): raise exception.IRMCOperationError( operation=_("setting secure boot mode"), error=irmc_exception) + + +def resume_cleaning(task): + raid_common.update_raid_info( + task.node, task.node.raid_config) + manager_utils.notify_conductor_resume_clean(task) diff --git a/ironic/drivers/modules/irmc/raid.py b/ironic/drivers/modules/irmc/raid.py new file mode 100644 index 0000000000..0ec6452ee9 --- /dev/null +++ b/ironic/drivers/modules/irmc/raid.py @@ -0,0 +1,502 @@ +# Copyright 2018 FUJITSU LIMITED +# +# Licensed under the Apache License, Version 2.0 (the "License"); you may +# not use this file except in compliance with the License. You may obtain +# a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT +# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the +# License for the specific language governing permissions and limitations +# under the License. + +""" +Irmc RAID specific methods +""" +from futurist import periodics +from ironic_lib import metrics_utils +from oslo_log import log as logging +from oslo_utils import importutils +import six + +from ironic.common import exception +from ironic.common import raid as raid_common +from ironic.common import states +from ironic.conductor import task_manager +from ironic import conf +from ironic.drivers import base +from ironic.drivers.modules.irmc import common as irmc_common + +client = importutils.try_import('scciclient.irmc') + +LOG = logging.getLogger(__name__) +CONF = conf.CONF + +METRICS = metrics_utils.get_metrics_logger(__name__) + +RAID_LEVELS = { + '0': { + 'min_disks': 1, + 'max_disks': 1000, + 'factor': 0, + }, + '1': { + 'min_disks': 2, + 'max_disks': 2, + 'factor': 1, + }, + '5': { + 'min_disks': 3, + 'max_disks': 1000, + 'factor': 1, + }, + '6': { + 'min_disks': 4, + 'max_disks': 1000, + 'factor': 2, + }, + '10': { + 'min_disks': 4, + 'max_disks': 1000, + 'factor': 2, + }, + '50': { + 'min_disks': 6, + 'max_disks': 1000, + 'factor': 2, + } +} + +RAID_COMPLETING = 'completing' +RAID_COMPLETED = 'completed' +RAID_FAILED = 'failed' + + +def _get_raid_adapter(node): + """Get the RAID adapter info on a RAID controller. + + :param node: an ironic node object. + :returns: RAID adapter dictionary, None otherwise. + :raises: IRMCOperationError on an error from python-scciclient. + """ + irmc_info = node.driver_info + LOG.info('iRMC driver is gathering RAID adapter info for node %s', + node.uuid) + try: + return client.elcm.get_raid_adapter(irmc_info) + except client.elcm.ELCMProfileNotFound: + reason = ('Cannot find any RAID profile in "%s"' % node.uuid) + raise exception.IRMCOperationError(operation='RAID config', + error=reason) + + +def _get_fgi_status(report, node_uuid): + """Get a dict FGI(Foreground initialization) status on a RAID controller. + + :param report: SCCI report information. + :returns: FGI status on success, None if SCCIInvalidInputError and + waiting status if SCCIRAIDNotReady. + """ + try: + return client.scci.get_raid_fgi_status(report) + except client.scci.SCCIInvalidInputError: + LOG.warning('ServerViewRAID not available in %(node)s', + {'node': node_uuid}) + except client.scci.SCCIRAIDNotReady: + return RAID_COMPLETING + + +def _get_physical_disk(node): + """Get physical disks info on a RAID controller. + + This method only support to create the RAID configuration + on the RAIDAdapter 0. + + :param node: an ironic node object. + :returns: dict of physical disks on RAID controller. + """ + + physical_disk_dict = {} + raid_adapter = _get_raid_adapter(node) + physical_disks = raid_adapter['Server']['HWConfigurationIrmc'][ + 'Adapters']['RAIDAdapter'][0]['PhysicalDisks'] + + if physical_disks: + for disks in physical_disks['PhysicalDisk']: + physical_disk_dict.update({disks['Slot']: disks['Type']}) + + return physical_disk_dict + + +def _create_raid_adapter(node): + """Create RAID adapter info on a RAID controller. + + :param node: an ironic node object. + :raises: IRMCOperationError on an error from python-scciclient. + """ + + irmc_info = node.driver_info + target_raid_config = node.target_raid_config + + try: + return client.elcm.create_raid_configuration(irmc_info, + target_raid_config) + except client.elcm.ELCMProfileNotFound as exc: + LOG.error('iRMC driver failed with profile not found for node ' + '%(node_uuid)s. Reason: %(error)s.', + {'node_uuid': node.uuid, 'error': exc}) + raise exception.IRMCOperationError(operation='RAID config', + error=exc) + except client.scci.SCCIClientError as exc: + LOG.error('iRMC driver failed to create raid adapter info for node ' + '%(node_uuid)s. Reason: %(error)s.', + {'node_uuid': node.uuid, 'error': exc}) + raise exception.IRMCOperationError(operation='RAID config', + error=exc) + + +def _delete_raid_adapter(node): + """Delete the RAID adapter info on a RAID controller. + + :param node: an ironic node object. + :raises: IRMCOperationError if SCCI failed from python-scciclient. + """ + + irmc_info = node.driver_info + + try: + client.elcm.delete_raid_configuration(irmc_info) + except client.scci.SCCIClientError as exc: + LOG.error('iRMC driver failed to delete RAID configuration ' + 'for node %(node_uuid)s. Reason: %(error)s.', + {'node_uuid': node.uuid, 'error': exc}) + raise exception.IRMCOperationError(operation='RAID config', + error=exc) + + +def _commit_raid_config(task): + """Perform to commit RAID config into node.""" + + node = task.node + node_uuid = task.node.uuid + raid_config = {'logical_disks': []} + + raid_adapter = _get_raid_adapter(node) + + raid_adapter_info = raid_adapter['Server']['HWConfigurationIrmc'][ + 'Adapters']['RAIDAdapter'][0] + controller = raid_adapter_info['@AdapterId'] + raid_config['logical_disks'].append({'controller': controller}) + + logical_drives = raid_adapter_info['LogicalDrives']['LogicalDrive'] + for logical_drive in logical_drives: + raid_config['logical_disks'].append({'irmc_raid_info': { + 'logical_drive_number': logical_drive['@Number'], 'raid_level': + logical_drive['RaidLevel'], 'name': logical_drive['Name'], + ' size': logical_drive['Size']}}) + for physical_drive in \ + raid_adapter_info['PhysicalDisks']['PhysicalDisk']: + raid_config['logical_disks'].append({'physical_drives': { + 'physical_drive': physical_drive}}) + node.raid_config = raid_config + + raid_common.update_raid_info(node, node.raid_config) + LOG.info('RAID config is created successfully on node %s', + node_uuid) + + return states.CLEANWAIT + + +def _validate_logical_drive_capacity(disk, valid_disk_slots): + physical_disks = valid_disk_slots['PhysicalDisk'] + size_gb = {} + all_volume_list = [] + physical_disk_list = [] + + for size in physical_disks: + size_gb.update({size['@Number']: size['Size']['#text']}) + all_volume_list.append(size['Size']['#text']) + + factor = RAID_LEVELS[disk['raid_level']]['factor'] + + if disk.get('physical_disks'): + selected_disks = \ + [physical_disk for physical_disk in disk['physical_disks']] + for volume in selected_disks: + physical_disk_list.append(size_gb[volume]) + if disk['raid_level'] == '10': + valid_capacity = \ + min(physical_disk_list) * (len(physical_disk_list) / 2) + else: + valid_capacity = \ + min(physical_disk_list) * (len(physical_disk_list) - factor) + else: + valid_capacity = \ + min(all_volume_list) * \ + ((RAID_LEVELS[disk['raid_level']]['min_disks']) - factor) + + if disk['size_gb'] > valid_capacity: + raise exception.InvalidParameterValue( + 'Insufficient disk capacity with %s GB' % disk['size_gb']) + + if disk['size_gb'] == valid_capacity: + disk['size_gb'] = 'MAX' + + +def _validate_physical_disks(node, logical_disks): + """Validate physical disks on a RAID configuration. + + :param node: an ironic node object. + :param logical_disks: RAID info to set RAID configuration + :raises: IRMCOperationError on an error. + """ + raid_adapter = _get_raid_adapter(node) + physical_disk_dict = _get_physical_disk(node) + if raid_adapter is None: + reason = ('Cannot find any raid profile in "%s"' % node.uuid) + raise exception.IRMCOperationError(operation='RAID config', + error=reason) + if physical_disk_dict is None: + reason = ('Cannot find any physical disks in "%s"' % node.uuid) + raise exception.IRMCOperationError(operation='RAID config', + error=reason) + valid_disks = raid_adapter['Server']['HWConfigurationIrmc'][ + 'Adapters']['RAIDAdapter'][0]['PhysicalDisks'] + if valid_disks is None: + reason = ('Cannot find any HDD over in the node "%s"' % node.uuid) + raise exception.IRMCOperationError(operation='RAID config', + error=reason) + valid_disk_slots = [slot['Slot'] for slot in valid_disks['PhysicalDisk']] + remain_valid_disk_slots = list(valid_disk_slots) + number_of_valid_disks = len(valid_disk_slots) + used_valid_disk_slots = [] + + for disk in logical_disks: + # Check raid_level value in the target_raid_config of node + if disk.get('raid_level') not in RAID_LEVELS: + reason = ('RAID level is not supported: "%s"' + % disk.get('raid_level')) + raise exception.IRMCOperationError(operation='RAID config', + error=reason) + + min_disk_value = RAID_LEVELS[disk['raid_level']]['min_disks'] + max_disk_value = RAID_LEVELS[disk['raid_level']]['max_disks'] + remain_valid_disks = number_of_valid_disks - min_disk_value + number_of_valid_disks = number_of_valid_disks - min_disk_value + + if remain_valid_disks < 0: + reason = ('Physical disks do not enough slots for raid "%s"' + % disk['raid_level']) + raise exception.IRMCOperationError(operation='RAID config', + error=reason) + + if 'physical_disks' in disk: + type_of_disks = [] + number_of_physical_disks = len(disk['physical_disks']) + # Check number of physical disks along with raid level + if number_of_physical_disks > max_disk_value: + reason = ("Too many disks requested for RAID level %(level)s, " + "maximum is %(max)s", + {'level': disk['raid_level'], 'max': max_disk_value}) + raise exception.InvalidParameterValue(err=reason) + if number_of_physical_disks < min_disk_value: + reason = ("Not enough disks requested for RAID level " + "%(level)s, minimum is %(min)s ", + {'level': disk['raid_level'], 'min': min_disk_value}) + raise exception.IRMCOperationError(operation='RAID config', + error=reason) + # Check physical disks in valid disk slots + for phys_disk in disk['physical_disks']: + if int(phys_disk) not in valid_disk_slots: + reason = ("Incorrect physical disk %(disk)s, correct are " + "%(valid)s", + {'disk': phys_disk, 'valid': valid_disk_slots}) + raise exception.IRMCOperationError(operation='RAID config', + error=reason) + type_of_disks.append(physical_disk_dict[int(phys_disk)]) + if physical_disk_dict[int(phys_disk)] != type_of_disks[0]: + reason = ('Cannot create RAID configuration with ' + 'different hard drives type %s' + % physical_disk_dict[int(phys_disk)]) + raise exception.IRMCOperationError(operation='RAID config', + error=reason) + # Check physical disk values with used disk slots + if int(phys_disk) in used_valid_disk_slots: + reason = ("Disk %s is already used in a RAID configuration" + % disk['raid_level']) + raise exception.IRMCOperationError(operation='RAID config', + error=reason) + + used_valid_disk_slots.append(int(phys_disk)) + remain_valid_disk_slots.remove(int(phys_disk)) + + if disk['size_gb'] != 'MAX': + # Validate size_gb value input + _validate_logical_drive_capacity(disk, valid_disks) + + +class IRMCRAID(base.RAIDInterface): + + def get_properties(self): + """Return the properties of the interface.""" + return irmc_common.COMMON_PROPERTIES + + @METRICS.timer('IRMCRAID.create_configuration') + @base.clean_step(priority=0, argsinfo={ + 'create_root_volume': { + 'description': ('This specifies whether to create the root volume.' + 'Defaults to `True`.' + ), + 'required': False + }, + 'create_nonroot_volumes': { + 'description': ('This specifies whether to create the non-root ' + 'volumes. ' + 'Defaults to `True`.' + ), + 'required': False + } + }) + def create_configuration(self, task, + create_root_volume=True, + create_nonroot_volumes=True): + """Create the RAID configuration. + + This method creates the RAID configuration on the given node. + + :param task: a TaskManager instance containing the node to act on. + :param create_root_volume: If True, a root volume is created + during RAID configuration. Otherwise, no root volume is + created. Default is True. + :param create_nonroot_volumes: If True, non-root volumes are + created. If False, no non-root volumes are created. Default + is True. + :returns: states.CLEANWAIT if RAID configuration is in progress + asynchronously. + :raises: MissingParameterValue, if node.target_raid_config is missing + or empty. + :raises: IRMCOperationError on an error from scciclient + """ + + node = task.node + + if not node.target_raid_config: + raise exception.MissingParameterValue( + 'Missing the target_raid_config in node %s' % node.uuid) + + target_raid_config = node.target_raid_config.copy() + + logical_disks = target_raid_config['logical_disks'] + for log_disk in logical_disks: + if log_disk.get('raid_level'): + log_disk['raid_level'] = six.text_type( + log_disk['raid_level']).replace('+', '') + + # Validate physical disks on Fujitsu BM Server + _validate_physical_disks(node, logical_disks) + + # Executing raid configuration on Fujitsu BM Server + _create_raid_adapter(node) + + return _commit_raid_config(task) + + @METRICS.timer('IRMCRAID.delete_configuration') + @base.clean_step(priority=0) + def delete_configuration(self, task): + """Delete the RAID configuration. + + :param task: a TaskManager instance containing the node to act on. + :returns: states.CLEANWAIT if deletion is in progress + asynchronously or None if it is complete. + """ + node = task.node + node_uuid = task.node.uuid + + # Default delete everything raid configuration in BM Server + _delete_raid_adapter(node) + node.raid_config = {} + node.save() + LOG.info('RAID config is deleted successfully on node %(node_id)s.' + 'RAID config will clear and return (cfg)s value', + {'node_id': node_uuid, 'cfg': node.raid_config}) + + @METRICS.timer('IRMCRAID._query_raid_config_fgi_status') + @periodics.periodic( + spacing=CONF.irmc.query_raid_config_fgi_status_interval) + def _query_raid_config_fgi_status(self, manager, context): + """Periodic tasks to check the progress of running RAID config.""" + + filters = {'reserved': False, 'provision_state': states.CLEANWAIT, + 'maintenance': False} + fields = ['raid_config'] + node_list = manager.iter_nodes(fields=fields, filters=filters) + for (node_uuid, driver, raid_config) in node_list: + try: + lock_purpose = 'checking async RAID configuration tasks' + with task_manager.acquire(context, node_uuid, + purpose=lock_purpose, + shared=True) as task: + node = task.node + node_uuid = task.node.uuid + if not isinstance(task.driver.raid, IRMCRAID): + continue + if task.node.target_raid_config is None: + continue + if not raid_config or raid_config.get('fgi_status'): + continue + task.upgrade_lock() + if node.provision_state != states.CLEANWAIT: + continue + # Avoid hitting clean_callback_timeout expiration + node.touch_provisioning() + + try: + report = irmc_common.get_irmc_report(node) + except client.scci.SCCIInvalidInputError: + raid_config.update({'fgi_status': RAID_FAILED}) + raid_common.update_raid_info(node, raid_config) + self._set_clean_failed(task, RAID_FAILED) + continue + except client.scci.SCCIClientError: + raid_config.update({'fgi_status': RAID_FAILED}) + raid_common.update_raid_info(node, raid_config) + self._set_clean_failed(task, RAID_FAILED) + continue + + fgi_status_dict = _get_fgi_status(report, node_uuid) + # Note(trungnv): Allow to check until RAID mechanism to be + # completed with RAID information in report. + if fgi_status_dict == 'completing': + continue + if not fgi_status_dict: + raid_config.update({'fgi_status': RAID_FAILED}) + raid_common.update_raid_info(node, raid_config) + self._set_clean_failed(task, fgi_status_dict) + continue + if all(fgi_status == 'Idle' for fgi_status in + fgi_status_dict.values()): + raid_config.update({'fgi_status': RAID_COMPLETED}) + raid_common.update_raid_info(node, raid_config) + LOG.info('RAID configuration has completed on ' + 'node %(node)s with fgi_status is %(fgi)s', + {'node': node_uuid, 'fgi': RAID_COMPLETED}) + irmc_common.resume_cleaning(task) + + except exception.NodeNotFound: + LOG.info('During query_raid_config_job_status, node ' + '%(node)s was not found raid_config and presumed ' + 'deleted by another process.', {'node': node_uuid}) + except exception.NodeLocked: + LOG.info('During query_raid_config_job_status, node ' + '%(node)s was already locked by another process. ' + 'Skip.', {'node': node_uuid}) + + def _set_clean_failed(self, task, fgi_status_dict): + LOG.error('RAID configuration task failed for node %(node)s. ' + 'with FGI status is: %(fgi)s. ', + {'node': task.node.uuid, 'fgi': fgi_status_dict}) + fgi_message = 'ServerViewRAID not available in Baremetal Server' + task.node.last_error = fgi_message + task.process_event('fail') diff --git a/ironic/tests/unit/drivers/modules/irmc/test_periodic_task.py b/ironic/tests/unit/drivers/modules/irmc/test_periodic_task.py new file mode 100644 index 0000000000..ec0d33ce78 --- /dev/null +++ b/ironic/tests/unit/drivers/modules/irmc/test_periodic_task.py @@ -0,0 +1,294 @@ +# Copyright 2018 FUJITSU LIMITED +# +# Licensed under the Apache License, Version 2.0 (the "License"); you may +# not use this file except in compliance with the License. You may obtain +# a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT +# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the +# License for the specific language governing permissions and limitations +# under the License. + +""" +Test class for iRMC periodic tasks +""" + +import mock +from oslo_utils import uuidutils + +from ironic.conductor import task_manager +from ironic.drivers.modules.irmc import common as irmc_common +from ironic.drivers.modules.irmc import raid as irmc_raid +from ironic.drivers.modules import noop +from ironic.tests.unit.drivers.modules.irmc import test_common +from ironic.tests.unit.objects import utils as obj_utils + + +class iRMCPeriodicTaskTestCase(test_common.BaseIRMCTest): + + def setUp(self): + super(iRMCPeriodicTaskTestCase, self).setUp() + self.node_2 = obj_utils.create_test_node( + self.context, driver='fake-hardware', + uuid=uuidutils.generate_uuid()) + self.driver = mock.Mock(raid=irmc_raid.IRMCRAID()) + self.raid_config = { + 'logical_disks': [ + {'controller': 'RAIDAdapter0'}, + {'irmc_raid_info': + {' size': {'#text': 465, '@Unit': 'GB'}, + 'logical_drive_number': 0, + 'name': 'LogicalDrive_0', + 'raid_level': '1'}}]} + self.target_raid_config = { + 'logical_disks': [ + { + 'key': 'value' + }]} + + @mock.patch.object(irmc_common, 'get_irmc_report') + def test__query_raid_config_fgi_status_without_node( + self, report_mock): + mock_manager = mock.Mock() + node_list = [] + mock_manager.iter_nodes.return_value = node_list + raid_object = irmc_raid.IRMCRAID() + raid_object._query_raid_config_fgi_status(mock_manager, None) + self.assertEqual(0, report_mock.call_count) + + @mock.patch.object(irmc_common, 'get_irmc_report') + @mock.patch.object(task_manager, 'acquire', autospec=True) + def test__query_raid_config_fgi_status_without_raid_object( + self, mock_acquire, report_mock): + mock_manager = mock.Mock() + raid_config = self.raid_config + task = mock.Mock(node=self.node, driver=self.driver) + mock_acquire.return_value = mock.MagicMock( + __enter__=mock.MagicMock(return_value=task)) + node_list = [(self.node.uuid, 'irmc', raid_config)] + mock_manager.iter_nodes.return_value = node_list + task.driver.raid = noop.NoRAID() + raid_object = irmc_raid.IRMCRAID() + raid_object._query_raid_config_fgi_status(mock_manager, + self.context) + self.assertEqual(0, report_mock.call_count) + + @mock.patch.object(irmc_common, 'get_irmc_report') + @mock.patch.object(task_manager, 'acquire', autospec=True) + def test__query_raid_config_fgi_status_without_input( + self, mock_acquire, report_mock): + mock_manager = mock.Mock() + raid_config = self.raid_config + task = mock.Mock(node=self.node, driver=self.driver) + mock_acquire.return_value = mock.MagicMock( + __enter__=mock.MagicMock(return_value=task)) + node_list = [(self.node.uuid, 'irmc', raid_config)] + mock_manager.iter_nodes.return_value = node_list + # Set none target_raid_config input + task.node.target_raid_config = None + task.node.save() + task.driver.raid._query_raid_config_fgi_status(mock_manager, + self.context) + self.assertEqual(0, report_mock.call_count) + + @mock.patch.object(irmc_common, 'get_irmc_report') + @mock.patch.object(task_manager, 'acquire', autospec=True) + def test__query_raid_config_fgi_status_without_raid_config( + self, mock_acquire, report_mock): + mock_manager = mock.Mock() + raid_config = {} + task = mock.Mock(node=self.node, driver=self.driver) + mock_acquire.return_value = mock.MagicMock( + __enter__=mock.MagicMock(return_value=task)) + node_list = [(self.node.uuid, 'irmc', raid_config)] + mock_manager.iter_nodes.return_value = node_list + task.driver.raid._query_raid_config_fgi_status(mock_manager, + self.context) + self.assertEqual(0, report_mock.call_count) + + @mock.patch.object(irmc_common, 'get_irmc_report') + @mock.patch.object(task_manager, 'acquire', autospec=True) + def test__query_raid_config_fgi_status_without_fgi_status( + self, mock_acquire, report_mock): + mock_manager = mock.Mock() + raid_config = { + 'logical_disks': [ + {'controller': 'RAIDAdapter0'}, + {'irmc_raid_info': + {' size': {'#text': 465, '@Unit': 'GB'}, + 'logical_drive_number': 0, + 'name': 'LogicalDrive_0', + 'raid_level': '1'}}]} + task = mock.Mock(node=self.node, driver=self.driver) + mock_acquire.return_value = mock.MagicMock( + __enter__=mock.MagicMock(return_value=task)) + node_list = [(self.node.uuid, 'irmc', raid_config)] + mock_manager.iter_nodes.return_value = node_list + task.driver.raid._query_raid_config_fgi_status(mock_manager, + self.context) + self.assertEqual(0, report_mock.call_count) + + @mock.patch.object(irmc_common, 'get_irmc_report') + @mock.patch.object(task_manager, 'acquire', autospec=True) + def test__query_raid_config_fgi_status_other_clean_state( + self, mock_acquire, report_mock): + mock_manager = mock.Mock() + raid_config = self.raid_config + task = mock.Mock(node=self.node, driver=self.driver) + mock_acquire.return_value = mock.MagicMock( + __enter__=mock.MagicMock(return_value=task)) + node_list = [(self.node.uuid, 'irmc', raid_config)] + mock_manager.iter_nodes.return_value = node_list + # Set provision state value + task.node.provision_state = 'cleaning' + task.node.save() + task.driver.raid._query_raid_config_fgi_status(mock_manager, + self.context) + self.assertEqual(0, report_mock.call_count) + + @mock.patch('ironic.drivers.modules.irmc.raid.IRMCRAID._set_clean_failed') + @mock.patch('ironic.drivers.modules.irmc.raid._get_fgi_status') + @mock.patch.object(irmc_common, 'get_irmc_report') + @mock.patch.object(task_manager, 'acquire', autospec=True) + def test__query_raid_config_fgi_status_completing_status( + self, mock_acquire, report_mock, fgi_mock, clean_fail_mock): + mock_manager = mock.Mock() + fgi_mock.return_value = 'completing' + node_list = [(self.node.uuid, 'irmc', self.raid_config)] + mock_manager.iter_nodes.return_value = node_list + task = mock.Mock(node=self.node, driver=self.driver) + mock_acquire.return_value = mock.MagicMock( + __enter__=mock.MagicMock(return_value=task)) + # Set provision state value + task.node.provision_state = 'clean wait' + task.node.target_raid_config = self.target_raid_config + task.node.raid_config = self.raid_config + task.node.save() + + task.driver.raid._query_raid_config_fgi_status(mock_manager, + self.context) + self.assertEqual(0, clean_fail_mock.call_count) + report_mock.assert_called_once_with(task.node) + fgi_mock.assert_called_once_with(report_mock.return_value, + self.node.uuid) + + @mock.patch('ironic.drivers.modules.irmc.raid.IRMCRAID._set_clean_failed') + @mock.patch('ironic.drivers.modules.irmc.raid._get_fgi_status') + @mock.patch.object(irmc_common, 'get_irmc_report') + @mock.patch.object(task_manager, 'acquire', autospec=True) + def test__query_raid_config_fgi_status_with_clean_fail( + self, mock_acquire, report_mock, fgi_mock, clean_fail_mock): + mock_manager = mock.Mock() + raid_config = self.raid_config + fgi_mock.return_value = None + fgi_status_dict = None + task = mock.Mock(node=self.node, driver=self.driver) + mock_acquire.return_value = mock.MagicMock( + __enter__=mock.MagicMock(return_value=task)) + node_list = [(self.node.uuid, 'irmc', raid_config)] + mock_manager.iter_nodes.return_value = node_list + # Set provision state value + task.node.provision_state = 'clean wait' + task.node.target_raid_config = self.target_raid_config + task.node.raid_config = self.raid_config + task.node.save() + task.driver.raid._query_raid_config_fgi_status(mock_manager, + self.context) + clean_fail_mock.assert_called_once_with(task, fgi_status_dict) + report_mock.assert_called_once_with(task.node) + fgi_mock.assert_called_once_with(report_mock.return_value, + self.node.uuid) + + @mock.patch.object(irmc_common, 'resume_cleaning') + @mock.patch('ironic.drivers.modules.irmc.raid.IRMCRAID._set_clean_failed') + @mock.patch('ironic.drivers.modules.irmc.raid._get_fgi_status') + @mock.patch.object(irmc_common, 'get_irmc_report') + @mock.patch.object(task_manager, 'acquire', autospec=True) + def test__query_raid_config_fgi_status_with_complete_cleaning( + self, mock_acquire, report_mock, fgi_mock, clean_fail_mock, + clean_mock): + mock_manager = mock.Mock() + raid_config = self.raid_config + fgi_mock.return_value = {'0': 'Idle', '1': 'Idle'} + task = mock.Mock(node=self.node, driver=self.driver) + mock_acquire.return_value = mock.MagicMock( + __enter__=mock.MagicMock(return_value=task)) + node_list = [(self.node.uuid, 'irmc', raid_config)] + mock_manager.iter_nodes.return_value = node_list + # Set provision state value + task.node.provision_state = 'clean wait' + task.node.target_raid_config = self.target_raid_config + task.node.save() + task.driver.raid._query_raid_config_fgi_status(mock_manager, + self.context) + self.assertEqual(0, clean_fail_mock.call_count) + report_mock.assert_called_once_with(task.node) + fgi_mock.assert_called_once_with(report_mock.return_value, + self.node.uuid) + clean_mock.assert_called_once_with(task) + + @mock.patch.object(irmc_common, 'resume_cleaning') + @mock.patch('ironic.drivers.modules.irmc.raid.IRMCRAID._set_clean_failed') + @mock.patch('ironic.drivers.modules.irmc.raid._get_fgi_status') + @mock.patch.object(irmc_common, 'get_irmc_report') + @mock.patch.object(task_manager, 'acquire', autospec=True) + def test__query_raid_config_fgi_status_with_two_nodes_without_raid_config( + self, mock_acquire, report_mock, fgi_mock, clean_fail_mock, + clean_mock): + mock_manager = mock.Mock() + raid_config = self.raid_config + raid_config_2 = {} + fgi_mock.return_value = {'0': 'Idle', '1': 'Idle'} + task = mock.Mock(node=self.node, driver=self.driver) + mock_acquire.return_value = mock.MagicMock( + __enter__=mock.MagicMock(return_value=task)) + node_list = [(self.node_2.uuid, 'irmc', raid_config_2), + (self.node.uuid, 'irmc', raid_config)] + mock_manager.iter_nodes.return_value = node_list + # Set provision state value + task.node.provision_state = 'clean wait' + task.node.target_raid_config = self.target_raid_config + task.node.save() + task.driver.raid._query_raid_config_fgi_status(mock_manager, + self.context) + self.assertEqual(0, clean_fail_mock.call_count) + report_mock.assert_called_once_with(task.node) + fgi_mock.assert_called_once_with(report_mock.return_value, + self.node.uuid) + clean_mock.assert_called_once_with(task) + + @mock.patch.object(irmc_common, 'resume_cleaning') + @mock.patch('ironic.drivers.modules.irmc.raid.IRMCRAID._set_clean_failed') + @mock.patch('ironic.drivers.modules.irmc.raid._get_fgi_status') + @mock.patch.object(irmc_common, 'get_irmc_report') + @mock.patch.object(task_manager, 'acquire', autospec=True) + def test__query_raid_config_fgi_status_with_two_nodes_with_fgi_status_none( + self, mock_acquire, report_mock, fgi_mock, clean_fail_mock, + clean_mock): + mock_manager = mock.Mock() + raid_config = self.raid_config + raid_config_2 = self.raid_config.copy() + fgi_status_dict = {} + fgi_mock.side_effect = [{}, {'0': 'Idle', '1': 'Idle'}] + node_list = [(self.node_2.uuid, 'fake-hardware', raid_config_2), + (self.node.uuid, 'irmc', raid_config)] + mock_manager.iter_nodes.return_value = node_list + task = mock.Mock(node=self.node_2, driver=self.driver) + mock_acquire.return_value = mock.MagicMock( + __enter__=mock.MagicMock(return_value=task)) + task.node.provision_state = 'clean wait' + task.node.target_raid_config = self.target_raid_config + task.node.save() + task.driver.raid._query_raid_config_fgi_status(mock_manager, + self.context) + report_mock.assert_has_calls( + [mock.call(task.node), mock.call(task.node)]) + fgi_mock.assert_has_calls([mock.call(report_mock.return_value, + self.node_2.uuid), + mock.call(report_mock.return_value, + self.node_2.uuid)]) + clean_fail_mock.assert_called_once_with(task, fgi_status_dict) + clean_mock.assert_called_once_with(task) diff --git a/ironic/tests/unit/drivers/modules/irmc/test_raid.py b/ironic/tests/unit/drivers/modules/irmc/test_raid.py new file mode 100644 index 0000000000..8dc2421eda --- /dev/null +++ b/ironic/tests/unit/drivers/modules/irmc/test_raid.py @@ -0,0 +1,809 @@ +# Copyright 2018 FUJITSU LIMITED +# +# Licensed under the Apache License, Version 2.0 (the "License"); you may +# not use this file except in compliance with the License. You may obtain +# a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT +# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the +# License for the specific language governing permissions and limitations +# under the License. + +""" +Test class for IRMC RAID configuration +""" + +import mock + +from ironic.common import exception +from ironic.conductor import task_manager +from ironic.drivers.modules.irmc import raid +from ironic.tests.unit.drivers.modules.irmc import test_common + + +class IRMCRaidConfigurationInternalMethodsTestCase(test_common.BaseIRMCTest): + + def setUp(self): + super(IRMCRaidConfigurationInternalMethodsTestCase, self).setUp() + self.raid_adapter_profile = { + "Server": { + "HWConfigurationIrmc": { + "Adapters": { + "RAIDAdapter": [ + { + "@AdapterId": "RAIDAdapter0", + "@ConfigurationType": "Addressing", + "Arrays": None, + "LogicalDrives": None, + "PhysicalDisks": { + "PhysicalDisk": [ + { + "@Number": "0", + "@Action": "None", + "Slot": 0, + }, + { + "@Number": "1", + "@Action": "None", + "Slot": 1 + }, + { + "@Number": "2", + "@Action": "None", + "Slot": 2 + }, + { + "@Number": "3", + "@Action": "None", + "Slot": 3 + } + ] + } + } + ] + } + } + } + } + + self.valid_disk_slots = { + "PhysicalDisk": [ + { + "@Number": "0", + "Slot": 0, + "Size": { + "@Unit": "GB", + "#text": 1000 + } + }, + { + "@Number": "1", + "Slot": 1, + "Size": { + "@Unit": "GB", + "#text": 1000 + } + }, + { + "@Number": "2", + "Slot": 2, + "Size": { + "@Unit": "GB", + "#text": 1000 + } + }, + { + "@Number": "3", + "Slot": 3, + "Size": { + "@Unit": "GB", + "#text": 1000 + } + }, + { + "@Number": "4", + "Slot": 4, + "Size": { + "@Unit": "GB", + "#text": 1000 + } + }, + { + "@Number": "5", + "Slot": 5, + "Size": { + "@Unit": "GB", + "#text": 1000 + } + }, + { + "@Number": "6", + "Slot": 6, + "Size": { + "@Unit": "GB", + "#text": 1000 + } + }, + { + "@Number": "7", + "Slot": 7, + "Size": { + "@Unit": "GB", + "#text": 1000 + } + } + ] + } + + @mock.patch('ironic.drivers.modules.irmc.raid._get_physical_disk') + @mock.patch('ironic.drivers.modules.irmc.raid._get_raid_adapter') + def test___fail_validation_with_none_raid_adapter_profile( + self, get_raid_adapter_mock, get_physical_disk_mock): + get_raid_adapter_mock.return_value = None + target_raid_config = { + "logical_disks": [ + { + "size_gb": "50", + "raid_level": "0" + } + ] + } + + with task_manager.acquire(self.context, self.node.uuid, + shared=True) as task: + self.assertRaises(exception.IRMCOperationError, + raid._validate_physical_disks, + task.node, target_raid_config['logical_disks']) + + @mock.patch('ironic.drivers.modules.irmc.raid._get_physical_disk') + @mock.patch('ironic.drivers.modules.irmc.raid._get_raid_adapter') + def test___fail_validation_without_raid_level( + self, get_raid_adapter_mock, get_physical_disk_mock): + get_raid_adapter_mock.return_value = self.raid_adapter_profile + target_raid_config = { + "logical_disks": [ + { + "size_gb": "50" + } + ] + } + + with task_manager.acquire(self.context, self.node.uuid, + shared=True) as task: + self.assertRaises(exception.IRMCOperationError, + raid._validate_physical_disks, + task.node, target_raid_config['logical_disks']) + + @mock.patch('ironic.drivers.modules.irmc.raid._get_physical_disk') + @mock.patch('ironic.drivers.modules.irmc.raid._get_raid_adapter') + def test___fail_validation_with_raid_level_is_none(self, + get_raid_adapter_mock, + get_physical_disk_mock): + get_raid_adapter_mock.return_value = self.raid_adapter_profile + target_raid_config = { + "logical_disks": [ + { + "size_gb": "50", + "raid_level": "" + } + ] + } + + with task_manager.acquire(self.context, self.node.uuid, + shared=True) as task: + self.assertRaises(exception.IRMCOperationError, + raid._validate_physical_disks, + task.node, target_raid_config['logical_disks']) + + @mock.patch('ironic.drivers.modules.irmc.raid._get_physical_disk') + @mock.patch('ironic.drivers.modules.irmc.raid._get_raid_adapter') + def test__fail_validation_without_physical_disks( + self, get_raid_adapter_mock, get_physical_disk_mock): + get_raid_adapter_mock.return_value = { + "Server": { + "HWConfigurationIrmc": { + "Adapters": { + "RAIDAdapter": [ + { + "@AdapterId": "RAIDAdapter0", + "@ConfigurationType": "Addressing", + "Arrays": None, + "LogicalDrives": None, + "PhysicalDisks": None + } + ] + } + } + } + } + + target_raid_config = { + "logical_disks": [ + { + "size_gb": "50", + "raid_level": "1" + } + ] + } + + with task_manager.acquire(self.context, self.node.uuid, + shared=True) as task: + self.assertRaises(exception.IRMCOperationError, + raid._validate_physical_disks, + task.node, target_raid_config['logical_disks']) + + @mock.patch('ironic.drivers.modules.irmc.raid._get_physical_disk') + @mock.patch('ironic.drivers.modules.irmc.raid._get_raid_adapter') + def test___fail_validation_with_raid_level_outside_list( + self, get_raid_adapter_mock, get_physical_disk_mock): + get_raid_adapter_mock.return_value = self.raid_adapter_profile + target_raid_config = { + "logical_disks": [ + { + "size_gb": "50", + "raid_level": "2" + } + ] + } + + with task_manager.acquire(self.context, self.node.uuid, + shared=True) as task: + self.assertRaises(exception.IRMCOperationError, + raid._validate_physical_disks, + task.node, target_raid_config['logical_disks']) + + @mock.patch( + 'ironic.drivers.modules.irmc.raid._validate_logical_drive_capacity') + @mock.patch('ironic.drivers.modules.irmc.raid._get_physical_disk') + @mock.patch('ironic.drivers.modules.irmc.raid._get_raid_adapter') + def test__fail_validation_with_not_enough_valid_disks( + self, get_raid_adapter_mock, get_physical_disk_mock, + capacity_mock): + get_raid_adapter_mock.return_value = self.raid_adapter_profile + target_raid_config = { + "logical_disks": [ + { + "size_gb": "50", + "raid_level": "5" + }, + { + "size_gb": "50", + "raid_level": "1" + }, + ] + } + + with task_manager.acquire(self.context, self.node.uuid, + shared=True) as task: + self.assertRaises(exception.IRMCOperationError, + raid._validate_physical_disks, + task.node, target_raid_config['logical_disks']) + + @mock.patch('ironic.drivers.modules.irmc.raid._get_physical_disk') + @mock.patch('ironic.drivers.modules.irmc.raid._get_raid_adapter') + def test__fail_validation_with_physical_disk_insufficient( + self, get_raid_adapter_mock, get_physical_disk_mock): + get_raid_adapter_mock.return_value = self.raid_adapter_profile + target_raid_config = { + "logical_disks": [ + { + "size_gb": "50", + "raid_level": "1", + "physical_disks": [ + "0", + "1", + "2" + ] + }, + ] + } + + with task_manager.acquire(self.context, self.node.uuid, + shared=True) as task: + self.assertRaises(exception.InvalidParameterValue, + raid._validate_physical_disks, + task.node, target_raid_config['logical_disks']) + + @mock.patch('ironic.drivers.modules.irmc.raid._get_physical_disk') + @mock.patch('ironic.drivers.modules.irmc.raid._get_raid_adapter') + def test__fail_validation_with_physical_disk_not_enough_disks( + self, get_raid_adapter_mock, get_physical_disk_mock): + get_raid_adapter_mock.return_value = self.raid_adapter_profile + target_raid_config = { + "logical_disks": [ + { + "size_gb": "50", + "raid_level": "5", + "physical_disks": [ + "0", + "1" + ] + }, + ] + } + + with task_manager.acquire(self.context, self.node.uuid, + shared=True) as task: + self.assertRaises(exception.IRMCOperationError, + raid._validate_physical_disks, + task.node, target_raid_config['logical_disks']) + + @mock.patch('ironic.drivers.modules.irmc.raid._get_physical_disk') + @mock.patch('ironic.drivers.modules.irmc.raid._get_raid_adapter') + def test__fail_validation_with_physical_disk_incorrect_valid_disks( + self, get_raid_adapter_mock, get_physical_disk_mock): + get_raid_adapter_mock.return_value = self.raid_adapter_profile + target_raid_config = { + "logical_disks": [ + { + "size_gb": "50", + "raid_level": "10", + "physical_disks": [ + "0", + "1", + "2", + "3", + "4" + ] + }, + ] + } + + with task_manager.acquire(self.context, self.node.uuid, + shared=True) as task: + self.assertRaises(exception.IRMCOperationError, + raid._validate_physical_disks, + task.node, target_raid_config['logical_disks']) + + @mock.patch('ironic.drivers.modules.irmc.raid._get_physical_disk') + @mock.patch('ironic.drivers.modules.irmc.raid._get_raid_adapter') + def test__fail_validation_with_physical_disk_outside_valid_disks_1( + self, get_raid_adapter_mock, get_physical_disk_mock): + get_raid_adapter_mock.return_value = self.raid_adapter_profile + target_raid_config = { + "logical_disks": [ + { + "size_gb": "50", + "raid_level": "1", + "physical_disks": [ + "4", + "5" + ] + }, + ] + } + + with task_manager.acquire(self.context, self.node.uuid, + shared=True) as task: + self.assertRaises(exception.IRMCOperationError, + raid._validate_physical_disks, + task.node, target_raid_config['logical_disks']) + + @mock.patch( + 'ironic.drivers.modules.irmc.raid._validate_logical_drive_capacity') + @mock.patch('ironic.drivers.modules.irmc.raid._get_physical_disk') + @mock.patch('ironic.drivers.modules.irmc.raid._get_raid_adapter') + def test__fail_validation_with_physical_disk_outside_valid_slots_2( + self, get_raid_adapter_mock, get_physical_disk_mock, + capacity_mock): + get_raid_adapter_mock.return_value = self.raid_adapter_profile + target_raid_config = { + "logical_disks": [ + { + "size_gb": "50", + "raid_level": "5", + "physical_disks": [ + "0", + "1", + "2" + ] + }, + { + "size_gb": "50", + "raid_level": "0", + "physical_disks": [ + "4" + ] + }, + ] + } + + with task_manager.acquire(self.context, self.node.uuid, + shared=True) as task: + self.assertRaises(exception.IRMCOperationError, + raid._validate_physical_disks, + task.node, target_raid_config['logical_disks']) + + @mock.patch( + 'ironic.drivers.modules.irmc.raid._validate_logical_drive_capacity') + @mock.patch('ironic.drivers.modules.irmc.raid._get_physical_disk') + @mock.patch('ironic.drivers.modules.irmc.raid._get_raid_adapter') + def test__fail_validation_with_duplicated_physical_disks( + self, get_raid_adapter_mock, get_physical_disk_mock, + capacity_mock): + get_raid_adapter_mock.return_value = self.raid_adapter_profile + target_raid_config = { + "logical_disks": [ + { + "size_gb": "50", + "raid_level": "1", + "physical_disks": [ + "0", + "1" + ] + }, + { + "size_gb": "50", + "raid_level": "1", + "physical_disks": [ + "1", + "2" + ] + }, + ] + } + + with task_manager.acquire(self.context, self.node.uuid, + shared=True) as task: + self.assertRaises(exception.IRMCOperationError, + raid._validate_physical_disks, + task.node, target_raid_config['logical_disks']) + + @mock.patch('ironic.drivers.modules.irmc.raid._get_raid_adapter') + def test__fail_validation_with_difference_physical_disks_type( + self, get_raid_adapter_mock): + get_raid_adapter_mock.return_value = { + "Server": { + "HWConfigurationIrmc": { + "Adapters": { + "RAIDAdapter": [ + { + "@AdapterId": "RAIDAdapter0", + "@ConfigurationType": "Addressing", + "Arrays": None, + "LogicalDrives": None, + "PhysicalDisks": { + "PhysicalDisk": [ + { + "@Number": "0", + "Slot": 0, + "Type": "HDD", + }, + { + "@Number": "1", + "Slot": 1, + "Type": "SSD", + } + ] + } + } + ] + } + } + } + } + target_raid_config = { + "logical_disks": [ + { + "size_gb": "50", + "raid_level": "1", + "physical_disks": [ + "0", + "1" + ] + } + ] + } + + with task_manager.acquire(self.context, self.node.uuid, + shared=True) as task: + self.assertRaises(exception.IRMCOperationError, + raid._validate_physical_disks, + task.node, target_raid_config['logical_disks']) + + def test__fail_validate_capacity_raid_0(self): + disk = { + "size_gb": 3000, + "raid_level": "0" + } + self.assertRaises(exception.InvalidParameterValue, + raid._validate_logical_drive_capacity, + disk, self.valid_disk_slots) + + def test__fail_validate_capacity_raid_1(self): + disk = { + "size_gb": 3000, + "raid_level": "1" + } + self.assertRaises(exception.InvalidParameterValue, + raid._validate_logical_drive_capacity, + disk, self.valid_disk_slots) + + def test__fail_validate_capacity_raid_5(self): + disk = { + "size_gb": 3000, + "raid_level": "5" + } + self.assertRaises(exception.InvalidParameterValue, + raid._validate_logical_drive_capacity, + disk, self.valid_disk_slots) + + def test__fail_validate_capacity_raid_6(self): + disk = { + "size_gb": 3000, + "raid_level": "6" + } + self.assertRaises(exception.InvalidParameterValue, + raid._validate_logical_drive_capacity, + disk, self.valid_disk_slots) + + def test__fail_validate_capacity_raid_10(self): + disk = { + "size_gb": 3000, + "raid_level": "10" + } + self.assertRaises(exception.InvalidParameterValue, + raid._validate_logical_drive_capacity, + disk, self.valid_disk_slots) + + def test__fail_validate_capacity_raid_50(self): + disk = { + "size_gb": 5000, + "raid_level": "50" + } + self.assertRaises(exception.InvalidParameterValue, + raid._validate_logical_drive_capacity, + disk, self.valid_disk_slots) + + def test__fail_validate_capacity_with_physical_disk(self): + disk = { + "size_gb": 4000, + "raid_level": "5", + "physical_disks": [ + "0", + "1", + "3", + "4" + ] + } + self.assertRaises(exception.InvalidParameterValue, + raid._validate_logical_drive_capacity, + disk, self.valid_disk_slots) + + @mock.patch('ironic.common.raid.update_raid_info') + @mock.patch('ironic.drivers.modules.irmc.raid.client') + def test__commit_raid_config_with_logical_drives(self, client_mock, + update_raid_info_mock): + client_mock.elcm.get_raid_adapter.return_value = { + "Server": { + "HWConfigurationIrmc": { + "Adapters": { + "RAIDAdapter": [ + { + "@AdapterId": "RAIDAdapter0", + "@ConfigurationType": "Addressing", + "Arrays": { + "Array": [ + { + "@Number": 0, + "@ConfigurationType": "Addressing", + "PhysicalDiskRefs": { + "PhysicalDiskRef": [ + { + "@Number": "0" + }, + { + "@Number": "1" + } + ] + } + } + ] + }, + "LogicalDrives": { + "LogicalDrive": [ + { + "@Number": 0, + "@Action": "None", + "RaidLevel": "1", + "Name": "LogicalDrive_0", + "Size": { + "@Unit": "GB", + "#text": 465 + }, + "ArrayRefs": { + "ArrayRef": [ + { + "@Number": 0 + } + ] + } + } + ] + }, + "PhysicalDisks": { + "PhysicalDisk": [ + { + "@Number": "0", + "@Action": "None", + "Slot": 0, + "PDStatus": "Operational" + }, + { + "@Number": "1", + "@Action": "None", + "Slot": 1, + "PDStatus": "Operational" + } + ] + } + } + ] + } + } + } + } + + expected_raid_config = [ + {'controller': 'RAIDAdapter0'}, + {'irmc_raid_info': {' size': {'#text': 465, '@Unit': 'GB'}, + 'logical_drive_number': 0, + 'name': 'LogicalDrive_0', + 'raid_level': '1'}}, + {'physical_drives': {'physical_drive': {'@Action': 'None', + '@Number': '0', + 'PDStatus': 'Operational', + 'Slot': 0}}}, + {'physical_drives': {'physical_drive': {'@Action': 'None', + '@Number': '1', + 'PDStatus': 'Operational', + 'Slot': 1}}}] + + with task_manager.acquire(self.context, self.node.uuid, + shared=True) as task: + raid._commit_raid_config(task) + client_mock.elcm.get_raid_adapter.assert_called_once_with( + task.node.driver_info) + update_raid_info_mock.assert_called_once_with( + task.node, task.node.raid_config) + self.assertEqual(task.node.raid_config['logical_disks'], + expected_raid_config) + + +class IRMCRaidConfigurationTestCase(test_common.BaseIRMCTest): + + def setUp(self): + super(IRMCRaidConfigurationTestCase, self).setUp() + self.config(enabled_raid_interfaces=['irmc']) + self.raid_adapter_profile = { + "Server": { + "HWConfigurationIrmc": { + "Adapters": { + "RAIDAdapter": [ + { + "@AdapterId": "RAIDAdapter0", + "@ConfigurationType": "Addressing", + "Arrays": None, + "LogicalDrives": None, + "PhysicalDisks": { + "PhysicalDisk": [ + { + "@Number": "0", + "@Action": "None", + "Slot": 0, + }, + { + "@Number": "1", + "@Action": "None", + "Slot": 1 + }, + { + "@Number": "2", + "@Action": "None", + "Slot": 2 + }, + { + "@Number": "3", + "@Action": "None", + "Slot": 3 + } + ] + } + } + ] + } + } + } + } + + def test_fail_create_raid_without_target_raid_config(self): + + with task_manager.acquire(self.context, self.node.uuid, + shared=True) as task: + + task.node.target_raid_config = {} + raid_configuration = raid.IRMCRAID() + + self.assertRaises(exception.MissingParameterValue, + raid_configuration.create_configuration, task) + + @mock.patch('ironic.drivers.modules.irmc.raid._validate_physical_disks') + @mock.patch('ironic.drivers.modules.irmc.raid._create_raid_adapter') + @mock.patch('ironic.drivers.modules.irmc.raid._commit_raid_config') + def test_create_raid_with_raid_1_and_0(self, commit_mock, + create_raid_mock, validation_mock): + expected_input = { + "logical_disks": [ + { + "raid_level": "10" + }, + ] + } + + with task_manager.acquire(self.context, self.node.uuid, + shared=True) as task: + task.node.target_raid_config = { + "logical_disks": [ + { + "raid_level": "1+0" + }, + ] + } + + task.driver.raid.create_configuration(task) + create_raid_mock.assert_called_once_with(task.node) + validation_mock.assert_called_once_with( + task.node, expected_input['logical_disks']) + commit_mock.assert_called_once_with(task) + + @mock.patch('ironic.drivers.modules.irmc.raid._validate_physical_disks') + @mock.patch('ironic.drivers.modules.irmc.raid._create_raid_adapter') + @mock.patch('ironic.drivers.modules.irmc.raid._commit_raid_config') + def test_create_raid_with_raid_5_and_0(self, commit_mock, + create_raid_mock, validation_mock): + expected_input = { + "logical_disks": [ + { + "raid_level": "50" + }, + ] + } + + with task_manager.acquire(self.context, self.node.uuid, + shared=True) as task: + task.node.target_raid_config = { + "logical_disks": [ + { + "raid_level": "5+0" + }, + ] + } + + task.driver.raid.create_configuration(task) + create_raid_mock.assert_called_once_with(task.node) + validation_mock.assert_called_once_with( + task.node, expected_input['logical_disks']) + commit_mock.assert_called_once_with(task) + + @mock.patch('ironic.drivers.modules.irmc.raid._delete_raid_adapter') + def test_delete_raid_configuration(self, delete_raid_mock): + + with task_manager.acquire(self.context, self.node.uuid, + shared=True) as task: + task.driver.raid.delete_configuration(task) + delete_raid_mock.assert_called_once_with(task.node) + + @mock.patch('ironic.drivers.modules.irmc.raid._delete_raid_adapter') + def test_delete_raid_configuration_return_cleared_raid_config( + self, delete_raid_mock): + with task_manager.acquire(self.context, self.node.uuid, + shared=True) as task: + + expected_raid_config = {} + + task.driver.raid.delete_configuration(task) + self.assertEqual(expected_raid_config, task.node.raid_config) + delete_raid_mock.assert_called_once_with(task.node) diff --git a/ironic/tests/unit/drivers/test_irmc.py b/ironic/tests/unit/drivers/test_irmc.py index 9489d5eea0..0f712c56c2 100644 --- a/ironic/tests/unit/drivers/test_irmc.py +++ b/ironic/tests/unit/drivers/test_irmc.py @@ -21,6 +21,7 @@ from ironic.drivers import irmc from ironic.drivers.modules import agent from ironic.drivers.modules import inspector from ironic.drivers.modules import ipmitool +from ironic.drivers.modules.irmc import raid from ironic.drivers.modules import iscsi_deploy from ironic.drivers.modules import noop from ironic.tests.unit.db import base as db_base @@ -40,7 +41,7 @@ class IRMCHardwareTestCase(db_base.DbTestCase): enabled_inspect_interfaces=['irmc'], enabled_management_interfaces=['irmc'], enabled_power_interfaces=['irmc', 'ipmitool'], - enabled_raid_interfaces=['no-raid', 'agent'], + enabled_raid_interfaces=['no-raid', 'agent', 'irmc'], enabled_rescue_interfaces=['no-rescue', 'agent']) def test_default_interfaces(self): @@ -132,3 +133,27 @@ class IRMCHardwareTestCase(db_base.DbTestCase): noop.NoRAID) self.assertIsInstance(task.driver.rescue, noop.NoRescue) + + def test_override_with_raid_configuration(self): + node = obj_utils.create_test_node( + self.context, driver='irmc', + deploy_interface='direct', + rescue_interface='agent', + raid_interface='irmc') + with task_manager.acquire(self.context, node.id) as task: + self.assertIsInstance(task.driver.boot, + irmc.boot.IRMCVirtualMediaBoot) + self.assertIsInstance(task.driver.console, + ipmitool.IPMISocatConsole) + self.assertIsInstance(task.driver.deploy, + agent.AgentDeploy) + self.assertIsInstance(task.driver.inspect, + irmc.inspect.IRMCInspect) + self.assertIsInstance(task.driver.management, + irmc.management.IRMCManagement) + self.assertIsInstance(task.driver.power, + irmc.power.IRMCPower) + self.assertIsInstance(task.driver.raid, + raid.IRMCRAID) + self.assertIsInstance(task.driver.rescue, + agent.AgentRescue) diff --git a/releasenotes/notes/irmc-manual-clean-create-raid-configuration-bccef8496520bf8c.yaml b/releasenotes/notes/irmc-manual-clean-create-raid-configuration-bccef8496520bf8c.yaml new file mode 100644 index 0000000000..6edd0b2d53 --- /dev/null +++ b/releasenotes/notes/irmc-manual-clean-create-raid-configuration-bccef8496520bf8c.yaml @@ -0,0 +1,6 @@ +--- +features: + - Adds out-of-band RAID configuration solution for the iRMC driver which + makes the functionality available via manual cleaning. + See `iRMC hardware type documentation `_ + for more details. diff --git a/setup.cfg b/setup.cfg index 831bba4d1f..eb81574e3a 100644 --- a/setup.cfg +++ b/setup.cfg @@ -122,6 +122,7 @@ ironic.hardware.interfaces.raid = agent = ironic.drivers.modules.agent:AgentRAID fake = ironic.drivers.modules.fake:FakeRAID idrac = ironic.drivers.modules.drac.raid:DracRAID + irmc = ironic.drivers.modules.irmc.raid:IRMCRAID no-raid = ironic.drivers.modules.noop:NoRAID ironic.hardware.interfaces.rescue =