From bf3039184123f915fdf1bea1171557200e533888 Mon Sep 17 00:00:00 2001 From: Liuqing Jing Date: Thu, 19 Nov 2015 17:38:21 +0800 Subject: [PATCH] Support workflow creation Partial-Implements: blueprint workflow Change-Id: I19f4a82b373cbc6f4c4367648ec1c66a35ce4e2e --- evoque/api/controllers/v1/__init__.py | 2 + evoque/api/controllers/v1/workflow.py | 28 ++++++++++ evoque/api/hooks.py | 3 ++ evoque/cmd/engine.py | 2 + evoque/db/api.py | 5 ++ evoque/db/sqlalchemy/api.py | 8 +++ .../migrate_repo/versions/001_evoque_init.py | 21 ++++++++ evoque/db/sqlalchemy/models.py | 18 +++++++ evoque/db/sqlalchemy/types.py | 5 ++ evoque/engine/workflow/__init__.py | 51 +++++++++++++++++++ evoque/engine/workflow/api.py | 22 ++++++++ evoque/engine/workflow/endpoint.py | 25 +++++++++ evoque/engine/workflow/spiff.py | 33 ++++++++++++ evoque/opts.py | 3 ++ setup.cfg | 3 ++ 15 files changed, 229 insertions(+) create mode 100644 evoque/api/controllers/v1/workflow.py create mode 100644 evoque/engine/workflow/__init__.py create mode 100644 evoque/engine/workflow/api.py create mode 100644 evoque/engine/workflow/endpoint.py create mode 100644 evoque/engine/workflow/spiff.py diff --git a/evoque/api/controllers/v1/__init__.py b/evoque/api/controllers/v1/__init__.py index 5817264..6812176 100644 --- a/evoque/api/controllers/v1/__init__.py +++ b/evoque/api/controllers/v1/__init__.py @@ -13,6 +13,7 @@ import pecan from evoque.api.controllers.v1 import ticket +from evoque.api.controllers.v1 import workflow class Controller(object): @@ -20,6 +21,7 @@ class Controller(object): def __init__(self): self.sub_controllers = { "ticket": ticket.Controller(), + "workflow": workflow.Controller(), } for name, ctrl in self.sub_controllers.items(): diff --git a/evoque/api/controllers/v1/workflow.py b/evoque/api/controllers/v1/workflow.py new file mode 100644 index 0000000..6c8479b --- /dev/null +++ b/evoque/api/controllers/v1/workflow.py @@ -0,0 +1,28 @@ +# 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 pecan +from pecan import rest + + +class Controller(rest.RestController): + + @pecan.expose('json') + def get(self): + return {"version": "1.0.0"} + + @pecan.expose('json') + def post(self, **kwargs): + workflow = pecan.request.workflow_api.workflow_create( + name=kwargs['name'], wf_spec=kwargs['wf_spec']) + + return workflow diff --git a/evoque/api/hooks.py b/evoque/api/hooks.py index 1d85ddb..67c4b0e 100644 --- a/evoque/api/hooks.py +++ b/evoque/api/hooks.py @@ -16,6 +16,7 @@ from oslo_config import cfg from evoque.common import context from evoque.engine.ticket import api as ticket_api +from evoque.engine.workflow import api as workflow_api CONF = cfg.CONF CONF.import_opt('auth_uri', 'keystonemiddleware.auth_token', @@ -79,3 +80,5 @@ class RPCHook(hooks.PecanHook): def before(self, state): state.request.ticket_api = ticket_api.API( context=state.request.context) + state.request.workflow_api = workflow_api.API( + context=state.request.context) diff --git a/evoque/cmd/engine.py b/evoque/cmd/engine.py index 69ad69b..1ac4d68 100644 --- a/evoque/cmd/engine.py +++ b/evoque/cmd/engine.py @@ -23,6 +23,7 @@ from evoque.common.i18n import _LI from evoque.common import rpc_service from evoque.common import service as evoque_service from evoque.engine.ticket import endpoint as ticket_endpoint +from evoque.engine.workflow import endpoint as workflow_endpoint LOG = logging.getLogger(__name__) @@ -36,6 +37,7 @@ def main(): endpoints = [ ticket_endpoint.Handler(), + workflow_endpoint.Handler(), ] server = rpc_service.Service.create("evoque-engine", diff --git a/evoque/db/api.py b/evoque/db/api.py index 6798696..418d367 100644 --- a/evoque/db/api.py +++ b/evoque/db/api.py @@ -45,6 +45,11 @@ def ticket_get_all(context, filters=None, limit=None, marker=None, sort_key=None, sort_dir=None) +# Workflows +def workflow_create(context, values): + return IMPL.workflow_create(context, values) + + # Utils def db_sync(engine, version=None): """Migrate the database to `version` or the most recent version.""" diff --git a/evoque/db/sqlalchemy/api.py b/evoque/db/sqlalchemy/api.py index cc82a67..2bf6768 100644 --- a/evoque/db/sqlalchemy/api.py +++ b/evoque/db/sqlalchemy/api.py @@ -89,6 +89,14 @@ def ticket_get_all(context, filters=None, limit=None, marker=None, sort_key, sort_dir, query) +# Workflows +def workflow_create(context, values): + workflow_ref = models.Workflow() + workflow_ref.update(values) + workflow_ref.save(_session()) + return workflow_ref + + # Utils def db_sync(engine, version=None): """Migrate the database to `version` or the most recent version.""" diff --git a/evoque/db/sqlalchemy/migrate_repo/versions/001_evoque_init.py b/evoque/db/sqlalchemy/migrate_repo/versions/001_evoque_init.py index ab5c30a..32649a1 100644 --- a/evoque/db/sqlalchemy/migrate_repo/versions/001_evoque_init.py +++ b/evoque/db/sqlalchemy/migrate_repo/versions/001_evoque_init.py @@ -40,8 +40,29 @@ def upgrade(migrate_engine): mysql_charset='utf8' ) + workflow = sqlalchemy.Table( + 'workflow', meta, + sqlalchemy.Column('id', sqlalchemy.String(36), + primary_key=True, nullable=False), + sqlalchemy.Column('name', sqlalchemy.String(255)), + sqlalchemy.Column('spec', types.MediumText()), + sqlalchemy.Column('user', sqlalchemy.String(32)), + sqlalchemy.Column('project', sqlalchemy.String(32)), + sqlalchemy.Column('domain', sqlalchemy.String(32)), + sqlalchemy.Column('user_id', sqlalchemy.String(255)), + sqlalchemy.Column('project_id', sqlalchemy.String(255)), + sqlalchemy.Column('domain_id', sqlalchemy.String(255)), + sqlalchemy.Column('metadata', types.Dict), + sqlalchemy.Column('created_at', sqlalchemy.DateTime), + sqlalchemy.Column('updated_at', sqlalchemy.DateTime), + sqlalchemy.Column('deleted_at', sqlalchemy.DateTime), + mysql_engine='InnoDB', + mysql_charset='utf8' + ) + tables = ( ticket, + workflow ) for index, table in enumerate(tables): diff --git a/evoque/db/sqlalchemy/models.py b/evoque/db/sqlalchemy/models.py index 4df1394..5819709 100644 --- a/evoque/db/sqlalchemy/models.py +++ b/evoque/db/sqlalchemy/models.py @@ -51,3 +51,21 @@ class Ticket(BASE, EvoqueBase): user_id = sqlalchemy.Column(sqlalchemy.String(255)) project_id = sqlalchemy.Column(sqlalchemy.String(255)) domain_id = sqlalchemy.Column(sqlalchemy.String(255)) + + +class Workflow(BASE, EvoqueBase): + """Represents a workflow created by the Evoque engine.""" + + __tablename__ = 'workflow' + + id = sqlalchemy.Column('id', sqlalchemy.String(36), primary_key=True, + default=lambda: str(uuid.uuid4())) + name = sqlalchemy.Column('name', sqlalchemy.String(255)) + spec = sqlalchemy.Column('spec', types.MediumText()) + + user = sqlalchemy.Column(sqlalchemy.String(32)) + project = sqlalchemy.Column(sqlalchemy.String(32)) + domain = sqlalchemy.Column(sqlalchemy.String(32)) + user_id = sqlalchemy.Column(sqlalchemy.String(255)) + project_id = sqlalchemy.Column(sqlalchemy.String(255)) + domain_id = sqlalchemy.Column(sqlalchemy.String(255)) diff --git a/evoque/db/sqlalchemy/types.py b/evoque/db/sqlalchemy/types.py index 9731a6a..a439039 100644 --- a/evoque/db/sqlalchemy/types.py +++ b/evoque/db/sqlalchemy/types.py @@ -13,6 +13,7 @@ import json from sqlalchemy.dialects import mysql +from sqlalchemy import Text from sqlalchemy import types @@ -32,3 +33,7 @@ class Dict(types.TypeDecorator): if value is None: return None return json.loads(value) + + +def MediumText(): + return Text().with_variant(mysql.MEDIUMTEXT(), 'mysql') diff --git a/evoque/engine/workflow/__init__.py b/evoque/engine/workflow/__init__.py new file mode 100644 index 0000000..c69955b --- /dev/null +++ b/evoque/engine/workflow/__init__.py @@ -0,0 +1,51 @@ +# 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 abc +import six + +from oslo_config import cfg +from stevedore import driver + +from evoque.common.i18n import _ + +OPTS = [ + cfg.StrOpt('workflow_engine', + default='spiff', + help=_('The Evoque workflow engine driver.')), +] + +CONF = cfg.CONF +CONF.register_opts(OPTS) + + +def get_workflow(engine=CONF.workflow_engine, namespace='evoque.workflow'): + """Get workflow driver and load it. + + :param engine: workflow engine + :param namespace: Namespace to use to look for drivers. + """ + + loaded_driver = driver.DriverManager(namespace, engine) + return loaded_driver.driver() + + +@six.add_metaclass(abc.ABCMeta) +class Base(object): + """Base class for workflow operations.""" + + def __init__(self): + super(Base, self).__init__() + + @abc.abstractmethod + def workflow_create(self, wf_spec): + """Create a workflow""" diff --git a/evoque/engine/workflow/api.py b/evoque/engine/workflow/api.py new file mode 100644 index 0000000..9e41108 --- /dev/null +++ b/evoque/engine/workflow/api.py @@ -0,0 +1,22 @@ +# 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 evoque.common import rpc_service + + +class API(rpc_service.API): + def __init__(self, transport=None, context=None, topic=None): + super(API, self).__init__(transport, context, + topic="evoque-engine") + + def workflow_create(self, name, wf_spec): + return self._call('workflow_create', name=name, wf_spec=wf_spec) diff --git a/evoque/engine/workflow/endpoint.py b/evoque/engine/workflow/endpoint.py new file mode 100644 index 0000000..58625d7 --- /dev/null +++ b/evoque/engine/workflow/endpoint.py @@ -0,0 +1,25 @@ +# 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 evoque.engine import workflow + + +class Handler(object): + + def __init__(self): + super(Handler, self).__init__() + self.workflow_driver = workflow.get_workflow() + + def workflow_create(self, context, name, wf_spec): + workflow = self.workflow_driver.workflow_create( + context, name, wf_spec) + return {'workflow': workflow} diff --git a/evoque/engine/workflow/spiff.py b/evoque/engine/workflow/spiff.py new file mode 100644 index 0000000..8efd532 --- /dev/null +++ b/evoque/engine/workflow/spiff.py @@ -0,0 +1,33 @@ +# 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 evoque.db import api as db_api +from evoque.engine import workflow + + +class Workflow(workflow.Base): + """Workflow engine powered by `SpiffWorkflow` + + https://github.com/knipknap/SpiffWorkflow + """ + + def __init__(self): + super(Workflow, self).__init__() + + def workflow_create(self, context, name, wf_spec): + """Create a workflow""" + values = { + 'name': name, + 'spec': wf_spec + } + workflow = db_api.workflow_create(context, values) + return workflow diff --git a/evoque/opts.py b/evoque/opts.py index 9f49293..2cb11f0 100644 --- a/evoque/opts.py +++ b/evoque/opts.py @@ -52,5 +52,8 @@ def list_opts(): cfg.StrOpt('host', default=socket.getfqdn(), help=_('The listen IP for the Evoque engine server.')), + cfg.StrOpt('workflow_engine', + default='spiff', + help=_('The Evoque workflow engine driver.')), )), ] diff --git a/setup.cfg b/setup.cfg index acc343f..a20d2b2 100644 --- a/setup.cfg +++ b/setup.cfg @@ -29,6 +29,9 @@ console_scripts = evoque-manage = evoque.cmd.manage:main evoque-engine = evoque.cmd.engine:main +evoque.workflow = + spiff = evoque.engine.workflow.spiff:Workflow + oslo.config.opts = evoque = evoque.opts:list_opts