diff --git a/driver-requirements.txt b/driver-requirements.txt index 5ee2943f02..8241d62503 100644 --- a/driver-requirements.txt +++ b/driver-requirements.txt @@ -4,7 +4,7 @@ # python projects they should package as optional dependencies for Ironic. # These are available on pypi -proliantutils>=2.6.0 +proliantutils>=2.7.0 pysnmp>=4.3.0,<5.0.0 python-ironic-inspector-client>=1.5.0 python-scciclient>=0.8.0 diff --git a/ironic/common/raid.py b/ironic/common/raid.py index 54a3419c09..3f503beb3c 100644 --- a/ironic/common/raid.py +++ b/ironic/common/raid.py @@ -126,3 +126,51 @@ def update_raid_info(node, raid_config): node.properties = properties node.save() + + +def filter_target_raid_config( + node, create_root_volume=True, create_nonroot_volumes=True): + """Filter the target raid config based on root volume creation + + This method can be used by any raid interface which wants to filter + out target raid config based on condition whether the root volume + will be created or not. + + :param node: a node object + :param create_root_volume: A boolean default value True governing + if the root volume is returned else root volumes will be filtered + out. + :param create_nonroot_volumes: A boolean default value True governing + if the non root volume is returned else non-root volumes will be + filtered out. + :raises: MissingParameterValue, if node.target_raid_config is missing + or was found to be empty after skipping root volume and/or non-root + volumes. + :returns: It will return filtered target_raid_config + """ + if not node.target_raid_config: + raise exception.MissingParameterValue( + _("Node %s has no target RAID configuration.") % node.uuid) + + target_raid_config = node.target_raid_config.copy() + + error_msg_list = [] + if not create_root_volume: + target_raid_config['logical_disks'] = [ + x for x in target_raid_config['logical_disks'] + if not x.get('is_root_volume')] + error_msg_list.append(_("skipping root volume")) + + if not create_nonroot_volumes: + target_raid_config['logical_disks'] = [ + x for x in target_raid_config['logical_disks'] + if x.get('is_root_volume')] + error_msg_list.append(_("skipping non-root volumes")) + + if not target_raid_config['logical_disks']: + error_msg = _(' and ').join(error_msg_list) + raise exception.MissingParameterValue( + _("Node %(node)s has empty target RAID configuration " + "after %(msg)s.") % {'node': node.uuid, 'msg': error_msg}) + + return target_raid_config diff --git a/ironic/drivers/ilo.py b/ironic/drivers/ilo.py index 4cf4a6317e..3540c6944c 100644 --- a/ironic/drivers/ilo.py +++ b/ironic/drivers/ilo.py @@ -22,6 +22,7 @@ from ironic.drivers.modules.ilo import console from ironic.drivers.modules.ilo import inspect from ironic.drivers.modules.ilo import management from ironic.drivers.modules.ilo import power +from ironic.drivers.modules.ilo import raid from ironic.drivers.modules.ilo import vendor from ironic.drivers.modules import inspector from ironic.drivers.modules import noop @@ -69,3 +70,15 @@ class IloHardware(generic.GenericHardware): def supported_vendor_interfaces(self): """List of supported power interfaces.""" return [vendor.VendorPassthru, noop.NoVendor] + + +class Ilo5Hardware(IloHardware): + """iLO5 hardware type. + + iLO5 hardware type is targeted for iLO5 based Proliant Gen10 servers. + """ + + @property + def supported_raid_interfaces(self): + """List of supported raid interfaces.""" + return [raid.Ilo5RAID, noop.NoRAID] diff --git a/ironic/drivers/modules/agent.py b/ironic/drivers/modules/agent.py index 020ebbc1f2..c9344ab153 100644 --- a/ironic/drivers/modules/agent.py +++ b/ironic/drivers/modules/agent.py @@ -698,32 +698,10 @@ class AgentRAID(base.RAIDInterface): 'create_nonroot_volumes': create_nonroot_volumes, 'target_raid_config': node.target_raid_config}) - if not node.target_raid_config: - raise exception.MissingParameterValue( - _("Node %s has no target RAID configuration.") % node.uuid) - - target_raid_config = node.target_raid_config.copy() - - error_msg_list = [] - if not create_root_volume: - target_raid_config['logical_disks'] = [ - x for x in target_raid_config['logical_disks'] - if not x.get('is_root_volume')] - error_msg_list.append(_("skipping root volume")) - - if not create_nonroot_volumes: - error_msg_list.append(_("skipping non-root volumes")) - - target_raid_config['logical_disks'] = [ - x for x in target_raid_config['logical_disks'] - if x.get('is_root_volume')] - - if not target_raid_config['logical_disks']: - error_msg = _(' and ').join(error_msg_list) - raise exception.MissingParameterValue( - _("Node %(node)s has empty target RAID configuration " - "after %(msg)s.") % {'node': node.uuid, 'msg': error_msg}) - + target_raid_config = raid.filter_target_raid_config( + node, + create_root_volume=create_root_volume, + create_nonroot_volumes=create_nonroot_volumes) # Rewrite it back to the node object, but no need to save it as # we need to just send this to the agent ramdisk. node.driver_internal_info['target_raid_config'] = target_raid_config diff --git a/ironic/drivers/modules/ilo/raid.py b/ironic/drivers/modules/ilo/raid.py new file mode 100644 index 0000000000..07d6951331 --- /dev/null +++ b/ironic/drivers/modules/ilo/raid.py @@ -0,0 +1,235 @@ +# Copyright 2018 Hewlett Packard Enterprise Development LP +# +# 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. + +""" +iLO5 RAID specific methods +""" + +from ironic_lib import metrics_utils +from oslo_log import log as logging +from oslo_utils import importutils + +from ironic.common import exception +from ironic.common.i18n import _ +from ironic.common import raid +from ironic.common import states +from ironic.conductor import utils as manager_utils +from ironic import conf +from ironic.drivers import base +from ironic.drivers.modules import deploy_utils +from ironic.drivers.modules.ilo import common as ilo_common + + +LOG = logging.getLogger(__name__) +CONF = conf.CONF +METRICS = metrics_utils.get_metrics_logger(__name__) + +ilo_error = importutils.try_import('proliantutils.exception') + + +class Ilo5RAID(base.RAIDInterface): + """Implementation of OOB RAIDInterface for iLO5.""" + + def get_properties(self): + """Return the properties of the interface.""" + return ilo_common.REQUIRED_PROPERTIES + + def _set_clean_failed(self, task, msg, exc): + LOG.error("RAID configuration job failed for node %(node)s. " + "Message: '%(message)s'.", + {'node': task.node.uuid, 'message': msg}) + task.node.last_error = msg + task.process_event('fail') + + def _set_driver_internal_true_value(self, task, *keys): + driver_internal_info = task.node.driver_internal_info + for key in keys: + driver_internal_info[key] = True + task.node.driver_internal_info = driver_internal_info + task.node.save() + + def _set_driver_internal_false_value(self, task, *keys): + driver_internal_info = task.node.driver_internal_info + for key in keys: + driver_internal_info[key] = False + task.node.driver_internal_info = driver_internal_info + task.node.save() + + def _pop_driver_internal_values(self, task, *keys): + driver_internal_info = task.node.driver_internal_info + for key in keys: + driver_internal_info.pop(key, None) + task.node.driver_internal_info = driver_internal_info + task.node.save() + + def _prepare_for_read_raid(self, task, raid_step): + deploy_opts = deploy_utils.build_agent_options(task.node) + task.driver.boot.prepare_ramdisk(task, deploy_opts) + manager_utils.node_power_action(task, states.REBOOT) + if raid_step == 'create_raid': + self._set_driver_internal_true_value( + task, 'ilo_raid_create_in_progress') + else: + self._set_driver_internal_true_value( + task, 'ilo_raid_delete_in_progress') + self._set_driver_internal_true_value(task, 'cleaning_reboot') + self._set_driver_internal_false_value(task, 'skip_current_clean_step') + + @METRICS.timer('Ilo5RAID.create_configuration') + @base.clean_step(priority=0, abortable=False, 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 a RAID configuration on a bare metal using agent ramdisk. + + This method creates a RAID configuration on the given node. + + :param task: a TaskManager instance. + :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. + :raises: MissingParameterValue, if node.target_raid_config is missing + or was found to be empty after skipping root volume and/or non-root + volumes. + :raises: NodeCleaningFailure, on failure to execute step. + """ + node = task.node + target_raid_config = raid.filter_target_raid_config( + node, create_root_volume=create_root_volume, + create_nonroot_volumes=create_nonroot_volumes) + driver_internal_info = node.driver_internal_info + driver_internal_info['target_raid_config'] = target_raid_config + LOG.debug("Calling OOB RAID create_configuration for node %(node)s " + "with the following target RAID configuration: %(target)s", + {'node': node.uuid, 'target': target_raid_config}) + ilo_object = ilo_common.get_ilo_object(node) + + try: + # Raid configuration in progress, checking status + if not driver_internal_info.get('ilo_raid_create_in_progress'): + ilo_object.create_raid_configuration(target_raid_config) + self._prepare_for_read_raid(task, 'create_raid') + return states.CLEANWAIT + else: + # Raid configuration is done, updating raid_config + raid_conf = ( + ilo_object.read_raid_configuration( + raid_config=target_raid_config)) + if len(raid_conf['logical_disks']): + raid.update_raid_info(node, raid_conf) + LOG.debug("Node %(uuid)s raid create clean step is done.", + {'uuid': node.uuid}) + self._pop_driver_internal_values( + task, 'ilo_raid_create_in_progress', + 'cleaning_reboot', 'skip_current_clean_step') + node.driver_internal_info = driver_internal_info + node.save() + else: + # Raid configuration failed + msg = "Unable to create raid" + self._pop_driver_internal_values( + task, 'ilo_raid_create_in_progress', + 'cleaning_reboot', 'skip_current_clean_step') + node.driver_internal_info = driver_internal_info + node.save() + raise exception.NodeCleaningFailure( + "Clean step create_configuration failed " + "on node %(node)s with error: %(err)s" % + {'node': node.uuid, 'err': msg}) + except ilo_error.IloError as ilo_exception: + operation = (_("Failed to create raid configuration on node %s") + % node.uuid) + self._pop_driver_internal_values(task, + 'ilo_raid_create_in_progress', + 'cleaning_reboot', + 'skip_current_clean_step') + node.driver_internal_info = driver_internal_info + node.save() + self._set_clean_failed(task, operation, ilo_exception) + + @METRICS.timer('Ilo5RAID.delete_configuration') + @base.clean_step(priority=0, abortable=False) + def delete_configuration(self, task): + """Delete the RAID configuration. + + :param task: a TaskManager instance containing the node to act on. + :raises: NodeCleaningFailure, on failure to execute step. + """ + node = task.node + LOG.debug("OOB RAID delete_configuration invoked for node %s.", + node.uuid) + driver_internal_info = node.driver_internal_info + ilo_object = ilo_common.get_ilo_object(node) + + try: + # Raid configuration in progress, checking status + if not driver_internal_info.get('ilo_raid_delete_in_progress'): + ilo_object.delete_raid_configuration() + self._prepare_for_read_raid(task, 'delete_raid') + return states.CLEANWAIT + else: + # Raid configuration is done, updating raid_config + raid_conf = ilo_object.read_raid_configuration() + if not len(raid_conf['logical_disks']): + node.raid_config = {} + LOG.debug("Node %(uuid)s raid delete clean step is done.", + {'uuid': node.uuid}) + self._pop_driver_internal_values( + task, 'ilo_raid_delete_in_progress', + 'cleaning_reboot', 'skip_current_clean_step') + node.driver_internal_info = driver_internal_info + node.save() + else: + # Raid configuration failed + msg = ("Unable to delete this logical disks: %s" % + raid_conf['logical_disks']) + self._pop_driver_internal_values( + task, 'ilo_raid_delete_in_progress', + 'cleaning_reboot', 'skip_current_clean_step') + node.driver_internal_info = driver_internal_info + node.save() + raise exception.NodeCleaningFailure( + "Clean step delete_configuration failed " + "on node %(node)s with error: %(err)s" % + {'node': node.uuid, 'err': msg}) + except ilo_error.IloLogicalDriveNotFoundError: + LOG.info("No logical drive found to delete on node %(node)s", + {'node': node.uuid}) + except ilo_error.IloError as ilo_exception: + operation = (_("Failed to delete raid configuration on node %s") + % node.uuid) + self._pop_driver_internal_values(task, + 'ilo_raid_delete_in_progress', + 'cleaning_reboot', + 'skip_current_clean_step') + node.driver_internal_info = driver_internal_info + node.save() + self._set_clean_failed(task, operation, ilo_exception) diff --git a/ironic/tests/unit/common/test_raid.py b/ironic/tests/unit/common/test_raid.py index 40ae9f400f..004af870d0 100644 --- a/ironic/tests/unit/common/test_raid.py +++ b/ironic/tests/unit/common/test_raid.py @@ -161,6 +161,21 @@ class ValidateRaidConfigurationTestCase(base.TestCase): class RaidPublicMethodsTestCase(db_base.DbTestCase): + def setUp(self): + super(RaidPublicMethodsTestCase, self).setUp() + self.target_raid_config = { + "logical_disks": [ + {'size_gb': 200, 'raid_level': 0, 'is_root_volume': True}, + {'size_gb': 200, 'raid_level': 5} + ]} + n = { + 'boot_interface': 'pxe', + 'deploy_interface': 'direct', + 'raid_interface': 'agent', + 'target_raid_config': self.target_raid_config, + } + self.node = obj_utils.create_test_node(self.context, **n) + def test_get_logical_disk_properties(self): with open(drivers_base.RAID_CONFIG_SCHEMA, 'r') as raid_schema_fobj: schema = json.load(raid_schema_fobj) @@ -186,7 +201,7 @@ class RaidPublicMethodsTestCase(db_base.DbTestCase): def _test_update_raid_info(self, current_config, capabilities=None): - node = obj_utils.create_test_node(self.context) + node = self.node if capabilities: properties = node.properties properties['capabilities'] = capabilities @@ -239,3 +254,37 @@ class RaidPublicMethodsTestCase(db_base.DbTestCase): self.assertRaises(exception.InvalidParameterValue, self._test_update_raid_info, current_config) + + def test_filter_target_raid_config(self): + result = raid.filter_target_raid_config(self.node) + self.assertEqual(self.node.target_raid_config, result) + + def test_filter_target_raid_config_skip_root(self): + result = raid.filter_target_raid_config( + self.node, create_root_volume=False) + exp_target_raid_config = { + "logical_disks": [{'size_gb': 200, 'raid_level': 5}]} + self.assertEqual(exp_target_raid_config, result) + + def test_filter_target_raid_config_skip_nonroot(self): + result = raid.filter_target_raid_config( + self.node, create_nonroot_volumes=False) + exp_target_raid_config = { + "logical_disks": [{'size_gb': 200, + 'raid_level': 0, + 'is_root_volume': True}]} + self.assertEqual(exp_target_raid_config, result) + + def test_filter_target_raid_config_no_target_raid_config_after_skipping( + self): + self.assertRaises(exception.MissingParameterValue, + raid.filter_target_raid_config, + self.node, create_root_volume=False, + create_nonroot_volumes=False) + + def test_filter_target_raid_config_empty_target_raid_config(self): + self.node.target_raid_config = {} + self.node.save() + self.assertRaises(exception.MissingParameterValue, + raid.filter_target_raid_config, + self.node) diff --git a/ironic/tests/unit/drivers/modules/ilo/test_raid.py b/ironic/tests/unit/drivers/modules/ilo/test_raid.py new file mode 100644 index 0000000000..34b859023d --- /dev/null +++ b/ironic/tests/unit/drivers/modules/ilo/test_raid.py @@ -0,0 +1,342 @@ +# Copyright 2018 Hewlett Packard Enterprise Development LP +# +# 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 Raid Interface used by iLO5.""" + +import mock +from oslo_utils import importutils + +from ironic.common import exception +from ironic.common import raid +from ironic.common import states +from ironic.conductor import task_manager +from ironic.conductor import utils as manager_utils +from ironic.drivers.modules import deploy_utils +from ironic.drivers.modules.ilo import common as ilo_common +from ironic.drivers.modules.ilo import raid as ilo_raid +from ironic.tests.unit.db import base as db_base +from ironic.tests.unit.db import utils as db_utils +from ironic.tests.unit.objects import utils as obj_utils + +ilo_error = importutils.try_import('proliantutils.exception') + +INFO_DICT = db_utils.get_test_ilo_info() + + +class Ilo5RAIDTestCase(db_base.DbTestCase): + + def setUp(self): + super(Ilo5RAIDTestCase, self).setUp() + self.driver = mock.Mock(raid=ilo_raid.Ilo5RAID()) + self.target_raid_config = { + "logical_disks": [ + {'size_gb': 200, 'raid_level': 0, 'is_root_volume': True}, + {'size_gb': 200, 'raid_level': 5} + ]} + self.clean_step = {'step': 'create_configuration', + 'interface': 'raid'} + n = { + 'driver': 'ilo5', + 'driver_info': INFO_DICT, + 'target_raid_config': self.target_raid_config, + 'clean_step': self.clean_step, + } + self.config(enabled_hardware_types=['ilo5'], + enabled_boot_interfaces=['ilo-virtual-media'], + enabled_console_interfaces=['ilo'], + enabled_deploy_interfaces=['iscsi'], + enabled_inspect_interfaces=['ilo'], + enabled_management_interfaces=['ilo'], + enabled_power_interfaces=['ilo'], + enabled_raid_interfaces=['ilo5']) + self.node = obj_utils.create_test_node(self.context, **n) + + @mock.patch.object(deploy_utils, 'build_agent_options', autospec=True) + @mock.patch.object(manager_utils, 'node_power_action', autospec=True) + def test__prepare_for_read_raid_create_raid( + self, mock_reboot, mock_build_opt): + with task_manager.acquire(self.context, self.node.uuid) as task: + mock_build_opt.return_value = [] + task.driver.raid._prepare_for_read_raid(task, 'create_raid') + self.assertTrue( + task.node.driver_internal_info.get( + 'ilo_raid_create_in_progress')) + self.assertTrue( + task.node.driver_internal_info.get( + 'cleaning_reboot')) + self.assertFalse( + task.node.driver_internal_info.get( + 'skip_current_clean_step')) + mock_reboot.assert_called_once_with(task, states.REBOOT) + + @mock.patch.object(deploy_utils, 'build_agent_options', autospec=True) + @mock.patch.object(manager_utils, 'node_power_action', autospec=True) + def test__prepare_for_read_raid_delete_raid( + self, mock_reboot, mock_build_opt): + with task_manager.acquire(self.context, self.node.uuid) as task: + mock_build_opt.return_value = [] + task.driver.raid._prepare_for_read_raid(task, 'delete_raid') + self.assertTrue( + task.node.driver_internal_info.get( + 'ilo_raid_delete_in_progress')) + self.assertTrue( + task.node.driver_internal_info.get( + 'cleaning_reboot')) + self.assertEqual( + task.node.driver_internal_info.get( + 'skip_current_clean_step'), False) + mock_reboot.assert_called_once_with(task, states.REBOOT) + + @mock.patch.object(ilo_raid.Ilo5RAID, '_prepare_for_read_raid') + @mock.patch.object(raid, 'filter_target_raid_config') + @mock.patch.object(ilo_common, 'get_ilo_object', autospec=True) + def test_create_configuration( + self, ilo_mock, filter_target_raid_config_mock, prepare_raid_mock): + ilo_mock_object = ilo_mock.return_value + with task_manager.acquire(self.context, self.node.uuid) as task: + filter_target_raid_config_mock.return_value = ( + self.target_raid_config) + result = task.driver.raid.create_configuration(task) + prepare_raid_mock.assert_called_once_with(task, 'create_raid') + (ilo_mock_object.create_raid_configuration. + assert_called_once_with(self.target_raid_config)) + self.assertEqual(states.CLEANWAIT, result) + + @mock.patch.object(raid, 'update_raid_info', autospec=True) + @mock.patch.object(raid, 'filter_target_raid_config') + @mock.patch.object(ilo_common, 'get_ilo_object', autospec=True) + def test_create_configuration_with_read_raid( + self, ilo_mock, filter_target_raid_config_mock, update_raid_mock): + raid_conf = {u'logical_disks': + [{u'size_gb': 89, + u'physical_disks': [u'5I:1:1'], + u'raid_level': u'0', + u'root_device_hint': {u'wwn': u'0x600508b1001c7e87'}, + u'controller': u'Smart Array P822 in Slot 1', + u'volume_name': u'0006EB7BPDVTF0BRH5L0EAEDDA'}] + } + ilo_mock_object = ilo_mock.return_value + self.node.driver_internal_info = {'ilo_raid_create_in_progress': True} + self.node.save() + with task_manager.acquire(self.context, self.node.uuid) as task: + filter_target_raid_config_mock.return_value = ( + self.target_raid_config) + ilo_mock_object.read_raid_configuration.return_value = raid_conf + task.driver.raid.create_configuration(task) + update_raid_mock.assert_called_once_with(task.node, raid_conf) + self.assertNotIn('ilo_raid_create_in_progress', + task.node.driver_internal_info) + self.assertNotIn('cleaning_reboot', + task.node.driver_internal_info) + self.assertNotIn('skip_current_clean_step', + task.node.driver_internal_info) + + @mock.patch.object(raid, 'filter_target_raid_config') + @mock.patch.object(ilo_common, 'get_ilo_object', autospec=True) + def test_create_configuration_with_read_raid_failed( + self, ilo_mock, filter_target_raid_config_mock): + raid_conf = {u'logical_disks': []} + self.node.driver_internal_info = {'ilo_raid_create_in_progress': True} + self.node.save() + ilo_mock_object = ilo_mock.return_value + with task_manager.acquire(self.context, self.node.uuid) as task: + filter_target_raid_config_mock.return_value = ( + self.target_raid_config) + ilo_mock_object.read_raid_configuration.return_value = raid_conf + self.assertRaises(exception.NodeCleaningFailure, + task.driver.raid.create_configuration, task) + self.assertNotIn('ilo_raid_create_in_progress', + task.node.driver_internal_info) + self.assertNotIn('cleaning_reboot', + task.node.driver_internal_info) + self.assertNotIn('skip_current_clean_step', + task.node.driver_internal_info) + + @mock.patch.object(raid, 'filter_target_raid_config') + @mock.patch.object(ilo_common, 'get_ilo_object', autospec=True) + def test_create_configuration_empty_target_raid_config( + self, ilo_mock, filter_target_raid_config_mock): + self.node.target_raid_config = {} + self.node.save() + ilo_mock_object = ilo_mock.return_value + with task_manager.acquire(self.context, self.node.uuid) as task: + msg = "Node %s has no target RAID configuration" % self.node.uuid + filter_target_raid_config_mock.side_effect = ( + exception.MissingParameterValue(msg)) + self.assertRaises(exception.MissingParameterValue, + task.driver.raid.create_configuration, task) + self.assertFalse(ilo_mock_object.create_raid_configuration.called) + + @mock.patch.object(ilo_raid.Ilo5RAID, '_prepare_for_read_raid') + @mock.patch.object(raid, 'filter_target_raid_config') + @mock.patch.object(ilo_common, 'get_ilo_object', autospec=True) + def test_create_configuration_skip_root( + self, ilo_mock, filter_target_raid_config_mock, + prepare_raid_mock): + ilo_mock_object = ilo_mock.return_value + with task_manager.acquire(self.context, self.node.uuid) as task: + exp_target_raid_config = { + "logical_disks": [ + {'size_gb': 200, 'raid_level': 5} + ]} + filter_target_raid_config_mock.return_value = ( + exp_target_raid_config) + result = task.driver.raid.create_configuration( + task, create_root_volume=False) + (ilo_mock_object.create_raid_configuration. + assert_called_once_with(exp_target_raid_config)) + self.assertEqual(states.CLEANWAIT, result) + prepare_raid_mock.assert_called_once_with(task, 'create_raid') + self.assertEqual( + exp_target_raid_config, + task.node.driver_internal_info['target_raid_config']) + + @mock.patch.object(ilo_raid.Ilo5RAID, '_prepare_for_read_raid') + @mock.patch.object(raid, 'filter_target_raid_config') + @mock.patch.object(ilo_common, 'get_ilo_object', autospec=True) + def test_create_configuration_skip_non_root( + self, ilo_mock, filter_target_raid_config_mock, prepare_raid_mock): + ilo_mock_object = ilo_mock.return_value + with task_manager.acquire(self.context, self.node.uuid) as task: + exp_target_raid_config = { + "logical_disks": [ + {'size_gb': 200, 'raid_level': 0, 'is_root_volume': True} + ]} + filter_target_raid_config_mock.return_value = ( + exp_target_raid_config) + result = task.driver.raid.create_configuration( + task, create_nonroot_volumes=False) + (ilo_mock_object.create_raid_configuration. + assert_called_once_with(exp_target_raid_config)) + prepare_raid_mock.assert_called_once_with(task, 'create_raid') + self.assertEqual(states.CLEANWAIT, result) + self.assertEqual( + exp_target_raid_config, + task.node.driver_internal_info['target_raid_config']) + + @mock.patch.object(raid, 'filter_target_raid_config') + @mock.patch.object(ilo_common, 'get_ilo_object', autospec=True) + def test_create_configuration_skip_root_skip_non_root( + self, ilo_mock, filter_target_raid_config_mock): + ilo_mock_object = ilo_mock.return_value + with task_manager.acquire(self.context, self.node.uuid) as task: + msg = "Node %s has no target RAID configuration" % self.node.uuid + filter_target_raid_config_mock.side_effect = ( + exception.MissingParameterValue(msg)) + self.assertRaises( + exception.MissingParameterValue, + task.driver.raid.create_configuration, + task, False, False) + self.assertFalse(ilo_mock_object.create_raid_configuration.called) + + @mock.patch.object(ilo_raid.Ilo5RAID, '_set_clean_failed') + @mock.patch.object(ilo_common, 'get_ilo_object', autospec=True) + def test_create_configuration_ilo_error(self, ilo_mock, + set_clean_failed_mock): + ilo_mock_object = ilo_mock.return_value + exc = ilo_error.IloError('error') + ilo_mock_object.create_raid_configuration.side_effect = exc + with task_manager.acquire(self.context, self.node.uuid) as task: + task.driver.raid.create_configuration( + task, create_nonroot_volumes=False) + set_clean_failed_mock.assert_called_once_with( + task, + 'Failed to create raid configuration ' + 'on node %s' % self.node.uuid, exc) + self.assertNotIn('ilo_raid_create_in_progress', + task.node.driver_internal_info) + self.assertNotIn('cleaning_reboot', + task.node.driver_internal_info) + self.assertNotIn('skip_current_clean_step', + task.node.driver_internal_info) + + @mock.patch.object(ilo_raid.Ilo5RAID, '_prepare_for_read_raid') + @mock.patch.object(ilo_common, 'get_ilo_object', autospec=True) + def test_delete_configuration(self, ilo_mock, prepare_raid_mock): + ilo_mock_object = ilo_mock.return_value + with task_manager.acquire(self.context, self.node.uuid) as task: + result = task.driver.raid.delete_configuration(task) + self.assertEqual(states.CLEANWAIT, result) + ilo_mock_object.delete_raid_configuration.assert_called_once_with() + prepare_raid_mock.assert_called_once_with(task, 'delete_raid') + + @mock.patch.object(ilo_raid.LOG, 'info', spec_set=True, + autospec=True) + @mock.patch.object(ilo_raid.Ilo5RAID, '_prepare_for_read_raid') + @mock.patch.object(ilo_common, 'get_ilo_object', autospec=True) + def test_delete_configuration_no_logical_drive( + self, ilo_mock, prepare_raid_mock, log_mock): + ilo_mock_object = ilo_mock.return_value + exc = ilo_error.IloLogicalDriveNotFoundError('No logical drive found') + with task_manager.acquire(self.context, self.node.uuid) as task: + ilo_mock_object.delete_raid_configuration.side_effect = exc + task.driver.raid.delete_configuration(task) + self.assertTrue(log_mock.called) + + @mock.patch.object(ilo_common, 'get_ilo_object', autospec=True) + def test_delete_configuration_with_read_raid(self, ilo_mock): + raid_conf = {u'logical_disks': []} + self.node.driver_internal_info = {'ilo_raid_delete_in_progress': True} + self.node.save() + ilo_mock_object = ilo_mock.return_value + with task_manager.acquire(self.context, self.node.uuid) as task: + ilo_mock_object.read_raid_configuration.return_value = raid_conf + task.driver.raid.delete_configuration(task) + self.assertEqual(self.node.raid_config, {}) + self.assertNotIn('ilo_raid_delete_in_progress', + task.node.driver_internal_info) + self.assertNotIn('cleaning_reboot', + task.node.driver_internal_info) + self.assertNotIn('skip_current_clean_step', + task.node.driver_internal_info) + + @mock.patch.object(ilo_common, 'get_ilo_object', autospec=True) + def test_delete_configuration_with_read_raid_failed(self, ilo_mock): + raid_conf = {u'logical_disks': [{'size_gb': 200, + 'raid_level': 0, + 'is_root_volume': True}]} + self.node.driver_internal_info = {'ilo_raid_delete_in_progress': True} + self.node.save() + ilo_mock_object = ilo_mock.return_value + with task_manager.acquire(self.context, self.node.uuid) as task: + ilo_mock_object.read_raid_configuration.return_value = raid_conf + self.assertRaises(exception.NodeCleaningFailure, + task.driver.raid.delete_configuration, task) + self.assertNotIn('ilo_raid_delete_in_progress', + task.node.driver_internal_info) + self.assertNotIn('cleaning_reboot', + task.node.driver_internal_info) + self.assertNotIn('skip_current_clean_step', + task.node.driver_internal_info) + + @mock.patch.object(ilo_raid.Ilo5RAID, '_set_clean_failed') + @mock.patch.object(ilo_common, 'get_ilo_object', autospec=True) + def test_delete_configuration_ilo_error(self, ilo_mock, + set_clean_failed_mock): + ilo_mock_object = ilo_mock.return_value + exc = ilo_error.IloError('error') + ilo_mock_object.delete_raid_configuration.side_effect = exc + with task_manager.acquire(self.context, self.node.uuid) as task: + task.driver.raid.delete_configuration(task) + ilo_mock_object.delete_raid_configuration.assert_called_once_with() + self.assertNotIn('ilo_raid_delete_in_progress', + task.node.driver_internal_info) + self.assertNotIn('cleaning_reboot', + task.node.driver_internal_info) + self.assertNotIn('skip_current_clean_step', + task.node.driver_internal_info) + set_clean_failed_mock.assert_called_once_with( + task, + 'Failed to delete raid configuration ' + 'on node %s' % self.node.uuid, exc) diff --git a/ironic/tests/unit/drivers/modules/test_agent.py b/ironic/tests/unit/drivers/modules/test_agent.py index 16c653c433..5e893a98ca 100644 --- a/ironic/tests/unit/drivers/modules/test_agent.py +++ b/ironic/tests/unit/drivers/modules/test_agent.py @@ -1406,12 +1406,15 @@ class AgentRAIDTestCase(db_base.DbTestCase): self.assertEqual(0, ret[0]['priority']) self.assertEqual(0, ret[1]['priority']) + @mock.patch.object(raid, 'filter_target_raid_config') @mock.patch.object(deploy_utils, 'agent_execute_clean_step', autospec=True) - def test_create_configuration(self, execute_mock): + def test_create_configuration(self, execute_mock, + filter_target_raid_config_mock): with task_manager.acquire(self.context, self.node.uuid) as task: execute_mock.return_value = states.CLEANWAIT - + filter_target_raid_config_mock.return_value = ( + self.target_raid_config) return_value = task.driver.raid.create_configuration(task) self.assertEqual(states.CLEANWAIT, return_value) @@ -1420,65 +1423,76 @@ class AgentRAIDTestCase(db_base.DbTestCase): task.node.driver_internal_info['target_raid_config']) execute_mock.assert_called_once_with(task, self.clean_step) + @mock.patch.object(raid, 'filter_target_raid_config') @mock.patch.object(deploy_utils, 'agent_execute_clean_step', autospec=True) - def test_create_configuration_skip_root(self, execute_mock): + def test_create_configuration_skip_root(self, execute_mock, + filter_target_raid_config_mock): with task_manager.acquire(self.context, self.node.uuid) as task: execute_mock.return_value = states.CLEANWAIT - - return_value = task.driver.raid.create_configuration( - task, create_root_volume=False) - - self.assertEqual(states.CLEANWAIT, return_value) - execute_mock.assert_called_once_with(task, self.clean_step) exp_target_raid_config = { "logical_disks": [ {'size_gb': 200, 'raid_level': 5} ]} + filter_target_raid_config_mock.return_value = ( + exp_target_raid_config) + return_value = task.driver.raid.create_configuration( + task, create_root_volume=False) + self.assertEqual(states.CLEANWAIT, return_value) + execute_mock.assert_called_once_with(task, self.clean_step) self.assertEqual( exp_target_raid_config, task.node.driver_internal_info['target_raid_config']) + @mock.patch.object(raid, 'filter_target_raid_config') @mock.patch.object(deploy_utils, 'agent_execute_clean_step', autospec=True) - def test_create_configuration_skip_nonroot(self, execute_mock): + def test_create_configuration_skip_nonroot(self, execute_mock, + filter_target_raid_config_mock): with task_manager.acquire(self.context, self.node.uuid) as task: execute_mock.return_value = states.CLEANWAIT - - return_value = task.driver.raid.create_configuration( - task, create_nonroot_volumes=False) - - self.assertEqual(states.CLEANWAIT, return_value) - execute_mock.assert_called_once_with(task, self.clean_step) exp_target_raid_config = { "logical_disks": [ {'size_gb': 200, 'raid_level': 0, 'is_root_volume': True}, ]} + filter_target_raid_config_mock.return_value = ( + exp_target_raid_config) + return_value = task.driver.raid.create_configuration( + task, create_nonroot_volumes=False) + self.assertEqual(states.CLEANWAIT, return_value) + execute_mock.assert_called_once_with(task, self.clean_step) self.assertEqual( exp_target_raid_config, task.node.driver_internal_info['target_raid_config']) + @mock.patch.object(raid, 'filter_target_raid_config') @mock.patch.object(deploy_utils, 'agent_execute_clean_step', autospec=True) def test_create_configuration_no_target_raid_config_after_skipping( - self, execute_mock): + self, execute_mock, filter_target_raid_config_mock): with task_manager.acquire(self.context, self.node.uuid) as task: + msg = "Node %s has no target RAID configuration" % self.node.uuid + filter_target_raid_config_mock.side_effect = ( + exception.MissingParameterValue(msg)) self.assertRaises( exception.MissingParameterValue, task.driver.raid.create_configuration, task, create_root_volume=False, create_nonroot_volumes=False) - self.assertFalse(execute_mock.called) + @mock.patch.object(raid, 'filter_target_raid_config') @mock.patch.object(deploy_utils, 'agent_execute_clean_step', autospec=True) def test_create_configuration_empty_target_raid_config( - self, execute_mock): + self, execute_mock, filter_target_raid_config_mock): execute_mock.return_value = states.CLEANING self.node.target_raid_config = {} self.node.save() with task_manager.acquire(self.context, self.node.uuid) as task: + msg = "Node %s has no target RAID configuration" % self.node.uuid + filter_target_raid_config_mock.side_effect = ( + exception.MissingParameterValue(msg)) self.assertRaises(exception.MissingParameterValue, task.driver.raid.create_configuration, task) diff --git a/ironic/tests/unit/drivers/test_ilo.py b/ironic/tests/unit/drivers/test_ilo.py index 321ace576f..ed5359fa05 100644 --- a/ironic/tests/unit/drivers/test_ilo.py +++ b/ironic/tests/unit/drivers/test_ilo.py @@ -19,6 +19,7 @@ Test class for iLO Drivers from ironic.conductor import task_manager from ironic.drivers import ilo from ironic.drivers.modules import agent +from ironic.drivers.modules.ilo import raid from ironic.drivers.modules import inspector from ironic.drivers.modules import iscsi_deploy from ironic.drivers.modules import noop @@ -165,3 +166,47 @@ class IloHardwareTestCase(db_base.DbTestCase): agent.AgentDeploy) self.assertIsInstance(task.driver.raid, agent.AgentRAID) + + +class Ilo5HardwareTestCase(db_base.DbTestCase): + + def setUp(self): + super(Ilo5HardwareTestCase, self).setUp() + self.config(enabled_hardware_types=['ilo5'], + enabled_boot_interfaces=['ilo-virtual-media', 'ilo-pxe'], + enabled_console_interfaces=['ilo'], + enabled_deploy_interfaces=['iscsi', 'direct'], + enabled_inspect_interfaces=['ilo'], + enabled_management_interfaces=['ilo'], + enabled_power_interfaces=['ilo'], + enabled_raid_interfaces=['ilo5'], + enabled_rescue_interfaces=['no-rescue', 'agent'], + enabled_vendor_interfaces=['ilo', 'no-vendor']) + + def test_default_interfaces(self): + node = obj_utils.create_test_node(self.context, driver='ilo5') + with task_manager.acquire(self.context, node.id) as task: + self.assertIsInstance(task.driver.raid, raid.Ilo5RAID) + + def test_override_with_no_raid(self): + self.config(enabled_raid_interfaces=['no-raid', 'ilo5']) + node = obj_utils.create_test_node(self.context, driver='ilo5', + raid_interface='no-raid') + with task_manager.acquire(self.context, node.id) as task: + self.assertIsInstance(task.driver.raid, noop.NoRAID) + self.assertIsInstance(task.driver.boot, + ilo.boot.IloVirtualMediaBoot) + self.assertIsInstance(task.driver.console, + ilo.console.IloConsoleInterface) + self.assertIsInstance(task.driver.deploy, + iscsi_deploy.ISCSIDeploy) + self.assertIsInstance(task.driver.inspect, + ilo.inspect.IloInspect) + self.assertIsInstance(task.driver.management, + ilo.management.IloManagement) + self.assertIsInstance(task.driver.power, + ilo.power.IloPower) + self.assertIsInstance(task.driver.rescue, + noop.NoRescue) + self.assertIsInstance(task.driver.vendor, + ilo.vendor.VendorPassthru) diff --git a/ironic/tests/unit/drivers/third_party_driver_mocks.py b/ironic/tests/unit/drivers/third_party_driver_mocks.py index 56c3a38783..03850a48e6 100644 --- a/ironic/tests/unit/drivers/third_party_driver_mocks.py +++ b/ironic/tests/unit/drivers/third_party_driver_mocks.py @@ -56,6 +56,8 @@ if not proliantutils: sys.modules['proliantutils.utils'] = proliantutils.utils proliantutils.utils.process_firmware_image = mock.MagicMock() proliantutils.exception.IloError = type('IloError', (Exception,), {}) + proliantutils.exception.IloLogicalDriveNotFoundError = ( + type('IloLogicalDriveNotFoundError', (Exception,), {})) command_exception = type('IloCommandNotSupportedError', (Exception,), {}) proliantutils.exception.IloCommandNotSupportedError = command_exception proliantutils.exception.IloCommandNotSupportedInBiosError = type( diff --git a/releasenotes/notes/ilo5-oob-raid-a0eac60f7d77a4fc.yaml b/releasenotes/notes/ilo5-oob-raid-a0eac60f7d77a4fc.yaml new file mode 100644 index 0000000000..4325cb09fc --- /dev/null +++ b/releasenotes/notes/ilo5-oob-raid-a0eac60f7d77a4fc.yaml @@ -0,0 +1,12 @@ +--- +features: + - Adds new hardware type ``ilo5``. Including all other hardware interfaces + ``ilo`` hardware type supports, this has one new RAID interface ``ilo5``. + - Adds functionality to perform out-of-band RAID operation for iLO5 based + HPE Proliant servers. +upgrade: + - The ``create_raid_configuration``, ``delete_raid_configuration`` and + ``read_raid_configuration`` interfaces of 'proliantutils' library has been + enhanced to support out-of-band RAID operation for ``ilo5`` hardware type. + To leverage this feature, the 'proliantutils' library needs to be upgraded + to version '2.7.0'. diff --git a/setup.cfg b/setup.cfg index 128b75e5ab..cfab34dd3f 100644 --- a/setup.cfg +++ b/setup.cfg @@ -125,6 +125,7 @@ ironic.hardware.interfaces.raid = agent = ironic.drivers.modules.agent:AgentRAID fake = ironic.drivers.modules.fake:FakeRAID idrac = ironic.drivers.modules.drac.raid:DracRAID + ilo5 = ironic.drivers.modules.ilo.raid:Ilo5RAID irmc = ironic.drivers.modules.irmc.raid:IRMCRAID no-raid = ironic.drivers.modules.noop:NoRAID @@ -152,6 +153,7 @@ ironic.hardware.types = fake-hardware = ironic.drivers.fake_hardware:FakeHardware idrac = ironic.drivers.drac:IDRACHardware ilo = ironic.drivers.ilo:IloHardware + ilo5 = ironic.drivers.ilo:Ilo5Hardware ipmi = ironic.drivers.ipmi:IPMIHardware irmc = ironic.drivers.irmc:IRMCHardware manual-management = ironic.drivers.generic:ManualManagementHardware