Implementing DSL specification v2 (partially)

* Rewriting package 'workbook' to have v1 and v2 separately
* Refactoring all required places to use v1 spec explicitly
* Refactoring and making specs more consistent

TODO:
* Fully implement spec v2 when it's complete

Change-Id: I4d6949588635ff45775f68186ea43b16163b6a72
This commit is contained in:
Renat Akhmerov 2014-07-29 12:26:48 +07:00
parent 9f78c6f251
commit f997ca183d
46 changed files with 1162 additions and 244 deletions

View File

@ -22,8 +22,7 @@ from mistral.actions import std_actions
from mistral import exceptions as exc from mistral import exceptions as exc
from mistral import expressions as expr from mistral import expressions as expr
from mistral.openstack.common import log as logging from mistral.openstack.common import log as logging
from mistral.workbook import actions from mistral.workbook import parser as spec_parser
from mistral.workbook import tasks
LOG = logging.getLogger(__name__) LOG = logging.getLogger(__name__)
@ -112,16 +111,15 @@ def _has_action_context_param(action_cls):
def _create_adhoc_action(db_task, openstack_context): def _create_adhoc_action(db_task, openstack_context):
task_spec = tasks.TaskSpec(db_task['task_spec']) task_spec = spec_parser.get_task_spec(db_task['task_spec'])
full_action_name = task_spec.get_full_action_name() full_action_name = task_spec.get_full_action_name()
# TODO(rakhmerov): Fix model attributes during refactoring.
raw_action_spec = db_task['action_spec'] raw_action_spec = db_task['action_spec']
if not raw_action_spec: if not raw_action_spec:
return None return None
action_spec = actions.ActionSpec(raw_action_spec) action_spec = spec_parser.get_action_spec(raw_action_spec)
LOG.info('Using ad-hoc action [action=%s, db_task=%s]' % LOG.info('Using ad-hoc action [action=%s, db_task=%s]' %
(full_action_name, db_task)) (full_action_name, db_task))
@ -148,7 +146,8 @@ def _create_adhoc_action(db_task, openstack_context):
def create_action(db_task): def create_action(db_task):
task_spec = tasks.TaskSpec(db_task['task_spec']) task_spec = spec_parser.get_task_spec(db_task['task_spec'])
full_action_name = task_spec.get_full_action_name() full_action_name = task_spec.get_full_action_name()
action_cls = get_action_class(full_action_name) action_cls = get_action_class(full_action_name)

View File

@ -29,14 +29,13 @@ cfg.CONF.import_opt('workflow_trace_log_name', 'mistral.config')
from mistral.actions import action_factory as a_f from mistral.actions import action_factory as a_f
from mistral import context as auth_context from mistral import context as auth_context
from mistral.db import api as db_api from mistral.db import api as db_api
from mistral import dsl_parser as parser
from mistral.engine import data_flow from mistral.engine import data_flow
from mistral.engine import retry from mistral.engine import retry
from mistral.engine import states from mistral.engine import states
from mistral.engine import workflow from mistral.engine import workflow
from mistral import exceptions as exc from mistral import exceptions as exc
from mistral.openstack.common import log as logging from mistral.openstack.common import log as logging
from mistral.workbook import tasks as wb_task from mistral.workbook import parser as spec_parser
LOG = logging.getLogger(__name__) LOG = logging.getLogger(__name__)
@ -187,7 +186,7 @@ class Engine(object):
WF_TRACE.info("Task '%s' [%s -> %s, result = %s]" % WF_TRACE.info("Task '%s' [%s -> %s, result = %s]" %
(task.name, task.state, state, result)) (task.name, task.state, state, result))
action_name = wb_task.TaskSpec(task.task_spec)\ action_name = spec_parser.get_task_spec(task.task_spec)\
.get_full_action_name() .get_full_action_name()
if not a_f.get_action_class(action_name): if not a_f.get_action_class(action_name):
@ -378,7 +377,8 @@ class Engine(object):
@classmethod @classmethod
def _get_workbook(cls, workbook_name): def _get_workbook(cls, workbook_name):
wb = db_api.workbook_get(workbook_name) wb = db_api.workbook_get(workbook_name)
return parser.get_workbook(wb.definition)
return spec_parser.get_workbook_spec_from_yaml(wb.definition)
@classmethod @classmethod
def _determine_execution_state(cls, execution, tasks): def _determine_execution_state(cls, execution, tasks):

View File

@ -24,7 +24,7 @@ from mistral import exceptions as exc
from mistral import expressions as expr from mistral import expressions as expr
from mistral.openstack.common import log as logging from mistral.openstack.common import log as logging
from mistral.services import trusts from mistral.services import trusts
from mistral.workbook import tasks as wb_task from mistral.workbook import parser as spec_parser
LOG = logging.getLogger(__name__) LOG = logging.getLogger(__name__)
@ -87,7 +87,7 @@ def prepare_tasks(tasks_to_start, context, workbook, tasks):
# Get action name. Unwrap ad-hoc and reevaluate params if # Get action name. Unwrap ad-hoc and reevaluate params if
# necessary. # necessary.
action_name = wb_task.TaskSpec(task.task_spec)\ action_name = spec_parser.get_task_spec(task.task_spec)\
.get_full_action_name() .get_full_action_name()
openstack_ctx = context.get('openstack') openstack_ctx = context.get('openstack')

View File

@ -86,6 +86,10 @@ class ApplicationContextNotFoundException(MistralException):
message = "Application context not found" message = "Application context not found"
class DSLParsingException(MistralException):
http_code = 400
class InvalidModelException(MistralException): class InvalidModelException(MistralException):
http_code = 400 http_code = 400
message = "Wrong entity definition" message = "Wrong entity definition"

View File

@ -16,13 +16,13 @@
from mistral import context from mistral import context
from mistral.db import api as db_api from mistral.db import api as db_api
from mistral import dsl_parser as parser
from mistral import engine from mistral import engine
from mistral.openstack.common import log from mistral.openstack.common import log
from mistral.openstack.common import periodic_task from mistral.openstack.common import periodic_task
from mistral.openstack.common import threadgroup from mistral.openstack.common import threadgroup
from mistral.services import scheduler as sched from mistral.services import scheduler as sched
from mistral.services import trusts from mistral.services import trusts
from mistral.workbook import parser as spec_parser
LOG = log.getLogger(__name__) LOG = log.getLogger(__name__)
@ -32,6 +32,7 @@ class MistralPeriodicTasks(periodic_task.PeriodicTasks):
def __init__(self, transport=None): def __init__(self, transport=None):
super(MistralPeriodicTasks, self).__init__() super(MistralPeriodicTasks, self).__init__()
self.transport = engine.get_transport(transport) self.transport = engine.get_transport(transport)
self.engine = engine.EngineClient(self.transport) self.engine = engine.EngineClient(self.transport)
@ -44,11 +45,13 @@ class MistralPeriodicTasks(periodic_task.PeriodicTasks):
context.set_ctx(ctx) context.set_ctx(ctx)
wb = db_api.workbook_get(trigger['workbook_name']) wb = db_api.workbook_get(trigger['workbook_name'])
context.set_ctx(trusts.create_context(wb)) context.set_ctx(trusts.create_context(wb))
try: try:
task = parser.get_workbook( task = spec_parser.get_workbook_spec_from_yaml(
wb['definition']).get_trigger_task_name(trigger['name']) wb['definition']).get_trigger_task_name(trigger['name'])
self.engine.start_workflow_execution(wb['name'], task) self.engine.start_workflow_execution(wb['name'], task)
finally: finally:
sched.set_next_execution_time(trigger) sched.set_next_execution_time(trigger)

View File

@ -17,7 +17,7 @@
from croniter import croniter from croniter import croniter
import datetime import datetime
from mistral.db import api as db_api from mistral.db import api as db_api
from mistral import dsl_parser as parser from mistral.workbook import parser as spec_parser
def get_next_triggers(): def get_next_triggers():
@ -54,8 +54,11 @@ def create_associated_triggers(db_workbook):
if not db_workbook['definition']: if not db_workbook['definition']:
return return
workbook = parser.get_workbook(db_workbook['definition']) wb_spec = spec_parser.get_workbook_spec_from_yaml(
triggers = workbook.get_triggers() db_workbook['definition']
)
triggers = wb_spec.get_triggers()
# Prepare all triggers data in advance to make db transaction shorter. # Prepare all triggers data in advance to make db transaction shorter.
db_triggers = [] db_triggers = []

View File

@ -37,7 +37,7 @@ Workflow:
parameters: parameters:
action: Service:action action: Service:action
triggers: Triggers:
create-vms: create-vms:
type: periodic type: periodic
tasks: create-vms tasks: create-vms

View File

@ -45,7 +45,7 @@ Workflow:
task-four: task-four:
action: MyRest.async-action action: MyRest.async-action
triggers: Triggers:
my-cron: my-cron:
type: periodic type: periodic
tasks: start-task tasks: start-task

View File

@ -0,0 +1,13 @@
---
Version: '2.0'
Workflows:
wf1:
type: reverse
tasks:
task1:
action: std.echo output="Hey"
task2:
action: std.echo output="Hi!"
requires: [task1]

View File

@ -93,7 +93,7 @@ Workflow:
on-finish: on-finish:
create-vms create-vms
triggers: Triggers:
create-vms: create-vms:
type: periodic type: periodic
tasks: create-vms tasks: create-vms

View File

@ -20,11 +20,11 @@ import json
from mistral.actions import action_factory as a_f from mistral.actions import action_factory as a_f
from mistral.actions import std_actions as std from mistral.actions import std_actions as std
from mistral.db.sqlalchemy import models from mistral.db.sqlalchemy import models
from mistral import dsl_parser as parser
from mistral.engine import data_flow from mistral.engine import data_flow
from mistral import exceptions from mistral import exceptions
from mistral.openstack.common import log as logging from mistral.openstack.common import log as logging
from mistral.tests import base from mistral.tests import base
from mistral.workbook import parser as spec_parser
LOG = logging.getLogger(__name__) LOG = logging.getLogger(__name__)
@ -323,16 +323,17 @@ class ActionFactoryTest(base.BaseTest):
self.assertEqual("'Tango and Cash' is a cool movie!", action.run()) self.assertEqual("'Tango and Cash' is a cool movie!", action.run())
def test_resolve_adhoc_action_name(self): def test_resolve_adhoc_action_name(self):
workbook = parser.get_workbook( wb = spec_parser.get_workbook_spec_from_yaml(
base.get_resource('control_flow/one_sync_task.yaml')) base.get_resource('control_flow/one_sync_task.yaml'))
action_name = 'MyActions.concat' action_name = 'MyActions.concat'
action = a_f.resolve_adhoc_action_name(workbook, action_name) action = a_f.resolve_adhoc_action_name(wb, action_name)
self.assertEqual('std.echo', action) self.assertEqual('std.echo', action)
def test_convert_adhoc_action_params(self): def test_convert_adhoc_action_params(self):
workbook = parser.get_workbook( wb = spec_parser.get_workbook_spec_from_yaml(
base.get_resource('control_flow/one_sync_task.yaml')) base.get_resource('control_flow/one_sync_task.yaml'))
action_name = 'MyActions.concat' action_name = 'MyActions.concat'
@ -341,20 +342,20 @@ class ActionFactoryTest(base.BaseTest):
'right': 'Stanley' 'right': 'Stanley'
} }
parameters = a_f.convert_adhoc_action_params(workbook, parameters = a_f.convert_adhoc_action_params(wb,
action_name, action_name,
params) params)
self.assertEqual({'output': 'Stormin Stanley'}, parameters) self.assertEqual({'output': 'Stormin Stanley'}, parameters)
def test_convert_adhoc_action_result(self): def test_convert_adhoc_action_result(self):
workbook = parser.get_workbook( wb = spec_parser.get_workbook_spec_from_yaml(
base.get_resource('control_flow/one_sync_task.yaml')) base.get_resource('control_flow/one_sync_task.yaml'))
action_name = 'MyActions.concat' action_name = 'MyActions.concat'
result = {'output': 'Stormin Stanley'} result = {'output': 'Stormin Stanley'}
parameters = a_f.convert_adhoc_action_result(workbook, parameters = a_f.convert_adhoc_action_result(wb,
action_name, action_name,
result) result)

View File

@ -19,7 +19,7 @@ import copy
from mistral.actions import base as actions_base from mistral.actions import base as actions_base
from mistral.actions import std_actions as std from mistral.actions import std_actions as std
from mistral.tests import base from mistral.tests import base
from mistral.workbook import namespaces as ns from mistral.workbook import parser as spec_parser
NS_SPEC = { NS_SPEC = {
'name': 'my_namespace', 'name': 'my_namespace',
@ -67,7 +67,8 @@ class AdHocActionTest(base.BaseTest):
def test_adhoc_echo_action(self): def test_adhoc_echo_action(self):
ns_raw_spec = copy.copy(NS_SPEC) ns_raw_spec = copy.copy(NS_SPEC)
action_spec = ns.NamespaceSpec(ns_raw_spec).actions.get('my_action') action_spec = spec_parser.get_namespace_spec(ns_raw_spec).\
actions.get('my_action')
# With dic-like output formatter. # With dic-like output formatter.
action = std.AdHocAction(None, std.EchoAction, action_spec, action = std.AdHocAction(None, std.EchoAction, action_spec,
@ -77,7 +78,8 @@ class AdHocActionTest(base.BaseTest):
# With list-like output formatter. # With list-like output formatter.
ns_raw_spec['actions']['my_action']['output'] = ['$', '$'] ns_raw_spec['actions']['my_action']['output'] = ['$', '$']
action_spec = ns.NamespaceSpec(ns_raw_spec).actions.get('my_action') action_spec = spec_parser.get_namespace_spec(ns_raw_spec).\
actions.get('my_action')
action = std.AdHocAction(None, std.EchoAction, action_spec, action = std.AdHocAction(None, std.EchoAction, action_spec,
first="Tango", second="Cash") first="Tango", second="Cash")
@ -88,7 +90,8 @@ class AdHocActionTest(base.BaseTest):
# With single-object output formatter. # With single-object output formatter.
ns_raw_spec['actions']['my_action']['output'] = \ ns_raw_spec['actions']['my_action']['output'] = \
"'{$}' is a cool movie!" "'{$}' is a cool movie!"
action_spec = ns.NamespaceSpec(ns_raw_spec).actions.get('my_action') action_spec = spec_parser.get_namespace_spec(ns_raw_spec).\
actions.get('my_action')
action = std.AdHocAction(None, std.EchoAction, action_spec, action = std.AdHocAction(None, std.EchoAction, action_spec,
first="Tango", second="Cash") first="Tango", second="Cash")
@ -98,7 +101,8 @@ class AdHocActionTest(base.BaseTest):
def test_adhoc_echo_action_with_namespace_parameters(self): def test_adhoc_echo_action_with_namespace_parameters(self):
ns_raw_spec = copy.copy(NS_SPEC_WITH_PARAMS) ns_raw_spec = copy.copy(NS_SPEC_WITH_PARAMS)
action_spec = ns.NamespaceSpec(ns_raw_spec).actions.get('my_action') action_spec = spec_parser.get_namespace_spec(ns_raw_spec).\
actions.get('my_action')
action = std.AdHocAction(None, MyAction, action_spec, action = std.AdHocAction(None, MyAction, action_spec,
first="Bifur", first="Bifur",

View File

@ -22,7 +22,7 @@ from mistral.engine import data_flow
from mistral.engine import states from mistral.engine import states
from mistral.openstack.common import log as logging from mistral.openstack.common import log as logging
from mistral.tests import base from mistral.tests import base
from mistral.workbook import workbook from mistral.workbook import parser as spec_parser
LOG = logging.getLogger(__name__) LOG = logging.getLogger(__name__)
@ -93,7 +93,7 @@ class DataFlowModuleTest(base.DbTestCase):
self.assertEqual('val32', parameters['p2']) self.assertEqual('val32', parameters['p2'])
def test_prepare_tasks(self): def test_prepare_tasks(self):
wb = workbook.WorkbookSpec(WORKBOOK) wb = spec_parser.get_workbook_spec(WORKBOOK)
tasks = [ tasks = [
db_api.task_create(EXEC_ID, TASK.copy()), db_api.task_create(EXEC_ID, TASK.copy()),

View File

@ -21,13 +21,13 @@ from oslo.config import cfg
from mistral.actions import std_actions from mistral.actions import std_actions
from mistral.db import api as db_api from mistral.db import api as db_api
from mistral.db.sqlalchemy import models as m from mistral.db.sqlalchemy import models as m
from mistral import dsl_parser as parser
from mistral import engine from mistral import engine
from mistral.engine.drivers.default import engine as concrete_engine from mistral.engine.drivers.default import engine as concrete_engine
from mistral.engine import states from mistral.engine import states
from mistral import exceptions as exc from mistral import exceptions as exc
from mistral.openstack.common import log as logging from mistral.openstack.common import log as logging
from mistral.tests import base from mistral.tests import base
from mistral.workbook import parser as spec_parser
LOG = logging.getLogger(__name__) LOG = logging.getLogger(__name__)
@ -51,7 +51,7 @@ def get_mock_workbook(file, name='my_wb'):
def _get_workbook(workbook_name): def _get_workbook(workbook_name):
wb = db_api.workbook_get(workbook_name) wb = db_api.workbook_get(workbook_name)
return parser.get_workbook(wb["definition"]) return spec_parser.get_workbook_spec_from_yaml(wb["definition"])
class FailBeforeSuccessMocker(object): class FailBeforeSuccessMocker(object):

View File

@ -14,10 +14,11 @@
# See the License for the specific language governing permissions and # See the License for the specific language governing permissions and
# limitations under the License. # limitations under the License.
from mistral import dsl_parser as parser
from mistral.engine import states from mistral.engine import states
from mistral.engine import workflow from mistral.engine import workflow
from mistral.tests import base from mistral.tests import base
from mistral.workbook import parser as spec_parser
TASKS = [ TASKS = [
{ {
@ -51,8 +52,10 @@ class WorkflowTest(base.DbTestCase):
super(WorkflowTest, self).setUp() super(WorkflowTest, self).setUp()
def test_find_workflow_tasks(self): def test_find_workflow_tasks(self):
wb_definition = base.get_resource("test_rest.yaml")
tasks = workflow.find_workflow_tasks( tasks = workflow.find_workflow_tasks(
parser.get_workbook(base.get_resource("test_rest.yaml")), spec_parser.get_workbook_spec_from_yaml(wb_definition),
"attach-volumes" "attach-volumes"
) )
@ -62,8 +65,10 @@ class WorkflowTest(base.DbTestCase):
self._assert_single_item(tasks, name='attach-volumes') self._assert_single_item(tasks, name='attach-volumes')
def test_find_workflow_tasks_order(self): def test_find_workflow_tasks_order(self):
wb_definition = base.get_resource("test_order.yaml")
tasks = workflow.find_workflow_tasks( tasks = workflow.find_workflow_tasks(
parser.get_workbook(base.get_resource("test_order.yaml")), spec_parser.get_workbook_spec_from_yaml(wb_definition),
'task' 'task'
) )

View File

@ -0,0 +1,60 @@
# -*- coding: utf-8 -*-
#
# Copyright 2013 - Mirantis, 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 mistral.tests import base
from mistral import utils
class UtilsTest(base.BaseTest):
def setUp(self):
super(UtilsTest, self).setUp()
def test_merge_dicts(self):
left = {
'key1': {
'key11': "val11"
},
'key2': 'val2'
}
right = {
'key1': {
'key11': "val111111",
'key12': "val12",
'key13': {
'key131': 'val131'
}
},
'key2': 'val2222',
'key3': 'val3'
}
utils.merge_dicts(left, right)
self.assertDictEqual(
{
'key1': {
'key11': "val11",
'key12': "val12",
'key13': {
'key131': 'val131'
}
},
'key2': 'val2',
'key3': 'val3'
},
left
)

View File

@ -14,8 +14,8 @@
# See the License for the specific language governing permissions and # See the License for the specific language governing permissions and
# limitations under the License. # limitations under the License.
from mistral import dsl_parser as parser
from mistral.tests import base from mistral.tests import base
from mistral.workbook import parser
SIMPLE_WORKBOOK = """ SIMPLE_WORKBOOK = """
@ -26,20 +26,23 @@ Workflow:
""" """
class DSLModelTest(base.BaseTest): class DSLv1ModelTest(base.BaseTest):
def setUp(self): def setUp(self):
super(DSLModelTest, self).setUp() super(DSLv1ModelTest, self).setUp()
# TODO(rakhmerov): Need to have a dedicated resource.
self.doc = base.get_resource("test_rest.yaml") self.doc = base.get_resource("test_rest.yaml")
def test_load_dsl(self): def test_load_dsl(self):
wb = parser.get_workbook(self.doc) wb = parser.get_workbook_spec_from_yaml(self.doc)
self.assertEqual(wb.workflow.tasks.items, wb.tasks.items) self.assertEqual(wb.workflow.tasks.items, wb.tasks.items)
self.assertEqual(wb.tasks.get("create-vms").name, "create-vms") self.assertEqual(wb.tasks.get("create-vms").name, "create-vms")
self.assertEqual(4, len(wb.namespaces.get("MyRest").actions)) self.assertEqual(4, len(wb.namespaces.get("MyRest").actions))
def test_tasks(self): def test_tasks(self):
wb = parser.get_workbook(self.doc) wb = parser.get_workbook_spec_from_yaml(self.doc)
self.assertEqual(len(wb.tasks), 6) self.assertEqual(len(wb.tasks), 6)
attach_volumes = wb.tasks.get("attach-volumes") attach_volumes = wb.tasks.get("attach-volumes")
@ -67,7 +70,7 @@ class DSLModelTest(base.BaseTest):
self.assertEqual(subseq_finish, {"create-vms": ''}) self.assertEqual(subseq_finish, {"create-vms": ''})
def test_actions(self): def test_actions(self):
wb = parser.get_workbook(self.doc) wb = parser.get_workbook_spec_from_yaml(self.doc)
actions = wb.namespaces.get("MyRest").actions actions = wb.namespaces.get("MyRest").actions
@ -92,7 +95,7 @@ class DSLModelTest(base.BaseTest):
self.assertEqual("MyRest", action.namespace) self.assertEqual("MyRest", action.namespace)
def test_namespaces(self): def test_namespaces(self):
wb = parser.get_workbook(self.doc) wb = parser.get_workbook_spec_from_yaml(self.doc)
self.assertEqual(len(wb.namespaces), 2) self.assertEqual(len(wb.namespaces), 2)
@ -101,9 +104,9 @@ class DSLModelTest(base.BaseTest):
self.assertEqual(1, len(nova_namespace.actions)) self.assertEqual(1, len(nova_namespace.actions))
def test_workbook_without_namespaces(self): def test_workbook_without_namespaces(self):
parser.get_workbook(SIMPLE_WORKBOOK) parser.get_workbook_spec_from_yaml(SIMPLE_WORKBOOK)
def test_triggers(self): def test_triggers(self):
wb = parser.get_workbook(self.doc) wb = parser.get_workbook_spec_from_yaml(self.doc)
self.assertEqual(len(wb.get_triggers()), 1) self.assertEqual(len(wb.get_triggers()), 1)

View File

@ -14,6 +14,8 @@
# See the License for the specific language governing permissions and # See the License for the specific language governing permissions and
# limitations under the License. # limitations under the License.
# TODO(rakhmerov): Can we just extend dsl_specs_v1.py and remove this one?.
SAMPLE_TASK_SPEC = { SAMPLE_TASK_SPEC = {
'action': 'MyRest:create-vm', 'action': 'MyRest:create-vm',
'name': 'create-vms', 'name': 'create-vms',
@ -23,27 +25,31 @@ SAMPLE_TASK_SPEC = {
} }
from mistral.tests import base from mistral.tests import base
from mistral.workbook import tasks from mistral.workbook.v1 import tasks
class GetOnStateTest(base.BaseTest): class GetOnStateTest(base.BaseTest):
def setUp(self): def setUp(self):
super(GetOnStateTest, self).setUp() super(GetOnStateTest, self).setUp()
self.task = tasks.TaskSpec(SAMPLE_TASK_SPEC) self.task = tasks.TaskSpec(SAMPLE_TASK_SPEC)
def test_state_finish(self): def test_state_finish(self):
on_finish = self.task.get_on_finish() on_finish = self.task.get_on_finish()
self.assertIsInstance(on_finish, dict) self.assertIsInstance(on_finish, dict)
self.assertIn("attach-volumes", on_finish) self.assertIn("attach-volumes", on_finish)
def test_state_error(self): def test_state_error(self):
on_error = self.task.get_on_error() on_error = self.task.get_on_error()
self.assertIsInstance(on_error, dict) self.assertIsInstance(on_error, dict)
self.assertEqual(len(on_error), 2) self.assertEqual(len(on_error), 2)
self.assertIn("task1", on_error) self.assertIn("task1", on_error)
def test_state_success(self): def test_state_success(self):
on_success = self.task.get_on_success() on_success = self.task.get_on_success()
self.assertIsInstance(on_success, dict) self.assertIsInstance(on_success, dict)
self.assertEqual(len(on_success), 3) self.assertEqual(len(on_success), 3)
self.assertIn("task1", on_success) self.assertIn("task1", on_success)

View File

@ -0,0 +1,166 @@
# -*- coding: utf-8 -*-
#
# Copyright 2013 - Mirantis, 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 mistral.tests import base
from mistral.workbook import parser as spec_parser
VALID_WB = """
---
Version: '2.0'
Namespaces:
ns1:
actions:
action1:
class: std.echo
base-parameters:
output: "Hello {$.name}!"
Workflows:
wf1:
type: reverse
parameters:
- name
- age
tasks:
task1:
action: ns1.action1 name="{$.name}"
task2:
requires: [task1]
action: std.echo output="Thanks {$.name}!"
wf2:
type: direct
tasks:
task3:
workflow: wf1 name="John Doe" age=32
"""
# TODO(rakhmerov): Add more tests when v2 spec is complete.
# TODO(rakhmerov): Add negative tests.
class DSLv2ModelTest(base.BaseTest):
def setUp(self):
super(DSLv2ModelTest, self).setUp()
def test_valid_workbook_spec(self):
wb_spec = spec_parser.get_workbook_spec_from_yaml(VALID_WB)
# Workbook.
ns_specs = wb_spec.get_namespaces()
wf_specs = wb_spec.get_workflows()
tr_specs = wb_spec.get_triggers()
self.assertEqual('2.0', wb_spec.get_version())
self.assertIsNotNone(ns_specs)
self.assertIsNotNone(wf_specs)
self.assertIsNone(tr_specs)
# Namespaces.
self.assertEqual(1, len(ns_specs))
ns_spec = ns_specs.get('ns1')
self.assertIsNotNone(ns_spec)
self.assertEqual('2.0', ns_spec.get_version())
self.assertEqual('ns1', ns_spec.get_name())
self.assertIsNone(ns_spec.get_class())
self.assertIsNone(ns_spec.get_base_parameters())
self.assertIsNone(ns_spec.get_parameters())
action_specs = ns_spec.get_actions()
self.assertEqual(1, len(action_specs))
# Actions.
action_spec = action_specs.get('action1')
self.assertIsNotNone(action_spec)
self.assertEqual('2.0', action_spec.get_version())
self.assertEqual('action1', action_spec.get_name())
self.assertEqual('std.echo', action_spec.get_class())
self.assertDictEqual(
{'output': 'Hello {$.name}!'},
action_spec.get_base_parameters()
)
self.assertDictEqual({}, action_spec.get_parameters())
self.assertIsNone(action_spec.get_output())
# Workflows.
self.assertEqual(2, len(wf_specs))
wf1_spec = wf_specs.get('wf1')
self.assertEqual('2.0', wf1_spec.get_version())
self.assertEqual('wf1', wf1_spec.get_name())
self.assertEqual('reverse', wf1_spec.get_type())
self.assertEqual(2, len(wf1_spec.get_tasks()))
# Tasks.
task1_spec = wf1_spec.get_tasks().get('task1')
self.assertIsNotNone(task1_spec)
self.assertEqual('2.0', task1_spec.get_version())
self.assertEqual('task1', task1_spec.get_name())
self.assertEqual('ns1.action1', task1_spec.get_action_name())
self.assertEqual('action1', task1_spec.get_short_action_name())
self.assertEqual({'name': '{$.name}'}, task1_spec.get_parameters())
task2_spec = wf1_spec.get_tasks().get('task2')
self.assertIsNotNone(task2_spec)
self.assertEqual('2.0', task2_spec.get_version())
self.assertEqual('task2', task2_spec.get_name())
self.assertEqual('std.echo', task2_spec.get_action_name())
self.assertEqual('echo', task2_spec.get_short_action_name())
self.assertEqual('std', task2_spec.get_action_namespace())
self.assertIsNone(task2_spec.get_workflow_name())
self.assertIsNone(task2_spec.get_short_workflow_name())
self.assertIsNone(task2_spec.get_workflow_namespace())
self.assertEqual(
{'output': 'Thanks {$.name}!'},
task2_spec.get_parameters()
)
wf2_spec = wf_specs.get('wf2')
self.assertEqual('2.0', wf2_spec.get_version())
self.assertEqual('wf2', wf2_spec.get_name())
self.assertEqual('direct', wf2_spec.get_type())
self.assertEqual(1, len(wf2_spec.get_tasks()))
task3_spec = wf2_spec.get_tasks().get('task3')
self.assertIsNotNone(task3_spec)
self.assertEqual('2.0', task3_spec.get_version())
self.assertEqual('task3', task3_spec.get_name())
self.assertIsNone(task3_spec.get_action_name())
self.assertIsNone(task3_spec.get_short_action_name())
self.assertIsNone(task3_spec.get_action_namespace())
self.assertEqual('wf1', task3_spec.get_workflow_name())
self.assertEqual('wf1', task3_spec.get_short_workflow_name())
self.assertIsNone(task3_spec.get_workflow_namespace())
self.assertEqual(
{'name': 'John Doe', 'age': '32'},
task3_spec.get_parameters()
)

View File

View File

@ -14,14 +14,35 @@
# See the License for the specific language governing permissions and # See the License for the specific language governing permissions and
# limitations under the License. # limitations under the License.
from mistral.db.sqlalchemy import models as m
from mistral.openstack.common import log as logging from mistral.openstack.common import log as logging
from mistral.tests import base from mistral.tests import base
from mistral.workbook import parser as spec_parser
from mistral.workflow import reverse_workflow as r_wf
LOG = logging.getLogger(__name__) LOG = logging.getLogger(__name__)
class ReverseWorkflowHandlerTest(base.BaseTest): class ReverseWorkflowHandlerTest(base.BaseTest):
def setUp(self):
super(ReverseWorkflowHandlerTest, self).setUp()
wf_spec = spec_parser.get_workbook_spec_from_yaml(
base.get_resource('dsl_v2/reverse_workflow.yaml')
)
exec_db = m.WorkflowExecution()
exec_db.update({
'id': '1-2-3-4',
'wf_spec': wf_spec.to_dict()
})
self.handler = r_wf.ReverseWorkflowHandler(exec_db)
def test_start_workflow(self): def test_start_workflow(self):
# self.handler.start_workflow()
# TODO(rakhmerov): Implement. # TODO(rakhmerov): Implement.
pass pass

View File

@ -65,7 +65,7 @@ def set_thread_local(var_name, val):
if val: if val:
gl_storage = _get_greenlet_local_storage() gl_storage = _get_greenlet_local_storage()
if not gl_storage: if not gl_storage:
gl_storage =\ gl_storage = \
_th_loc_storage.greenlet_locals[corolocal.get_ident()] = {} _th_loc_storage.greenlet_locals[corolocal.get_ident()] = {}
gl_storage[var_name] = val gl_storage[var_name] = val
@ -93,3 +93,20 @@ def log_exec(logger, level=logging.INFO):
return _logged return _logged
return _decorator return _decorator
def merge_dicts(left, right):
"""Merges two dictionaries.
Values of right dictionary recursively get merged into left dictionary.
:param left: Left dictionary.
:param right: Right dictionary.
"""
for k, v in right.iteritems():
if k not in left:
left[k] = v
else:
left_v = left[k]
if isinstance(left_v, dict) and isinstance(v, dict):
merge_dicts(left_v, v)

View File

@ -1,36 +0,0 @@
# -*- coding: utf-8 -*-
#
# Copyright 2013 - Mirantis, 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 mistral.workbook import base
class ActionSpec(base.BaseSpec):
_required_keys = ['name', 'class', 'namespace']
def __init__(self, action):
super(ActionSpec, self).__init__(action)
if self.validate():
self.name = action['name']
self.clazz = action['class']
self.namespace = action['namespace']
self.base_parameters = action.get('base-parameters', {})
self.parameters = action.get('parameters', {})
self.output = action.get('output', {})
class ActionSpecList(base.BaseSpecList):
item_class = ActionSpec

View File

@ -1,5 +1,3 @@
# -*- coding: utf-8 -*-
#
# Copyright 2013 - Mirantis, Inc. # Copyright 2013 - Mirantis, Inc.
# #
# Licensed under the Apache License, Version 2.0 (the "License"); # Licensed under the Apache License, Version 2.0 (the "License");
@ -14,26 +12,65 @@
# See the License for the specific language governing permissions and # See the License for the specific language governing permissions and
# limitations under the License. # limitations under the License.
from mistral import exceptions import jsonschema
import six
from mistral import exceptions as exc
class BaseSpec(object): class BaseSpec(object):
_required_keys = [] # See http://json-schema.org
_schema = {
"type": "object",
}
_version = "1.0"
def __init__(self, data): def __init__(self, data):
self._data = data self._data = data
self.validate()
def validate(self): def validate(self):
if not all(k in self._data for k in self._required_keys): try:
message = ("Wrong model definition for: %s. It should contain" jsonschema.validate(self._data, self._schema)
" required keys: %s" % (self.__class__.__name__, except jsonschema.ValidationError as e:
self._required_keys)) raise exc.InvalidModelException("Invalid DSL: %s" % e)
raise exceptions.InvalidModelException(message)
return True def _spec_property(self, prop_name, spec_cls):
prop_val = self._data.get(prop_name)
return spec_cls(prop_val) if prop_val else None
def _inject_version(self, prop_names):
for prop_name in prop_names:
prop_data = self._data.get(prop_name)
if isinstance(prop_data, dict):
prop_data['version'] = self._version
def _get_as_dict(self, prop_name):
prop_val = self._data.get(prop_name)
if not prop_val:
return {}
if isinstance(prop_val, dict):
return prop_val
elif isinstance(prop_val, list):
result = {}
for t in prop_val:
result.update(t if isinstance(t, dict) else {t: ''})
return result
elif isinstance(prop_val, six.string_types):
return {prop_val: ''}
def to_dict(self): def to_dict(self):
return self._data return self._data
def get_version(self):
return self._version
class BaseSpecList(object): class BaseSpecList(object):
item_class = None item_class = None
@ -42,11 +79,9 @@ class BaseSpecList(object):
self.items = {} self.items = {}
for k, v in data.items(): for k, v in data.items():
v['name'] = k if k != 'version':
self.items[k] = self.item_class(v) v['name'] = k
self.items[k] = self.item_class(v)
for name in self:
self.get(name).validate()
def __iter__(self): def __iter__(self):
return iter(self.items) return iter(self.items)

View File

@ -1,69 +0,0 @@
# -*- coding: utf-8 -*-
#
# Copyright 2013 - Mirantis, 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 mistral.workbook import actions
from mistral.workbook import base
def merge_dicts(left, right):
for k, v in right.iteritems():
if k not in left:
left[k] = v
else:
left_v = left[k]
if isinstance(left_v, dict) and isinstance(v, dict):
merge_dicts(left_v, v)
def merge_base_parameters(action, ns_base_parameters):
if not ns_base_parameters:
return
if 'base-parameters' not in action:
action['base-parameters'] = ns_base_parameters
return
action_base_parameters = action['base-parameters']
merge_dicts(action_base_parameters, ns_base_parameters)
class NamespaceSpec(base.BaseSpec):
_required_keys = ['name', 'actions']
def __init__(self, namespace):
super(NamespaceSpec, self).__init__(namespace)
if self.validate():
self.name = namespace['name']
self.clazz = namespace.get('class')
self.base_parameters = namespace.get('base-parameters')
self.parameters = namespace.get('parameters')
for _, action in namespace['actions'].iteritems():
action['namespace'] = self.name
if 'class' not in action:
action['class'] = self.clazz
merge_base_parameters(action, self.base_parameters)
self.actions = actions.ActionSpecList(namespace['actions'])
class NamespaceSpecList(base.BaseSpecList):
item_class = NamespaceSpec

111
mistral/workbook/parser.py Normal file
View File

@ -0,0 +1,111 @@
# Copyright 2013 - Mirantis, 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 yaml
from yaml import error
from mistral import exceptions as exc
from mistral.workbook.v1 import actions as actions_v1
from mistral.workbook.v1 import namespaces as ns_v1
from mistral.workbook.v1 import tasks as tasks_v1
from mistral.workbook.v1 import workbook as wb_v1
from mistral.workbook.v1 import workflow as wf_v1
from mistral.workbook.v2 import actions as actions_v2
from mistral.workbook.v2 import namespaces as ns_v2
from mistral.workbook.v2 import tasks as tasks_v2
from mistral.workbook.v2 import workbook as wb_v2
from mistral.workbook.v2 import workflows as wf_v2
V1_0 = '1.0'
V2_0 = '2.0'
ALL_VERSIONS = [V1_0, V2_0]
def parse_yaml(text):
"""Loads a text in YAML format as dictionary object.
:param text: YAML text.
:return: Parsed YAML document as dictionary.
"""
try:
return yaml.safe_load(text)
except error.YAMLError as e:
raise RuntimeError("Definition could not be parsed: %s\n" % e)
def _get_spec_version(spec_dict):
# If version is not specified it will '1.0' by default.
ver = V1_0
if 'Version' in spec_dict:
ver = spec_dict['Version']
elif 'version' in spec_dict:
ver = spec_dict['version']
if ver not in ALL_VERSIONS:
raise exc.DSLParsingException('Unsupported DSL version: %s' % ver)
return ver
# Factory methods to get specifications either from raw YAML formatted text or
# from dictionaries parsed from YAML formatted text.
def get_workbook_spec(spec_dict):
if _get_spec_version(spec_dict) == V1_0:
return wb_v1.WorkbookSpec(spec_dict)
else:
return wb_v2.WorkbookSpec(spec_dict)
def get_workbook_spec_from_yaml(text):
spec_dict = parse_yaml(text)
return get_workbook_spec(spec_dict)
def get_namespace_spec(spec_dict):
if _get_spec_version(spec_dict) == V1_0:
return ns_v1.NamespaceSpec(spec_dict)
else:
return ns_v2.NamespaceSpec(spec_dict)
def get_action_spec(spec_dict):
if _get_spec_version(spec_dict) == V1_0:
return actions_v1.ActionSpec(spec_dict)
else:
return actions_v2.ActionSpec(spec_dict)
def get_workflow_spec(spec_dict):
if _get_spec_version(spec_dict) == V1_0:
return wf_v1.WorkflowSpec(spec_dict)
else:
return wf_v2.WorkflowSpec(spec_dict)
def get_task_spec(spec_dict):
if _get_spec_version(spec_dict) == V1_0:
return tasks_v1.TaskSpec(spec_dict)
else:
return tasks_v2.TaskSpec(spec_dict)
def get_trigger_spec(spec_dict):
# TODO(rakhmerov): Implement.
pass

View File

@ -0,0 +1,46 @@
# Copyright 2014 - Mirantis, 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 mistral.workbook import base
class ActionSpec(base.BaseSpec):
# See http://json-schema.org
_schema = {
"type": "object",
"properties": {
"name": {"type": "string"},
"class": {"type": "string"},
"namespace": {"type": "string"},
"base-parameters": {"type": "object"},
"parameters": {"type": "array"},
"output": {},
},
"required": ["name", "class", "namespace"],
"additionalProperties": False
}
def __init__(self, data):
super(ActionSpec, self).__init__(data)
self.name = data['name']
self.clazz = data['class']
self.namespace = data['namespace']
self.base_parameters = data.get('base-parameters', {})
self.parameters = data.get('parameters', {})
self.output = data.get('output')
class ActionSpecList(base.BaseSpecList):
item_class = ActionSpec

View File

@ -0,0 +1,67 @@
# Copyright 2014 - Mirantis, 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 mistral import utils
from mistral.workbook import base
from mistral.workbook.v1 import actions
def merge_base_parameters(action, ns_base_parameters):
if not ns_base_parameters:
return
if 'base-parameters' not in action:
action['base-parameters'] = ns_base_parameters
return
action_base_parameters = action['base-parameters']
utils.merge_dicts(action_base_parameters, ns_base_parameters)
class NamespaceSpec(base.BaseSpec):
# See http://json-schema.org
_schema = {
"type": "object",
"properties": {
"name": {"type": "string"},
"class": {"type": ["string", "null"]},
"base-parameters": {"type": ["object", "null"]},
"actions": {"type": "object"}
},
"required": ["name", "actions"],
"additionalProperties": False
}
def __init__(self, data):
super(NamespaceSpec, self).__init__(data)
self.name = data['name']
self.clazz = data.get('class')
self.base_parameters = data.get('base-parameters')
self.parameters = data.get('parameters')
for _, action in data['actions'].iteritems():
action['namespace'] = self.name
if 'class' not in action:
action['class'] = self.clazz
merge_base_parameters(action, self.base_parameters)
self.actions = actions.ActionSpecList(data['actions'])
class NamespaceSpecList(base.BaseSpecList):
item_class = NamespaceSpec

View File

@ -1,6 +1,4 @@
# -*- coding: utf-8 -*- # Copyright 2014 - Mirantis, Inc.
#
# Copyright 2013 - Mirantis, Inc.
# #
# Licensed under the Apache License, Version 2.0 (the "License"); # Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License. # you may not use this file except in compliance with the License.
@ -14,24 +12,37 @@
# See the License for the specific language governing permissions and # See the License for the specific language governing permissions and
# limitations under the License. # limitations under the License.
import six
from mistral.workbook import base from mistral.workbook import base
class TaskSpec(base.BaseSpec): class TaskSpec(base.BaseSpec):
_required_keys = ['name', 'action'] # See http://json-schema.org
_schema = {
"type": "object",
"properties": {
"name": {"type": "string"},
"action": {"type": ["string", "null"]},
"parameters": {"type": ["object", "null"]},
"publish": {"type": ["object", "null"]},
"retry": {"type": ["object", "null"]},
"requires": {"type": ["object", "string", "array", "null"]},
"on-finish": {"type": ["string", "array", "null"]},
"on-success": {"type": ["string", "array", "null"]},
"on-error": {"type": ["string", "array", "null"]}
},
"required": ["name", "action"],
"additionalProperties": False
}
def __init__(self, task): def __init__(self, data):
super(TaskSpec, self).__init__(task) super(TaskSpec, self).__init__(data)
self._prepare(task) self._prepare(data)
if self.validate(): self.requires = data.get('requires')
self.requires = task['requires'] self.action = data['action']
self.action = task['action'] self.name = data['name']
self.name = task['name'] self.parameters = data.get('parameters', {})
self.parameters = task.get('parameters', {})
def _prepare(self, task): def _prepare(self, task):
if task: if task:
@ -42,22 +53,6 @@ class TaskSpec(base.BaseSpec):
elif isinstance(req, dict): elif isinstance(req, dict):
task['requires'] = req task['requires'] = req
def _get_as_dict(self, key):
tasks = self.get_property(key)
if not tasks:
return {}
if isinstance(tasks, dict):
return tasks
elif isinstance(tasks, list):
result = {}
for t in tasks:
result.update(t if isinstance(t, dict) else {t: ''})
return result
elif isinstance(tasks, six.string_types):
return {tasks: ''}
def get_property(self, property_name, default=None): def get_property(self, property_name, default=None):
return self._data.get(property_name, default) return self._data.get(property_name, default)

View File

@ -1,6 +1,4 @@
# -*- coding: utf-8 -*- # Copyright 2014 - Mirantis, Inc.
#
# Copyright 2013 - Mirantis, Inc.
# #
# Licensed under the Apache License, Version 2.0 (the "License"); # Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License. # you may not use this file except in compliance with the License.
@ -15,28 +13,38 @@
# limitations under the License. # limitations under the License.
from mistral.workbook import base from mistral.workbook import base
from mistral.workbook import namespaces from mistral.workbook.v1 import namespaces
from mistral.workbook import workflow from mistral.workbook.v1 import workflow
class WorkbookSpec(base.BaseSpec): class WorkbookSpec(base.BaseSpec):
_required_keys = ['Workflow'] # See http://json-schema.org
_schema = {
"type": "object",
"properties": {
"Namespaces": {"type": "object"},
"Workflow": {"type": "object"},
"Triggers": {"type": "object"}
},
"required": ["Workflow"],
"additionalProperties": False
}
def __init__(self, doc): def __init__(self, doc):
super(WorkbookSpec, self).__init__(doc) super(WorkbookSpec, self).__init__(doc)
self.namespaces = {} self.namespaces = {}
if self.validate(): ns_dict = self._data.get('Namespaces')
ns_dict = self._data.get('Namespaces')
if ns_dict: if ns_dict:
self.namespaces = namespaces.NamespaceSpecList(ns_dict) self.namespaces = namespaces.NamespaceSpecList(ns_dict)
self.workflow = workflow.WorkflowSpec(self._data['Workflow']) self.workflow = workflow.WorkflowSpec(self._data['Workflow'])
self.tasks = self.workflow.tasks self.tasks = self.workflow.tasks
def get_triggers(self): def get_triggers(self):
triggers_from_data = self._data.get("triggers", None) triggers_from_data = self._data.get("Triggers", None)
if not triggers_from_data: if not triggers_from_data:
return [] return []
@ -63,5 +71,5 @@ class WorkbookSpec(base.BaseSpec):
return self.namespaces.get(namespace_name).actions return self.namespaces.get(namespace_name).actions
def get_trigger_task_name(self, trigger_name): def get_trigger_task_name(self, trigger_name):
trigger = self._data["triggers"].get(trigger_name) trigger = self._data["Triggers"].get(trigger_name)
return trigger.get('tasks') if trigger else "" return trigger.get('tasks') if trigger else ""

View File

@ -1,6 +1,4 @@
# -*- coding: utf-8 -*- # Copyright 2014 - Mirantis, Inc.
#
# Copyright 2013 - Mirantis, Inc.
# #
# Licensed under the Apache License, Version 2.0 (the "License"); # Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License. # you may not use this file except in compliance with the License.
@ -15,14 +13,21 @@
# limitations under the License. # limitations under the License.
from mistral.workbook import base from mistral.workbook import base
from mistral.workbook import tasks from mistral.workbook.v1 import tasks
class WorkflowSpec(base.BaseSpec): class WorkflowSpec(base.BaseSpec):
_required_keys = ['tasks'] # See http://json-schema.org
_schema = {
"type": "object",
"properties": {
"tasks": {"type": "object"},
},
"required": ["tasks"],
"additionalProperties": False
}
def __init__(self, workflow): def __init__(self, workflow):
super(WorkflowSpec, self).__init__(workflow) super(WorkflowSpec, self).__init__(workflow)
if self.validate(): self.tasks = tasks.TaskSpecList(workflow['tasks'])
self.tasks = tasks.TaskSpecList(workflow['tasks'])

View File

@ -0,0 +1,68 @@
# Copyright 2014 - Mirantis, 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 mistral.workbook import base
# TODO(rakhmerov): In progress.
class ActionSpec(base.BaseSpec):
# See http://json-schema.org
_schema = {
"type": "object",
"properties": {
"name": {"type": "string"},
"class": {"type": "string"},
"namespace": {"type": "string"},
"base-parameters": {"type": "object"},
"parameters": {"type": "array"},
"output": {},
},
"required": ["name", "class", "namespace"],
"additionalProperties": False
}
_version = '2.0'
def __init__(self, data):
super(ActionSpec, self).__init__(data)
self._name = data['name']
self._class = data['class']
self._namespace = data['namespace']
self._base_parameters = data.get('base-parameters', {})
self._parameters = data.get('parameters', {})
self._output = data.get('output')
def get_name(self):
return self._name
def get_class(self):
return self._class
def get_namespace(self):
return self._namespace
def get_base_parameters(self):
return self._base_parameters
def get_parameters(self):
return self._parameters
def get_output(self):
return self._output
class ActionSpecList(base.BaseSpecList):
item_class = ActionSpec

View File

@ -0,0 +1,85 @@
# Copyright 2014 - Mirantis, 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 mistral import utils
from mistral.workbook import base
from mistral.workbook.v2 import actions
# TODO(rakhmerov): It currently duplicates the method in ../v1/namespaces.py
def merge_base_parameters(action, ns_base_parameters):
if not ns_base_parameters:
return
if 'base-parameters' not in action:
action['base-parameters'] = ns_base_parameters
return
action_base_parameters = action['base-parameters']
utils.merge_dicts(action_base_parameters, ns_base_parameters)
class NamespaceSpec(base.BaseSpec):
# See http://json-schema.org
_schema = {
"type": "object",
"properties": {
"name": {"type": "string"},
"class": {"type": ["string", "null"]},
"base-parameters": {"type": ["object", "null"]},
"actions": {"type": "object"}
},
"required": ["name", "actions"],
"additionalProperties": False
}
_version = '2.0'
def __init__(self, data):
super(NamespaceSpec, self).__init__(data)
self._name = data['name']
self._clazz = data.get('class')
self._base_parameters = data.get('base-parameters')
self._parameters = data.get('parameters')
for _, action in data['actions'].iteritems():
action['namespace'] = self._name
if 'class' not in action:
action['class'] = self._clazz
merge_base_parameters(action, self._base_parameters)
self._actions = self._spec_property('actions', actions.ActionSpecList)
def get_name(self):
return self._name
def get_class(self):
return self._clazz
def get_base_parameters(self):
return self._base_parameters
def get_parameters(self):
return self._parameters
def get_actions(self):
return self._actions
class NamespaceSpecList(base.BaseSpecList):
item_class = NamespaceSpec

View File

@ -0,0 +1,49 @@
# Copyright 2014 - Mirantis, 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 mistral.workbook import base
# TODO(rakhmerov): In progress.
class RetrySpec(base.BaseSpec):
# See http://json-schema.org
_schema = {
"type": "object",
"properties": {
"count": {"type": "integer"},
"break-on": {"type": "string"},
"delay": {"type": "integer"}
},
"required": ["count", "delay"],
"additionalProperties": False
}
_version = '2.0'
def __init__(self, data):
super(RetrySpec, self).__init__(data)
self._count = data['count']
self._break_on = data['break-on']
self._delay = data['delay']
def get_count(self):
return self._count
def get_break_on(self):
return self._break_on
def get_delay(self):
return self._delay

View File

@ -0,0 +1,152 @@
# Copyright 2014 - Mirantis, 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 re
import six
from mistral import exceptions as exc
from mistral import utils
from mistral.workbook import base
from mistral.workbook.v2 import retry
# TODO(rakhmerov): In progress.
CMD_PTRN = re.compile("^[\w\.]+[^=\s\"]*")
PARAMS_PTRN = re.compile("([\w]+)=(\".*\"|\'.*'|[\d\.]*)")
class TaskSpec(base.BaseSpec):
# See http://json-schema.org
_schema = {
"type": "object",
"properties": {
"name": {"type": "string"},
"action": {"type": ["string", "null"]},
"workflow": {"type": ["string", "null"]},
"parameters": {"type": ["object", "null"]},
"publish": {"type": ["object", "null"]},
"retry": {"type": ["object", "null"]},
"requires": {"type": ["string", "array", "null"]},
"on-finish": {"type": ["string", "object", "array", "null"]},
"on-success": {"type": ["string", "object", "array", "null"]},
"on-error": {"type": ["string", "object", "array", "null"]}
},
"required": ["name"],
"additionalProperties": False
}
_version = '2.0'
def __init__(self, data):
super(TaskSpec, self).__init__(data)
self._name = data['name']
self._action = data.get('action', None)
self._workflow = data.get('workflow', None)
self._parameters = data.get('parameters', {})
self._publish = data.get('publish', {})
self._retry = self._spec_property('retry', retry.RetrySpec)
self._requires = data.get('requires', [])
self._process_action_and_workflow()
def _process_action_and_workflow(self):
if self._action and self._workflow:
msg = "Task properties 'action' and 'workflow' can't be" \
" specified both:" % self._data
raise exc.InvalidModelException(msg)
if not self._action and not self._workflow:
msg = "One of task properties 'action' or 'workflow' must be" \
" specified:" % self._data
raise exc.InvalidModelException(msg)
if self._action:
self._action, params = self._parse_cmd_and_params(self._action)
elif self._workflow:
self._workflow, params = self._parse_cmd_and_params(self._workflow)
utils.merge_dicts(self._parameters, params)
def _parse_cmd_and_params(self, cmd_str):
# TODO(rakhmerov): Try to find a way with one expression.
cmd_matcher = CMD_PTRN.search(cmd_str)
if not cmd_matcher:
msg = "Invalid action/workflow task property: %s" % cmd_str
raise exc.InvalidModelException(msg)
cmd = cmd_matcher.group()
params = {}
for k, v in re.findall(PARAMS_PTRN, cmd_str):
params[k] = v.replace('"', '').replace("'", '')
return cmd, params
def get_name(self):
return self._name
def get_action_name(self):
return self._action if self._action else None
def get_action_namespace(self):
if not self._action:
return None
arr = self._action.split('.')
return arr[0] if len(arr) > 1 else None
def get_short_action_name(self):
return self._action.split('.')[-1] if self._action else None
def get_workflow_name(self):
return self._workflow
def get_workflow_namespace(self):
if not self._workflow:
return None
arr = self._workflow.split('.')
return arr[0] if len(arr) > 1 else None
def get_short_workflow_name(self):
return self._workflow.split('.')[-1] if self._workflow else None
def get_parameters(self):
return self._parameters
def get_retry(self):
return self._retry
def get_requires(self):
if isinstance(self._requires, six.string_types):
return [self._requires]
return self._requires
def get_on_finish(self):
return self._get_as_dict("on-finish")
def get_on_success(self):
return self._get_as_dict("on-success")
def get_on_error(self):
return self._get_as_dict("on-error")
class TaskSpecList(base.BaseSpecList):
item_class = TaskSpec

View File

@ -1,6 +1,4 @@
# -*- coding: utf-8 -*- # Copyright 2014 - Mirantis, Inc.
#
# Copyright 2013 - Mirantis, Inc.
# #
# Licensed under the Apache License, Version 2.0 (the "License"); # Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License. # you may not use this file except in compliance with the License.
@ -14,19 +12,18 @@
# See the License for the specific language governing permissions and # See the License for the specific language governing permissions and
# limitations under the License. # limitations under the License.
import yaml from mistral.workbook import base
from yaml import error
from mistral.workbook import workbook # TODO(rakhmerov): In progress.
def parse(workbook_definition): class TriggerSpec(base.BaseSpec):
"""Loads a workbook definition in YAML format as dictionary object.""" _version = '2.0'
try:
return yaml.safe_load(workbook_definition) def __init__(self, data):
except error.YAMLError as exc: super(TriggerSpec, self).__init__(data)
raise RuntimeError("Definition could not be parsed: %s\n" % exc) # TODO(rakhmerov): Implement.
def get_workbook(workbook_definition): class TriggerSpecList(base.BaseSpecList):
return workbook.WorkbookSpec(parse(workbook_definition)) item_class = TriggerSpec

View File

@ -0,0 +1,54 @@
# Copyright 2014 - Mirantis, 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 mistral.workbook import base
from mistral.workbook.v2 import namespaces as ns
from mistral.workbook.v2 import triggers as tr
from mistral.workbook.v2 import workflows as wf
class WorkbookSpec(base.BaseSpec):
# See http://json-schema.org
_schema = {
"type": "object",
"properties": {
"Version": {"value": "2.0"},
"Namespaces": {"type": "object"},
"Workflows": {"type": "object"},
"Triggers": {"type": "object"}
},
"additionalProperties": False
}
_version = '2.0'
def __init__(self, data):
super(WorkbookSpec, self).__init__(data)
self._inject_version(['Namespaces', 'Workflows', 'Triggers'])
self._namespaces = \
self._spec_property('Namespaces', ns.NamespaceSpecList)
self._workflows = \
self._spec_property('Workflows', wf.WorkflowSpecList)
self._triggers = self._spec_property('Triggers', tr.TriggerSpecList)
def get_namespaces(self):
return self._namespaces
def get_workflows(self):
return self._workflows
def get_triggers(self):
return self._triggers

View File

@ -0,0 +1,53 @@
# Copyright 2014 - Mirantis, 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 mistral.workbook import base
from mistral.workbook.v2 import tasks
class WorkflowSpec(base.BaseSpec):
# See http://json-schema.org
_schema = {
"type": "object",
"properties": {
"name": {"type": "string"},
"type": {"enum": ["reverse", "direct"]},
"parameters": {"type": ["array", "null"]},
"tasks": {"type": "object"},
},
"required": ["name", "type", "tasks"],
"additionalProperties": False
}
_version = '2.0'
def __init__(self, data):
super(WorkflowSpec, self).__init__(data)
self._name = data['name']
self._type = data['type']
self._tasks = self._spec_property('tasks', tasks.TaskSpecList)
def get_name(self):
return self._name
def get_type(self):
return self._type
def get_tasks(self):
return self._tasks
class WorkflowSpecList(base.BaseSpecList):
item_class = WorkflowSpec

View File

@ -1,5 +1,3 @@
# -*- coding: utf-8 -*-
#
# Copyright 2014 - Mirantis, Inc. # Copyright 2014 - Mirantis, Inc.
# #
# Licensed under the Apache License, Version 2.0 (the "License"); # Licensed under the Apache License, Version 2.0 (the "License");

View File

@ -1,5 +1,3 @@
# -*- coding: utf-8 -*-
#
# Copyright 2014 - Mirantis, Inc. # Copyright 2014 - Mirantis, Inc.
# #
# Licensed under the Apache License, Version 2.0 (the "License"); # Licensed under the Apache License, Version 2.0 (the "License");

View File

@ -1,5 +1,3 @@
# -*- coding: utf-8 -*-
#
# Copyright 2014 - Mirantis, Inc. # Copyright 2014 - Mirantis, Inc.
# #
# Licensed under the Apache License, Version 2.0 (the "License"); # Licensed under the Apache License, Version 2.0 (the "License");

View File

@ -1,5 +1,3 @@
# -*- coding: utf-8 -*-
#
# Copyright 2014 - Mirantis, Inc. # Copyright 2014 - Mirantis, Inc.
# #
# Licensed under the Apache License, Version 2.0 (the "License"); # Licensed under the Apache License, Version 2.0 (the "License");

View File

@ -24,3 +24,4 @@ six>=1.7.0
SQLAlchemy>=0.7.8,!=0.9.5,<=0.9.99 SQLAlchemy>=0.7.8,!=0.9.5,<=0.9.99
stevedore>=0.14 stevedore>=0.14
yaql==0.2.1 # This is not in global requirements yaql==0.2.1 # This is not in global requirements
jsonschema>=2.0.0,<3.0.0