BIOS Settings: Add BIOSInterface

* Adds 'bios' interface to 'BaseDriver'

* Adds BIOSInterface driver class

* Adds fake & no-bios drivers and entries

* Implements it for 'fake-hardare' hardware type

* Adds configuration parameters:
  + [DEFAULT]/enabled_bios_interfaces
  + [DEFAULT]/default_bios_interface

* Adds 'bios_interface' field to Node object

* Handle 'bios_interface' field in _convert_to_version

* Adds bios in CLEANING_INTERFACE_PRIORITY

Drivers can implement this interface to do BIOS
configuration.

Co-Authored-By: Yolanda Robla Mota <yroblamo@redhat.com>
Co-Authored-By: Luong Anh Tuan <tuanla@vn.fujitsu.com>
Change-Id: I7e57130242b6cab21b54e35dc3c0b7819bdc43c0
Story: #1712032
This commit is contained in:
Zenghui Shi 2018-05-08 14:49:05 +08:00
parent 02aad838a5
commit 1e24ef9dde
22 changed files with 335 additions and 21 deletions

View File

@ -596,7 +596,8 @@ def calculate_migration_delta(driver_name, driver_class,
None if a migration is not possible.
"""
# NOTE(dtantsur): provide defaults for optional interfaces
defaults = {'console': 'no-console',
defaults = {'bios': 'no-bios',
'console': 'no-console',
'inspect': 'no-inspect',
'raid': 'no-raid',
'rescue': 'no-rescue',

View File

@ -103,7 +103,7 @@ RELEASE_MAPPING = {
'api': '1.39',
'rpc': '1.44',
'objects': {
'Node': ['1.23'],
'Node': ['1.24'],
'Conductor': ['1.2'],
'Chassis': ['1.3'],
'Port': ['1.8'],

View File

@ -1869,6 +1869,11 @@ class ConductorManager(base_manager.BaseConductorManager):
task.node.instance_info)
task.node.driver_internal_info['is_whole_disk_image'] = iwdi
for iface_name in task.driver.non_vendor_interfaces:
# TODO(zshi): Remove this check in 'bios' API patch
# Do not have to return the validation result for 'bios'
# interface.
if iface_name == 'bios':
continue
iface = getattr(task.driver, iface_name, None)
result = reason = None
if iface:

View File

@ -34,9 +34,10 @@ CLEANING_INTERFACE_PRIORITY = {
# by which interface is implementing the clean step. The clean step of the
# interface with the highest value here, will be executed first in that
# case.
'power': 4,
'management': 3,
'deploy': 2,
'power': 5,
'management': 4,
'deploy': 3,
'bios': 2,
'raid': 1,
}

View File

@ -52,6 +52,18 @@ _DEFAULT_IFACE_HELP = _('Default {0} interface to be used for nodes that '
'be found by enumerating the '
'"ironic.hardware.interfaces.{0}" entrypoint.')
# TODO(zshi) Remove this in BIOS API patch.
_ENABLED_IFACE_HELP_FOR_BIOS = (_ENABLED_IFACE_HELP +
_(' This option is part of BIOS feature '
'work, which is not currently exposed to '
'users.'))
# TODO(zshi) Remove this in BIOS API patch.
_DEFAULT_IFACE_HELP_FOR_BIOS = (_DEFAULT_IFACE_HELP +
_(' This option is part of BIOS feature '
'work, which is not currently exposed to '
'users.'))
api_opts = [
cfg.StrOpt(
'auth_strategy',
@ -103,6 +115,11 @@ driver_opts = [
'A complete list of hardware types present on your '
'system may be found by enumerating the '
'"ironic.hardware.types" entrypoint.')),
cfg.ListOpt('enabled_bios_interfaces',
default=['no-bios'],
help=_ENABLED_IFACE_HELP_FOR_BIOS.format('bios')),
cfg.StrOpt('default_bios_interface',
help=_DEFAULT_IFACE_HELP_FOR_BIOS.format('bios')),
cfg.ListOpt('enabled_boot_interfaces',
default=['pxe'],
help=_ENABLED_IFACE_HELP.format('boot')),

View File

@ -176,14 +176,21 @@ class BareDriver(BaseDriver):
"""
core_interfaces = BaseDriver.core_interfaces + ('network',)
bios = None
"""`Standard` attribute for BIOS related features.
A reference to an instance of :class:BIOSInterface.
May be None, if unsupported by a driver.
"""
storage = None
"""`Standard` attribute for (remote) storage interface.
A reference to an instance of :class:StorageInterface.
"""
standard_interfaces = (BaseDriver.standard_interfaces + ('rescue',
'storage',))
standard_interfaces = (BaseDriver.standard_interfaces + ('bios',
'rescue', 'storage',))
ALL_INTERFACES = set(BareDriver().all_interfaces)
@ -917,6 +924,87 @@ class InspectInterface(BaseInterface):
"""
class BIOSInterface(BaseInterface):
interface_type = 'bios'
def __new__(cls, *args, **kwargs):
# Wrap the apply_configuration and factory_reset into a decorator
# which call cache_bios_settings() to update the node's BIOS setting
# table after apply_configuration and factory_reset have finished.
super_new = super(BIOSInterface, cls).__new__
instance = super_new(cls, *args, **kwargs)
def wrapper(func):
@six.wraps(func)
def wrapped(self, task, *args, **kwargs):
func(task, *args, **kwargs)
instance.cache_bios_settings(task)
return wrapped
for n, method in inspect.getmembers(instance, inspect.ismethod):
if n == "apply_configuration":
instance.apply_configuration = wrapper(method)
elif n == "factory_reset":
instance.factory_reset = wrapper(method)
return instance
@abc.abstractmethod
def apply_configuration(self, task, settings):
"""Validate & apply BIOS settings on the given node.
This method takes the BIOS settings from the settings param and
applies BIOS settings on the given node. It may also validate the
given bios settings before applying any settings and manage
failures when setting an invalid BIOS config. In the case of
needing password to update the BIOS config, it will be taken from
the driver_info properties. After the BIOS configuration is done,
cache_bios_settings will be called to update the node's BIOS setting
table with the BIOS configuration applied on the node.
:param task: a TaskManager instance.
:param settings: Dictonary containing the BIOS configuration.
:raises: UnsupportedDriverExtension, if the node's driver doesn't
support BIOS configuration.
:raises: InvalidParameterValue, if validation of settings fails.
:raises: MissingParameterValue, if some required parameters are
missing.
:returns: states.CLEANWAIT if BIOS configuration is in progress
asynchronously or None if it is complete.
"""
@abc.abstractmethod
def factory_reset(self, task):
"""Reset BIOS configuration to factory default on the given node.
This method resets BIOS configuration to factory default on the
given node. After the BIOS reset action is done, cache_bios_settings
will be called to update the node's BIOS settings table with default
bios settings.
:param task: a TaskManager instance.
:raises: UnsupportedDriverExtension, if the node's driver doesn't
support BIOS reset.
:returns: states.CLEANWAIT if BIOS configuration is in progress
asynchronously or None if it is complete.
"""
@abc.abstractmethod
def cache_bios_settings(self, task):
"""Store or update BIOS properties on the given node.
This method stores BIOS properties to the bios_settings table during
'cleaning' operation and updates bios_settings table when
apply_configuration() and factory_reset() are called to set new BIOS
configurations. It will also update the timestamp of each bios setting.
:param task: a TaskManager instance.
:raises: UnsupportedDriverExtension, if the node's driver doesn't
support getting BIOS properties from bare metal.
:returns: None.
"""
class RAIDInterface(BaseInterface):
interface_type = 'raid'

View File

@ -73,7 +73,7 @@ class FakeDriver(base.BaseDriver):
def to_hardware_type(cls):
return 'fake-hardware', {
iface: 'fake'
for iface in ['boot', 'console', 'deploy', 'inspect',
for iface in ['bios', 'boot', 'console', 'deploy', 'inspect',
'management', 'power', 'raid', 'rescue', 'vendor']
}

View File

@ -32,6 +32,10 @@ class FakeHardware(hardware_type.AbstractHardwareType):
All fake implementations are still expected to be enabled in the
configuration.
"""
@property
def supported_bios_interfaces(self):
"""List of classes of supported bios interfaces."""
return [fake.FakeBIOS, noop.NoBIOS]
@property
def supported_boot_interfaces(self):

View File

@ -62,6 +62,10 @@ class AbstractHardwareType(object):
"""List of supported power interfaces."""
# Optional hardware interfaces
@property
def supported_bios_interfaces(self):
"""List of supported bios interfaces."""
return [noop.NoBIOS]
@property
def supported_console_interfaces(self):

View File

@ -29,6 +29,7 @@ from ironic.common import exception
from ironic.common.i18n import _
from ironic.common import states
from ironic.drivers import base
from ironic import objects
class FakePower(base.PowerInterface):
@ -236,6 +237,33 @@ class FakeRAID(base.RAIDInterface):
pass
class FakeBIOS(base.BIOSInterface):
"""Example implementation of simple BIOSInterface."""
def get_properties(self):
return {}
def validate(self, task):
pass
def apply_configuration(self, task, settings):
node_id = task.node.id
try:
objects.BIOSSettingList.create(task.context, node_id, settings)
except exception.BIOSSettingAlreadyExists:
objects.BIOSSettingList.save(task.context, node_id, settings)
def factory_reset(self, task):
node_id = task.node.id
setting_objs = objects.BIOSSettingList.get_by_node_id(
task.context, node_id)
for setting in setting_objs:
objects.BIOSSetting.delete(task.context, node_id, setting.name)
def cache_bios_settings(self, task):
pass
class FakeStorage(base.StorageInterface):
"""Example implementation of simple storage Interface."""

View File

@ -68,3 +68,16 @@ class NoRAID(FailMixin, base.RAIDInterface):
def validate_raid_config(self, task, raid_config):
_fail(self, task)
class NoBIOS(FailMixin, base.BIOSInterface):
"""BIOS interface implementation that raises errors on all requests."""
def apply_configuration(self, task, settings):
_fail(self, task, settings)
def factory_reset(self, task):
_fail(self, task)
def cache_bios_settings(self, task):
pass

View File

@ -59,7 +59,8 @@ class Node(base.IronicObject, object_base.VersionedObjectDictCompat):
# Version 1.21: Add storage_interface field
# Version 1.22: Add rescue_interface field
# Version 1.23: Add traits field
VERSION = '1.23'
# Version 1.24: Add bios_interface field
VERSION = '1.24'
dbapi = db_api.get_instance()
@ -119,6 +120,7 @@ class Node(base.IronicObject, object_base.VersionedObjectDictCompat):
'extra': object_fields.FlexibleDictField(nullable=True),
'bios_interface': object_fields.StringField(nullable=True),
'boot_interface': object_fields.StringField(nullable=True),
'console_interface': object_fields.StringField(nullable=True),
'deploy_interface': object_fields.StringField(nullable=True),
@ -130,7 +132,6 @@ class Node(base.IronicObject, object_base.VersionedObjectDictCompat):
'rescue_interface': object_fields.StringField(nullable=True),
'storage_interface': object_fields.StringField(nullable=True),
'vendor_interface': object_fields.StringField(nullable=True),
'traits': object_fields.ObjectField('TraitList', nullable=True),
}
@ -476,6 +477,9 @@ class Node(base.IronicObject, object_base.VersionedObjectDictCompat):
Version 1.23: traits field was added. Its default value is
None. For versions prior to this, it should be set to None (or
removed).
Version 1.24: bios_interface field was added. Its default value is
None. For versions prior to this, it should be set to None (or
removed).
:param target_version: the desired version of the object
:param remove_unavailable_fields: True to remove fields that are
@ -511,6 +515,21 @@ class Node(base.IronicObject, object_base.VersionedObjectDictCompat):
elif self.traits is not None:
self.traits = None
bios_iface_is_set = self.obj_attr_is_set('bios_interface')
if target_version >= (1, 24):
# Target version supports bios_interface.
if not bios_iface_is_set:
# Set it to its default value if it is not set.
self.bios_interface = None
elif bios_iface_is_set:
# Target version does not support bios_interface, and it is set.
if remove_unavailable_fields:
# (De)serialising: remove unavailable fields.
delattr(self, 'bios_interface')
elif self.bios_interface is not None:
# DB: set unavailable field to the default of None.
self.bios_interface = None
@base.IronicObjectRegistry.register
class NodePayload(notification.NotificationPayloadBase):
@ -558,6 +577,11 @@ class NodePayload(notification.NotificationPayloadBase):
'uuid': ('node', 'uuid')
}
# TODO(zshi): At a later point in time, once bios_interface is able
# to be leveraged, we need to add the bios_interface field to payload
# and increment the object versions for all objects that inherit the
# NodePayload object.
# Version 1.0: Initial version, based off of Node version 1.18.
# Version 1.1: Type of network_interface changed to just nullable string
# similar to version 1.20 of Node.

View File

@ -215,9 +215,11 @@ class TestListDrivers(base.BaseApiTest):
if use_dynamic:
for iface in driver_base.ALL_INTERFACES:
if latest_if or iface not in ['rescue', 'storage']:
self.assertIn('default_%s_interface' % iface, data)
self.assertIn('enabled_%s_interfaces' % iface, data)
if iface != 'bios':
if latest_if or iface not in ['rescue', 'storage']:
self.assertIn('default_%s_interface' % iface, data)
self.assertIn('enabled_%s_interfaces' % iface, data)
self.assertIsNotNone(data['default_deploy_interface'])
self.assertIsNotNone(data['enabled_deploy_interfaces'])
else:

View File

@ -108,7 +108,7 @@ class DriverLoadTestCase(db_base.DbTestCase):
with task_manager.acquire(self.context, node.id) as task:
for iface in drivers_base.ALL_INTERFACES:
impl = getattr(task.driver, iface)
if iface == 'rescue':
if iface in ['bios', 'rescue']:
self.assertIsNone(impl)
else:
self.assertIsNotNone(impl)
@ -572,6 +572,11 @@ class DefaultInterfaceTestCase(db_base.DbTestCase):
class TestFakeHardware(hardware_type.AbstractHardwareType):
@property
def supported_bios_interfaces(self):
"""List of supported bios interfaces."""
return [fake.FakeBIOS]
@property
def supported_boot_interfaces(self):
"""List of supported boot interfaces."""
@ -796,6 +801,7 @@ class HardwareTypeLoadTestCase(db_base.DbTestCase):
def _test_enabled_supported_interfaces(self, enable_storage):
ht = fake_hardware.FakeHardware()
expected = {
'bios': set(['fake', 'no-bios']),
'boot': set(['fake']),
'console': set(['fake', 'no-console']),
'deploy': set(['fake']),
@ -850,6 +856,7 @@ class ClassicDriverMigrationTestCase(base.TestCase):
delta = driver_factory.calculate_migration_delta(
'drv', self.driver_cls, False)
self.assertEqual({'driver': 'hw-type',
'bios_interface': 'no-bios',
'console_interface': 'new-console',
'inspect_interface': 'new-inspect',
'raid_interface': 'no-raid',
@ -881,6 +888,7 @@ class ClassicDriverMigrationTestCase(base.TestCase):
delta = driver_factory.calculate_migration_delta(
'drv', self.driver_cls, True)
self.assertEqual({'driver': 'hw-type',
'bios_interface': 'no-bios',
'console_interface': 'new-console',
'inspect_interface': 'no-inspect',
'raid_interface': 'no-raid',

View File

@ -177,6 +177,7 @@ class ServiceSetUpMixin(object):
self.config(enabled_raid_interfaces=['fake', 'no-raid'])
self.config(enabled_rescue_interfaces=['fake', 'no-rescue'])
self.config(enabled_vendor_interfaces=['fake', 'no-vendor'])
self.config(enabled_bios_interfaces=['fake', 'no-bios'])
self.service = manager.ConductorManager(self.hostname, 'test-topic')
mock_the_extension_manager()

View File

@ -3405,6 +3405,7 @@ class MiscTestCase(mgr_utils.ServiceSetUpMixin, mgr_utils.CommonMixIn,
'network': {'result': True},
'storage': {'result': True},
'rescue': {'reason': reason, 'result': None}}
self.assertEqual(expected, ret)
mock_iwdi.assert_called_once_with(self.context, node.instance_info)

View File

@ -730,6 +730,7 @@ class NodePowerActionTestCase(db_base.DbTestCase):
raid_interface='no-raid',
rescue_interface='no-rescue',
vendor_interface='no-vendor',
bios_interface='no-bios',
power_state=states.POWER_ON)
self.config(enabled_boot_interfaces=['fake'])
self.config(enabled_deploy_interfaces=['fake'])

View File

@ -29,10 +29,18 @@ def hardware_interface_extension_manager(interface):
class NoInterfacesTestCase(base.TestCase):
iface_types = ['console', 'inspect', 'raid', 'rescue', 'vendor']
iface_types = ['bios', 'console', 'inspect', 'raid', 'rescue', 'vendor']
task = mock.Mock(node=mock.Mock(driver='pxe_foobar', spec=['driver']),
spec=['node'])
def test_bios(self):
self.assertRaises(exception.UnsupportedDriverExtension,
getattr(noop.NoBIOS(), 'apply_configuration'),
self, self.task, '')
self.assertRaises(exception.UnsupportedDriverExtension,
getattr(noop.NoBIOS(), 'factory_reset'),
self, self.task)
def test_console(self):
for method in ('start_console', 'stop_console', 'get_console'):
self.assertRaises(exception.UnsupportedDriverExtension,

View File

@ -422,6 +422,43 @@ class TestDeployInterface(base.TestCase):
self.assertTrue(mock_log.called)
class MyBIOSInterface(driver_base.BIOSInterface):
def get_properties(self):
pass
def validate(self, task):
pass
def apply_configuration(self, task, settings):
pass
def factory_reset(self, task):
pass
def cache_bios_settings(self, task):
pass
class TestBIOSInterface(base.TestCase):
@mock.patch.object(MyBIOSInterface, 'cache_bios_settings', autospec=True)
def test_apply_configuration_wrapper(self, cache_bios_settings_mock):
bios = MyBIOSInterface()
task_mock = mock.MagicMock()
bios.apply_configuration(bios, task_mock, "")
cache_bios_settings_mock.assert_called_once_with(bios, task_mock)
@mock.patch.object(MyBIOSInterface, 'cache_bios_settings', autospec=True)
def test_factory_reset_wrapper(self, cache_bios_settings_mock):
bios = MyBIOSInterface()
task_mock = mock.MagicMock()
bios.factory_reset(bios, task_mock)
cache_bios_settings_mock.assert_called_once_with(bios, task_mock)
class TestBootInterface(base.TestCase):
def test_validate_rescue_default_impl(self):
@ -449,16 +486,20 @@ class TestBaseDriver(base.TestCase):
# get modified by a child class
self.assertEqual(('deploy', 'power'),
driver_base.BaseDriver.core_interfaces)
self.assertEqual(('boot', 'console', 'inspect', 'management', 'raid'),
driver_base.BaseDriver.standard_interfaces)
self.assertEqual(
('boot', 'console', 'inspect', 'management', 'raid'),
driver_base.BaseDriver.standard_interfaces
)
# Ensure that instantiating an instance of a derived class does not
# change our variables.
driver_base.BareDriver()
self.assertEqual(('deploy', 'power'),
driver_base.BaseDriver.core_interfaces)
self.assertEqual(('boot', 'console', 'inspect', 'management', 'raid'),
driver_base.BaseDriver.standard_interfaces)
self.assertEqual(
('boot', 'console', 'inspect', 'management', 'raid'),
driver_base.BaseDriver.standard_interfaces
)
class TestBareDriver(base.TestCase):
@ -469,7 +510,7 @@ class TestBareDriver(base.TestCase):
self.assertEqual(('deploy', 'power', 'network'),
driver_base.BareDriver.core_interfaces)
self.assertEqual(
('boot', 'console', 'inspect', 'management', 'raid',
('boot', 'console', 'inspect', 'management', 'raid', 'bios',
'rescue', 'storage'),
driver_base.BareDriver.standard_interfaces
)

View File

@ -497,6 +497,69 @@ class TestConvertToVersion(db_base.DbTestCase):
self.assertIsNone(node.traits)
self.assertEqual({}, node.obj_get_changes())
def test_bios_supported_missing(self):
# bios_interface not set, should be set to default.
node = obj_utils.get_test_node(self.ctxt, **self.fake_node)
delattr(node, 'bios_interface')
node.obj_reset_changes()
node._convert_to_version("1.24")
self.assertIsNone(node.bios_interface)
self.assertEqual({'bios_interface': None},
node.obj_get_changes())
def test_bios_supported_set(self):
# bios_interface set, no change required.
node = obj_utils.get_test_node(self.ctxt, **self.fake_node)
node.bios_interface = 'fake'
node.obj_reset_changes()
node._convert_to_version("1.24")
self.assertEqual('fake', node.bios_interface)
self.assertEqual({}, node.obj_get_changes())
def test_bios_unsupported_missing(self):
# bios_interface not set, no change required.
node = obj_utils.get_test_node(self.ctxt, **self.fake_node)
delattr(node, 'bios_interface')
node.obj_reset_changes()
node._convert_to_version("1.23")
self.assertNotIn('bios_interface', node)
self.assertEqual({}, node.obj_get_changes())
def test_bios_unsupported_set_remove(self):
# bios_interface set, should be removed.
node = obj_utils.get_test_node(self.ctxt, **self.fake_node)
node.bios_interface = 'fake'
node.obj_reset_changes()
node._convert_to_version("1.23")
self.assertNotIn('bios_interface', node)
self.assertEqual({}, node.obj_get_changes())
def test_bios_unsupported_set_no_remove_non_default(self):
# bios_interface set, should be set to default.
node = obj_utils.get_test_node(self.ctxt, **self.fake_node)
node.bios_interface = 'fake'
node.obj_reset_changes()
node._convert_to_version("1.23", False)
self.assertIsNone(node.bios_interface)
self.assertEqual({'bios_interface': None},
node.obj_get_changes())
def test_bios_unsupported_set_no_remove_default(self):
# bios_interface set, no change required.
node = obj_utils.get_test_node(self.ctxt, **self.fake_node)
node.bios_interface = None
node.obj_reset_changes()
node._convert_to_version("1.23", False)
self.assertIsNone(node.bios_interface)
self.assertEqual({}, node.obj_get_changes())
class TestNodePayloads(db_base.DbTestCase):

View File

@ -663,7 +663,7 @@ class TestObject(_LocalTest, _TestObject):
# version bump. It is an MD5 hash of the object fields and remotable methods.
# The fingerprint values should only be changed if there is a version bump.
expected_object_fingerprints = {
'Node': '1.23-6bebf8dbcd2ce15407c946bd091f80b4',
'Node': '1.24-7d3d504e5e0d2535b2390d558b27196a',
'MyObj': '1.5-9459d30d6954bffc7a9afd347a807ca6',
'Chassis': '1.3-d656e039fd8ae9f34efc232ab3980905',
'Port': '1.8-898a47921f4a1f53fcdddd4eeb179e0b',

View File

@ -87,6 +87,10 @@ ironic.drivers =
pxe_iscsi_cimc = ironic.drivers.pxe:PXEAndCIMCDriver
pxe_agent_cimc = ironic.drivers.agent:AgentAndCIMCDriver
ironic.hardware.interfaces.bios =
fake = ironic.drivers.modules.fake:FakeBIOS
no-bios = ironic.drivers.modules.noop:NoBIOS
ironic.hardware.interfaces.boot =
fake = ironic.drivers.modules.fake:FakeBoot
ilo-pxe = ironic.drivers.modules.ilo.boot:IloPXEBoot