Added an async_command decorator

Functions with the async_command decorator will be run inside an
AsyncCommandResult. The tests are passing but aren't really unit tests,
as they always run inside the async decorator.
This commit is contained in:
Josh Gachnang 2014-02-06 16:40:01 -08:00
parent 7c22a4e3bf
commit 1de60e9262
3 changed files with 131 additions and 86 deletions

47
teeth_agent/decorators.py Normal file

@ -0,0 +1,47 @@
"""
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 functools
import inspect
from teeth_agent import base
def async_command(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):
@functools.wraps(func)
def wrapper(self, *args, **kwargs):
# Run a validator before passing everything off to async.
# validators should raise exceptions or return silently.
if validator:
validator(*args, **kwargs)
# Grab the variable names from the func definition.
command_names = inspect.getargspec(func)[0]
# Create dict {command_name: arg,...}
if command_names[0] == "self":
command_params = dict(zip(command_names[1:len(args)+1], args))
else:
command_params = dict(zip(command_names[:len(args)], args))
# Add all of kwargs
command_params = dict(command_params.items() + kwargs.items())
return base.AsyncCommandResult(func.__name__,
command_params,
func).start()
return wrapper
return async_decorator

@ -23,6 +23,7 @@ import time
from teeth_agent import base
from teeth_agent import configdrive
from teeth_agent import decorators
from teeth_agent import errors
from teeth_agent import hardware
@ -133,6 +134,22 @@ def _verify_image(image_info, image_location):
return False
def _validate_image_info(image_info, *args, **kwargs):
for field in ['id', 'urls', 'hashes']:
if field not in image_info:
msg = 'Image is missing \'{}\' field.'.format(field)
raise errors.InvalidCommandParamsError(msg)
if type(image_info['urls']) != list or not image_info['urls']:
raise errors.InvalidCommandParamsError(
'Image \'urls\' must be a list with at least one element.')
if type(image_info['hashes']) != dict or not image_info['hashes']:
raise errors.InvalidCommandParamsError(
'Image \'hashes\' must be a dictionary with at least one '
'element.')
class StandbyMode(base.BaseAgentMode):
def __init__(self):
super(StandbyMode, self).__init__('STANDBY')
@ -140,61 +157,15 @@ class StandbyMode(base.BaseAgentMode):
self.command_map['prepare_image'] = self.prepare_image
self.command_map['run_image'] = self.run_image
def _validate_image_info(self, image_info):
for field in ['id', 'urls', 'hashes']:
if field not in image_info:
msg = 'Image is missing \'{}\' field.'.format(field)
raise errors.InvalidCommandParamsError(msg)
if type(image_info['urls']) != list or not image_info['urls']:
raise errors.InvalidCommandParamsError(
'Image \'urls\' must be a list with at least one element.')
if type(image_info['hashes']) != dict or not image_info['hashes']:
raise errors.InvalidCommandParamsError(
'Image \'hashes\' must be a dictionary with at least one '
'element.')
@decorators.async_command(_validate_image_info)
def cache_image(self, image_info):
self._validate_image_info(image_info)
command_params = {
'image_info': image_info
}
return base.AsyncCommandResult('cache_image',
command_params,
self._thread_cache_image).start()
def prepare_image(self, image_info, metadata, files):
self._validate_image_info(image_info)
command_params = {
'image_info': image_info,
'metadata': metadata,
'files': files
}
return base.AsyncCommandResult('prepare_image',
command_params,
self._thread_prepare_image).start()
def run_image(self, image_info):
self._validate_image_info(image_info)
command_params = {
'image_info': image_info
}
return base.AsyncCommandResult('run_image',
command_params,
self._thread_run_image).start()
def _thread_cache_image(self, command_name, image_info):
device = hardware.get_manager().get_os_install_device()
_download_image(image_info)
_write_image(image_info, device)
def _thread_prepare_image(self, command_name, image_info, metadata, files):
@decorators.async_command(_validate_image_info)
def prepare_image(self, image_info, metadata, files):
location = _configdrive_location()
device = hardware.get_manager().get_os_install_device()
@ -205,7 +176,8 @@ class StandbyMode(base.BaseAgentMode):
configdrive.write_configdrive(location, metadata, files)
_copy_configdrive_to_disk(location, device)
def _thread_run_image(self, command_name):
@decorators.async_command()
def run_image(self):
script = _path_to_script('shell/reboot.sh')
log.info("Rebooting system")
command = ['/bin/bash', script]

@ -42,7 +42,7 @@ class TestStandbyMode(unittest.TestCase):
}
def test_validate_image_info_success(self):
self.agent_mode._validate_image_info(self._build_fake_image_info())
standby._validate_image_info(self._build_fake_image_info())
def test_validate_image_info_missing_field(self):
for field in ['id', 'urls', 'hashes']:
@ -50,7 +50,7 @@ class TestStandbyMode(unittest.TestCase):
del invalid_info[field]
self.assertRaises(errors.InvalidCommandParamsError,
self.agent_mode._validate_image_info,
standby._validate_image_info,
invalid_info)
def test_validate_image_info_invalid_urls(self):
@ -58,7 +58,7 @@ class TestStandbyMode(unittest.TestCase):
invalid_info['urls'] = 'this_is_not_a_list'
self.assertRaises(errors.InvalidCommandParamsError,
self.agent_mode._validate_image_info,
standby._validate_image_info,
invalid_info)
def test_validate_image_info_empty_urls(self):
@ -66,7 +66,7 @@ class TestStandbyMode(unittest.TestCase):
invalid_info['urls'] = []
self.assertRaises(errors.InvalidCommandParamsError,
self.agent_mode._validate_image_info,
standby._validate_image_info,
invalid_info)
def test_validate_image_info_invalid_hashes(self):
@ -74,7 +74,7 @@ class TestStandbyMode(unittest.TestCase):
invalid_info['hashes'] = 'this_is_not_a_dict'
self.assertRaises(errors.InvalidCommandParamsError,
self.agent_mode._validate_image_info,
standby._validate_image_info,
invalid_info)
def test_validate_image_info_empty_hashes(self):
@ -82,7 +82,7 @@ class TestStandbyMode(unittest.TestCase):
invalid_info['hashes'] = {}
self.assertRaises(errors.InvalidCommandParamsError,
self.agent_mode._validate_image_info,
standby._validate_image_info,
invalid_info)
def test_cache_image_success(self):
@ -217,46 +217,72 @@ class TestStandbyMode(unittest.TestCase):
self.assertFalse(verified)
self.assertEqual(md5_mock.call_count, 1)
@mock.patch('teeth_agent.standby._write_image', autospec=True)
@mock.patch('teeth_agent.standby._download_image', autospec=True)
def test_cache_image(self, download_mock, write_mock):
image_info = self._build_fake_image_info()
download_mock.return_value = None
write_mock.return_value = None
async_result = self.agent_mode.cache_image(image_info)
while not async_result.is_done():
time.sleep(0.01)
download_mock.assert_called_once_with(image_info)
write_mock.assert_called_once_with(image_info, None)
self.assertEqual('SUCCEEDED', async_result.command_status)
self.assertEqual(None, async_result.command_result)
@mock.patch('teeth_agent.standby._copy_configdrive_to_disk', autospec=True)
@mock.patch('teeth_agent.standby.configdrive.write_configdrive',
autospec=True)
@mock.patch('teeth_agent.hardware.get_manager', autospec=True)
@mock.patch('teeth_agent.standby._write_image', autospec=True)
@mock.patch('teeth_agent.standby._download_image', autospec=True)
@mock.patch('teeth_agent.standby._configdrive_location', autospec=True)
def test_prepare_image(self,
location_mock,
download_mock,
write_mock,
hardware_mock,
configdrive_mock,
configdrive_copy_mock):
image_info = self._build_fake_image_info()
location_mock.return_value = "THE CLOUD"
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"
configdrive_mock.return_value = None
configdrive_copy_mock.return_value = None
async_result = self.agent_mode.prepare_image(image_info, {}, [])
while not async_result.is_done():
time.sleep(0.01)
download_mock.assert_called_once_with(image_info)
write_mock.assert_called_once_with(image_info, "manager")
configdrive_mock.assert_called_once_with("THE CLOUD", {}, [])
configdrive_copy_mock.assert_called_once_with("THE CLOUD", "manager")
self.assertEqual('SUCCEEDED', async_result.command_status)
self.assertEqual(None, async_result.command_result)
@mock.patch('subprocess.call', autospec=True)
def test_run_image(self, call_mock):
script = standby._path_to_script('shell/reboot.sh')
command = ['/bin/bash', script]
call_mock.return_value = 0
self.agent_mode._thread_run_image('run_image')
success_result = self.agent_mode.run_image()
while not success_result.is_done():
time.sleep(0.01)
call_mock.assert_called_once_with(command)
call_mock.reset_mock()
call_mock.return_value = 1
with self.assertRaises(errors.SystemRebootError):
self.agent_mode._thread_run_image('run_image')
failed_result = self.agent_mode.run_image()
while not failed_result.is_done():
time.sleep(0.01)
call_mock.assert_called_once_with(command)
def test_cache_image_async(self):
image_info = self._build_fake_image_info()
self.agent_mode._thread_cache_image = mock.Mock(return_value='test')
async_result = self.agent_mode.cache_image(image_info)
while not async_result.is_done():
time.sleep(0.1)
self.assertEqual('SUCCEEDED', async_result.command_status)
self.assertEqual('test', async_result.command_result)
def test_prepare_image_async(self):
image_info = self._build_fake_image_info()
self.agent_mode._thread_prepare_image = mock.Mock(return_value='test')
async_result = self.agent_mode.prepare_image(image_info, {}, [])
while not async_result.is_done():
time.sleep(0.1)
self.assertEqual('SUCCEEDED', async_result.command_status)
self.assertEqual('test', async_result.command_result)
def test_run_image_async(self):
image_info = self._build_fake_image_info()
self.agent_mode._thread_run_image = mock.Mock(return_value='test')
async_result = self.agent_mode.run_image(image_info)
while not async_result.is_done():
time.sleep(0.1)
self.assertEqual('SUCCEEDED', async_result.command_status)
self.assertEqual('test', async_result.command_result)
self.assertEqual('FAILED', failed_result.command_status)