Add support for BIOS update emulation
This change adds UpdateService resource and SimpleUpdate action to sushy-tools. The goal behind this addition is enabling support for automated testing of firmware update functionality in virtualized environments. Note: no actual firmware update is performed with these API calls, the only result is incrementing the BiosVersion value stored in instance metadata. Change-Id: I22d9b7614a5a7243e08d40392a0a49a31ee00034
This commit is contained in:
parent
c4fb8531f9
commit
57e560f76a
@ -0,0 +1,8 @@
|
||||
---
|
||||
features:
|
||||
- |
|
||||
Adds support for BIOS update emulation by introducing UpdateService.
|
||||
No actual updates are performed (all the code is doing is incrementing
|
||||
BIOS version value in libvirt xml) however this functionality may be
|
||||
used for automated testing of firmware update features in Ironic. Note
|
||||
BMC firmware update emulation is not supported at this time.
|
88
sushy_tools/emulator/controllers/update_service.py
Normal file
88
sushy_tools/emulator/controllers/update_service.py
Normal file
@ -0,0 +1,88 @@
|
||||
# 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 flask
|
||||
|
||||
from sushy_tools.emulator import api_utils
|
||||
from sushy_tools import error
|
||||
|
||||
|
||||
update_service = flask.Blueprint(
|
||||
'UpdateService', __name__,
|
||||
url_prefix='/redfish/v1/UpdateService/')
|
||||
|
||||
|
||||
@update_service.route('', methods=['GET'])
|
||||
@api_utils.returns_json
|
||||
def update_service_resource():
|
||||
api_utils.debug('Serving update service resources')
|
||||
|
||||
return flask.render_template(
|
||||
'update_service.json'
|
||||
)
|
||||
|
||||
|
||||
@update_service.route('/Actions/UpdateService.SimpleUpdate',
|
||||
methods=['POST'])
|
||||
@api_utils.returns_json
|
||||
def update_service_simple_update():
|
||||
image_uri = flask.request.json.get('ImageURI')
|
||||
targets = flask.request.json.get('Targets')
|
||||
api_utils.debug('Received Targets: "%s"', targets)
|
||||
if not image_uri or not targets:
|
||||
message = "Missing ImageURI and/or Targets."
|
||||
return flask.render_template('error.json', message=message), 400
|
||||
|
||||
for target in targets:
|
||||
# NOTE(janders) since we only support BIOS let's ignore Manager targets
|
||||
if "Manager" in target:
|
||||
message = "Manager is not currently a supported Target."
|
||||
return flask.render_template('error.json', message=message), 400
|
||||
|
||||
identity = target.rsplit('/', 1)[-1]
|
||||
# NOTE(janders) iterate over the array? narrow down which one is needed
|
||||
# first? I suppose the former since we may want to update multiple
|
||||
api_utils.debug('Fetching BIOS information for System "%s"',
|
||||
target)
|
||||
try:
|
||||
versions = flask.current_app.systems.get_versions(identity)
|
||||
|
||||
except error.NotSupportedError as ex:
|
||||
api_utils.warning(
|
||||
'System failed to fetch BIOS information with exception %s',
|
||||
ex)
|
||||
message = "Failed fetching BIOS information"
|
||||
return flask.render_template('error.json', message=message), 500
|
||||
|
||||
bios_version = versions.get('BiosVersion')
|
||||
|
||||
api_utils.debug('Current BIOS version for System "%s" is "%s" ,'
|
||||
'attempting upgrade.',
|
||||
target, bios_version)
|
||||
|
||||
bios_version = bios_version.split('.')
|
||||
bios_version[1] = str(int(bios_version[1]) + 1)
|
||||
bios_version = '.'.join(bios_version)
|
||||
firmware_versions = {"BiosVersion": bios_version}
|
||||
|
||||
try:
|
||||
flask.current_app.systems.set_versions(identity, firmware_versions)
|
||||
except error.NotSupportedError as ex:
|
||||
api_utils.warning('System failed to update bios with exception %s',
|
||||
ex)
|
||||
message = "Failed updating BIOS version"
|
||||
return flask.render_template('error.json', message=message), 500
|
||||
|
||||
api_utils.info(
|
||||
'Emulated BIOS upgrade has been successful for '
|
||||
'System %s, new version is "%s".', target, bios_version)
|
||||
return '', 204, {'Location': '/redfish/v1/TaskService/Tasks/42'}
|
@ -26,6 +26,7 @@ from werkzeug import exceptions as wz_exc
|
||||
|
||||
from sushy_tools.emulator import api_utils
|
||||
from sushy_tools.emulator.controllers import certificate_service as certctl
|
||||
from sushy_tools.emulator.controllers import update_service as usctl
|
||||
from sushy_tools.emulator.controllers import virtual_media as vmctl
|
||||
from sushy_tools.emulator import memoize
|
||||
from sushy_tools.emulator.resources import chassis as chsdriver
|
||||
@ -195,6 +196,7 @@ class Application(flask.Flask):
|
||||
app = Application()
|
||||
app.register_blueprint(certctl.certificate_service)
|
||||
app.register_blueprint(vmctl.virtual_media)
|
||||
app.register_blueprint(usctl.update_service)
|
||||
|
||||
|
||||
@app.errorhandler(Exception)
|
||||
@ -922,6 +924,22 @@ def message_registry():
|
||||
return app.render_template('message_registry.json')
|
||||
|
||||
|
||||
@app.route('/redfish/v1/TaskService',
|
||||
methods=['GET'])
|
||||
@api_utils.ensure_instance_access
|
||||
@api_utils.returns_json
|
||||
def simple_task_service():
|
||||
return app.render_template('task_service.json')
|
||||
|
||||
|
||||
@app.route('/redfish/v1/TaskService/Tasks/42',
|
||||
methods=['GET'])
|
||||
@api_utils.ensure_instance_access
|
||||
@api_utils.returns_json
|
||||
def simple_task():
|
||||
return app.render_template('task.json')
|
||||
|
||||
|
||||
def parse_args():
|
||||
parser = argparse.ArgumentParser('sushy-emulator')
|
||||
parser.add_argument('--config',
|
||||
|
@ -24,6 +24,9 @@
|
||||
"CertificateService": {
|
||||
"@odata.id": "/redfish/v1/CertificateService"
|
||||
},
|
||||
"UpdateService": {
|
||||
"@odata.id": "/redfish/v1/UpdateService"
|
||||
},
|
||||
{% endif %}
|
||||
"@odata.id": "/redfish/v1/",
|
||||
"@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."
|
||||
|
11
sushy_tools/emulator/templates/task.json
Normal file
11
sushy_tools/emulator/templates/task.json
Normal file
@ -0,0 +1,11 @@
|
||||
{
|
||||
"@odata.type":"#Task.v1_4_3.Task",
|
||||
"Id":"42",
|
||||
"Name":"Task 42",
|
||||
"Description": "Task description",
|
||||
"TaskMonitor":"/taskmon/42",
|
||||
"TaskState":"Completed",
|
||||
"TaskStatus":"OK",
|
||||
"PercentComplete": 100,
|
||||
"@odata.id":"/redfish/v1/TaskService/Tasks/42"
|
||||
}
|
19
sushy_tools/emulator/templates/task_service.json
Normal file
19
sushy_tools/emulator/templates/task_service.json
Normal file
@ -0,0 +1,19 @@
|
||||
{
|
||||
"@odata.type": "#TaskService.v1_1_2.TaskService",
|
||||
"Id": "TaskService",
|
||||
"Name": "Tasks Service",
|
||||
"DateTime": "2015-03-13T04:14:33+06:00",
|
||||
"CompletedTaskOverWritePolicy": "Manual",
|
||||
"LifeCycleEventOnTaskStateChange": true,
|
||||
"Status": {
|
||||
"State": "Enabled",
|
||||
"Health": "OK"
|
||||
},
|
||||
"ServiceEnabled": true,
|
||||
"Tasks": {
|
||||
"@odata.id": "/redfish/v1/TaskService/Tasks"
|
||||
},
|
||||
"Oem": {},
|
||||
"@odata.context": "/redfish/v1/$metadata#TaskService.TaskService",
|
||||
"@odata.id": "/redfish/v1/TaskService"
|
||||
}
|
27
sushy_tools/emulator/templates/update_service.json
Normal file
27
sushy_tools/emulator/templates/update_service.json
Normal file
@ -0,0 +1,27 @@
|
||||
{
|
||||
"@odata.context": "/redfish/v1/$metadata#UpdateService.UpdateService",
|
||||
"@odata.id": "/redfish/v1/UpdateService",
|
||||
"@odata.type": "#UpdateService.v1_11_0.UpdateService",
|
||||
"Actions": {
|
||||
"#UpdateService.SimpleUpdate": {
|
||||
"TransferProtocol@Redfish.AllowableValues": [
|
||||
"HTTP",
|
||||
"HTTPS"
|
||||
],
|
||||
"target": "/redfish/v1/UpdateService/Actions/UpdateService.SimpleUpdate"
|
||||
}
|
||||
},
|
||||
"Description": "Represents the properties for the Update Service",
|
||||
"FirmwareInventory": {
|
||||
"@odata.id": "/redfish/v1/UpdateService/FirmwareInventory"
|
||||
},
|
||||
"HttpPushUri": "/redfish/v1/UpdateService/FirmwareInventory",
|
||||
"Id": "UpdateService",
|
||||
"MaxImageSizeBytes": null,
|
||||
"Name": "Update Service",
|
||||
"ServiceEnabled": true,
|
||||
"SoftwareInventory": {
|
||||
"@odata.id": "/redfish/v1/UpdateService/SoftwareInventory"
|
||||
}
|
||||
}
|
||||
|
128
sushy_tools/tests/unit/emulator/resources/test_update_service.py
Normal file
128
sushy_tools/tests/unit/emulator/resources/test_update_service.py
Normal file
@ -0,0 +1,128 @@
|
||||
# Copyright 2019 Red Hat, Inc.
|
||||
# All Rights Reserved.
|
||||
#
|
||||
# Licensed under the Apache License, Version 2.0 (the "License"); you may
|
||||
# not use this file except in compliance with the License. You may obtain
|
||||
# a copy of the License at
|
||||
#
|
||||
# http://www.apache.org/licenses/LICENSE-2.0
|
||||
#
|
||||
# Unless required by applicable law or agreed to in writing, software
|
||||
# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
|
||||
# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
|
||||
# License for the specific language governing permissions and limitations
|
||||
# under the License.
|
||||
|
||||
from sushy_tools.tests.unit.emulator import test_main
|
||||
|
||||
|
||||
class UpdateServiceTestCase(test_main.EmulatorTestCase):
|
||||
|
||||
def test_update_service_get(self):
|
||||
actions = {
|
||||
"#UpdateService.SimpleUpdate": {
|
||||
"TransferProtocol@Redfish.AllowableValues": [
|
||||
"HTTP",
|
||||
"HTTPS"
|
||||
],
|
||||
"target": "/redfish/v1/UpdateService/Actions/"
|
||||
"UpdateService.SimpleUpdate"
|
||||
}
|
||||
}
|
||||
|
||||
firmwareinventory = {
|
||||
"@odata.id": "/redfish/v1/UpdateService/FirmwareInventory"
|
||||
}
|
||||
|
||||
softwareinventory = {
|
||||
"@odata.id": "/redfish/v1/UpdateService/SoftwareInventory"
|
||||
}
|
||||
|
||||
resp = self.app.get('/redfish/v1/UpdateService')
|
||||
self.assertEqual(200, resp.status_code)
|
||||
|
||||
self.assertEqual('/redfish/v1/$metadata#UpdateService.UpdateService',
|
||||
resp.json['@odata.context'])
|
||||
self.assertEqual('/redfish/v1/UpdateService', resp.json['@odata.id'])
|
||||
self.assertEqual('#UpdateService.v1_11_0.UpdateService',
|
||||
resp.json['@odata.type'])
|
||||
self.assertEqual(actions, resp.json['Actions'])
|
||||
self.assertEqual('Represents the properties for the Update Service',
|
||||
resp.json['Description'])
|
||||
self.assertEqual(firmwareinventory, resp.json['FirmwareInventory'])
|
||||
self.assertEqual('/redfish/v1/UpdateService/FirmwareInventory',
|
||||
resp.json['HttpPushUri'])
|
||||
self.assertEqual('UpdateService', resp.json['Id'])
|
||||
self.assertIsNone(resp.json['MaxImageSizeBytes'])
|
||||
self.assertEqual('Update Service', resp.json['Name'])
|
||||
self.assertTrue(resp.json['ServiceEnabled'])
|
||||
self.assertEqual(softwareinventory, resp.json['SoftwareInventory'])
|
||||
|
||||
@test_main.patch_resource('indicators')
|
||||
@test_main.patch_resource('chassis')
|
||||
@test_main.patch_resource('managers')
|
||||
@test_main.patch_resource('systems')
|
||||
def test_update_service_simpleupdate(self, systems_mock, managers_mock,
|
||||
chassis_mock, indicators_mock):
|
||||
systems_mock = systems_mock.return_value
|
||||
systems_mock.uuid.return_value = 'zzzz-yyyy-xxxx'
|
||||
systems_mock.get_power_state.return_value = 'On'
|
||||
systems_mock.get_total_memory.return_value = 1
|
||||
systems_mock.get_total_cpus.return_value = 2
|
||||
systems_mock.get_boot_device.return_value = 'Cd'
|
||||
systems_mock.get_boot_mode.return_value = 'Legacy'
|
||||
systems_mock.get_versions().get.return_value = '1.0.0'
|
||||
get_versions = systems_mock.get_versions
|
||||
managers_mock.return_value.get_managers_for_system.return_value = [
|
||||
'aaaa-bbbb-cccc']
|
||||
chassis_mock.return_value.chassis = ['chassis0']
|
||||
indicators_mock.return_value.get_indicator_state.return_value = 'Off'
|
||||
|
||||
resp = self.app.get('/redfish/v1/Systems/zzzz-yyyy-xxxx')
|
||||
self.assertEqual(200, resp.status_code)
|
||||
self.assertEqual('zzzz-yyyy-xxxx', resp.json['Id'])
|
||||
self.assertEqual('1.0.0', resp.json['BiosVersion'])
|
||||
get_versions.assert_called()
|
||||
|
||||
args = {
|
||||
'ImageURI': 'http://10.6.48.48:8080/ilo5278.bin',
|
||||
'TransferProtocol': 'HTTP',
|
||||
'Targets': ['/redfish/v1/Systems/'
|
||||
'zzzz-yyyy-xxxx']
|
||||
}
|
||||
set_versions = systems_mock.set_versions
|
||||
response = self.app.post('/redfish/v1/UpdateService/Actions/'
|
||||
'UpdateService.SimpleUpdate', json=args)
|
||||
self.assertEqual(204, response.status_code)
|
||||
set_versions.assert_called_once_with('zzzz-yyyy-xxxx',
|
||||
{'BiosVersion': '1.1.0'})
|
||||
|
||||
def test_update_service_invalid_params(self):
|
||||
args = {
|
||||
'NonexistentURI': 'asdf://nowhere.com',
|
||||
}
|
||||
|
||||
response = self.app.post('/redfish/v1/UpdateService/Actions/'
|
||||
'UpdateService.SimpleUpdate', json=args)
|
||||
self.assertEqual('Base.1.0.GeneralError',
|
||||
response.json['error']['code'])
|
||||
self.assertEqual('Missing ImageURI and/or Targets.',
|
||||
response.json['error']['message'])
|
||||
|
||||
@test_main.patch_resource('systems')
|
||||
def test_update_service_simpleupdate_manager(self, systems_mock):
|
||||
systems_mock = systems_mock.return_value
|
||||
systems_mock.uuid.return_value = 'zzzz-yyyy-xxxx'
|
||||
args = {
|
||||
'ImageURI': 'http://10.6.48.48:8080/ilo5278.bin',
|
||||
'TransferProtocol': 'HTTP',
|
||||
'Targets': ['/redfish/v1/Managers/'
|
||||
'zzzz-yyyy-xxxx']
|
||||
}
|
||||
set_versions = systems_mock.set_versions
|
||||
response = self.app.post('/redfish/v1/UpdateService/Actions/'
|
||||
'UpdateService.SimpleUpdate', json=args)
|
||||
self.assertEqual(400, response.status_code)
|
||||
self.assertEqual('Manager is not currently a supported Target.',
|
||||
response.json['error']['message'])
|
||||
set_versions.assert_not_called()
|
@ -975,3 +975,22 @@ class RegistryTestCase(EmulatorTestCase):
|
||||
"NumberOfArgs": 0,
|
||||
"Resolution": "No response action is required."},
|
||||
messages['BIOS001'])
|
||||
|
||||
|
||||
class TaskServiceTestCase(EmulatorTestCase):
|
||||
|
||||
def test_task_service(self):
|
||||
response = self.app.get('/redfish/v1/TaskService')
|
||||
self.assertEqual(200, response.status_code)
|
||||
self.assertEqual('/redfish/v1/TaskService',
|
||||
response.json['@odata.id'])
|
||||
self.assertEqual('Tasks Service', response.json['Name'])
|
||||
self.assertEqual(True, response.json['ServiceEnabled'])
|
||||
|
||||
def test_task_service_task(self):
|
||||
response = self.app.get('/redfish/v1/TaskService/Tasks/42')
|
||||
self.assertEqual(200, response.status_code)
|
||||
self.assertEqual('/redfish/v1/TaskService/Tasks/42',
|
||||
response.json['@odata.id'])
|
||||
self.assertEqual('Task 42', response.json['Name'])
|
||||
self.assertEqual('Completed', response.json['TaskState'])
|
||||
|
Loading…
Reference in New Issue
Block a user