Agent supports post-clean-step operations

This adds a new decorator in agent_base_vendor:
post_clean_step_hook(interface, step). A method with that decorator
will be invoked after the successful completion of the (clean) step.
The method itself should take two arguments: 1. the task, 2. the
command object corresponding to the completed clean step that was
returned from IPA as a result of a 'get_commands_status' request to
IPA.

Change-Id: I2d6ef6f81c134e9b3d38fea956e86cfc5c3c4ad4
Depends-On: I85220ffe9842576d73b91d194cfdaca6c437ebd2
Implements: blueprint inband-raid-configuration
Co-Authored-By: Ramakrishnan G <rameshg87@gmail.com>
This commit is contained in:
Ruby Loo
2015-09-10 16:25:58 +00:00
committed by Ramakrishnan G
parent cba0eaf903
commit 45e55a0940
2 changed files with 238 additions and 10 deletions

View File

@@ -60,6 +60,27 @@ CONF.register_opts(agent_opts, group='agent')
LOG = log.getLogger(__name__)
# This contains a nested dictionary containing the post clean step
# hooks registered for each clean step of every interface.
# Every key of POST_CLEAN_STEP_HOOKS is an interface and its value
# is a dictionary. For this inner dictionary, the key is the name of
# the clean-step method in the interface, and the value is the post
# clean-step hook -- the function that is to be called after successful
# completion of the clean step.
#
# For example:
# POST_CLEAN_STEP_HOOKS =
# {
# 'raid': {'create_configuration': <post-create function>,
# 'delete_configuration': <post-delete function>}
# }
#
# It means that method '<post-create function>' is to be called after
# successfully completing the clean step 'create_configuration' of
# raid interface. '<post-delete function>' is to be called after
# completing 'delete_configuration' of raid interface.
POST_CLEAN_STEP_HOOKS = {}
def _time():
"""Broken out for testing."""
@@ -71,6 +92,51 @@ def _get_client():
return client
def post_clean_step_hook(interface, step):
"""Decorator method for adding a post clean step hook.
This is a mechanism for adding a post clean step hook for a particular
clean step. The hook will get executed after the clean step gets executed
successfully. The hook is not invoked on failure of the clean step.
Any method to be made as a hook may be decorated with @post_clean_step_hook
mentioning the interface and step after which the hook should be executed.
A TaskManager instance and the object for the last completed command
(provided by agent) will be passed to the hook method. The return value of
this method will be ignored. Any exception raised by this method will be
treated as a failure of the clean step and the node will be moved to
CLEANFAIL state.
:param interface: name of the interface
:param step: The name of the step after which it should be executed.
:returns: A method which registers the given method as a post clean
step hook.
"""
def decorator(func):
POST_CLEAN_STEP_HOOKS.setdefault(interface, {})[step] = func
return func
return decorator
def _get_post_clean_step_hook(node):
"""Get post clean step hook for the currently executing clean step.
This method reads node.clean_step and returns the post clean
step hook for the currently executing clean step.
:param node: a node object
:returns: a method if there is a post clean step hook for this clean
step; None otherwise
"""
interface = node.clean_step.get('interface')
step = node.clean_step.get('step')
try:
return POST_CLEAN_STEP_HOOKS[interface][step]
except KeyError:
pass
class BaseAgentVendor(base.VendorInterface):
def __init__(self):
@@ -165,10 +231,11 @@ class BaseAgentVendor(base.VendorInterface):
to tell if an ordering change will cause a cleaning issue. Therefore,
we restart cleaning.
"""
node = task.node
command = self._get_completed_cleaning_command(task)
LOG.debug('Cleaning command status for node %(node)s on step %(step)s:'
' %(command)s', {'node': task.node.uuid,
'step': task.node.clean_step,
' %(command)s', {'node': node.uuid,
'step': node.clean_step,
'command': command})
if not command:
@@ -178,38 +245,58 @@ class BaseAgentVendor(base.VendorInterface):
if command.get('command_status') == 'FAILED':
msg = (_('Agent returned error for clean step %(step)s on node '
'%(node)s : %(err)s.') %
{'node': task.node.uuid,
{'node': node.uuid,
'err': command.get('command_error'),
'step': task.node.clean_step})
'step': node.clean_step})
LOG.error(msg)
return manager.cleaning_error_handler(task, msg)
elif command.get('command_status') == 'CLEAN_VERSION_MISMATCH':
# Restart cleaning, agent must have rebooted to new version
LOG.info(_LI('Node %s detected a clean version mismatch, '
'resetting clean steps and rebooting the node.'),
task.node.uuid)
node.uuid)
try:
manager.set_node_cleaning_steps(task)
except exception.NodeCleaningFailure:
msg = (_('Could not restart cleaning on node %(node)s: '
'%(err)s.') %
{'node': task.node.uuid,
{'node': node.uuid,
'err': command.get('command_error'),
'step': task.node.clean_step})
'step': node.clean_step})
LOG.exception(msg)
return manager.cleaning_error_handler(task, msg)
self._notify_conductor_resume_clean(task)
elif command.get('command_status') == 'SUCCEEDED':
clean_step_hook = _get_post_clean_step_hook(node)
if clean_step_hook is not None:
LOG.debug('For node %(node)s, executing post clean step '
'hook %(method)s for clean step %(step)s' %
{'method': clean_step_hook.__name__,
'node': node.uuid,
'step': node.clean_step})
try:
clean_step_hook(task, command)
except Exception as e:
msg = (_('For node %(node)s, post clean step hook '
'%(method)s failed for clean step %(step)s.'
'Error: %(error)s') %
{'method': clean_step_hook.__name__,
'node': node.uuid,
'error': e,
'step': node.clean_step})
LOG.exception(msg)
return manager.cleaning_error_handler(task, msg)
LOG.info(_LI('Agent on node %s returned cleaning command success, '
'moving to next clean step'), task.node.uuid)
'moving to next clean step'), node.uuid)
self._notify_conductor_resume_clean(task)
else:
msg = (_('Agent returned unknown status for clean step %(step)s '
'on node %(node)s : %(err)s.') %
{'node': task.node.uuid,
{'node': node.uuid,
'err': command.get('command_status'),
'step': task.node.clean_step})
'step': node.clean_step})
LOG.error(msg)
return manager.cleaning_error_handler(task, msg)

View File

@@ -709,6 +709,68 @@ class TestBaseAgentVendor(db_base.DbTestCase):
self.passthru.continue_cleaning(task)
notify_mock.assert_called_once_with(mock.ANY, task)
@mock.patch.object(agent_base_vendor,
'_get_post_clean_step_hook', autospec=True)
@mock.patch.object(agent_base_vendor.BaseAgentVendor,
'_notify_conductor_resume_clean', autospec=True)
@mock.patch.object(agent_client.AgentClient, 'get_commands_status',
autospec=True)
def test_continue_cleaning_with_hook(
self, status_mock, notify_mock, get_hook_mock):
self.node.clean_step = {
'priority': 10,
'interface': 'raid',
'step': 'create_configuration',
}
self.node.save()
command_status = {
'command_status': 'SUCCEEDED',
'command_name': 'execute_clean_step',
'command_result': {'clean_step': self.node.clean_step}}
status_mock.return_value = [command_status]
hook_mock = mock.MagicMock(spec=types.FunctionType, __name__='foo')
get_hook_mock.return_value = hook_mock
with task_manager.acquire(self.context, self.node.uuid,
shared=False) as task:
self.passthru.continue_cleaning(task)
get_hook_mock.assert_called_once_with(task.node)
hook_mock.assert_called_once_with(task, command_status)
notify_mock.assert_called_once_with(mock.ANY, task)
@mock.patch.object(agent_base_vendor.BaseAgentVendor,
'_notify_conductor_resume_clean', autospec=True)
@mock.patch.object(agent_base_vendor,
'_get_post_clean_step_hook', autospec=True)
@mock.patch.object(manager, 'cleaning_error_handler', autospec=True)
@mock.patch.object(agent_client.AgentClient, 'get_commands_status',
autospec=True)
def test_continue_cleaning_with_hook_fails(
self, status_mock, error_handler_mock, get_hook_mock,
notify_mock):
self.node.clean_step = {
'priority': 10,
'interface': 'raid',
'step': 'create_configuration',
}
self.node.save()
command_status = {
'command_status': 'SUCCEEDED',
'command_name': 'execute_clean_step',
'command_result': {'clean_step': self.node.clean_step}}
status_mock.return_value = [command_status]
hook_mock = mock.MagicMock(spec=types.FunctionType, __name__='foo')
hook_mock.side_effect = RuntimeError('error')
get_hook_mock.return_value = hook_mock
with task_manager.acquire(self.context, self.node.uuid,
shared=False) as task:
self.passthru.continue_cleaning(task)
get_hook_mock.assert_called_once_with(task.node)
hook_mock.assert_called_once_with(task, command_status)
error_handler_mock.assert_called_once_with(task, mock.ANY)
self.assertFalse(notify_mock.called)
@mock.patch.object(agent_base_vendor.BaseAgentVendor,
'_notify_conductor_resume_clean', autospec=True)
@mock.patch.object(agent_client.AgentClient, 'get_commands_status',
@@ -805,3 +867,82 @@ class TestBaseAgentVendor(db_base.DbTestCase):
shared=False) as task:
self.passthru.continue_cleaning(task)
error_mock.assert_called_once_with(task, mock.ANY)
def _test_clean_step_hook(self, hook_dict_mock):
"""Helper method for unit tests related to clean step hooks.
This is a helper method for other unit tests related to
clean step hooks. It acceps a mock 'hook_dict_mock' which is
a MagicMock and sets it up to function as a mock dictionary.
After that, it defines a dummy hook_method for two clean steps
raid.create_configuration and raid.delete_configuration.
:param hook_dict_mock: An instance of mock.MagicMock() which
is the mocked value of agent_base_vendor.POST_CLEAN_STEP_HOOKS
:returns: a tuple, where the first item is the hook method created
by this method and second item is the backend dictionary for
the mocked hook_dict_mock
"""
hook_dict = {}
def get(key, default):
return hook_dict.get(key, default)
def getitem(self, key):
return hook_dict[key]
def setdefault(key, default):
if key not in hook_dict:
hook_dict[key] = default
return hook_dict[key]
hook_dict_mock.get = get
hook_dict_mock.__getitem__ = getitem
hook_dict_mock.setdefault = setdefault
some_function_mock = mock.MagicMock()
@agent_base_vendor.post_clean_step_hook(
interface='raid', step='delete_configuration')
@agent_base_vendor.post_clean_step_hook(
interface='raid', step='create_configuration')
def hook_method():
some_function_mock('some-arguments')
return hook_method, hook_dict
@mock.patch.object(agent_base_vendor, 'POST_CLEAN_STEP_HOOKS',
spec_set=dict)
def test_post_clean_step_hook(self, hook_dict_mock):
# This unit test makes sure that hook methods are registered
# properly and entries are made in
# agent_base_vendor.POST_CLEAN_STEP_HOOKS
hook_method, hook_dict = self._test_clean_step_hook(hook_dict_mock)
self.assertEqual(hook_method,
hook_dict['raid']['create_configuration'])
self.assertEqual(hook_method,
hook_dict['raid']['delete_configuration'])
@mock.patch.object(agent_base_vendor, 'POST_CLEAN_STEP_HOOKS',
spec_set=dict)
def test__get_post_clean_step_hook(self, hook_dict_mock):
# Check if agent_base_vendor._get_post_clean_step_hook can get
# clean step for which hook is registered.
hook_method, hook_dict = self._test_clean_step_hook(hook_dict_mock)
self.node.clean_step = {'step': 'create_configuration',
'interface': 'raid'}
self.node.save()
hook_returned = agent_base_vendor._get_post_clean_step_hook(self.node)
self.assertEqual(hook_method, hook_returned)
@mock.patch.object(agent_base_vendor, 'POST_CLEAN_STEP_HOOKS',
spec_set=dict)
def test__get_post_clean_step_hook_no_hook_registered(
self, hook_dict_mock):
# Make sure agent_base_vendor._get_post_clean_step_hook returns
# None when no clean step hook is registered for the clean step.
hook_method, hook_dict = self._test_clean_step_hook(hook_dict_mock)
self.node.clean_step = {'step': 'some-clean-step',
'interface': 'some-other-interface'}
self.node.save()
hook_returned = agent_base_vendor._get_post_clean_step_hook(self.node)
self.assertIsNone(hook_returned)