Cancel Action Plan

This patch adds feature to cancel action plan in watcher.
A General flow from watcher-api to watcher-applier is implemented.

action plan cancel can cancel any [ongoing, pending, recommended]
action plan, it will update the action states also to "cancelled".
For ongoing actions in action plan, actions needs to be aborted.
Seperate patches will be added to support abort operation
in each action.

Notification part is addressed by a seperate blueprint.
https://blueprints.launchpad.net/watcher/+spec/notifications-actionplan-cancel

Change-Id: I895a5eaca5239d5657702c8d1875b9ece21682dc
Partially-Implements: blueprint cancel-action-plan
This commit is contained in:
aditi 2017-05-26 11:40:36 +00:00
parent 58d86de064
commit d7a44739a6
16 changed files with 362 additions and 28 deletions

View File

@ -0,0 +1,4 @@
---
features:
- |
Adds feature to cancel an action-plan.

View File

@ -488,6 +488,7 @@ class ActionPlansController(rest.RestController):
raise exception.PatchError(patch=patch, reason=e) raise exception.PatchError(patch=patch, reason=e)
launch_action_plan = False launch_action_plan = False
cancel_action_plan = False
# transitions that are allowed via PATCH # transitions that are allowed via PATCH
allowed_patch_transitions = [ allowed_patch_transitions = [
@ -496,7 +497,7 @@ class ActionPlansController(rest.RestController):
(ap_objects.State.RECOMMENDED, (ap_objects.State.RECOMMENDED,
ap_objects.State.CANCELLED), ap_objects.State.CANCELLED),
(ap_objects.State.ONGOING, (ap_objects.State.ONGOING,
ap_objects.State.CANCELLED), ap_objects.State.CANCELLING),
(ap_objects.State.PENDING, (ap_objects.State.PENDING,
ap_objects.State.CANCELLED), ap_objects.State.CANCELLED),
] ]
@ -515,6 +516,8 @@ class ActionPlansController(rest.RestController):
if action_plan.state == ap_objects.State.PENDING: if action_plan.state == ap_objects.State.PENDING:
launch_action_plan = True launch_action_plan = True
if action_plan.state == ap_objects.State.CANCELLED:
cancel_action_plan = True
# Update only the fields that have changed # Update only the fields that have changed
for field in objects.ActionPlan.fields: for field in objects.ActionPlan.fields:
@ -534,6 +537,16 @@ class ActionPlansController(rest.RestController):
action_plan_to_update.save() action_plan_to_update.save()
# NOTE: if action plan is cancelled from pending or recommended
# state update action state here only
if cancel_action_plan:
filters = {'action_plan_uuid': action_plan.uuid}
actions = objects.Action.list(pecan.request.context,
filters=filters, eager=True)
for a in actions:
a.state = objects.action.State.CANCELLED
a.save()
if launch_action_plan: if launch_action_plan:
applier_client = rpcapi.ApplierAPI() applier_client = rpcapi.ApplierAPI()
applier_client.launch_action_plan(pecan.request.context, applier_client.launch_action_plan(pecan.request.context,

View File

@ -20,6 +20,7 @@ from oslo_log import log
from watcher.applier.action_plan import base from watcher.applier.action_plan import base
from watcher.applier import default from watcher.applier import default
from watcher.common import exception
from watcher import notifications from watcher import notifications
from watcher import objects from watcher import objects
from watcher.objects import fields from watcher.objects import fields
@ -39,6 +40,9 @@ class DefaultActionPlanHandler(base.BaseActionPlanHandler):
try: try:
action_plan = objects.ActionPlan.get_by_uuid( action_plan = objects.ActionPlan.get_by_uuid(
self.ctx, self.action_plan_uuid, eager=True) self.ctx, self.action_plan_uuid, eager=True)
if action_plan.state == objects.action_plan.State.CANCELLED:
self._update_action_from_pending_to_cancelled()
return
action_plan.state = objects.action_plan.State.ONGOING action_plan.state = objects.action_plan.State.ONGOING
action_plan.save() action_plan.save()
notifications.action_plan.send_action_notification( notifications.action_plan.send_action_notification(
@ -54,6 +58,12 @@ class DefaultActionPlanHandler(base.BaseActionPlanHandler):
self.ctx, action_plan, self.ctx, action_plan,
action=fields.NotificationAction.EXECUTION, action=fields.NotificationAction.EXECUTION,
phase=fields.NotificationPhase.END) phase=fields.NotificationPhase.END)
except exception.ActionPlanCancelled as e:
LOG.exception(e)
action_plan.state = objects.action_plan.State.CANCELLED
self._update_action_from_pending_to_cancelled()
except Exception as e: except Exception as e:
LOG.exception(e) LOG.exception(e)
action_plan.state = objects.action_plan.State.FAILED action_plan.state = objects.action_plan.State.FAILED
@ -64,3 +74,12 @@ class DefaultActionPlanHandler(base.BaseActionPlanHandler):
phase=fields.NotificationPhase.ERROR) phase=fields.NotificationPhase.ERROR)
finally: finally:
action_plan.save() action_plan.save()
def _update_action_from_pending_to_cancelled(self):
filters = {'action_plan_uuid': self.action_plan_uuid,
'state': objects.action.State.PENDING}
actions = objects.Action.list(self.ctx, filters=filters, eager=True)
if actions:
for a in actions:
a.state = objects.action.State.CANCELLED
a.save()

View File

@ -32,6 +32,9 @@ class BaseAction(loadable.Loadable):
# watcher dashboard and will be nested in input_parameters # watcher dashboard and will be nested in input_parameters
RESOURCE_ID = 'resource_id' RESOURCE_ID = 'resource_id'
# Add action class name to the list, if implementing abort.
ABORT_TRUE = ['Sleep', 'Nop']
def __init__(self, config, osc=None): def __init__(self, config, osc=None):
"""Constructor """Constructor
@ -134,3 +137,6 @@ class BaseAction(loadable.Loadable):
def get_description(self): def get_description(self):
"""Description of the action""" """Description of the action"""
raise NotImplementedError() raise NotImplementedError()
def check_abort(self):
return bool(self.__class__.__name__ in self.ABORT_TRUE)

View File

@ -164,6 +164,10 @@ class Migrate(base.BaseAction):
def revert(self): def revert(self):
return self.migrate(destination=self.source_node) return self.migrate(destination=self.source_node)
def abort(self):
# TODO(adisky): implement abort for migration
LOG.warning("Abort for migration not implemented")
def pre_condition(self): def pre_condition(self):
# TODO(jed): check if the instance exists / check if the instance is on # TODO(jed): check if the instance exists / check if the instance is on
# the source_node # the source_node

View File

@ -23,7 +23,6 @@ import voluptuous
from watcher.applier.actions import base from watcher.applier.actions import base
LOG = log.getLogger(__name__) LOG = log.getLogger(__name__)
@ -69,3 +68,6 @@ class Nop(base.BaseAction):
def get_description(self): def get_description(self):
"""Description of the action""" """Description of the action"""
return "Logging a NOP message" return "Logging a NOP message"
def abort(self):
LOG.debug("Abort action NOP")

View File

@ -70,3 +70,6 @@ class Sleep(base.BaseAction):
def get_description(self): def get_description(self):
"""Description of the action""" """Description of the action"""
return "Wait for a given interval in seconds." return "Wait for a given interval in seconds."
def abort(self):
LOG.debug("Abort action sleep")

View File

@ -17,13 +17,17 @@
# #
import abc import abc
import six
import time
import eventlet
from oslo_log import log from oslo_log import log
import six
from taskflow import task as flow_task from taskflow import task as flow_task
from watcher.applier.actions import factory from watcher.applier.actions import factory
from watcher.common import clients from watcher.common import clients
from watcher.common import exception
from watcher.common.loader import loadable from watcher.common.loader import loadable
from watcher import notifications from watcher import notifications
from watcher import objects from watcher import objects
@ -32,6 +36,9 @@ from watcher.objects import fields
LOG = log.getLogger(__name__) LOG = log.getLogger(__name__)
CANCEL_STATE = [objects.action_plan.State.CANCELLING,
objects.action_plan.State.CANCELLED]
@six.add_metaclass(abc.ABCMeta) @six.add_metaclass(abc.ABCMeta)
class BaseWorkFlowEngine(loadable.Loadable): class BaseWorkFlowEngine(loadable.Loadable):
@ -81,6 +88,10 @@ class BaseWorkFlowEngine(loadable.Loadable):
def notify(self, action, state): def notify(self, action, state):
db_action = objects.Action.get_by_uuid(self.context, action.uuid, db_action = objects.Action.get_by_uuid(self.context, action.uuid,
eager=True) eager=True)
if (db_action.state in [objects.action.State.CANCELLING,
objects.action.State.CANCELLED] and
state == objects.action.State.SUCCEEDED):
return
db_action.state = state db_action.state = state
db_action.save() db_action.save()
@ -122,16 +133,34 @@ class BaseTaskFlowActionContainer(flow_task.Task):
def do_post_execute(self): def do_post_execute(self):
raise NotImplementedError() raise NotImplementedError()
@abc.abstractmethod
def do_revert(self):
raise NotImplementedError()
@abc.abstractmethod
def do_abort(self, *args, **kwargs):
raise NotImplementedError()
# NOTE(alexchadin): taskflow does 3 method calls (pre_execute, execute, # NOTE(alexchadin): taskflow does 3 method calls (pre_execute, execute,
# post_execute) independently. We want to support notifications in base # post_execute) independently. We want to support notifications in base
# class, so child's methods should be named with `do_` prefix and wrapped. # class, so child's methods should be named with `do_` prefix and wrapped.
def pre_execute(self): def pre_execute(self):
try: try:
# NOTE(adisky): check the state of action plan before starting
# next action, if action plan is cancelled raise the exceptions
# so that taskflow does not schedule further actions.
action_plan = objects.ActionPlan.get_by_id(
self.engine.context, self._db_action.action_plan_id)
if action_plan.state in CANCEL_STATE:
raise exception.ActionPlanCancelled(uuid=action_plan.uuid)
self.do_pre_execute() self.do_pre_execute()
notifications.action.send_execution_notification( notifications.action.send_execution_notification(
self.engine.context, self._db_action, self.engine.context, self._db_action,
fields.NotificationAction.EXECUTION, fields.NotificationAction.EXECUTION,
fields.NotificationPhase.START) fields.NotificationPhase.START)
except exception.ActionPlanCancelled as e:
LOG.exception(e)
raise
except Exception as e: except Exception as e:
LOG.exception(e) LOG.exception(e)
self.engine.notify(self._db_action, objects.action.State.FAILED) self.engine.notify(self._db_action, objects.action.State.FAILED)
@ -142,22 +171,59 @@ class BaseTaskFlowActionContainer(flow_task.Task):
priority=fields.NotificationPriority.ERROR) priority=fields.NotificationPriority.ERROR)
def execute(self, *args, **kwargs): def execute(self, *args, **kwargs):
def _do_execute_action(*args, **kwargs):
try:
self.do_execute(*args, **kwargs)
notifications.action.send_execution_notification(
self.engine.context, self._db_action,
fields.NotificationAction.EXECUTION,
fields.NotificationPhase.END)
except Exception as e:
LOG.exception(e)
LOG.error('The workflow engine has failed'
'to execute the action: %s', self.name)
self.engine.notify(self._db_action,
objects.action.State.FAILED)
notifications.action.send_execution_notification(
self.engine.context, self._db_action,
fields.NotificationAction.EXECUTION,
fields.NotificationPhase.ERROR,
priority=fields.NotificationPriority.ERROR)
raise
# NOTE: spawn a new thread for action execution, so that if action plan
# is cancelled workflow engine will not wait to finish action execution
et = eventlet.spawn(_do_execute_action, *args, **kwargs)
# NOTE: check for the state of action plan periodically,so that if
# action is finished or action plan is cancelled we can exit from here.
while True:
action_object = objects.Action.get_by_uuid(
self.engine.context, self._db_action.uuid, eager=True)
action_plan_object = objects.ActionPlan.get_by_id(
self.engine.context, action_object.action_plan_id)
if (action_object.state in [objects.action.State.SUCCEEDED,
objects.action.State.FAILED] or
action_plan_object.state in CANCEL_STATE):
break
time.sleep(2)
try: try:
self.do_execute(*args, **kwargs) # NOTE: kill the action execution thread, if action plan is
notifications.action.send_execution_notification( # cancelled for all other cases wait for the result from action
self.engine.context, self._db_action, # execution thread.
fields.NotificationAction.EXECUTION, # Not all actions support abort operations, kill only those action
fields.NotificationPhase.END) # which support abort operations
abort = self.action.check_abort()
if (action_plan_object.state in CANCEL_STATE and abort):
et.kill()
et.wait()
# NOTE: catch the greenlet exit exception due to thread kill,
# taskflow will call revert for the action,
# we will redirect it to abort.
except eventlet.greenlet.GreenletExit:
raise exception.ActionPlanCancelled(uuid=action_plan_object.uuid)
except Exception as e: except Exception as e:
LOG.exception(e) LOG.exception(e)
LOG.error('The workflow engine has failed '
'to execute the action: %s', self.name)
self.engine.notify(self._db_action, objects.action.State.FAILED)
notifications.action.send_execution_notification(
self.engine.context, self._db_action,
fields.NotificationAction.EXECUTION,
fields.NotificationPhase.ERROR,
priority=fields.NotificationPriority.ERROR)
raise raise
def post_execute(self): def post_execute(self):
@ -171,3 +237,24 @@ class BaseTaskFlowActionContainer(flow_task.Task):
fields.NotificationAction.EXECUTION, fields.NotificationAction.EXECUTION,
fields.NotificationPhase.ERROR, fields.NotificationPhase.ERROR,
priority=fields.NotificationPriority.ERROR) priority=fields.NotificationPriority.ERROR)
def revert(self, *args, **kwargs):
action_plan = objects.ActionPlan.get_by_id(
self.engine.context, self._db_action.action_plan_id, eager=True)
# NOTE: check if revert cause by cancel action plan or
# some other exception occured during action plan execution
# if due to some other exception keep the flow intact.
if action_plan.state not in CANCEL_STATE:
self.do_revert()
action_object = objects.Action.get_by_uuid(
self.engine.context, self._db_action.uuid, eager=True)
if action_object.state == objects.action.State.ONGOING:
action_object.state = objects.action.State.CANCELLING
action_object.save()
self.abort()
if action_object.state == objects.action.State.PENDING:
action_object.state = objects.action.State.CANCELLED
action_object.save()
def abort(self, *args, **kwargs):
self.do_abort(*args, **kwargs)

View File

@ -19,6 +19,7 @@ from oslo_concurrency import processutils
from oslo_config import cfg from oslo_config import cfg
from oslo_log import log from oslo_log import log
from taskflow import engines from taskflow import engines
from taskflow import exceptions as tf_exception
from taskflow.patterns import graph_flow as gf from taskflow.patterns import graph_flow as gf
from taskflow import task as flow_task from taskflow import task as flow_task
@ -90,6 +91,15 @@ class DefaultWorkFlowEngine(base.BaseWorkFlowEngine):
return flow return flow
except exception.ActionPlanCancelled as e:
raise
except tf_exception.WrappedFailure as e:
if e.check("watcher.common.exception.ActionPlanCancelled"):
raise exception.ActionPlanCancelled
else:
raise exception.WorkflowExecutionException(error=e)
except Exception as e: except Exception as e:
raise exception.WorkflowExecutionException(error=e) raise exception.WorkflowExecutionException(error=e)
@ -121,7 +131,7 @@ class TaskFlowActionContainer(base.BaseTaskFlowActionContainer):
LOG.debug("Post-condition action: %s", self.name) LOG.debug("Post-condition action: %s", self.name)
self.action.post_condition() self.action.post_condition()
def revert(self, *args, **kwargs): def do_revert(self, *args, **kwargs):
LOG.warning("Revert action: %s", self.name) LOG.warning("Revert action: %s", self.name)
try: try:
# TODO(jed): do we need to update the states in case of failure? # TODO(jed): do we need to update the states in case of failure?
@ -130,6 +140,15 @@ class TaskFlowActionContainer(base.BaseTaskFlowActionContainer):
LOG.exception(e) LOG.exception(e)
LOG.critical("Oops! We need a disaster recover plan.") LOG.critical("Oops! We need a disaster recover plan.")
def do_abort(self, *args, **kwargs):
LOG.warning("Aborting action: %s", self.name)
try:
self.action.abort()
self.engine.notify(self._db_action, objects.action.State.CANCELLED)
except Exception as e:
self.engine.notify(self._db_action, objects.action.State.FAILED)
LOG.exception(e)
class TaskFlowNop(flow_task.Task): class TaskFlowNop(flow_task.Task):
"""This class is used in case of the workflow have only one Action. """This class is used in case of the workflow have only one Action.

View File

@ -274,6 +274,10 @@ class ActionPlanReferenced(Invalid):
"multiple actions") "multiple actions")
class ActionPlanCancelled(WatcherException):
msg_fmt = _("Action Plan with UUID %(uuid)s is cancelled by user")
class ActionPlanIsOngoing(Conflict): class ActionPlanIsOngoing(Conflict):
msg_fmt = _("Action Plan %(action_plan)s is currently running.") msg_fmt = _("Action Plan %(action_plan)s is currently running.")

View File

@ -30,6 +30,7 @@ class State(object):
SUCCEEDED = 'SUCCEEDED' SUCCEEDED = 'SUCCEEDED'
DELETED = 'DELETED' DELETED = 'DELETED'
CANCELLED = 'CANCELLED' CANCELLED = 'CANCELLED'
CANCELLING = 'CANCELLING'
@base.WatcherObjectRegistry.register @base.WatcherObjectRegistry.register

View File

@ -94,6 +94,7 @@ class State(object):
DELETED = 'DELETED' DELETED = 'DELETED'
CANCELLED = 'CANCELLED' CANCELLED = 'CANCELLED'
SUPERSEDED = 'SUPERSEDED' SUPERSEDED = 'SUPERSEDED'
CANCELLING = 'CANCELLING'
@base.WatcherObjectRegistry.register @base.WatcherObjectRegistry.register

View File

@ -456,7 +456,7 @@ ALLOWED_TRANSITIONS = [
{"original_state": objects.action_plan.State.RECOMMENDED, {"original_state": objects.action_plan.State.RECOMMENDED,
"new_state": objects.action_plan.State.CANCELLED}, "new_state": objects.action_plan.State.CANCELLED},
{"original_state": objects.action_plan.State.ONGOING, {"original_state": objects.action_plan.State.ONGOING,
"new_state": objects.action_plan.State.CANCELLED}, "new_state": objects.action_plan.State.CANCELLING},
{"original_state": objects.action_plan.State.PENDING, {"original_state": objects.action_plan.State.PENDING,
"new_state": objects.action_plan.State.CANCELLED}, "new_state": objects.action_plan.State.CANCELLED},
] ]

View File

@ -19,6 +19,7 @@ import mock
from watcher.applier.action_plan import default from watcher.applier.action_plan import default
from watcher.applier import default as ap_applier from watcher.applier import default as ap_applier
from watcher.common import exception
from watcher import notifications from watcher import notifications
from watcher import objects from watcher import objects
from watcher.objects import action_plan as ap_objects from watcher.objects import action_plan as ap_objects
@ -99,3 +100,27 @@ class TestDefaultActionPlanHandler(base.DbTestCase):
self.m_action_plan_notifications self.m_action_plan_notifications
.send_action_notification .send_action_notification
.call_args_list) .call_args_list)
@mock.patch.object(objects.ActionPlan, "get_by_uuid")
def test_cancel_action_plan(self, m_get_action_plan):
m_get_action_plan.return_value = self.action_plan
self.action_plan.state = ap_objects.State.CANCELLED
self.action_plan.save()
command = default.DefaultActionPlanHandler(
self.context, mock.MagicMock(), self.action_plan.uuid)
command.execute()
action = self.action.get_by_uuid(self.context, self.action.uuid)
self.assertEqual(ap_objects.State.CANCELLED, self.action_plan.state)
self.assertEqual(objects.action.State.CANCELLED, action.state)
@mock.patch.object(ap_applier.DefaultApplier, "execute")
@mock.patch.object(objects.ActionPlan, "get_by_uuid")
def test_cancel_action_plan_with_exception(self, m_get_action_plan,
m_execute):
m_get_action_plan.return_value = self.action_plan
m_execute.side_effect = exception.ActionPlanCancelled(
self.action_plan.uuid)
command = default.DefaultActionPlanHandler(
self.context, mock.MagicMock(), self.action_plan.uuid)
command.execute()
self.assertEqual(ap_objects.State.CANCELLED, self.action_plan.state)

View File

@ -29,6 +29,7 @@ from watcher.common import utils
from watcher import notifications from watcher import notifications
from watcher import objects from watcher import objects
from watcher.tests.db import base from watcher.tests.db import base
from watcher.tests.objects import utils as obj_utils
class ExpectedException(Exception): class ExpectedException(Exception):
@ -75,7 +76,8 @@ class TestDefaultWorkFlowEngine(base.DbTestCase):
except Exception as exc: except Exception as exc:
self.fail(exc) self.fail(exc)
def create_action(self, action_type, parameters, parents=None, uuid=None): def create_action(self, action_type, parameters, parents=None, uuid=None,
state=None):
action = { action = {
'uuid': uuid or utils.generate_uuid(), 'uuid': uuid or utils.generate_uuid(),
'action_plan_id': 0, 'action_plan_id': 0,
@ -88,7 +90,6 @@ class TestDefaultWorkFlowEngine(base.DbTestCase):
new_action = objects.Action(self.context, **action) new_action = objects.Action(self.context, **action)
with mock.patch.object(notifications.action, 'send_create'): with mock.patch.object(notifications.action, 'send_create'):
new_action.create() new_action.create()
return new_action return new_action
def check_action_state(self, action, expected_state): def check_action_state(self, action, expected_state):
@ -110,10 +111,14 @@ class TestDefaultWorkFlowEngine(base.DbTestCase):
except Exception as exc: except Exception as exc:
self.fail(exc) self.fail(exc)
@mock.patch.object(objects.ActionPlan, "get_by_id")
@mock.patch.object(notifications.action, 'send_execution_notification') @mock.patch.object(notifications.action, 'send_execution_notification')
@mock.patch.object(notifications.action, 'send_update') @mock.patch.object(notifications.action, 'send_update')
def test_execute_with_one_action(self, mock_send_update, def test_execute_with_one_action(self, mock_send_update,
mock_execution_notification): mock_execution_notification,
m_get_actionplan):
m_get_actionplan.return_value = obj_utils.get_test_action_plan(
self.context, id=0)
actions = [self.create_action("nop", {'message': 'test'})] actions = [self.create_action("nop", {'message': 'test'})]
try: try:
self.engine.execute(actions) self.engine.execute(actions)
@ -122,10 +127,14 @@ class TestDefaultWorkFlowEngine(base.DbTestCase):
except Exception as exc: except Exception as exc:
self.fail(exc) self.fail(exc)
@mock.patch.object(objects.ActionPlan, "get_by_id")
@mock.patch.object(notifications.action, 'send_execution_notification') @mock.patch.object(notifications.action, 'send_execution_notification')
@mock.patch.object(notifications.action, 'send_update') @mock.patch.object(notifications.action, 'send_update')
def test_execute_nop_sleep(self, mock_send_update, def test_execute_nop_sleep(self, mock_send_update,
mock_execution_notification): mock_execution_notification,
m_get_actionplan):
m_get_actionplan.return_value = obj_utils.get_test_action_plan(
self.context, id=0)
actions = [] actions = []
first_nop = self.create_action("nop", {'message': 'test'}) first_nop = self.create_action("nop", {'message': 'test'})
second_nop = self.create_action("nop", {'message': 'second test'}) second_nop = self.create_action("nop", {'message': 'second test'})
@ -140,10 +149,14 @@ class TestDefaultWorkFlowEngine(base.DbTestCase):
except Exception as exc: except Exception as exc:
self.fail(exc) self.fail(exc)
@mock.patch.object(objects.ActionPlan, "get_by_id")
@mock.patch.object(notifications.action, 'send_execution_notification') @mock.patch.object(notifications.action, 'send_execution_notification')
@mock.patch.object(notifications.action, 'send_update') @mock.patch.object(notifications.action, 'send_update')
def test_execute_with_parents(self, mock_send_update, def test_execute_with_parents(self, mock_send_update,
mock_execution_notification): mock_execution_notification,
m_get_actionplan):
m_get_actionplan.return_value = obj_utils.get_test_action_plan(
self.context, id=0)
actions = [] actions = []
first_nop = self.create_action( first_nop = self.create_action(
"nop", {'message': 'test'}, "nop", {'message': 'test'},
@ -208,9 +221,13 @@ class TestDefaultWorkFlowEngine(base.DbTestCase):
except Exception as exc: except Exception as exc:
self.fail(exc) self.fail(exc)
@mock.patch.object(objects.ActionPlan, "get_by_id")
@mock.patch.object(notifications.action, 'send_execution_notification') @mock.patch.object(notifications.action, 'send_execution_notification')
@mock.patch.object(notifications.action, 'send_update') @mock.patch.object(notifications.action, 'send_update')
def test_execute_with_two_actions(self, m_send_update, m_execution): def test_execute_with_two_actions(self, m_send_update, m_execution,
m_get_actionplan):
m_get_actionplan.return_value = obj_utils.get_test_action_plan(
self.context, id=0)
actions = [] actions = []
second = self.create_action("sleep", {'duration': 0.0}) second = self.create_action("sleep", {'duration': 0.0})
first = self.create_action("nop", {'message': 'test'}) first = self.create_action("nop", {'message': 'test'})
@ -225,11 +242,14 @@ class TestDefaultWorkFlowEngine(base.DbTestCase):
except Exception as exc: except Exception as exc:
self.fail(exc) self.fail(exc)
@mock.patch.object(objects.ActionPlan, "get_by_id")
@mock.patch.object(notifications.action, 'send_execution_notification') @mock.patch.object(notifications.action, 'send_execution_notification')
@mock.patch.object(notifications.action, 'send_update') @mock.patch.object(notifications.action, 'send_update')
def test_execute_with_three_actions(self, m_send_update, m_execution): def test_execute_with_three_actions(self, m_send_update, m_execution,
m_get_actionplan):
m_get_actionplan.return_value = obj_utils.get_test_action_plan(
self.context, id=0)
actions = [] actions = []
third = self.create_action("nop", {'message': 'next'}) third = self.create_action("nop", {'message': 'next'})
second = self.create_action("sleep", {'duration': 0.0}) second = self.create_action("sleep", {'duration': 0.0})
first = self.create_action("nop", {'message': 'hello'}) first = self.create_action("nop", {'message': 'hello'})
@ -249,9 +269,13 @@ class TestDefaultWorkFlowEngine(base.DbTestCase):
except Exception as exc: except Exception as exc:
self.fail(exc) self.fail(exc)
@mock.patch.object(objects.ActionPlan, "get_by_id")
@mock.patch.object(notifications.action, 'send_execution_notification') @mock.patch.object(notifications.action, 'send_execution_notification')
@mock.patch.object(notifications.action, 'send_update') @mock.patch.object(notifications.action, 'send_update')
def test_execute_with_exception(self, m_send_update, m_execution): def test_execute_with_exception(self, m_send_update, m_execution,
m_get_actionplan):
m_get_actionplan.return_value = obj_utils.get_test_action_plan(
self.context, id=0)
actions = [] actions = []
third = self.create_action("no_exist", {'message': 'next'}) third = self.create_action("no_exist", {'message': 'next'})
@ -273,11 +297,14 @@ class TestDefaultWorkFlowEngine(base.DbTestCase):
self.check_action_state(second, objects.action.State.SUCCEEDED) self.check_action_state(second, objects.action.State.SUCCEEDED)
self.check_action_state(third, objects.action.State.FAILED) self.check_action_state(third, objects.action.State.FAILED)
@mock.patch.object(objects.ActionPlan, "get_by_id")
@mock.patch.object(notifications.action, 'send_execution_notification') @mock.patch.object(notifications.action, 'send_execution_notification')
@mock.patch.object(notifications.action, 'send_update') @mock.patch.object(notifications.action, 'send_update')
@mock.patch.object(factory.ActionFactory, "make_action") @mock.patch.object(factory.ActionFactory, "make_action")
def test_execute_with_action_exception(self, m_make_action, m_send_update, def test_execute_with_action_exception(self, m_make_action, m_send_update,
m_send_execution): m_send_execution, m_get_actionplan):
m_get_actionplan.return_value = obj_utils.get_test_action_plan(
self.context, id=0)
actions = [self.create_action("fake_action", {})] actions = [self.create_action("fake_action", {})]
m_make_action.return_value = FakeAction(mock.Mock()) m_make_action.return_value = FakeAction(mock.Mock())
@ -286,3 +313,43 @@ class TestDefaultWorkFlowEngine(base.DbTestCase):
self.assertIsInstance(exc.kwargs['error'], ExpectedException) self.assertIsInstance(exc.kwargs['error'], ExpectedException)
self.check_action_state(actions[0], objects.action.State.FAILED) self.check_action_state(actions[0], objects.action.State.FAILED)
@mock.patch.object(objects.ActionPlan, "get_by_uuid")
def test_execute_with_action_plan_cancel(self, m_get_actionplan):
obj_utils.create_test_goal(self.context)
strategy = obj_utils.create_test_strategy(self.context)
audit = obj_utils.create_test_audit(
self.context, strategy_id=strategy.id)
action_plan = obj_utils.create_test_action_plan(
self.context, audit_id=audit.id,
strategy_id=strategy.id,
state=objects.action_plan.State.CANCELLING)
action1 = obj_utils.create_test_action(
self.context, action_plan_id=action_plan.id,
action_type='nop', state=objects.action.State.SUCCEEDED,
input_parameters={'message': 'hello World'})
action2 = obj_utils.create_test_action(
self.context, action_plan_id=action_plan.id,
action_type='nop', state=objects.action.State.ONGOING,
uuid='9eb51e14-936d-4d12-a500-6ba0f5e0bb1c',
input_parameters={'message': 'hello World'})
action3 = obj_utils.create_test_action(
self.context, action_plan_id=action_plan.id,
action_type='nop', state=objects.action.State.PENDING,
uuid='bc7eee5c-4fbe-4def-9744-b539be55aa19',
input_parameters={'message': 'hello World'})
m_get_actionplan.return_value = action_plan
actions = []
actions.append(action1)
actions.append(action2)
actions.append(action3)
self.assertRaises(exception.ActionPlanCancelled,
self.engine.execute, actions)
try:
self.check_action_state(action1, objects.action.State.SUCCEEDED)
self.check_action_state(action2, objects.action.State.CANCELLED)
self.check_action_state(action3, objects.action.State.CANCELLED)
except Exception as exc:
self.fail(exc)

View File

@ -0,0 +1,79 @@
# -*- encoding: utf-8 -*-
# Copyright (c) 2015 b<>com
#
# Authors: Jean-Emile DARTOIS <jean-emile.dartois@b-com.com>
#
# 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 eventlet
import mock
from watcher.applier.workflow_engine import default as tflow
from watcher import objects
from watcher.tests.db import base
from watcher.tests.objects import utils as obj_utils
class TestTaskFlowActionContainer(base.DbTestCase):
def setUp(self):
super(TestTaskFlowActionContainer, self).setUp()
self.engine = tflow.DefaultWorkFlowEngine(
config=mock.Mock(),
context=self.context,
applier_manager=mock.MagicMock())
obj_utils.create_test_goal(self.context)
self.strategy = obj_utils.create_test_strategy(self.context)
self.audit = obj_utils.create_test_audit(
self.context, strategy_id=self.strategy.id)
def test_execute(self):
action_plan = obj_utils.create_test_action_plan(
self.context, audit_id=self.audit.id,
strategy_id=self.strategy.id,
state=objects.action.State.ONGOING)
action = obj_utils.create_test_action(
self.context, action_plan_id=action_plan.id,
state=objects.action.State.ONGOING,
action_type='nop',
input_parameters={'message': 'hello World'})
action_container = tflow.TaskFlowActionContainer(
db_action=action,
engine=self.engine)
action_container.execute()
self.assertTrue(action.state, objects.action.State.SUCCEEDED)
@mock.patch('eventlet.spawn')
def test_execute_with_cancel_action_plan(self, mock_eventlet_spawn):
action_plan = obj_utils.create_test_action_plan(
self.context, audit_id=self.audit.id,
strategy_id=self.strategy.id,
state=objects.action_plan.State.CANCELLING)
action = obj_utils.create_test_action(
self.context, action_plan_id=action_plan.id,
state=objects.action.State.ONGOING,
action_type='nop',
input_parameters={'message': 'hello World'})
action_container = tflow.TaskFlowActionContainer(
db_action=action,
engine=self.engine)
def empty_test():
pass
et = eventlet.spawn(empty_test)
mock_eventlet_spawn.return_value = et
action_container.execute()
et.kill.assert_called_with()