Merge "Add support for BIOS update emulation"

This commit is contained in:
Zuul 2024-07-12 13:37:58 +00:00 committed by Gerrit Code Review
commit e402462e30
9 changed files with 321 additions and 0 deletions

View File

@ -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.

View 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'}

View File

@ -26,6 +26,7 @@ from werkzeug import exceptions as wz_exc
from sushy_tools.emulator import api_utils from sushy_tools.emulator import api_utils
from sushy_tools.emulator.controllers import certificate_service as certctl 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.controllers import virtual_media as vmctl
from sushy_tools.emulator import memoize from sushy_tools.emulator import memoize
from sushy_tools.emulator.resources import chassis as chsdriver from sushy_tools.emulator.resources import chassis as chsdriver
@ -195,6 +196,7 @@ class Application(flask.Flask):
app = Application() app = Application()
app.register_blueprint(certctl.certificate_service) app.register_blueprint(certctl.certificate_service)
app.register_blueprint(vmctl.virtual_media) app.register_blueprint(vmctl.virtual_media)
app.register_blueprint(usctl.update_service)
@app.errorhandler(Exception) @app.errorhandler(Exception)
@ -927,6 +929,22 @@ def message_registry():
return app.render_template('message_registry.json') 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(): def parse_args():
parser = argparse.ArgumentParser('sushy-emulator') parser = argparse.ArgumentParser('sushy-emulator')
parser.add_argument('--config', parser.add_argument('--config',

View File

@ -24,6 +24,9 @@
"CertificateService": { "CertificateService": {
"@odata.id": "/redfish/v1/CertificateService" "@odata.id": "/redfish/v1/CertificateService"
}, },
"UpdateService": {
"@odata.id": "/redfish/v1/UpdateService"
},
{% endif %} {% endif %}
"@odata.id": "/redfish/v1/", "@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." "@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,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"
}

View 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"
}

View 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"
}
}

View 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()

View File

@ -975,3 +975,22 @@ class RegistryTestCase(EmulatorTestCase):
"NumberOfArgs": 0, "NumberOfArgs": 0,
"Resolution": "No response action is required."}, "Resolution": "No response action is required."},
messages['BIOS001']) 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'])