Merge "Enhance decorators in agent and automate creation of command_map"

This commit is contained in:
Jenkins
2014-09-12 17:56:31 +00:00
committed by Gerrit Code Review
8 changed files with 77 additions and 63 deletions

View File

@@ -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:

View File

@@ -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()

View File

@@ -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():

View File

@@ -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')

View File

@@ -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)

View File

@@ -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()

View File

@@ -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,

View File

@@ -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])