diff --git a/ironic_python_agent/agent.py b/ironic_python_agent/agent.py index ab4593b85..6fef34a72 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 7219fbdac..a7abdb301 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 def97ba4a..cc73387da 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 d2de593c3..63b831972 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 55cbb5723..d7e0fd510 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. - 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) diff --git a/ironic_python_agent/tests/agent.py b/ironic_python_agent/tests/agent.py index a3622bbb3..f11473e4b 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 2101d237e..cfa01dd4c 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 01cfa3141..b49dacf02 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 d6c5f1430..fa107d10b 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 000000000..e08900cbf --- /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 5a6c5d57f..9a45b48b8 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')