eb6c0513c6
This new function will allow user to get a list of tasks matching certain filter. For example only task in state ERROR from the current execution. It is very useful for debugging, but also very expensive, since it might require multiple DB queries. In addition it is important to remember a lot of data can return from this function, so it should be used carefully Change-Id: I452175bfb60636ed8de9b2b1ceab615359765964 Implements: blueprint yaql-tasks-function Implements: blueprint yaql-errors-function
257 lines
7.4 KiB
Python
257 lines
7.4 KiB
Python
# Copyright 2015 - Mirantis, Inc.
|
|
# Copyright 2016 - Brocade Communications Systems, 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.
|
|
|
|
from functools import partial
|
|
|
|
from oslo_serialization import jsonutils
|
|
from stevedore import extension
|
|
import yaql
|
|
|
|
from mistral.db.v2 import api as db_api
|
|
from mistral import utils
|
|
|
|
|
|
ROOT_YAQL_CONTEXT = None
|
|
|
|
|
|
def get_yaql_context(data_context):
|
|
global ROOT_YAQL_CONTEXT
|
|
|
|
if not ROOT_YAQL_CONTEXT:
|
|
ROOT_YAQL_CONTEXT = yaql.create_context()
|
|
|
|
_register_yaql_functions(ROOT_YAQL_CONTEXT)
|
|
new_ctx = ROOT_YAQL_CONTEXT.create_child_context()
|
|
new_ctx['$'] = data_context
|
|
|
|
if isinstance(data_context, dict):
|
|
new_ctx['__env'] = data_context.get('__env')
|
|
new_ctx['__execution'] = data_context.get('__execution')
|
|
new_ctx['__task_execution'] = data_context.get('__task_execution')
|
|
|
|
return new_ctx
|
|
|
|
|
|
def get_jinja_context(data_context):
|
|
new_ctx = {
|
|
'_': data_context
|
|
}
|
|
|
|
_register_jinja_functions(new_ctx)
|
|
|
|
if isinstance(data_context, dict):
|
|
new_ctx['__env'] = data_context.get('__env')
|
|
new_ctx['__execution'] = data_context.get('__execution')
|
|
new_ctx['__task_execution'] = data_context.get('__task_execution')
|
|
|
|
return new_ctx
|
|
|
|
|
|
def get_custom_functions():
|
|
"""Get custom functions
|
|
|
|
Retreives the list of custom evaluation functions
|
|
"""
|
|
functions = dict()
|
|
|
|
mgr = extension.ExtensionManager(
|
|
namespace='mistral.expression.functions',
|
|
invoke_on_load=False
|
|
)
|
|
for name in mgr.names():
|
|
functions[name] = mgr[name].plugin
|
|
|
|
return functions
|
|
|
|
|
|
def _register_yaql_functions(yaql_ctx):
|
|
functions = get_custom_functions()
|
|
|
|
for name in functions:
|
|
yaql_ctx.register_function(functions[name], name=name)
|
|
|
|
|
|
def _register_jinja_functions(jinja_ctx):
|
|
functions = get_custom_functions()
|
|
|
|
for name in functions:
|
|
jinja_ctx[name] = partial(functions[name], jinja_ctx['_'])
|
|
|
|
|
|
# Additional YAQL functions needed by Mistral.
|
|
# If a function name ends with underscore then it doesn't need to pass
|
|
# the name of the function when context registers it.
|
|
|
|
def env_(context):
|
|
return context['__env']
|
|
|
|
|
|
def execution_(context):
|
|
wf_ex = db_api.get_workflow_execution(context['__execution']['id'])
|
|
|
|
return {
|
|
'id': wf_ex.id,
|
|
'name': wf_ex.name,
|
|
'spec': wf_ex.spec,
|
|
'input': wf_ex.input,
|
|
'params': wf_ex.params
|
|
}
|
|
|
|
|
|
def json_pp_(context, data=None):
|
|
return jsonutils.dumps(
|
|
data or context,
|
|
indent=4
|
|
).replace("\\n", "\n").replace(" \n", "\n")
|
|
|
|
|
|
def task_(context, task_name):
|
|
|
|
# This section may not exist in a context if it's calculated not in
|
|
# task scope.
|
|
cur_task = context['__task_execution']
|
|
|
|
if cur_task and cur_task['name'] == task_name:
|
|
task_ex = db_api.get_task_execution(cur_task['id'])
|
|
else:
|
|
task_execs = db_api.get_task_executions(
|
|
workflow_execution_id=context['__execution']['id'],
|
|
name=task_name
|
|
)
|
|
|
|
# TODO(rakhmerov): Account for multiple executions (i.e. in case of
|
|
# cycles).
|
|
task_ex = task_execs[-1] if len(task_execs) > 0 else None
|
|
|
|
if not task_ex:
|
|
return None
|
|
|
|
# We don't use to_dict() db model method because not all fields
|
|
# make sense for user.
|
|
return _convert_to_user_model(task_ex)
|
|
|
|
|
|
def _should_pass_filter(t, state, flat):
|
|
# Start from assuming all is true, check only if needed.
|
|
state_match = True
|
|
flat_match = True
|
|
|
|
if state:
|
|
state_match = t['state'] == state
|
|
|
|
if flat:
|
|
is_action = t['type'] == utils.ACTION_TASK_TYPE
|
|
|
|
if not is_action:
|
|
nested_execs = db_api.get_workflow_executions(
|
|
task_execution_id=t.id
|
|
)
|
|
|
|
for n in nested_execs:
|
|
flat_match = flat_match and n.state != t.state
|
|
|
|
return state_match and flat_match
|
|
|
|
|
|
def _get_tasks_from_db(workflow_execution_id=None, recursive=False, state=None,
|
|
flat=False):
|
|
task_execs = []
|
|
nested_task_exs = []
|
|
|
|
kwargs = {}
|
|
|
|
if workflow_execution_id:
|
|
kwargs['workflow_execution_id'] = workflow_execution_id
|
|
|
|
# We can't add state to query if we want to filter by workflow_execution_id
|
|
# recursively. There might be a workflow_execution in one state with a
|
|
# nested workflow execution that has a task in the desired state until we
|
|
# have an optimization for queering all workflow executions under a given
|
|
# top level workflow execution, this is the way to go.
|
|
if state and not (workflow_execution_id and recursive):
|
|
kwargs['state'] = state
|
|
|
|
task_execs.extend(db_api.get_task_executions(**kwargs))
|
|
|
|
# If it is not recursive no need to check nested workflows.
|
|
# If there is no workflow execution id, we already have all we need, and
|
|
# doing more queries will just create duplication in the results.
|
|
if recursive and workflow_execution_id:
|
|
for t in task_execs:
|
|
if t.type == utils.WORKFLOW_TASK_TYPE:
|
|
# Get nested workflow execution that matches the task.
|
|
nested_workflow_executions = db_api.get_workflow_executions(
|
|
task_execution_id=t.id
|
|
)
|
|
|
|
# There might be zero nested executions.
|
|
for nested_workflow_execution in nested_workflow_executions:
|
|
nested_task_exs.extend(
|
|
_get_tasks_from_db(
|
|
nested_workflow_execution.id,
|
|
recursive,
|
|
state,
|
|
flat
|
|
)
|
|
)
|
|
|
|
if state or flat:
|
|
# Filter by state and flat.
|
|
task_execs = [
|
|
t for t in task_execs if _should_pass_filter(t, state, flat)
|
|
]
|
|
|
|
# The nested tasks were already filtered, since this is a recursion.
|
|
task_execs.extend(nested_task_exs)
|
|
|
|
return task_execs
|
|
|
|
|
|
def tasks_(context, workflow_execution_id=None, recursive=False, state=None,
|
|
flat=False):
|
|
|
|
task_execs = _get_tasks_from_db(
|
|
workflow_execution_id,
|
|
recursive,
|
|
state,
|
|
flat
|
|
)
|
|
|
|
# Convert task_execs to user model and return.
|
|
return [_convert_to_user_model(t) for t in task_execs]
|
|
|
|
|
|
def _convert_to_user_model(task_ex):
|
|
# Importing data_flow in order to break cycle dependency between modules.
|
|
from mistral.workflow import data_flow
|
|
|
|
# We don't use to_dict() db model method because not all fields
|
|
# make sense for user.
|
|
return {
|
|
'id': task_ex.id,
|
|
'name': task_ex.name,
|
|
'spec': task_ex.spec,
|
|
'state': task_ex.state,
|
|
'state_info': task_ex.state_info,
|
|
'result': data_flow.get_task_execution_result(task_ex),
|
|
'published': task_ex.published,
|
|
'type': task_ex.type,
|
|
'workflow_execution_id': task_ex.workflow_execution_id
|
|
}
|
|
|
|
|
|
def uuid_(context=None):
|
|
return utils.generate_unicode_uuid()
|