Merge "Add Redfish vmedia boot interface to idrac HW type"
This commit is contained in:
commit
4895f36ffb
@ -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
|
||||
|
@ -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."""
|
||||
|
161
ironic/drivers/modules/drac/boot.py
Normal file
161
ironic/drivers/modules/drac/boot.py
Normal 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)
|
167
ironic/tests/unit/drivers/modules/drac/test_boot.py
Normal file
167
ironic/tests/unit/drivers/modules/drac/test_boot.py
Normal 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)
|
@ -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=[
|
||||
|
@ -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)
|
||||
|
@ -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
|
||||
|
||||
|
@ -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>`_.
|
@ -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
|
||||
|
Loading…
Reference in New Issue
Block a user