From 0ad6f8758747cf0d0796c89e0e8f8bdbf6f0421d Mon Sep 17 00:00:00 2001 From: Ilya Etingof Date: Wed, 24 Jul 2019 12:48:59 +0200 Subject: [PATCH] Add Redfish vmedia boot interface to idrac HW type This change adds idrac hardware type support of a virtual media boot interface implementation that utilizes the Redfish out-of-band (OOB) management protocol and is compatible with the integrated Dell Remote Access Controller (iDRAC) baseboard management controller (BMC). It is named 'idrac-redfish-virtual-media'. The iDRAC Redfish Service almost entirely interoperates with the virtual media boot workflow suggested by the Redfish standard. The only difference is configuring the system to boot from the inserted virtual media. The standard workflow expects it to be referred to as a CD-ROM or floppy disk drive boot source, no different from their physical counterparts. However, the iDRAC refers to them as virtual boot sources, distinct from their physical counterparts. Presently, the standard does not define virtual CD-ROM nor virtual floppy disk drive boot sources. However, the iDRAC provides a Redfish OEM extension for setting the system to boot from one of those virtual boot sources. To circumvent the above issue, the Python class which implements 'idrac-redfish-virtual-media' is derived from the class which implements the generic, vendor-independent 'redfish-virtual-media' interface. It overrides the method which sets the boot device to facilitate use of the aforementioned iDRAC Redfish Service OEM extension. The idrac hardware type declares support for that new interface implementation, in addition to all boot interface implementations it has been supporting. The priority order is retained by assigning the new 'idrac-redfish-virtual-media' the lowest priority. A new idrac hardware type Python package dependency is introduced. It is on 'sushy-oem-idrac'. [1] https://pypi.org/project/sushy-oem-idrac/ Co-Authored-By: Richard G. Pioso Story: 2006570 Task: 36675 Change-Id: I416019fc1ed3ab2a3a3dbc4443571123ef90e327 --- driver-requirements.txt | 3 + ironic/drivers/drac.py | 8 + ironic/drivers/modules/drac/boot.py | 161 +++++++++++++++++ .../unit/drivers/modules/drac/test_boot.py | 167 ++++++++++++++++++ .../tests/unit/drivers/modules/drac/utils.py | 2 + ironic/tests/unit/drivers/test_drac.py | 16 +- .../unit/drivers/third_party_driver_mocks.py | 2 + ...redfish-boot-support-036396b48d3f71f4.yaml | 21 +++ setup.cfg | 1 + 9 files changed, 379 insertions(+), 2 deletions(-) create mode 100644 ironic/drivers/modules/drac/boot.py create mode 100644 ironic/tests/unit/drivers/modules/drac/test_boot.py create mode 100644 releasenotes/notes/idrac-add-redfish-boot-support-036396b48d3f71f4.yaml diff --git a/driver-requirements.txt b/driver-requirements.txt index beb5421862..e3ea2d2d37 100644 --- a/driver-requirements.txt +++ b/driver-requirements.txt @@ -18,3 +18,6 @@ ansible>=2.4 # HUAWEI iBMC hardware type uses the python-ibmcclient library python-ibmcclient>=0.1.0 + +# Dell EMC iDRAC sushy OEM extension +sushy-oem-idrac<=0.1.0 diff --git a/ironic/drivers/drac.py b/ironic/drivers/drac.py index 73afa57d39..2c6a1e6e5a 100644 --- a/ironic/drivers/drac.py +++ b/ironic/drivers/drac.py @@ -18,13 +18,16 @@ DRAC Driver for remote system management using Dell Remote Access Card. from oslo_config import cfg from ironic.drivers import generic +from ironic.drivers.modules.drac import boot from ironic.drivers.modules.drac import inspect as drac_inspect from ironic.drivers.modules.drac import management from ironic.drivers.modules.drac import power from ironic.drivers.modules.drac import raid from ironic.drivers.modules.drac import vendor_passthru from ironic.drivers.modules import inspector +from ironic.drivers.modules import ipxe from ironic.drivers.modules import noop +from ironic.drivers.modules import pxe CONF = cfg.CONF @@ -35,6 +38,11 @@ class IDRACHardware(generic.GenericHardware): # Required hardware interfaces + @property + def supported_boot_interfaces(self): + """List of supported boot interfaces.""" + return [ipxe.iPXEBoot, pxe.PXEBoot, boot.DracRedfishVirtualMediaBoot] + @property def supported_management_interfaces(self): """List of supported management interfaces.""" diff --git a/ironic/drivers/modules/drac/boot.py b/ironic/drivers/modules/drac/boot.py new file mode 100644 index 0000000000..771dc7f5ec --- /dev/null +++ b/ironic/drivers/modules/drac/boot.py @@ -0,0 +1,161 @@ +# Copyright 2019 Red Hat, Inc. +# All Rights Reserved. +# Copyright (c) 2019 Dell Inc. or its subsidiaries. +# +# 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. + +from oslo_log import log +from oslo_utils import importutils + +from ironic.common import boot_devices +from ironic.common import exception +from ironic.common.i18n import _ +from ironic.drivers.modules.redfish import boot as redfish_boot +from ironic.drivers.modules.redfish import utils as redfish_utils + +LOG = log.getLogger(__name__) + +sushy = importutils.try_import('sushy') + + +class DracRedfishVirtualMediaBoot(redfish_boot.RedfishVirtualMediaBoot): + """iDRAC Redfish interface for virtual media boot-related actions. + + Virtual Media allows booting the system from "virtual" + CD/DVD drive containing user image that BMC "inserts" + into the drive. + + The CD/DVD images must be in ISO format and (depending on + BMC implementation) could be pulled over HTTP, served as + iSCSI targets or NFS volumes. + + The baseline boot workflow is mostly based on the standard + Redfish virtual media boot interface, which looks like + this: + + 1. Pull kernel, ramdisk and ESP if UEFI boot is requested (FAT partition + image with EFI boot loader) images + 2. Create bootable ISO out of images (#1), push it to Glance and + pass to the BMC as Swift temporary URL + 3. Optionally create floppy image with desired system configuration data, + push it to Glance and pass to the BMC as Swift temporary URL + 4. Insert CD/DVD and (optionally) floppy images and set proper boot mode + + For building deploy or rescue ISO, redfish boot interface uses + `deploy_kernel`/`deploy_ramdisk` or `rescue_kernel`/`rescue_ramdisk` + properties from `[instance_info]` or `[driver_info]`. + + For building boot (user) ISO, redfish boot interface seeks `kernel_id` + and `ramdisk_id` properties in the Glance image metadata found in + `[instance_info]image_source` node property. + + iDRAC virtual media boot interface only differs by the way how it + sets the node to boot from a virtual media device - this is done + via OEM action call implemented in Dell sushy OEM extension package. + """ + + if sushy: + VIRTUAL_MEDIA_DEVICES = { + boot_devices.FLOPPY: sushy.VIRTUAL_MEDIA_FLOPPY, + boot_devices.CDROM: sushy.VIRTUAL_MEDIA_CD + } + + @classmethod + def _set_boot_device(cls, task, device, persistent=False): + """Set boot device for a node. + + Dell iDRAC Redfish implementation does not support setting + boot device to virtual media via standard Redfish means. + Instead, Dell BMC sets boot device to local physical CD/floppy. + However, it is still feasible to boot from a virtual media + device by invoking Dell OEM extension. + + :param task: a TaskManager instance. + :param device: the boot device, one of + :mod:`ironic.common.boot_devices`. + :param persistent: Whether to set next-boot, or make the change + permanent. Default: False. + :raises: InvalidParameterValue if the validation of the + ManagementInterface fails. + """ + # NOTE(etingof): always treat CD/floppy as virtual + if device not in cls.VIRTUAL_MEDIA_DEVICES: + LOG.debug( + 'Treating boot device %(device)s as a non-virtual ' + 'media device for node %(node)s', + {'device': device, 'node': task.node.uuid}) + super(DracRedfishVirtualMediaBoot, cls)._set_boot_device( + task, device, persistent) + return + + device = cls.VIRTUAL_MEDIA_DEVICES[device] + + system = redfish_utils.get_system(task.node) + + for manager in system.managers: + + # This call makes Sushy go fishing in the ocean of Sushy + # OEM extensions installed on the system. If it finds one + # for 'Dell' which implements the 'Manager' resource + # extension, it uses it to create an object which + # instantiates itself from the OEM JSON. The object is + # returned here. + # + # If the extension could not be found for one manager, it + # will not be found for any others until it is installed, so + # abruptly exit the for loop. The vendor and resource name, + # 'Dell' and 'Manager', respectively, used to search for the + # extension are invariant in the loop. + try: + manager_oem = manager.get_oem_extension('Dell') + except sushy.exceptions.OEMExtensionNotFoundError as e: + error_msg = (_("Search for Sushy OEM extension Python package " + "'sushy-oem-idrac' failed for node %(node)s. " + "Ensure it is installed. Error: %(error)s") % + {'node': task.node.uuid, 'error': e}) + LOG.error(error_msg) + raise exception.RedfishError(error=error_msg) + + try: + manager_oem.set_virtual_boot_device( + device, persistent=persistent, manager=manager, + system=system) + except sushy.exceptions.SushyError as e: + LOG.debug("Sushy OEM extension Python package " + "'sushy-oem-idrac' failed to set virtual boot " + "device with system %(system)s manager %(manager)s " + "for node %(node)s. Will try next manager, if " + "available. Error: %(error)s", + {'system': system.uuid if system.uuid else + system.identity, + 'manager': manager.uuid if manager.uuid else + manager.identity, + 'node': task.node.uuid, + 'error': e}) + continue + + LOG.info("Set node %(node)s boot device to %(device)s via OEM", + {'node': task.node.uuid, 'device': device}) + break + + else: + error_msg = (_('iDRAC Redfish set boot device 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) diff --git a/ironic/tests/unit/drivers/modules/drac/test_boot.py b/ironic/tests/unit/drivers/modules/drac/test_boot.py new file mode 100644 index 0000000000..ec8ae4fe68 --- /dev/null +++ b/ironic/tests/unit/drivers/modules/drac/test_boot.py @@ -0,0 +1,167 @@ +# Copyright 2019 Red Hat, Inc. +# All Rights Reserved. +# Copyright (c) 2019 Dell Inc. or its subsidiaries. +# +# Licensed under the Apache License, Version 2.0 (the "License"); you may +# not use this file except in compliance with the License. You may obtain +# a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT +# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the +# License for the specific language governing permissions and limitations +# under the License. + +""" +Test class for DRAC boot interface +""" + +import mock +from oslo_utils import importutils + +from ironic.common import boot_devices +from ironic.common import exception +from ironic.conductor import task_manager +from ironic.drivers.modules.drac import boot as drac_boot +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 + + +@mock.patch.object(drac_boot, 'redfish_utils', autospec=True) +class DracBootTestCase(test_utils.BaseDracTest): + + def setUp(self): + super(DracBootTestCase, self).setUp() + self.node = obj_utils.create_test_node( + self.context, driver='idrac', driver_info=INFO_DICT) + + def test__set_boot_device_persistent(self, mock_redfish_utils): + + mock_system = mock_redfish_utils.get_system.return_value + + mock_manager = mock.MagicMock() + + mock_system.managers = [mock_manager] + + mock_manager_oem = mock_manager.get_oem_extension.return_value + + with task_manager.acquire(self.context, self.node.uuid, + shared=False) as task: + task.driver.boot._set_boot_device( + task, boot_devices.CDROM, persistent=True) + + mock_manager_oem.set_virtual_boot_device.assert_called_once_with( + 'cd', persistent=True, manager=mock_manager, + system=mock_system) + + def test__set_boot_device_cd(self, mock_redfish_utils): + + mock_system = mock_redfish_utils.get_system.return_value + + mock_manager = mock.MagicMock() + + mock_system.managers = [mock_manager] + + mock_manager_oem = mock_manager.get_oem_extension.return_value + + with task_manager.acquire(self.context, self.node.uuid, + shared=False) as task: + task.driver.boot._set_boot_device(task, boot_devices.CDROM) + + mock_manager_oem.set_virtual_boot_device.assert_called_once_with( + 'cd', persistent=False, manager=mock_manager, + system=mock_system) + + def test__set_boot_device_floppy(self, mock_redfish_utils): + + mock_system = mock_redfish_utils.get_system.return_value + + mock_manager = mock.MagicMock() + + mock_system.managers = [mock_manager] + + mock_manager_oem = mock_manager.get_oem_extension.return_value + + with task_manager.acquire(self.context, self.node.uuid, + shared=False) as task: + task.driver.boot._set_boot_device(task, boot_devices.FLOPPY) + + mock_manager_oem.set_virtual_boot_device.assert_called_once_with( + 'floppy', persistent=False, manager=mock_manager, + system=mock_system) + + def test__set_boot_device_disk(self, mock_redfish_utils): + + mock_system = mock_redfish_utils.get_system.return_value + + with task_manager.acquire(self.context, self.node.uuid, + shared=False) as task: + task.driver.boot._set_boot_device(task, boot_devices.DISK) + + self.assertFalse(mock_system.called) + + def test__set_boot_device_missing_oem(self, mock_redfish_utils): + + mock_system = mock_redfish_utils.get_system.return_value + + mock_manager = mock.MagicMock() + + mock_system.managers = [mock_manager] + + mock_manager.get_oem_extension.side_effect = ( + sushy.exceptions.OEMExtensionNotFoundError) + + with task_manager.acquire(self.context, self.node.uuid, + shared=False) as task: + self.assertRaises(exception.RedfishError, + task.driver.boot._set_boot_device, + task, boot_devices.CDROM) + + def test__set_boot_device_failover(self, mock_redfish_utils): + + mock_system = mock_redfish_utils.get_system.return_value + + mock_manager_fail = mock.MagicMock() + mock_manager_ok = mock.MagicMock() + + mock_system.managers = [mock_manager_fail, mock_manager_ok] + + mock_svbd_fail = (mock_manager_fail.get_oem_extension + .return_value.set_virtual_boot_device) + + mock_svbd_ok = (mock_manager_ok.get_oem_extension + .return_value.set_virtual_boot_device) + + mock_svbd_fail.side_effect = sushy.exceptions.SushyError + + with task_manager.acquire(self.context, self.node.uuid, + shared=False) as task: + task.driver.boot._set_boot_device(task, boot_devices.CDROM) + + self.assertFalse(mock_system.called) + + mock_svbd_fail.assert_called_once_with( + 'cd', manager=mock_manager_fail, persistent=False, + system=mock_system) + + mock_svbd_ok.assert_called_once_with( + 'cd', manager=mock_manager_ok, persistent=False, + system=mock_system) + + def test__set_boot_device_no_manager(self, mock_redfish_utils): + + mock_system = mock_redfish_utils.get_system.return_value + + mock_system.managers = [] + + with task_manager.acquire(self.context, self.node.uuid, + shared=False) as task: + self.assertRaises(exception.RedfishError, + task.driver.boot._set_boot_device, + task, boot_devices.CDROM) diff --git a/ironic/tests/unit/drivers/modules/drac/utils.py b/ironic/tests/unit/drivers/modules/drac/utils.py index f9bc6fc634..1828f3faf5 100644 --- a/ironic/tests/unit/drivers/modules/drac/utils.py +++ b/ironic/tests/unit/drivers/modules/drac/utils.py @@ -29,6 +29,8 @@ class BaseDracTest(db_base.DbTestCase): def setUp(self): super(BaseDracTest, self).setUp() self.config(enabled_hardware_types=['idrac', 'fake-hardware'], + enabled_boot_interfaces=[ + 'idrac-redfish-virtual-media', 'fake'], enabled_power_interfaces=['idrac-wsman', 'fake'], enabled_management_interfaces=['idrac-wsman', 'fake'], enabled_inspect_interfaces=[ diff --git a/ironic/tests/unit/drivers/test_drac.py b/ironic/tests/unit/drivers/test_drac.py index 3dfe50424c..c5b56d0193 100644 --- a/ironic/tests/unit/drivers/test_drac.py +++ b/ironic/tests/unit/drivers/test_drac.py @@ -16,10 +16,10 @@ from ironic.conductor import task_manager from ironic.drivers.modules import agent from ironic.drivers.modules import drac from ironic.drivers.modules import inspector +from ironic.drivers.modules import ipxe from ironic.drivers.modules import iscsi_deploy from ironic.drivers.modules.network import flat as flat_net from ironic.drivers.modules import noop -from ironic.drivers.modules import pxe from ironic.drivers.modules.storage import noop as noop_storage from ironic.tests.unit.db import base as db_base from ironic.tests.unit.objects import utils as obj_utils @@ -29,7 +29,10 @@ class IDRACHardwareTestCase(db_base.DbTestCase): def setUp(self): super(IDRACHardwareTestCase, self).setUp() + self.config_temp_dir('http_root', group='deploy') self.config(enabled_hardware_types=['idrac'], + enabled_boot_interfaces=[ + 'idrac-redfish-virtual-media', 'ipxe', 'pxe'], enabled_management_interfaces=[ 'idrac', 'idrac-redfish', 'idrac-wsman'], enabled_power_interfaces=[ @@ -46,7 +49,7 @@ class IDRACHardwareTestCase(db_base.DbTestCase): def _validate_interfaces(self, driver, **kwargs): self.assertIsInstance( driver.boot, - kwargs.get('boot', pxe.PXEBoot)) + kwargs.get('boot', ipxe.iPXEBoot)) self.assertIsInstance( driver.deploy, kwargs.get('deploy', iscsi_deploy.ISCSIDeploy)) @@ -149,3 +152,12 @@ class IDRACHardwareTestCase(db_base.DbTestCase): self._validate_interfaces( task.driver, inspect=drac.inspect.DracRedfishInspect) + + def test_override_with_redfish_virtual_media_boot(self): + node = obj_utils.create_test_node( + self.context, driver='idrac', + boot_interface='idrac-redfish-virtual-media') + with task_manager.acquire(self.context, node.id) as task: + self._validate_interfaces( + task.driver, + boot=drac.boot.DracRedfishVirtualMediaBoot) diff --git a/ironic/tests/unit/drivers/third_party_driver_mocks.py b/ironic/tests/unit/drivers/third_party_driver_mocks.py index 53a2b17ae4..ef13238504 100644 --- a/ironic/tests/unit/drivers/third_party_driver_mocks.py +++ b/ironic/tests/unit/drivers/third_party_driver_mocks.py @@ -213,6 +213,8 @@ if not sushy: type('ResourceNotFoundError', (sushy.exceptions.SushyError,), {})) sushy.exceptions.MissingAttributeError = ( type('MissingAttributeError', (sushy.exceptions.SushyError,), {})) + sushy.exceptions.OEMExtensionNotFoundError = ( + type('OEMExtensionNotFoundError', (sushy.exceptions.SushyError,), {})) sushy.auth = mock.MagicMock(spec_set=mock_specs.SUSHY_AUTH_SPEC) sys.modules['sushy.auth'] = sushy.auth diff --git a/releasenotes/notes/idrac-add-redfish-boot-support-036396b48d3f71f4.yaml b/releasenotes/notes/idrac-add-redfish-boot-support-036396b48d3f71f4.yaml new file mode 100644 index 0000000000..00c581daff --- /dev/null +++ b/releasenotes/notes/idrac-add-redfish-boot-support-036396b48d3f71f4.yaml @@ -0,0 +1,21 @@ +--- +features: + - | + Adds ``idrac`` hardware type support of a virtual media boot + interface implementation that utilizes the Redfish out-of-band (OOB) + management protocol and is compatible with the integrated Dell + Remote Access Controller (iDRAC) baseboard management controller + (BMC). It is named ``idrac-redfish-virtual-media``. + + The ``idrac`` hardware type declares support for that new interface + implementation, in addition to all boot interface implementations it + has been supporting. The highest priority boot interfaces remain the + same. It now supports the following boot interface implementations, + listed in priority order from highest to lowest: ``ipxe``, ``pxe``, + and ``idrac-redfish-virtual-media``. + + To use the new boot interface, install the ``sushy-oem-idrac`` + Python package. + + For more information, see `story 2006570 + `_. diff --git a/setup.cfg b/setup.cfg index 4a4c9c84ce..c993ab8faf 100644 --- a/setup.cfg +++ b/setup.cfg @@ -63,6 +63,7 @@ ironic.hardware.interfaces.bios = ironic.hardware.interfaces.boot = fake = ironic.drivers.modules.fake:FakeBoot + idrac-redfish-virtual-media = ironic.drivers.modules.drac.boot:DracRedfishVirtualMediaBoot ilo-pxe = ironic.drivers.modules.ilo.boot:IloPXEBoot ilo-ipxe = ironic.drivers.modules.ilo.boot:IloiPXEBoot ilo-virtual-media = ironic.drivers.modules.ilo.boot:IloVirtualMediaBoot