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:
@@ -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)
|
||||
|
||||
|
||||
@@ -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)
|
||||
|
||||
Reference in New Issue
Block a user