diff --git a/doc/source/admin/emulator.conf b/doc/source/admin/emulator.conf index 75fdfeb2..e364193e 100644 --- a/doc/source/admin/emulator.conf +++ b/doc/source/admin/emulator.conf @@ -31,3 +31,20 @@ SUSHY_EMULATOR_BOOT_LOADER_MAP = { 'aarch64': None } } + +# This map contains statically configured Redfish Manager(s) linked +# up with the Systems each Manager pretends to manage. +# +# The first managerc in the list will pretend to manage all other +# resources. +# +# If this map is not present in the configuration, a single default +# Manager is configured automatically to manage all available Systems. +SUSHY_EMULATOR_MANAGERS = [ + { + "Id": "BMC", + "Name": "Manager", + "ServiceEntryPointUUID": "92384634-2938-2342-8820-489239905423", + "UUID": "58893887-8974-2487-2389-841168418919" + } +] diff --git a/doc/source/user/dynamic-emulator.rst b/doc/source/user/dynamic-emulator.rst index 8997a056..b9230c7b 100644 --- a/doc/source/user/dynamic-emulator.rst +++ b/doc/source/user/dynamic-emulator.rst @@ -2,14 +2,24 @@ Virtual Redfish BMC =================== -The virtual Redfish BMC is functionally similar to the +The Virtual Redfish BMC is functionally similar to the `Virtual BMC `_ tool except that the frontend protocol is Redfish rather than IPMI. The Redfish -commands coming from the client get executed against the virtualization -backend. That lets you control virtual machine instances over Redfish. +commands coming from the client are handled by one or more resource-specific +drivers. -The libvirt backend -------------------- +Systems resource +---------------- + +For *Systems* resource, emulator maintains two drivers relying on +a virtualization backend to emulate bare metal machines by means of +virtual machines. + +The following sections will explain how to configure and use +each of these drivers. + +Systems resource driver: libvirt +++++++++++++++++++++++++++++++++ First thing you need is to set up some libvirt-managed virtual machines (AKA domains) to manipulate. The following command will create a new @@ -76,7 +86,7 @@ You can have as many domains as you need. The domains can be concurrently managed over Redfish and some other tool like *Virtual BMC*. UEFI boot -+++++++++ +~~~~~~~~~ By default, `legacy` or `BIOS` mode is used to boot the instance. However, libvirt domain can be configured to boot via UEFI firmware. This process @@ -140,8 +150,8 @@ Now you can run `sushy-emulator` with the updated configuration file: The images you will serve to your VMs need to be UEFI-bootable. -The OpenStack backend ---------------------- +Systems resource driver: OpenStack +++++++++++++++++++++++++++++++++++ You can use an OpenStack cloud instances to simulate Redfish-managed baremetal machines. This setup is known under the name of @@ -203,3 +213,45 @@ And flip its power state via the Redfish call: You can have as many OpenStack instances as you need. The instances can be concurrently managed over Redfish and functionally similar tools. + +Managers resource +----------------- + +For emulating *Managers* resource, the user can statically configure +one or more imaginary Managers. The first configured manager will +pretend to manage all *Systems* and potentially other resources. + +.. code-block:: python + + SUSHY_EMULATOR_MANAGERS = [ + { + "Id": "BMC", + "Name": "Manager", + "ServiceEntryPointUUID": "92384634-2938-2342-8820-489239905423", + "UUID": "58893887-8974-2487-2389-841168418919" + } + ] + +By default a single manager with be configured automatically. + +Managers will be revealed when querying the *Managers* resource +directly, as well as other resources they manage or have some +other relations. + +.. code-block:: bash + + curl http://localhost:8000/redfish/v1/Managers + { + "@odata.type": "#ManagerCollection.ManagerCollection", + "Name": "Manager Collection", + "Members@odata.count": 1, + "Members": [ + + { + "@odata.id": "/redfish/v1/Managers/58893887-8974-2487-2389-841168418919" + } + + ], + "@odata.context": "/redfish/v1/$metadata#ManagerCollection.ManagerCollection", + "@odata.id": "/redfish/v1/Managers", + "@Redfish.Copyright": "Copyright 2014-2017 Distributed Management Task Force, Inc. (DMTF). For the full DMTF copyright policy, see http://www.dmtf.org/about/policies/copyright." diff --git a/releasenotes/notes/add-managers-resource-ffa58e329eccc058.yaml b/releasenotes/notes/add-managers-resource-ffa58e329eccc058.yaml new file mode 100644 index 00000000..9646203c --- /dev/null +++ b/releasenotes/notes/add-managers-resource-ffa58e329eccc058.yaml @@ -0,0 +1,6 @@ +--- +features: + - | + Adds Managers resource emulation to dynamic Redfish emulator. Emulated + Computer Systems link up automatically to the first of the configured + Managers (just one by default). diff --git a/sushy_tools/emulator/base.py b/sushy_tools/emulator/base.py new file mode 100644 index 00000000..9e660bed --- /dev/null +++ b/sushy_tools/emulator/base.py @@ -0,0 +1,37 @@ +# 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. + + +class DriverBase(object): + """Common base for Redfish Systems, Managers and Chassis models""" + + @classmethod + def initialize(cls, **kwargs): + """Initialize class attributes + + Since drivers may need to cache thing short-term. The emulator + instantiates the driver every time it serves a client query. + + Driver objects can cache whenever it makes sense for the duration + of a single session. It is guaranteed that the driver object will + never be reused for any other session. + + The `initialize` method is provided to set up the driver in a way + that would affect all the subsequent sessions. + + :params **kwargs: driver-specific parameters + :returns: initialized driver class + """ + return cls diff --git a/sushy_tools/emulator/main.py b/sushy_tools/emulator/main.py index 4169f154..d39bf3aa 100755 --- a/sushy_tools/emulator/main.py +++ b/sushy_tools/emulator/main.py @@ -14,12 +14,14 @@ # under the License. import argparse +from datetime import datetime import functools import json import os import ssl import sys +from sushy_tools.emulator.resources.managers import staticdriver from sushy_tools.emulator.resources.systems import libvirtdriver from sushy_tools.emulator.resources.systems import novadriver from sushy_tools import error @@ -27,7 +29,6 @@ from sushy_tools.error import FishyError import flask - app = flask.Flask(__name__) # Turn off strict_slashes on all routes app.url_map.strict_slashes = False @@ -36,6 +37,7 @@ app.url_map.strict_slashes = False class Resources(object): SYSTEMS = None + MANAGERS = None def __new__(cls, *args, **kwargs): @@ -75,14 +77,23 @@ class Resources(object): 'Initialized system resource backed by %s ' 'driver', cls.SYSTEMS().driver) + if cls.MANAGERS is None: + cls.MANAGERS = staticdriver.StaticDriver.initialize(app.config) + + app.logger.debug( + 'Initialized manager resource backed by %s ' + 'driver', cls.MANAGERS().driver) + return super(Resources, cls).__new__(cls, *args, **kwargs) def __enter__(self): self.systems = self.SYSTEMS() + self.managers = self.MANAGERS() return self def __exit__(self, exc_type, exc_val, exc_tb): del self.systems + del self.managers def instance_denied(**kwargs): @@ -145,6 +156,50 @@ def root_resource(): return flask.render_template('root.json') +@app.route('/redfish/v1/Managers') +@returns_json +def manager_collection_resource(): + with Resources() as resources: + + app.logger.debug('Serving managers list') + + return flask.render_template( + 'manager_collection.json', + manager_count=len(resources.managers.managers), + managers=resources.managers.managers) + + +@app.route('/redfish/v1/Managers/', methods=['GET']) +@returns_json +def manager_resource(identity): + if flask.request.method == 'GET': + + with Resources() as resources: + + app.logger.debug('Serving resources for manager "%s"', identity) + + managers = resources.managers + + uuid = managers.uuid(identity) + + # the first manager gets all resources + if uuid == managers.managers[0]: + systems = resources.systems.systems + + else: + systems = [] + + return flask.render_template( + 'manager.json', + dateTime=datetime.now().strftime('%Y-%M-%dT%H:%M:%S+00:00'), + identity=identity, + name=resources.managers.name(identity), + uuid=uuid, + serviceEntryPointUUID=resources.managers.uuid(identity), + systems=systems + ) + + @app.route('/redfish/v1/Systems') @returns_json def system_collection_resource(): @@ -167,6 +222,7 @@ def system_resource(identity): app.logger.debug('Serving resources for system "%s"', identity) with Resources() as resources: + return flask.render_template( 'system.json', identity=identity, @@ -176,7 +232,8 @@ def system_resource(identity): total_memory_gb=resources.systems.get_total_memory(identity), total_cpus=resources.systems.get_total_cpus(identity), boot_source_target=resources.systems.get_boot_device(identity), - boot_source_mode=resources.systems.get_boot_mode(identity) + boot_source_mode=resources.systems.get_boot_mode(identity), + managers=resources.managers.managers[:1] ) elif flask.request.method == 'PATCH': diff --git a/sushy_tools/emulator/resources/managers/__init__.py b/sushy_tools/emulator/resources/managers/__init__.py new file mode 100644 index 00000000..e69de29b diff --git a/sushy_tools/emulator/resources/managers/base.py b/sushy_tools/emulator/resources/managers/base.py new file mode 100644 index 00000000..e72a9a91 --- /dev/null +++ b/sushy_tools/emulator/resources/managers/base.py @@ -0,0 +1,69 @@ +# 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. + +import abc +import six + +from sushy_tools.emulator import base + + +@six.add_metaclass(abc.ABCMeta) +class AbstractManagersDriver(base.DriverBase): + """Base class backing Redfish Managers""" + + @classmethod + def initialize(cls, config): + cls._config = config + return cls + + @abc.abstractproperty + def driver(self): + """Return human-friendly driver information + + :returns: driver information as `str` + """ + + @abc.abstractproperty + def managers(self): + """Return available Redfish managers + + :returns: list of UUIDs representing the managers + """ + + @abc.abstractmethod + def uuid(self, identity): + """Get Redfish manager UUID + + The universal unique identifier (UUID) for this system. Can be used + in place of manager name if there are duplicates. + + If driver backend does not support non-unique manager identity, + this method may just return the `identity`. + + :returns: Redfish manager UUID + """ + + @abc.abstractmethod + def name(self, identity): + """Get Redfish manager name by UUID + + The universal unique identifier (UUID) for this Redfish manager. + Can be used in place of manager name if there are duplicates. + + If driver backend does not support manager names, this method may + just return the `identity`. + + :returns: Redfish manager name + """ diff --git a/sushy_tools/emulator/resources/managers/staticdriver.py b/sushy_tools/emulator/resources/managers/staticdriver.py new file mode 100644 index 00000000..c20b6609 --- /dev/null +++ b/sushy_tools/emulator/resources/managers/staticdriver.py @@ -0,0 +1,126 @@ +# 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. + +import logging +import uuid + +from sushy_tools.emulator.resources.managers.base import AbstractManagersDriver +from sushy_tools import error + + +logger = logging.getLogger(__name__) + + +class StaticDriver(AbstractManagersDriver): + """Redfish manager backed by configuration file""" + + def __init__(self): + + managers = self._config.get('SUSHY_EMULATOR_MANAGERS') + if not managers: + # Default Manager + managers = [ + { + "Id": "BMC", + "Name": "Manager", + "ServiceEntryPointUUID": "92384634-2938-2342-" + "8820-489239905423", + "UUID": "58893887-8974-2487-2389-841168418919", + } + ] + + self._managers_by_id = { + x['Id']: x for x in managers + } + self._managers_by_uuid = { + x['UUID']: x for x in managers if 'UUID' in x + } + self._managers_by_name = { + x['Name']: x for x in managers if 'Name' in x + } + + if len(self._managers_by_uuid) != len(managers): + raise error.FishyError( + 'Conflicting UUIDs in static managers configuration') + + def _get_manager(self, identity): + try: + uu_identity = str(uuid.UUID(identity)) + + return self._managers_by_uuid[uu_identity] + + except (ValueError, KeyError): + + try: + uu_identity = self._managers_by_name[identity]['UUID'] + + except KeyError: + + try: + uu_identity = self._managers_by_id[identity]['UUID'] + + except KeyError: + msg = ('Error finding manager by UUID/Name/Id ' + '"%(identity)s"' % {'identity': identity}) + + logger.debug(msg) + + raise error.FishyError(msg) + + raise error.AliasAccessError(uu_identity) + + @property + def driver(self): + """Return human-friendly driver information + + :returns: driver information as `str` + """ + return '' + + @property + def managers(self): + """Return available Redfish managers + + :returns: list of UUIDs representing the managers + """ + return sorted(self._managers_by_uuid) + + def uuid(self, identity): + """Get Redfish manager UUID + + The universal unique identifier (UUID) for this system. Can be used + in place of manager name if there are duplicates. + + If driver backend does not support non-unique manager identity, + this method may just return the `identity`. + + :returns: Redfish manager UUID + """ + manager = self._get_manager(identity) + return manager.get('UUID') + + def name(self, identity): + """Get Redfish manager name by UUID + + The universal unique identifier (UUID) for this Redfish manager. + Can be used in place of manager name if there are duplicates. + + If driver backend does not support manager names, this method may + just return the `identity`. + + :returns: Redfish manager name + """ + manager = self._get_manager(identity) + return manager.get('Name') diff --git a/sushy_tools/emulator/templates/manager.json b/sushy_tools/emulator/templates/manager.json new file mode 100644 index 00000000..e0550fb6 --- /dev/null +++ b/sushy_tools/emulator/templates/manager.json @@ -0,0 +1,32 @@ +{ + "@odata.type": "#Manager.v1_3_1.Manager", + "Id": {{ identity|string|tojson }}, + "Name": {{ name|string|tojson }}, + "UUID": {{ uuid|string|tojson }}, + "ServiceEntryPointUUID": {{ serviceEntryPointUUID|string|tojson }}, + "ManagerType": "BMC", + "Description": "Contoso BMC", + "Model": "Joo Janta 200", + "DateTime": {{ dateTime|string|tojson }}, + "DateTimeLocalOffset": "+00:00", + "Status": { + "State": "Enabled", + "Health": "OK" + }, + "PowerState": "On", + "FirmwareVersion": "1.00", + "Links": { + "ManagerForServers": [ + {% for system in systems %} + { + "@odata.id": {{ "/redfish/v1/Systems/%s"|format(system)|tojson }} + }{% if not loop.last %},{% endif %} + {% endfor %} + ], + "ManagerForChassis": [ + ] + }, + "@odata.context": "/redfish/v1/$metadata#Manager.Manager", + "@odata.id": {{ "/redfish/v1/Managers/%s"|format(identity)|string|tojson }}, + "@Redfish.Copyright": "Copyright 2014-2017 Distributed Management Task Force, Inc. (DMTF). For the full DMTF copyright policy, see http://www.dmtf.org/about/policies/copyright." +} \ No newline at end of file diff --git a/sushy_tools/emulator/templates/manager_collection.json b/sushy_tools/emulator/templates/manager_collection.json new file mode 100644 index 00000000..ed83001c --- /dev/null +++ b/sushy_tools/emulator/templates/manager_collection.json @@ -0,0 +1,16 @@ +{ + "@odata.type": "#ManagerCollection.ManagerCollection", + "Name": "Manager Collection", + "Members@odata.count": {{ manager_count }}, + "Members": [ + {% for manager in managers %} + { + "@odata.id": {{ "/redfish/v1/Managers/%s"|format(manager)|tojson }} + }{% if not loop.last %},{% endif %} + {% endfor %} + ], + "Oem": {}, + "@odata.context": "/redfish/v1/$metadata#ManagerCollection.ManagerCollection", + "@odata.id": "/redfish/v1/Managers", + "@Redfish.Copyright": "Copyright 2014-2017 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/root.json b/sushy_tools/emulator/templates/root.json index ce8c0a02..8f00fc81 100644 --- a/sushy_tools/emulator/templates/root.json +++ b/sushy_tools/emulator/templates/root.json @@ -7,6 +7,9 @@ "Systems": { "@odata.id": "/redfish/v1/Systems" }, + "Managers": { + "@odata.id": "/redfish/v1/Managers" + }, "@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/system.json b/sushy_tools/emulator/templates/system.json index 99c0d44e..f0da0af0 100644 --- a/sushy_tools/emulator/templates/system.json +++ b/sushy_tools/emulator/templates/system.json @@ -73,6 +73,11 @@ "Chassis": [ ], "ManagedBy": [ + {%- for manager in managers %} + { + "@odata.id": {{ "/redfish/v1/Managers/%s"|format(manager)|tojson }} + }{% if not loop.last %},{% endif %} + {% endfor -%} ] }, "Actions": { diff --git a/sushy_tools/tests/unit/emulator/resources/managers/__init__.py b/sushy_tools/tests/unit/emulator/resources/managers/__init__.py new file mode 100644 index 00000000..e69de29b diff --git a/sushy_tools/tests/unit/emulator/resources/managers/test_static.py b/sushy_tools/tests/unit/emulator/resources/managers/test_static.py new file mode 100644 index 00000000..7cd13c31 --- /dev/null +++ b/sushy_tools/tests/unit/emulator/resources/managers/test_static.py @@ -0,0 +1,74 @@ +# 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. +import uuid + +from oslotest import base + +from sushy_tools.emulator.resources.managers.staticdriver import StaticDriver +from sushy_tools import error + + +class StaticDriverTestCase(base.BaseTestCase): + + def setUp(self): + self.managers = [ + { + "Id": "BMC", + "Name": "The manager", + "UUID": "58893887-8974-2487-2389-841168418919", + "ServiceEntryPointUUID": "92384634-2938-2342-8820-489239905423" + } + ] + + self.identity = self.managers[0]['Id'] + self.uuid = self.managers[0]['UUID'] + self.name = self.managers[0]['Name'] + + test_driver = StaticDriver.initialize( + {'SUSHY_EMULATOR_MANAGERS': self.managers}) + self.test_driver = test_driver() + super(StaticDriverTestCase, self).setUp() + + def test__get_manager_by_id(self): + self.assertRaises( + error.AliasAccessError, self.test_driver._get_manager, + self.identity) + + def test__get_manager_by_name(self): + self.assertRaises( + error.AliasAccessError, self.test_driver._get_manager, self.name) + + def test__get_manager_by_uuid(self): + domain_id = uuid.UUID(self.uuid) + manager = self.test_driver._get_manager(str(domain_id)) + self.assertEqual( + self.managers[0], manager) + + def test_uuid_ok(self): + self.assertEqual(self.uuid, self.test_driver.uuid(self.uuid)) + + def test_uuid_fail(self): + self.assertRaises(error.FishyError, self.test_driver.uuid, 'xxx') + + def test_name_ok(self): + self.assertRaises(error.AliasAccessError, + self.test_driver.name, self.name) + + def test_name_fail(self): + self.assertRaises(error.FishyError, self.test_driver.name, 'xxx') + + def test_managers(self): + managers = self.test_driver.managers + self.assertEqual([self.uuid], managers) diff --git a/sushy_tools/tests/unit/emulator/test_main.py b/sushy_tools/tests/unit/emulator/test_main.py index 7896c3e4..5e29b01a 100644 --- a/sushy_tools/tests/unit/emulator/test_main.py +++ b/sushy_tools/tests/unit/emulator/test_main.py @@ -40,9 +40,41 @@ class EmulatorTestCase(base.BaseTestCase): self.assertEqual(200, response.status_code) self.assertEqual('RedvirtService', response.json['Id']) - def test_collection_resource(self, resources_mock): + def test_manager_collection_resource(self, resources_mock): + resources_mock = resources_mock.return_value.__enter__.return_value + managers_mock = resources_mock.managers + type(managers_mock).managers = mock.PropertyMock( + return_value=['bmc0', 'bmc1']) + response = self.app.get('/redfish/v1/Managers') + self.assertEqual(200, response.status_code) + self.assertEqual({'@odata.id': '/redfish/v1/Managers/bmc0'}, + response.json['Members'][0]) + self.assertEqual({'@odata.id': '/redfish/v1/Managers/bmc1'}, + response.json['Members'][1]) + + def test_manager_resource_get(self, resources_mock): resources_mock = resources_mock.return_value.__enter__.return_value systems_mock = resources_mock.systems + systems_mock.systems = ['xxx'] + managers_mock = resources_mock.managers + managers_mock.managers = ['xxxx-yyyy-zzzz'] + managers_mock.uuid.return_value = 'xxxx-yyyy-zzzz' + managers_mock.name.return_value = 'name' + + response = self.app.get('/redfish/v1/Managers/xxxx-yyyy-zzzz') + + self.assertEqual(200, response.status_code) + self.assertEqual('xxxx-yyyy-zzzz', response.json['Id']) + self.assertEqual('xxxx-yyyy-zzzz', response.json['UUID']) + self.assertEqual('xxxx-yyyy-zzzz', + response.json['ServiceEntryPointUUID']) + self.assertEqual([{'@odata.id': '/redfish/v1/Systems/xxx'}], + response.json['Links']['ManagerForServers']) + + def test_system_collection_resource(self, resources_mock): + resources_mock = resources_mock.return_value.__enter__.return_value + systems_mock = resources_mock.systems + type(systems_mock).systems = mock.PropertyMock( return_value=['host0', 'host1']) response = self.app.get('/redfish/v1/Systems') @@ -61,6 +93,8 @@ class EmulatorTestCase(base.BaseTestCase): systems_mock.get_total_cpus.return_value = 2 systems_mock.get_boot_device.return_value = 'Cd' systems_mock.get_boot_mode.return_value = 'Legacy' + managers_mock = resources_mock.managers + managers_mock.managers = ['aaaa-bbbb-cccc'] response = self.app.get('/redfish/v1/Systems/xxxx-yyyy-zzzz') @@ -75,6 +109,9 @@ class EmulatorTestCase(base.BaseTestCase): 'Cd', response.json['Boot']['BootSourceOverrideTarget']) self.assertEqual( 'Legacy', response.json['Boot']['BootSourceOverrideMode']) + self.assertEqual( + [{'@odata.id': '/redfish/v1/Managers/aaaa-bbbb-cccc'}], + response.json['Links']['ManagedBy']) def test_system_resource_patch(self, resources_mock): resources_mock = resources_mock.return_value.__enter__.return_value