Merge "DRAC : idrac-redfish inspect updates pxe port"

This commit is contained in:
Zuul 2021-03-29 10:07:21 +00:00 committed by Gerrit Code Review
commit 6fe8e6beb8
5 changed files with 433 additions and 7 deletions
ironic
drivers/modules
tests/unit/drivers/modules
releasenotes/notes

@ -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):

@ -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

@ -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)

@ -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))

@ -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.