deb-mistral/mistral/workflow/data_flow.py
Renat Akhmerov b27d0d3ca8 Error result: allow actions to return instance of wf_utils.Result
* Actions are now allowed to return instance of workflow.utils.Result
  if they need a notion of "error result" which means that something
  went wrong but we can see what exactly is wrong. An example is std.http
  action which can return HTTP status code instead of just blindly raising
  an exception. See the bug description for more details.
* In "on-error" clause we now can evaluate condition expressions based
  on an error result returned by task action.
* "publish" clause is still ignored when task has ERROR state.

TODO:
 * Add more tests, especially for various corner cases.
 * Fix standard actions (e.g std.http).
 * Make sure workflows can also return error result.
 * Add corresponding functional tests.

Change-Id: If1531b3da31de95be9717570a29d87a1a424d7a5
2015-07-16 13:27:14 +06:00

279 lines
7.9 KiB
Python

# Copyright 2013 - Mirantis, Inc.
# Copyright 2015 - StackStorm, Inc.
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.
import copy
from oslo_config import cfg
from oslo_log import log as logging
from mistral import context as auth_ctx
from mistral.db.v2 import api as db_api
from mistral.db.v2.sqlalchemy import models
from mistral import expressions as expr
from mistral import utils
from mistral.utils import inspect_utils
from mistral.workbook import parser as spec_parser
from mistral.workflow import states
LOG = logging.getLogger(__name__)
CONF = cfg.CONF
def evaluate_upstream_context(upstream_task_execs):
published_vars = {}
ctx = {}
for t_ex in upstream_task_execs:
# TODO(rakhmerov): These two merges look confusing. So it's a
# temporary solution.There's still the bug
# https://bugs.launchpad.net/mistral/+bug/1424461 that needs to be
# fixed using context variable versioning.
published_vars = utils.merge_dicts(
published_vars,
t_ex.published
)
utils.merge_dicts(
ctx,
evaluate_task_outbound_context(t_ex, include_result=False)
)
ctx = utils.merge_dicts(ctx, published_vars)
# TODO(rakhmerov): IMO, this method shouldn't deal with these task ids or
# anything else related to task proxies. Need to refactor.
return utils.merge_dicts(
ctx,
_get_task_identifiers_dict(upstream_task_execs)
)
# TODO(rakhmerov): Think how to gt rid of this method and the whole trick
# with upstream tasks. It doesn't look clear from design standpoint.
def _get_task_identifiers_dict(task_execs):
tasks = {}
for task_ex in task_execs:
tasks[task_ex.id] = task_ex.name
return {"__tasks": tasks}
def _extract_execution_result(ex):
if isinstance(ex, models.WorkflowExecution):
return ex.output
if ex.output:
return ex.output['result']
def invalidate_task_execution_result(task_ex):
for ex in task_ex.executions:
ex.accepted = False
def get_task_execution_result(task_ex):
action_execs = task_ex.executions
action_execs.sort(
key=lambda x: x.runtime_context.get('with_items_index')
)
results = [
_extract_execution_result(ex)
for ex in task_ex.executions
if hasattr(ex, 'output') and ex.accepted
]
task_spec = spec_parser.get_task_spec(task_ex.spec)
if task_spec.get_with_items():
return results
return results[0] if len(results) == 1 else results
class TaskResultProxy(object):
def __init__(self, task_id):
self.task_id = task_id
def get(self):
task_ex = db_api.get_task_execution(self.task_id)
return get_task_execution_result(task_ex)
def __str__(self):
return "%s [task_id = '%s']" % (self.__class__.__name__, self.task_id)
def __repr__(self):
return self.__str__()
class ProxyAwareDict(dict):
def __getitem__(self, item):
val = super(ProxyAwareDict, self).__getitem__(item)
if isinstance(val, TaskResultProxy):
return val.get()
return val
def get(self, k, d=None):
try:
return self.__getitem__(k)
except KeyError:
return d
def iteritems(self):
for k, _ in super(ProxyAwareDict, self).iteritems():
yield k, self[k]
def to_builtin_dict(self):
return {k: self[k] for k, _ in self.iteritems()}
def publish_variables(task_ex, task_spec):
if task_ex.state != states.SUCCESS:
return
expr_ctx = extract_task_result_proxies_to_context(task_ex.in_context)
if task_ex.name in expr_ctx:
LOG.warning(
'Shadowing context variable with task name while publishing: %s' %
task_ex.name
)
# Add result of current task to context for variables evaluation.
expr_ctx[task_ex.name] = TaskResultProxy(task_ex.id)
task_ex.published = expr.evaluate_recursively(
task_spec.get_publish(),
expr_ctx
)
def destroy_task_result(task_ex):
for ex in task_ex.executions:
if hasattr(ex, 'output'):
ex.output = {}
def evaluate_task_outbound_context(task_ex, include_result=True):
"""Evaluates task outbound Data Flow context.
This method assumes that complete task output (after publisher etc.)
has already been evaluated.
:param task_ex: DB task.
:param include_result: boolean argument, if True - include the
TaskResultProxy in outbound context under <task_name> key.
:return: Outbound task Data Flow context.
"""
in_context = (copy.deepcopy(dict(task_ex.in_context))
if task_ex.in_context is not None else {})
out_ctx = utils.merge_dicts(in_context, task_ex.published)
# Add task output under key 'taskName'.
if include_result:
task_ex_result = TaskResultProxy(task_ex.id)
out_ctx = utils.merge_dicts(
out_ctx,
{task_ex.name: task_ex_result or None}
)
return ProxyAwareDict(out_ctx)
def evaluate_workflow_output(wf_spec, context):
"""Evaluates workflow output.
:param wf_spec: Workflow specification.
:param context: Final Data Flow context (cause task's outbound context).
"""
# Convert context to ProxyAwareDict for correct output evaluation.
context = ProxyAwareDict(copy.deepcopy(context))
output_dict = wf_spec.get_output()
# Evaluate workflow 'publish' clause using the final workflow context.
output = expr.evaluate_recursively(output_dict, context)
# TODO(rakhmerov): Many don't like that we return the whole context
# TODO(rakhmerov): if 'output' is not explicitly defined.
return ProxyAwareDict(output or context).to_builtin_dict()
def add_openstack_data_to_context(wf_ex):
wf_ex.context = wf_ex.context or {}
if CONF.pecan.auth_enable:
exec_ctx = auth_ctx.ctx()
LOG.debug('Data flow security context: %s' % exec_ctx)
if exec_ctx:
wf_ex.context.update({'openstack': exec_ctx.to_dict()})
def add_execution_to_context(wf_ex):
wf_ex.context = wf_ex.context or {}
wf_ex.context['__execution'] = {
'id': wf_ex.id,
'spec': wf_ex.spec,
'params': wf_ex.params,
'input': wf_ex.input
}
def add_environment_to_context(wf_ex):
wf_ex.context = wf_ex.context or {}
# If env variables are provided, add an evaluated copy into the context.
if 'env' in wf_ex.params:
env = copy.deepcopy(wf_ex.params['env'])
# An env variable can be an expression of other env variables.
wf_ex.context['__env'] = expr.evaluate_recursively(env, {'__env': env})
def add_workflow_variables_to_context(wf_ex, wf_spec):
wf_ex.context = wf_ex.context or {}
return utils.merge_dicts(
wf_ex.context,
expr.evaluate_recursively(wf_spec.get_vars(), wf_ex.context)
)
# TODO(rakhmerov): Think how to get rid of this method. It should not be
# exposed in API.
def extract_task_result_proxies_to_context(ctx):
ctx = ProxyAwareDict(copy.deepcopy(ctx))
for task_ex_id, task_ex_name in ctx['__tasks'].iteritems():
ctx[task_ex_name] = TaskResultProxy(task_ex_id)
return ctx
def evaluate_object_fields(obj, context):
fields = inspect_utils.get_public_fields(obj)
evaluated_fields = expr.evaluate_recursively(fields, context)
for k, v in evaluated_fields.items():
setattr(obj, k, v)