Add memoization to expensive emulator calls

Drivers of Redfish emulator now cache the
outcome of a select calls choosing either the
two policies:

* Cache the information for the lifetime of the
  emulator process if it's unlikely for such
  data to get out of sync
* Cache the more dynamic information just for
  the duration of a single Redfish REST API
  call

This change improves emulator responsiveness.

Change-Id: Ief46a0a7b82900ccf6d4c9ebb417f711a9b00513
This commit is contained in:
Ilya Etingof 2018-10-23 18:24:56 +02:00
parent b6ccaf6da8
commit b13ea4e9c7
9 changed files with 308 additions and 112 deletions

View File

@ -21,6 +21,25 @@ import six
class AbstractDriver(object): class AbstractDriver(object):
"""Base class for all virtualization drivers""" """Base class for all virtualization drivers"""
@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
@abc.abstractproperty @abc.abstractproperty
def driver(self): def driver(self):
"""Return human-friendly driver information """Return human-friendly driver information

View File

@ -13,12 +13,13 @@
# License for the specific language governing permissions and limitations # License for the specific language governing permissions and limitations
# under the License. # under the License.
from collections import namedtuple
import logging import logging
import uuid import uuid
import xml.etree.ElementTree as ET import xml.etree.ElementTree as ET
from collections import namedtuple
from sushy_tools.emulator.drivers.base import AbstractDriver from sushy_tools.emulator.drivers.base import AbstractDriver
from sushy_tools.emulator.drivers import memoize
from sushy_tools import error from sushy_tools import error
try: try:
@ -102,15 +103,17 @@ class LibvirtDriver(AbstractDriver):
"NicBoot1": "NetworkBoot", "NicBoot1": "NetworkBoot",
"ProcTurboMode": "Enabled"} "ProcTurboMode": "Enabled"}
def __init__(self, config, uri=None): @classmethod
self._config = config def initialize(cls, config, uri=None):
self._uri = uri or self.LIBVIRT_URI cls._config = config
self.BOOT_LOADER_MAP = self._config.get( cls._uri = uri or cls.LIBVIRT_URI
'SUSHY_EMULATOR_BOOT_LOADER_MAP', self.BOOT_LOADER_MAP) cls.BOOT_LOADER_MAP = cls._config.get(
self.KNOWN_BOOT_LOADERS = set(y 'SUSHY_EMULATOR_BOOT_LOADER_MAP', cls.BOOT_LOADER_MAP)
for x in self.BOOT_LOADER_MAP.values() cls.KNOWN_BOOT_LOADERS = set(y for x in cls.BOOT_LOADER_MAP.values()
for y in x.values()) for y in x.values())
return cls
@memoize.memoize()
def _get_domain(self, identity, readonly=False): def _get_domain(self, identity, readonly=False):
with libvirt_open(self._uri, readonly=readonly) as conn: with libvirt_open(self._uri, readonly=readonly) as conn:
try: try:
@ -517,6 +520,7 @@ class LibvirtDriver(AbstractDriver):
update_existing_attributes) update_existing_attributes)
if result.attributes_written: if result.attributes_written:
try: try:
with libvirt_open(self._uri) as conn: with libvirt_open(self._uri) as conn:
conn.defineXML(ET.tostring(result.tree).decode('utf-8')) conn.defineXML(ET.tostring(result.tree).decode('utf-8'))

View File

@ -0,0 +1,56 @@
# Copyright 2018 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 functools import wraps
def memoize(permanent_cache=None):
"""Cache the return value of the decorated method.
:param permanent_cache: a `dict` like object to use as a cache.
If not given, the `._cache` attribute would be added to
the object of the decorated method pointing to a newly
created `dict`.
:return: decorated function
"""
def decorator(method):
@wraps(method)
def wrapped(self, *args, **kwargs):
if permanent_cache is None:
try:
cache = self._cache
except AttributeError:
cache = self._cache = {}
else:
cache = permanent_cache
method_cache = cache.setdefault(method, {})
key = frozenset(args), frozenset(kwargs)
try:
return method_cache[key]
except KeyError:
rv = method(self, *args, **kwargs)
method_cache[key] = rv
return rv
return wrapped
return decorator

View File

@ -17,6 +17,7 @@ import logging
import math import math
from sushy_tools.emulator.drivers.base import AbstractDriver from sushy_tools.emulator.drivers.base import AbstractDriver
from sushy_tools.emulator.drivers import memoize
from sushy_tools import error from sushy_tools import error
try: try:
@ -51,11 +52,16 @@ class OpenStackDriver(AbstractDriver):
BOOT_MODE_MAP_REV = {v: k for k, v in BOOT_MODE_MAP.items()} BOOT_MODE_MAP_REV = {v: k for k, v in BOOT_MODE_MAP.items()}
def __init__(self, config, os_cloud, readonly=False): PERMANENT_CACHE = {}
self._cc = openstack.connect(cloud=os_cloud)
self._os_cloud = os_cloud
self._config = config
@classmethod
def initialize(cls, config, os_cloud, readonly=False):
cls._cc = openstack.connect(cloud=os_cloud)
cls._os_cloud = os_cloud
cls._config = config
return cls
@memoize.memoize()
def _get_instance(self, identity): def _get_instance(self, identity):
instance = self._cc.get_server(identity) instance = self._cc.get_server(identity)
if instance: if instance:
@ -72,10 +78,20 @@ class OpenStackDriver(AbstractDriver):
raise error.FishyError(msg) raise error.FishyError(msg)
@memoize.memoize(permanent_cache=PERMANENT_CACHE)
def _get_flavor(self, identity): def _get_flavor(self, identity):
instance = self._get_instance(identity) instance = self._get_instance(identity)
flavor = self._cc.get_flavor(instance.flavor.id) return self._cc.get_flavor(instance.flavor.id)
return flavor
@memoize.memoize(permanent_cache=PERMANENT_CACHE)
def _get_image_info(self, identity):
return self._cc.image.find_image(identity)
def _get_server_metadata(self, identity):
return self._cc.compute.get_server_metadata(identity).to_dict()
def _set_server_metadata(self, identity, metadata):
self._cc.compute.set_server_metadata(identity, metadata)
@property @property
def driver(self): def driver(self):
@ -190,7 +206,7 @@ class OpenStackDriver(AbstractDriver):
except error.FishyError: except error.FishyError:
return return
metadata = self._cc.compute.get_server_metadata(instance.id).to_dict() metadata = self._get_server_metadata(instance.id)
# NOTE(etingof): the following probably only works with # NOTE(etingof): the following probably only works with
# libvirt-backed compute nodes # libvirt-backed compute nodes
@ -224,7 +240,6 @@ class OpenStackDriver(AbstractDriver):
# NOTE(etingof): the following probably only works with # NOTE(etingof): the following probably only works with
# libvirt-backed compute nodes # libvirt-backed compute nodes
self._cc.compute.set_server_metadata( self._cc.compute.set_server_metadata(
instance.id, **{'libvirt:pxe-first': '1' instance.id, **{'libvirt:pxe-first': '1'
if target == 'network' else ''} if target == 'network' else ''}
@ -238,7 +253,7 @@ class OpenStackDriver(AbstractDriver):
""" """
instance = self._get_instance(identity) instance = self._get_instance(identity)
image = self._cc.image.find_image(instance.image['id']) image = self._get_image_info(instance.image['id'])
hw_firmware_type = getattr(image, 'hw_firmware_type', None) hw_firmware_type = getattr(image, 'hw_firmware_type', None)

View File

@ -14,6 +14,7 @@
# under the License. # under the License.
import argparse import argparse
import contextlib
import functools import functools
import json import json
import os import os
@ -32,22 +33,22 @@ app = flask.Flask(__name__)
# Turn off strict_slashes on all routes # Turn off strict_slashes on all routes
app.url_map.strict_slashes = False app.url_map.strict_slashes = False
driver = None DRIVER = None
def init_virt_driver(decorated_func): def init_virt_driver(decorated_func):
@functools.wraps(decorated_func) @functools.wraps(decorated_func)
def decorator(*args, **kwargs): def decorator(*args, **kwargs):
global driver global DRIVER
if driver is None: if DRIVER is None:
if 'OS_CLOUD' in os.environ: if 'OS_CLOUD' in os.environ:
if not novadriver.is_loaded: if not novadriver.is_loaded:
app.logger.error('Nova driver not loaded') app.logger.error('Nova driver not loaded')
sys.exit(1) sys.exit(1)
driver = novadriver.OpenStackDriver( DRIVER = novadriver.OpenStackDriver.initialize(
app.config, os.environ['OS_CLOUD']) app.config, os.environ['OS_CLOUD'])
else: else:
@ -55,7 +56,7 @@ def init_virt_driver(decorated_func):
app.logger.error('libvirt driver not loaded') app.logger.error('libvirt driver not loaded')
sys.exit(1) sys.exit(1)
driver = libvirtdriver.LibvirtDriver( DRIVER = libvirtdriver.LibvirtDriver.initialize(
app.config, app.config,
os.environ.get( os.environ.get(
'SUSHY_EMULATOR_LIBVIRT_URI', 'SUSHY_EMULATOR_LIBVIRT_URI',
@ -63,7 +64,7 @@ def init_virt_driver(decorated_func):
os.environ.get('SUSHY_EMULATOR_LIBVIRT_URL')) os.environ.get('SUSHY_EMULATOR_LIBVIRT_URL'))
) )
app.logger.debug('Running with %s', driver.driver) app.logger.debug('Running with %s', DRIVER.driver)
return decorated_func(*args, **kwargs) return decorated_func(*args, **kwargs)
@ -99,6 +100,11 @@ def ensure_instance_access(decorated_func):
return decorator return decorator
@contextlib.contextmanager
def virt_driver():
yield DRIVER()
def returns_json(decorated_func): def returns_json(decorated_func):
@functools.wraps(decorated_func) @functools.wraps(decorated_func)
def decorator(*args, **kwargs): def decorator(*args, **kwargs):
@ -135,8 +141,9 @@ def root_resource():
@init_virt_driver @init_virt_driver
@returns_json @returns_json
def system_collection_resource(): def system_collection_resource():
systems = [system for system in driver.systems with virt_driver() as driver:
if not instance_denied(identity=system)] systems = [system for system in driver.systems
if not instance_denied(identity=system)]
app.logger.debug('Serving systems list') app.logger.debug('Serving systems list')
@ -153,46 +160,47 @@ def system_resource(identity):
app.logger.debug('Serving resources for system "%s"', identity) app.logger.debug('Serving resources for system "%s"', identity)
return flask.render_template( with virt_driver() as driver:
'system.json', return flask.render_template(
identity=identity, 'system.json', identity=identity,
name=driver.name(identity), uuid=driver.uuid(identity),
uuid=driver.uuid(identity), power_state=driver.get_power_state(identity),
power_state=driver.get_power_state(identity), total_memory_gb=driver.get_total_memory(identity),
total_memory_gb=driver.get_total_memory(identity), total_cpus=driver.get_total_cpus(identity),
total_cpus=driver.get_total_cpus(identity), boot_source_target=driver.get_boot_device(identity),
boot_source_target=driver.get_boot_device(identity), boot_source_mode=driver.get_boot_mode(identity)
boot_source_mode=driver.get_boot_mode(identity) )
)
elif flask.request.method == 'PATCH': elif flask.request.method == 'PATCH':
boot = flask.request.json.get('Boot') boot = flask.request.json.get('Boot')
if not boot: if not boot:
return 'PATCH only works for the Boot element', 400 return 'PATCH only works for the Boot element', 400
target = boot.get('BootSourceOverrideTarget') with virt_driver() as driver:
if target: target = boot.get('BootSourceOverrideTarget')
# NOTE(lucasagomes): In libvirt we always set the boot
# device frequency to "continuous" so, we are ignoring the
# BootSourceOverrideEnabled element here
driver.set_boot_device(identity, target) if target:
# NOTE(lucasagomes): In libvirt we always set the boot
# device frequency to "continuous" so, we are ignoring the
# BootSourceOverrideEnabled element here
app.logger.info('Set boot device to "%s" for system "%s"', driver.set_boot_device(identity, target)
target, identity)
mode = boot.get('BootSourceOverrideMode') app.logger.info('Set boot device to "%s" for system "%s"',
target, identity)
if mode: mode = boot.get('BootSourceOverrideMode')
driver.set_boot_mode(identity, mode)
app.logger.info('Set boot mode to "%s" for system "%s"', if mode:
mode, identity) driver.set_boot_mode(identity, mode)
if not target and not mode: app.logger.info('Set boot mode to "%s" for system "%s"',
return ('Missing the BootSourceOverrideTarget and/or ' mode, identity)
'BootSourceOverrideMode element', 400)
if not target and not mode:
return ('Missing the BootSourceOverrideTarget and/or '
'BootSourceOverrideMode element', 400)
return '', 204 return '', 204
@ -203,7 +211,9 @@ def system_resource(identity):
@ensure_instance_access @ensure_instance_access
@returns_json @returns_json
def ethernet_interfaces_collection(identity): def ethernet_interfaces_collection(identity):
nics = driver.get_nics(identity) with virt_driver() as driver:
nics = driver.get_nics(identity)
return flask.render_template( return flask.render_template(
'ethernet_interfaces_collection.json', identity=identity, 'ethernet_interfaces_collection.json', identity=identity,
nics=nics) nics=nics)
@ -215,7 +225,9 @@ def ethernet_interfaces_collection(identity):
@ensure_instance_access @ensure_instance_access
@returns_json @returns_json
def ethernet_interface(identity, nic_id): def ethernet_interface(identity, nic_id):
nics = driver.get_nics(identity) with virt_driver() as driver:
nics = driver.get_nics(identity)
for nic in nics: for nic in nics:
if nic['id'] == nic_id: if nic['id'] == nic_id:
return flask.render_template( return flask.render_template(
@ -232,7 +244,8 @@ def ethernet_interface(identity, nic_id):
def system_reset_action(identity): def system_reset_action(identity):
reset_type = flask.request.json.get('ResetType') reset_type = flask.request.json.get('ResetType')
driver.set_power_state(identity, reset_type) with virt_driver() as driver:
driver.set_power_state(identity, reset_type)
app.logger.info('System "%s" power state set to "%s"', app.logger.info('System "%s" power state set to "%s"',
identity, reset_type) identity, reset_type)
@ -245,7 +258,8 @@ def system_reset_action(identity):
@ensure_instance_access @ensure_instance_access
@returns_json @returns_json
def bios(identity): def bios(identity):
bios = driver.get_bios(identity) with virt_driver() as driver:
bios = driver.get_bios(identity)
app.logger.debug('Serving BIOS for system "%s"', identity) app.logger.debug('Serving BIOS for system "%s"', identity)
@ -263,7 +277,8 @@ def bios(identity):
def bios_settings(identity): def bios_settings(identity):
if flask.request.method == 'GET': if flask.request.method == 'GET':
bios = driver.get_bios(identity) with virt_driver() as driver:
bios = driver.get_bios(identity)
app.logger.debug('Serving BIOS Settings for system "%s"', identity) app.logger.debug('Serving BIOS Settings for system "%s"', identity)
@ -275,7 +290,9 @@ def bios_settings(identity):
elif flask.request.method == 'PATCH': elif flask.request.method == 'PATCH':
attributes = flask.request.json.get('Attributes') attributes = flask.request.json.get('Attributes')
driver.set_bios(identity, attributes) with virt_driver() as driver:
driver.set_bios(identity, attributes)
app.logger.info('System "%s" BIOS attributes "%s" updated', app.logger.info('System "%s" BIOS attributes "%s" updated',
identity, attributes) identity, attributes)
return '', 204 return '', 204
@ -288,8 +305,11 @@ def bios_settings(identity):
@returns_json @returns_json
def system_reset_bios(identity): def system_reset_bios(identity):
driver.reset_bios(identity) with virt_driver() as driver:
driver.reset_bios(identity)
app.logger.info('BIOS for system "%s" reset', identity) app.logger.info('BIOS for system "%s" reset', identity)
return '', 204 return '', 204
@ -336,7 +356,7 @@ def parse_args():
def main(): def main():
global driver global DRIVER
args = parse_args() args = parse_args()
@ -350,20 +370,21 @@ def main():
app.logger.error('Nova driver not loaded') app.logger.error('Nova driver not loaded')
return 1 return 1
driver = novadriver.OpenStackDriver(app.config, os_cloud) DRIVER = novadriver.OpenStackDriver.initialize(
app.config, args.os_cloud)
else: else:
if not libvirtdriver.is_loaded: if not libvirtdriver.is_loaded:
app.logger.error('libvirt driver not loaded') app.logger.error('libvirt driver not loaded')
return 1 return 1
driver = libvirtdriver.LibvirtDriver( DRIVER = libvirtdriver.LibvirtDriver.initialize(
app.config, app.config,
args.libvirt_uri or args.libvirt_uri or
app.config.get('SUSHY_EMULATOR_LIBVIRT_URI', '') app.config.get('SUSHY_EMULATOR_LIBVIRT_URI', '')
) )
app.logger.debug('Running with %s', driver.driver) app.logger.debug('Running with %s', DRIVER.driver)
ssl_context = None ssl_context = None

View File

@ -25,7 +25,8 @@ class LibvirtDriverTestCase(base.BaseTestCase):
uuid = 'c7a5fdbd-cdaf-9455-926a-d65c16db1809' uuid = 'c7a5fdbd-cdaf-9455-926a-d65c16db1809'
def setUp(self): def setUp(self):
self.test_driver = LibvirtDriver({}) test_driver_class = LibvirtDriver.initialize({})
self.test_driver = test_driver_class()
super(LibvirtDriverTestCase, self).setUp() super(LibvirtDriverTestCase, self).setUp()
@mock.patch('libvirt.open', autospec=True) @mock.patch('libvirt.open', autospec=True)

View File

@ -16,23 +16,24 @@ from six.moves import mock
from sushy_tools.emulator import main from sushy_tools.emulator import main
@mock.patch('sushy_tools.emulator.main.driver', autospec=True) @mock.patch.object(main, 'DRIVER') # This enables libvirt driver
class EmulatorTestCase(base.BaseTestCase): class EmulatorTestCase(base.BaseTestCase):
name = 'QEmu-fedora-i686' name = 'QEmu-fedora-i686'
uuid = 'c7a5fdbd-cdaf-9455-926a-d65c16db1809' uuid = 'c7a5fdbd-cdaf-9455-926a-d65c16db1809'
def setUp(self): def setUp(self):
main.driver = None main.DRIVER = None
self.app = main.app.test_client() self.app = main.app.test_client()
super(EmulatorTestCase, self).setUp() super(EmulatorTestCase, self).setUp()
def test_error(self, driver_mock): def test_error(self, driver_mock):
driver_mock.get_power_state.side_effect = Exception('Fish is dead') driver_mock.return_value.get_power_state.side_effect = (
Exception('Fish is dead'))
response = self.app.get('/redfish/v1/Systems/' + self.uuid) response = self.app.get('/redfish/v1/Systems/' + self.uuid)
self.assertEqual('500 INTERNAL SERVER ERROR', response.status) self.assertEqual(500, response.status_code)
def test_root_resource(self, driver_mock): def test_root_resource(self, driver_mock):
response = self.app.get('/redfish/v1/') response = self.app.get('/redfish/v1/')
@ -40,7 +41,7 @@ class EmulatorTestCase(base.BaseTestCase):
self.assertEqual('RedvirtService', response.json['Id']) self.assertEqual('RedvirtService', response.json['Id'])
def test_collection_resource(self, driver_mock): def test_collection_resource(self, driver_mock):
type(driver_mock).systems = mock.PropertyMock( type(driver_mock.return_value).systems = mock.PropertyMock(
return_value=['host0', 'host1']) return_value=['host0', 'host1'])
response = self.app.get('/redfish/v1/Systems') response = self.app.get('/redfish/v1/Systems')
self.assertEqual(200, response.status_code) self.assertEqual(200, response.status_code)
@ -50,12 +51,12 @@ class EmulatorTestCase(base.BaseTestCase):
response.json['Members'][1]) response.json['Members'][1])
def test_system_resource_get(self, driver_mock): def test_system_resource_get(self, driver_mock):
driver_mock.uuid.return_value = 'zzzz-yyyy-xxxx' driver_mock.return_value.uuid.return_value = 'zzzz-yyyy-xxxx'
driver_mock.get_power_state.return_value = 'On' driver_mock.return_value.get_power_state.return_value = 'On'
driver_mock.get_total_memory.return_value = 1 driver_mock.return_value.get_total_memory.return_value = 1
driver_mock.get_total_cpus.return_value = 2 driver_mock.return_value.get_total_cpus.return_value = 2
driver_mock.get_boot_device.return_value = 'Cd' driver_mock.return_value.get_boot_device.return_value = 'Cd'
driver_mock.get_boot_mode.return_value = 'Legacy' driver_mock.return_value.get_boot_mode.return_value = 'Legacy'
response = self.app.get('/redfish/v1/Systems/xxxx-yyyy-zzzz') response = self.app.get('/redfish/v1/Systems/xxxx-yyyy-zzzz')
@ -76,7 +77,7 @@ class EmulatorTestCase(base.BaseTestCase):
response = self.app.patch('/redfish/v1/Systems/xxxx-yyyy-zzzz', response = self.app.patch('/redfish/v1/Systems/xxxx-yyyy-zzzz',
json=data) json=data)
self.assertEqual(204, response.status_code) self.assertEqual(204, response.status_code)
set_boot_device = driver_mock.set_boot_device set_boot_device = driver_mock.return_value.set_boot_device
set_boot_device.assert_called_once_with('xxxx-yyyy-zzzz', 'Cd') set_boot_device.assert_called_once_with('xxxx-yyyy-zzzz', 'Cd')
def test_system_reset_action_on(self, driver_mock): def test_system_reset_action_on(self, driver_mock):
@ -85,7 +86,7 @@ class EmulatorTestCase(base.BaseTestCase):
'/redfish/v1/Systems/xxxx-yyyy-zzzz/Actions/ComputerSystem.Reset', '/redfish/v1/Systems/xxxx-yyyy-zzzz/Actions/ComputerSystem.Reset',
json=data) json=data)
self.assertEqual(204, response.status_code) self.assertEqual(204, response.status_code)
set_power_state = driver_mock.set_power_state set_power_state = driver_mock.return_value.set_power_state
set_power_state.assert_called_once_with('xxxx-yyyy-zzzz', 'On') set_power_state.assert_called_once_with('xxxx-yyyy-zzzz', 'On')
def test_system_reset_action_forceon(self, driver_mock): def test_system_reset_action_forceon(self, driver_mock):
@ -94,7 +95,7 @@ class EmulatorTestCase(base.BaseTestCase):
'/redfish/v1/Systems/xxxx-yyyy-zzzz/Actions/ComputerSystem.Reset', '/redfish/v1/Systems/xxxx-yyyy-zzzz/Actions/ComputerSystem.Reset',
json=data) json=data)
self.assertEqual(204, response.status_code) self.assertEqual(204, response.status_code)
set_power_state = driver_mock.set_power_state set_power_state = driver_mock.return_value.set_power_state
set_power_state.assert_called_once_with('xxxx-yyyy-zzzz', 'ForceOn') set_power_state.assert_called_once_with('xxxx-yyyy-zzzz', 'ForceOn')
def test_system_reset_action_forceoff(self, driver_mock): def test_system_reset_action_forceoff(self, driver_mock):
@ -103,7 +104,7 @@ class EmulatorTestCase(base.BaseTestCase):
'/redfish/v1/Systems/xxxx-yyyy-zzzz/Actions/ComputerSystem.Reset', '/redfish/v1/Systems/xxxx-yyyy-zzzz/Actions/ComputerSystem.Reset',
json=data) json=data)
self.assertEqual(204, response.status_code) self.assertEqual(204, response.status_code)
set_power_state = driver_mock.set_power_state set_power_state = driver_mock.return_value.set_power_state
set_power_state.assert_called_once_with('xxxx-yyyy-zzzz', 'ForceOff') set_power_state.assert_called_once_with('xxxx-yyyy-zzzz', 'ForceOff')
def test_system_reset_action_shutdown(self, driver_mock): def test_system_reset_action_shutdown(self, driver_mock):
@ -112,7 +113,7 @@ class EmulatorTestCase(base.BaseTestCase):
'/redfish/v1/Systems/xxxx-yyyy-zzzz/Actions/ComputerSystem.Reset', '/redfish/v1/Systems/xxxx-yyyy-zzzz/Actions/ComputerSystem.Reset',
json=data) json=data)
self.assertEqual(204, response.status_code) self.assertEqual(204, response.status_code)
set_power_state = driver_mock.set_power_state set_power_state = driver_mock.return_value.set_power_state
set_power_state.assert_called_once_with( set_power_state.assert_called_once_with(
'xxxx-yyyy-zzzz', 'GracefulShutdown') 'xxxx-yyyy-zzzz', 'GracefulShutdown')
@ -122,7 +123,7 @@ class EmulatorTestCase(base.BaseTestCase):
'/redfish/v1/Systems/xxxx-yyyy-zzzz/Actions/ComputerSystem.Reset', '/redfish/v1/Systems/xxxx-yyyy-zzzz/Actions/ComputerSystem.Reset',
json=data) json=data)
self.assertEqual(204, response.status_code) self.assertEqual(204, response.status_code)
set_power_state = driver_mock.set_power_state set_power_state = driver_mock.return_value.set_power_state
set_power_state.assert_called_once_with( set_power_state.assert_called_once_with(
'xxxx-yyyy-zzzz', 'GracefulRestart') 'xxxx-yyyy-zzzz', 'GracefulRestart')
@ -132,7 +133,7 @@ class EmulatorTestCase(base.BaseTestCase):
'/redfish/v1/Systems/xxxx-yyyy-zzzz/Actions/ComputerSystem.Reset', '/redfish/v1/Systems/xxxx-yyyy-zzzz/Actions/ComputerSystem.Reset',
json=data) json=data)
self.assertEqual(204, response.status_code) self.assertEqual(204, response.status_code)
set_power_state = driver_mock.set_power_state set_power_state = driver_mock.return_value.set_power_state
set_power_state.assert_called_once_with( set_power_state.assert_called_once_with(
'xxxx-yyyy-zzzz', 'ForceRestart') 'xxxx-yyyy-zzzz', 'ForceRestart')
@ -142,7 +143,7 @@ class EmulatorTestCase(base.BaseTestCase):
'/redfish/v1/Systems/xxxx-yyyy-zzzz/Actions/ComputerSystem.Reset', '/redfish/v1/Systems/xxxx-yyyy-zzzz/Actions/ComputerSystem.Reset',
json=data) json=data)
self.assertEqual(204, response.status_code) self.assertEqual(204, response.status_code)
set_power_state = driver_mock.set_power_state set_power_state = driver_mock.return_value.set_power_state
set_power_state.assert_called_once_with('xxxx-yyyy-zzzz', 'Nmi') set_power_state.assert_called_once_with('xxxx-yyyy-zzzz', 'Nmi')
@mock.patch.dict(main.app.config, {}, clear=True) @mock.patch.dict(main.app.config, {}, clear=True)
@ -170,19 +171,20 @@ class EmulatorTestCase(base.BaseTestCase):
self.assertTrue(main.instance_denied(identity='b')) self.assertTrue(main.instance_denied(identity='b'))
def test_get_bios(self, driver_mock): def test_get_bios(self, driver_mock):
driver_mock.get_bios.return_value = {"attribute 1": "value 1", driver_mock.return_value.get_bios.return_value = {
"attribute 2": "value 2"} "attribute 1": "value 1",
"attribute 2": "value 2"}
response = self.app.get('/redfish/v1/Systems/' + self.uuid + '/BIOS') response = self.app.get('/redfish/v1/Systems/' + self.uuid + '/BIOS')
self.assertEqual('200 OK', response.status) self.assertEqual(200, response.status_code)
self.assertEqual('BIOS', response.json['Id']) self.assertEqual('BIOS', response.json['Id'])
self.assertEqual({"attribute 1": "value 1", self.assertEqual({"attribute 1": "value 1",
"attribute 2": "value 2"}, "attribute 2": "value 2"},
response.json['Attributes']) response.json['Attributes'])
def test_get_bios_existing(self, driver_mock): def test_get_bios_existing(self, driver_mock):
driver_mock.get_bios.return_value = {"attribute 1": "value 1", driver_mock.return_value.get_bios.return_value = {
"attribute 2": "value 2"} "attribute 1": "value 1", "attribute 2": "value 2"}
response = self.app.get( response = self.app.get(
'/redfish/v1/Systems/' + self.uuid + '/BIOS/Settings') '/redfish/v1/Systems/' + self.uuid + '/BIOS/Settings')
@ -194,39 +196,33 @@ class EmulatorTestCase(base.BaseTestCase):
def test_bios_settings_patch(self, driver_mock): def test_bios_settings_patch(self, driver_mock):
data = {'Attributes': {'key': 'value'}} data = {'Attributes': {'key': 'value'}}
self.app.driver = driver_mock
response = self.app.patch( response = self.app.patch(
'/redfish/v1/Systems/xxxx-yyyy-zzzz/BIOS/Settings', '/redfish/v1/Systems/xxxx-yyyy-zzzz/BIOS/Settings',
json=data) json=data)
self.assertEqual(204, response.status_code) self.assertEqual(204, response.status_code)
driver_mock.set_bios.assert_called_once_with('xxxx-yyyy-zzzz', driver_mock.return_value.set_bios.assert_called_once_with(
{'key': 'value'}) 'xxxx-yyyy-zzzz', {'key': 'value'})
def test_set_bios(self, driver_mock): def test_set_bios(self, driver_mock):
data = {'Attributes': {'key': 'value'}} data = {'Attributes': {'key': 'value'}}
self.app.driver = driver_mock
response = self.app.patch( response = self.app.patch(
'/redfish/v1/Systems/xxxx-yyyy-zzzz/BIOS/Settings', '/redfish/v1/Systems/xxxx-yyyy-zzzz/BIOS/Settings',
json=data) json=data)
self.assertEqual(204, response.status_code) self.assertEqual(204, response.status_code)
driver_mock.set_bios.assert_called_once_with( driver_mock.return_value.set_bios.assert_called_once_with(
'xxxx-yyyy-zzzz', data['Attributes']) 'xxxx-yyyy-zzzz', data['Attributes'])
def test_reset_bios(self, driver_mock): def test_reset_bios(self, driver_mock):
self.app.driver = driver_mock
response = self.app.post('/redfish/v1/Systems/' + self.uuid + response = self.app.post('/redfish/v1/Systems/' + self.uuid +
'/BIOS/Actions/Bios.ResetBios') '/BIOS/Actions/Bios.ResetBios')
self.assertEqual(204, response.status_code) self.assertEqual(204, response.status_code)
driver_mock.reset_bios.assert_called_once_with(self.uuid) driver_mock.return_value.reset_bios.assert_called_once_with(self.uuid)
def test_ethernet_interfaces_collection(self, driver_mock): def test_ethernet_interfaces_collection(self, driver_mock):
driver_mock.get_nics.return_value = [{'id': 'nic1', driver_mock.return_value.get_nics.return_value = [
'mac': '52:54:00:4e:5d:37'}, {'id': 'nic1', 'mac': '52:54:00:4e:5d:37'},
{'id': 'nic2', {'id': 'nic2', 'mac': '00:11:22:33:44:55'}]
'mac': '00:11:22:33:44:55'}]
response = self.app.get('redfish/v1/Systems/' + self.uuid + response = self.app.get('redfish/v1/Systems/' + self.uuid +
'/EthernetInterfaces') '/EthernetInterfaces')
@ -241,7 +237,7 @@ class EmulatorTestCase(base.BaseTestCase):
[m['@odata.id'] for m in response.json['Members']]) [m['@odata.id'] for m in response.json['Members']])
def test_ethernet_interfaces_collection_empty(self, driver_mock): def test_ethernet_interfaces_collection_empty(self, driver_mock):
driver_mock.get_nics.return_value = [] driver_mock.return_value.get_nics.return_value = []
response = self.app.get('redfish/v1/Systems/' + self.uuid + response = self.app.get('redfish/v1/Systems/' + self.uuid +
'/EthernetInterfaces') '/EthernetInterfaces')
@ -252,10 +248,9 @@ class EmulatorTestCase(base.BaseTestCase):
self.assertEqual([], response.json['Members']) self.assertEqual([], response.json['Members'])
def test_ethernet_interface(self, driver_mock): def test_ethernet_interface(self, driver_mock):
driver_mock.get_nics.return_value = [{'id': 'nic1', driver_mock.return_value.get_nics.return_value = [
'mac': '52:54:00:4e:5d:37'}, {'id': 'nic1', 'mac': '52:54:00:4e:5d:37'},
{'id': 'nic2', {'id': 'nic2', 'mac': '00:11:22:33:44:55'}]
'mac': '00:11:22:33:44:55'}]
response = self.app.get('/redfish/v1/Systems/' + self.uuid + response = self.app.get('/redfish/v1/Systems/' + self.uuid +
'/EthernetInterfaces/nic2') '/EthernetInterfaces/nic2')
@ -271,10 +266,9 @@ class EmulatorTestCase(base.BaseTestCase):
response.json['@odata.id']) response.json['@odata.id'])
def test_ethernet_interface_not_found(self, driver_mock): def test_ethernet_interface_not_found(self, driver_mock):
driver_mock.get_nics.return_value = [{'id': 'nic1', driver_mock.return_value.get_nics.return_value = [
'mac': '52:54:00:4e:5d:37'}, {'id': 'nic1', 'mac': '52:54:00:4e:5d:37'},
{'id': 'nic2', {'id': 'nic2', 'mac': '00:11:22:33:44:55'}]
'mac': '00:11:22:33:44:55'}]
response = self.app.get('/redfish/v1/Systems/' + self.uuid + response = self.app.get('/redfish/v1/Systems/' + self.uuid +
'/EthernetInterfaces/nic3') '/EthernetInterfaces/nic3')

View File

@ -0,0 +1,81 @@
# Copyright 2018 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 oslotest import base
from sushy_tools.emulator.drivers import memoize
class MemoizeTestCase(base.BaseTestCase):
def test_local_cache(self):
class Driver(object):
call_count = 0
@memoize.memoize()
def fun(self, *args, **kwargs):
self.call_count += 1
return args, kwargs
driver = Driver()
result = driver.fun(1, x=2)
self.assertEqual(((1,), {'x': 2}), result)
result = driver.fun(1, x=2)
self.assertEqual(((1,), {'x': 2}), result)
# Due to memoization, expect just one call
self.assertEqual(1, driver.call_count)
driver = Driver()
result = driver.fun(1, x=2)
self.assertEqual(((1,), {'x': 2}), result)
# Due to volatile cache, expect one more call
self.assertEqual(1, driver.call_count)
def test_external_cache(self):
permanent_cache = {}
class Driver(object):
call_count = 0
@memoize.memoize(permanent_cache=permanent_cache)
def fun(self, *args, **kwargs):
self.call_count += 1
return args, kwargs
driver = Driver()
result = driver.fun(1, x=2)
self.assertEqual(((1,), {'x': 2}), result)
result = driver.fun(1, x=2)
self.assertEqual(((1,), {'x': 2}), result)
# Due to memoization, expect just one call
self.assertEqual(1, driver.call_count)
driver = Driver()
result = driver.fun(1, x=2)
self.assertEqual(((1,), {'x': 2}), result)
# Due to permanent cache, expect no more calls
self.assertEqual(0, driver.call_count)

View File

@ -21,6 +21,7 @@ from sushy_tools.emulator.drivers.novadriver import OpenStackDriver
from sushy_tools import error from sushy_tools import error
@mock.patch.dict(OpenStackDriver.PERMANENT_CACHE)
class NovaDriverTestCase(base.BaseTestCase): class NovaDriverTestCase(base.BaseTestCase):
name = 'QEmu-fedora-i686' name = 'QEmu-fedora-i686'
@ -30,7 +31,8 @@ class NovaDriverTestCase(base.BaseTestCase):
self.nova_patcher = mock.patch('openstack.connect', autospec=True) self.nova_patcher = mock.patch('openstack.connect', autospec=True)
self.nova_mock = self.nova_patcher.start() self.nova_mock = self.nova_patcher.start()
self.test_driver = OpenStackDriver({}, 'fake-cloud') test_driver_class = OpenStackDriver.initialize({}, 'fake-cloud')
self.test_driver = test_driver_class()
super(NovaDriverTestCase, self).setUp() super(NovaDriverTestCase, self).setUp()
@ -145,6 +147,7 @@ class NovaDriverTestCase(base.BaseTestCase):
self.nova_mock.return_value.get_server.return_value = server self.nova_mock.return_value.get_server.return_value = server
image = mock.Mock(hw_firmware_type='bios') image = mock.Mock(hw_firmware_type='bios')
self.nova_mock.return_value.image.find_image.return_value = image self.nova_mock.return_value.image.find_image.return_value = image
boot_mode = self.test_driver.get_boot_mode(self.uuid) boot_mode = self.test_driver.get_boot_mode(self.uuid)
@ -216,7 +219,9 @@ class NovaDriverTestCase(base.BaseTestCase):
server = mock.Mock(id=self.uuid, addresses=addresses) server = mock.Mock(id=self.uuid, addresses=addresses)
self.nova_mock.return_value.get_server.return_value = server self.nova_mock.return_value.get_server.return_value = server
nics = self.test_driver.get_nics(self.uuid) nics = self.test_driver.get_nics(self.uuid)
self.assertEqual([{'id': 'fa:16:3e:22:18:31', self.assertEqual([{'id': 'fa:16:3e:22:18:31',
'mac': 'fa:16:3e:22:18:31'}, 'mac': 'fa:16:3e:22:18:31'},
{'id': 'fa:16:3e:46:e3:ac', {'id': 'fa:16:3e:46:e3:ac',