Merge "Enhance decorators in agent and automate creation of command_map"
This commit is contained in:
@@ -13,6 +13,7 @@
|
||||
# limitations under the License.
|
||||
|
||||
import functools
|
||||
import inspect
|
||||
import threading
|
||||
import uuid
|
||||
|
||||
@@ -111,14 +112,18 @@ class BaseAgentExtension(object):
|
||||
def __init__(self):
|
||||
super(BaseAgentExtension, self).__init__()
|
||||
self.log = log.getLogger(__name__)
|
||||
self.command_map = {}
|
||||
self.command_map = dict(
|
||||
(v.command_name, v)
|
||||
for k, v in inspect.getmembers(self)
|
||||
if hasattr(v, 'command_name')
|
||||
)
|
||||
|
||||
def execute(self, command_name, **kwargs):
|
||||
if command_name not in self.command_map:
|
||||
cmd = self.command_map.get(command_name)
|
||||
if cmd is None:
|
||||
raise errors.InvalidCommandError(
|
||||
'Unknown command: {0}'.format(command_name))
|
||||
|
||||
return self.command_map[command_name](command_name, **kwargs)
|
||||
return cmd(**kwargs)
|
||||
|
||||
def check_cmd_presence(self, ext_obj, ext, cmd):
|
||||
if not (hasattr(ext_obj, 'execute') and hasattr(ext_obj, 'command_map')
|
||||
@@ -181,14 +186,16 @@ class ExecuteCommandMixin(object):
|
||||
return result
|
||||
|
||||
|
||||
def async_command(validator=None):
|
||||
def async_command(command_name, validator=None):
|
||||
"""Will run the command in an AsyncCommandResult in its own thread.
|
||||
command_name is set based on the func name and command_params will
|
||||
be whatever args/kwargs you pass into the decorated command.
|
||||
"""
|
||||
def async_decorator(func):
|
||||
func.command_name = command_name
|
||||
|
||||
@functools.wraps(func)
|
||||
def wrapper(self, command_name, **command_params):
|
||||
def wrapper(self, **command_params):
|
||||
# Run a validator before passing everything off to async.
|
||||
# validators should raise exceptions or return silently.
|
||||
if validator:
|
||||
@@ -205,15 +212,17 @@ def async_command(validator=None):
|
||||
return async_decorator
|
||||
|
||||
|
||||
def sync_command(validator=None):
|
||||
def sync_command(command_name, validator=None):
|
||||
"""Decorate a method in order to wrap up its return value in a
|
||||
SyncCommandResult. For consistency with @async_command() can also accept a
|
||||
validator which will be used to validate input, although a synchronous
|
||||
command can also choose to implement validation inline.
|
||||
"""
|
||||
def sync_decorator(func):
|
||||
func.command_name = command_name
|
||||
|
||||
@functools.wraps(func)
|
||||
def wrapper(self, command_name, **command_params):
|
||||
def wrapper(self, **command_params):
|
||||
# Run a validator before passing everything off to async.
|
||||
# validators should raise exceptions or return silently.
|
||||
if validator:
|
||||
|
||||
@@ -17,10 +17,6 @@ from ironic_python_agent import hardware
|
||||
|
||||
|
||||
class DecomExtension(base.BaseAgentExtension):
|
||||
def __init__(self):
|
||||
super(DecomExtension, self).__init__()
|
||||
self.command_map['erase_hardware'] = self.erase_hardware
|
||||
|
||||
@base.async_command()
|
||||
@base.async_command('erase_hardware')
|
||||
def erase_hardware(self):
|
||||
hardware.get_manager().erase_devices()
|
||||
|
||||
@@ -31,11 +31,7 @@ def _validate_exts(ext, flow=None):
|
||||
|
||||
|
||||
class FlowExtension(base.BaseAgentExtension, base.ExecuteCommandMixin):
|
||||
def __init__(self):
|
||||
super(FlowExtension, self).__init__()
|
||||
self.command_map['start_flow'] = self.start_flow
|
||||
|
||||
@base.async_command(_validate_exts)
|
||||
@base.async_command('start_flow', _validate_exts)
|
||||
def start_flow(self, flow=None):
|
||||
for task in flow:
|
||||
for method, params in task.items():
|
||||
|
||||
@@ -174,13 +174,10 @@ def _validate_image_info(ext, image_info=None, **kwargs):
|
||||
class StandbyExtension(base.BaseAgentExtension):
|
||||
def __init__(self):
|
||||
super(StandbyExtension, self).__init__()
|
||||
self.command_map['cache_image'] = self.cache_image
|
||||
self.command_map['prepare_image'] = self.prepare_image
|
||||
self.command_map['run_image'] = self.run_image
|
||||
|
||||
self.cached_image_id = None
|
||||
|
||||
@base.async_command(_validate_image_info)
|
||||
@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()
|
||||
|
||||
@@ -189,7 +186,7 @@ class StandbyExtension(base.BaseAgentExtension):
|
||||
_write_image(image_info, device)
|
||||
self.cached_image_id = image_info['id']
|
||||
|
||||
@base.async_command(_validate_image_info)
|
||||
@base.async_command('prepare_image', _validate_image_info)
|
||||
def prepare_image(self,
|
||||
image_info=None,
|
||||
configdrive=None):
|
||||
@@ -204,7 +201,7 @@ class StandbyExtension(base.BaseAgentExtension):
|
||||
if configdrive is not None:
|
||||
_write_configdrive_to_partition(configdrive, device)
|
||||
|
||||
@base.async_command()
|
||||
@base.async_command('run_image')
|
||||
def run_image(self):
|
||||
script = _path_to_script('shell/reboot.sh')
|
||||
LOG.info('Rebooting system')
|
||||
|
||||
@@ -31,23 +31,26 @@ class ExecutionError(errors.RESTError):
|
||||
|
||||
|
||||
class FakeExtension(base.BaseAgentExtension):
|
||||
def __init__(self):
|
||||
super(FakeExtension, self).__init__()
|
||||
self.command_map['fake_async_command'] = self.fake_async_command
|
||||
self.command_map['fake_sync_command'] = self.fake_sync_command
|
||||
|
||||
@base.async_command(_fake_validator)
|
||||
@base.async_command('fake_async_command', _fake_validator)
|
||||
def fake_async_command(self, is_valid=False, param=None):
|
||||
if param == 'v2':
|
||||
raise ExecutionError()
|
||||
return param
|
||||
|
||||
@base.sync_command(_fake_validator)
|
||||
@base.sync_command('fake_sync_command', _fake_validator)
|
||||
def fake_sync_command(self, is_valid=False, param=None):
|
||||
if param == 'v2':
|
||||
raise ExecutionError()
|
||||
return param
|
||||
|
||||
@base.async_command('other_async_name')
|
||||
def second_async_command(self):
|
||||
pass
|
||||
|
||||
@base.sync_command('other_sync_name')
|
||||
def second_sync_command(self):
|
||||
pass
|
||||
|
||||
|
||||
class FakeAgent(base.ExecuteCommandMixin):
|
||||
def __init__(self):
|
||||
@@ -70,7 +73,7 @@ class TestExecuteCommandMixin(test_base.BaseTestCase):
|
||||
[extension.Extension('fake', None, FakeExtension, fake_extension)])
|
||||
|
||||
self.agent.execute_command('fake.do_something', foo='bar')
|
||||
do_something_impl.assert_called_once_with('do_something', foo='bar')
|
||||
do_something_impl.assert_called_once_with(foo='bar')
|
||||
|
||||
def test_execute_invalid_command(self):
|
||||
self.assertRaises(errors.InvalidCommandError,
|
||||
@@ -147,6 +150,11 @@ class TestExtensionDecorators(test_base.BaseTestCase):
|
||||
self.assertIsInstance(result.command_error, ExecutionError)
|
||||
self.assertEqual(None, result.command_result)
|
||||
|
||||
def test_async_command_name(self):
|
||||
self.assertEqual(
|
||||
'other_async_name',
|
||||
self.extension.second_async_command.command_name)
|
||||
|
||||
def test_sync_command_success(self):
|
||||
result = self.extension.execute('fake_sync_command', param='v1')
|
||||
self.assertIsInstance(result, base.SyncCommandResult)
|
||||
@@ -168,3 +176,17 @@ class TestExtensionDecorators(test_base.BaseTestCase):
|
||||
self.extension.execute,
|
||||
'fake_sync_command',
|
||||
param='v2')
|
||||
|
||||
def test_sync_command_name(self):
|
||||
self.assertEqual(
|
||||
'other_sync_name',
|
||||
self.extension.second_sync_command.command_name)
|
||||
|
||||
def test_command_map(self):
|
||||
expected_map = {
|
||||
'fake_async_command': self.extension.fake_async_command,
|
||||
'fake_sync_command': self.extension.fake_sync_command,
|
||||
'other_async_name': self.extension.second_async_command,
|
||||
'other_sync_name': self.extension.second_sync_command,
|
||||
}
|
||||
self.assertEqual(expected_map, self.extension.command_map)
|
||||
|
||||
@@ -26,6 +26,6 @@ class TestDecomExtension(test_base.BaseTestCase):
|
||||
@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
|
||||
result = self.agent_extension.erase_hardware('erase_hardware')
|
||||
result = self.agent_extension.erase_hardware()
|
||||
result.join()
|
||||
hardware_manager.erase_devices.assert_called_once_with()
|
||||
|
||||
@@ -36,16 +36,11 @@ FLOW_INFO = [
|
||||
|
||||
|
||||
class FakeExtension(base.BaseAgentExtension):
|
||||
def __init__(self):
|
||||
super(FakeExtension, self).__init__()
|
||||
self.command_map['sleep'] = self.sleep
|
||||
self.command_map['sync_sleep'] = self.sync_sleep
|
||||
|
||||
@base.async_command()
|
||||
@base.async_command('sleep')
|
||||
def sleep(self, sleep_info=None):
|
||||
time.sleep(sleep_info['time'])
|
||||
|
||||
@base.sync_command()
|
||||
@base.sync_command('sync_sleep')
|
||||
def sync_sleep(self, sleep_info=None):
|
||||
time.sleep(sleep_info['time'])
|
||||
|
||||
@@ -61,7 +56,7 @@ class TestFlowExtension(test_base.BaseTestCase):
|
||||
|
||||
@mock.patch('time.sleep', autospec=True)
|
||||
def test_sleep_flow_success(self, sleep_mock):
|
||||
result = self.agent_extension.start_flow('start_flow', flow=FLOW_INFO)
|
||||
result = self.agent_extension.start_flow(flow=FLOW_INFO)
|
||||
result.join()
|
||||
sleep_calls = [mock.call(i) for i in range(1, 8)]
|
||||
sleep_mock.assert_has_calls(sleep_calls)
|
||||
@@ -69,7 +64,7 @@ class TestFlowExtension(test_base.BaseTestCase):
|
||||
@mock.patch('time.sleep', autospec=True)
|
||||
def test_sleep_flow_failed(self, sleep_mock):
|
||||
sleep_mock.side_effect = errors.RESTError()
|
||||
result = self.agent_extension.start_flow('start_flow', flow=FLOW_INFO)
|
||||
result = self.agent_extension.start_flow(flow=FLOW_INFO)
|
||||
result.join()
|
||||
self.assertEqual(base.AgentCommandStatus.FAILED, result.command_status)
|
||||
self.assertTrue(isinstance(result.command_error,
|
||||
@@ -78,8 +73,7 @@ class TestFlowExtension(test_base.BaseTestCase):
|
||||
@mock.patch('time.sleep', autospec=True)
|
||||
def test_sleep_flow_failed_on_second_command(self, sleep_mock):
|
||||
sleep_mock.side_effect = [None, Exception('foo'), None, None]
|
||||
result = self.agent_extension.start_flow('start_flow',
|
||||
flow=FLOW_INFO[:4])
|
||||
result = self.agent_extension.start_flow(flow=FLOW_INFO[:4])
|
||||
result.join()
|
||||
self.assertEqual(base.AgentCommandStatus.FAILED, result.command_status)
|
||||
self.assertTrue(isinstance(result.command_error,
|
||||
|
||||
@@ -87,7 +87,6 @@ class TestStandbyExtension(test_base.BaseTestCase):
|
||||
def test_cache_image_invalid_image_list(self):
|
||||
self.assertRaises(errors.InvalidCommandParamsError,
|
||||
self.agent_extension.cache_image,
|
||||
'cache_image',
|
||||
image_info={'foo': 'bar'})
|
||||
|
||||
def test_image_location(self):
|
||||
@@ -262,8 +261,7 @@ class TestStandbyExtension(test_base.BaseTestCase):
|
||||
write_mock.return_value = None
|
||||
manager_mock = hardware_mock.return_value
|
||||
manager_mock.get_os_install_device.return_value = 'manager'
|
||||
async_result = self.agent_extension.cache_image('cache_image',
|
||||
image_info=image_info)
|
||||
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')
|
||||
@@ -284,9 +282,9 @@ class TestStandbyExtension(test_base.BaseTestCase):
|
||||
write_mock.return_value = None
|
||||
manager_mock = hardware_mock.return_value
|
||||
manager_mock.get_os_install_device.return_value = 'manager'
|
||||
async_result = self.agent_extension.cache_image('cache_image',
|
||||
image_info=image_info,
|
||||
force=True)
|
||||
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')
|
||||
@@ -308,8 +306,7 @@ class TestStandbyExtension(test_base.BaseTestCase):
|
||||
write_mock.return_value = None
|
||||
manager_mock = hardware_mock.return_value
|
||||
manager_mock.get_os_install_device.return_value = 'manager'
|
||||
async_result = self.agent_extension.cache_image('cache_image',
|
||||
image_info=image_info)
|
||||
async_result = self.agent_extension.cache_image(image_info=image_info)
|
||||
async_result.join()
|
||||
self.assertFalse(download_mock.called)
|
||||
self.assertFalse(write_mock.called)
|
||||
@@ -342,9 +339,10 @@ class TestStandbyExtension(test_base.BaseTestCase):
|
||||
manager_mock.get_os_install_device.return_value = 'manager'
|
||||
configdrive_copy_mock.return_value = None
|
||||
|
||||
async_result = self.agent_extension.prepare_image('prepare_image',
|
||||
image_info=image_info,
|
||||
configdrive='configdrive_data')
|
||||
async_result = self.agent_extension.prepare_image(
|
||||
image_info=image_info,
|
||||
configdrive='configdrive_data'
|
||||
)
|
||||
async_result.join()
|
||||
|
||||
download_mock.assert_called_once_with(image_info)
|
||||
@@ -359,9 +357,10 @@ class TestStandbyExtension(test_base.BaseTestCase):
|
||||
write_mock.reset_mock()
|
||||
configdrive_copy_mock.reset_mock()
|
||||
# image is now cached, make sure download/write doesn't happen
|
||||
async_result = self.agent_extension.prepare_image('prepare_image',
|
||||
image_info=image_info,
|
||||
configdrive='configdrive_data')
|
||||
async_result = self.agent_extension.prepare_image(
|
||||
image_info=image_info,
|
||||
configdrive='configdrive_data'
|
||||
)
|
||||
async_result.join()
|
||||
|
||||
self.assertEqual(download_mock.call_count, 0)
|
||||
@@ -392,9 +391,10 @@ class TestStandbyExtension(test_base.BaseTestCase):
|
||||
manager_mock.get_os_install_device.return_value = 'manager'
|
||||
configdrive_copy_mock.return_value = None
|
||||
|
||||
async_result = self.agent_extension.prepare_image('prepare_image',
|
||||
image_info=image_info,
|
||||
configdrive=None)
|
||||
async_result = self.agent_extension.prepare_image(
|
||||
image_info=image_info,
|
||||
configdrive=None
|
||||
)
|
||||
async_result.join()
|
||||
|
||||
download_mock.assert_called_once_with(image_info)
|
||||
@@ -410,7 +410,7 @@ class TestStandbyExtension(test_base.BaseTestCase):
|
||||
command = ['/bin/bash', script]
|
||||
execute_mock.return_value = ('', '')
|
||||
|
||||
success_result = self.agent_extension.run_image('run_image')
|
||||
success_result = self.agent_extension.run_image()
|
||||
success_result.join()
|
||||
|
||||
execute_mock.assert_called_once_with(*command, check_exit_code=[0])
|
||||
@@ -420,7 +420,7 @@ class TestStandbyExtension(test_base.BaseTestCase):
|
||||
execute_mock.return_value = ('', '')
|
||||
execute_mock.side_effect = processutils.ProcessExecutionError
|
||||
|
||||
failed_result = self.agent_extension.run_image('run_image')
|
||||
failed_result = self.agent_extension.run_image()
|
||||
failed_result.join()
|
||||
|
||||
execute_mock.assert_called_once_with(*command, check_exit_code=[0])
|
||||
|
||||
Reference in New Issue
Block a user