Merge "Firmware Interface"

This commit is contained in:
Zuul 2023-07-19 06:00:30 +00:00 committed by Gerrit Code Review
commit ef73871524
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('default_bios_interface', None)
driver.pop('enabled_bios_interfaces', 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, def convert_with_links(name, hosts, detail=False, interface_info=None,
fields=None, sanitize=True): 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 allocation
from ironic.api.controllers.v1 import bios from ironic.api.controllers.v1 import bios
from ironic.api.controllers.v1 import collection 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 notification_utils as notify
from ironic.api.controllers.v1 import port from ironic.api.controllers.v1 import port
from ironic.api.controllers.v1 import portgroup from ironic.api.controllers.v1 import portgroup
@ -169,6 +170,7 @@ def node_schema():
'driver': {'type': 'string'}, 'driver': {'type': 'string'},
'driver_info': {'type': ['object', 'null']}, 'driver_info': {'type': ['object', 'null']},
'extra': {'type': ['object', 'null']}, 'extra': {'type': ['object', 'null']},
'firmware_interface': {'type': ['string', 'null']},
'inspect_interface': {'type': ['string', 'null']}, 'inspect_interface': {'type': ['string', 'null']},
'instance_info': {'type': ['object', 'null']}, 'instance_info': {'type': ['object', 'null']},
'instance_uuid': {'type': ['string', 'null']}, 'instance_uuid': {'type': ['string', 'null']},
@ -283,7 +285,8 @@ PATCH_ALLOWED_FIELDS = [
'shard', 'shard',
'storage_interface', 'storage_interface',
'vendor_interface', 'vendor_interface',
'parent_node' 'parent_node',
'firmware_interface'
] ]
TRAITS_SCHEMA = { TRAITS_SCHEMA = {
@ -1395,6 +1398,7 @@ def _get_fields_for_node_query(fields=None):
'driver_internal_info', 'driver_internal_info',
'extra', 'extra',
'fault', 'fault',
'firmware_interface',
'inspection_finished_at', 'inspection_finished_at',
'inspection_started_at', 'inspection_started_at',
'inspect_interface', 'inspect_interface',
@ -2114,6 +2118,7 @@ class NodesController(rest.RestController):
'history': NodeHistoryController, 'history': NodeHistoryController,
'inventory': NodeInventoryController, 'inventory': NodeInventoryController,
'children': NodeChildrenController, 'children': NodeChildrenController,
'firmware': firmware.NodeFirmwareController,
} }
@pecan.expose() @pecan.expose()
@ -2139,7 +2144,9 @@ class NodesController(rest.RestController):
or (remainder[0] == 'history' or (remainder[0] == 'history'
and not api_utils.allow_node_history()) and not api_utils.allow_node_history())
or (remainder[0] == 'inventory' 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) pecan.abort(http_client.NOT_FOUND)
if remainder[0] == 'traits' and not api_utils.allow_traits(): if remainder[0] == 'traits' and not api_utils.allow_traits():
# NOTE(mgoddard): Returning here will ensure we exhibit the # 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, 'boot_mode': versions.MINOR_75_NODE_BOOT_MODE,
'secure_boot': versions.MINOR_75_NODE_BOOT_MODE, 'secure_boot': versions.MINOR_75_NODE_BOOT_MODE,
'shard': versions.MINOR_82_NODE_SHARD, '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: for field in V31_FIELDS:
@ -2006,3 +2007,11 @@ def allow_continue_inspection_endpoint():
""" """
return (new_continue_inspection_endpoint() return (new_continue_inspection_endpoint()
or api.request.version.minor == versions.MINOR_1_INITIAL_VERSION) 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_83_PARENT_CHILD_NODES = 83
MINOR_84_CONTINUE_INSPECTION = 84 MINOR_84_CONTINUE_INSPECTION = 84
MINOR_85_UNHOLD_VERB = 85 MINOR_85_UNHOLD_VERB = 85
MINOR_86_FIRMWARE_INTERFACE = 86
# When adding another version, update: # When adding another version, update:
# - MINOR_MAX_VERSION # - MINOR_MAX_VERSION
@ -216,7 +217,7 @@ MINOR_85_UNHOLD_VERB = 85
# explanation of what changed in the new version # explanation of what changed in the new version
# - common/release_mappings.py, RELEASE_MAPPING['master']['api'] # - 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 # String representations of the minor and maximum versions
_MIN_VERSION_STRING = '{}.{}'.format(BASE_VERSION, MINOR_1_INITIAL_VERSION) _MIN_VERSION_STRING = '{}.{}'.format(BASE_VERSION, MINOR_1_INITIAL_VERSION)

View File

@ -1009,6 +1009,15 @@ node_policies = [
'the API clients.', 'the API clients.',
operations=[{'path': '/nodes/{node_ident}', 'method': 'PATCH'}], 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 = """ deprecated_port_reason = """

View File

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

View File

@ -115,6 +115,11 @@ driver_opts = [
help=_ENABLED_IFACE_HELP.format('deploy')), help=_ENABLED_IFACE_HELP.format('deploy')),
cfg.StrOpt('default_deploy_interface', cfg.StrOpt('default_deploy_interface',
help=_DEFAULT_IFACE_HELP.format('deploy')), 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', cfg.ListOpt('enabled_inspect_interfaces',
default=['no-inspect', 'redfish'], default=['no-inspect', 'redfish'],
help=_ENABLED_IFACE_HELP.format('inspect')), help=_ENABLED_IFACE_HELP.format('inspect')),

View File

@ -78,6 +78,12 @@ opts = [
'rescue driver. Two comma-delimited values will ' 'rescue driver. Two comma-delimited values will '
'result in a delay with a triangular random ' 'result in a delay with a triangular random '
'distribution, weighted on the first value.')), '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. 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 inspect = None
"""`Standard` attribute for inspection related features. """`Standard` attribute for inspection related features.
@ -161,7 +167,8 @@ class BareDriver(object):
@property @property
def optional_interfaces(self): def optional_interfaces(self):
"""Interfaces that can be no-op.""" """Interfaces that can be no-op."""
return ['bios', 'console', 'inspect', 'raid', 'rescue', 'storage'] return ['bios', 'console', 'firmware', 'inspect', 'raid', 'rescue',
'storage']
@property @property
def all_interfaces(self): 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): def _validate_argsinfo(argsinfo):
"""Validate args info. """Validate args info.

View File

@ -86,3 +86,8 @@ class FakeHardware(generic.GenericHardware):
return [ return [
fake.FakeVendorB, fake.FakeVendorA fake.FakeVendorB, fake.FakeVendorA
] + super().supported_vendor_interfaces ] + 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, return [noop_storage.NoopStorage, cinder.CinderStorage,
external_storage.ExternalStorage] external_storage.ExternalStorage]
@property
def supported_firmware_interfaces(self):
"""List of supported firmware interfaces."""
return [noop.NoFirmware]
class ManualManagementHardware(GenericHardware): class ManualManagementHardware(GenericHardware):
"""Hardware type that uses manual power and boot management. """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.""" """List of supported vendor interfaces."""
return [noop.NoVendor] return [noop.NoVendor]
@property
def supported_firmware_interfaces(self):
"""List of supported firmware interfaces."""
return [noop.NoFirmware]
def get_properties(self): def get_properties(self):
"""Get the properties of the hardware type. """Get the properties of the hardware type.

View File

@ -443,3 +443,24 @@ class FakeRescue(base.RescueInterface):
def unrescue(self, task): def unrescue(self, task):
sleep(CONF.fake.rescue_delay) sleep(CONF.fake.rescue_delay)
return states.ACTIVE 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): def cache_bios_settings(self, task):
pass 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.36: Add boot_mode and secure_boot fields
# Version 1.37: Add shard field # Version 1.37: Add shard field
# Version 1.38: Add parent_node 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() dbapi = db_api.get_instance()
@ -155,6 +156,7 @@ class Node(base.IronicObject, object_base.VersionedObjectDictCompat):
'boot_interface': object_fields.StringField(nullable=True), 'boot_interface': object_fields.StringField(nullable=True),
'console_interface': object_fields.StringField(nullable=True), 'console_interface': object_fields.StringField(nullable=True),
'deploy_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), 'inspect_interface': object_fields.StringField(nullable=True),
'management_interface': object_fields.StringField(nullable=True), 'management_interface': object_fields.StringField(nullable=True),
'network_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. 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 Version 1.37: shard was added. Default is None. For versions prior to
this, it should be set to None or removed. 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 target_version: the desired version of the object
:param remove_unavailable_fields: True to remove fields that are :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), ('automated_clean', 28), ('protected_reason', 29),
('owner', 30), ('allocation_id', 31), ('description', 32), ('owner', 30), ('allocation_id', 31), ('description', 32),
('retired_reason', 33), ('lessee', 34), ('boot_mode', 36), ('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: for name, minor in fields:
self._adjust_field_to_version(name, None, target_version, self._adjust_field_to_version(name, None, target_version,

View File

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

View File

@ -219,7 +219,7 @@ class TestListDrivers(base.BaseApiTest):
for iface in driver_base.ALL_INTERFACES: for iface in driver_base.ALL_INTERFACES:
if iface != 'bios': 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('default_%s_interface' % iface, data)
self.assertIn('enabled_%s_interfaces' % 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) '/nodes/%s' % self.child_node.uuid, body, headers=headers)
self.assertEqual(http_client.OK, response.status_code) self.assertEqual(http_client.OK, response.status_code)
self.mock_update_node.assert_called_once() 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) value=fake_setting)
db_utils.create_test_node_trait( db_utils.create_test_node_trait(
node_id=fake_db_node['id']) 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_history = db_utils.create_test_history(node_id=fake_db_node.id)
fake_inventory = db_utils.create_test_inventory( fake_inventory = db_utils.create_test_inventory(
node_id=fake_db_node.id) node_id=fake_db_node.id)

View File

@ -3946,3 +3946,35 @@ lessee_cannot_get_a_nodes_children:
method: get method: get
headers: *lessee_reader_headers headers: *lessee_reader_headers
assert_status: 404 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 headers: *reader_headers
body: *patch_parent_node body: *patch_parent_node
assert_status: 403 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.""" """List of supported deploy interfaces."""
return [fake.FakeDeploy] return [fake.FakeDeploy]
@property
def supported_firmware_interfaces(self):
"""List of supported firmware interfaces."""
return [fake.FakeFirmware]
@property @property
def supported_inspect_interfaces(self): def supported_inspect_interfaces(self):
"""List of supported inspect interfaces.""" """List of supported inspect interfaces."""
@ -586,6 +591,7 @@ class HardwareTypeLoadTestCase(db_base.DbTestCase):
'boot': set(['fake']), 'boot': set(['fake']),
'console': set(['fake', 'no-console']), 'console': set(['fake', 'no-console']),
'deploy': set(['fake']), 'deploy': set(['fake']),
'firmware': set(['fake', 'no-firmware']),
'inspect': set(['fake', 'no-inspect']), 'inspect': set(['fake', 'no-inspect']),
'management': set(['fake']), 'management': set(['fake']),
'network': set(['noop']), 'network': set(['noop']),

View File

@ -3578,7 +3578,8 @@ class MiscTestCase(mgr_utils.ServiceSetUpMixin, mgr_utils.CommonMixIn,
'network': {'result': True}, 'network': {'result': True},
'storage': {'result': True}, 'storage': {'result': True},
'rescue': {'result': True}, 'rescue': {'result': True},
'bios': {'result': True}} 'bios': {'result': True},
'firmware': {'result': True}}
self.assertEqual(expected, ret) self.assertEqual(expected, ret)
mock_iwdi.assert_called_once_with(self.context, expected_info) 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) 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): class TestBareDriver(base.TestCase):
def test_class_variables(self): def test_class_variables(self):
self.assertEqual(['boot', 'deploy', 'management', 'network', 'power'], self.assertEqual(['boot', 'deploy', 'management', 'network', 'power'],
driver_base.BareDriver().core_interfaces) driver_base.BareDriver().core_interfaces)
self.assertEqual( self.assertEqual(
['bios', 'console', 'inspect', 'raid', 'rescue', 'storage'], ['bios', 'console', 'firmware', 'inspect', 'raid',
'rescue', 'storage'],
driver_base.BareDriver().optional_interfaces driver_base.BareDriver().optional_interfaces
) )

View File

@ -1378,6 +1378,68 @@ class TestConvertToVersion(db_base.DbTestCase):
self.assertIsNone(node.secure_boot) self.assertIsNone(node.secure_boot)
self.assertEqual({}, node.obj_get_changes()) 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): 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. # 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. # The fingerprint values should only be changed if there is a version bump.
expected_object_fingerprints = { expected_object_fingerprints = {
'Node': '1.38-7e7fdaa2c2bb01153ad567c9f1081cb7', 'Node': '1.39-ee3f5ff28b79f9fabf84a50e34a71684',
'MyObj': '1.5-9459d30d6954bffc7a9afd347a807ca6', 'MyObj': '1.5-9459d30d6954bffc7a9afd347a807ca6',
'Chassis': '1.3-d656e039fd8ae9f34efc232ab3980905', 'Chassis': '1.3-d656e039fd8ae9f34efc232ab3980905',
'Port': '1.11-97bf15b61224f26c65e90f007d78bfd2', 'Port': '1.11-97bf15b61224f26c65e90f007d78bfd2',

View File

@ -93,6 +93,10 @@ ironic.hardware.interfaces.deploy =
fake = ironic.drivers.modules.fake:FakeDeploy fake = ironic.drivers.modules.fake:FakeDeploy
ramdisk = ironic.drivers.modules.ramdisk:RamdiskDeploy 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 = ironic.hardware.interfaces.inspect =
fake = ironic.drivers.modules.fake:FakeInspect fake = ironic.drivers.modules.fake:FakeInspect
idrac = ironic.drivers.modules.drac.inspect:DracInspect idrac = ironic.drivers.modules.drac.inspect:DracInspect