Merge "Add Redfish vmedia boot interface to idrac HW type"

This commit is contained in:
Zuul 2019-10-14 12:57:54 +00:00 committed by Gerrit Code Review
commit 4895f36ffb
9 changed files with 379 additions and 2 deletions

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -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
<https://storyboard.openstack.org/#!/story/2006570>`_.

View File

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