DRAC : idrac-redfish inspect updates pxe port

Extends generic Redfish OOB inspection to add setting pxe_enabled
on the discovered ports for the idrac hardware type.
It retrieves the list of PXE device MAC addresses using an OEM extension
to the Redfish API if necessary
and updates the pxe_enabled field of Port.

Change-Id: Ife8387a3bdb75717b896cf1067d2490084665e49
Story: 2006819
Task: 37380
Co-Authored-By: Mahendra Kamble <mahendra.kamble358@gmail.com>
Co-Authored-By: Sonali Borkar <sonaliborkar85@gmail.com>
This commit is contained in:
Pradip Kadam 2019-11-07 04:32:44 -05:00 committed by sonaliborkar85
parent 8e34aa53ce
commit 7d82d7f3dd
5 changed files with 433 additions and 7 deletions

View File

@ -20,31 +20,147 @@ from oslo_log import log as logging
from oslo_utils import importutils from oslo_utils import importutils
from oslo_utils import units from oslo_utils import units
from ironic.common import boot_modes
from ironic.common import exception from ironic.common import exception
from ironic.common.i18n import _ from ironic.common.i18n import _
from ironic.common import states from ironic.common import states
from ironic.common import utils from ironic.common import utils
from ironic.drivers import base from ironic.drivers import base
from ironic.drivers.modules.drac import common as drac_common 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 inspect as redfish_inspect
from ironic.drivers.modules.redfish import utils as redfish_utils
from ironic import objects from ironic import objects
drac_exceptions = importutils.try_import('dracclient.exceptions') drac_exceptions = importutils.try_import('dracclient.exceptions')
sushy = importutils.try_import('sushy')
LOG = logging.getLogger(__name__) LOG = logging.getLogger(__name__)
METRICS = metrics_utils.get_metrics_logger(__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): 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, def inspect_hardware(self, task):
vendor-independent Redfish interface. Future resolution of Dell EMC- """Inspect hardware to get the hardware properties.
specific incompatibilities and introduction of vendor value added
should be implemented by this class. Inspects hardware to get the essential properties.
""" It fails if any of the essential properties
pass 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): class DracWSManInspect(base.InspectInterface):

View File

@ -26,6 +26,7 @@ from ironic.drivers import base
from ironic.drivers.modules import inspect_utils from ironic.drivers.modules import inspect_utils
from ironic.drivers.modules.redfish import utils as redfish_utils from ironic.drivers.modules.redfish import utils as redfish_utils
from ironic.drivers import utils as drivers_utils from ironic.drivers import utils as drivers_utils
from ironic import objects
LOG = log.getLogger(__name__) LOG = log.getLogger(__name__)
@ -157,6 +158,32 @@ class RedfishInspect(base.InspectInterface):
self._create_ports(task, system) 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 return states.MANAGEABLE
def _create_ports(self, task, system): 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`` # value by 1 GB as consumers like Ironic requires the ``local_gb``
# to be returned 1 less than actual size. # to be returned 1 less than actual size.
return max(0, int(local_gb / units.Gi - 1)) 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

View File

@ -18,16 +18,23 @@ Test class for DRAC inspection interface
from unittest import mock from unittest import mock
from dracclient import exceptions as drac_exceptions 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 exception
from ironic.common import states from ironic.common import states
from ironic.conductor import task_manager from ironic.conductor import task_manager
from ironic.drivers.modules.drac import common as drac_common from ironic.drivers.modules.drac import common as drac_common
from ironic.drivers.modules.drac import inspect as drac_inspect 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 import objects
from ironic.tests.unit.drivers.modules.drac import utils as test_utils from ironic.tests.unit.drivers.modules.drac import utils as test_utils
from ironic.tests.unit.objects import utils as obj_utils from ironic.tests.unit.objects import utils as obj_utils
sushy = importutils.try_import('sushy')
INFO_DICT = test_utils.INFO_DICT INFO_DICT = test_utils.INFO_DICT
@ -538,3 +545,158 @@ class DracInspectionTestCase(test_utils.BaseDracTest):
mock_client, self.nics, self.node) mock_client, self.nics, self.node)
self.assertEqual(expected_pxe_nic, pxe_dev_nics) 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)

View File

@ -19,9 +19,12 @@ from oslo_utils import importutils
from oslo_utils import units from oslo_utils import units
from ironic.common import exception from ironic.common import exception
from ironic.common import states
from ironic.conductor import task_manager from ironic.conductor import task_manager
from ironic.drivers.modules import inspect_utils 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.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 base as db_base
from ironic.tests.unit.db import utils as db_utils from ironic.tests.unit.db import utils as db_utils
from ironic.tests.unit.objects import utils as obj_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) task.driver.inspect.inspect_hardware(task)
self.assertEqual(expected_properties, task.node.properties) 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))

View File

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