mistral/mistral/api/controllers/v2/resources.py

879 lines
25 KiB
Python

# Copyright 2013 - Mirantis, Inc.
# Copyright 2018 - Extreme Networks, 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 wsme
from wsme import types as wtypes
from mistral.api.controllers import resource
from mistral.api.controllers.v2 import types
from mistral import exceptions as exc
from mistral import utils
from mistral.workflow import states
SCOPE_TYPES = wtypes.Enum(str, 'private', 'public')
class ScopedResource(object):
"""Utilities for scoped resources"""
@classmethod
def validate_scope(cls, scope):
if scope not in SCOPE_TYPES.values:
raise exc.InvalidModelException(
"Scope must be one of the following: %s; actual: "
"%s" % (SCOPE_TYPES.values, scope)
)
class Workbook(resource.Resource, ScopedResource):
"""Workbook resource."""
id = wtypes.text
name = wtypes.text
namespace = wtypes.text
definition = wtypes.text
"workbook definition in Mistral v2 DSL"
tags = [wtypes.text]
scope = SCOPE_TYPES
"'private' or 'public'"
project_id = wsme.wsattr(wtypes.text, readonly=True)
created_at = wtypes.text
updated_at = wtypes.text
@classmethod
def sample(cls):
return cls(id='123e4567-e89b-12d3-a456-426655440000',
name='book',
definition='HERE GOES'
'WORKBOOK DEFINITION IN MISTRAL DSL v2',
tags=['large', 'expensive'],
scope='private',
project_id='a7eb669e9819420ea4bd1453e672c0a7',
created_at='1970-01-01T00:00:00.000000',
updated_at='1970-01-01T00:00:00.000000',
namespace='')
class Workbooks(resource.ResourceList):
"""A collection of Workbooks."""
workbooks = [Workbook]
def __init__(self, **kwargs):
self._type = 'workbooks'
super(Workbooks, self).__init__(**kwargs)
@classmethod
def sample(cls):
return cls(workbooks=[Workbook.sample()])
class Workflow(resource.Resource, ScopedResource):
"""Workflow resource."""
id = wtypes.text
name = wtypes.text
namespace = wtypes.text
input = wtypes.text
definition = wtypes.text
"Workflow definition in Mistral v2 DSL"
tags = [wtypes.text]
scope = SCOPE_TYPES
"'private' or 'public'"
project_id = wtypes.text
created_at = wtypes.text
updated_at = wtypes.text
@classmethod
def sample(cls):
return cls(id='123e4567-e89b-12d3-a456-426655440000',
name='flow',
input='param1, param2',
definition='HERE GOES'
'WORKFLOW DEFINITION IN MISTRAL DSL v2',
tags=['large', 'expensive'],
scope='private',
project_id='a7eb669e9819420ea4bd1453e672c0a7',
created_at='1970-01-01T00:00:00.000000',
updated_at='1970-01-01T00:00:00.000000',
namespace='')
@classmethod
def _set_input(cls, obj, wf_spec):
input_list = []
if wf_spec:
input = wf_spec.get('input', [])
for param in input:
if isinstance(param, dict):
for k, v in param.items():
input_list.append("%s=%s" % (k, v))
else:
input_list.append(param)
setattr(obj, 'input', ", ".join(input_list) if input_list else '')
return obj
@classmethod
def from_dict(cls, d):
obj = super(Workflow, cls).from_dict(d)
return cls._set_input(obj, d.get('spec'))
@classmethod
def from_db_model(cls, db_model):
obj = super(Workflow, cls).from_db_model(db_model)
return cls._set_input(obj, db_model.spec)
@classmethod
def from_tuples(cls, tuple_iterator):
obj = cls()
spec = None
for col_name, col_val in tuple_iterator:
if hasattr(obj, col_name):
# Convert all datetime values to strings.
setattr(obj, col_name, utils.datetime_to_str(col_val))
if col_name == 'spec':
spec = col_val
if spec:
obj = cls._set_input(obj, spec)
return obj
class Workflows(resource.ResourceList):
"""A collection of workflows."""
workflows = [Workflow]
def __init__(self, **kwargs):
self._type = 'workflows'
super(Workflows, self).__init__(**kwargs)
@classmethod
def sample(cls):
workflows_sample = cls()
workflows_sample.workflows = [Workflow.sample()]
workflows_sample.next = ("http://localhost:8989/v2/workflows?"
"sort_keys=id,name&"
"sort_dirs=asc,desc&limit=10&"
"marker=123e4567-e89b-12d3-a456-426655440000")
return workflows_sample
class Action(resource.Resource, ScopedResource):
"""Action resource.
NOTE: *name* is immutable. Note that name and description get inferred
from action definition when Mistral service receives a POST request.
So they can't be changed in another way.
"""
id = wtypes.text
name = wtypes.text
is_system = bool
input = wtypes.text
description = wtypes.text
tags = [wtypes.text]
definition = wtypes.text
scope = SCOPE_TYPES
project_id = wsme.wsattr(wtypes.text, readonly=True)
created_at = wtypes.text
updated_at = wtypes.text
@classmethod
def sample(cls):
return cls(
id='123e4567-e89b-12d3-a456-426655440000',
name='flow',
definition='HERE GOES ACTION DEFINITION IN MISTRAL DSL v2',
tags=['large', 'expensive'],
scope='private',
project_id='a7eb669e9819420ea4bd1453e672c0a7',
created_at='1970-01-01T00:00:00.000000',
updated_at='1970-01-01T00:00:00.000000'
)
class Actions(resource.ResourceList):
"""A collection of Actions."""
actions = [Action]
def __init__(self, **kwargs):
self._type = 'actions'
super(Actions, self).__init__(**kwargs)
@classmethod
def sample(cls):
sample = cls()
sample.actions = [Action.sample()]
sample.next = (
"http://localhost:8989/v2/actions?sort_keys=id,name&"
"sort_dirs=asc,desc&limit=10&"
"marker=123e4567-e89b-12d3-a456-426655440000"
)
return sample
class Execution(resource.Resource):
"""Execution resource."""
id = wtypes.text
"execution ID. It is immutable and auto assigned or determined by the API "
"client on execution creation. "
"If it's passed to POST method from a client it'll be assigned to the "
"newly created execution object, but only if an execution with such ID "
"doesn't exist. If it exists, then the endpoint will just return "
"execution properties in JSON."
workflow_id = wtypes.text
"workflow ID"
workflow_name = wtypes.text
"workflow name"
workflow_namespace = wtypes.text
"""Workflow namespace. The workflow namespace is also saved
under params and passed to all sub-workflow executions. When looking for
the next sub-workflow to run, The correct workflow will be found by
name and namespace, where the namespace can be the workflow namespace or
the default namespace. Workflows in the same namespace as the top workflow
will be given a higher priority."""
description = wtypes.text
"description of workflow execution"
params = types.jsontype
"""'params' define workflow type specific parameters. Specific parameters
are:
'task_name' - the name of the target task. Only for reverse workflows.
'env' - A string value containing the name of the stored environment
object or a dictionary with the environment variables used during
workflow execution and accessible as 'env()' from within expressions
(YAQL or Jinja) defined in the workflow text.
'evaluate_env' - If present, controls whether or not Mistral should
recursively find and evaluate all expressions (YAQL or Jinja) within
the specified environment (via 'env' parameter). 'True' - evaluate
all expressions recursively in the environment structure. 'False' -
don't evaluate expressions. 'True' by default.
"""
task_execution_id = wtypes.text
"reference to the parent task execution"
root_execution_id = wtypes.text
"reference to the root execution"
source_execution_id = wtypes.text
"""reference to a workflow execution id which will signal the api to
perform a lookup of a current workflow_execution and create a replica
based on that workflow inputs and parameters"""
state = wtypes.text
"state can be one of: IDLE, RUNNING, SUCCESS, ERROR, PAUSED"
state_info = wtypes.text
"an optional state information string"
input = types.jsontype
"input is a JSON structure containing workflow input values"
output = types.jsontype
"output is a workflow output"
created_at = wtypes.text
updated_at = wtypes.text
project_id = wsme.wsattr(wtypes.text, readonly=True)
@classmethod
def sample(cls):
return cls(
id='123e4567-e89b-12d3-a456-426655440000',
workflow_name='flow',
workflow_namespace='some_namespace',
workflow_id='123e4567-e89b-12d3-a456-426655441111',
description='this is the first execution.',
project_id='40a908dbddfe48ad80a87fb30fa70a03',
state='SUCCESS',
input={},
output={},
params={
'env': {'k1': 'abc', 'k2': 123},
'notify': [
{
'type': 'webhook',
'url': 'http://endpoint/of/webhook',
'headers': {
'Content-Type': 'application/json',
'X-Auth-Token': '123456789'
}
},
{
'type': 'queue',
'topic': 'failover_queue',
'backend': 'rabbitmq',
'host': '127.0.0.1',
'port': 5432
}
]
},
created_at='1970-01-01T00:00:00.000000',
updated_at='1970-01-01T00:00:00.000000'
)
class Executions(resource.ResourceList):
"""A collection of Execution resources."""
executions = [Execution]
def __init__(self, **kwargs):
self._type = 'executions'
super(Executions, self).__init__(**kwargs)
@classmethod
def sample(cls):
sample = cls()
sample.executions = [Execution.sample()]
sample.next = (
"http://localhost:8989/v2/executions?"
"sort_keys=id,workflow_name&sort_dirs=asc,desc&limit=10&"
"marker=123e4567-e89b-12d3-a456-426655440000"
)
return sample
class Task(resource.Resource):
"""Task resource."""
id = wtypes.text
name = wtypes.text
type = wtypes.text
workflow_name = wtypes.text
workflow_namespace = wtypes.text
workflow_id = wtypes.text
workflow_execution_id = wtypes.text
state = wtypes.text
"""state can take one of the following values:
IDLE, RUNNING, SUCCESS, ERROR, DELAYED"""
state_info = wtypes.text
"an optional state information string"
project_id = wsme.wsattr(wtypes.text, readonly=True)
runtime_context = types.jsontype
result = wtypes.text
published = types.jsontype
processed = bool
created_at = wtypes.text
updated_at = wtypes.text
started_at = wtypes.text
finished_at = wtypes.text
# Add this param to make Mistral API work with WSME 0.8.0 or higher version
reset = wsme.wsattr(bool, mandatory=True)
env = types.jsontype
@classmethod
def sample(cls):
return cls(
id='123e4567-e89b-12d3-a456-426655440000',
workflow_name='flow',
workflow_id='123e4567-e89b-12d3-a456-426655441111',
workflow_execution_id='123e4567-e89b-12d3-a456-426655440000',
name='task',
state=states.SUCCESS,
project_id='40a908dbddfe48ad80a87fb30fa70a03',
runtime_context={
'triggered_by': [
{
'task_id': '123-123-123',
'event': 'on-success'
}
]
},
result='task result',
published={'key': 'value'},
processed=True,
created_at='1970-01-01T00:00:00.000000',
updated_at='1970-01-01T00:00:00.000000',
reset=True
)
class Tasks(resource.ResourceList):
"""A collection of tasks."""
tasks = [Task]
def __init__(self, **kwargs):
self._type = 'tasks'
super(Tasks, self).__init__(**kwargs)
@classmethod
def sample(cls):
return cls(tasks=[Task.sample()])
class ActionExecution(resource.Resource):
"""ActionExecution resource."""
id = wtypes.text
workflow_name = wtypes.text
workflow_namespace = wtypes.text
task_name = wtypes.text
task_execution_id = wtypes.text
state = wtypes.text
state_info = wtypes.text
tags = [wtypes.text]
name = wtypes.text
description = wtypes.text
project_id = wsme.wsattr(wtypes.text, readonly=True)
accepted = bool
input = types.jsontype
output = types.jsontype
created_at = wtypes.text
updated_at = wtypes.text
params = types.jsontype # TODO(rakhmerov): What is this??
@classmethod
def sample(cls):
return cls(
id='123e4567-e89b-12d3-a456-426655440000',
workflow_name='flow',
task_name='task1',
workflow_execution_id='653e4127-e89b-12d3-a456-426655440076',
task_execution_id='343e45623-e89b-12d3-a456-426655440090',
state=states.SUCCESS,
state_info=states.SUCCESS,
tags=['foo', 'fee'],
name='std.echo',
description='My running action',
project_id='40a908dbddfe48ad80a87fb30fa70a03',
accepted=True,
input={'first_name': 'John', 'last_name': 'Doe'},
output={'some_output': 'Hello, John Doe!'},
created_at='1970-01-01T00:00:00.000000',
updated_at='1970-01-01T00:00:00.000000',
params={'save_result': True, "run_sync": False}
)
class ActionExecutions(resource.ResourceList):
"""A collection of action_executions."""
action_executions = [ActionExecution]
def __init__(self, **kwargs):
self._type = 'action_executions'
super(ActionExecutions, self).__init__(**kwargs)
@classmethod
def sample(cls):
return cls(action_executions=[ActionExecution.sample()])
class CronTrigger(resource.Resource):
"""CronTrigger resource."""
id = wtypes.text
name = wtypes.text
workflow_name = wtypes.text
workflow_id = wtypes.text
workflow_input = types.jsontype
workflow_params = types.jsontype
project_id = wsme.wsattr(wtypes.text, readonly=True)
scope = SCOPE_TYPES
pattern = wtypes.text
remaining_executions = wtypes.IntegerType(minimum=1)
first_execution_time = wtypes.text
next_execution_time = wtypes.text
created_at = wtypes.text
updated_at = wtypes.text
@classmethod
def sample(cls):
return cls(
id='123e4567-e89b-12d3-a456-426655440000',
name='my_trigger',
workflow_name='my_wf',
workflow_id='123e4567-e89b-12d3-a456-426655441111',
workflow_input={},
workflow_params={},
project_id='40a908dbddfe48ad80a87fb30fa70a03',
scope='private',
pattern='* * * * *',
remaining_executions=42,
created_at='1970-01-01T00:00:00.000000',
updated_at='1970-01-01T00:00:00.000000'
)
class CronTriggers(resource.ResourceList):
"""A collection of cron triggers."""
cron_triggers = [CronTrigger]
def __init__(self, **kwargs):
self._type = 'cron_triggers'
super(CronTriggers, self).__init__(**kwargs)
@classmethod
def sample(cls):
return cls(cron_triggers=[CronTrigger.sample()])
class Environment(resource.Resource):
"""Environment resource."""
id = wtypes.text
name = wtypes.text
description = wtypes.text
variables = types.jsontype
scope = SCOPE_TYPES
project_id = wsme.wsattr(wtypes.text, readonly=True)
created_at = wtypes.text
updated_at = wtypes.text
@classmethod
def sample(cls):
return cls(
id='123e4567-e89b-12d3-a456-426655440000',
name='sample',
description='example environment entry',
variables={
'server': 'localhost',
'database': 'temp',
'timeout': 600,
'verbose': True
},
scope='private',
project_id='40a908dbddfe48ad80a87fb30fa70a03',
created_at='1970-01-01T00:00:00.000000',
updated_at='1970-01-01T00:00:00.000000'
)
class Environments(resource.ResourceList):
"""A collection of Environment resources."""
environments = [Environment]
def __init__(self, **kwargs):
self._type = 'environments'
super(Environments, self).__init__(**kwargs)
@classmethod
def sample(cls):
return cls(environments=[Environment.sample()])
class Member(resource.Resource):
id = types.uuid
resource_id = wtypes.text
resource_type = wtypes.text
project_id = wtypes.text
member_id = wtypes.text
status = wtypes.Enum(str, 'pending', 'accepted', 'rejected')
created_at = wtypes.text
updated_at = wtypes.text
@classmethod
def sample(cls):
return cls(
id='123e4567-e89b-12d3-a456-426655440000',
resource_id='123e4567-e89b-12d3-a456-426655440011',
resource_type='workflow',
project_id='40a908dbddfe48ad80a87fb30fa70a03',
member_id='a7eb669e9819420ea4bd1453e672c0a7',
status='accepted',
created_at='1970-01-01T00:00:00.000000',
updated_at='1970-01-01T00:00:00.000000'
)
class Members(resource.ResourceList):
members = [Member]
@classmethod
def sample(cls):
return cls(members=[Member.sample()])
class Service(resource.Resource):
"""Service resource."""
name = wtypes.text
type = wtypes.text
@classmethod
def sample(cls):
return cls(name='host1_1234', type='executor_group')
class Services(resource.Resource):
"""A collection of Services."""
services = [Service]
@classmethod
def sample(cls):
return cls(services=[Service.sample()])
class EventTrigger(resource.Resource):
"""EventTrigger resource."""
id = wsme.wsattr(wtypes.text, readonly=True)
created_at = wsme.wsattr(wtypes.text, readonly=True)
updated_at = wsme.wsattr(wtypes.text, readonly=True)
project_id = wsme.wsattr(wtypes.text, readonly=True)
name = wtypes.text
workflow_id = types.uuid
workflow_input = types.jsontype
workflow_params = types.jsontype
exchange = wtypes.text
topic = wtypes.text
event = wtypes.text
scope = SCOPE_TYPES
@classmethod
def sample(cls):
return cls(id='123e4567-e89b-12d3-a456-426655441414',
created_at='1970-01-01T00:00:00.000000',
updated_at='1970-01-01T00:00:00.000000',
project_id='project',
name='expiration_event_trigger',
workflow_id='123e4567-e89b-12d3-a456-426655441414',
workflow_input={},
workflow_params={},
exchange='nova',
topic='notifications',
event='compute.instance.create.end')
class EventTriggers(resource.ResourceList):
"""A collection of event triggers."""
event_triggers = [EventTrigger]
def __init__(self, **kwargs):
self._type = 'event_triggers'
super(EventTriggers, self).__init__(**kwargs)
@classmethod
def sample(cls):
triggers_sample = cls()
triggers_sample.event_triggers = [EventTrigger.sample()]
triggers_sample.next = ("http://localhost:8989/v2/event_triggers?"
"sort_keys=id,name&"
"sort_dirs=asc,desc&limit=10&"
"marker=123e4567-e89b-12d3-a456-426655440000")
return triggers_sample
class BaseExecutionReportEntry(resource.Resource):
"""Execution report entry resource."""
id = wtypes.text
name = wtypes.text
created_at = wtypes.text
updated_at = wtypes.text
state = wtypes.text
state_info = wtypes.text
@classmethod
def sample(cls):
# TODO(rakhmerov): complete
return cls(
id='123e4567-e89b-12d3-a456-426655441414',
created_at='2019-01-30T00:00:00.000000',
updated_at='2019-01-30T00:00:00.000000',
state=states.SUCCESS
)
class ActionExecutionReportEntry(BaseExecutionReportEntry):
"""Action execution report entry resource."""
accepted = bool
last_heartbeat = wtypes.text
@classmethod
def sample(cls):
sample = super(ActionExecutionReportEntry, cls).sample()
sample.accepted = True
sample.last_heartbeat = '2019-01-30T00:00:00.000000'
return sample
class WorkflowExecutionReportEntry(BaseExecutionReportEntry):
"""Workflow execution report entry resource."""
# NOTE(rakhmerov): task_executions has to be declared below
# after we declare a class for task execution entry resource.
@classmethod
def sample(cls):
sample = super(WorkflowExecutionReportEntry, cls).sample()
# We can't define a non-empty list task executions here because
# the needed class is not defined yet. Since this is just a sample
# we can sacrifice it.
sample.task_executions = []
return sample
class TaskExecutionReportEntry(BaseExecutionReportEntry):
"""Task execution report entity resource."""
action_executions = [ActionExecutionReportEntry]
workflow_executions = [WorkflowExecutionReportEntry]
@classmethod
def sample(cls):
sample = super(TaskExecutionReportEntry, cls).sample()
sample.action_executions = [ActionExecutionReportEntry.sample()]
sample.workflow_executions = []
return sample
# We have to declare this field later because of the dynamic binding.
# It can't be within WorkflowExecutionReportEntry before
# TaskExecutionReportEntry is declared.
WorkflowExecutionReportEntry.task_executions = [TaskExecutionReportEntry]
wtypes.registry.reregister(WorkflowExecutionReportEntry)
class ExecutionReportStatistics(resource.Resource):
"""Execution report statistics.
TODO(rakhmerov): There's much more we can add here. For example,
information about action, average (and also min and max) task execution
run time etc.
"""
total_tasks_count = wtypes.IntegerType(minimum=0)
running_tasks_count = wtypes.IntegerType(minimum=0)
success_tasks_count = wtypes.IntegerType(minimum=0)
error_tasks_count = wtypes.IntegerType(minimum=0)
idle_tasks_count = wtypes.IntegerType(minimum=0)
paused_tasks_count = wtypes.IntegerType(minimum=0)
def __init__(self, **kw):
self.total_tasks_count = 0
self.running_tasks_count = 0
self.success_tasks_count = 0
self.error_tasks_count = 0
self.idle_tasks_count = 0
self.paused_tasks_count = 0
super(ExecutionReportStatistics, self).__init__(**kw)
def increment_running(self):
self.running_tasks_count += 1
self.total_tasks_count += 1
def increment_success(self):
self.success_tasks_count += 1
self.total_tasks_count += 1
def increment_error(self):
self.error_tasks_count += 1
self.total_tasks_count += 1
def increment_idle(self):
self.idle_tasks_count += 1
self.total_tasks_count += 1
def increment_paused(self):
self.paused_tasks_count += 1
self.total_tasks_count += 1
@classmethod
def sample(cls):
return cls(
total_tasks_count=10,
running_tasks_count=3,
success_tasks_count=5,
error_tasks_count=2,
idle_tasks_count=0,
paused_tasks_count=0
)
class ExecutionReport(resource.Resource):
"""Execution report resource."""
statistics = ExecutionReportStatistics
"""General statistics about the workflow execution hierarchy."""
root_workflow_execution = WorkflowExecutionReportEntry
"""Root entry of the report associated with a workflow execution."""
@classmethod
def sample(cls):
sample = cls()
sample.statistics = ExecutionReportStatistics.sample()
sample.root_workflow_execution = WorkflowExecutionReportEntry.sample()
return sample