Allow use of multiple simultaneous HW managers

Currently we pick the most specific manager and use it. Instead, call
each method on each hardware manager in priority order, and consider the
call successful if the method exists and doesn't throw
IncompatibleHardwareMethodError.

This is an API breaking change for anyone with out-of-tree
HardwareManagers.

Closes-bug: 1408469
Change-Id: I30c65c9259acd4f200cb554e7d688344b7486a58
This commit is contained in:
Jay Faulkner 2014-12-19 13:24:21 -08:00
parent 8dd54446e3
commit 2bbec5770c
11 changed files with 326 additions and 95 deletions

@ -70,7 +70,6 @@ class IronicPythonAgentHeartbeater(threading.Thread):
"""
super(IronicPythonAgentHeartbeater, self).__init__()
self.agent = agent
self.hardware = hardware.get_manager()
self.api = ironic_api_client.APIClient(agent.api_url,
agent.driver_name)
self.log = log.getLogger(__name__)
@ -156,7 +155,6 @@ class IronicPythonAgent(base.ExecuteCommandMixin):
self.api = app.VersionSelectorApplication(self)
self.heartbeater = IronicPythonAgentHeartbeater(self)
self.heartbeat_timeout = None
self.hardware = hardware.get_manager()
self.log = log.getLogger(__name__)
self.started_at = None
self.node = None
@ -201,7 +199,8 @@ class IronicPythonAgent(base.ExecuteCommandMixin):
attempts = 0
while (attempts < self.ip_lookup_attempts):
for iface in ifaces:
found_ip = self.hardware.get_ipv4_addr(iface)
found_ip = hardware.dispatch_to_managers('get_ipv4_addr',
iface)
if found_ip is not None:
self.advertise_address = (found_ip,
self.advertise_address[1])
@ -223,7 +222,7 @@ class IronicPythonAgent(base.ExecuteCommandMixin):
be found.
"""
iface_list = [iface.serialize()['name'] for iface in
self.hardware.list_network_interfaces()]
hardware.dispatch_to_managers('list_network_interfaces')]
iface_list = [name for name in iface_list if 'lo' not in name]
if len(iface_list) == 0:
@ -278,7 +277,8 @@ class IronicPythonAgent(base.ExecuteCommandMixin):
self.started_at = _time()
if not self.standalone:
content = self.api_client.lookup_node(
hardware_info=self.hardware.list_hardware_info(),
hardware_info=hardware.dispatch_to_managers(
'list_hardware_info'),
timeout=self.lookup_timeout,
starting_interval=self.lookup_interval)

@ -247,3 +247,43 @@ class UnknownNodeError(Exception):
if message is not None:
self.message = message
super(UnknownNodeError, self).__init__(self.message)
class HardwareManagerNotFound(Exception):
"""Error raised when no valid HardwareManager can be found."""
message = 'No valid HardwareManager found.'
def __init__(self, message=None):
if message is not None:
self.message = message
super(HardwareManagerNotFound, self).__init__(self.message)
class HardwareManagerMethodNotFound(RESTError):
"""Error raised when all HardwareManagers fail to handle a method."""
msg = 'No HardwareManager found to handle method'
message = msg + '.'
def __init__(self, method=None):
if method is not None:
self.details = (self.msg + ': "{0}".').format(method)
else:
self.details = self.message
super(HardwareManagerMethodNotFound, self).__init__(self.details)
class IncompatibleHardwareMethodError(RESTError):
"""Error raised when HardwareManager method is incompatible with node
hardware.
"""
message = 'HardwareManager method is not compatible with hardware.'
def __init__(self, details=None):
if details is not None:
self.details = details
else:
self.details = self.message
super(IncompatibleHardwareMethodError, self).__init__(self.details)

@ -19,4 +19,4 @@ from ironic_python_agent import hardware
class DecomExtension(base.BaseAgentExtension):
@base.async_command('erase_hardware')
def erase_hardware(self):
hardware.get_manager().erase_devices()
hardware.dispatch_to_managers('erase_devices')

@ -193,7 +193,7 @@ class StandbyExtension(base.BaseAgentExtension):
@base.async_command('cache_image', _validate_image_info)
def cache_image(self, image_info=None, force=False):
device = hardware.get_manager().get_os_install_device()
device = hardware.dispatch_to_managers('get_os_install_device')
if self.cached_image_id != image_info['id'] or force:
_download_image(image_info)
@ -204,7 +204,7 @@ class StandbyExtension(base.BaseAgentExtension):
def prepare_image(self,
image_info=None,
configdrive=None):
device = hardware.get_manager().get_os_install_device()
device = hardware.dispatch_to_managers('get_os_install_device')
# don't write image again if already cached
if self.cached_image_id != image_info['id']:

@ -27,15 +27,20 @@ from ironic_python_agent import errors
from ironic_python_agent.openstack.common import log
from ironic_python_agent import utils
_global_manager = None
_global_managers = None
LOG = log.getLogger()
class HardwareSupport(object):
"""These are just guidelines to suggest values that might be returned by
calls to `evaluate_hardware_support`. No HardwareManager in mainline
ironic-python-agent will ever offer a value greater than `MAINLINE`.
Service Providers should feel free to return values greater than
SERVICE_PROVIDER to distinguish between additional levels of support.
"""Example priorities for hardware managers.
Priorities for HardwareManagers are integers, where largest means most
specific and smallest means most generic. These values are guidelines
that suggest values that might be returned by calls to
`evaluate_hardware_support()`. No HardwareManager in mainline IPA will
ever return a value greater than MAINLINE. Third party hardware managers
should feel free to return values of SERVICE_PROVIDER or greater to
distinguish between additional levels of hardware support.
"""
NONE = 0
GENERIC = 1
@ -91,27 +96,21 @@ class HardwareManager(object):
def evaluate_hardware_support(self):
pass
@abc.abstractmethod
def list_network_interfaces(self):
pass
raise errors.IncompatibleHardwareMethodError
@abc.abstractmethod
def get_cpus(self):
pass
raise errors.IncompatibleHardwareMethodError
@abc.abstractmethod
def list_block_devices(self):
pass
raise errors.IncompatibleHardwareMethodError
@abc.abstractmethod
def get_memory(self):
pass
raise errors.IncompatibleHardwareMethodError
@abc.abstractmethod
def get_os_install_device(self):
pass
raise errors.IncompatibleHardwareMethodError
@abc.abstractmethod
def erase_block_device(self, block_device):
"""Attempt to erase a block device.
@ -129,11 +128,12 @@ class HardwareManager(object):
encouraged.
:param block_device: a BlockDevice indicating a device to be erased.
:raises: BlockDeviceEraseError when an error occurs erasing a block
device, or if the block device is not supported.
:raises IncompatibleHardwareMethodError: when there is no known way to
erase the block device
:raises BlockDeviceEraseError: when there is an error erasing the
block device
"""
pass
raise errors.IncompatibleHardwareMethodError
def erase_devices(self):
"""Erase any device that holds user data.
@ -270,10 +270,10 @@ class GenericHardwareManager(HardwareManager):
if self._ata_erase(block_device):
return
# NOTE(russell_h): Support for additional generic erase methods should
# be added above this raise, in order of precedence.
raise errors.BlockDeviceEraseError(('Unable to erase block device '
'{0}: device is unsupported.').format(block_device.name))
msg = ('Unable to erase block device {0}: device is unsupported.'
).format(block_device.name)
LOG.error(msg)
raise errors.IncompatibleHardwareMethodError(msg)
def _get_ata_security_lines(self, block_device):
output = utils.execute('hdparm', '-I', block_device.name)[0]
@ -332,11 +332,19 @@ def _compare_extensions(ext1, ext2):
return mgr2.evaluate_hardware_support() - mgr1.evaluate_hardware_support()
def get_manager():
global _global_manager
def _get_managers():
"""Get a list of hardware managers in priority order.
if not _global_manager:
LOG = log.getLogger()
Use stevedore to find all eligible hardware managers, sort them based on
self-reported (via evaluate_hardware_support()) priorities, and return them
in a list. The resulting list is cached in _global_managers.
:returns: Priority-sorted list of hardware managers
:raises HardwareManagerNotFound: if no valid hardware managers found
"""
global _global_managers
if not _global_managers:
extension_manager = stevedore.ExtensionManager(
namespace='ironic_python_agent.hardware_managers',
invoke_on_load=True)
@ -344,22 +352,54 @@ def get_manager():
# There will always be at least one extension available (the
# GenericHardwareManager).
if six.PY2:
preferred_extension = sorted(
extension_manager,
_compare_extensions)[0]
extensions = sorted(extension_manager, _compare_extensions)
else:
preferred_extension = sorted(
extension_manager,
key=functools.cmp_to_key(_compare_extensions))[0]
extensions = sorted(extension_manager,
key=functools.cmp_to_key(_compare_extensions))
preferred_manager = preferred_extension.obj
preferred_managers = []
if preferred_manager.evaluate_hardware_support() <= 0:
raise RuntimeError('No suitable HardwareManager could be found')
for extension in extensions:
if extension.obj.evaluate_hardware_support() > 0:
preferred_managers.append(extension.obj)
LOG.info('Hardware manager found: {0}'.format(
extension.entry_point_target))
LOG.info('selected hardware manager {0}'.format(
preferred_extension.entry_point_target))
if not preferred_managers:
raise errors.HardwareManagerNotFound
_global_manager = preferred_manager
_global_managers = preferred_managers
return _global_manager
return _global_managers
def dispatch_to_managers(method, *args, **kwargs):
"""Dispatch a method to best suited hardware manager.
Dispatches the given method in priority order as sorted by
`_get_managers`. If the method doesn't exist or raises
IncompatibleHardwareMethodError, it is attempted again with a more generic
hardware manager. This continues until a method executes that returns
any result without raising an IncompatibleHardwareMethodError.
:param method: hardware manager method to dispatch
:param *args: arguments to dispatched method
:param **kwargs: keyword arguments to dispatched method
:returns: result of successful dispatch of method
:raises HardwareManagerMethodNotFound: if all managers failed the method
:raises HardwareManagerNotFound: if no valid hardware managers found
"""
managers = _get_managers()
for manager in managers:
if getattr(manager, method, None):
try:
return getattr(manager, method)(*args, **kwargs)
except(errors.IncompatibleHardwareMethodError):
LOG.debug('HardwareManager {0} does not support {1}'
.format(manager, method))
else:
LOG.debug('HardwareManager {0} does not have method {1}'
.format(manager, method))
raise errors.HardwareManagerMethodNotFound(method)

@ -201,7 +201,15 @@ class TestBaseAgent(test_base.BaseTestCase):
@mock.patch('os.read')
@mock.patch('select.poll')
@mock.patch('time.sleep', return_value=None)
def test_ipv4_lookup(self, mock_time_sleep, mock_poll, mock_read):
@mock.patch.object(hardware.GenericHardwareManager,
'list_network_interfaces')
@mock.patch.object(hardware.GenericHardwareManager, 'get_ipv4_addr')
def test_ipv4_lookup(self,
mock_get_ipv4,
mock_list_net,
mock_time_sleep,
mock_poll,
mock_read):
homeless_agent = agent.IronicPythonAgent('https://fake_api.example.'
'org:8081/',
(None, 9990),
@ -214,10 +222,6 @@ class TestBaseAgent(test_base.BaseTestCase):
'agent_ipmitool',
False)
homeless_agent.hardware = mock.Mock()
mock_list_net = homeless_agent.hardware.list_network_interfaces
mock_get_ipv4 = homeless_agent.hardware.get_ipv4_addr
mock_poll.return_value.poll.return_value = True
mock_read.return_value = 'a'

@ -23,9 +23,9 @@ class TestDecomExtension(test_base.BaseTestCase):
super(TestDecomExtension, self).setUp()
self.agent_extension = decom.DecomExtension()
@mock.patch('ironic_python_agent.hardware.get_manager', autospec=True)
def test_erase_hardware(self, mocked_get_manager):
hardware_manager = mocked_get_manager.return_value
@mock.patch('ironic_python_agent.hardware.dispatch_to_managers',
autospec=True)
def test_erase_devices(self, mocked_dispatch):
result = self.agent_extension.erase_hardware()
result.join()
hardware_manager.erase_devices.assert_called_once_with()
mocked_dispatch.assert_called_once_with('erase_devices')

@ -265,66 +265,70 @@ class TestStandbyExtension(test_base.BaseTestCase):
self.assertFalse(verified)
self.assertEqual(md5_mock.call_count, 1)
@mock.patch('ironic_python_agent.hardware.get_manager', autospec=True)
@mock.patch('ironic_python_agent.hardware.dispatch_to_managers',
autospec=True)
@mock.patch('ironic_python_agent.extensions.standby._write_image',
autospec=True)
@mock.patch('ironic_python_agent.extensions.standby._download_image',
autospec=True)
def test_cache_image(self, download_mock, write_mock, hardware_mock):
def test_cache_image(self, download_mock, write_mock, dispatch_mock):
image_info = self._build_fake_image_info()
download_mock.return_value = None
write_mock.return_value = None
manager_mock = hardware_mock.return_value
manager_mock.get_os_install_device.return_value = 'manager'
dispatch_mock.return_value = 'manager'
async_result = self.agent_extension.cache_image(image_info=image_info)
async_result.join()
download_mock.assert_called_once_with(image_info)
write_mock.assert_called_once_with(image_info, 'manager')
dispatch_mock.assert_called_once_with('get_os_install_device')
self.assertEqual(self.agent_extension.cached_image_id,
image_info['id'])
self.assertEqual('SUCCEEDED', async_result.command_status)
self.assertEqual(None, async_result.command_result)
@mock.patch('ironic_python_agent.hardware.get_manager', autospec=True)
@mock.patch('ironic_python_agent.hardware.dispatch_to_managers',
autospec=True)
@mock.patch('ironic_python_agent.extensions.standby._write_image',
autospec=True)
@mock.patch('ironic_python_agent.extensions.standby._download_image',
autospec=True)
def test_cache_image_force(self, download_mock, write_mock, hardware_mock):
def test_cache_image_force(self, download_mock, write_mock,
dispatch_mock):
image_info = self._build_fake_image_info()
self.agent_extension.cached_image_id = image_info['id']
download_mock.return_value = None
write_mock.return_value = None
manager_mock = hardware_mock.return_value
manager_mock.get_os_install_device.return_value = 'manager'
dispatch_mock.return_value = 'manager'
async_result = self.agent_extension.cache_image(
image_info=image_info, force=True
)
async_result.join()
download_mock.assert_called_once_with(image_info)
write_mock.assert_called_once_with(image_info, 'manager')
dispatch_mock.assert_called_once_with('get_os_install_device')
self.assertEqual(self.agent_extension.cached_image_id,
image_info['id'])
self.assertEqual('SUCCEEDED', async_result.command_status)
self.assertEqual(None, async_result.command_result)
@mock.patch('ironic_python_agent.hardware.get_manager', autospec=True)
@mock.patch('ironic_python_agent.hardware.dispatch_to_managers',
autospec=True)
@mock.patch('ironic_python_agent.extensions.standby._write_image',
autospec=True)
@mock.patch('ironic_python_agent.extensions.standby._download_image',
autospec=True)
def test_cache_image_cached(self, download_mock, write_mock,
hardware_mock):
dispatch_mock):
image_info = self._build_fake_image_info()
self.agent_extension.cached_image_id = image_info['id']
download_mock.return_value = None
write_mock.return_value = None
manager_mock = hardware_mock.return_value
manager_mock.get_os_install_device.return_value = 'manager'
dispatch_mock.return_value = 'manager'
async_result = self.agent_extension.cache_image(image_info=image_info)
async_result.join()
self.assertFalse(download_mock.called)
self.assertFalse(write_mock.called)
dispatch_mock.assert_called_once_with('get_os_install_device')
self.assertEqual(self.agent_extension.cached_image_id,
image_info['id'])
self.assertEqual('SUCCEEDED', async_result.command_status)
@ -333,7 +337,8 @@ class TestStandbyExtension(test_base.BaseTestCase):
@mock.patch(('ironic_python_agent.extensions.standby.'
'_write_configdrive_to_partition'),
autospec=True)
@mock.patch('ironic_python_agent.hardware.get_manager', autospec=True)
@mock.patch('ironic_python_agent.hardware.dispatch_to_managers',
autospec=True)
@mock.patch('ironic_python_agent.extensions.standby._write_image',
autospec=True)
@mock.patch('ironic_python_agent.extensions.standby._download_image',
@ -344,14 +349,13 @@ class TestStandbyExtension(test_base.BaseTestCase):
location_mock,
download_mock,
write_mock,
hardware_mock,
dispatch_mock,
configdrive_copy_mock):
image_info = self._build_fake_image_info()
location_mock.return_value = '/tmp/configdrive'
download_mock.return_value = None
write_mock.return_value = None
manager_mock = hardware_mock.return_value
manager_mock.get_os_install_device.return_value = 'manager'
dispatch_mock.return_value = 'manager'
configdrive_copy_mock.return_value = None
async_result = self.agent_extension.prepare_image(
@ -362,6 +366,7 @@ class TestStandbyExtension(test_base.BaseTestCase):
download_mock.assert_called_once_with(image_info)
write_mock.assert_called_once_with(image_info, 'manager')
dispatch_mock.assert_called_once_with('get_os_install_device')
configdrive_copy_mock.assert_called_once_with('configdrive_data',
'manager')
@ -389,7 +394,8 @@ class TestStandbyExtension(test_base.BaseTestCase):
@mock.patch(('ironic_python_agent.extensions.standby.'
'_write_configdrive_to_partition'),
autospec=True)
@mock.patch('ironic_python_agent.hardware.get_manager', autospec=True)
@mock.patch('ironic_python_agent.hardware.dispatch_to_managers',
autospec=True)
@mock.patch('ironic_python_agent.extensions.standby._write_image',
autospec=True)
@mock.patch('ironic_python_agent.extensions.standby._download_image',
@ -397,13 +403,12 @@ class TestStandbyExtension(test_base.BaseTestCase):
def test_prepare_image_no_configdrive(self,
download_mock,
write_mock,
hardware_mock,
dispatch_mock,
configdrive_copy_mock):
image_info = self._build_fake_image_info()
download_mock.return_value = None
write_mock.return_value = None
manager_mock = hardware_mock.return_value
manager_mock.get_os_install_device.return_value = 'manager'
dispatch_mock.return_value = 'manager'
configdrive_copy_mock.return_value = None
async_result = self.agent_extension.prepare_image(
@ -414,6 +419,7 @@ class TestStandbyExtension(test_base.BaseTestCase):
download_mock.assert_called_once_with(image_info)
write_mock.assert_called_once_with(image_info, 'manager')
dispatch_mock.assert_called_once_with('get_os_install_device')
self.assertEqual(configdrive_copy_mock.call_count, 0)
self.assertEqual('SUCCEEDED', async_result.command_status)

@ -157,17 +157,6 @@ class TestHardwareManagerLoading(test_base.BaseTestCase):
ext1, ext2, ext3
])
@mock.patch('stevedore.ExtensionManager')
def test_hardware_manager_loading(self, mocked_extension_mgr_constructor):
hardware._global_manager = None
mocked_extension_mgr_constructor.return_value = self.fake_ext_mgr
preferred_hw_manager = hardware.get_manager()
mocked_extension_mgr_constructor.assert_called_once_with(
namespace='ironic_python_agent.hardware_managers',
invoke_on_load=True)
self.assertEqual(self.correct_hw_manager, preferred_hw_manager)
class TestGenericHardwareManager(test_base.BaseTestCase):
def setUp(self):
@ -355,7 +344,7 @@ class TestGenericHardwareManager(test_base.BaseTestCase):
])
@mock.patch.object(utils, 'execute')
def test_erase_block_device_ata_nosecurtiy(self, mocked_execute):
def test_erase_block_device_ata_nosecurity(self, mocked_execute):
hdparm_output = HDPARM_INFO_TEMPLATE.split('\nSecurity:')[0]
mocked_execute.side_effect = [
@ -364,7 +353,7 @@ class TestGenericHardwareManager(test_base.BaseTestCase):
block_device = hardware.BlockDevice('/dev/sda', 'big', 1073741824,
True)
self.assertRaises(errors.BlockDeviceEraseError,
self.assertRaises(errors.IncompatibleHardwareMethodError,
self.hardware.erase_block_device,
block_device)
@ -382,7 +371,7 @@ class TestGenericHardwareManager(test_base.BaseTestCase):
block_device = hardware.BlockDevice('/dev/sda', 'big', 1073741824,
True)
self.assertRaises(errors.BlockDeviceEraseError,
self.assertRaises(errors.IncompatibleHardwareMethodError,
self.hardware.erase_block_device,
block_device)

@ -0,0 +1,154 @@
# Copyright 2013 Rackspace, Inc.
#
# 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 mock
from oslotest import base as test_base
from stevedore import extension
from ironic_python_agent import errors
from ironic_python_agent import hardware
def counted(fn):
def wrapper(*args, **kwargs):
wrapper.called += 1
return fn(*args, **kwargs)
wrapper.called = 0
wrapper.__name__ = fn.__name__
return wrapper
class FakeGenericHardwareManager(hardware.HardwareManager):
@counted
def generic_only(self):
return True
@counted
def specific_only(self):
raise Exception("Test fail: This method should not be called")
@counted
def mainline_fail(self):
return True
@counted
def both_succeed(self):
return True
@counted
def unexpected_fail(self):
raise Exception("Test fail: This method should not be called")
@counted
def evaluate_hardware_support(self):
return hardware.HardwareSupport.GENERIC
class FakeMainlineHardwareManager(hardware.HardwareManager):
@counted
def specific_only(self):
return True
@counted
def mainline_fail(self):
raise errors.IncompatibleHardwareMethodError
@counted
def both_succeed(self):
return True
@counted
def unexpected_fail(self):
raise RuntimeError('A problem was encountered')
@counted
def evaluate_hardware_support(self):
return hardware.HardwareSupport.MAINLINE
class TestMultipleHardwareManagerLoading(test_base.BaseTestCase):
def setUp(self):
super(TestMultipleHardwareManagerLoading, self).setUp()
fake_ep = mock.Mock()
fake_ep.module_name = 'fake'
fake_ep.attrs = ['fake attrs']
ext1 = extension.Extension('fake_generic', fake_ep, None,
FakeGenericHardwareManager())
ext2 = extension.Extension('fake_mainline', fake_ep, None,
FakeMainlineHardwareManager())
self.fake_ext_mgr = extension.ExtensionManager.make_test_instance([
ext1, ext2
])
self.extension_mgr_patcher = mock.patch('stevedore.ExtensionManager')
self.mocked_extension_mgr = self.extension_mgr_patcher.start()
self.mocked_extension_mgr.return_value = self.fake_ext_mgr
hardware._global_managers = None
def tearDown(self):
super(TestMultipleHardwareManagerLoading, self).tearDown()
self.extension_mgr_patcher.stop()
def test_mainline_method_only(self):
hardware.dispatch_to_managers('specific_only')
self.assertEqual(1, FakeMainlineHardwareManager.specific_only.called)
def test_generic_method_only(self):
hardware.dispatch_to_managers('generic_only')
self.assertEqual(1, FakeGenericHardwareManager.generic_only.called)
def test_both_succeed(self):
"""In the case where both managers will work; only the most specific
manager should have it's function called.
"""
hardware.dispatch_to_managers('both_succeed')
self.assertEqual(1, FakeMainlineHardwareManager.both_succeed.called)
self.assertEqual(0, FakeGenericHardwareManager.both_succeed.called)
def test_mainline_fails(self):
"""Ensure that if the mainline manager is unable to run the method
that we properly fall back to generic.
"""
hardware.dispatch_to_managers('mainline_fail')
self.assertEqual(1, FakeMainlineHardwareManager.mainline_fail.called)
self.assertEqual(1, FakeGenericHardwareManager.mainline_fail.called)
def test_manager_method_not_found(self):
self.assertRaises(errors.HardwareManagerMethodNotFound,
hardware.dispatch_to_managers,
'fake_method')
def test_method_fails(self):
self.assertRaises(RuntimeError,
hardware.dispatch_to_managers,
'unexpected_fail')
class TestNoHardwareManagerLoading(test_base.BaseTestCase):
def setUp(self):
super(TestNoHardwareManagerLoading, self).setUp()
self.empty_ext_mgr = extension.ExtensionManager.make_test_instance([])
@mock.patch('stevedore.ExtensionManager')
def test_no_managers_found(self, mocked_extension_mgr_constructor):
mocked_extension_mgr_constructor.return_value = self.empty_ext_mgr
hardware._global_managers = None
self.assertRaises(errors.HardwareManagerNotFound,
hardware.dispatch_to_managers,
'some_method')

@ -17,7 +17,6 @@ import binascii
import mock
from oslotest import base as test_base
from ironic_python_agent import hardware
from ironic_python_agent import netutils
# hexlify-ed output from LLDP packet
@ -33,7 +32,6 @@ FAKE_LLDP_PACKET = binascii.unhexlify(
class TestNetutils(test_base.BaseTestCase):
def setUp(self):
super(TestNetutils, self).setUp()
self.hardware = hardware.GenericHardwareManager()
@mock.patch('fcntl.ioctl')
@mock.patch('select.select')