Merge "Introduce BIOS API"

This commit is contained in:
Zuul 2018-06-25 15:53:35 +00:00 committed by Gerrit Code Review
commit acc27a1b15
11 changed files with 626 additions and 0 deletions

View File

@ -0,0 +1,4 @@
---
features:
- |
Adds support for the BIOS resource to the library.

View File

@ -20,3 +20,10 @@ class ActionField(base.CompositeField):
class ResetActionField(ActionField):
allowed_values = base.Field('ResetType@Redfish.AllowableValues',
adapter=list)
class IdRefField(base.CompositeField):
"""Reference to the resource for updating settings"""
resource_uri = base.Field('@odata.id')
"""The unique identifier for a resource"""

101
sushy/resources/settings.py Normal file
View File

@ -0,0 +1,101 @@
# 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.
# This is referred from Redfish standard schema.
# http://redfish.dmtf.org/schemas/v1/Settings.v1_0_0.json
from sushy.resources import base
from sushy.resources import common
class MessageListField(base.ListField):
"""List of messages with details of settings update status"""
message_id = base.Field('MessageId', required=True)
"""The key for this message which can be used
to look up the message in a message registry
"""
message = base.Field('Message')
"""Human readable message, if provided"""
severity = base.Field('Severity')
"""Severity of the error"""
resolution = base.Field('Resolution')
"""Used to provide suggestions on how to resolve
the situation that caused the error
"""
_related_properties = base.Field('RelatedProperties')
"""List of properties described by the message"""
message_args = base.Field('MessageArgs')
"""List of message substitution arguments for the message
referenced by `message_id` from the message registry
"""
class SettingsField(base.CompositeField):
"""The settings of a resource
Represents the future state and configuration of the resource. The
field is added to resources that support future state and
configuration.
This field includes several properties to help clients monitor when
the resource is consumed by the service and determine the results of
applying the values, which may or may not have been successful.
"""
def __init__(self):
super(SettingsField, self).__init__(path="@Redfish.Settings")
time = base.Field('Time')
"""Indicates the time the settings were applied to the server"""
_etag = base.Field('ETag')
"""The ETag of the resource to which the settings were applied,
after the application
"""
_settings_object_idref = common.IdRefField("SettingsObject")
"""Reference to the resource the client may PUT/PATCH in order
to change this resource
"""
messages = MessageListField("Messages")
"""Represents the results of the last time the values of the Settings
resource were applied to the server"""
def commit(self, connector, value, etag=None):
"""Commits new settings values
The new values will be applied when the system or a service
restarts.
:param connector: A Connector instance
:param value: Value representing JSON whose structure is specific
to each resource and the caller must format it correctly
:param etag: Optional ETag of resource version to update. If
this ETag is provided and it does not match on server, then
the new values will not be committed
"""
connector.patch(self.resource_uri,
data=value,
headers={'If-Match': etag} if etag else None)
@property
def resource_uri(self):
return self._settings_object_idref.resource_uri

View File

@ -0,0 +1,162 @@
# 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.
# This is referred from Redfish standard schema.
# https://redfish.dmtf.org/schemas/Bios.v1_0_3.json
import logging
from sushy import exceptions
from sushy.resources import base
from sushy.resources import common
from sushy.resources import settings
LOG = logging.getLogger(__name__)
class ActionsField(base.CompositeField):
change_password = common.ActionField('#Bios.ChangePassword')
reset_bios = common.ActionField('#Bios.ResetBios')
class Bios(base.ResourceBase):
identity = base.Field('Id', required=True)
"""The Bios resource identity string"""
name = base.Field('Name')
"""The name of the resource"""
description = base.Field('Description')
"""Human-readable description of the BIOS resource"""
attribute_registry = base.Field('AttributeRegistry')
"""The Resource ID of the Attribute Registry
for the BIOS Attributes resource
"""
_settings = settings.SettingsField()
"""Results of last BIOS attribute update"""
attributes = base.Field('Attributes')
"""Vendor-specific key-value dict of effective BIOS attributes
Attributes cannot be updated directly.
To update use :py:func:`~set_attribute` or :py:func:`~set_attributes`
"""
_actions = ActionsField('Actions')
_etag = base.Field('@odata.etag')
_pending_settings_resource = None
@property
def pending_attributes(self):
"""Pending BIOS attributes
BIOS attributes that have been comitted to the system,
but for them to take effect system restart is necessary
"""
if not self._pending_settings_resource:
self._pending_settings_resource = Bios(
self._conn,
self._settings.resource_uri,
redfish_version=self.redfish_version)
self._pending_settings_resource.refresh(force=False)
return self._pending_settings_resource.attributes
def set_attribute(self, key, value):
"""Update an attribute
Attribute update is not immediate but requires system restart.
Committed attributes can be checked at :py:attr:`~pending_attributes`
property
:param key: Attribute name
:param value: Attribute value
"""
self.set_attributes({key: value})
def set_attributes(self, value):
"""Update many attributes at once
Attribute update is not immediate but requires system restart.
Committed attributes can be checked at :py:attr:`~pending_attributes`
property
:param value: Key-value pairs for attribute name and value
"""
self._settings.commit(self._conn,
{'Attributes': value},
self._etag)
if self._pending_settings_resource:
self._pending_settings_resource.invalidate()
def _get_reset_bios_action_element(self):
actions = self._actions
if not actions:
raise exceptions.MissingAttributeError(attribute="Actions",
resource=self._path)
reset_bios_action = actions.reset_bios
if not reset_bios_action:
raise exceptions.MissingActionError(action='#Bios.ResetBios',
resource=self._path)
return reset_bios_action
def _get_change_password_element(self):
actions = self._actions
if not actions:
raise exceptions.MissingAttributeError(attribute="Actions",
resource=self._path)
change_password_action = actions.change_password
if not change_password_action:
raise exceptions.MissingActionError(action='#Bios.ChangePassword',
resource=self._path)
return change_password_action
def reset_bios(self):
"""Reset the BIOS attributes to default"""
target_uri = self._get_reset_bios_action_element().target_uri
LOG.debug('Resetting BIOS attributes %s ...', self.identity)
self._conn.post(target_uri)
LOG.info('BIOS attributes %s is being reset', self.identity)
def change_password(self, new_password, old_password, password_name):
"""Change BIOS password"""
target_uri = self._get_change_password_element().target_uri
LOG.debug('Changing BIOS password %s ...', self.identity)
self._conn.post(target_uri, data={'NewPassword': new_password,
'OldPassword': old_password,
'PasswordName': password_name})
LOG.info('BIOS password %s is being changed', self.identity)
def _do_refresh(self, force=False):
"""Do custom resource specific refresh activities
On refresh, all sub-resources are marked as stale, i.e.
greedy-refresh not done for them unless forced by ``force``
argument.
"""
if self._pending_settings_resource is not None:
self._pending_settings_resource.invalidate(force)

View File

@ -18,6 +18,7 @@ import logging
from sushy import exceptions
from sushy.resources import base
from sushy.resources import common
from sushy.resources.system import bios
from sushy.resources.system import constants as sys_cons
from sushy.resources.system import ethernet_interface
from sushy.resources.system import mappings as sys_maps
@ -130,6 +131,8 @@ class System(base.ResourceBase):
_ethernet_interfaces = None
_bios = None
def __init__(self, connector, identity, redfish_version=None):
"""A class representing a ComputerSystem
@ -289,6 +292,23 @@ class System(base.ResourceBase):
self._ethernet_interfaces.refresh(force=False)
return self._ethernet_interfaces
@property
def bios(self):
"""Property to reference `Bios` instance
It is set once when the first time it is queried. On refresh,
this property is marked as stale (greedy-refresh not done).
Here the actual refresh of the sub-resource happens, if stale.
"""
if self._bios is None:
self._bios = bios.Bios(
self._conn,
utils.get_sub_resource_path_by(self, 'Bios'),
redfish_version=self.redfish_version)
self._bios.refresh(force=False)
return self._bios
def _do_refresh(self, force=False):
"""Do custom resource specific refresh activities
@ -300,6 +320,8 @@ class System(base.ResourceBase):
self._processors.invalidate(force)
if self._ethernet_interfaces is not None:
self._ethernet_interfaces.invalidate(force)
if self._bios is not None:
self._bios.invalidate(force)
class SystemCollection(base.ResourceCollectionBase):

View File

@ -0,0 +1,45 @@
{
"@odata.type": "#Bios.v1_0_0.Bios",
"Id": "BIOS",
"Name": "BIOS Configuration Current Settings",
"AttributeRegistry": "BiosAttributeRegistryP89.v1_0_0",
"Attributes": {
"AdminPhone": "",
"BootMode": "Uefi",
"EmbeddedSata": "Raid",
"NicBoot1": "NetworkBoot",
"NicBoot2": "Disabled",
"PowerProfile": "MaxPerf",
"ProcCoreDisable": 0,
"ProcHyperthreading": "Enabled",
"ProcTurboMode": "Enabled",
"UsbControl": "UsbEnabled"
},
"@Redfish.Settings": {
"@odata.type": "#Settings.v1_0_0.Settings",
"ETag": "9234ac83b9700123cc32",
"Messages": [
{
"MessageId": "Base.1.0.SettingsFailed",
"RelatedProperties": [
"#/Attributes/ProcTurboMode"
]
}
],
"SettingsObject": {
"@odata.id": "/redfish/v1/Systems/437XR1138R2/BIOS/Settings"
},
"Time": "2016-03-07T14:44.30-05:00"
},
"Actions": {
"#Bios.ResetBios": {
"target": "/redfish/v1/Systems/437XR1138R2/BIOS/Actions/Bios.ResetBios"
},
"#Bios.ChangePassword": {
"target": "/redfish/v1/Systems/437XR1138R2/BIOS/Actions/Bios.ChangePassword"
}
},
"@odata.etag": "123",
"@odata.context": "/redfish/v1/$metadata#Bios.Bios",
"@odata.id": "/redfish/v1/Systems/437XR1138R2/BIOS"
}

View File

@ -0,0 +1,21 @@
{
"@odata.type": "#Bios.v1_0_0.Bios",
"Id": "Settings",
"Name": "BIOS Configuration Pending Settings",
"AttributeRegistry": "BiosAttributeRegistryP89.v1_0_0",
"Attributes": {
"AdminPhone": "(404) 555-1212",
"BootMode": "Uefi",
"EmbeddedSata": "Ahci",
"NicBoot1": "NetworkBoot",
"NicBoot2": "NetworkBoot",
"PowerProfile": "MaxPerf",
"ProcCoreDisable": 0,
"ProcHyperthreading": "Enabled",
"ProcTurboMode": "Disabled",
"UsbControl": "UsbEnabled"
},
"@odata.context": "/redfish/v1/$metadata#Bios.Bios",
"@odata.id": "/redfish/v1/Systems/437XR1138R2/BIOS/Settings",
"@Redfish.Copyright": "Copyright 2014-2016 Distributed Management Task Force, Inc. (DMTF). For the full DMTF copyright policy, see http://www.dmtf.org/about/policies/copyright."
}

View File

@ -0,0 +1,22 @@
{
"@Redfish.Settings": {
"@odata.type": "#Settings.v1_0_0.Settings",
"ETag": "9234ac83b9700123cc32",
"Messages": [{
"MessageId": "Base.1.0.SettingsFailed",
"Message": "Settings update failed due to invalid value",
"Severity": "High",
"Resolution": "Fix the value and try again",
"MessageArgs": [
"arg1"
],
"RelatedProperties": [
"#/Attributes/ProcTurboMode"
]
}],
"SettingsObject": {
"@odata.id": "/redfish/v1/Systems/437XR1138R2/BIOS/Settings"
},
"Time": "2016-03-07T14:44.30-05:00"
}
}

View File

@ -0,0 +1,154 @@
# 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
import mock
from sushy import exceptions
from sushy.resources.system import bios
from sushy.tests.unit import base
class BiosTestCase(base.TestCase):
def setUp(self):
super(BiosTestCase, self).setUp()
self.conn = mock.Mock()
with open('sushy/tests/unit/json_samples/bios.json', 'r') as f:
bios_json = json.loads(f.read())
with open('sushy/tests/unit/json_samples/bios_settings.json',
'r') as f:
bios_settings_json = json.loads(f.read())
self.conn.get.return_value.json.side_effect = [
bios_json,
bios_settings_json,
bios_settings_json]
self.sys_bios = bios.Bios(
self.conn, '/redfish/v1/Systems/437XR1138R2/BIOS',
redfish_version='1.0.2')
def test__parse_attributes(self):
self.sys_bios._parse_attributes()
self.assertEqual('1.0.2', self.sys_bios.redfish_version)
self.assertEqual('BIOS', self.sys_bios.identity)
self.assertEqual('BIOS Configuration Current Settings',
self.sys_bios.name)
self.assertIsNone(self.sys_bios.description)
self.assertEqual('123', self.sys_bios._etag)
self.assertEqual('BiosAttributeRegistryP89.v1_0_0',
self.sys_bios.attribute_registry)
self.assertEqual('', self.sys_bios.attributes['AdminPhone'])
self.assertEqual('Uefi', self.sys_bios.attributes['BootMode'])
self.assertEqual(0, self.sys_bios.attributes['ProcCoreDisable'])
# testing here if settings subfield parsed by checking ETag,
# other settings fields tested in specific settings test
self.assertEqual('9234ac83b9700123cc32',
self.sys_bios._settings._etag)
self.assertEqual('(404) 555-1212',
self.sys_bios.pending_attributes['AdminPhone'])
def test_set_attribute(self):
self.sys_bios.set_attribute('ProcTurboMode', 'Disabled')
self.sys_bios._conn.patch.assert_called_once_with(
'/redfish/v1/Systems/437XR1138R2/BIOS/Settings',
data={'Attributes': {'ProcTurboMode': 'Disabled'}},
headers={'If-Match': '123'})
def test_set_attribute_on_refresh(self):
# make it to instantiate pending attributes
self.sys_bios.pending_attributes
self.sys_bios.set_attribute('ProcTurboMode', 'Disabled')
self.assertTrue(self.sys_bios._pending_settings_resource._is_stale)
# make it to refresh pending attributes on next retrieval
self.sys_bios.pending_attributes
self.assertFalse(self.sys_bios._pending_settings_resource._is_stale)
def test_set_attributes(self):
self.sys_bios.set_attributes({'ProcTurboMode': 'Disabled',
'UsbControl': 'UsbDisabled'})
self.sys_bios._conn.patch.assert_called_once_with(
'/redfish/v1/Systems/437XR1138R2/BIOS/Settings',
data={'Attributes': {'ProcTurboMode': 'Disabled',
'UsbControl': 'UsbDisabled'}},
headers={'If-Match': '123'})
def test_set_attributes_on_refresh(self):
# make it to instantiate pending attributes
self.sys_bios.pending_attributes
self.sys_bios.set_attributes({'ProcTurboMode': 'Disabled',
'UsbControl': 'UsbDisabled'})
self.assertTrue(self.sys_bios._pending_settings_resource._is_stale)
# make it to refresh pending attributes on next retrieval
self.sys_bios.pending_attributes
self.assertFalse(self.sys_bios._pending_settings_resource._is_stale)
def test__get_reset_bios_action_element(self):
value = self.sys_bios._get_reset_bios_action_element()
self.assertEqual('/redfish/v1/Systems/437XR1138R2/BIOS/Actions/'
'Bios.ResetBios',
value.target_uri)
def test_reset_bios_missing_action(self):
self.sys_bios._actions.reset_bios = None
self.assertRaisesRegex(
exceptions.MissingActionError, '#Bios.ResetBios',
self.sys_bios.reset_bios)
def test__parse_attributes_missing_reset_bios_target(self):
self.sys_bios.json['Actions']['#Bios.ResetBios'].pop(
'target')
self.assertRaisesRegex(
exceptions.MissingAttributeError,
'attribute Actions/#Bios.ResetBios/target',
self.sys_bios._parse_attributes)
def test_reset_bios(self):
self.sys_bios.reset_bios()
self.sys_bios._conn.post.assert_called_once_with(
'/redfish/v1/Systems/437XR1138R2/BIOS/Actions/Bios.ResetBios')
def test__get_change_password_element(self):
value = self.sys_bios._get_change_password_element()
self.assertEqual("/redfish/v1/Systems/437XR1138R2/BIOS/Actions/"
"Bios.ChangePassword",
value.target_uri)
def test_change_password_missing_action(self):
self.sys_bios._actions.change_password = None
self.assertRaisesRegex(
exceptions.MissingActionError, '#Bios.ChangePassword',
self.sys_bios.change_password, 'newpassword',
'oldpassword',
'adminpassword')
def test__parse_attributes_missing_change_password_target(self):
self.sys_bios.json['Actions']['#Bios.ChangePassword'].pop(
'target')
self.assertRaisesRegex(
exceptions.MissingAttributeError,
'attribute Actions/#Bios.ChangePassword/target',
self.sys_bios._parse_attributes)
def test_change_password(self):
self.sys_bios.change_password('newpassword',
'oldpassword',
'adminpassword')
self.sys_bios._conn.post.assert_called_once_with(
'/redfish/v1/Systems/437XR1138R2/BIOS/Actions/Bios.ChangePassword',
data={'OldPassword': 'oldpassword',
'NewPassword': 'newpassword',
'PasswordName': 'adminpassword'})

View File

@ -19,6 +19,7 @@ import mock
import sushy
from sushy import exceptions
from sushy.resources.system import bios
from sushy.resources.system import constants as sys_cons
from sushy.resources.system import ethernet_interface
from sushy.resources.system import mappings as sys_map
@ -66,6 +67,7 @@ class SystemTestCase(base.TestCase):
self.assertEqual("OK", self.sys_inst.memory_summary.health)
self.assertIsNone(self.sys_inst._processors)
self.assertIsNone(self.sys_inst._ethernet_interfaces)
self.assertIsNone(self.sys_inst._bios)
def test__parse_attributes_missing_actions(self):
self.sys_inst.json.pop('Actions')
@ -379,6 +381,18 @@ class SystemTestCase(base.TestCase):
self.assertIsInstance(self.sys_inst._ethernet_interfaces,
ethernet_interface.EthernetInterfaceCollection)
def test_bios(self):
self.conn.get.return_value.json.reset_mock()
bios_return_value = None
with open('sushy/tests/unit/json_samples/bios.json', 'r') as f:
bios_return_value = json.loads(f.read())
self.conn.get.return_value.json.side_effect = [bios_return_value]
self.assertIsNone(self.sys_inst._bios)
self.assertIsInstance(self.sys_inst.bios, bios.Bios)
self.assertEqual('BIOS Configuration Current Settings',
self.sys_inst.bios.name)
class SystemCollectionTestCase(base.TestCase):

View File

@ -0,0 +1,74 @@
# Copyright 2017 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.
import json
import mock
from sushy.resources import settings
from sushy.tests.unit import base
class SettingsFieldTestCase(base.TestCase):
def setUp(self):
super(SettingsFieldTestCase, self).setUp()
with open('sushy/tests/unit/json_samples/settings.json',
'r') as f:
self.json = json.loads(f.read())
self.settings = settings.SettingsField()
def test__load(self):
instance = self.settings._load(self.json, mock.Mock())
self.assertEqual('9234ac83b9700123cc32',
instance._etag)
self.assertEqual('2016-03-07T14:44.30-05:00',
instance.time)
self.assertEqual('/redfish/v1/Systems/437XR1138R2/BIOS/Settings',
instance._settings_object_idref.resource_uri)
self.assertEqual('Base.1.0.SettingsFailed',
instance.messages[0].message_id)
self.assertEqual('Settings update failed due to invalid value',
instance.messages[0].message)
self.assertEqual('High',
instance.messages[0].severity)
self.assertEqual('Fix the value and try again',
instance.messages[0].resolution)
self.assertEqual('arg1',
instance.messages[0].message_args[0])
self.assertEqual('#/Attributes/ProcTurboMode',
instance.messages[0]._related_properties[0])
self.assertEqual('/redfish/v1/Systems/437XR1138R2/BIOS/Settings',
instance._settings_object_idref.resource_uri)
def test_commit(self):
conn = mock.Mock()
instance = self.settings._load(self.json, conn)
instance.commit(conn, {'Attributes': {'key': 'value'}})
conn.patch.assert_called_once_with(
'/redfish/v1/Systems/437XR1138R2/BIOS/Settings',
data={'Attributes': {'key': 'value'}}, headers=None)
def test_commit_with_etag(self):
conn = mock.Mock()
instance = self.settings._load(self.json, conn)
instance.commit(conn,
{'Attributes': {'key': 'value'}},
'123')
conn.patch.assert_called_once_with(
'/redfish/v1/Systems/437XR1138R2/BIOS/Settings',
data={'Attributes': {'key': 'value'}},
headers={'If-Match': '123'})