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:
parent
7c22a4e3bf
commit
1de60e9262
teeth_agent
47
teeth_agent/decorators.py
Normal file
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)
|
||||
|
Loading…
x
Reference in New Issue
Block a user