diff --git a/doc/source/admin/drivers/redfish.rst b/doc/source/admin/drivers/redfish.rst index a0adac60cc..bcfb683214 100644 --- a/doc/source/admin/drivers/redfish.rst +++ b/doc/source/admin/drivers/redfish.rst @@ -21,7 +21,8 @@ Enabling the Redfish driver =========================== #. Add ``redfish`` to the list of ``enabled_hardware_types``, - ``enabled_power_interfaces`` and ``enabled_management_interfaces`` + ``enabled_power_interfaces``, ``enabled_management_interfaces`` and + ``enabled_inspect_interfaces`` in ``/etc/ironic/ironic.conf``. For example:: [DEFAULT] @@ -29,6 +30,7 @@ Enabling the Redfish driver enabled_hardware_types = ipmi,redfish enabled_power_interfaces = ipmitool,redfish enabled_management_interfaces = ipmitool,redfish + enabled_inspect_interfaces = inspector,redfish #. Restart the ironic conductor service:: @@ -104,6 +106,21 @@ bare metal node as well as set it to either Legacy BIOS or UEFI. it remains the responsibility of the operator to configure proper boot mode to their bare metal nodes. +Out-Of-Band inspection +^^^^^^^^^^^^^^^^^^^^^^ + +The ``redfish`` hardware type can inspect the bare metal node by querying +Redfish BMC. This process if quick and reliable compared to the way +how the ``inspector`` hardware type works e.g. booting bare metal node into +the introspection ramdisk. + +.. note:: + + The ``redfish`` inspect interface largely relies on the optional parts + of the Redfish specification. Not all Redfish-compliant BMCs might serve + the required information, in which case bare metal node inspection would + fail. + .. _Redfish: http://redfish.dmtf.org/ .. _Sushy: https://git.openstack.org/cgit/openstack/sushy .. _TLS: https://en.wikipedia.org/wiki/Transport_Layer_Security diff --git a/ironic/drivers/modules/deploy_utils.py b/ironic/drivers/modules/deploy_utils.py index ba7990117c..21cb895eb1 100644 --- a/ironic/drivers/modules/deploy_utils.py +++ b/ironic/drivers/modules/deploy_utils.py @@ -579,6 +579,34 @@ def get_single_nic_with_vif_port_id(task): return port.address +def create_ports_if_not_exist(task, macs): + """Create ironic ports for the mac addresses. + + Creates ironic ports for the mac addresses returned with inspection + or as requested by operator. + + :param task: a TaskManager instance. + :param macs: A dictionary of port numbers to mac addresses + returned by node inspection. + + """ + node = task.node + for port_num, mac in macs.items(): + port_dict = {'address': mac, 'node_id': node.id} + port = objects.Port(task.context, **port_dict) + + try: + port.create() + LOG.info(_("Port %(port_num)s created for MAC address %(address)s " + "for node %(node)s"), + {'address': mac, 'node': node.uuid, 'port_num': port_num}) + except exception.MACAlreadyExists: + LOG.warning(_("Port %(port_num)s already exists for MAC address" + "%(address)s for node %(node)s"), + {'address': mac, 'node': node.uuid, + 'port_num': port_num}) + + def agent_get_clean_steps(task, interface=None, override_priorities=None): """Get the list of cached clean steps from the agent. diff --git a/ironic/drivers/modules/ilo/inspect.py b/ironic/drivers/modules/ilo/inspect.py index 6884a675e5..231a0d90b4 100644 --- a/ironic/drivers/modules/ilo/inspect.py +++ b/ironic/drivers/modules/ilo/inspect.py @@ -22,8 +22,8 @@ from ironic.common import states from ironic.common import utils from ironic.conductor import utils as conductor_utils from ironic.drivers import base +from ironic.drivers.modules import deploy_utils from ironic.drivers.modules.ilo import common as ilo_common -from ironic import objects ilo_error = importutils.try_import('proliantutils.exception') @@ -48,32 +48,6 @@ CAPABILITIES_KEYS = {'secure_boot', 'rom_firmware_version', 'nvdimm_n', 'logical_nvdimm_n', 'persistent_memory'} -def _create_ports_if_not_exist(task, macs): - """Create ironic ports for the mac addresses. - - Creates ironic ports for the mac addresses returned with inspection - or as requested by operator. - - :param task: a TaskManager instance. - :param macs: A dictionary of port numbers to mac addresses - returned by node inspection. - - """ - node = task.node - for mac in macs.values(): - port_dict = {'address': mac, 'node_id': node.id} - port = objects.Port(task.context, **port_dict) - - try: - port.create() - LOG.info("Port created for MAC address %(address)s for node " - "%(node)s", {'address': mac, 'node': node.uuid}) - except exception.MACAlreadyExists: - LOG.warning("Port already exists for MAC address %(address)s " - "for node %(node)s", - {'address': mac, 'node': node.uuid}) - - def _get_essential_properties(node, ilo_object): """Inspects the node and get essential scheduling properties @@ -270,7 +244,7 @@ class IloInspect(base.InspectInterface): task.node.save() # Create ports for the nics detected. - _create_ports_if_not_exist(task, result['macs']) + deploy_utils.create_ports_if_not_exist(task, result['macs']) LOG.debug("Node properties for %(node)s are updated as " "%(properties)s", diff --git a/ironic/drivers/modules/redfish/inspect.py b/ironic/drivers/modules/redfish/inspect.py new file mode 100644 index 0000000000..8fc92a15e2 --- /dev/null +++ b/ironic/drivers/modules/redfish/inspect.py @@ -0,0 +1,192 @@ +# 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. +""" +Redfish Inspect Interface +""" + +from oslo_log import log +from oslo_utils import importutils +from oslo_utils import units + +from ironic.common import exception +from ironic.common.i18n import _ +from ironic.common import states +from ironic.drivers import base +from ironic.drivers.modules import deploy_utils +from ironic.drivers.modules.redfish import utils as redfish_utils + +LOG = log.getLogger(__name__) + +sushy = importutils.try_import('sushy') + +if sushy: + CPU_ARCH_MAP = { + sushy.PROCESSOR_ARCH_x86: 'x86_64', + sushy.PROCESSOR_ARCH_IA_64: 'ia64', + sushy.PROCESSOR_ARCH_ARM: 'arm', + sushy.PROCESSOR_ARCH_MIPS: 'mips', + sushy.PROCESSOR_ARCH_OEM: 'oem' + } + + +class RedfishInspect(base.InspectInterface): + + def __init__(self): + """Initialize the Redfish inspection interface. + + :raises: DriverLoadError if the driver can't be loaded due to + missing dependencies + """ + super(RedfishInspect, self).__init__() + if not sushy: + raise exception.DriverLoadError( + driver='redfish', + reason=_('Unable to import the sushy library')) + + def get_properties(self): + """Return the properties of the interface. + + :returns: dictionary of : entries. + """ + return redfish_utils.COMMON_PROPERTIES.copy() + + def validate(self, task): + """Validate the driver-specific Node deployment info. + + This method validates whether the 'driver_info' properties of + the task's node contains the required information for this + interface to function. + + This method is often executed synchronously in API requests, so it + should not conduct long-running checks. + + :param task: A TaskManager instance containing the node to act on. + :raises: InvalidParameterValue on malformed parameter(s) + :raises: MissingParameterValue on missing parameter(s) + """ + redfish_utils.parse_driver_info(task.node) + + def inspect_hardware(self, task): + """Inspect hardware to get the hardware properties. + + Inspects hardware to get the essential properties. + It fails if any of the essential properties + are not received from the node. + + :param task: a TaskManager instance. + :raises: HardwareInspectionFailure if essential properties + could not be retrieved successfully. + :returns: The resulting state of inspection. + + """ + system = redfish_utils.get_system(task.node) + + # get the essential properties and update the node properties + # with it. + inspected_properties = task.node.properties + + if system.memory_summary and system.memory_summary.size_gib: + inspected_properties['memory_mb'] = str( + system.memory_summary.size_gib * units.Ki) + + if system.processors and system.processors.summary: + cpus, arch = system.processors.summary + if cpus: + inspected_properties['cpus'] = cpus + + if arch: + try: + inspected_properties['cpu_arch'] = CPU_ARCH_MAP[arch] + + except KeyError: + LOG.warning( + _("Unknown CPU arch %(arch)s discovered " + "for Node %(node)s"), {'node': task.node.uuid, + 'arch': arch}) + + simple_storage_size = 0 + + try: + if (system.simple_storage and + system.simple_storage.disks_sizes_bytes): + simple_storage_size = [ + size for size in system.simple_storage.disks_sizes_bytes + if size >= 4 * units.Gi + ] or [0] + + simple_storage_size = simple_storage_size[0] + + except sushy.SushyError: + LOG.info( + _("No simple storage information discovered " + "for Node %(node)s"), {'node': task.node.uuid}) + + storage_size = 0 + + try: + if system.storage and system.storage.volumes_sizes_bytes: + storage_size = [ + size for size in system.storage.volumes_sizes_bytes + if size >= 4 * units.Gi + ] or [0] + + storage_size = storage_size[0] + + except sushy.SushyError: + LOG.info(_("No storage volume information discovered " + "for Node %(node)s"), {'node': task.node.uuid}) + + local_gb = max(simple_storage_size, storage_size) + + # Note(deray): Convert the received size to GiB and reduce the + # value by 1 GB as consumers like Ironic requires the ``local_gb`` + # to be returned 1 less than actual size. + local_gb = max(0, int(local_gb / units.Gi - 1)) + + if local_gb: + inspected_properties['local_gb'] = str(local_gb) + + else: + LOG.warning(_("Could not provide a valid storage size configured " + "for Node %(node)s"), {'node': task.node.uuid}) + + valid_keys = self.ESSENTIAL_PROPERTIES + missing_keys = valid_keys - set(inspected_properties) + if missing_keys: + error = (_('Failed to discover the following properties: ' + '%(missing_keys)s on node %(node)s'), + {'missing_keys': ', '.join(missing_keys), + 'node': task.node.uuid}) + raise exception.HardwareInspectionFailure(error=error) + + task.node.properties = inspected_properties + task.node.save() + + LOG.debug(_("Node properties for %(node)s are updated as " + "%(properties)s"), + {'properties': inspected_properties, + 'node': task.node.uuid}) + + if (system.ethernet_interfaces and + system.ethernet_interfaces.eth_summary): + macs = system.ethernet_interfaces.eth_summary + + # Create ports for the nics detected. + deploy_utils.create_ports_if_not_exist(task, macs) + + else: + LOG.info(_("No NIC information discovered " + "for Node %(node)s"), {'node': task.node.uuid}) + + LOG.info(_("Node %(node)s inspected."), {'node': task.node.uuid}) + + return states.MANAGEABLE diff --git a/ironic/drivers/redfish.py b/ironic/drivers/redfish.py index ecf7c789a1..9e5c933311 100644 --- a/ironic/drivers/redfish.py +++ b/ironic/drivers/redfish.py @@ -14,6 +14,9 @@ # under the License. from ironic.drivers import generic +from ironic.drivers.modules import inspector +from ironic.drivers.modules import noop +from ironic.drivers.modules.redfish import inspect as redfish_inspect from ironic.drivers.modules.redfish import management as redfish_mgmt from ironic.drivers.modules.redfish import power as redfish_power @@ -30,3 +33,9 @@ class RedfishHardware(generic.GenericHardware): def supported_power_interfaces(self): """List of supported power interfaces.""" return [redfish_power.RedfishPower] + + @property + def supported_inspect_interfaces(self): + """List of supported power interfaces.""" + return [redfish_inspect.RedfishInspect, inspector.Inspector, + noop.NoInspect] diff --git a/ironic/tests/unit/drivers/modules/ilo/test_inspect.py b/ironic/tests/unit/drivers/modules/ilo/test_inspect.py index 8c73d5c761..2efc104f0f 100644 --- a/ironic/tests/unit/drivers/modules/ilo/test_inspect.py +++ b/ironic/tests/unit/drivers/modules/ilo/test_inspect.py @@ -23,10 +23,10 @@ from ironic.common import states from ironic.common import utils from ironic.conductor import task_manager from ironic.conductor import utils as conductor_utils +from ironic.drivers.modules import deploy_utils from ironic.drivers.modules.ilo import common as ilo_common from ironic.drivers.modules.ilo import inspect as ilo_inspect from ironic.drivers.modules.ilo import power as ilo_power -from ironic import objects from ironic.tests.unit.drivers.modules.ilo import test_common @@ -51,7 +51,7 @@ class IloInspectTestCase(test_common.BaseIloTest): @mock.patch.object(ilo_inspect, '_get_capabilities', spec_set=True, autospec=True) - @mock.patch.object(ilo_inspect, '_create_ports_if_not_exist', + @mock.patch.object(deploy_utils, 'create_ports_if_not_exist', spec_set=True, autospec=True) @mock.patch.object(ilo_inspect, '_get_essential_properties', spec_set=True, autospec=True) @@ -88,7 +88,7 @@ class IloInspectTestCase(test_common.BaseIloTest): spec_set=True, autospec=True) @mock.patch.object(ilo_inspect, '_get_capabilities', spec_set=True, autospec=True) - @mock.patch.object(ilo_inspect, '_create_ports_if_not_exist', + @mock.patch.object(deploy_utils, 'create_ports_if_not_exist', spec_set=True, autospec=True) @mock.patch.object(ilo_inspect, '_get_essential_properties', spec_set=True, autospec=True) @@ -131,7 +131,7 @@ class IloInspectTestCase(test_common.BaseIloTest): @mock.patch.object(ilo_inspect, '_get_capabilities', spec_set=True, autospec=True) - @mock.patch.object(ilo_inspect, '_create_ports_if_not_exist', + @mock.patch.object(deploy_utils, 'create_ports_if_not_exist', spec_set=True, autospec=True) @mock.patch.object(ilo_inspect, '_get_essential_properties', spec_set=True, autospec=True) @@ -170,7 +170,7 @@ class IloInspectTestCase(test_common.BaseIloTest): @mock.patch.object(ilo_inspect, '_get_capabilities', spec_set=True, autospec=True) - @mock.patch.object(ilo_inspect, '_create_ports_if_not_exist', + @mock.patch.object(deploy_utils, 'create_ports_if_not_exist', spec_set=True, autospec=True) @mock.patch.object(ilo_inspect, '_get_essential_properties', spec_set=True, autospec=True) @@ -209,7 +209,7 @@ class IloInspectTestCase(test_common.BaseIloTest): @mock.patch.object(ilo_inspect, '_get_capabilities', spec_set=True, autospec=True) - @mock.patch.object(ilo_inspect, '_create_ports_if_not_exist', + @mock.patch.object(deploy_utils, 'create_ports_if_not_exist', spec_set=True, autospec=True) @mock.patch.object(ilo_inspect, '_get_essential_properties', spec_set=True, autospec=True) @@ -256,38 +256,6 @@ class IloInspectTestCase(test_common.BaseIloTest): class TestInspectPrivateMethods(test_common.BaseIloTest): - @mock.patch.object(ilo_inspect.LOG, 'info', spec_set=True, autospec=True) - @mock.patch.object(objects, 'Port', spec_set=True, autospec=True) - def test__create_ports_if_not_exist(self, port_mock, log_mock): - macs = {'Port 1': 'aa:aa:aa:aa:aa:aa', 'Port 2': 'bb:bb:bb:bb:bb:bb'} - node_id = self.node.id - port_dict1 = {'address': 'aa:aa:aa:aa:aa:aa', 'node_id': node_id} - port_dict2 = {'address': 'bb:bb:bb:bb:bb:bb', 'node_id': node_id} - port_obj1, port_obj2 = mock.MagicMock(), mock.MagicMock() - port_mock.side_effect = [port_obj1, port_obj2] - with task_manager.acquire(self.context, self.node.uuid, - shared=False) as task: - ilo_inspect._create_ports_if_not_exist(task, macs) - self.assertTrue(log_mock.called) - expected_calls = [mock.call(task.context, **port_dict1), - mock.call(task.context, **port_dict2)] - port_mock.assert_has_calls(expected_calls, any_order=True) - port_obj1.create.assert_called_once_with() - port_obj2.create.assert_called_once_with() - - @mock.patch.object(ilo_inspect.LOG, 'warning', - spec_set=True, autospec=True) - @mock.patch.object(objects.Port, 'create', spec_set=True, autospec=True) - def test__create_ports_if_not_exist_mac_exception(self, - create_mock, - log_mock): - create_mock.side_effect = exception.MACAlreadyExists('f') - macs = {'Port 1': 'aa:aa:aa:aa:aa:aa', 'Port 2': 'bb:bb:bb:bb:bb:bb'} - with task_manager.acquire(self.context, self.node.uuid, - shared=False) as task: - ilo_inspect._create_ports_if_not_exist(task, macs) - self.assertEqual(2, log_mock.call_count) - def test__get_essential_properties_ok(self): ilo_mock = mock.MagicMock(spec=['get_essential_properties']) properties = {'memory_mb': '512', 'local_gb': '10', diff --git a/ironic/tests/unit/drivers/modules/redfish/test_inspect.py b/ironic/tests/unit/drivers/modules/redfish/test_inspect.py new file mode 100644 index 0000000000..b705856385 --- /dev/null +++ b/ironic/tests/unit/drivers/modules/redfish/test_inspect.py @@ -0,0 +1,189 @@ +# Copyright 2017 Red Hat, Inc. +# All Rights Reserved. +# +# 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. + +import mock +from oslo_utils import importutils +from oslo_utils import units + +from ironic.common import exception +from ironic.conductor import task_manager +from ironic.drivers.modules import deploy_utils +from ironic.drivers.modules.redfish import utils as redfish_utils +from ironic.tests.unit.db import base as db_base +from ironic.tests.unit.db import utils as db_utils +from ironic.tests.unit.objects import utils as obj_utils + +sushy = importutils.try_import('sushy') + +INFO_DICT = db_utils.get_test_redfish_info() + + +class MockedSushyError(Exception): + pass + + +class RedfishInspectTestCase(db_base.DbTestCase): + + def setUp(self): + super(RedfishInspectTestCase, self).setUp() + self.config(enabled_hardware_types=['redfish'], + enabled_power_interfaces=['redfish'], + enabled_management_interfaces=['redfish'], + enabled_inspect_interfaces=['redfish']) + self.node = obj_utils.create_test_node( + self.context, driver='redfish', driver_info=INFO_DICT) + + def init_system_mock(self, system_mock, **properties): + + system_mock.reset() + + system_mock.memory_summary.size_gib = 2 + + system_mock.processors.summary = '8', 'MIPS' + + system_mock.simple_storage.disks_sizes_bytes = ( + 1 * units.Gi, units.Gi * 3, units.Gi * 5) + system_mock.storage.volumes_sizes_bytes = ( + 2 * units.Gi, units.Gi * 4, units.Gi * 6) + + system_mock.ethernet_interfaces.eth_summary = { + '1': '00:11:22:33:44:55', '2': '66:77:88:99:AA:BB' + } + + return system_mock + + def test_get_properties(self): + with task_manager.acquire(self.context, self.node.uuid, + shared=True) as task: + properties = task.driver.get_properties() + for prop in redfish_utils.COMMON_PROPERTIES: + self.assertIn(prop, properties) + + @mock.patch.object(redfish_utils, 'parse_driver_info', autospec=True) + def test_validate(self, mock_parse_driver_info): + with task_manager.acquire(self.context, self.node.uuid, + shared=True) as task: + task.driver.management.validate(task) + mock_parse_driver_info.assert_called_once_with(task.node) + + @mock.patch.object(redfish_utils, 'get_system', autospec=True) + @mock.patch.object(deploy_utils, 'create_ports_if_not_exist', + autospec=True) + def test_inspect_hardware_ok(self, mock_create_ports_if_not_exist, + mock_get_system): + expected_properties = { + 'cpu_arch': 'mips', 'cpus': '8', + 'local_gb': '4', 'memory_mb': '2048' + } + + system = self.init_system_mock(mock_get_system.return_value) + + with task_manager.acquire(self.context, self.node.uuid, + shared=True) as task: + task.driver.inspect.inspect_hardware(task) + mock_create_ports_if_not_exist.assert_called_once_with( + task, system.ethernet_interfaces.eth_summary) + mock_get_system.assert_called_once_with(task.node) + self.assertEqual(expected_properties, task.node.properties) + + @mock.patch.object(redfish_utils, 'get_system', autospec=True) + def test_inspect_hardware_fail_missing_cpu(self, mock_get_system): + system_mock = self.init_system_mock(mock_get_system.return_value) + system_mock.processors.summary = None, None + + with task_manager.acquire(self.context, self.node.uuid, + shared=True) as task: + task.node.properties.pop('cpu_arch') + self.assertRaises(exception.HardwareInspectionFailure, + task.driver.inspect.inspect_hardware, task) + + @mock.patch.object(redfish_utils, 'get_system', autospec=True) + def test_inspect_hardware_ignore_missing_cpu(self, mock_get_system): + system_mock = self.init_system_mock(mock_get_system.return_value) + system_mock.processors.summary = None, None + + with task_manager.acquire(self.context, self.node.uuid, + shared=True) as task: + expected_properties = { + 'cpu_arch': 'x86_64', 'cpus': '8', + 'local_gb': '4', 'memory_mb': '2048' + } + task.driver.inspect.inspect_hardware(task) + self.assertEqual(expected_properties, task.node.properties) + + @mock.patch.object(redfish_utils, 'get_system', autospec=True) + def test_inspect_hardware_fail_missing_local_gb(self, mock_get_system): + system_mock = self.init_system_mock(mock_get_system.return_value) + system_mock.simple_storage.disks_sizes_bytes = None + system_mock.storage.volumes_sizes_bytes = None + + with task_manager.acquire(self.context, self.node.uuid, + shared=True) as task: + task.node.properties.pop('local_gb') + self.assertRaises(exception.HardwareInspectionFailure, + task.driver.inspect.inspect_hardware, task) + + @mock.patch.object(redfish_utils, 'get_system', autospec=True) + def test_inspect_hardware_ignore_missing_local_gb(self, mock_get_system): + system_mock = self.init_system_mock(mock_get_system.return_value) + system_mock.simple_storage.disks_sizes_bytes = None + system_mock.storage.volumes_sizes_bytes = None + + with task_manager.acquire(self.context, self.node.uuid, + shared=True) as task: + expected_properties = { + 'cpu_arch': 'mips', 'cpus': '8', + 'local_gb': '10', 'memory_mb': '2048' + } + task.driver.inspect.inspect_hardware(task) + self.assertEqual(expected_properties, task.node.properties) + + @mock.patch.object(redfish_utils, 'get_system', autospec=True) + def test_inspect_hardware_fail_missing_memory_mb(self, mock_get_system): + system_mock = self.init_system_mock(mock_get_system.return_value) + system_mock.memory_summary.size_gib = None + + with task_manager.acquire(self.context, self.node.uuid, + shared=True) as task: + task.node.properties.pop('memory_mb') + self.assertRaises(exception.HardwareInspectionFailure, + task.driver.inspect.inspect_hardware, task) + + @mock.patch.object(redfish_utils, 'get_system', autospec=True) + def test_inspect_hardware_ignore_missing_memory_mb(self, mock_get_system): + system_mock = self.init_system_mock(mock_get_system.return_value) + system_mock.memory_summary.size_gib = None + + with task_manager.acquire(self.context, self.node.uuid, + shared=True) as task: + expected_properties = { + 'cpu_arch': 'mips', 'cpus': '8', + 'local_gb': '4', 'memory_mb': '4096' + } + task.driver.inspect.inspect_hardware(task) + self.assertEqual(expected_properties, task.node.properties) + + @mock.patch.object(redfish_utils, 'get_system', autospec=True) + @mock.patch.object(deploy_utils, 'create_ports_if_not_exist', + autospec=True) + def test_inspect_hardware_ignore_missing_nics( + self, mock_create_ports_if_not_exist, mock_get_system): + system_mock = self.init_system_mock(mock_get_system.return_value) + system_mock.ethernet_interfaces.eth_summary = None + + with task_manager.acquire(self.context, self.node.uuid, + shared=True) as task: + task.driver.inspect.inspect_hardware(task) + self.assertFalse(mock_create_ports_if_not_exist.called) diff --git a/ironic/tests/unit/drivers/modules/redfish/test_management.py b/ironic/tests/unit/drivers/modules/redfish/test_management.py index d42e4a4e76..f35ffce5e8 100644 --- a/ironic/tests/unit/drivers/modules/redfish/test_management.py +++ b/ironic/tests/unit/drivers/modules/redfish/test_management.py @@ -41,7 +41,8 @@ class RedfishManagementTestCase(db_base.DbTestCase): super(RedfishManagementTestCase, self).setUp() self.config(enabled_hardware_types=['redfish'], enabled_power_interfaces=['redfish'], - enabled_management_interfaces=['redfish']) + enabled_management_interfaces=['redfish'], + enabled_inspect_interfaces=['redfish']) self.node = obj_utils.create_test_node( self.context, driver='redfish', driver_info=INFO_DICT) diff --git a/ironic/tests/unit/drivers/modules/redfish/test_power.py b/ironic/tests/unit/drivers/modules/redfish/test_power.py index 2c8b964f62..2318b62175 100644 --- a/ironic/tests/unit/drivers/modules/redfish/test_power.py +++ b/ironic/tests/unit/drivers/modules/redfish/test_power.py @@ -41,7 +41,8 @@ class RedfishPowerTestCase(db_base.DbTestCase): super(RedfishPowerTestCase, self).setUp() self.config(enabled_hardware_types=['redfish'], enabled_power_interfaces=['redfish'], - enabled_management_interfaces=['redfish']) + enabled_management_interfaces=['redfish'], + enabled_inspect_interfaces=['redfish']) self.node = obj_utils.create_test_node( self.context, driver='redfish', driver_info=INFO_DICT) diff --git a/ironic/tests/unit/drivers/modules/test_deploy_utils.py b/ironic/tests/unit/drivers/modules/test_deploy_utils.py index 3cecc5ec1f..6f93548569 100644 --- a/ironic/tests/unit/drivers/modules/test_deploy_utils.py +++ b/ironic/tests/unit/drivers/modules/test_deploy_utils.py @@ -44,6 +44,7 @@ from ironic.drivers.modules import image_cache from ironic.drivers.modules import pxe from ironic.drivers.modules.storage import cinder from ironic.drivers import utils as driver_utils +from ironic import objects from ironic.tests import base as tests_base from ironic.tests.unit.db import base as db_base from ironic.tests.unit.db import utils as db_utils @@ -1307,6 +1308,38 @@ class OtherFunctionTestCase(db_base.DbTestCase): self.assertRaises(exception.InvalidParameterValue, utils.get_ironic_api_url) + @mock.patch.object(utils.LOG, 'info', spec_set=True, autospec=True) + @mock.patch.object(objects, 'Port', spec_set=True, autospec=True) + def test_create_ports_if_not_exist(self, port_mock, log_mock): + macs = {'Port 1': 'aa:aa:aa:aa:aa:aa', 'Port 2': 'bb:bb:bb:bb:bb:bb'} + node_id = self.node.id + port_dict1 = {'address': 'aa:aa:aa:aa:aa:aa', 'node_id': node_id} + port_dict2 = {'address': 'bb:bb:bb:bb:bb:bb', 'node_id': node_id} + port_obj1, port_obj2 = mock.MagicMock(), mock.MagicMock() + port_mock.side_effect = [port_obj1, port_obj2] + with task_manager.acquire(self.context, self.node.uuid, + shared=False) as task: + utils.create_ports_if_not_exist(task, macs) + self.assertTrue(log_mock.called) + expected_calls = [mock.call(task.context, **port_dict1), + mock.call(task.context, **port_dict2)] + port_mock.assert_has_calls(expected_calls, any_order=True) + port_obj1.create.assert_called_once_with() + port_obj2.create.assert_called_once_with() + + @mock.patch.object(utils.LOG, 'warning', + spec_set=True, autospec=True) + @mock.patch.object(objects.Port, 'create', spec_set=True, autospec=True) + def test_create_ports_if_not_exist_mac_exception(self, + create_mock, + log_mock): + create_mock.side_effect = exception.MACAlreadyExists('f') + macs = {'Port 1': 'aa:aa:aa:aa:aa:aa', 'Port 2': 'bb:bb:bb:bb:bb:bb'} + with task_manager.acquire(self.context, self.node.uuid, + shared=False) as task: + utils.create_ports_if_not_exist(task, macs) + self.assertEqual(2, log_mock.call_count) + class GetSingleNicTestCase(db_base.DbTestCase): diff --git a/ironic/tests/unit/drivers/test_redfish.py b/ironic/tests/unit/drivers/test_redfish.py index bddd63ce47..073df7d772 100644 --- a/ironic/tests/unit/drivers/test_redfish.py +++ b/ironic/tests/unit/drivers/test_redfish.py @@ -17,6 +17,7 @@ from ironic.conductor import task_manager from ironic.drivers.modules import iscsi_deploy from ironic.drivers.modules import noop from ironic.drivers.modules import pxe +from ironic.drivers.modules.redfish import inspect as redfish_inspect from ironic.drivers.modules.redfish import management as redfish_mgmt from ironic.drivers.modules.redfish import power as redfish_power from ironic.tests.unit.db import base as db_base @@ -29,11 +30,14 @@ class RedfishHardwareTestCase(db_base.DbTestCase): super(RedfishHardwareTestCase, self).setUp() self.config(enabled_hardware_types=['redfish'], enabled_power_interfaces=['redfish'], - enabled_management_interfaces=['redfish']) + enabled_management_interfaces=['redfish'], + enabled_inspect_interfaces=['redfish']) def test_default_interfaces(self): node = obj_utils.create_test_node(self.context, driver='redfish') with task_manager.acquire(self.context, node.id) as task: + self.assertIsInstance(task.driver.inspect, + redfish_inspect.RedfishInspect) self.assertIsInstance(task.driver.management, redfish_mgmt.RedfishManagement) self.assertIsInstance(task.driver.power, diff --git a/ironic/tests/unit/drivers/third_party_driver_mock_specs.py b/ironic/tests/unit/drivers/third_party_driver_mock_specs.py index a5b1c6b5a9..11fc9646d2 100644 --- a/ironic/tests/unit/drivers/third_party_driver_mock_specs.py +++ b/ironic/tests/unit/drivers/third_party_driver_mock_specs.py @@ -146,6 +146,11 @@ SUSHY_CONSTANTS_SPEC = ( 'BOOT_SOURCE_ENABLED_ONCE', 'BOOT_SOURCE_MODE_BIOS', 'BOOT_SOURCE_MODE_UEFI', + 'PROCESSOR_ARCH_x86', + 'PROCESSOR_ARCH_IA_64', + 'PROCESSOR_ARCH_ARM', + 'PROCESSOR_ARCH_MIPS', + 'PROCESSOR_ARCH_OEM', ) XCLARITY_SPEC = ( diff --git a/ironic/tests/unit/drivers/third_party_driver_mocks.py b/ironic/tests/unit/drivers/third_party_driver_mocks.py index fe955ba39b..fa60312910 100644 --- a/ironic/tests/unit/drivers/third_party_driver_mocks.py +++ b/ironic/tests/unit/drivers/third_party_driver_mocks.py @@ -219,7 +219,12 @@ if not sushy: BOOT_SOURCE_ENABLED_CONTINUOUS='continuous', BOOT_SOURCE_ENABLED_ONCE='once', BOOT_SOURCE_MODE_BIOS='bios', - BOOT_SOURCE_MODE_UEFI='uefi' + BOOT_SOURCE_MODE_UEFI='uefi', + PROCESSOR_ARCH_x86='x86 or x86-64', + PROCESSOR_ARCH_IA_64='Intel Itanium', + PROCESSOR_ARCH_ARM='ARM', + PROCESSOR_ARCH_MIPS='MIPS', + PROCESSOR_ARCH_OEM='OEM-defined', ) sys.modules['sushy'] = sushy diff --git a/releasenotes/notes/add-redfish-inspect-interface-1577e70167f24ae4.yaml b/releasenotes/notes/add-redfish-inspect-interface-1577e70167f24ae4.yaml new file mode 100644 index 0000000000..f5d066a892 --- /dev/null +++ b/releasenotes/notes/add-redfish-inspect-interface-1577e70167f24ae4.yaml @@ -0,0 +1,7 @@ +--- +features: + - | + Adds out-of-band inspection support to ``redfish`` hardware type'. + Successful inspection populates mandatory properties: "cpus", + "local_gb", "cpu_arch", "memory_mb" and creates ironic ports + for inspected nodes. diff --git a/setup.cfg b/setup.cfg index 1cf0f0e872..3c234debea 100644 --- a/setup.cfg +++ b/setup.cfg @@ -88,6 +88,7 @@ ironic.hardware.interfaces.inspect = inspector = ironic.drivers.modules.inspector:Inspector irmc = ironic.drivers.modules.irmc.inspect:IRMCInspect no-inspect = ironic.drivers.modules.noop:NoInspect + redfish = ironic.drivers.modules.redfish.inspect:RedfishInspect ironic.hardware.interfaces.management = cimc = ironic.drivers.modules.cimc.management:CIMCManagement