Add support for BIOS Attribute Registry

Add support for caching Attribute Registries. In particular,
cache the BIOS Attribute Registry and provide a function to return it
if it matches the AttributeRegistry field in System BIOS.

Change-Id: I1b83cfd9d9a44ac00e423c589bcc2c53c14ad478
This commit is contained in:
Bob Fournier 2021-04-01 20:18:26 -04:00
parent c0da841700
commit 2d88bc9d86
12 changed files with 495 additions and 34 deletions

View File

@ -0,0 +1,6 @@
---
features:
- |
Add support for caching Redfish Attribute Registries. In particular,
cache the BIOS Attribute Registry and provide a function to return it
if it matches the AttributeRegistry field in System BIOS.

View File

@ -466,14 +466,16 @@ class Sushy(base.ResourceBase):
@property
@utils.cache_it
def registries(self):
"""Gets and combines all message registries together
"""Gets and combines all registries together
Fetches all registries if any provided by Redfish service
and combines together with packaged standard registries.
Both message and attribute registries are supported from
the Redfish service.
:returns: dict of combined message registries keyed by both the
:returns: dict of combined registries keyed by both the
registry name (Registry_name.Major_version.Minor_version) and the
message registry file identity, with the value being the actual
registry file identity, with the value being the actual
registry itself.
"""
standard = self._get_standard_message_registry_collection()
@ -487,12 +489,19 @@ class Sushy(base.ResourceBase):
if registry_col:
provided = registry_col.get_members()
for r in provided:
message_registry = r.get_message_registry(
# Check for Message and Attribute registries
registry = r.get_message_registry(
self._language,
self._public_connector)
registries[r.registry] = message_registry
if r.identity not in registries:
registries[r.identity] = message_registry
if not registry:
registry = r.get_attribute_registry(
self._language,
self._public_connector)
if registry:
registries[r.registry] = registry
if r.identity not in registries:
registries[r.identity] = registry
return registries

View File

@ -0,0 +1,98 @@
# 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.
# The Redfish standard schema that defines the AttributeRegistry is at:
# https://redfish.dmtf.org/schemas/v1/AttributeRegistry.v1_3_5.json
import logging
from sushy.resources import base
LOG = logging.getLogger(__name__)
class AttributeListField(base.ListField):
name = base.Field('AttributeName', required=True)
"""The unique name for the attribute"""
default_value = base.Field('DefaultValue')
"""The default value for the attribute"""
attribute_type = base.Field('Type')
"""The attribute type"""
unique = base.Field('IsSystemUniqueProperty', adapter=bool)
"""Indicates whether this attribute is unique for this system"""
display_name = base.Field('DisplayName')
"""User-readable display string for attribute in the defined language"""
immutable = base.Field('Immutable', adapter=bool)
"""An indication of whether this attribute is immutable"""
read_only = base.Field('ReadOnly', adapter=bool)
"""An indication of whether this attribute is read-only"""
reset_required = base.Field('ResetRequired', adapter=bool)
"""An indication of whether this attribute is read-only"""
lower_bound = base.Field('LowerBound')
"""The lower limit for an integer attribute"""
max_length = base.Field('MaxLength')
"""The maximum character length of the string attribute"""
min_length = base.Field('MinLength')
"""The minimum character length of the string attribute"""
upper_bound = base.Field('UpperBound')
"""The upper limit for an integer attribute"""
allowable_values = base.Field('Value')
"""An array of the possible values for enumerated attribute values"""
class AttributeRegistryEntryField(base.CompositeField):
attributes = AttributeListField('Attributes')
"""List of attributes in this registry"""
# Vendors may have aditional items such as Dependencies, Menus, etc.
# Only get the attributes.
class AttributeRegistry(base.ResourceBase):
identity = base.Field('Id', required=True)
"""The Attribute registry identity string"""
name = base.Field('Name', required=True)
"""The name of the attribute registry"""
description = base.Field('Description')
"""Human-readable description of the registry"""
language = base.Field('Language', required=True)
"""RFC 5646 compliant language code for the registry"""
owning_entity = base.Field('OwningEntity', required=True)
"""Organization or company that publishes this registry"""
registry_version = base.Field('RegistryVersion', required=True)
"""The version of this registry"""
supported_systems = base.Field('SupportedSystems')
"""The system that this registry supports"""
registry_entries = AttributeRegistryEntryField('RegistryEntries')
"""Field containing Attributes, Dependencies, Menus etc."""

View File

@ -87,9 +87,11 @@ class MessageRegistry(base.ResourceBase):
def parse_message(message_registries, message_field):
"""Using message registries parse the message and substitute any parms
"""Parse the messages in registries and substitute any parms
:param message_registries: dict of Message Registries
Check only registries that support messages.
:param registries: dict of Message Registries
:param message_field: settings.MessageListField to parse
:returns: parsed settings.MessageListField with missing attributes filled
@ -99,8 +101,9 @@ def parse_message(message_registries, message_field):
if '.' in message_field.message_id:
registry, msg_key = message_field.message_id.rsplit('.', 1)
if (registry in message_registries and msg_key
in message_registries[registry].messages):
if (registry in message_registries
and hasattr(message_registries[registry], "messages")
and msg_key in message_registries[registry].messages):
reg_msg = message_registries[registry].messages[msg_key]
else:
# Some firmware only reports the MessageKey and no RegistryName.

View File

@ -17,6 +17,7 @@
import logging
from sushy.resources import base
from sushy.resources.registry import attribute_registry
from sushy.resources.registry import message_registry
LOG = logging.getLogger(__name__)
@ -76,15 +77,43 @@ class MessageRegistryFile(base.ResourceBase):
"""List of locations of Registry files for each supported language"""
def get_message_registry(self, language, public_connector):
"""Load message registry file depending on its source
Will try to find `MessageRegistry` based on `odata.type` property and
provided language. If desired language is not found, will pick a
registry that has 'default' language.
"""Get a Message Registry from the location
:param language: RFC 5646 language code for registry files
:param public_connector: connector to use when downloading registry
from the Internet
:returns: a MessageRegistry or None if not found
"""
return self._get_registry(language, public_connector,
'MessageRegistry',
message_registry.MessageRegistry)
def get_attribute_registry(self, language, public_connector):
"""Get an Attribute Registry from the location
:param language: RFC 5646 language code for registry files
:param public_connector: connector to use when downloading registry
from the Internet
:returns: an AttributeRegistry or None if not found
"""
return self._get_registry(language, public_connector,
'AttributeRegistry',
attribute_registry.AttributeRegistry)
def _get_registry(self, language, public_connector, requested_type,
registry_class):
"""Load registry file depending on the registry type
Will try to find requested_type based on `odata.type` property,
location, and provided language. If desired language is not found,
will pick a registry that has 'default' language.
:param language: RFC 5646 language code for registry files
:param public_connector: connector to use when downloading registry
from the Internet
:param requested_type: string identifying registry
:param registry_class: registry class
:returns: registry or None if not found
"""
# NOTE (etingof): as per RFC5646, languages are case-insensitive
@ -127,34 +156,35 @@ class MessageRegistryFile(base.ResourceBase):
continue
try:
registry = RegistryType(*args, **kwargs)
registry_type = RegistryType(*args, **kwargs)
except Exception as exc:
LOG.warning(
'Cannot load message registry type from location '
'Cannot load registry type from location '
'%(location)s: %(error)s', {
'location': kwargs['path'],
'error': exc})
continue
if registry._odata_type.endswith('MessageRegistry'):
if registry_type._odata_type.endswith(requested_type):
try:
return message_registry.MessageRegistry(*args, **kwargs)
return registry_class(*args, **kwargs)
except Exception as exc:
LOG.warning(
'Cannot load message registry from location '
'Cannot load registry %(type)s from location '
'%(location)s: %(error)s', {
'type': requested_type,
'location': kwargs['path'],
'error': exc})
continue
LOG.debug('Ignoring unsupported flavor of registry %(registry)s',
{'registry': registry._odata_type})
{'registry': registry_type._odata_type})
return
LOG.warning('No message registry found for %(language)s or '
'default', {'language': language})
LOG.warning('No registry found for %(language)s or default',
{'language': language})
class MessageRegistryFileCollection(base.ResourceCollectionBase):

View File

@ -247,3 +247,31 @@ class Bios(base.ResourceBase):
:returns: List of supported update apply time names
"""
return self._settings._supported_apply_times
def get_attribute_registry(self, language='en'):
"""Get the Attribute Registry associated with this BIOS instance
:param language: RFC 5646 language code for Message Registries.
Indicates language of registry to be used. Defaults to 'en'.
:returns: the BIOS Attribute Registry
"""
registries = self._registries
for key, registry in registries.items():
# Check that BIOS attribute_registry matches the identity
# of a registry, and this is the requested language
if registry and self._attribute_registry == registry.identity:
if language != registry.language:
LOG.debug('Found BIOS attribute registry but '
'language %(lang)s does not match '
'%(reg_lang)s',
{'lang': language,
'reg_lang': registry.language})
continue
return registry
LOG.info('BIOS attribute registry %(registry)s '
'not available for language %(lang)s',
{'registry': self._attribute_registry,
'lang': language})
return None

View File

@ -0,0 +1,65 @@
{
"@odata.context": "/redfish/v1/$metadata#AttributeRegistry.AttributeRegistry",
"@odata.id": "/redfish/v1/Test/Bios/BiosRegistry",
"@odata.type": "#AttributeRegistry.v1_1_1.AttributeRegistry",
"Description": "This is a test of BIOS Attribute Registry",
"Id": "BiosAttributeRegistryP89.v1_0_0",
"Language": "en",
"Name": "BIOS Attribute Registry",
"OwningEntity": "VendorA",
"RegistryVersion": "1.0.0",
"SupportedSystems": [
{
"FirmwareVersion": "2.0",
"ProductName": "Ultra 4",
"SystemId": "URGR8"
}
],
"RegistryEntries": {
"Attributes": [
{
"AttributeName": "SystemModelName",
"CurrentValue": null,
"DisplayName": "System Model Name",
"DisplayOrder": 200,
"HelpText": "Indicates the product name of the system.",
"Hidden": false,
"Immutable": true,
"MaxLength": 40,
"MenuPath": "./SysInformationRef",
"MinLength": 0,
"ReadOnly": true,
"Type": "String",
"ValueExpression": null,
"WriteOnly": false
},
{
"AttributeName": "ProcVirtualization",
"CurrentValue": null,
"DisplayName": "Virtualization Technology",
"DisplayOrder": 404,
"HelpText": "When set to Enabled, the BIOS will enable processor Virtualization features",
"Hidden": false,
"Immutable": false,
"MenuPath": "./ProcSettingsRef",
"ReadOnly": false,
"ResetRequired": true,
"Type": "Enumeration",
"Value": [
{
"ValueDisplayName": "Enabled",
"ValueName": "Enabled"
},
{
"ValueDisplayName": "Disabled",
"ValueName": "Disabled"
}
],
"WarningText": null,
"WriteOnly": false
}
]
}
}

View File

@ -0,0 +1,29 @@
{
"@odata.context": "/redfish/v1/$metadata#MessageRegistryFile.MessageRegistryFile",
"@odata.id": "/redfish/v1/Registries/BiosAttributeRegistry.v1_0",
"@odata.type": "#MessageRegistryFile.v1_0_4.MessageRegistryFile",
"Id": "BiosAttributeRegistry.v1_0",
"Description": "Registry Definition File for BiosAttributeRegistry.v1_0",
"Languages": [
"en",
"ja",
"zh"
],
"Location": [
{
"Language": "en",
"Uri": "/redfish/v1/registrystore/registries/en/biosattributeregistry.v1_0"
},
{
"Language": "ja",
"Uri": "/redfish/v1/registrystore/registries/ja/biosattributeregistry.v1_0"
},
{
"Language": "zh",
"Uri": "/redfish/v1/registrystore/registries/zh/biosattributeregistry.v1_0"
}
],
"Name": "BiosAttributeRegistry.v1_0 Message Registry File",
"Registry": "BiosAttributeRegistry.v1_0"
}

View File

@ -0,0 +1,69 @@
# 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.
import json
from unittest import mock
from sushy.resources.registry import attribute_registry
from sushy.tests.unit import base
class AttributeRegistryTestCase(base.TestCase):
def setUp(self):
super(AttributeRegistryTestCase, self).setUp()
self.conn = mock.Mock()
with open('sushy/tests/unit/json_samples/'
'bios_attribute_registry.json') as f:
self.json_doc = json.load(f)
self.conn.get.return_value.json.return_value = self.json_doc
self.registry = attribute_registry.AttributeRegistry(
self.conn, '/redfish/v1/Test/Bios/BiosRegistry',
redfish_version='1.0.2')
def test__parse_attributes(self):
self.registry._parse_attributes(self.json_doc)
self.assertEqual('BiosAttributeRegistryP89.v1_0_0',
self.registry.identity)
self.assertEqual('BIOS Attribute Registry', self.registry.name)
self.assertEqual('en', self.registry.language)
self.assertEqual('This is a test of BIOS Attribute Registry',
self.registry.description)
self.assertEqual('1.0.0', self.registry.registry_version)
self.assertEqual('VendorA', self.registry.owning_entity)
self.assertEqual([{'FirmwareVersion': '2.0', 'ProductName': 'Ultra 4',
'SystemId': 'URGR8'}],
self.registry.supported_systems)
attributes = self.registry.registry_entries.attributes[0]
self.assertEqual('SystemModelName', attributes.name)
self.assertEqual('System Model Name', attributes.display_name)
self.assertEqual(True, attributes.immutable)
self.assertEqual(True, attributes.read_only)
self.assertEqual('String', attributes.attribute_type)
attributes = self.registry.registry_entries.attributes[1]
self.assertEqual('ProcVirtualization', attributes.name)
self.assertEqual('Virtualization Technology', attributes.display_name)
self.assertEqual(False, attributes.immutable)
self.assertEqual(False, attributes.read_only)
self.assertEqual(True, attributes.reset_required)
self.assertEqual('Enumeration', attributes.attribute_type)
self.assertEqual([{'ValueDisplayName': 'Enabled',
'ValueName': 'Enabled'},
{'ValueDisplayName': 'Disabled',
'ValueName': 'Disabled'}],
attributes.allowable_values)

View File

@ -19,6 +19,7 @@ from unittest import mock
from sushy.resources import base as sushy_base
from sushy.resources import constants as res_cons
from sushy.resources.registry import attribute_registry
from sushy.resources.registry import message_registry
from sushy.tests.unit import base
@ -117,7 +118,7 @@ class MessageRegistryTestCase(base.TestCase):
self.registry._parse_attributes(self.json_doc)
self.assertEqual('warning', self.registry.messages['Success'].severity)
def test__parse_attribtues_unknown_param_type(self):
def test__parse_attributes_unknown_param_type(self):
self.registry.json['Messages']['Failed']['ParamTypes'] = \
['unknown_type']
self.assertRaisesRegex(KeyError,
@ -291,3 +292,28 @@ class MessageRegistryTestCase(base.TestCase):
self.assertEqual(res_cons.SEVERITY_WARNING, parsed_msg.severity)
self.assertEqual('Property\'s arg1 value cannot be greater than '
'unknown.', parsed_msg.message)
def test_parse_message_multiple_registries(self):
conn = mock.Mock()
with open('sushy/tests/unit/json_samples/message_registry.json') as f:
conn.get.return_value.json.return_value = json.load(f)
msg_registry = message_registry.MessageRegistry(
conn, '/redfish/v1/Registries/Test',
redfish_version='1.0.2')
attr_registry = attribute_registry.AttributeRegistry(
self.conn, '/redfish/v1/Test/Bios/BiosRegistry',
redfish_version='1.0.2')
registries = {'Test.1.0.2': attr_registry,
'Test.1.0.0': msg_registry}
message_field = sushy_base.MessageListField('Foo')
message_field.message_id = 'Test.1.0.0.TooBig'
message_field.message_args = ['arg1', 10]
message_field.severity = None
message_field.resolution = None
parsed_msg = message_registry.parse_message(registries, message_field)
self.assertEqual('Try again', parsed_msg.resolution)
self.assertEqual(res_cons.SEVERITY_WARNING, parsed_msg.severity)
self.assertEqual('Property\'s arg1 value cannot be greater than 10.',
parsed_msg.message)

View File

@ -155,7 +155,7 @@ class MessageRegistryFileTestCase(base.TestCase):
mock_msg_reg.assert_not_called()
self.assertIsNone(registry)
mock_log.warning.assert_called_with(
'No message registry found for %(language)s or default',
'No registry found for %(language)s or default',
{'language': 'en'})
@mock.patch('sushy.resources.registry.message_registry.MessageRegistry',
@ -189,12 +189,13 @@ class MessageRegistryFileTestCase(base.TestCase):
expected_calls = [
mock.call(
'Cannot load message registry from location %(location)s: '
'Cannot load registry %(type)s from location %(location)s: '
'%(error)s',
{'location': {'extref': 'http://127.0.0.1/reg'},
{'type': 'MessageRegistry',
'location': {'extref': 'http://127.0.0.1/reg'},
'error': mock.ANY}),
mock.call(
'No message registry found for %(language)s or default',
'No registry found for %(language)s or default',
{'language': 'en'})
]
@ -224,12 +225,12 @@ class MessageRegistryFileTestCase(base.TestCase):
self.assertTrue(mock_reg_type.called)
self.assertIsNone(registry)
mock_log.warning.assert_any_call(
'Cannot load message registry type from location '
'Cannot load registry type from location '
'%(location)s: %(error)s',
{'location': '/redfish/v1/Registries/Test/Test.1.0.json',
'error': mock.ANY})
mock_log.warning.assert_called_with(
'No message registry found for %(language)s or default',
'No registry found for %(language)s or default',
{'language': 'en'})
@mock.patch('sushy.resources.registry.message_registry_file.RegistryType',
@ -258,7 +259,7 @@ class MessageRegistryFileTestCase(base.TestCase):
mock_msg_reg.assert_not_called()
self.assertIsNone(registry)
mock_log.warning.assert_called_with(
'No message registry found for %(language)s or default',
'No registry found for %(language)s or default',
{'language': 'en'})
@mock.patch('sushy.resources.base.logging.warning',
@ -272,6 +273,44 @@ class MessageRegistryFileTestCase(base.TestCase):
'attribute "[\'Registry\']"')
class BiosRegistryTestCase(base.TestCase):
def setUp(self):
super(BiosRegistryTestCase, self).setUp()
self.conn = mock.Mock()
with open('sushy/tests/unit/json_samples/'
'bios_attribute_registry_file.json') as f:
self.json_doc = json.load(f)
self.conn.get.return_value.json.return_value = self.json_doc
self.reg_file = message_registry_file.MessageRegistryFile(
self.conn, '/redfish/v1/Registries/BiosAttributeRegistry.v1_0',
redfish_version='1.0.2')
@mock.patch('sushy.resources.registry.attribute_registry.'
'AttributeRegistry',
autospec=True)
@mock.patch('sushy.resources.base.JsonDataReader', autospec=True)
def test_get_bios_registry_uri(self, mock_reader, mock_msg_reg):
mock_reader_rv = mock.Mock()
mock_reader.return_value = mock_reader_rv
mock_reader_rv.get_data.return_value = FieldData(200, {}, {
"@odata.type": "#AttributeRegistry.v1_0_0.AttributeRegistry",
"Id": "BiosAttributeRegistry.v1_0",
})
mock_msg_reg_rv = mock.Mock()
mock_msg_reg.return_value = mock_msg_reg_rv
registry = self.reg_file.get_attribute_registry('en', None)
mock_msg_reg.assert_called_once_with(
self.conn,
path='/redfish/v1/registrystore/registries/en/'
'biosattributeregistry.v1_0',
reader=None, redfish_version=self.reg_file.redfish_version)
self.assertEqual(mock_msg_reg_rv, registry)
class MessageRegistryFileCollectionTestCase(base.TestCase):
def setUp(self):

View File

@ -21,6 +21,7 @@ from dateutil import parser
from sushy import exceptions
from sushy.resources import constants as res_cons
from sushy.resources.registry import attribute_registry
from sushy.resources.registry import message_registry
from sushy.resources import settings
from sushy.resources.system import bios
@ -42,16 +43,28 @@ class BiosTestCase(base.TestCase):
self.bios_settings_json,
self.bios_settings_json]
registries = {}
conn = mock.Mock()
with open('sushy/tests/unit/json_samples/message_registry.json') as f:
conn.get.return_value.json.return_value = json.load(f)
registry = message_registry.MessageRegistry(
msg_reg = message_registry.MessageRegistry(
conn, '/redfish/v1/Registries/Test',
redfish_version='1.0.2')
registries['Test.1.0'] = msg_reg
with open('sushy/tests/unit/json_samples/'
'bios_attribute_registry.json') as f:
conn.get.return_value.json.return_value = json.load(f)
bios_reg = attribute_registry.AttributeRegistry(
conn, '/redfish/v1/Registries/BiosRegistryTest',
redfish_version='1.0.2')
registries['BiosRegistry.1.0'] = bios_reg
self.sys_bios = bios.Bios(
self.conn, '/redfish/v1/Systems/437XR1138R2/BIOS',
registries={'Test.1.0': registry},
registries=registries,
redfish_version='1.0.2')
def test__parse_attributes(self):
@ -333,3 +346,49 @@ class BiosTestCase(base.TestCase):
data={'OldPassword': 'oldpassword',
'NewPassword': 'newpassword',
'PasswordName': 'adminpassword'})
def test_get_attribute_registry(self):
registry = self.sys_bios.get_attribute_registry()
self.assertEqual(registry.name, 'BIOS Attribute Registry')
self.assertEqual(registry.description, 'This is a test of BIOS '
'Attribute Registry')
self.assertEqual(registry.registry_entries.attributes[0].name,
'SystemModelName')
self.assertEqual(registry.registry_entries.attributes[0].display_name,
'System Model Name')
self.assertEqual(registry.registry_entries.attributes[0].immutable,
True)
self.assertEqual(registry.registry_entries.attributes[0].read_only,
True)
self.assertEqual(registry.registry_entries.attributes[0].
attribute_type, 'String')
self.assertEqual(registry.registry_entries.attributes[1].name,
'ProcVirtualization')
self.assertEqual(registry.registry_entries.attributes[1].display_name,
'Virtualization Technology')
self.assertEqual(registry.registry_entries.attributes[1].immutable,
False)
self.assertEqual(registry.registry_entries.attributes[1].read_only,
False)
self.assertEqual(registry.registry_entries.attributes[1].
reset_required, True)
self.assertEqual(registry.registry_entries.attributes[1].
attribute_type, 'Enumeration')
self.assertEqual(registry.registry_entries.attributes[1].
allowable_values,
[{'ValueDisplayName': 'Enabled',
'ValueName': 'Enabled'},
{'ValueDisplayName': 'Disabled',
'ValueName': 'Disabled'}])
def test_get_attribute_registry_no_lang(self):
registry = self.sys_bios.get_attribute_registry(language='zh')
self.assertIsNone(registry)
def test_get_attribute_registry_not_found(self):
self.sys_bios._attribute_registry = "Unknown"
registry = self.sys_bios.get_attribute_registry()
self.assertIsNone(registry)