Merge "Cancel Action Plan"
This commit is contained in:
@@ -0,0 +1,4 @@
|
|||||||
|
---
|
||||||
|
features:
|
||||||
|
- |
|
||||||
|
Adds feature to cancel an action-plan.
|
||||||
@@ -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,
|
||||||
|
|||||||
@@ -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()
|
||||||
|
|||||||
@@ -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)
|
||||||
|
|||||||
@@ -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
|
||||||
|
|||||||
@@ -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")
|
||||||
|
|||||||
@@ -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")
|
||||||
|
|||||||
@@ -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)
|
||||||
|
|||||||
@@ -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.
|
||||||
|
|||||||
@@ -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.")
|
||||||
|
|
||||||
|
|||||||
@@ -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
|
||||||
|
|||||||
@@ -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
|
||||||
|
|||||||
@@ -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},
|
||||||
]
|
]
|
||||||
|
|||||||
@@ -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)
|
||||||
|
|||||||
@@ -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)
|
||||||
|
|||||||
@@ -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()
|
||||||
Reference in New Issue
Block a user