Firmware Interface

FirmwareInterface base
New Config options [default]
- enabled_firmware_interfaces
- default_firmware_interface

New FirmwareInterface base with update method
Implementations of FirmwareInterface
- FakeFirmware (fake)
- NoFirmware (no-firmware)

New entrypoint ironic.hardware.interfaces.firmware
* fake and no-firmware

Api Controllers
- Updated: driver/node/utils/versions
- Created: firmware

Unit tests
api-ref for Node Firmware

Fake and Noop implementation for FirmwareInterface

Change-Id: Ib3b9cb22099819f97d5eab1e3f1b670cb91cbb25
This commit is contained in:
Iury Gregory Melo Ferreira 2023-06-01 23:40:46 -03:00
parent f5e33812bd
commit aecb581082
30 changed files with 503 additions and 14 deletions

View File

@ -0,0 +1,48 @@
.. -*- rst -*-
=====================
Node Firmware (nodes)
=====================
.. versionadded:: 1.84
Given a Node identifier (``uuid`` or ``name``), the API exposes the list of
all Firmware Components associated with that Node.
These endpoints do not allow modification of the Firmware Components; that
should be done by using ``clean steps``.
List all Firmware Components by Node
====================================
.. rest_method:: GET /v1/nodes/{node_ident}/firmware
Return a list of Firmware Components associated with ``node_ident``.
Normal response code: 200
Error codes: 404
Request
-------
.. rest_parameters:: parameters.yaml
- node_ident: node_ident
Response
--------
.. rest_parameters:: parameters.yaml
- firmware: firmware_components
- created_at: created_at
- updated_at: updated_at
- component: firmware_component
- initial_version: firmware_component_initial_version
- current_version: firmware_component_current_version
- last_version_flashed: firmware_component_last_version_flashed
**Example list of a Node's Firmware Components:**
.. literalinclude:: samples/node-firmware-components-list-response.json

View File

@ -0,0 +1,20 @@
{
"firmware": [
{
"created_at": "2016-08-18T22:28:49.653974+00:00",
"updated_at": "2016-08-18T22:28:49.653974+00:00",
"component": "BMC",
"initial_version": "v1.0.0",
"current_version": "v1.2.0",
"last_version_flashed": "v1.2.0"
},
{
"created_at": "2016-08-18T22:28:49.653974+00:00",
"updated_at": "2016-08-18T22:28:49.653974+00:00",
"component": "BIOS",
"initial_version": "v1.0.0",
"current_version": "v1.1.5",
"last_version_flashed": "v1.1.5"
}
]
}

View File

@ -80,6 +80,10 @@ def hide_fields_in_newer_versions(driver):
driver.pop('default_bios_interface', None)
driver.pop('enabled_bios_interfaces', None)
if not api_utils.allow_firmware_interface():
driver.pop('default_firmware_interface', None)
driver.pop('enabled_firmware_interfaces', None)
def convert_with_links(name, hosts, detail=False, interface_info=None,
fields=None, sanitize=True):

View File

@ -0,0 +1,75 @@
# Copyright 2023 Red Hat Inc.
# All Rights Reserved.
#
# 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 ironic_lib import metrics_utils
from pecan import rest
from ironic import api
from ironic.api.controllers.v1 import utils as api_utils
from ironic.api import method
from ironic.common import args
from ironic import objects
METRICS = metrics_utils.get_metrics_logger(__name__)
_DEFAULT_RETURN_FIELDS = ('component', 'initial_version', 'current_version',
'last_version_flashed')
# NOTE(iurygregory): Keeping same parameters just in case we decide
# to support /v1/nodes/<node_uuid>/firmware/<component>
def convert_with_links(rpc_firmware, node_uuid, detail=None, fields=None):
"""Build a dict containing a firmware component."""
fw_component = api_utils.object_to_dict(
rpc_firmware,
include_uuid=False,
fields=fields,
)
return fw_component
def collection_from_list(node_ident, firmware_components, detail=None,
fields=None):
firmware_list = []
for fw_cmp in firmware_components:
firmware_list.append(convert_with_links(fw_cmp, node_ident,
detail, fields))
return {'firmware': firmware_list}
class NodeFirmwareController(rest.RestController):
"""REST controller for Firmware."""
def __init__(self, node_ident=None):
super(NodeFirmwareController, self).__init__()
self.node_ident = node_ident
@METRICS.timer('NodeFirmwareController.get_all')
@method.expose()
@args.validate(fields=args.string_list, detail=args.boolean)
def get_all(self, detail=None, fields=None):
"""List node firmware components."""
node = api_utils.check_node_policy_and_retrieve(
'baremetal:node:firmware:get', self.node_ident)
allow_query = api_utils.allow_firmware_interface
fields = api_utils.get_request_return_fields(fields, detail,
_DEFAULT_RETURN_FIELDS,
allow_query, allow_query)
components = objects.FirmwareComponentList.get_by_node_id(
api.request.context, node.id)
return collection_from_list(self.node_ident, components,
detail, fields)

View File

@ -32,6 +32,7 @@ from ironic.api.controllers import link
from ironic.api.controllers.v1 import allocation
from ironic.api.controllers.v1 import bios
from ironic.api.controllers.v1 import collection
from ironic.api.controllers.v1 import firmware
from ironic.api.controllers.v1 import notification_utils as notify
from ironic.api.controllers.v1 import port
from ironic.api.controllers.v1 import portgroup
@ -169,6 +170,7 @@ def node_schema():
'driver': {'type': 'string'},
'driver_info': {'type': ['object', 'null']},
'extra': {'type': ['object', 'null']},
'firmware_interface': {'type': ['string', 'null']},
'inspect_interface': {'type': ['string', 'null']},
'instance_info': {'type': ['object', 'null']},
'instance_uuid': {'type': ['string', 'null']},
@ -283,7 +285,8 @@ PATCH_ALLOWED_FIELDS = [
'shard',
'storage_interface',
'vendor_interface',
'parent_node'
'parent_node',
'firmware_interface'
]
TRAITS_SCHEMA = {
@ -1395,6 +1398,7 @@ def _get_fields_for_node_query(fields=None):
'driver_internal_info',
'extra',
'fault',
'firmware_interface',
'inspection_finished_at',
'inspection_started_at',
'inspect_interface',
@ -2114,6 +2118,7 @@ class NodesController(rest.RestController):
'history': NodeHistoryController,
'inventory': NodeInventoryController,
'children': NodeChildrenController,
'firmware': firmware.NodeFirmwareController,
}
@pecan.expose()
@ -2139,7 +2144,9 @@ class NodesController(rest.RestController):
or (remainder[0] == 'history'
and not api_utils.allow_node_history())
or (remainder[0] == 'inventory'
and not api_utils.allow_node_inventory())):
and not api_utils.allow_node_inventory())
or (remainder[0] == 'firmware'
and not api_utils.allow_firmware_interface())):
pecan.abort(http_client.NOT_FOUND)
if remainder[0] == 'traits' and not api_utils.allow_traits():
# NOTE(mgoddard): Returning here will ensure we exhibit the

View File

@ -807,7 +807,8 @@ VERSIONED_FIELDS = {
'boot_mode': versions.MINOR_75_NODE_BOOT_MODE,
'secure_boot': versions.MINOR_75_NODE_BOOT_MODE,
'shard': versions.MINOR_82_NODE_SHARD,
'parent_node': versions.MINOR_83_PARENT_CHILD_NODES
'parent_node': versions.MINOR_83_PARENT_CHILD_NODES,
'firmware_interface': versions.MINOR_86_FIRMWARE_INTERFACE
}
for field in V31_FIELDS:
@ -2006,3 +2007,11 @@ def allow_continue_inspection_endpoint():
"""
return (new_continue_inspection_endpoint()
or api.request.version.minor == versions.MINOR_1_INITIAL_VERSION)
def allow_firmware_interface():
"""Check if we should support firmware interface and endpoints.
Version 1.84 of the API added support for firmware interface.
"""
return api.request.version.minor >= versions.MINOR_86_FIRMWARE_INTERFACE

View File

@ -209,6 +209,7 @@ MINOR_82_NODE_SHARD = 82
MINOR_83_PARENT_CHILD_NODES = 83
MINOR_84_CONTINUE_INSPECTION = 84
MINOR_85_UNHOLD_VERB = 85
MINOR_86_FIRMWARE_INTERFACE = 86
# When adding another version, update:
# - MINOR_MAX_VERSION
@ -216,7 +217,7 @@ MINOR_85_UNHOLD_VERB = 85
# explanation of what changed in the new version
# - common/release_mappings.py, RELEASE_MAPPING['master']['api']
MINOR_MAX_VERSION = MINOR_85_UNHOLD_VERB
MINOR_MAX_VERSION = MINOR_86_FIRMWARE_INTERFACE
# String representations of the minor and maximum versions
_MIN_VERSION_STRING = '{}.{}'.format(BASE_VERSION, MINOR_1_INITIAL_VERSION)

View File

@ -1009,6 +1009,15 @@ node_policies = [
'the API clients.',
operations=[{'path': '/nodes/{node_ident}', 'method': 'PATCH'}],
),
policy.DocumentedRuleDefault(
name='baremetal:node:firmware:get',
check_str=SYSTEM_OR_PROJECT_READER,
scope_types=['system', 'project'],
description='Retrieve Node Firmware components information',
operations=[
{'path': '/nodes/{node_ident}/firmware', 'method': 'GET'}
],
),
]
deprecated_port_reason = """

View File

@ -574,12 +574,12 @@ RELEASE_MAPPING = {
}
},
'master': {
'api': '1.85',
'api': '1.86',
'rpc': '1.56',
'objects': {
'Allocation': ['1.1'],
'BIOSSetting': ['1.1'],
'Node': ['1.38', '1.37'],
'Node': ['1.39', '1.38', '1.37'],
'NodeHistory': ['1.0'],
'NodeInventory': ['1.0'],
'Conductor': ['1.3'],

View File

@ -115,6 +115,11 @@ driver_opts = [
help=_ENABLED_IFACE_HELP.format('deploy')),
cfg.StrOpt('default_deploy_interface',
help=_DEFAULT_IFACE_HELP.format('deploy')),
cfg.ListOpt('enabled_firmware_interfaces',
default=['no-firmware'],
help=_ENABLED_IFACE_HELP.format('firmware')),
cfg.StrOpt('default_firmware_interface',
help=_DEFAULT_IFACE_HELP.format('firmware')),
cfg.ListOpt('enabled_inspect_interfaces',
default=['no-inspect', 'redfish'],
help=_ENABLED_IFACE_HELP.format('inspect')),

View File

@ -78,6 +78,12 @@ opts = [
'rescue driver. Two comma-delimited values will '
'result in a delay with a triangular random '
'distribution, weighted on the first value.')),
cfg.StrOpt('firmware_delay',
default='0',
help=_('Delay in seconds for operations with the fake '
'firmware driver. Two comma-delimited values will '
'result in a delay with a triangular random '
'distribution, weighted on the first value.')),
]

View File

@ -105,6 +105,12 @@ class BareDriver(object):
A reference to an instance of :class:DeployInterface.
"""
firmware = None
"""`Standard` attribute for inspection related features.
A reference to an instance of :class:FirmwareInterface.
"""
inspect = None
"""`Standard` attribute for inspection related features.
@ -161,7 +167,8 @@ class BareDriver(object):
@property
def optional_interfaces(self):
"""Interfaces that can be no-op."""
return ['bios', 'console', 'inspect', 'raid', 'rescue', 'storage']
return ['bios', 'console', 'firmware', 'inspect', 'raid', 'rescue',
'storage']
@property
def all_interfaces(self):
@ -1736,6 +1743,55 @@ class StorageInterface(BaseInterface, metaclass=abc.ABCMeta):
"""
def cache_firmware_components(func):
"""A decorator to cache firmware components after running the function.
:param func: Function or method to wrap.
"""
@functools.wraps(func)
def wrapped(self, task, *args, **kwargs):
result = func(self, task, *args, **kwargs)
self.cache_firmware_components(task)
return result
return wrapped
class FirmwareInterface(BaseInterface):
"""Base class for firmware interface"""
interface_type = 'firmware'
@abc.abstractmethod
def update(self, task, settings):
"""Update the Firmware on the given using the settings for components.
:param task: a TaskManager instance.
:param settings: a list of dictionaries, each dictionary contains the
component name and the url that will be used to update the
firmware.
:raises: UnsupportedDriverExtension, if the node's driver doesn't
support update via the interface.
:raises: InvalidParameterValue, if validation of the settings fails.
:raises: MissingParamterValue, if some required parameters are
missing.
:returns: states.CLEANWAIT if Firmware update with the settings is in
progress asynchronously of None if it is complete.
"""
@abc.abstractmethod
def cache_firmware_components(self, task):
"""Store or update Firmware Components on the given node.
This method stores Firmware Components to the firmware_information
table during 'cleaning' operation. It will also update the timestamp
of each Firmware Component.
:param task: a TaskManager instance.
:raises: UnsupportedDriverExtension, if the node's driver doesn't
support getting Firmware Components from bare metal.
"""
def _validate_argsinfo(argsinfo):
"""Validate args info.

View File

@ -86,3 +86,8 @@ class FakeHardware(generic.GenericHardware):
return [
fake.FakeVendorB, fake.FakeVendorA
] + super().supported_vendor_interfaces
@property
def supported_firmware_interfaces(self):
"""List of classes of supported bios interfaces."""
return [fake.FakeFirmware] + super().supported_firmware_interfaces

View File

@ -86,6 +86,11 @@ class GenericHardware(hardware_type.AbstractHardwareType):
return [noop_storage.NoopStorage, cinder.CinderStorage,
external_storage.ExternalStorage]
@property
def supported_firmware_interfaces(self):
"""List of supported firmware interfaces."""
return [noop.NoFirmware]
class ManualManagementHardware(GenericHardware):
"""Hardware type that uses manual power and boot management.

View File

@ -103,6 +103,11 @@ class AbstractHardwareType(object, metaclass=abc.ABCMeta):
"""List of supported vendor interfaces."""
return [noop.NoVendor]
@property
def supported_firmware_interfaces(self):
"""List of supported firmware interfaces."""
return [noop.NoFirmware]
def get_properties(self):
"""Get the properties of the hardware type.

View File

@ -443,3 +443,24 @@ class FakeRescue(base.RescueInterface):
def unrescue(self, task):
sleep(CONF.fake.rescue_delay)
return states.ACTIVE
class FakeFirmware(base.FirmwareInterface):
"""Example implementation of a simple firmware interface."""
def get_properties(self):
return {}
def validate(self, task):
pass
@base.clean_step(priority=0, argsinfo={
'settings': {'description': ('List of Firmware components, each item '
'needs to contain a dictionary with name/value pairs'),
'required': True}})
def update(self, task, settings):
sleep(CONF.fake.firmware_delay)
def cache_firmware_components(self, task):
sleep(CONF.fake.firmware_delay)
pass

View File

@ -81,3 +81,13 @@ class NoBIOS(FailMixin, base.BIOSInterface):
def cache_bios_settings(self, task):
pass
class NoFirmware(FailMixin, base.FirmwareInterface):
"""Firmware interface implementation that raises errors on all requests"""
def update(self, task, settings):
_fail(self, task, settings)
def cache_firmware_components(self, task):
pass

View File

@ -80,7 +80,8 @@ class Node(base.IronicObject, object_base.VersionedObjectDictCompat):
# Version 1.36: Add boot_mode and secure_boot fields
# Version 1.37: Add shard field
# Version 1.38: Add parent_node field
VERSION = '1.38'
# Version 1.39: Add firmware_interface field
VERSION = '1.39'
dbapi = db_api.get_instance()
@ -155,6 +156,7 @@ class Node(base.IronicObject, object_base.VersionedObjectDictCompat):
'boot_interface': object_fields.StringField(nullable=True),
'console_interface': object_fields.StringField(nullable=True),
'deploy_interface': object_fields.StringField(nullable=True),
'firmware_interface': object_fields.StringField(nullable=True),
'inspect_interface': object_fields.StringField(nullable=True),
'management_interface': object_fields.StringField(nullable=True),
'network_interface': object_fields.StringField(nullable=True),
@ -662,6 +664,9 @@ class Node(base.IronicObject, object_base.VersionedObjectDictCompat):
For versions prior to this, it should be set to None or removed.
Version 1.37: shard was added. Default is None. For versions prior to
this, it should be set to None or removed.
Version 1.39: firmware_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
@ -677,7 +682,8 @@ class Node(base.IronicObject, object_base.VersionedObjectDictCompat):
('automated_clean', 28), ('protected_reason', 29),
('owner', 30), ('allocation_id', 31), ('description', 32),
('retired_reason', 33), ('lessee', 34), ('boot_mode', 36),
('secure_boot', 36), ('shard', 37)]
('secure_boot', 36), ('shard', 37),
('firmware_interface', 39)]
for name, minor in fields:
self._adjust_field_to_version(name, None, target_version,

View File

@ -72,8 +72,8 @@ class BaseApiTest(db_base.DbTestCase):
def _make_app(self):
# Determine where we are so we can set up paths in the config
root_dir = self.path_get()
root_dir = self.path_get()
self.app_config = {
'app': {
'root': self.root_controller,

View File

@ -219,7 +219,7 @@ class TestListDrivers(base.BaseApiTest):
for iface in driver_base.ALL_INTERFACES:
if iface != 'bios':
if latest_if or iface not in ['rescue', 'storage']:
if latest_if or iface not in ['rescue', 'storage', 'firmware']:
self.assertIn('default_%s_interface' % iface, data)
self.assertIn('enabled_%s_interfaces' % iface, data)

View File

@ -8478,3 +8478,40 @@ class TestNodeParentNodePatch(test_api_base.BaseApiTest):
'/nodes/%s' % self.child_node.uuid, body, headers=headers)
self.assertEqual(http_client.OK, response.status_code)
self.mock_update_node.assert_called_once()
class TestNodeFirmwareComponent(test_api_base.BaseApiTest):
def setUp(self):
super(TestNodeFirmwareComponent, self).setUp()
self.version = "1.86"
self.node = obj_utils.create_test_node(
self.context, id=1)
self.fw_cmp = obj_utils.create_test_firmware_component(
self.context, node_id=self.node.id)
self.fw_cmp2 = obj_utils.create_test_firmware_component(
self.context, node_id=self.node.id, component='BIOS')
def test_get_all_firmware_components(self):
ret = self.get_json('/nodes/%s/firmware' % self.node.uuid,
headers={api_base.Version.string: self.version})
expected_components = [
{'created_at': ret['firmware'][0]['created_at'],
'updated_at': ret['firmware'][0]['updated_at'],
'component': 'BIOS',
'initial_version': 'v1.0.0', 'current_version': 'v1.0.0',
'last_version_flashed': None},
{'created_at': ret['firmware'][1]['created_at'],
'updated_at': ret['firmware'][1]['updated_at'],
'component': 'bmc',
'initial_version': 'v1.0.0', 'current_version': 'v1.0.0',
'last_version_flashed': None}]
self.assertEqual({'firmware': expected_components}, ret)
def test_wrong_version_get_all_firmware_components_old_version(self):
ret = self.get_json('/nodes/%s/firmware' % self.node.uuid,
headers={api_base.Version.string: "1.81"},
expect_errors=True)
self.assertEqual(http_client.NOT_FOUND, ret.status_int)

View File

@ -285,6 +285,10 @@ class TestRBACModelBeforeScopesBase(TestACLBase):
value=fake_setting)
db_utils.create_test_node_trait(
node_id=fake_db_node['id'])
# Create a Fake Firmware Component BMC
db_utils.create_test_firmware_component(
node_id=fake_db_node['id'],
)
fake_history = db_utils.create_test_history(node_id=fake_db_node.id)
fake_inventory = db_utils.create_test_inventory(
node_id=fake_db_node.id)

View File

@ -3946,3 +3946,35 @@ lessee_cannot_get_a_nodes_children:
method: get
headers: *lessee_reader_headers
assert_status: 404
# Node Firmware
owner_reader_can_get_firmware_components:
path: '/v1/nodes/{owner_node_ident}/firmware'
method: get
headers: *owner_reader_headers
assert_status: 200
lessee_reader_can_get_firmware_components:
path: '/v1/nodes/{lessee_node_ident}/firmware'
method: get
headers: *lessee_reader_headers
assert_status: 200
third_party_admin_cannot_get_firmware_components:
path: '/v1/nodes/{owner_node_ident}/firmware'
method: get
headers: *third_party_admin_headers
assert_status: 404
service_can_get_firmware_components_owner_project:
path: '/v1/nodes/{owner_node_ident}/firmware'
method: get
headers: *service_headers_owner_project
assert_status: 200
service_cannot_get_firmware_components:
path: '/v1/nodes/{owner_node_ident}/firmware'
method: get
headers: *service_headers
assert_status: 404

View File

@ -2340,3 +2340,23 @@ parent_node_patch_by_reader:
headers: *reader_headers
body: *patch_parent_node
assert_status: 403
# Node Firmware - baremetal:node:firmware:get
nodes_firmware_component_get_admin:
path: '/v1/nodes/{node_ident}/firmware'
method: get
headers: *admin_headers
assert_status: 200
nodes_firmware_component_get_member:
path: '/v1/nodes/{node_ident}/firmware'
method: get
headers: *scoped_member_headers
assert_status: 200
nodes_firmware_component_get_reader:
path: '/v1/nodes/{node_ident}/firmware'
method: get
headers: *reader_headers
assert_status: 200

View File

@ -378,6 +378,11 @@ class TestFakeHardware(hardware_type.AbstractHardwareType):
"""List of supported deploy interfaces."""
return [fake.FakeDeploy]
@property
def supported_firmware_interfaces(self):
"""List of supported firmware interfaces."""
return [fake.FakeFirmware]
@property
def supported_inspect_interfaces(self):
"""List of supported inspect interfaces."""
@ -586,6 +591,7 @@ class HardwareTypeLoadTestCase(db_base.DbTestCase):
'boot': set(['fake']),
'console': set(['fake', 'no-console']),
'deploy': set(['fake']),
'firmware': set(['fake', 'no-firmware']),
'inspect': set(['fake', 'no-inspect']),
'management': set(['fake']),
'network': set(['noop']),

View File

@ -3578,7 +3578,8 @@ class MiscTestCase(mgr_utils.ServiceSetUpMixin, mgr_utils.CommonMixIn,
'network': {'result': True},
'storage': {'result': True},
'rescue': {'result': True},
'bios': {'result': True}}
'bios': {'result': True},
'firmware': {'result': True}}
self.assertEqual(expected, ret)
mock_iwdi.assert_called_once_with(self.context, expected_info)

View File

@ -839,12 +839,43 @@ class TestManagementInterface(base.TestCase):
management.get_mac_addresses, task_mock)
class MyFirmwareInterface(driver_base.FirmwareInterface):
def get_properties(self):
pass
def validate(self, task):
pass
@driver_base.cache_firmware_components
def update(self, task, settings):
return "return_update"
def cache_firmware_components(self, task):
pass
class TestFirmwareInterface(base.TestCase):
@mock.patch.object(MyFirmwareInterface, 'cache_firmware_components',
autospec=True)
def test_update_with_wrapper(self, cache_firmware_components_mock):
firmware = MyFirmwareInterface()
task_mock = mock.MagicMock()
actual = firmware.update(task_mock, "")
cache_firmware_components_mock.assert_called_once_with(
firmware, task_mock)
self.assertEqual(actual, "return_update")
class TestBareDriver(base.TestCase):
def test_class_variables(self):
self.assertEqual(['boot', 'deploy', 'management', 'network', 'power'],
driver_base.BareDriver().core_interfaces)
self.assertEqual(
['bios', 'console', 'inspect', 'raid', 'rescue', 'storage'],
['bios', 'console', 'firmware', 'inspect', 'raid',
'rescue', 'storage'],
driver_base.BareDriver().optional_interfaces
)

View File

@ -1377,6 +1377,68 @@ class TestConvertToVersion(db_base.DbTestCase):
self.assertIsNone(node.secure_boot)
self.assertEqual({}, node.obj_get_changes())
def test_firmware_supported_missing(self):
# firmware_interface not set, should be set to default.
node = obj_utils.get_test_node(self.ctxt, **self.fake_node)
delattr(node, 'firmware_interface')
node.obj_reset_changes()
node._convert_to_version("1.39")
self.assertIsNone(node.firmware_interface)
self.assertEqual({'firmware_interface': None},
node.obj_get_changes())
def test_firmware_supported_set(self):
# firmware_interface set, no change required.
node = obj_utils.get_test_node(self.ctxt, **self.fake_node)
node.firmware_interface = 'fake'
node.obj_reset_changes()
node._convert_to_version("1.39")
self.assertEqual('fake', node.firmware_interface)
self.assertEqual({}, node.obj_get_changes())
def test_firmware_unsupported_missing(self):
# firmware_interface not set, no change required.
node = obj_utils.get_test_node(self.ctxt, **self.fake_node)
delattr(node, 'firmware_interface')
node.obj_reset_changes()
node._convert_to_version("1.38")
self.assertNotIn('firmware_interface', node)
self.assertEqual({}, node.obj_get_changes())
def test_firmware_unsupported_set_remove(self):
# firmware_interface set, should be removed.
node = obj_utils.get_test_node(self.ctxt, **self.fake_node)
node.firmware_interface = 'fake'
node.obj_reset_changes()
node._convert_to_version("1.38")
self.assertNotIn('firmware_interface', node)
self.assertEqual({}, node.obj_get_changes())
def test_firmware_unsupported_set_no_remove_non_default(self):
# firmware_interface set, should be set to default.
node = obj_utils.get_test_node(self.ctxt, **self.fake_node)
node.firmware_interface = 'fake'
node.obj_reset_changes()
node._convert_to_version("1.38", False)
self.assertIsNone(node.firmware_interface)
self.assertEqual({'firmware_interface': None}, node.obj_get_changes())
def test_firmware_unsupported_set_no_remove_default(self):
# firmware_interface set, no change required.
node = obj_utils.get_test_node(self.ctxt, **self.fake_node)
node.firmware_interface = None
node.obj_reset_changes()
node._convert_to_version("1.38", False)
self.assertIsNone(node.firmware_interface)
self.assertEqual({}, node.obj_get_changes())
class TestNodePayloads(db_base.DbTestCase):

View File

@ -676,7 +676,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.38-7e7fdaa2c2bb01153ad567c9f1081cb7',
'Node': '1.39-ee3f5ff28b79f9fabf84a50e34a71684',
'MyObj': '1.5-9459d30d6954bffc7a9afd347a807ca6',
'Chassis': '1.3-d656e039fd8ae9f34efc232ab3980905',
'Port': '1.11-97bf15b61224f26c65e90f007d78bfd2',

View File

@ -93,6 +93,10 @@ ironic.hardware.interfaces.deploy =
fake = ironic.drivers.modules.fake:FakeDeploy
ramdisk = ironic.drivers.modules.ramdisk:RamdiskDeploy
ironic.hardware.interfaces.firmware =
fake = ironic.drivers.modules.fake:FakeFirmware
no-firmware = ironic.drivers.modules.noop:NoFirmware
ironic.hardware.interfaces.inspect =
fake = ironic.drivers.modules.fake:FakeInspect
idrac = ironic.drivers.modules.drac.inspect:DracInspect