Merge "Add support for the `UpdateService` resource"

This commit is contained in:
Zuul 2019-02-18 11:13:57 +00:00 committed by Gerrit Code Review
commit 0b3241b0bd
15 changed files with 619 additions and 0 deletions

View File

@ -0,0 +1,5 @@
---
features:
- |
Adds support for the UpdateService resource to the library.
`UpdateService` is responsible for managing firmware updates.

View File

@ -24,6 +24,7 @@ from sushy.resources.registry import message_registry_file
from sushy.resources.sessionservice import session
from sushy.resources.sessionservice import sessionservice
from sushy.resources.system import system
from sushy.resources.updateservice import updateservice
LOG = logging.getLogger(__name__)
@ -79,6 +80,9 @@ class Sushy(base.ResourceBase):
_registries_path = base.Field(['Registries', '@odata.id'])
"""Registries path"""
_update_service_path = base.Field(['UpdateService', '@odata.id'])
"""UpdateService path"""
def __init__(self, base_url, username=None, password=None,
root_prefix='/redfish/v1/', verify=True,
auth=None, connector=None):
@ -226,6 +230,19 @@ class Sushy(base.ResourceBase):
return session.Session(self._conn, identity,
redfish_version=self.redfish_version)
def get_update_service(self):
"""Get the UpdateService object
:returns: The UpdateService object
"""
if not self._update_service_path:
raise exceptions.MissingAttributeError(
attribute='UpdateService/@odata.id', resource=self._path)
return updateservice.UpdateService(
self._conn, self._update_service_path,
redfish_version=self.redfish_version)
def _get_registry_collection(self):
"""Get MessageRegistryFileCollection object

View File

@ -0,0 +1,26 @@
# 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.
# Values come from the Redfish UpdateService json-schema.
# https://redfish.dmtf.org/schemas/UpdateService.v1_2_2.json
# Transfer Protocol Type constants
TRANSFER_PROTOCOL_TYPE_CIFS = 'CIFS'
TRANSFER_PROTOCOL_TYPE_FTP = 'FTP'
TRANSFER_PROTOCOL_TYPE_SFTP = 'SFTP'
TRANSFER_PROTOCOL_TYPE_HTTP = 'HTTP'
TRANSFER_PROTOCOL_TYPE_HTTPS = 'HTTPS'
TRANSFER_PROTOCOL_TYPE_SCP = 'SCP'
TRANSFER_PROTOCOL_TYPE_TFTP = 'TFTP'
TRANSFER_PROTOCOL_TYPE_OEM = 'OEM'
TRANSFER_PROTOCOL_TYPE_NFS = 'NFS'

View File

@ -0,0 +1,35 @@
# 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 sushy.resources.updateservice import constants as ups_cons
from sushy import utils
TRANSFER_PROTOCOL_TYPE_VALUE_MAP = {
'Common Internet File System Protocol':
ups_cons.TRANSFER_PROTOCOL_TYPE_CIFS,
'File Transfer Protocol': ups_cons.TRANSFER_PROTOCOL_TYPE_FTP,
'Secure File Transfer Protocol': ups_cons.TRANSFER_PROTOCOL_TYPE_SFTP,
'Hypertext Transfer Protocol': ups_cons.TRANSFER_PROTOCOL_TYPE_HTTP,
'HTTP Secure Protocol': ups_cons.TRANSFER_PROTOCOL_TYPE_HTTPS,
'Secure File Copy Protocol': ups_cons.TRANSFER_PROTOCOL_TYPE_SCP,
'Trivial File Transfer Protocol': ups_cons.TRANSFER_PROTOCOL_TYPE_TFTP,
'A protocol defined by the manufacturer':
ups_cons.TRANSFER_PROTOCOL_TYPE_OEM,
'Network File System Protocol': ups_cons.TRANSFER_PROTOCOL_TYPE_NFS
}
TRANSFER_PROTOCOL_TYPE_VALUE_MAP_REV = (
utils.revert_dictionary(TRANSFER_PROTOCOL_TYPE_VALUE_MAP))
TRANSFER_PROTOCOL_TYPE_VALUE_MAP[
'Network File System Protocol'] = ups_cons.TRANSFER_PROTOCOL_TYPE_NFS

View File

@ -0,0 +1,96 @@
# 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/SoftwareInventory.v1_2_0.json
import logging
from sushy.resources import base
from sushy.resources import common
LOG = logging.getLogger(__name__)
class SoftwareInventory(base.ResourceBase):
identity = base.Field('Id', required=True)
"""The software inventory identity"""
lowest_supported_version = base.Field('LowestSupportedVersion')
"""The lowest supported version of the software"""
manufacturer = base.Field('Manufacturer')
"""The manufacturer of the software"""
name = base.Field('Name', required=True)
"""The software inventory name"""
release_date = base.Field('ReleaseDate')
"""Release date of the software"""
related_item = base.Field('RelatedItem')
"""The ID(s) of the resources associated with the software inventory
item"""
status = common.StatusField('Status')
"""The status of the software inventory"""
software_id = base.Field('SoftwareId')
"""The identity of the software"""
uefi_device_paths = base.Field('UefiDevicePaths')
"""Represents the UEFI Device Path(s)"""
updateable = base.Field('Updateable')
"""Indicates whether this software can be updated by the update
service"""
version = base.Field('Version')
"""The version of the software"""
def __init__(self, connector, identity, redfish_version=None):
"""A class representing a SoftwareInventory
:param connector: A Connector instance
:param identity: The identity of the SoftwareInventory resources
:param redfish_version: The version of RedFish. Used to construct
the object according to schema of given version.
"""
super(SoftwareInventory, self).__init__(
connector,
identity,
redfish_version)
class SoftwareInventoryCollection(base.ResourceCollectionBase):
name = base.Field('Name')
"""The software inventory collection name"""
description = base.Field('Description')
"""The software inventory collection description"""
@property
def _resource_type(self):
return SoftwareInventory
def __init__(self, connector, identity, redfish_version=None):
"""A class representing a SoftwareInventoryCollection
:param connector: A Connector instance
:param identity: The identity of SoftwareInventory resource
:param redfish_version: The version of RedFish. Used to construct
the object according to schema of given version.
"""
super(SoftwareInventoryCollection, self).__init__(
connector, identity, redfish_version)

View File

@ -0,0 +1,155 @@
# 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/UpdateService.v1_2_2.json
import logging
from sushy import exceptions
from sushy.resources import base
from sushy.resources import common
from sushy.resources.updateservice import mappings as up_maps
from sushy.resources.updateservice import softwareinventory
from sushy import utils
LOG = logging.getLogger(__name__)
class SimpleUpdateActionField(common.ActionField):
image_uri = base.Field('ImageURI')
"""The URI of the software image to be installed"""
targets = base.Field('Targets')
"""The array of URIs indicating where the update image is to be""" + \
"""applied"""
transfer_protocol = base.MappedField(
'TransferProtocol',
up_maps.TRANSFER_PROTOCOL_TYPE_VALUE_MAP)
"""The network protocol used by the Update Service"""
class ActionsField(base.CompositeField):
simple_update = SimpleUpdateActionField(
'#UpdateService.SimpleUpdate')
class UpdateService(base.ResourceBase):
identity = base.Field('Id', required=True)
"""The update service identity"""
http_push_uri = base.Field('HttpPushUri')
"""The URI used to perform an HTTP or HTTPS push update to the Update
Service"""
http_push_uri_targets = base.Field('HttpPushUriTargets')
"""The array of URIs indicating the target for applying the""" + \
"""update image"""
http_push_uri_targets_busy = base.Field('HttpPushUriTargetsBusy')
"""This represents if the HttpPushUriTargets property is reserved""" + \
"""by anyclient"""
name = base.Field('Name', required=True)
"""The update service name"""
service_enabled = base.Field('ServiceEnabled')
"""The status of whether this service is enabled"""
status = common.StatusField('Status')
"""The status of the update service"""
_actions = ActionsField('Actions', required=True)
def __init__(self, connector, identity, redfish_version=None):
"""A class representing a UpdateService
:param connector: A Connector instance
:param identity: The identity of the UpdateService resource
:param redfish_version: The version of RedFish. Used to construct
the object according to schema of given version
"""
super(UpdateService, self).__init__(
connector,
identity,
redfish_version)
def _get_simple_update_element(self):
simple_update_action = self._actions.simple_update
if not simple_update_action:
raise exceptions.MissingAttributeError(
action='#UpdateService.SimpleUpdate',
resource=self._path)
return simple_update_action
def get_allowed_transfer_protocol_values(self):
"""Get the allowed values for transfer protocol.
:returns: A set of allowed values.
:raises: MissingAttributeError, if Actions/#UpdateService.SimpleUpdate
attribute not present.
"""
simple_update_action = self._get_simple_update_element()
if not simple_update_action.transfer_protocol:
LOG.warning(
'Could not figure out the allowed values for the simple '
'update action for UpdateService %s', self.identity)
return set(up_maps.TRANSFER_PROTOCOL_TYPE_VALUE_MAP_REV)
return set(up_maps.TRANSFER_PROTOCOL_TYPE_VALUE_MAP[v] for v in
simple_update_action.transfer_protocol if v in
up_maps.TRANSFER_PROTOCOL_TYPE_VALUE_MAP)
def simple_update(self, image_uri, targets, transfer_protocol):
"""Simple Update is used to update software components"""
transfer_protocol = transfer_protocol
valid_transfer_protocols = self.get_allowed_transfer_protocol_values()
if transfer_protocol not in valid_transfer_protocols:
raise exceptions.InvalidParameterValueError(
parameter='transfer_protocol', value=transfer_protocol,
valid_values=valid_transfer_protocols)
self._conn.post(data={
'ImageURI': image_uri,
'Targets': targets,
'TransferProtocol': transfer_protocol})
def _get_software_inventory_collection_path(self):
"""Helper function to find the SoftwareInventoryCollections path"""
soft_inv_col = self.json.get('SoftwareInventory')
if not soft_inv_col:
raise exceptions.MissingAttributeError(
attribute='SoftwareInventory', resource=self._path)
return soft_inv_col.get('@odata.id')
@property
@utils.cache_it
def software_inventory(self):
"""Property to reference SoftwareInventoryCollection instance"""
return softwareinventory.SoftwareInventoryCollection(
self._conn, self._get_software_inventory_collection_path,
redfish_version=self.redfish_version)
@property
@utils.cache_it
def firmware_inventory(self):
"""Property to reference SoftwareInventoryCollection instance"""
return softwareinventory.SoftwareInventoryCollection(
self._conn, self._get_software_inventory_collection_path,
redfish_version=self.redfish_version)

View File

@ -27,6 +27,9 @@
"SessionService": {
"@odata.id": "/redfish/v1/SessionService"
},
"UpdateService": {
"@odata.id": "/redfish/v1/UpdateService"
},
"AccountService": {
"@odata.id": "/redfish/v1/AccountService"
},

View File

@ -0,0 +1,29 @@
{
"@odata.type": "#SoftwareInventory.v1_2_0.SoftwareInventory",
"Id": "BMC",
"Name": "Contoso BMC Firmware",
"Status": {
"State": "Enabled",
"Health": "OK"
},
"Updateable": true,
"Manufacturer": "Contoso",
"ReleaseDate": "2017-08-22T12:00:00",
"Version": "1.45.455b66-rev4",
"SoftwareId": "1624A9DF-5E13-47FC-874A-DF3AFF143089",
"LowestSupportedVersion": "1.30.367a12-rev1",
"UefiDevicePaths": [
"BMC(0x1,0x0ABCDEF)"
],
"RelatedItem": [
{
"@odata.id": "/redfish/v1/Managers/1"
}
],
"Actions": {
"Oem": {}
},
"Oem": {},
"@odata.context": "/redfish/v1/$metadata#SoftwareInventory.SoftwareInventory",
"@odata.id": "/redfish/v1/UpdateService/FirmwareInventory/BMC"
}

View File

@ -0,0 +1,14 @@
{
"@odata.type": "#SoftwareInventoryCollection.v1_4_0.SoftwareInventoryCollection",
"@odata.id": "/redfish/v1/UpdateService/SoftwareInventory",
"Name": "Software Inventory Collection",
"Members@odata.count": 2,
"Members": [
{
"@odata.id": "/redfish/v1/UpdateService/FirmwareInventory"
},
{
"@odata.id": "/redfish/v1/UpdateService/SoftwareInventory"
}
]
}

View File

@ -0,0 +1,30 @@
{
"@odata.type": "#UpdateService.v1_2_1.UpdateService",
"Id": "UpdateService",
"Name": "Update service",
"Status": {
"State": "Enabled",
"Health": "OK",
"HealthRollup": "OK"
},
"ServiceEnabled": true,
"HttpPushUri": "/FWUpdate",
"HttpPushUriTargets": ["/FWUpdate"],
"HttpPushUriTargetsBusy": false,
"FirmwareInventory": {
"@odata.id": "/redfish/v1/UpdateService/FirmwareInventory"
},
"SoftwareInventory": {
"@odata.id": "/redfish/v1/UpdateService/SoftwareInventory"
},
"Actions": {
"#UpdateService.SimpleUpdate": {
"target": "/redfish/v1/UpdateService/Actions/SimpleUpdate",
"@Redfish.ActionInfo": "/redfish/v1/UpdateService/SimpleUpdateActionInfo"
},
"Oem": {}
},
"Oem": {},
"@odata.context": "/redfish/v1/$metadata#UpdateService.UpdateService",
"@odata.id": "/redfish/v1/UpdateService"
}

View File

@ -0,0 +1,90 @@
# 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 import constants as res_cons
from sushy.resources.updateservice import softwareinventory
from sushy.tests.unit import base
class SoftwareInventoryTestCase(base.TestCase):
def setUp(self):
super(SoftwareInventoryTestCase, self).setUp()
conn = mock.Mock()
with open(
'sushy/tests/unit/json_samples/softwareinventory.json') as f:
conn.get.return_value.json.return_value = json.load(f)
self.soft_inv = softwareinventory.SoftwareInventory(
conn,
'/redfish/v1/UpdateService/SoftwareInventory/1',
redfish_version='1.3.0')
def test__parse_attributes(self):
self.soft_inv._parse_attributes()
self.assertEqual('BMC', self.soft_inv.identity)
self.assertEqual(
'1.30.367a12-rev1',
self.soft_inv.lowest_supported_version)
self.assertEqual('Contoso', self.soft_inv.manufacturer)
self.assertEqual('Contoso BMC Firmware', self.soft_inv.name)
self.assertEqual('2017-08-22T12:00:00', self.soft_inv.release_date)
self.assertEqual(
res_cons.STATE_ENABLED,
self.soft_inv.status.state)
self.assertEqual(res_cons.HEALTH_OK, self.soft_inv.status.health)
self.assertEqual(
'1624A9DF-5E13-47FC-874A-DF3AFF143089',
self.soft_inv.software_id)
self.assertTrue(self.soft_inv.updateable)
self.assertEqual('1.45.455b66-rev4', self.soft_inv.version)
def test__parse_attributes_missing_identity(self):
self.soft_inv.json.pop('Id')
self.assertRaisesRegex(
exceptions.MissingAttributeError, 'attribute Id',
self.soft_inv._parse_attributes)
class SoftwareInventoryCollectionTestCase(base.TestCase):
def setUp(self):
super(SoftwareInventoryCollectionTestCase, self).setUp()
conn = mock.Mock()
with open('sushy/tests/unit/json_samples/'
'softwareinventory_collection.json') as f:
conn.get.return_value.json.return_value = json.load(f)
self.soft_inv_col = softwareinventory.SoftwareInventoryCollection(
conn, '/redfish/v1/UpdateService/SoftwareInventory',
redfish_version='1.3.0')
def test__parse_attributes(self):
self.soft_inv_col._parse_attributes()
self.assertEqual('1.3.0', self.soft_inv_col.redfish_version)
self.assertEqual(
'Software Inventory Collection',
self.soft_inv_col.name)
@mock.patch.object(
softwareinventory, 'SoftwareInventory', autospec=True)
def test_get_member(self, mock_softwareinventory):
path = '/redfish/v1/UpdateService/SoftwareInventory/1'
self.soft_inv_col.get_member(path)
mock_softwareinventory.assert_called_once_with(
self.soft_inv_col._conn, path,
redfish_version=self.soft_inv_col.redfish_version)

View File

@ -0,0 +1,106 @@
# 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 import constants as res_cons
from sushy.resources.updateservice import constants as ups_cons
from sushy.resources.updateservice import softwareinventory
from sushy.resources.updateservice import updateservice
from sushy.tests.unit import base
class UpdateServiceTestCase(base.TestCase):
def setUp(self):
super(UpdateServiceTestCase, self).setUp()
self.conn = mock.Mock()
with open('sushy/tests/unit/json_samples/updateservice.json') as f:
self.conn.get.return_value.json.return_value = json.load(f)
self.upd_serv = updateservice.UpdateService(
self.conn, '/redfish/v1/UpdateService/UpdateService',
redfish_version='1.3.0')
def test__parse_attributes(self):
self.upd_serv._parse_attributes()
self.assertEqual('UpdateService', self.upd_serv.identity)
self.assertEqual('/FWUpdate', self.upd_serv.http_push_uri)
self.assertIn('/FWUpdate', self.upd_serv.http_push_uri_targets)
self.assertFalse(self.upd_serv.http_push_uri_targets_busy)
self.assertEqual('Update service', self.upd_serv.name)
self.assertTrue(self.upd_serv.service_enabled)
self.assertEqual(res_cons.STATE_ENABLED, self.upd_serv.status.state)
self.assertEqual(res_cons.HEALTH_OK, self.upd_serv.status.health)
self.assertEqual(
res_cons.HEALTH_OK,
self.upd_serv.status.health_rollup)
def test__parse_attributes_missing_actions(self):
self.upd_serv.json.pop('Actions')
self.assertRaisesRegex(
exceptions.MissingAttributeError, 'attribute Actions',
self.upd_serv._parse_attributes)
def test_simple_update(self):
self.upd_serv.simple_update(
image_uri='local.server/update.exe',
targets='/redfish/v1/UpdateService/Actions/SimpleUpdate',
transfer_protocol=ups_cons.TRANSFER_PROTOCOL_TYPE_HTTPS)
self.upd_serv._conn.post.assert_called_once_with(
data={
'ImageURI': 'local.server/update.exe',
'Targets': '/redfish/v1/UpdateService/Actions/SimpleUpdate',
'TransferProtocol': 'HTTPS'})
def test_software_inventory(self):
# | GIVEN |
self.conn.get.return_value.json.reset_mock()
with open('sushy/tests/unit/json_samples/'
'softwareinventory_collection.json') as f:
self.conn.get.return_value.json.return_value = json.load(f)
# | WHEN |
actual_software_inventory = self.upd_serv.software_inventory
# | THEN |
self.assertIsInstance(actual_software_inventory,
softwareinventory.SoftwareInventoryCollection)
self.conn.get.return_value.json.assert_called_once_with()
# reset mock
self.conn.get.return_value.json.reset_mock()
# | WHEN & THEN |
self.assertIs(actual_software_inventory,
self.upd_serv.software_inventory)
self.conn.get.return_value.json.assert_not_called()
def test_firmware_inventory(self):
# | GIVEN |
self.conn.get.return_value.json.reset_mock()
with open('sushy/tests/unit/json_samples/'
'softwareinventory_collection.json') as f:
self.conn.get.return_value.json.return_value = json.load(f)
# | WHEN |
actual_firmware_inventory = self.upd_serv.firmware_inventory
# | THEN |
self.assertIsInstance(actual_firmware_inventory,
softwareinventory.SoftwareInventoryCollection)
self.conn.get.return_value.json.assert_called_once_with()
# reset mock
self.conn.get.return_value.json.reset_mock()
# | WHEN & THEN |
self.assertIs(actual_firmware_inventory,
self.upd_serv.firmware_inventory)
self.conn.get.return_value.json.assert_not_called()

View File

@ -27,6 +27,7 @@ from sushy.resources.registry import message_registry_file
from sushy.resources.sessionservice import session
from sushy.resources.sessionservice import sessionservice
from sushy.resources.system import system
from sushy.resources.updateservice import updateservice
from sushy.tests.unit import base
@ -143,6 +144,13 @@ class MainTestCase(base.TestCase):
self.root._conn, 'asdf',
redfish_version=self.root.redfish_version)
@mock.patch.object(updateservice, 'UpdateService', autospec=True)
def test_get_update_service(self, mock_upd_serv):
self.root.get_update_service()
mock_upd_serv.assert_called_once_with(
self.root._conn, '/redfish/v1/UpdateService',
redfish_version=self.root.redfish_version)
@mock.patch.object(message_registry_file,
'MessageRegistryFileCollection',
autospec=True)
@ -185,5 +193,10 @@ class BareMinimumMainTestCase(base.TestCase):
exceptions.MissingAttributeError,
'SessionService/@odata.id', self.root.get_session_service)
def test_get_update_service_when_updateservice_attr_absent(self):
self.assertRaisesRegex(
exceptions.MissingAttributeError,
'UpdateService/@odata.id', self.root.get_update_service)
def test__get_registry_collection_when_registries_attr_absent(self):
self.assertIsNone(self.root._get_registry_collection())