diff --git a/sushy_tools/emulator/drivers/base.py b/sushy_tools/emulator/drivers/base.py index e7f9ea4d..8b6ada52 100644 --- a/sushy_tools/emulator/drivers/base.py +++ b/sushy_tools/emulator/drivers/base.py @@ -21,6 +21,25 @@ import six class AbstractDriver(object): """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 def driver(self): """Return human-friendly driver information diff --git a/sushy_tools/emulator/drivers/libvirtdriver.py b/sushy_tools/emulator/drivers/libvirtdriver.py index b539f9e5..a96f2821 100644 --- a/sushy_tools/emulator/drivers/libvirtdriver.py +++ b/sushy_tools/emulator/drivers/libvirtdriver.py @@ -13,12 +13,13 @@ # License for the specific language governing permissions and limitations # under the License. +from collections import namedtuple import logging import uuid import xml.etree.ElementTree as ET -from collections import namedtuple from sushy_tools.emulator.drivers.base import AbstractDriver +from sushy_tools.emulator.drivers import memoize from sushy_tools import error try: @@ -102,15 +103,17 @@ class LibvirtDriver(AbstractDriver): "NicBoot1": "NetworkBoot", "ProcTurboMode": "Enabled"} - def __init__(self, config, uri=None): - self._config = config - self._uri = uri or self.LIBVIRT_URI - self.BOOT_LOADER_MAP = self._config.get( - 'SUSHY_EMULATOR_BOOT_LOADER_MAP', self.BOOT_LOADER_MAP) - self.KNOWN_BOOT_LOADERS = set(y - for x in self.BOOT_LOADER_MAP.values() - for y in x.values()) + @classmethod + def initialize(cls, config, uri=None): + cls._config = config + cls._uri = uri or cls.LIBVIRT_URI + cls.BOOT_LOADER_MAP = cls._config.get( + 'SUSHY_EMULATOR_BOOT_LOADER_MAP', cls.BOOT_LOADER_MAP) + cls.KNOWN_BOOT_LOADERS = set(y for x in cls.BOOT_LOADER_MAP.values() + for y in x.values()) + return cls + @memoize.memoize() def _get_domain(self, identity, readonly=False): with libvirt_open(self._uri, readonly=readonly) as conn: try: @@ -517,6 +520,7 @@ class LibvirtDriver(AbstractDriver): update_existing_attributes) if result.attributes_written: + try: with libvirt_open(self._uri) as conn: conn.defineXML(ET.tostring(result.tree).decode('utf-8')) diff --git a/sushy_tools/emulator/drivers/memoize.py b/sushy_tools/emulator/drivers/memoize.py new file mode 100644 index 00000000..135480b9 --- /dev/null +++ b/sushy_tools/emulator/drivers/memoize.py @@ -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 diff --git a/sushy_tools/emulator/drivers/novadriver.py b/sushy_tools/emulator/drivers/novadriver.py index 945edba3..4428e725 100644 --- a/sushy_tools/emulator/drivers/novadriver.py +++ b/sushy_tools/emulator/drivers/novadriver.py @@ -17,6 +17,7 @@ import logging import math from sushy_tools.emulator.drivers.base import AbstractDriver +from sushy_tools.emulator.drivers import memoize from sushy_tools import error try: @@ -51,11 +52,16 @@ class OpenStackDriver(AbstractDriver): BOOT_MODE_MAP_REV = {v: k for k, v in BOOT_MODE_MAP.items()} - def __init__(self, config, os_cloud, readonly=False): - self._cc = openstack.connect(cloud=os_cloud) - self._os_cloud = os_cloud - self._config = config + PERMANENT_CACHE = {} + @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): instance = self._cc.get_server(identity) if instance: @@ -72,10 +78,20 @@ class OpenStackDriver(AbstractDriver): raise error.FishyError(msg) + @memoize.memoize(permanent_cache=PERMANENT_CACHE) def _get_flavor(self, identity): instance = self._get_instance(identity) - flavor = self._cc.get_flavor(instance.flavor.id) - return flavor + return self._cc.get_flavor(instance.flavor.id) + + @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 def driver(self): @@ -190,7 +206,7 @@ class OpenStackDriver(AbstractDriver): except error.FishyError: 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 # libvirt-backed compute nodes @@ -224,7 +240,6 @@ class OpenStackDriver(AbstractDriver): # NOTE(etingof): the following probably only works with # libvirt-backed compute nodes - self._cc.compute.set_server_metadata( instance.id, **{'libvirt:pxe-first': '1' if target == 'network' else ''} @@ -238,7 +253,7 @@ class OpenStackDriver(AbstractDriver): """ 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) diff --git a/sushy_tools/emulator/main.py b/sushy_tools/emulator/main.py index f5eee3d5..57c5e161 100755 --- a/sushy_tools/emulator/main.py +++ b/sushy_tools/emulator/main.py @@ -14,6 +14,7 @@ # under the License. import argparse +import contextlib import functools import json import os @@ -32,22 +33,22 @@ app = flask.Flask(__name__) # Turn off strict_slashes on all routes app.url_map.strict_slashes = False -driver = None +DRIVER = None def init_virt_driver(decorated_func): @functools.wraps(decorated_func) def decorator(*args, **kwargs): - global driver + global DRIVER - if driver is None: + if DRIVER is None: if 'OS_CLOUD' in os.environ: if not novadriver.is_loaded: app.logger.error('Nova driver not loaded') sys.exit(1) - driver = novadriver.OpenStackDriver( + DRIVER = novadriver.OpenStackDriver.initialize( app.config, os.environ['OS_CLOUD']) else: @@ -55,7 +56,7 @@ def init_virt_driver(decorated_func): app.logger.error('libvirt driver not loaded') sys.exit(1) - driver = libvirtdriver.LibvirtDriver( + DRIVER = libvirtdriver.LibvirtDriver.initialize( app.config, os.environ.get( 'SUSHY_EMULATOR_LIBVIRT_URI', @@ -63,7 +64,7 @@ def init_virt_driver(decorated_func): 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) @@ -99,6 +100,11 @@ def ensure_instance_access(decorated_func): return decorator +@contextlib.contextmanager +def virt_driver(): + yield DRIVER() + + def returns_json(decorated_func): @functools.wraps(decorated_func) def decorator(*args, **kwargs): @@ -135,8 +141,9 @@ def root_resource(): @init_virt_driver @returns_json def system_collection_resource(): - systems = [system for system in driver.systems - if not instance_denied(identity=system)] + with virt_driver() as driver: + systems = [system for system in driver.systems + if not instance_denied(identity=system)] app.logger.debug('Serving systems list') @@ -153,46 +160,47 @@ def system_resource(identity): app.logger.debug('Serving resources for system "%s"', identity) - return flask.render_template( - 'system.json', - identity=identity, - name=driver.name(identity), - uuid=driver.uuid(identity), - power_state=driver.get_power_state(identity), - total_memory_gb=driver.get_total_memory(identity), - total_cpus=driver.get_total_cpus(identity), - boot_source_target=driver.get_boot_device(identity), - boot_source_mode=driver.get_boot_mode(identity) - ) + with virt_driver() as driver: + return flask.render_template( + 'system.json', identity=identity, + uuid=driver.uuid(identity), + power_state=driver.get_power_state(identity), + total_memory_gb=driver.get_total_memory(identity), + total_cpus=driver.get_total_cpus(identity), + boot_source_target=driver.get_boot_device(identity), + boot_source_mode=driver.get_boot_mode(identity) + ) elif flask.request.method == 'PATCH': boot = flask.request.json.get('Boot') if not boot: return 'PATCH only works for the Boot element', 400 - target = boot.get('BootSourceOverrideTarget') + with virt_driver() as driver: - if target: - # NOTE(lucasagomes): In libvirt we always set the boot - # device frequency to "continuous" so, we are ignoring the - # BootSourceOverrideEnabled element here + target = boot.get('BootSourceOverrideTarget') - 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"', - target, identity) + driver.set_boot_device(identity, target) - mode = boot.get('BootSourceOverrideMode') + app.logger.info('Set boot device to "%s" for system "%s"', + target, identity) - if mode: - driver.set_boot_mode(identity, mode) + mode = boot.get('BootSourceOverrideMode') - app.logger.info('Set boot mode to "%s" for system "%s"', - mode, identity) + if mode: + driver.set_boot_mode(identity, mode) - if not target and not mode: - return ('Missing the BootSourceOverrideTarget and/or ' - 'BootSourceOverrideMode element', 400) + app.logger.info('Set boot mode to "%s" for system "%s"', + mode, identity) + + if not target and not mode: + return ('Missing the BootSourceOverrideTarget and/or ' + 'BootSourceOverrideMode element', 400) return '', 204 @@ -203,7 +211,9 @@ def system_resource(identity): @ensure_instance_access @returns_json def ethernet_interfaces_collection(identity): - nics = driver.get_nics(identity) + with virt_driver() as driver: + nics = driver.get_nics(identity) + return flask.render_template( 'ethernet_interfaces_collection.json', identity=identity, nics=nics) @@ -215,7 +225,9 @@ def ethernet_interfaces_collection(identity): @ensure_instance_access @returns_json 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: if nic['id'] == nic_id: return flask.render_template( @@ -232,7 +244,8 @@ def ethernet_interface(identity, nic_id): def system_reset_action(identity): 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"', identity, reset_type) @@ -245,7 +258,8 @@ def system_reset_action(identity): @ensure_instance_access @returns_json 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) @@ -263,7 +277,8 @@ def bios(identity): def bios_settings(identity): 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) @@ -275,7 +290,9 @@ def bios_settings(identity): elif flask.request.method == 'PATCH': 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', identity, attributes) return '', 204 @@ -288,8 +305,11 @@ def bios_settings(identity): @returns_json 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) + return '', 204 @@ -336,7 +356,7 @@ def parse_args(): def main(): - global driver + global DRIVER args = parse_args() @@ -350,20 +370,21 @@ def main(): app.logger.error('Nova driver not loaded') return 1 - driver = novadriver.OpenStackDriver(app.config, os_cloud) + DRIVER = novadriver.OpenStackDriver.initialize( + app.config, args.os_cloud) else: if not libvirtdriver.is_loaded: app.logger.error('libvirt driver not loaded') return 1 - driver = libvirtdriver.LibvirtDriver( + DRIVER = libvirtdriver.LibvirtDriver.initialize( app.config, args.libvirt_uri or 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 diff --git a/sushy_tools/tests/unit/emulator/test_libvirt.py b/sushy_tools/tests/unit/emulator/test_libvirt.py index 998ab48a..d2440e68 100644 --- a/sushy_tools/tests/unit/emulator/test_libvirt.py +++ b/sushy_tools/tests/unit/emulator/test_libvirt.py @@ -25,7 +25,8 @@ class LibvirtDriverTestCase(base.BaseTestCase): uuid = 'c7a5fdbd-cdaf-9455-926a-d65c16db1809' def setUp(self): - self.test_driver = LibvirtDriver({}) + test_driver_class = LibvirtDriver.initialize({}) + self.test_driver = test_driver_class() super(LibvirtDriverTestCase, self).setUp() @mock.patch('libvirt.open', autospec=True) diff --git a/sushy_tools/tests/unit/emulator/test_main.py b/sushy_tools/tests/unit/emulator/test_main.py index e9ded997..a5df7dad 100644 --- a/sushy_tools/tests/unit/emulator/test_main.py +++ b/sushy_tools/tests/unit/emulator/test_main.py @@ -16,23 +16,24 @@ from six.moves import mock 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): name = 'QEmu-fedora-i686' uuid = 'c7a5fdbd-cdaf-9455-926a-d65c16db1809' def setUp(self): - main.driver = None + main.DRIVER = None self.app = main.app.test_client() super(EmulatorTestCase, self).setUp() 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) - self.assertEqual('500 INTERNAL SERVER ERROR', response.status) + self.assertEqual(500, response.status_code) def test_root_resource(self, driver_mock): response = self.app.get('/redfish/v1/') @@ -40,7 +41,7 @@ class EmulatorTestCase(base.BaseTestCase): self.assertEqual('RedvirtService', response.json['Id']) 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']) response = self.app.get('/redfish/v1/Systems') self.assertEqual(200, response.status_code) @@ -50,12 +51,12 @@ class EmulatorTestCase(base.BaseTestCase): response.json['Members'][1]) def test_system_resource_get(self, driver_mock): - driver_mock.uuid.return_value = 'zzzz-yyyy-xxxx' - driver_mock.get_power_state.return_value = 'On' - driver_mock.get_total_memory.return_value = 1 - driver_mock.get_total_cpus.return_value = 2 - driver_mock.get_boot_device.return_value = 'Cd' - driver_mock.get_boot_mode.return_value = 'Legacy' + driver_mock.return_value.uuid.return_value = 'zzzz-yyyy-xxxx' + driver_mock.return_value.get_power_state.return_value = 'On' + driver_mock.return_value.get_total_memory.return_value = 1 + driver_mock.return_value.get_total_cpus.return_value = 2 + driver_mock.return_value.get_boot_device.return_value = 'Cd' + driver_mock.return_value.get_boot_mode.return_value = 'Legacy' 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', json=data) 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') 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', json=data) 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') 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', json=data) 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') 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', json=data) 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') 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', json=data) 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', 'GracefulShutdown') @@ -122,7 +123,7 @@ class EmulatorTestCase(base.BaseTestCase): '/redfish/v1/Systems/xxxx-yyyy-zzzz/Actions/ComputerSystem.Reset', json=data) 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', 'GracefulRestart') @@ -132,7 +133,7 @@ class EmulatorTestCase(base.BaseTestCase): '/redfish/v1/Systems/xxxx-yyyy-zzzz/Actions/ComputerSystem.Reset', json=data) 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', 'ForceRestart') @@ -142,7 +143,7 @@ class EmulatorTestCase(base.BaseTestCase): '/redfish/v1/Systems/xxxx-yyyy-zzzz/Actions/ComputerSystem.Reset', json=data) 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') @mock.patch.dict(main.app.config, {}, clear=True) @@ -170,19 +171,20 @@ class EmulatorTestCase(base.BaseTestCase): self.assertTrue(main.instance_denied(identity='b')) def test_get_bios(self, driver_mock): - driver_mock.get_bios.return_value = {"attribute 1": "value 1", - "attribute 2": "value 2"} + driver_mock.return_value.get_bios.return_value = { + "attribute 1": "value 1", + "attribute 2": "value 2"} 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({"attribute 1": "value 1", "attribute 2": "value 2"}, response.json['Attributes']) def test_get_bios_existing(self, driver_mock): - driver_mock.get_bios.return_value = {"attribute 1": "value 1", - "attribute 2": "value 2"} + driver_mock.return_value.get_bios.return_value = { + "attribute 1": "value 1", "attribute 2": "value 2"} response = self.app.get( '/redfish/v1/Systems/' + self.uuid + '/BIOS/Settings') @@ -194,39 +196,33 @@ class EmulatorTestCase(base.BaseTestCase): def test_bios_settings_patch(self, driver_mock): data = {'Attributes': {'key': 'value'}} - self.app.driver = driver_mock response = self.app.patch( '/redfish/v1/Systems/xxxx-yyyy-zzzz/BIOS/Settings', json=data) - self.assertEqual(204, response.status_code) - driver_mock.set_bios.assert_called_once_with('xxxx-yyyy-zzzz', - {'key': 'value'}) + driver_mock.return_value.set_bios.assert_called_once_with( + 'xxxx-yyyy-zzzz', {'key': 'value'}) def test_set_bios(self, driver_mock): data = {'Attributes': {'key': 'value'}} - self.app.driver = driver_mock response = self.app.patch( '/redfish/v1/Systems/xxxx-yyyy-zzzz/BIOS/Settings', json=data) 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']) def test_reset_bios(self, driver_mock): - self.app.driver = driver_mock response = self.app.post('/redfish/v1/Systems/' + self.uuid + '/BIOS/Actions/Bios.ResetBios') - 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): - driver_mock.get_nics.return_value = [{'id': 'nic1', - 'mac': '52:54:00:4e:5d:37'}, - {'id': 'nic2', - 'mac': '00:11:22:33:44:55'}] + driver_mock.return_value.get_nics.return_value = [ + {'id': 'nic1', 'mac': '52:54:00:4e:5d:37'}, + {'id': 'nic2', 'mac': '00:11:22:33:44:55'}] response = self.app.get('redfish/v1/Systems/' + self.uuid + '/EthernetInterfaces') @@ -241,7 +237,7 @@ class EmulatorTestCase(base.BaseTestCase): [m['@odata.id'] for m in response.json['Members']]) 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 + '/EthernetInterfaces') @@ -252,10 +248,9 @@ class EmulatorTestCase(base.BaseTestCase): self.assertEqual([], response.json['Members']) def test_ethernet_interface(self, driver_mock): - driver_mock.get_nics.return_value = [{'id': 'nic1', - 'mac': '52:54:00:4e:5d:37'}, - {'id': 'nic2', - 'mac': '00:11:22:33:44:55'}] + driver_mock.return_value.get_nics.return_value = [ + {'id': 'nic1', 'mac': '52:54:00:4e:5d:37'}, + {'id': 'nic2', 'mac': '00:11:22:33:44:55'}] response = self.app.get('/redfish/v1/Systems/' + self.uuid + '/EthernetInterfaces/nic2') @@ -271,10 +266,9 @@ class EmulatorTestCase(base.BaseTestCase): response.json['@odata.id']) def test_ethernet_interface_not_found(self, driver_mock): - driver_mock.get_nics.return_value = [{'id': 'nic1', - 'mac': '52:54:00:4e:5d:37'}, - {'id': 'nic2', - 'mac': '00:11:22:33:44:55'}] + driver_mock.return_value.get_nics.return_value = [ + {'id': 'nic1', 'mac': '52:54:00:4e:5d:37'}, + {'id': 'nic2', 'mac': '00:11:22:33:44:55'}] response = self.app.get('/redfish/v1/Systems/' + self.uuid + '/EthernetInterfaces/nic3') diff --git a/sushy_tools/tests/unit/emulator/test_memoize.py b/sushy_tools/tests/unit/emulator/test_memoize.py new file mode 100644 index 00000000..dcf490ee --- /dev/null +++ b/sushy_tools/tests/unit/emulator/test_memoize.py @@ -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) diff --git a/sushy_tools/tests/unit/emulator/test_nova.py b/sushy_tools/tests/unit/emulator/test_nova.py index e6f2a40d..34d69511 100644 --- a/sushy_tools/tests/unit/emulator/test_nova.py +++ b/sushy_tools/tests/unit/emulator/test_nova.py @@ -21,6 +21,7 @@ from sushy_tools.emulator.drivers.novadriver import OpenStackDriver from sushy_tools import error +@mock.patch.dict(OpenStackDriver.PERMANENT_CACHE) class NovaDriverTestCase(base.BaseTestCase): name = 'QEmu-fedora-i686' @@ -30,7 +31,8 @@ class NovaDriverTestCase(base.BaseTestCase): self.nova_patcher = mock.patch('openstack.connect', autospec=True) 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() @@ -145,6 +147,7 @@ class NovaDriverTestCase(base.BaseTestCase): self.nova_mock.return_value.get_server.return_value = server image = mock.Mock(hw_firmware_type='bios') + self.nova_mock.return_value.image.find_image.return_value = image 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) self.nova_mock.return_value.get_server.return_value = server + nics = self.test_driver.get_nics(self.uuid) + self.assertEqual([{'id': 'fa:16:3e:22:18:31', 'mac': 'fa:16:3e:22:18:31'}, {'id': 'fa:16:3e:46:e3:ac',