From e15440370cca1f1a998d3607910697c3129d040a Mon Sep 17 00:00:00 2001 From: Bob Fournier Date: Fri, 16 Apr 2021 15:15:53 -0400 Subject: [PATCH] Include bios registry fields in bios API Provide the fields in the BIOS setting API - ``/v1/nodes/{node}/bios/{setting}``, and in the BIOS setting list API when details are requested - ``/v1/nodes//bios?detail=True``. Story: #2008571 Task: #42483 Change-Id: Ie86ec57e428e2bb2efd099a839105e51a94824ab --- .../source/baremetal-api-v1-nodes-bios.inc | 70 ++++++++++- api-ref/source/parameters.yaml | 67 ++++++++++- .../samples/node-bios-detail-response.json | 13 +- .../node-bios-list-details-response.json | 30 +++++ .../samples/node-bios-list-response.json | 4 +- .../contributor/webapi-version-history.rst | 11 ++ ironic/api/controllers/v1/bios.py | 41 +++++-- ironic/api/controllers/v1/utils.py | 26 +++- ironic/api/controllers/v1/versions.py | 4 +- ironic/common/release_mappings.py | 3 +- ironic/conductor/utils.py | 3 +- ironic/db/api.py | 4 + ...d96b6ccb9_add_bios_fields_from_registry.py | 46 +++++++ ironic/db/sqlalchemy/api.py | 21 ++++ ironic/db/sqlalchemy/models.py | 9 ++ ironic/objects/bios.py | 67 ++++++++++- .../unit/api/controllers/v1/test_node.py | 113 +++++++++++++++++- .../unit/common/test_release_mappings.py | 2 +- .../unit/db/sqlalchemy/test_migrations.py | 34 ++++++ ironic/tests/unit/db/test_bios_settings.py | 18 +-- ironic/tests/unit/db/utils.py | 16 ++- ironic/tests/unit/objects/test_bios.py | 50 ++++++-- ironic/tests/unit/objects/test_objects.py | 2 +- ...ios-registry-support-e7fd62908e9c222d.yaml | 8 ++ 24 files changed, 614 insertions(+), 48 deletions(-) create mode 100644 api-ref/source/samples/node-bios-list-details-response.json create mode 100644 ironic/db/sqlalchemy/alembic/versions/2bbd96b6ccb9_add_bios_fields_from_registry.py create mode 100644 releasenotes/notes/bios-registry-support-e7fd62908e9c222d.yaml diff --git a/api-ref/source/baremetal-api-v1-nodes-bios.inc b/api-ref/source/baremetal-api-v1-nodes-bios.inc index 4602b134b5..887e2029a9 100644 --- a/api-ref/source/baremetal-api-v1-nodes-bios.inc +++ b/api-ref/source/baremetal-api-v1-nodes-bios.inc @@ -19,6 +19,53 @@ List all Bios settings by Node Return a list of Bios settings associated with ``node_ident``. +.. versionadded:: 1.74 + Added additional fields from bios registry which can be retrieved using + ``?detail=True`` (see detailed response below). + Added ``fields`` selector to query for particular fields. + +Normal response code: 200 + +Error codes: 404 + +Request +------- + +.. rest_parameters:: parameters.yaml + + - node_ident: node_ident + - fields: fields + - detail: detail + +Response +-------- + +.. rest_parameters:: parameters.yaml + + - bios: bios_settings + - created_at: created_at + - updated_at: updated_at + - links: links + - name: bios_setting_name + - value: bios_setting_value + +**Example list of a Node's Bios settings:** + +.. literalinclude:: samples/node-bios-list-response.json + +List detailed Bios settings by Node +=================================== + +.. rest_method:: GET /v1/nodes/{node_ident}/bios/?detail=True + +Return a list of detailed Bios settings associated with ``node_ident``. +The detailed list includes the BIOS Attribute Registry information +retrieved via Redfish. + +.. versionadded:: 1.74 + Introduced + + Normal response code: 200 Error codes: 404 @@ -41,10 +88,19 @@ Response - links: links - name: bios_setting_name - value: bios_setting_value + - attribute_type: bios_setting_attribute_type + - allowable_values: bios_setting_allowable_values + - lower_bound: bios_setting_lower_bound + - max_length: bios_setting_max_length + - min_length: bios_setting_min_length + - read_only: bios_setting_read_only + - reset_required: bios_setting_reset_required + - unique: bios_setting_unique + - upper_bound: bios_setting_upper_bound **Example list of a Node's Bios settings:** -.. literalinclude:: samples/node-bios-list-response.json +.. literalinclude:: samples/node-bios-list-details-response.json Show single Bios setting of a Node @@ -55,6 +111,9 @@ Show single Bios setting of a Node Return the content of the specific bios ``bios_setting`` associated with ``node_ident``. +. versionadded:: 1.74 + Introduced fields from the BIOS registry. + Normal response code: 200 Error codes: 404 @@ -78,6 +137,15 @@ Response - links: links - name: bios_setting_name - value: bios_setting_value + - attribute_type: bios_setting_attribute_type + - allowable_values: bios_setting_allowable_values + - lower_bound: bios_setting_lower_bound + - max_length: bios_setting_max_length + - min_length: bios_setting_min_length + - read_only: bios_setting_read_only + - reset_required: bios_setting_reset_required + - unique: bios_setting_unique + - upper_bound: bios_setting_upper_bound **Example details of a Node's Bios setting details:** diff --git a/api-ref/source/parameters.yaml b/api-ref/source/parameters.yaml index 5129333f02..ef9b1b9756 100644 --- a/api-ref/source/parameters.yaml +++ b/api-ref/source/parameters.yaml @@ -505,12 +505,75 @@ bios_interface: in: body required: true type: string +bios_setting_allowable_values: + description: | + A list of allowable values when the attribute_type is "Enumeration", + otherwise None. + in: body + required: true + type: array +bios_setting_attribute_type: + description: | + A string describing the type of the Bios setting - "Enumeration", + "Integer", "String", "Boolean", or "Password". May be None. + in: body + required: true + type: string +bios_setting_lower_bound: + description: | + The lowest allowed value when attribute_type is "Integer". + May be None. + in: body + required: true + type: integer +bios_setting_max_length: + description: | + The maximum length when attribute_type is "String". + May be None. + in: body + required: true + type: integer +bios_setting_min_length: + description: | + The minimum length when attribute_type is "String". + May be None. + in: body + required: true + type: integer bios_setting_name: description: | The name of a Bios setting for a Node, eg. "virtualization". in: body required: true type: string +bios_setting_read_only: + description: | + This Bios seting is read only and can't be changed. + May be None. + in: body + required: true + type: boolean +bios_setting_reset_required: + description: | + After setting this Bios setting a node reboot is required. + May be None. + in: body + required: true + type: boolean +bios_setting_unique: + description: | + This Bios setting is unique to this node. + May be None. + in: body + required: true + type: boolean +bios_setting_upper_bound: + description: | + The lowest allowed value when attribute_type is "Integer". + May be None. + in: body + required: true + type: integer bios_setting_value: description: | The value of a Bios setting for a Node, eg. "on". @@ -520,7 +583,9 @@ bios_setting_value: bios_settings: description: | Optional list of one or more Bios settings. It includes following fields - "created_at", "updated_at", "links", "name", "value". + "created_at", "updated_at", "links", "name", "value", "attribute_type", + "allowable_values", "lower_bound", "max_length", "min_length", "read_only", + "reset_required", "unique", "upper_bound" in: body required: true type: array diff --git a/api-ref/source/samples/node-bios-detail-response.json b/api-ref/source/samples/node-bios-detail-response.json index a7b01028cc..195f897219 100644 --- a/api-ref/source/samples/node-bios-detail-response.json +++ b/api-ref/source/samples/node-bios-detail-response.json @@ -12,7 +12,16 @@ "rel": "bookmark" } ], - "name": "virtualization", - "value": "on" + "name": "Virtualization", + "value": "Enabled", + "attribute_type": "Enumeration", + "allowable_values": ["Enabled", "Disabled"], + "lower_bound": None, + "max_length": None, + "min_length": None, + "read_only": false, + "reset_required": None, + "unique": None, + "upper_bound": None } } diff --git a/api-ref/source/samples/node-bios-list-details-response.json b/api-ref/source/samples/node-bios-list-details-response.json new file mode 100644 index 0000000000..52a48b3b1f --- /dev/null +++ b/api-ref/source/samples/node-bios-list-details-response.json @@ -0,0 +1,30 @@ +{ + "bios": [ + { + "created_at": "2016-08-18T22:28:49.653974+00:00", + "updated_at": "2016-08-18T22:28:49.653974+00:00", + "links": [ + { + "href": "http://127.0.0.1:6385/v1/nodes/6d85703a-565d-469a-96ce-30b6de53079d/bios/virtualization", + "rel": "self" + }, + { + "href": "http://127.0.0.1:6385/v1/nodes/6d85703a-565d-469a-96ce-30b6de53079d/bios/virtualization", + "rel": "bookmark" + } + ], + "name": "Virtualization", + "value": "Enabled", + "attribute_type": "Enumeration", + "allowable_values": ["Enabled", "Disabled"], + "lower_bound": None, + "max_length": None, + "min_length": None, + "read_only": false, + "reset_required": None, + "unique": None, + "upper_bound": None + } + ] +} + diff --git a/api-ref/source/samples/node-bios-list-response.json b/api-ref/source/samples/node-bios-list-response.json index 87d5e9c898..8255c240e5 100644 --- a/api-ref/source/samples/node-bios-list-response.json +++ b/api-ref/source/samples/node-bios-list-response.json @@ -13,8 +13,8 @@ "rel": "bookmark" } ], - "name": "virtualization", - "value": "on" + "name": "Virtualization", + "value": "Enabled" } ] } diff --git a/doc/source/contributor/webapi-version-history.rst b/doc/source/contributor/webapi-version-history.rst index 37adc74cf1..10816c529d 100644 --- a/doc/source/contributor/webapi-version-history.rst +++ b/doc/source/contributor/webapi-version-history.rst @@ -2,8 +2,19 @@ REST API Version History ======================== +1.74 (Xena) +---------------------- + +Add support for BIOS registry fields which include details about the BIOS +setting. Included in the ``/v1/nodes/{node_ident}/bios/{setting}`` response. +Add a new selector to include the fields in the BIOS settings list: +* ``/v1/nodes/{node_ident}/bios/?detail=`` +Also add a fields selector to the the BIOS settings list: +* ``/v1/nodes/{node_ident}/bios/?fields=`` + 1.73 (Xena) ---------------------- + Add a new ``deploy`` verb as an alias to ``active`` and ``undeploy`` verb as an alias to ``deleted``. diff --git a/ironic/api/controllers/v1/bios.py b/ironic/api/controllers/v1/bios.py index 05fc46d8aa..8a3adf3b77 100644 --- a/ironic/api/controllers/v1/bios.py +++ b/ironic/api/controllers/v1/bios.py @@ -25,23 +25,34 @@ from ironic import objects METRICS = metrics_utils.get_metrics_logger(__name__) +_DEFAULT_RETURN_FIELDS = ('name', 'value') +_DEFAULT_FIELDS_WITH_REGISTRY = ('name', 'value', 'attribute_type', + 'allowable_values', 'lower_bound', + 'max_length', 'min_length', 'read_only', + 'reset_required', 'unique', 'upper_bound') -def convert_with_links(rpc_bios, node_uuid): + +def convert_with_links(rpc_bios, node_uuid, detail=None, fields=None): """Build a dict containing a bios setting value.""" + + if detail: + fields = _DEFAULT_FIELDS_WITH_REGISTRY + bios = api_utils.object_to_dict( rpc_bios, include_uuid=False, - fields=('name', 'value'), + fields=fields, link_resource='nodes', link_resource_args="%s/bios/%s" % (node_uuid, rpc_bios.name), ) return bios -def collection_from_list(node_ident, bios_settings): +def collection_from_list(node_ident, bios_settings, detail=None, fields=None): bios_list = [] for bios_setting in bios_settings: - bios_list.append(convert_with_links(bios_setting, node_ident)) + bios_list.append(convert_with_links(bios_setting, node_ident, + detail, fields)) return {'bios': bios_list} @@ -54,14 +65,23 @@ class NodeBiosController(rest.RestController): @METRICS.timer('NodeBiosController.get_all') @method.expose() - def get_all(self): + @args.validate(fields=args.string_list, detail=args.boolean) + def get_all(self, detail=None, fields=None): """List node bios settings.""" node = api_utils.check_node_policy_and_retrieve( 'baremetal:node:bios:get', self.node_ident) + # The BIOS detail and fields query were added in a later + # version, check if they are valid based on version + allow_query = api_utils.allow_query_bios + fields = api_utils.get_request_return_fields(fields, detail, + _DEFAULT_RETURN_FIELDS, + allow_query, allow_query) + settings = objects.BIOSSettingList.get_by_node_id( api.request.context, node.id) - return collection_from_list(self.node_ident, settings) + return collection_from_list(self.node_ident, settings, + detail, fields) @METRICS.timer('NodeBiosController.get_one') @method.expose() @@ -81,4 +101,11 @@ class NodeBiosController(rest.RestController): raise exception.BIOSSettingNotFound(node=node.uuid, name=setting_name) - return {setting_name: convert_with_links(setting, node.uuid)} + # Return fields based on version + if api_utils.allow_query_bios(): + fields = _DEFAULT_FIELDS_WITH_REGISTRY + else: + fields = _DEFAULT_RETURN_FIELDS + + return {setting_name: convert_with_links(setting, node.uuid, + fields=fields)} diff --git a/ironic/api/controllers/v1/utils.py b/ironic/api/controllers/v1/utils.py index 83c2a16027..4a753c4d11 100644 --- a/ironic/api/controllers/v1/utils.py +++ b/ironic/api/controllers/v1/utils.py @@ -1321,18 +1321,27 @@ def allow_detail_query(): return api.request.version.minor >= versions.MINOR_43_ENABLE_DETAIL_QUERY +def allow_query_bios(): + """Check if BIOS queries should be allowed based on version""" + + return api.request.version.minor >= versions.MINOR_74_BIOS_REGISTRY + + def allow_reset_interfaces(): """Check if passing a reset_interfaces query string is allowed.""" return api.request.version.minor >= versions.MINOR_45_RESET_INTERFACES -def get_request_return_fields(fields, detail, default_fields): +def get_request_return_fields(fields, detail, default_fields, + check_detail_version=allow_detail_query, + check_fields_version=None): """Calculate fields to return from an API request The fields query and detail=True query can not be passed into a request at the same time. To use the detail query we need to be on a version of the - API greater than 1.43. This function raises an InvalidParameterValue - exception if either of these conditions are not met. + API greater than expected, likewise some APIs require a certain version for + the fields query. This function raises an InvalidParameterValue exception + if any of these conditions are not met. If these checks pass then this function will return either the fields passed in or the default fields provided. @@ -1341,15 +1350,24 @@ def get_request_return_fields(fields, detail, default_fields): :param detail: The detail query passed into the API request. :param default_fields: The default fields to return if fields=None and detail=None. + :param check_detail_version: Function to check if detail query is allowed + based on the version. + :param check_fields_version: Function to check if fields query is allowed + based on the version. :raises: InvalidParameterValue if there is an invalid combination of query strings or API version. :returns: 'fields' passed in value or 'default_fields' """ - if detail is not None and not allow_detail_query(): + if detail is not None and not check_detail_version(): raise exception.InvalidParameterValue( "Invalid query parameter ?detail=%s received." % detail) + if (fields is not None and callable(check_fields_version) + and not check_fields_version()): + raise exception.InvalidParameterValue( + "Invalid query parameter ?fields=%s received." % fields) + if fields is not None and detail: raise exception.InvalidParameterValue( "Can not specify ?detail=True and fields in the same request.") diff --git a/ironic/api/controllers/v1/versions.py b/ironic/api/controllers/v1/versions.py index 97e03485ed..677a27475c 100644 --- a/ironic/api/controllers/v1/versions.py +++ b/ironic/api/controllers/v1/versions.py @@ -111,6 +111,7 @@ BASE_VERSION = 1 # v1.71: Add signifier for Scope based roles. # v1.72: Add agent_status and agent_status_message to /v1/heartbeat # v1.73: Add support for deploy and undeploy verbs +# v1.74: Add bios registry to /v1/nodes/{node}/bios/{setting} MINOR_0_JUNO = 0 MINOR_1_INITIAL_VERSION = 1 @@ -186,6 +187,7 @@ MINOR_70_CLEAN_DISABLE_RAMDISK = 70 MINOR_71_RBAC_SCOPES = 71 MINOR_72_HEARTBEAT_STATUS = 72 MINOR_73_DEPLOY_UNDEPLOY_VERBS = 73 +MINOR_74_BIOS_REGISTRY = 74 # When adding another version, update: # - MINOR_MAX_VERSION @@ -193,7 +195,7 @@ MINOR_73_DEPLOY_UNDEPLOY_VERBS = 73 # explanation of what changed in the new version # - common/release_mappings.py, RELEASE_MAPPING['master']['api'] -MINOR_MAX_VERSION = MINOR_73_DEPLOY_UNDEPLOY_VERBS +MINOR_MAX_VERSION = MINOR_74_BIOS_REGISTRY # String representations of the minor and maximum versions _MIN_VERSION_STRING = '{}.{}'.format(BASE_VERSION, MINOR_1_INITIAL_VERSION) diff --git a/ironic/common/release_mappings.py b/ironic/common/release_mappings.py index 526f424f7b..29cde3fc05 100644 --- a/ironic/common/release_mappings.py +++ b/ironic/common/release_mappings.py @@ -320,10 +320,11 @@ RELEASE_MAPPING = { } }, 'master': { - 'api': '1.73', + 'api': '1.74', 'rpc': '1.54', 'objects': { 'Allocation': ['1.1'], + 'BIOSSetting': ['1.1'], 'Node': ['1.35'], 'Conductor': ['1.3'], 'Chassis': ['1.3'], diff --git a/ironic/conductor/utils.py b/ironic/conductor/utils.py index 2a3e6b4f83..cbadd091c5 100644 --- a/ironic/conductor/utils.py +++ b/ironic/conductor/utils.py @@ -1385,8 +1385,7 @@ def store_agent_certificate(node, agent_verify_ca): def node_cache_bios_settings(task, node): """Do caching of bios settings if supported by driver""" try: - LOG.debug('BF getting BIOS info for node %s', - node.uuid) + LOG.debug('Getting BIOS info for node %s', node.uuid) task.driver.bios.cache_bios_settings(task) except exception.UnsupportedDriverExtension: LOG.warning('BIOS settings are not supported for node %s, ' diff --git a/ironic/db/api.py b/ironic/db/api.py index da08384785..0845fcd55e 100644 --- a/ironic/db/api.py +++ b/ironic/db/api.py @@ -1054,10 +1054,12 @@ class Connection(object, metaclass=abc.ABCMeta): { 'name': String, 'value': String, + additional settings from BIOS registry }, { 'name': String, 'value': String, + additional settings from BIOS registry }, ... ] @@ -1081,10 +1083,12 @@ class Connection(object, metaclass=abc.ABCMeta): { 'name': String, 'value': String, + additional settings from BIOS registry }, { 'name': String, 'value': String, + additional settings from BIOS registry }, ... ] diff --git a/ironic/db/sqlalchemy/alembic/versions/2bbd96b6ccb9_add_bios_fields_from_registry.py b/ironic/db/sqlalchemy/alembic/versions/2bbd96b6ccb9_add_bios_fields_from_registry.py new file mode 100644 index 0000000000..204bc46fd2 --- /dev/null +++ b/ironic/db/sqlalchemy/alembic/versions/2bbd96b6ccb9_add_bios_fields_from_registry.py @@ -0,0 +1,46 @@ +# 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. + +"""Add fields from BIOS registry + +Revision ID: 2bbd96b6ccb9 +Revises: ac00b586ab95 +Create Date: 2021-04-29 08:52:23.938863 + +""" +from alembic import op +import sqlalchemy as sa + +# revision identifiers, used by Alembic. +revision = '2bbd96b6ccb9' +down_revision = 'ac00b586ab95' + + +def upgrade(): + op.add_column('bios_settings', sa.Column('attribute_type', + sa.String(length=255), nullable=True)) + op.add_column('bios_settings', sa.Column('allowable_values', + sa.Text(), nullable=True)) + op.add_column('bios_settings', sa.Column('lower_bound', + sa.Integer(), nullable=True)) + op.add_column('bios_settings', sa.Column('max_length', + sa.Integer(), nullable=True)) + op.add_column('bios_settings', sa.Column('min_length', + sa.Integer(), nullable=True)) + op.add_column('bios_settings', sa.Column('read_only', + sa.Boolean(), nullable=True)) + op.add_column('bios_settings', sa.Column('reset_required', + sa.Boolean(), nullable=True)) + op.add_column('bios_settings', sa.Column('unique', + sa.Boolean(), nullable=True)) + op.add_column('bios_settings', sa.Column('upper_bound', + sa.Integer(), nullable=True)) diff --git a/ironic/db/sqlalchemy/api.py b/ironic/db/sqlalchemy/api.py index 702755e972..6f027c96af 100644 --- a/ironic/db/sqlalchemy/api.py +++ b/ironic/db/sqlalchemy/api.py @@ -1675,6 +1675,15 @@ class Connection(api.Connection): node_id=node_id, name=setting['name'], value=setting['value'], + attribute_type=setting.get('attribute_type'), + allowable_values=setting.get('allowable_values'), + lower_bound=setting.get('lower_bound'), + max_length=setting.get('max_length'), + min_length=setting.get('min_length'), + read_only=setting.get('read_only'), + reset_required=setting.get('reset_required'), + unique=setting.get('unique'), + upper_bound=setting.get('upper_bound'), version=version) bios_settings.append(bios_setting) session.add(bios_setting) @@ -1695,6 +1704,18 @@ class Connection(api.Connection): node_id=node_id, name=setting['name']) ref = query.one() ref.update({'value': setting['value'], + 'attribute_type': + setting.get('attribute_type'), + 'allowable_values': + setting.get('allowable_values'), + 'lower_bound': setting.get('lower_bound'), + 'max_length': setting.get('max_length'), + 'min_length': setting.get('min_length'), + 'read_only': setting.get('read_only'), + 'reset_required': + setting.get('reset_required'), + 'unique': setting.get('unique'), + 'upper_bound': setting.get('upper_bound'), 'version': version}) bios_settings.append(ref) session.flush() diff --git a/ironic/db/sqlalchemy/models.py b/ironic/db/sqlalchemy/models.py index ef85b5d6d7..96c18af260 100644 --- a/ironic/db/sqlalchemy/models.py +++ b/ironic/db/sqlalchemy/models.py @@ -339,6 +339,15 @@ class BIOSSetting(Base): primary_key=True, nullable=False) name = Column(String(255), primary_key=True, nullable=False) value = Column(Text, nullable=True) + attribute_type = Column(String(255), nullable=True) + allowable_values = Column(db_types.JsonEncodedList, nullable=True) + lower_bound = Column(Integer, nullable=True) + max_length = Column(Integer, nullable=True) + min_length = Column(Integer, nullable=True) + read_only = Column(Boolean, nullable=True) + reset_required = Column(Boolean, nullable=True) + unique = Column(Boolean, nullable=True) + upper_bound = Column(Integer, nullable=True) class Allocation(Base): diff --git a/ironic/objects/bios.py b/ironic/objects/bios.py index c7f705ef7e..cb6718074f 100644 --- a/ironic/objects/bios.py +++ b/ironic/objects/bios.py @@ -13,6 +13,7 @@ # License for the specific language governing permissions and limitations # under the License. +from oslo_utils import versionutils from oslo_versionedobjects import base as object_base from ironic.db import api as dbapi @@ -23,14 +24,29 @@ from ironic.objects import fields as object_fields @base.IronicObjectRegistry.register class BIOSSetting(base.IronicObject): # Version 1.0: Initial version - VERSION = '1.0' + # Version 1.1: Added registry + VERSION = '1.1' dbapi = dbapi.get_instance() + registry_fields = ('attribute_type', 'allowable_values', 'lower_bound', + 'max_length', 'min_length', 'read_only', + 'reset_required', 'unique', 'upper_bound') + fields = { 'node_id': object_fields.StringField(nullable=False), 'name': object_fields.StringField(nullable=False), 'value': object_fields.StringField(nullable=True), + 'attribute_type': object_fields.StringField(nullable=True), + 'allowable_values': object_fields.ListOfStringsField( + nullable=True), + 'lower_bound': object_fields.IntegerField(nullable=True), + 'max_length': object_fields.IntegerField(nullable=True), + 'min_length': object_fields.IntegerField(nullable=True), + 'read_only': object_fields.BooleanField(nullable=True), + 'reset_required': object_fields.BooleanField(nullable=True), + 'unique': object_fields.BooleanField(nullable=True), + 'upper_bound': object_fields.IntegerField(nullable=True) } # NOTE(xek): We don't want to enable RPC on this call just yet. Remotable @@ -50,9 +66,12 @@ class BIOSSetting(base.IronicObject): :raises: BIOSSettingAlreadyExists if the setting record already exists. """ values = self.do_version_changes_for_db() - setting = [{'name': values['name'], 'value': values['value']}] + settings = {'name': values['name'], 'value': values['value']} + for r in self.registry_fields: + settings[r] = values.get(r) + db_bios_setting = self.dbapi.create_bios_setting_list( - values['node_id'], setting, values['version']) + values['node_id'], [settings], values['version']) self._from_db_object(self._context, self, db_bios_setting[0]) # NOTE(xek): We don't want to enable RPC on this call just yet. Remotable @@ -72,9 +91,13 @@ class BIOSSetting(base.IronicObject): :raises: BIOSSettingNotFound if the bios setting name is not found. """ values = self.do_version_changes_for_db() - setting = [{'name': values['name'], 'value': values['value']}] + + settings = {'name': values['name'], 'value': values['value']} + for r in self.registry_fields: + settings[r] = values.get(r) + updated_bios_setting = self.dbapi.update_bios_setting_list( - values['node_id'], setting, values['version']) + values['node_id'], [settings], values['version']) self._from_db_object(self._context, self, updated_bios_setting[0]) # NOTE(xek): We don't want to enable RPC on this call just yet. Remotable @@ -111,6 +134,40 @@ class BIOSSetting(base.IronicObject): """ cls.dbapi.delete_bios_setting_list(node_id, [name]) + def _convert_to_version(self, target_version, + remove_unavailable_fields=True): + """Convert to the target version. + + Convert the object to the target version. The target version may be + the same, older, or newer than the version of the object. This is + used for DB interactions as well as for serialization/deserialization. + + Version 1.74: remove registry field for unsupported versions if + remove_unavailable_fields is True. + + :param target_version: the desired version of the object + :param remove_unavailable_fields: True to remove fields that are + unavailable in the target version; set this to True when + (de)serializing. False to set the unavailable fields to appropriate + values; set this to False for DB interactions. + """ + target_version = versionutils.convert_version_to_tuple(target_version) + + for field in self.get_registry_fields(): + field_is_set = self.obj_attr_is_set(field) + if target_version >= (1, 74): + # target version supports the major/minor specified + if not field_is_set: + # set it to its default value if it is not set + setattr(self, field, None) + elif field_is_set: + # target version does not support the field, and it is set + if remove_unavailable_fields: + # (De)serialising: remove unavailable fields + delattr(self, field) + elif self.registry: + setattr(self, field, None) + @base.IronicObjectRegistry.register class BIOSSettingList(base.IronicObjectListBase, base.IronicObject): diff --git a/ironic/tests/unit/api/controllers/v1/test_node.py b/ironic/tests/unit/api/controllers/v1/test_node.py index baa21d5a97..4a94671a92 100644 --- a/ironic/tests/unit/api/controllers/v1/test_node.py +++ b/ironic/tests/unit/api/controllers/v1/test_node.py @@ -6685,7 +6685,7 @@ class TestBIOS(test_api_base.BaseApiTest): def setUp(self): super(TestBIOS, self).setUp() - self.version = "1.40" + self.version = "1.74" self.node = obj_utils.create_test_node( self.context, id=1) self.bios = obj_utils.create_test_bios_setting(self.context, @@ -6718,13 +6718,40 @@ class TestBIOS(test_api_base.BaseApiTest): expected_json = { 'virtualization': { + 'allowable_values': ['on', 'off'], + 'attribute_type': 'Enumeration', 'created_at': ret['virtualization']['created_at'], - 'updated_at': ret['virtualization']['updated_at'], 'links': [ {'href': 'http://localhost/v1/nodes/%s/bios/virtualization' % self.node.uuid, u'rel': u'self'}, {'href': 'http://localhost/nodes/%s/bios/virtualization' % self.node.uuid, u'rel': u'bookmark'}], + 'lower_bound': None, + 'min_length': None, + 'max_length': None, + 'name': 'virtualization', + 'read_only': False, + 'reset_required': True, + 'unique': False, + 'updated_at': None, + 'upper_bound': None, + 'value': 'on'}} + + self.assertEqual(expected_json, ret) + + def test_get_one_bios_no_registry(self): + ret = self.get_json('/nodes/%s/bios/virtualization' % self.node.uuid, + headers={api_base.Version.string: "1.73"}) + + expected_json = { + 'virtualization': { + 'created_at': ret['virtualization']['created_at'], + 'updated_at': ret['virtualization']['updated_at'], + 'links': [ + {'href': 'http://localhost/v1/nodes/%s/bios/virtualization' + % self.node.uuid, 'rel': 'self'}, + {'href': 'http://localhost/nodes/%s/bios/virtualization' + % self.node.uuid, 'rel': 'bookmark'}], 'name': 'virtualization', 'value': 'on'}} self.assertEqual(expected_json, ret) @@ -6742,6 +6769,88 @@ class TestBIOS(test_api_base.BaseApiTest): self.assertIn("fake_setting", ret.json['error_message']) self.assertNotIn(self.node.id, ret.json['error_message']) + def test_get_all_bios_with_detail(self): + ret = self.get_json('/nodes/%s/bios?detail=True' % self.node.uuid, + headers={api_base.Version.string: self.version}) + + expected_json = [ + {'allowable_values': ['on', 'off'], + 'attribute_type': 'Enumeration', + 'created_at': ret['bios'][0]['created_at'], + 'links': [ + {'href': 'http://localhost/v1/nodes/%s/bios/virtualization' + % self.node.uuid, 'rel': 'self'}, + {'href': 'http://localhost/nodes/%s/bios/virtualization' + % self.node.uuid, 'rel': 'bookmark'}], + 'lower_bound': None, + 'max_length': None, + 'min_length': None, + 'name': 'virtualization', + 'read_only': False, + 'reset_required': True, + 'unique': False, + 'updated_at': None, + 'upper_bound': None, + 'value': 'on'}] + + self.assertEqual({'bios': expected_json}, ret) + + def test_get_all_bios_detail_false(self): + ret = self.get_json('/nodes/%s/bios?detail=False' % self.node.uuid, + headers={api_base.Version.string: self.version}) + + expected_json = [ + {'created_at': ret['bios'][0]['created_at'], + 'updated_at': ret['bios'][0]['updated_at'], + 'links': [ + {'href': 'http://localhost/v1/nodes/%s/bios/virtualization' + % self.node.uuid, 'rel': 'self'}, + {'href': 'http://localhost/nodes/%s/bios/virtualization' + % self.node.uuid, 'rel': 'bookmark'}], + 'name': 'virtualization', 'value': 'on'}] + self.assertEqual({'bios': expected_json}, ret) + + def test_get_all_bios_detail_old_version(self): + ret = self.get_json('/nodes/%s/bios?detail=True' % self.node.uuid, + headers={api_base.Version.string: "1.73"}, + expect_errors=True) + + self.assertEqual(http_client.BAD_REQUEST, ret.status_int) + + def test_get_bios_fields_old_version(self): + ret = self.get_json('/nodes/%s/bios?fields=name,read_only' + % self.node.uuid, + headers={api_base.Version.string: "1.73"}, + expect_errors=True) + + self.assertEqual(http_client.BAD_REQUEST, ret.status_int) + + def test_get_bios_detail_and_fields(self): + ret = self.get_json('/nodes/%s/bios?detail=True?fields=name,read_only' + % self.node.uuid, + headers={api_base.Version.string: "1.74"}, + expect_errors=True) + + self.assertEqual(http_client.BAD_REQUEST, ret.status_int) + + def test_get_bios_fields(self): + ret = self.get_json('/nodes/%s/bios?fields=name,read_only' + % self.node.uuid, + headers={api_base.Version.string: self.version}) + + expected_json = [ + {'created_at': ret['bios'][0]['created_at'], + 'links': [ + {'href': 'http://localhost/v1/nodes/%s/bios/virtualization' + % self.node.uuid, 'rel': 'self'}, + {'href': 'http://localhost/nodes/%s/bios/virtualization' + % self.node.uuid, 'rel': 'bookmark'}], + 'name': 'virtualization', + 'read_only': False, + 'updated_at': None}] + + self.assertEqual({'bios': expected_json}, ret) + class TestTraits(test_api_base.BaseApiTest): diff --git a/ironic/tests/unit/common/test_release_mappings.py b/ironic/tests/unit/common/test_release_mappings.py index b5adfa060e..db2d37c602 100644 --- a/ironic/tests/unit/common/test_release_mappings.py +++ b/ironic/tests/unit/common/test_release_mappings.py @@ -85,7 +85,7 @@ class ReleaseMappingsTestCase(base.TestCase): self.assertIn('master', release_mappings.RELEASE_MAPPING) model_names = set((s.__name__ for s in models.Base.__subclasses__())) exceptions = set(['NodeTag', 'ConductorHardwareInterfaces', - 'NodeTrait', 'BIOSSetting', 'DeployTemplateStep']) + 'NodeTrait', 'DeployTemplateStep']) # NOTE(xek): As a rule, all models which can be changed between # releases or are sent through RPC should have their counterpart # versioned objects. diff --git a/ironic/tests/unit/db/sqlalchemy/test_migrations.py b/ironic/tests/unit/db/sqlalchemy/test_migrations.py index 7a2641323b..3b81e3b92a 100644 --- a/ironic/tests/unit/db/sqlalchemy/test_migrations.py +++ b/ironic/tests/unit/db/sqlalchemy/test_migrations.py @@ -705,6 +705,40 @@ class MigrationCheckersMixin(object): bios_settings.c.name == setting['name'])).execute().first() self.assertEqual('on', setting['value']) + def _check_2bbd96b6ccb9(self, engine, data): + bios_settings = db_utils.get_table(engine, 'bios_settings') + col_names = [column.name for column in bios_settings.c] + self.assertIn('attribute_type', col_names) + self.assertIn('allowable_values', col_names) + self.assertIn('lower_bound', col_names) + self.assertIn('max_length', col_names) + self.assertIn('min_length', col_names) + self.assertIn('read_only', col_names) + self.assertIn('reset_required', col_names) + self.assertIn('unique', col_names) + self.assertIn('upper_bound', col_names) + self.assertIsInstance(bios_settings.c.attribute_type.type, + sqlalchemy.types.String) + self.assertIsInstance(bios_settings.c.allowable_values.type, + sqlalchemy.types.TEXT) + self.assertIsInstance(bios_settings.c.lower_bound.type, + sqlalchemy.types.Integer) + self.assertIsInstance(bios_settings.c.max_length.type, + sqlalchemy.types.Integer) + self.assertIsInstance(bios_settings.c.min_length.type, + sqlalchemy.types.Integer) + self.assertIsInstance(bios_settings.c.read_only.type, + (sqlalchemy.types.Boolean, + sqlalchemy.types.Integer)) + self.assertIsInstance(bios_settings.c.reset_required.type, + (sqlalchemy.types.Boolean, + sqlalchemy.types.Integer)) + self.assertIsInstance(bios_settings.c.unique.type, + (sqlalchemy.types.Boolean, + sqlalchemy.types.Integer)) + self.assertIsInstance(bios_settings.c.upper_bound.type, + sqlalchemy.types.Integer) + def _check_2d13bc3d6bba(self, engine, data): nodes = db_utils.get_table(engine, 'nodes') col_names = [column.name for column in nodes.c] diff --git a/ironic/tests/unit/db/test_bios_settings.py b/ironic/tests/unit/db/test_bios_settings.py index a13f516a35..9cb077e07c 100644 --- a/ironic/tests/unit/db/test_bios_settings.py +++ b/ironic/tests/unit/db/test_bios_settings.py @@ -29,7 +29,7 @@ class DbBIOSSettingTestCase(base.DbTestCase): self.assertEqual(result['node_id'], self.node.id) self.assertEqual(result['name'], 'virtualization') self.assertEqual(result['value'], 'on') - self.assertEqual(result['version'], '1.0') + self.assertEqual(result['version'], '1.1') def test_get_bios_setting_node_not_exist(self): self.assertRaises(exception.NodeNotFound, @@ -50,7 +50,7 @@ class DbBIOSSettingTestCase(base.DbTestCase): self.assertEqual(result[0]['node_id'], self.node.id) self.assertEqual(result[0]['name'], 'virtualization') self.assertEqual(result[0]['value'], 'on') - self.assertEqual(result[0]['version'], '1.0') + self.assertEqual(result[0]['version'], '1.1') self.assertEqual(len(result), 1) def test_get_bios_setting_list_node_not_exist(self): @@ -61,7 +61,7 @@ class DbBIOSSettingTestCase(base.DbTestCase): def test_create_bios_setting_list(self): settings = db_utils.get_test_bios_setting_setting_list() result = self.dbapi.create_bios_setting_list( - self.node.id, settings, '1.0') + self.node.id, settings, '1.1') self.assertCountEqual(['virtualization', 'hyperthread', 'numlock'], [setting.name for setting in result]) self.assertCountEqual(['on', 'enabled', 'off'], @@ -69,7 +69,7 @@ class DbBIOSSettingTestCase(base.DbTestCase): def test_create_bios_setting_list_duplicate(self): settings = db_utils.get_test_bios_setting_setting_list() - self.dbapi.create_bios_setting_list(self.node.id, settings, '1.0') + self.dbapi.create_bios_setting_list(self.node.id, settings, '1.1') self.assertRaises(exception.BIOSSettingAlreadyExists, self.dbapi.create_bios_setting_list, self.node.id, settings, '1.0') @@ -81,18 +81,18 @@ class DbBIOSSettingTestCase(base.DbTestCase): def test_update_bios_setting_list(self): settings = db_utils.get_test_bios_setting_setting_list() - self.dbapi.create_bios_setting_list(self.node.id, settings, '1.0') + self.dbapi.create_bios_setting_list(self.node.id, settings, '1.1') settings = [{'name': 'virtualization', 'value': 'off'}, {'name': 'hyperthread', 'value': 'disabled'}, {'name': 'numlock', 'value': 'on'}] result = self.dbapi.update_bios_setting_list( - self.node.id, settings, '1.0') + self.node.id, settings, '1.1') self.assertCountEqual(['off', 'disabled', 'on'], [setting.value for setting in result]) def test_update_bios_setting_list_setting_not_exist(self): settings = db_utils.get_test_bios_setting_setting_list() - self.dbapi.create_bios_setting_list(self.node.id, settings, '1.0') + self.dbapi.create_bios_setting_list(self.node.id, settings, '1.1') for setting in settings: setting['name'] = 'bios_name' self.assertRaises(exception.BIOSSettingNotFound, @@ -106,7 +106,7 @@ class DbBIOSSettingTestCase(base.DbTestCase): def test_delete_bios_setting_list(self): settings = db_utils.get_test_bios_setting_setting_list() - self.dbapi.create_bios_setting_list(self.node.id, settings, '1.0') + self.dbapi.create_bios_setting_list(self.node.id, settings, '1.1') name_list = [setting['name'] for setting in settings] self.dbapi.delete_bios_setting_list(self.node.id, name_list) self.assertRaises(exception.BIOSSettingNotFound, @@ -126,7 +126,7 @@ class DbBIOSSettingTestCase(base.DbTestCase): def test_delete_bios_setting_list_setting_not_exist(self): settings = db_utils.get_test_bios_setting_setting_list() - self.dbapi.create_bios_setting_list(self.node.id, settings, '1.0') + self.dbapi.create_bios_setting_list(self.node.id, settings, '1.1') self.assertRaises(exception.BIOSSettingListNotFound, self.dbapi.delete_bios_setting_list, self.node.id, ['fake-bios-option']) diff --git a/ironic/tests/unit/db/utils.py b/ironic/tests/unit/db/utils.py index bf025d6afb..cb8e553386 100644 --- a/ironic/tests/unit/db/utils.py +++ b/ironic/tests/unit/db/utils.py @@ -566,7 +566,12 @@ def create_test_bios_setting(**kw): node_id = bios_setting['node_id'] version = bios_setting['version'] settings = [{'name': bios_setting['name'], - 'value': bios_setting['value']}] + 'value': bios_setting['value'], + 'attribute_type': bios_setting['attribute_type'], + 'allowable_values': bios_setting['allowable_values'], + 'read_only': bios_setting['read_only'], + 'reset_required': bios_setting['reset_required'], + 'unique': bios_setting['unique']}] return dbapi.create_bios_setting_list(node_id, settings, version)[0] @@ -575,6 +580,15 @@ def get_test_bios_setting(**kw): 'node_id': kw.get('node_id', '123'), 'name': kw.get('name', 'virtualization'), 'value': kw.get('value', 'on'), + 'attribute_type': kw.get('attribute_type', 'Enumeration'), + 'allowable_values': kw.get('allowable_values', ['on', 'off']), + 'lower_bound': kw.get('lower_bound', None), + 'max_length': kw.get('max_length', None), + 'min_length': kw.get('max_length', None), + 'read_only': kw.get('read_only', False), + 'reset_required': kw.get('reset_required', True), + 'unique': kw.get('unique', False), + 'upper_bound': kw.get('upper_bound', None), 'version': kw.get('version', bios.BIOSSetting.VERSION), 'created_at': kw.get('created_at'), 'updated_at': kw.get('updated_at'), diff --git a/ironic/tests/unit/objects/test_bios.py b/ironic/tests/unit/objects/test_bios.py index 57538fbbf8..5f4d8b9993 100644 --- a/ironic/tests/unit/objects/test_bios.py +++ b/ironic/tests/unit/objects/test_bios.py @@ -42,6 +42,15 @@ class TestBIOSSettingObject(db_base.DbTestCase, obj_utils.SchemasTestMixIn): self.assertEqual(self.bios_setting['node_id'], bios_obj.node_id) self.assertEqual(self.bios_setting['name'], bios_obj.name) self.assertEqual(self.bios_setting['value'], bios_obj.value) + self.assertEqual(self.bios_setting['attribute_type'], + bios_obj.attribute_type) + self.assertEqual(self.bios_setting['allowable_values'], + bios_obj.allowable_values) + self.assertEqual(self.bios_setting['reset_required'], + bios_obj.reset_required) + self.assertEqual(self.bios_setting['read_only'], + bios_obj.read_only) + self.assertEqual(self.bios_setting['unique'], bios_obj.unique) @mock.patch.object(dbapi.IMPL, 'get_bios_setting_list', autospec=True) def test_get_by_node_id(self, mock_get_setting_list): @@ -67,9 +76,22 @@ class TestBIOSSettingObject(db_base.DbTestCase, obj_utils.SchemasTestMixIn): fake_call_args = {'node_id': self.bios_setting['node_id'], 'name': self.bios_setting['name'], 'value': self.bios_setting['value'], + 'attribute_type': + self.bios_setting['attribute_type'], + 'allowable_values': + self.bios_setting['allowable_values'], + 'read_only': self.bios_setting['read_only'], + 'reset_required': + self.bios_setting['reset_required'], + 'unique': self.bios_setting['unique'], 'version': self.bios_setting['version']} - setting = [{'name': self.bios_setting['name'], - 'value': self.bios_setting['value']}] + setting = [{'name': 'virtualization', 'value': 'on', 'attribute_type': + 'Enumeration', 'allowable_values': ['on', 'off'], + 'lower_bound': None, 'max_length': None, + 'min_length': None, 'read_only': False, + 'reset_required': True, 'unique': False, + 'upper_bound': None}] + bios_obj = objects.BIOSSetting(context=self.context, **fake_call_args) mock_create_list.return_value = [self.bios_setting] @@ -81,6 +103,15 @@ class TestBIOSSettingObject(db_base.DbTestCase, obj_utils.SchemasTestMixIn): self.assertEqual(self.bios_setting['node_id'], bios_obj.node_id) self.assertEqual(self.bios_setting['name'], bios_obj.name) self.assertEqual(self.bios_setting['value'], bios_obj.value) + self.assertEqual(self.bios_setting['attribute_type'], + bios_obj.attribute_type) + self.assertEqual(self.bios_setting['allowable_values'], + bios_obj.allowable_values) + self.assertEqual(self.bios_setting['read_only'], + bios_obj.read_only) + self.assertEqual(self.bios_setting['reset_required'], + bios_obj.reset_required) + self.assertEqual(self.bios_setting['unique'], bios_obj.unique) @mock.patch.object(dbapi.IMPL, 'update_bios_setting_list', autospec=True) def test_save(self, mock_update_list): @@ -89,7 +120,12 @@ class TestBIOSSettingObject(db_base.DbTestCase, obj_utils.SchemasTestMixIn): 'value': self.bios_setting['value'], 'version': self.bios_setting['version']} setting = [{'name': self.bios_setting['name'], - 'value': self.bios_setting['value']}] + 'value': self.bios_setting['value'], + 'attribute_type': None, 'allowable_values': None, + 'lower_bound': None, 'max_length': None, + 'min_length': None, 'read_only': None, + 'reset_required': None, 'unique': None, + 'upper_bound': None}] bios_obj = objects.BIOSSetting(context=self.context, **fake_call_args) mock_update_list.return_value = [self.bios_setting] @@ -111,7 +147,7 @@ class TestBIOSSettingObject(db_base.DbTestCase, obj_utils.SchemasTestMixIn): bios_obj_list = objects.BIOSSettingList.create( self.context, self.node_id, settings) - mock_create_list.assert_called_once_with(self.node_id, settings, '1.0') + mock_create_list.assert_called_once_with(self.node_id, settings, '1.1') self.assertEqual(self.context, bios_obj_list._context) self.assertEqual(2, len(bios_obj_list)) self.assertEqual(self.bios_setting['node_id'], @@ -120,7 +156,6 @@ class TestBIOSSettingObject(db_base.DbTestCase, obj_utils.SchemasTestMixIn): self.assertEqual(self.bios_setting['value'], bios_obj_list[0].value) self.assertEqual(bios_setting2['node_id'], bios_obj_list[1].node_id) self.assertEqual(bios_setting2['name'], bios_obj_list[1].name) - self.assertEqual(bios_setting2['value'], bios_obj_list[1].value) @mock.patch.object(dbapi.IMPL, 'update_bios_setting_list', autospec=True) def test_list_save(self, mock_update_list): @@ -131,7 +166,7 @@ class TestBIOSSettingObject(db_base.DbTestCase, obj_utils.SchemasTestMixIn): bios_obj_list = objects.BIOSSettingList.save( self.context, self.node_id, settings) - mock_update_list.assert_called_once_with(self.node_id, settings, '1.0') + mock_update_list.assert_called_once_with(self.node_id, settings, '1.1') self.assertEqual(self.context, bios_obj_list._context) self.assertEqual(2, len(bios_obj_list)) self.assertEqual(self.bios_setting['node_id'], @@ -189,8 +224,7 @@ class TestBIOSSettingObject(db_base.DbTestCase, obj_utils.SchemasTestMixIn): objects.BIOSSettingList.sync_node_setting(self.ctxt, node.id, settings)) - expected_delete = [{'name': bios_obj_1.name, - 'value': bios_obj_1.value}] + expected_delete = [{'name': 'virtualization', 'value': 'on'}] self.assertEqual(create, settings[:2]) self.assertEqual(update, []) self.assertEqual(delete, expected_delete) diff --git a/ironic/tests/unit/objects/test_objects.py b/ironic/tests/unit/objects/test_objects.py index 7b7788933d..4e38e123da 100644 --- a/ironic/tests/unit/objects/test_objects.py +++ b/ironic/tests/unit/objects/test_objects.py @@ -711,7 +711,7 @@ expected_object_fingerprints = { 'VolumeTargetCRUDPayload': '1.0-30dcc4735512c104a3a36a2ae1e2aeb2', 'Trait': '1.0-3f26cb70c8a10a3807d64c219453e347', 'TraitList': '1.0-33a2e1bb91ad4082f9f63429b77c1244', - 'BIOSSetting': '1.0-fd4a791dc2139a7cc21cefbbaedfd9e7', + 'BIOSSetting': '1.1-1137db88675a4e2d7f7bcc3a0d52345a', 'BIOSSettingList': '1.0-33a2e1bb91ad4082f9f63429b77c1244', 'Allocation': '1.1-38937f2854722f1057ec667b12878708', 'AllocationCRUDNotification': '1.0-59acc533c11d306f149846f922739c15', diff --git a/releasenotes/notes/bios-registry-support-e7fd62908e9c222d.yaml b/releasenotes/notes/bios-registry-support-e7fd62908e9c222d.yaml new file mode 100644 index 0000000000..adb49983ed --- /dev/null +++ b/releasenotes/notes/bios-registry-support-e7fd62908e9c222d.yaml @@ -0,0 +1,8 @@ +--- +features: + - | + Provide the registry fields in the BIOS setting API and in the BIOS setting + list when detail is requested. Also added fields selector to query API. + See `story + 2008571 `_. +