diff --git a/releasenotes/notes/add-updateservice-3332a9c15b5fb3ab.yaml b/releasenotes/notes/add-updateservice-3332a9c15b5fb3ab.yaml new file mode 100644 index 00000000..02423aef --- /dev/null +++ b/releasenotes/notes/add-updateservice-3332a9c15b5fb3ab.yaml @@ -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. diff --git a/sushy_tools/emulator/controllers/update_service.py b/sushy_tools/emulator/controllers/update_service.py new file mode 100644 index 00000000..0f5b1397 --- /dev/null +++ b/sushy_tools/emulator/controllers/update_service.py @@ -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'} diff --git a/sushy_tools/emulator/main.py b/sushy_tools/emulator/main.py index 0d21c8f6..03c202bb 100755 --- a/sushy_tools/emulator/main.py +++ b/sushy_tools/emulator/main.py @@ -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) @@ -927,6 +929,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', diff --git a/sushy_tools/emulator/templates/root.json b/sushy_tools/emulator/templates/root.json index e4b0e2ed..fe36a0b1 100644 --- a/sushy_tools/emulator/templates/root.json +++ b/sushy_tools/emulator/templates/root.json @@ -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." diff --git a/sushy_tools/emulator/templates/task.json b/sushy_tools/emulator/templates/task.json new file mode 100644 index 00000000..5934531d --- /dev/null +++ b/sushy_tools/emulator/templates/task.json @@ -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" + } diff --git a/sushy_tools/emulator/templates/task_service.json b/sushy_tools/emulator/templates/task_service.json new file mode 100644 index 00000000..18c00816 --- /dev/null +++ b/sushy_tools/emulator/templates/task_service.json @@ -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" +} diff --git a/sushy_tools/emulator/templates/update_service.json b/sushy_tools/emulator/templates/update_service.json new file mode 100644 index 00000000..5a8276af --- /dev/null +++ b/sushy_tools/emulator/templates/update_service.json @@ -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" + } +} + diff --git a/sushy_tools/tests/unit/emulator/resources/test_update_service.py b/sushy_tools/tests/unit/emulator/resources/test_update_service.py new file mode 100644 index 00000000..c803e0c4 --- /dev/null +++ b/sushy_tools/tests/unit/emulator/resources/test_update_service.py @@ -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() diff --git a/sushy_tools/tests/unit/emulator/test_main.py b/sushy_tools/tests/unit/emulator/test_main.py index 343d5d17..83d1876b 100644 --- a/sushy_tools/tests/unit/emulator/test_main.py +++ b/sushy_tools/tests/unit/emulator/test_main.py @@ -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'])