diff --git a/ironic/drivers/modules/drac/inspect.py b/ironic/drivers/modules/drac/inspect.py index 3218b3557c..3c7985e514 100644 --- a/ironic/drivers/modules/drac/inspect.py +++ b/ironic/drivers/modules/drac/inspect.py @@ -20,31 +20,147 @@ from oslo_log import log as logging from oslo_utils import importutils from oslo_utils import units +from ironic.common import boot_modes from ironic.common import exception from ironic.common.i18n import _ from ironic.common import states from ironic.common import utils from ironic.drivers import base from ironic.drivers.modules.drac import common as drac_common +from ironic.drivers.modules import inspect_utils from ironic.drivers.modules.redfish import inspect as redfish_inspect +from ironic.drivers.modules.redfish import utils as redfish_utils from ironic import objects drac_exceptions = importutils.try_import('dracclient.exceptions') +sushy = importutils.try_import('sushy') LOG = logging.getLogger(__name__) METRICS = metrics_utils.get_metrics_logger(__name__) +_PXE_DEV_ENABLED_INTERFACES = [('PxeDev1EnDis', 'PxeDev1Interface'), + ('PxeDev2EnDis', 'PxeDev2Interface'), + ('PxeDev3EnDis', 'PxeDev3Interface'), + ('PxeDev4EnDis', 'PxeDev4Interface')] +_BIOS_ENABLED_VALUE = 'Enabled' + class DracRedfishInspect(redfish_inspect.RedfishInspect): - """iDRAC Redfish interface for inspection-related actions. + """iDRAC Redfish interface for inspection-related actions.""" - Presently, this class entirely defers to its base class, a generic, - vendor-independent Redfish interface. Future resolution of Dell EMC- - specific incompatibilities and introduction of vendor value added - should be implemented by this class. - """ - pass + 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. + + """ + # Ensure we create a port for every NIC port found for consistency + # with our WSMAN inspect behavior and to work around a bug in some + # versions of the firmware where the port state is not being + # reported correctly. + + ethernet_interfaces_mac = self._get_mac_address(task) + inspect_utils.create_ports_if_not_exist(task, ethernet_interfaces_mac) + return super(DracRedfishInspect, self).inspect_hardware(task) + + def _get_mac_address(self, task): + """Get a list of MAC addresses + + :param task: a TaskManager instance. + :returns: Returns list of MAC addresses. + """ + system = redfish_utils.get_system(task.node) + # Get dictionary of ethernet interfaces + if system.ethernet_interfaces and system.ethernet_interfaces.summary: + ethernet_interfaces = system.ethernet_interfaces.get_members() + ethernet_interfaces_mac = { + interface.identity: interface.mac_address + for interface in ethernet_interfaces} + return ethernet_interfaces_mac + else: + return {} + + def _get_pxe_port_macs(self, task): + """Get a list of PXE port MAC addresses. + + :param task: a TaskManager instance. + :returns: Returns list of PXE port MAC addresses. + """ + system = redfish_utils.get_system(task.node) + ethernet_interfaces_mac = self._get_mac_address(task) + pxe_port_macs = [] + + if system.boot.mode == boot_modes.UEFI: + # When a server is in UEFI boot mode, the PXE NIC ports are + # stored in the PxeDevXEnDis and PxeDevXInterface BIOS + # settings. Get the PXE NIC ports from these settings and + # their MAC addresses. + for param, nic in _PXE_DEV_ENABLED_INTERFACES: + if system.bios.attributes[param] == _BIOS_ENABLED_VALUE: + nic_id = system.bios.attributes[nic] + # Get MAC address of the given nic_id + mac_address = ethernet_interfaces_mac[nic_id] + pxe_port_macs.append(mac_address) + elif system.boot.mode == boot_modes.LEGACY_BIOS: + # When a server is in BIOS boot mode, whether or not a + # NIC port is set to PXE boot is stored on the NIC port + # itself internally to the BMC. Getting this information + # requires using an OEM extension to export the system + # configuration, as the redfish standard does not specify + # how to get it, and Dell does not have OEM redfish calls + # to selectively retrieve it at this time. + # Get instance of Sushy OEM manager object + + for manager in system.managers: + try: + # Get instance of Sushy OEM manager object + oem_manager = manager.get_oem_extension('Dell') + except sushy.exceptions.OEMExtensionNotFoundError as e: + error_msg = (_("Search for Sushy OEM extension package " + "'sushy-oem-idrac' failed for node " + "%(node)s. Ensure it's installed. " + " Error: %(error)s") % + {'node': task.node.uuid, 'error': e}) + LOG.error(error_msg) + raise exception.RedfishError(error=error_msg) + + try: + pxe_port_macs_list = oem_manager.get_pxe_port_macs_bios( + ethernet_interfaces_mac) + pxe_port_macs = [mac for mac in pxe_port_macs_list] + return pxe_port_macs + except sushy.exceptions.OEMExtensionNotFoundError as e: + error_msg = (_("Search for Sushy OEM extension package " + "'sushy-oem-idrac' failed for node " + " %(node)s. Ensure it is installed. " + "Error: %(error)s") % + {'node': task.node.uuid, 'error': e}) + LOG.debug(error_msg) + continue + LOG.info("Get pxe port MAC addresses for %(node)s via OEM", + {'node': task.node.uuid}) + break + else: + error_msg = (_('iDRAC Redfish Get pxe port MAC addresse ' + 'failed for node %(node)s, because system ' + '%(system)s has no ' + 'manager%(no_manager)s.') % + {'node': task.node.uuid, + 'system': system.uuid if system.uuid else + system.identity, + 'no_manager': '' if not system.managers else + ' which could'}) + LOG.error(error_msg) + raise exception.RedfishError(error=error_msg) + return pxe_port_macs class DracWSManInspect(base.InspectInterface): diff --git a/ironic/drivers/modules/redfish/inspect.py b/ironic/drivers/modules/redfish/inspect.py index 10344c95db..4e968e4b36 100644 --- a/ironic/drivers/modules/redfish/inspect.py +++ b/ironic/drivers/modules/redfish/inspect.py @@ -26,6 +26,7 @@ from ironic.drivers import base from ironic.drivers.modules import inspect_utils from ironic.drivers.modules.redfish import utils as redfish_utils from ironic.drivers import utils as drivers_utils +from ironic import objects LOG = log.getLogger(__name__) @@ -157,6 +158,32 @@ class RedfishInspect(base.InspectInterface): self._create_ports(task, system) + pxe_port_macs = self._get_pxe_port_macs(task) + if pxe_port_macs is None: + LOG.warning("No PXE enabled NIC was found for node " + "%(node_uuid)s.", {'node_uuid': task.node.uuid}) + else: + pxe_port_macs = [macs.lower() for macs in pxe_port_macs] + + ports = objects.Port.list_by_node_id(task.context, task.node.id) + if ports: + for port in ports: + is_baremetal_pxe_port = (port.address.lower() + in pxe_port_macs) + if port.pxe_enabled != is_baremetal_pxe_port: + port.pxe_enabled = is_baremetal_pxe_port + port.save() + LOG.info('Port %(port)s having %(mac_address)s ' + 'updated with pxe_enabled %(pxe)s for ' + 'node %(node_uuid)s during inspection', + {'port': port.uuid, + 'mac_address': port.address, + 'pxe': port.pxe_enabled, + 'node_uuid': task.node.uuid}) + else: + LOG.warning("No port information discovered " + "for node %(node)s", {'node': task.node.uuid}) + return states.MANAGEABLE def _create_ports(self, task, system): @@ -236,3 +263,12 @@ class RedfishInspect(base.InspectInterface): # value by 1 GB as consumers like Ironic requires the ``local_gb`` # to be returned 1 less than actual size. return max(0, int(local_gb / units.Gi - 1)) + + def _get_pxe_port_macs(self, task): + """Get a list of PXE port MAC addresses. + + :param task: a TaskManager instance. + :returns: Returns list of PXE port MAC addresses. + If cannot be determined, returns None. + """ + return None diff --git a/ironic/tests/unit/drivers/modules/drac/test_inspect.py b/ironic/tests/unit/drivers/modules/drac/test_inspect.py index 64dc6d39f6..70e94ce49e 100644 --- a/ironic/tests/unit/drivers/modules/drac/test_inspect.py +++ b/ironic/tests/unit/drivers/modules/drac/test_inspect.py @@ -18,16 +18,23 @@ Test class for DRAC inspection interface from unittest import mock from dracclient import exceptions as drac_exceptions +from oslo_utils import importutils +from oslo_utils import units from ironic.common import exception from ironic.common import states from ironic.conductor import task_manager from ironic.drivers.modules.drac import common as drac_common from ironic.drivers.modules.drac import inspect as drac_inspect +from ironic.drivers.modules import inspect_utils +from ironic.drivers.modules.redfish import inspect as redfish_inspect +from ironic.drivers.modules.redfish import utils as redfish_utils from ironic import objects from ironic.tests.unit.drivers.modules.drac import utils as test_utils from ironic.tests.unit.objects import utils as obj_utils +sushy = importutils.try_import('sushy') + INFO_DICT = test_utils.INFO_DICT @@ -538,3 +545,158 @@ class DracInspectionTestCase(test_utils.BaseDracTest): mock_client, self.nics, self.node) self.assertEqual(expected_pxe_nic, pxe_dev_nics) + + +class DracRedfishInspectionTestCase(test_utils.BaseDracTest): + def setUp(self): + super(DracRedfishInspectionTestCase, self).setUp() + self.config(enabled_hardware_types=['idrac'], + enabled_power_interfaces=['idrac-redfish'], + enabled_management_interfaces=['idrac-redfish'], + enabled_inspect_interfaces=['idrac-redfish']) + self.node = obj_utils.create_test_node( + self.context, driver='idrac', + driver_info=INFO_DICT) + + def init_system_mock(self, system_mock, **properties): + system_mock.reset() + system_mock.boot.mode = 'uefi' + system_mock.bios.attributes = { + 'PxeDev1EnDis': 'Enabled', 'PxeDev2EnDis': 'Disabled', + 'PxeDev3EnDis': 'Disabled', 'PxeDev4EnDis': 'Disabled', + 'PxeDev1Interface': 'NIC.Integrated.1-1-1', + 'PxeDev2Interface': None, 'PxeDev3Interface': None, + 'PxeDev4Interface': None} + + 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.summary = { + '00:11:22:33:44:55': sushy.STATE_ENABLED, + '24:6E:96:70:49:00': sushy.STATE_DISABLED} + member_data = [{ + 'description': 'Integrated NIC 1 Port 1 Partition 1', + 'name': 'System Ethernet Interface', + 'full_duplex': False, + 'identity': 'NIC.Integrated.1-1-1', + 'mac_address': '24:6E:96:70:49:00', + 'mtu_size': None, + 'speed_mbps': 0, + 'vlan': None}] + system_mock.ethernet_interfaces.get_members.return_value = [ + test_utils.dict_to_namedtuple(values=interface) + for interface in member_data + ] + return system_mock + + def test_get_properties(self): + expected = redfish_utils.COMMON_PROPERTIES + driver = drac_inspect.DracRedfishInspect() + self.assertEqual(expected, driver.get_properties()) + + @mock.patch.object(redfish_utils, 'get_system', autospec=True) + def test__get_pxe_port_macs_with_UEFI_boot_mode(self, mock_get_system): + system_mock = self.init_system_mock(mock_get_system.return_value) + system_mock.boot.mode = 'uefi' + expected_pxe_mac = ['24:6E:96:70:49:00'] + with task_manager.acquire(self.context, self.node.uuid, + shared=True) as task: + pxe_port_macs = task.driver.inspect._get_pxe_port_macs(task) + self.assertEqual(expected_pxe_mac, pxe_port_macs) + + @mock.patch.object(redfish_utils, 'get_system', autospec=True) + def test__get_pxe_port_macs_with_BIOS_boot_mode(self, mock_get_system): + system_mock = self.init_system_mock(mock_get_system.return_value) + system_mock.boot.mode = 'bios' + mock_manager = mock.MagicMock() + system_mock.managers = [mock_manager] + mock_manager_oem = mock_manager.get_oem_extension.return_value + mock_manager_oem.get_pxe_port_macs_bios.return_value = \ + ['24:6E:96:70:49:00'] + expected_pxe_mac = ['24:6E:96:70:49:00'] + with task_manager.acquire(self.context, self.node.uuid, + shared=True) as task: + pxe_port_macs = task.driver.inspect._get_pxe_port_macs(task) + self.assertEqual(expected_pxe_mac, pxe_port_macs) + + @mock.patch.object(redfish_utils, 'get_system', autospec=True) + def test__get_pxe_port_macs_without_boot_mode(self, mock_get_system): + system_mock = self.init_system_mock(mock_get_system.return_value) + system_mock.boot.mode = None + expected_pxe_mac = [] + with task_manager.acquire(self.context, self.node.uuid, + shared=True) as task: + pxe_port_macs = task.driver.inspect._get_pxe_port_macs(task) + self.assertEqual(expected_pxe_mac, pxe_port_macs) + + @mock.patch.object(redfish_utils, 'get_system', autospec=True) + def test__get_pxe_port_macs_missing_oem(self, mock_get_system): + mock_system = self.init_system_mock(mock_get_system.return_value) + mock_manager = mock.MagicMock() + mock_system.boot.mode = 'bios' + mock_system.managers = [mock_manager] + set_mgr = ( + mock_manager.get_oem_extension.return_value.get_pxe_port_macs_bios) + set_mgr.side_effect = sushy.exceptions.OEMExtensionNotFoundError + + with task_manager.acquire(self.context, self.node.uuid, + shared=False) as task: + self.assertRaises(exception.RedfishError, + task.driver.inspect._get_pxe_port_macs, + task) + + @mock.patch.object(redfish_utils, 'get_system', autospec=True) + def test__get_pxe_port_macs_no_manager(self, mock_get_system): + mock_system = self.init_system_mock(mock_get_system.return_value) + mock_system.boot.mode = 'bios' + mock_system.managers = [] + with task_manager.acquire(self.context, self.node.uuid, + shared=True) as task: + self.assertRaises(exception.RedfishError, + task.driver.inspect._get_pxe_port_macs, + task) + + @mock.patch.object(redfish_inspect.RedfishInspect, 'inspect_hardware', + autospec=True) + @mock.patch.object(inspect_utils, 'create_ports_if_not_exist', + autospec=True) + def test_inspect_hardware_with_ethernet_interfaces_mac( + self, mock_create_ports_if_not_exist, mock_inspect_hardware): + ethernet_interfaces_mac = {'NIC.Integrated.1-1-1': + '24:6E:96:70:49:00'} + mock_inspect_hardware.return_value = states.MANAGEABLE + with task_manager.acquire(self.context, self.node.uuid, + shared=True) as task: + task.driver.inspect._get_mac_address = mock.Mock() + task.driver.inspect._get_mac_address.return_value = \ + ethernet_interfaces_mac + return_value = task.driver.inspect.inspect_hardware(task) + self.assertEqual(states.MANAGEABLE, return_value) + mock_create_ports_if_not_exist.assert_called_once_with( + task, ethernet_interfaces_mac) + + @mock.patch.object(redfish_utils, 'get_system', autospec=True) + def test__get_mac_address_with_ethernet_interfaces(self, mock_get_system): + self.init_system_mock(mock_get_system.return_value) + expected_value = {'NIC.Integrated.1-1-1': '24:6E:96:70:49:00'} + with task_manager.acquire(self.context, self.node.uuid, + shared=True) as task: + return_value = task.driver.inspect._get_mac_address(task) + self.assertEqual(expected_value, return_value) + + @mock.patch.object(redfish_utils, 'get_system', autospec=True) + def test__get_mac_address_without_ethernet_interfaces(self, + mock_get_system): + mock_system = self.init_system_mock(mock_get_system.return_value) + mock_system.ethernet_interfaces.summary = None + expected_value = {} + with task_manager.acquire(self.context, self.node.uuid, + shared=True) as task: + return_value = task.driver.inspect._get_mac_address(task) + self.assertEqual(expected_value, return_value) diff --git a/ironic/tests/unit/drivers/modules/redfish/test_inspect.py b/ironic/tests/unit/drivers/modules/redfish/test_inspect.py index 92b2b64f22..f67f6eb4c6 100644 --- a/ironic/tests/unit/drivers/modules/redfish/test_inspect.py +++ b/ironic/tests/unit/drivers/modules/redfish/test_inspect.py @@ -19,9 +19,12 @@ from oslo_utils import importutils from oslo_utils import units from ironic.common import exception +from ironic.common import states from ironic.conductor import task_manager from ironic.drivers.modules import inspect_utils +from ironic.drivers.modules.redfish import inspect from ironic.drivers.modules.redfish import utils as redfish_utils +from ironic import objects 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 @@ -235,3 +238,105 @@ class RedfishInspectTestCase(db_base.DbTestCase): } task.driver.inspect.inspect_hardware(task) self.assertEqual(expected_properties, task.node.properties) + + @mock.patch.object(objects.Port, 'list_by_node_id') # noqa + @mock.patch.object(redfish_utils, 'get_system', autospec=True) + def test_inspect_hardware_with_set_port_pxe_enabled( + self, mock_get_system, mock_list_by_node_id): + self.init_system_mock(mock_get_system.return_value) + + pxe_disabled_port = obj_utils.create_test_port( + self.context, uuid=self.node.uuid, node_id=self.node.id, + address='24:6E:96:70:49:00', pxe_enabled=False) + mock_list_by_node_id.return_value = [pxe_disabled_port] + port = mock_list_by_node_id.return_value + with task_manager.acquire(self.context, self.node.uuid, + shared=True) as task: + task.driver.inspect._get_pxe_port_macs = mock.Mock() + task.driver.inspect._get_pxe_port_macs.return_value = \ + ['24:6E:96:70:49:00'] + task.driver.inspect.inspect_hardware(task) + self.assertTrue(port[0].pxe_enabled) + + @mock.patch.object(objects.Port, 'list_by_node_id') # noqa + @mock.patch.object(redfish_utils, 'get_system', autospec=True) + def test_inspect_hardware_with_set_port_pxe_disabled( + self, mock_get_system, mock_list_by_node_id): + self.init_system_mock(mock_get_system.return_value) + + pxe_enabled_port = obj_utils.create_test_port( + self.context, uuid=self.node.uuid, + node_id=self.node.id, address='24:6E:96:70:49:01', + pxe_enabled=True) + mock_list_by_node_id.return_value = [pxe_enabled_port] + + with task_manager.acquire(self.context, self.node.uuid, + shared=True) as task: + task.driver.inspect._get_pxe_port_macs = mock.Mock() + task.driver.inspect._get_pxe_port_macs.return_value = \ + ['24:6E:96:70:49:00'] + task.driver.inspect.inspect_hardware(task) + port = mock_list_by_node_id.return_value + self.assertFalse(port[0].pxe_enabled) + + @mock.patch.object(objects.Port, 'list_by_node_id') # noqa + @mock.patch.object(redfish_utils, 'get_system', autospec=True) + def test_inspect_hardware_with_empty_pxe_port_macs( + self, mock_get_system, mock_list_by_node_id): + self.init_system_mock(mock_get_system.return_value) + + pxe_enabled_port = obj_utils.create_test_port( + self.context, uuid=self.node.uuid, + node_id=self.node.id, address='24:6E:96:70:49:01', + pxe_enabled=True) + mock_list_by_node_id.return_value = [pxe_enabled_port] + + with task_manager.acquire(self.context, self.node.uuid, + shared=True) as task: + task.driver.inspect._get_pxe_port_macs = mock.Mock() + task.driver.inspect._get_pxe_port_macs.return_value = [] + return_value = task.driver.inspect.inspect_hardware(task) + port = mock_list_by_node_id.return_value + self.assertFalse(port[0].pxe_enabled) + self.assertEqual(states.MANAGEABLE, return_value) + + @mock.patch.object(objects.Port, 'list_by_node_id') # noqa + @mock.patch.object(redfish_utils, 'get_system', autospec=True) + @mock.patch.object(inspect.LOG, 'warning', autospec=True) + def test_inspect_hardware_with_none_pxe_port_macs( + self, mock_log, mock_get_system, mock_list_by_node_id): + self.init_system_mock(mock_get_system.return_value) + + pxe_enabled_port = obj_utils.create_test_port( + self.context, uuid=self.node.uuid, + node_id=self.node.id, address='24:6E:96:70:49:01', + pxe_enabled=True) + mock_list_by_node_id.return_value = [pxe_enabled_port] + + with task_manager.acquire(self.context, self.node.uuid, + shared=True) as task: + task.driver.inspect._get_pxe_port_macs = mock.Mock() + task.driver.inspect._get_pxe_port_macs.return_value = None + task.driver.inspect.inspect_hardware(task) + port = mock_list_by_node_id.return_value + self.assertTrue(port[0].pxe_enabled) + mock_log.assert_called_once() + + @mock.patch.object(redfish_utils, 'get_system', autospec=True) + def test_create_port_when_its_state_is_none(self, mock_get_system): + self.init_system_mock(mock_get_system.return_value) + expected_port_mac_list = ["00:11:22:33:44:55", "24:6e:96:70:49:00"] + with task_manager.acquire(self.context, self.node.uuid, + shared=True) as task: + task.driver.inspect.inspect_hardware(task) + ports = objects.Port.list_by_node_id(task.context, self.node.id) + for port in ports: + self.assertIn(port.address, expected_port_mac_list) + + def test_get_pxe_port_macs(self): + expected_properties = None + with task_manager.acquire(self.context, self.node.uuid, + shared=True) as task: + task.driver.inspect._get_pxe_port_macs(task) + self.assertEqual(expected_properties, + task.driver.inspect._get_pxe_port_macs(task)) diff --git a/releasenotes/notes/add-pxe-nic-support-in-redfish-5359897135df1348.yaml b/releasenotes/notes/add-pxe-nic-support-in-redfish-5359897135df1348.yaml new file mode 100644 index 0000000000..63552890eb --- /dev/null +++ b/releasenotes/notes/add-pxe-nic-support-in-redfish-5359897135df1348.yaml @@ -0,0 +1,7 @@ +--- +features: + - | + Adds support for the discovery of PXE Enabled NICs using the + ``idrac-redfish`` inspect interface with the ``idrac`` hardware + type. With this feature, a port's ``pxe_enabled`` status will + be recorded on the bare metal port.