Add Managers resource support

This change adds basic Redfish Managers resource support based
on the newly introduces "manager" driver infrastructure. As of
this patch, the only backend for the Redfish manager implemented
is static config (Flask) file.

Story: 2005149
Task: 29856
Change-Id: I95e957ad02b602410049c5f078c0703e2f0a4962
This commit is contained in:
Ilya Etingof 2019-02-21 20:11:26 +01:00
parent e034afced2
commit f1ae2365a8
15 changed files with 542 additions and 11 deletions

View File

@ -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"
}
]

View File

@ -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 <https://opendev.org/openstack/virtualbmc>`_ 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."

View File

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

View File

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

View File

@ -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/<identity>', 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':

View File

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

View File

@ -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 '<static-managers>'
@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')

View File

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

View File

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

View File

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

View File

@ -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": {

View File

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

View File

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