summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
-rw-r--r--ironic_python_agent/agent.py10
-rw-r--r--ironic_python_agent/errors.py40
-rw-r--r--ironic_python_agent/extensions/decom.py2
-rw-r--r--ironic_python_agent/extensions/standby.py4
-rw-r--r--ironic_python_agent/hardware.py124
-rw-r--r--ironic_python_agent/tests/agent.py14
-rw-r--r--ironic_python_agent/tests/extensions/decom.py8
-rw-r--r--ironic_python_agent/tests/extensions/standby.py46
-rw-r--r--ironic_python_agent/tests/hardware.py17
-rw-r--r--ironic_python_agent/tests/multi_hardware.py154
-rw-r--r--ironic_python_agent/tests/netutils.py2
11 files changed, 326 insertions, 95 deletions
diff --git a/ironic_python_agent/agent.py b/ironic_python_agent/agent.py
index ab4593b8..6fef34a7 100644
--- a/ironic_python_agent/agent.py
+++ b/ironic_python_agent/agent.py
@@ -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)
diff --git a/ironic_python_agent/errors.py b/ironic_python_agent/errors.py
index 7219fbda..a7abdb30 100644
--- a/ironic_python_agent/errors.py
+++ b/ironic_python_agent/errors.py
@@ -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)
diff --git a/ironic_python_agent/extensions/decom.py b/ironic_python_agent/extensions/decom.py
index def97ba4..cc73387d 100644
--- a/ironic_python_agent/extensions/decom.py
+++ b/ironic_python_agent/extensions/decom.py
@@ -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')
diff --git a/ironic_python_agent/extensions/standby.py b/ironic_python_agent/extensions/standby.py
index d2de593c..63b83197 100644
--- a/ironic_python_agent/extensions/standby.py
+++ b/ironic_python_agent/extensions/standby.py
@@ -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']:
diff --git a/ironic_python_agent/hardware.py b/ironic_python_agent/hardware.py
index 55cbb572..d7e0fd51 100644
--- a/ironic_python_agent/hardware.py
+++ b/ironic_python_agent/hardware.py
@@ -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.
+
+ 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_manager:
- LOG = log.getLogger()
+ 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_managers = []
+
+ 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))
- preferred_manager = preferred_extension.obj
+ if not preferred_managers:
+ raise errors.HardwareManagerNotFound
- if preferred_manager.evaluate_hardware_support() <= 0:
- raise RuntimeError('No suitable HardwareManager could be found')
+ _global_managers = preferred_managers
- LOG.info('selected hardware manager {0}'.format(
- preferred_extension.entry_point_target))
+ return _global_managers
- _global_manager = preferred_manager
- return _global_manager
+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)
diff --git a/ironic_python_agent/tests/agent.py b/ironic_python_agent/tests/agent.py
index a3622bbb..f11473e4 100644
--- a/ironic_python_agent/tests/agent.py
+++ b/ironic_python_agent/tests/agent.py
@@ -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'
diff --git a/ironic_python_agent/tests/extensions/decom.py b/ironic_python_agent/tests/extensions/decom.py
index 2101d237..cfa01dd4 100644
--- a/ironic_python_agent/tests/extensions/decom.py
+++ b/ironic_python_agent/tests/extensions/decom.py
@@ -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')
diff --git a/ironic_python_agent/tests/extensions/standby.py b/ironic_python_agent/tests/extensions/standby.py
index 01cfa314..b49dacf0 100644
--- a/ironic_python_agent/tests/extensions/standby.py
+++ b/ironic_python_agent/tests/extensions/standby.py
@@ -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)
diff --git a/ironic_python_agent/tests/hardware.py b/ironic_python_agent/tests/hardware.py
index d6c5f143..fa107d10 100644
--- a/ironic_python_agent/tests/hardware.py
+++ b/ironic_python_agent/tests/hardware.py
@@ -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)
diff --git a/ironic_python_agent/tests/multi_hardware.py b/ironic_python_agent/tests/multi_hardware.py
new file mode 100644
index 00000000..e08900cb
--- /dev/null
+++ b/ironic_python_agent/tests/multi_hardware.py
@@ -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')
diff --git a/ironic_python_agent/tests/netutils.py b/ironic_python_agent/tests/netutils.py
index 5a6c5d57..9a45b48b 100644
--- a/ironic_python_agent/tests/netutils.py
+++ b/ironic_python_agent/tests/netutils.py
@@ -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')