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__()
|
super(IronicPythonAgentHeartbeater, self).__init__()
|
||||||
self.agent = agent
|
self.agent = agent
|
||||||
self.hardware = hardware.get_manager()
|
|
||||||
self.api = ironic_api_client.APIClient(agent.api_url,
|
self.api = ironic_api_client.APIClient(agent.api_url,
|
||||||
agent.driver_name)
|
agent.driver_name)
|
||||||
self.log = log.getLogger(__name__)
|
self.log = log.getLogger(__name__)
|
||||||
@ -156,7 +155,6 @@ class IronicPythonAgent(base.ExecuteCommandMixin):
|
|||||||
self.api = app.VersionSelectorApplication(self)
|
self.api = app.VersionSelectorApplication(self)
|
||||||
self.heartbeater = IronicPythonAgentHeartbeater(self)
|
self.heartbeater = IronicPythonAgentHeartbeater(self)
|
||||||
self.heartbeat_timeout = None
|
self.heartbeat_timeout = None
|
||||||
self.hardware = hardware.get_manager()
|
|
||||||
self.log = log.getLogger(__name__)
|
self.log = log.getLogger(__name__)
|
||||||
self.started_at = None
|
self.started_at = None
|
||||||
self.node = None
|
self.node = None
|
||||||
@ -201,7 +199,8 @@ class IronicPythonAgent(base.ExecuteCommandMixin):
|
|||||||
attempts = 0
|
attempts = 0
|
||||||
while (attempts < self.ip_lookup_attempts):
|
while (attempts < self.ip_lookup_attempts):
|
||||||
for iface in ifaces:
|
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:
|
if found_ip is not None:
|
||||||
self.advertise_address = (found_ip,
|
self.advertise_address = (found_ip,
|
||||||
self.advertise_address[1])
|
self.advertise_address[1])
|
||||||
@ -223,7 +222,7 @@ class IronicPythonAgent(base.ExecuteCommandMixin):
|
|||||||
be found.
|
be found.
|
||||||
"""
|
"""
|
||||||
iface_list = [iface.serialize()['name'] for iface in
|
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]
|
iface_list = [name for name in iface_list if 'lo' not in name]
|
||||||
|
|
||||||
if len(iface_list) == 0:
|
if len(iface_list) == 0:
|
||||||
@ -278,7 +277,8 @@ class IronicPythonAgent(base.ExecuteCommandMixin):
|
|||||||
self.started_at = _time()
|
self.started_at = _time()
|
||||||
if not self.standalone:
|
if not self.standalone:
|
||||||
content = self.api_client.lookup_node(
|
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,
|
timeout=self.lookup_timeout,
|
||||||
starting_interval=self.lookup_interval)
|
starting_interval=self.lookup_interval)
|
||||||
|
|
||||||
|
@ -247,3 +247,43 @@ class UnknownNodeError(Exception):
|
|||||||
if message is not None:
|
if message is not None:
|
||||||
self.message = message
|
self.message = message
|
||||||
super(UnknownNodeError, self).__init__(self.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):
|
class DecomExtension(base.BaseAgentExtension):
|
||||||
@base.async_command('erase_hardware')
|
@base.async_command('erase_hardware')
|
||||||
def erase_hardware(self):
|
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)
|
@base.async_command('cache_image', _validate_image_info)
|
||||||
def cache_image(self, image_info=None, force=False):
|
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:
|
if self.cached_image_id != image_info['id'] or force:
|
||||||
_download_image(image_info)
|
_download_image(image_info)
|
||||||
@ -204,7 +204,7 @@ class StandbyExtension(base.BaseAgentExtension):
|
|||||||
def prepare_image(self,
|
def prepare_image(self,
|
||||||
image_info=None,
|
image_info=None,
|
||||||
configdrive=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
|
# don't write image again if already cached
|
||||||
if self.cached_image_id != image_info['id']:
|
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.openstack.common import log
|
||||||
from ironic_python_agent import utils
|
from ironic_python_agent import utils
|
||||||
|
|
||||||
_global_manager = None
|
_global_managers = None
|
||||||
|
LOG = log.getLogger()
|
||||||
|
|
||||||
|
|
||||||
class HardwareSupport(object):
|
class HardwareSupport(object):
|
||||||
"""These are just guidelines to suggest values that might be returned by
|
"""Example priorities for hardware managers.
|
||||||
calls to `evaluate_hardware_support`. No HardwareManager in mainline
|
|
||||||
ironic-python-agent will ever offer a value greater than `MAINLINE`.
|
Priorities for HardwareManagers are integers, where largest means most
|
||||||
Service Providers should feel free to return values greater than
|
specific and smallest means most generic. These values are guidelines
|
||||||
SERVICE_PROVIDER to distinguish between additional levels of support.
|
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
|
NONE = 0
|
||||||
GENERIC = 1
|
GENERIC = 1
|
||||||
@ -91,27 +96,21 @@ class HardwareManager(object):
|
|||||||
def evaluate_hardware_support(self):
|
def evaluate_hardware_support(self):
|
||||||
pass
|
pass
|
||||||
|
|
||||||
@abc.abstractmethod
|
|
||||||
def list_network_interfaces(self):
|
def list_network_interfaces(self):
|
||||||
pass
|
raise errors.IncompatibleHardwareMethodError
|
||||||
|
|
||||||
@abc.abstractmethod
|
|
||||||
def get_cpus(self):
|
def get_cpus(self):
|
||||||
pass
|
raise errors.IncompatibleHardwareMethodError
|
||||||
|
|
||||||
@abc.abstractmethod
|
|
||||||
def list_block_devices(self):
|
def list_block_devices(self):
|
||||||
pass
|
raise errors.IncompatibleHardwareMethodError
|
||||||
|
|
||||||
@abc.abstractmethod
|
|
||||||
def get_memory(self):
|
def get_memory(self):
|
||||||
pass
|
raise errors.IncompatibleHardwareMethodError
|
||||||
|
|
||||||
@abc.abstractmethod
|
|
||||||
def get_os_install_device(self):
|
def get_os_install_device(self):
|
||||||
pass
|
raise errors.IncompatibleHardwareMethodError
|
||||||
|
|
||||||
@abc.abstractmethod
|
|
||||||
def erase_block_device(self, block_device):
|
def erase_block_device(self, block_device):
|
||||||
"""Attempt to erase a block device.
|
"""Attempt to erase a block device.
|
||||||
|
|
||||||
@ -129,11 +128,12 @@ class HardwareManager(object):
|
|||||||
encouraged.
|
encouraged.
|
||||||
|
|
||||||
:param block_device: a BlockDevice indicating a device to be erased.
|
:param block_device: a BlockDevice indicating a device to be erased.
|
||||||
:raises: BlockDeviceEraseError when an error occurs erasing a block
|
:raises IncompatibleHardwareMethodError: when there is no known way to
|
||||||
device, or if the block device is not supported.
|
erase the block device
|
||||||
|
:raises BlockDeviceEraseError: when there is an error erasing the
|
||||||
|
block device
|
||||||
"""
|
"""
|
||||||
pass
|
raise errors.IncompatibleHardwareMethodError
|
||||||
|
|
||||||
def erase_devices(self):
|
def erase_devices(self):
|
||||||
"""Erase any device that holds user data.
|
"""Erase any device that holds user data.
|
||||||
@ -270,10 +270,10 @@ class GenericHardwareManager(HardwareManager):
|
|||||||
if self._ata_erase(block_device):
|
if self._ata_erase(block_device):
|
||||||
return
|
return
|
||||||
|
|
||||||
# NOTE(russell_h): Support for additional generic erase methods should
|
msg = ('Unable to erase block device {0}: device is unsupported.'
|
||||||
# be added above this raise, in order of precedence.
|
).format(block_device.name)
|
||||||
raise errors.BlockDeviceEraseError(('Unable to erase block device '
|
LOG.error(msg)
|
||||||
'{0}: device is unsupported.').format(block_device.name))
|
raise errors.IncompatibleHardwareMethodError(msg)
|
||||||
|
|
||||||
def _get_ata_security_lines(self, block_device):
|
def _get_ata_security_lines(self, block_device):
|
||||||
output = utils.execute('hdparm', '-I', block_device.name)[0]
|
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()
|
return mgr2.evaluate_hardware_support() - mgr1.evaluate_hardware_support()
|
||||||
|
|
||||||
|
|
||||||
def get_manager():
|
def _get_managers():
|
||||||
global _global_manager
|
"""Get a list of hardware managers in priority order.
|
||||||
|
|
||||||
if not _global_manager:
|
Use stevedore to find all eligible hardware managers, sort them based on
|
||||||
LOG = log.getLogger()
|
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(
|
extension_manager = stevedore.ExtensionManager(
|
||||||
namespace='ironic_python_agent.hardware_managers',
|
namespace='ironic_python_agent.hardware_managers',
|
||||||
invoke_on_load=True)
|
invoke_on_load=True)
|
||||||
@ -344,22 +352,54 @@ def get_manager():
|
|||||||
# There will always be at least one extension available (the
|
# There will always be at least one extension available (the
|
||||||
# GenericHardwareManager).
|
# GenericHardwareManager).
|
||||||
if six.PY2:
|
if six.PY2:
|
||||||
preferred_extension = sorted(
|
extensions = sorted(extension_manager, _compare_extensions)
|
||||||
extension_manager,
|
|
||||||
_compare_extensions)[0]
|
|
||||||
else:
|
else:
|
||||||
preferred_extension = sorted(
|
extensions = sorted(extension_manager,
|
||||||
extension_manager,
|
key=functools.cmp_to_key(_compare_extensions))
|
||||||
key=functools.cmp_to_key(_compare_extensions))[0]
|
|
||||||
|
|
||||||
preferred_manager = preferred_extension.obj
|
preferred_managers = []
|
||||||
|
|
||||||
if preferred_manager.evaluate_hardware_support() <= 0:
|
for extension in extensions:
|
||||||
raise RuntimeError('No suitable HardwareManager could be found')
|
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(
|
if not preferred_managers:
|
||||||
preferred_extension.entry_point_target))
|
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('os.read')
|
||||||
@mock.patch('select.poll')
|
@mock.patch('select.poll')
|
||||||
@mock.patch('time.sleep', return_value=None)
|
@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.'
|
homeless_agent = agent.IronicPythonAgent('https://fake_api.example.'
|
||||||
'org:8081/',
|
'org:8081/',
|
||||||
(None, 9990),
|
(None, 9990),
|
||||||
@ -214,10 +222,6 @@ class TestBaseAgent(test_base.BaseTestCase):
|
|||||||
'agent_ipmitool',
|
'agent_ipmitool',
|
||||||
False)
|
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_poll.return_value.poll.return_value = True
|
||||||
mock_read.return_value = 'a'
|
mock_read.return_value = 'a'
|
||||||
|
|
||||||
|
@ -23,9 +23,9 @@ class TestDecomExtension(test_base.BaseTestCase):
|
|||||||
super(TestDecomExtension, self).setUp()
|
super(TestDecomExtension, self).setUp()
|
||||||
self.agent_extension = decom.DecomExtension()
|
self.agent_extension = decom.DecomExtension()
|
||||||
|
|
||||||
@mock.patch('ironic_python_agent.hardware.get_manager', autospec=True)
|
@mock.patch('ironic_python_agent.hardware.dispatch_to_managers',
|
||||||
def test_erase_hardware(self, mocked_get_manager):
|
autospec=True)
|
||||||
hardware_manager = mocked_get_manager.return_value
|
def test_erase_devices(self, mocked_dispatch):
|
||||||
result = self.agent_extension.erase_hardware()
|
result = self.agent_extension.erase_hardware()
|
||||||
result.join()
|
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.assertFalse(verified)
|
||||||
self.assertEqual(md5_mock.call_count, 1)
|
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',
|
@mock.patch('ironic_python_agent.extensions.standby._write_image',
|
||||||
autospec=True)
|
autospec=True)
|
||||||
@mock.patch('ironic_python_agent.extensions.standby._download_image',
|
@mock.patch('ironic_python_agent.extensions.standby._download_image',
|
||||||
autospec=True)
|
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()
|
image_info = self._build_fake_image_info()
|
||||||
download_mock.return_value = None
|
download_mock.return_value = None
|
||||||
write_mock.return_value = None
|
write_mock.return_value = None
|
||||||
manager_mock = hardware_mock.return_value
|
dispatch_mock.return_value = 'manager'
|
||||||
manager_mock.get_os_install_device.return_value = 'manager'
|
|
||||||
async_result = self.agent_extension.cache_image(image_info=image_info)
|
async_result = self.agent_extension.cache_image(image_info=image_info)
|
||||||
async_result.join()
|
async_result.join()
|
||||||
download_mock.assert_called_once_with(image_info)
|
download_mock.assert_called_once_with(image_info)
|
||||||
write_mock.assert_called_once_with(image_info, 'manager')
|
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,
|
self.assertEqual(self.agent_extension.cached_image_id,
|
||||||
image_info['id'])
|
image_info['id'])
|
||||||
self.assertEqual('SUCCEEDED', async_result.command_status)
|
self.assertEqual('SUCCEEDED', async_result.command_status)
|
||||||
self.assertEqual(None, async_result.command_result)
|
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',
|
@mock.patch('ironic_python_agent.extensions.standby._write_image',
|
||||||
autospec=True)
|
autospec=True)
|
||||||
@mock.patch('ironic_python_agent.extensions.standby._download_image',
|
@mock.patch('ironic_python_agent.extensions.standby._download_image',
|
||||||
autospec=True)
|
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()
|
image_info = self._build_fake_image_info()
|
||||||
self.agent_extension.cached_image_id = image_info['id']
|
self.agent_extension.cached_image_id = image_info['id']
|
||||||
download_mock.return_value = None
|
download_mock.return_value = None
|
||||||
write_mock.return_value = None
|
write_mock.return_value = None
|
||||||
manager_mock = hardware_mock.return_value
|
dispatch_mock.return_value = 'manager'
|
||||||
manager_mock.get_os_install_device.return_value = 'manager'
|
|
||||||
async_result = self.agent_extension.cache_image(
|
async_result = self.agent_extension.cache_image(
|
||||||
image_info=image_info, force=True
|
image_info=image_info, force=True
|
||||||
)
|
)
|
||||||
async_result.join()
|
async_result.join()
|
||||||
download_mock.assert_called_once_with(image_info)
|
download_mock.assert_called_once_with(image_info)
|
||||||
write_mock.assert_called_once_with(image_info, 'manager')
|
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,
|
self.assertEqual(self.agent_extension.cached_image_id,
|
||||||
image_info['id'])
|
image_info['id'])
|
||||||
self.assertEqual('SUCCEEDED', async_result.command_status)
|
self.assertEqual('SUCCEEDED', async_result.command_status)
|
||||||
self.assertEqual(None, async_result.command_result)
|
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',
|
@mock.patch('ironic_python_agent.extensions.standby._write_image',
|
||||||
autospec=True)
|
autospec=True)
|
||||||
@mock.patch('ironic_python_agent.extensions.standby._download_image',
|
@mock.patch('ironic_python_agent.extensions.standby._download_image',
|
||||||
autospec=True)
|
autospec=True)
|
||||||
def test_cache_image_cached(self, download_mock, write_mock,
|
def test_cache_image_cached(self, download_mock, write_mock,
|
||||||
hardware_mock):
|
dispatch_mock):
|
||||||
image_info = self._build_fake_image_info()
|
image_info = self._build_fake_image_info()
|
||||||
self.agent_extension.cached_image_id = image_info['id']
|
self.agent_extension.cached_image_id = image_info['id']
|
||||||
download_mock.return_value = None
|
download_mock.return_value = None
|
||||||
write_mock.return_value = None
|
write_mock.return_value = None
|
||||||
manager_mock = hardware_mock.return_value
|
dispatch_mock.return_value = 'manager'
|
||||||
manager_mock.get_os_install_device.return_value = 'manager'
|
|
||||||
async_result = self.agent_extension.cache_image(image_info=image_info)
|
async_result = self.agent_extension.cache_image(image_info=image_info)
|
||||||
async_result.join()
|
async_result.join()
|
||||||
self.assertFalse(download_mock.called)
|
self.assertFalse(download_mock.called)
|
||||||
self.assertFalse(write_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,
|
self.assertEqual(self.agent_extension.cached_image_id,
|
||||||
image_info['id'])
|
image_info['id'])
|
||||||
self.assertEqual('SUCCEEDED', async_result.command_status)
|
self.assertEqual('SUCCEEDED', async_result.command_status)
|
||||||
@ -333,7 +337,8 @@ class TestStandbyExtension(test_base.BaseTestCase):
|
|||||||
@mock.patch(('ironic_python_agent.extensions.standby.'
|
@mock.patch(('ironic_python_agent.extensions.standby.'
|
||||||
'_write_configdrive_to_partition'),
|
'_write_configdrive_to_partition'),
|
||||||
autospec=True)
|
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',
|
@mock.patch('ironic_python_agent.extensions.standby._write_image',
|
||||||
autospec=True)
|
autospec=True)
|
||||||
@mock.patch('ironic_python_agent.extensions.standby._download_image',
|
@mock.patch('ironic_python_agent.extensions.standby._download_image',
|
||||||
@ -344,14 +349,13 @@ class TestStandbyExtension(test_base.BaseTestCase):
|
|||||||
location_mock,
|
location_mock,
|
||||||
download_mock,
|
download_mock,
|
||||||
write_mock,
|
write_mock,
|
||||||
hardware_mock,
|
dispatch_mock,
|
||||||
configdrive_copy_mock):
|
configdrive_copy_mock):
|
||||||
image_info = self._build_fake_image_info()
|
image_info = self._build_fake_image_info()
|
||||||
location_mock.return_value = '/tmp/configdrive'
|
location_mock.return_value = '/tmp/configdrive'
|
||||||
download_mock.return_value = None
|
download_mock.return_value = None
|
||||||
write_mock.return_value = None
|
write_mock.return_value = None
|
||||||
manager_mock = hardware_mock.return_value
|
dispatch_mock.return_value = 'manager'
|
||||||
manager_mock.get_os_install_device.return_value = 'manager'
|
|
||||||
configdrive_copy_mock.return_value = None
|
configdrive_copy_mock.return_value = None
|
||||||
|
|
||||||
async_result = self.agent_extension.prepare_image(
|
async_result = self.agent_extension.prepare_image(
|
||||||
@ -362,6 +366,7 @@ class TestStandbyExtension(test_base.BaseTestCase):
|
|||||||
|
|
||||||
download_mock.assert_called_once_with(image_info)
|
download_mock.assert_called_once_with(image_info)
|
||||||
write_mock.assert_called_once_with(image_info, 'manager')
|
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',
|
configdrive_copy_mock.assert_called_once_with('configdrive_data',
|
||||||
'manager')
|
'manager')
|
||||||
|
|
||||||
@ -389,7 +394,8 @@ class TestStandbyExtension(test_base.BaseTestCase):
|
|||||||
@mock.patch(('ironic_python_agent.extensions.standby.'
|
@mock.patch(('ironic_python_agent.extensions.standby.'
|
||||||
'_write_configdrive_to_partition'),
|
'_write_configdrive_to_partition'),
|
||||||
autospec=True)
|
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',
|
@mock.patch('ironic_python_agent.extensions.standby._write_image',
|
||||||
autospec=True)
|
autospec=True)
|
||||||
@mock.patch('ironic_python_agent.extensions.standby._download_image',
|
@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,
|
def test_prepare_image_no_configdrive(self,
|
||||||
download_mock,
|
download_mock,
|
||||||
write_mock,
|
write_mock,
|
||||||
hardware_mock,
|
dispatch_mock,
|
||||||
configdrive_copy_mock):
|
configdrive_copy_mock):
|
||||||
image_info = self._build_fake_image_info()
|
image_info = self._build_fake_image_info()
|
||||||
download_mock.return_value = None
|
download_mock.return_value = None
|
||||||
write_mock.return_value = None
|
write_mock.return_value = None
|
||||||
manager_mock = hardware_mock.return_value
|
dispatch_mock.return_value = 'manager'
|
||||||
manager_mock.get_os_install_device.return_value = 'manager'
|
|
||||||
configdrive_copy_mock.return_value = None
|
configdrive_copy_mock.return_value = None
|
||||||
|
|
||||||
async_result = self.agent_extension.prepare_image(
|
async_result = self.agent_extension.prepare_image(
|
||||||
@ -414,6 +419,7 @@ class TestStandbyExtension(test_base.BaseTestCase):
|
|||||||
|
|
||||||
download_mock.assert_called_once_with(image_info)
|
download_mock.assert_called_once_with(image_info)
|
||||||
write_mock.assert_called_once_with(image_info, 'manager')
|
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(configdrive_copy_mock.call_count, 0)
|
||||||
self.assertEqual('SUCCEEDED', async_result.command_status)
|
self.assertEqual('SUCCEEDED', async_result.command_status)
|
||||||
|
@ -157,17 +157,6 @@ class TestHardwareManagerLoading(test_base.BaseTestCase):
|
|||||||
ext1, ext2, ext3
|
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):
|
class TestGenericHardwareManager(test_base.BaseTestCase):
|
||||||
def setUp(self):
|
def setUp(self):
|
||||||
@ -355,7 +344,7 @@ class TestGenericHardwareManager(test_base.BaseTestCase):
|
|||||||
])
|
])
|
||||||
|
|
||||||
@mock.patch.object(utils, 'execute')
|
@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]
|
hdparm_output = HDPARM_INFO_TEMPLATE.split('\nSecurity:')[0]
|
||||||
|
|
||||||
mocked_execute.side_effect = [
|
mocked_execute.side_effect = [
|
||||||
@ -364,7 +353,7 @@ class TestGenericHardwareManager(test_base.BaseTestCase):
|
|||||||
|
|
||||||
block_device = hardware.BlockDevice('/dev/sda', 'big', 1073741824,
|
block_device = hardware.BlockDevice('/dev/sda', 'big', 1073741824,
|
||||||
True)
|
True)
|
||||||
self.assertRaises(errors.BlockDeviceEraseError,
|
self.assertRaises(errors.IncompatibleHardwareMethodError,
|
||||||
self.hardware.erase_block_device,
|
self.hardware.erase_block_device,
|
||||||
block_device)
|
block_device)
|
||||||
|
|
||||||
@ -382,7 +371,7 @@ class TestGenericHardwareManager(test_base.BaseTestCase):
|
|||||||
|
|
||||||
block_device = hardware.BlockDevice('/dev/sda', 'big', 1073741824,
|
block_device = hardware.BlockDevice('/dev/sda', 'big', 1073741824,
|
||||||
True)
|
True)
|
||||||
self.assertRaises(errors.BlockDeviceEraseError,
|
self.assertRaises(errors.IncompatibleHardwareMethodError,
|
||||||
self.hardware.erase_block_device,
|
self.hardware.erase_block_device,
|
||||||
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
|
import mock
|
||||||
from oslotest import base as test_base
|
from oslotest import base as test_base
|
||||||
|
|
||||||
from ironic_python_agent import hardware
|
|
||||||
from ironic_python_agent import netutils
|
from ironic_python_agent import netutils
|
||||||
|
|
||||||
# hexlify-ed output from LLDP packet
|
# hexlify-ed output from LLDP packet
|
||||||
@ -33,7 +32,6 @@ FAKE_LLDP_PACKET = binascii.unhexlify(
|
|||||||
class TestNetutils(test_base.BaseTestCase):
|
class TestNetutils(test_base.BaseTestCase):
|
||||||
def setUp(self):
|
def setUp(self):
|
||||||
super(TestNetutils, self).setUp()
|
super(TestNetutils, self).setUp()
|
||||||
self.hardware = hardware.GenericHardwareManager()
|
|
||||||
|
|
||||||
@mock.patch('fcntl.ioctl')
|
@mock.patch('fcntl.ioctl')
|
||||||
@mock.patch('select.select')
|
@mock.patch('select.select')
|
||||||
|
Loading…
x
Reference in New Issue
Block a user