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:
parent
8dd54446e3
commit
2bbec5770c
ironic_python_agent
@ -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)
|
||||
|
||||
|
154
ironic_python_agent/tests/multi_hardware.py
Normal file
154
ironic_python_agent/tests/multi_hardware.py
Normal file
@ -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')
|
||||
|
Loading…
x
Reference in New Issue
Block a user