diff --git a/README.md b/README.md index a86c5209..1984545d 100644 --- a/README.md +++ b/README.md @@ -1,4 +1,24 @@ Mistral ======= -Task service for OpenStack cloud +Task Orchestration and Scheduling service for OpenStack cloud + + +Running in development mode +--------------------------- + +### Installation +First of all, in a shell run: + +*tox* + +This will install necessary virtual environments and run all the project tests. Installing virtual environments may take significant time (~10-15 mins). + +### Mistral configuration + +Open *etc/mistral.conf* file and fix configuration properties as needed. For example, *host* and *port* specified by default may not be desired in a particular environment. + +### Running Mistral API server +To run Mistral API server perform the following commands in a shell: + +*tox -epy27 -- /mistral/cmd/api.py --config-file etc/mistral.conf* \ No newline at end of file diff --git a/mistral/api/controllers/common_types.py b/mistral/api/controllers/common_types.py deleted file mode 100644 index 1bdd8b9c..00000000 --- a/mistral/api/controllers/common_types.py +++ /dev/null @@ -1,22 +0,0 @@ -# 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 wsme import types as wtypes - - -class Link(wtypes.Base): - """Web link.""" - - href = wtypes.text - target = wtypes.text diff --git a/mistral/api/controllers/resource.py b/mistral/api/controllers/resource.py new file mode 100644 index 00000000..b28bb3b4 --- /dev/null +++ b/mistral/api/controllers/resource.py @@ -0,0 +1,64 @@ +# -*- 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 wsme import types as wtypes + + +class Resource(wtypes.Base): + """REST API Resource.""" + + def to_dict(self): + # TODO: take care of nested resources + d = {} + + for attr in self._wsme_attributes: + d[attr.name] = getattr(self, attr.name) + + return d + + @classmethod + def from_dict(cls, d): + # TODO: take care of nested resources + obj = cls() + + for key, val in d.items(): + if hasattr(obj, key): + setattr(obj, key, val) + + return obj + + def __str__(self): + """WSME based implementation of __str__.""" + + res = "%s [" % type(self).__name__ + + first = True + for attr in self._wsme_attributes: + if not first: + res += ', ' + else: + first = False + + res += "%s='%s'" % (attr.name, getattr(self, attr.name)) + + return res + "]" + + +class Link(Resource): + """Web link.""" + + href = wtypes.text + target = wtypes.text diff --git a/mistral/api/controllers/root.py b/mistral/api/controllers/root.py index 7927e77f..8418a173 100644 --- a/mistral/api/controllers/root.py +++ b/mistral/api/controllers/root.py @@ -1,3 +1,5 @@ +# -*- coding: utf-8 -*- +# # Copyright 2013 - Mirantis, Inc. # # Licensed under the Apache License, Version 2.0 (the "License"); @@ -16,19 +18,21 @@ import pecan from wsme import types as wtypes import wsmeext.pecan as wsme_pecan -from mistral.api.controllers import common_types +from mistral.openstack.common import log as logging from mistral.api.controllers.v1 import root as v1_root +from mistral.api.controllers import resource +LOG = logging.getLogger(__name__) API_STATUS = wtypes.Enum(str, 'SUPPORTED', 'CURRENT', 'DEPRECATED') -class APIVersion(wtypes.Base): +class APIVersion(resource.Resource): """API Version.""" id = wtypes.text status = API_STATUS - link = common_types.Link + link = resource.Link class RootController(object): @@ -37,9 +41,11 @@ class RootController(object): @wsme_pecan.wsexpose([APIVersion]) def index(self): + LOG.debug("Fetching API versions.") + host_url = '%s/%s' % (pecan.request.host_url, 'v1') api_v1 = APIVersion(id='v1.0', status='CURRENT', - link=common_types.Link(href=host_url, target='v1')) + link=resource.Link(href=host_url, target='v1')) return [api_v1] diff --git a/mistral/api/controllers/v1/execution.py b/mistral/api/controllers/v1/execution.py new file mode 100644 index 00000000..065a24c5 --- /dev/null +++ b/mistral/api/controllers/v1/execution.py @@ -0,0 +1,94 @@ +# 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 pecan import rest +from pecan import abort +from wsme import types as wtypes +import wsmeext.pecan as wsme_pecan + +from mistral.api.controllers.v1 import task +from mistral.openstack.common import log as logging +from mistral.api.controllers import resource +from mistral.db import api as db_api + +LOG = logging.getLogger(__name__) + + +class Execution(resource.Resource): + """Execution resource.""" + + id = wtypes.text + workbook_name = wtypes.text + target_task = wtypes.text + state = wtypes.text + + +class Executions(resource.Resource): + """A collection of Execution resources.""" + + executions = [Execution] + + +class ExecutionsController(rest.RestController): + + tasks = task.TasksController() + + @wsme_pecan.wsexpose(Execution, wtypes.text, wtypes.text) + def get(self, workbook_name, id): + LOG.debug("Fetch execution [workbook_name=%s, id=%s]" % + (workbook_name, id)) + + values = db_api.execution_get(workbook_name, id) + + if not values: + abort(404) + else: + return Execution.from_dict(values) + + @wsme_pecan.wsexpose(Execution, wtypes.text, wtypes.text, body=Execution) + def put(self, workbook_name, id, execution): + LOG.debug("Update execution [workbook_name=%s, id=%s, execution=%s]" % + (workbook_name, id, execution)) + + values = db_api.execution_update(workbook_name, + id, + execution.to_dict()) + + return Execution.from_dict(values) + + @wsme_pecan.wsexpose(Execution, wtypes.text, body=Execution, + status_code=201) + def post(self, workbook_name, execution): + LOG.debug("Create listener [workbook_name=%s, execution=%s]" % + (workbook_name, execution)) + + values = db_api.execution_create(workbook_name, execution.to_dict()) + + return Execution.from_dict(values) + + @wsme_pecan.wsexpose(None, wtypes.text, wtypes.text, status_code=204) + def delete(self, workbook_name, id): + LOG.debug("Delete execution [workbook_name=%s, id=%s]" % + (workbook_name, id)) + + db_api.execution_delete(workbook_name, id) + + @wsme_pecan.wsexpose(Executions, wtypes.text) + def get_all(self, workbook_name): + LOG.debug("Fetch executions [workbook_name=%s]" % workbook_name) + + executions = [Execution.from_dict(values) + for values in db_api.executions_get(workbook_name)] + + return Executions(executions=executions) diff --git a/mistral/api/controllers/v1/listener.py b/mistral/api/controllers/v1/listener.py index a79a523a..64581e8f 100644 --- a/mistral/api/controllers/v1/listener.py +++ b/mistral/api/controllers/v1/listener.py @@ -1,3 +1,5 @@ +# -*- coding: utf-8 -*- +# # Copyright 2013 - Mirantis, Inc. # # Licensed under the Apache License, Version 2.0 (the "License"); @@ -12,19 +14,19 @@ # See the License for the specific language governing permissions and # limitations under the License. -import pecan from pecan import rest -import wsme +from pecan import abort from wsme import types as wtypes import wsmeext.pecan as wsme_pecan from mistral.openstack.common import log as logging - +from mistral.api.controllers import resource +from mistral.db import api as db_api LOG = logging.getLogger(__name__) -class Event(wtypes.Base): +class Event(resource.Resource): """Event descriptor.""" pass @@ -39,8 +41,8 @@ class ExecutionEvent(Event): workbook_name = wtypes.text -class Listener(wtypes.Base): - """Workbook resource.""" +class Listener(resource.Resource): + """Listener resource.""" id = wtypes.text description = wtypes.text @@ -49,71 +51,55 @@ class Listener(wtypes.Base): events = [Event] -class Listeners(wtypes.Base): - """A collection of Listeners.""" +class Listeners(resource.Resource): + """A collection of Listener resources.""" listeners = [Listener] - def __str__(self): - return "Listeners [listeners=%s]" % self.listeners - class ListenersController(rest.RestController): - """Operations on collection of listeners.""" - @wsme_pecan.wsexpose(Listener, wtypes.text, wtypes.text) def get(self, workbook_name, id): LOG.debug("Fetch listener [workbook_name=%s, id=%s]" % (workbook_name, id)) - # TODO: fetch the listener from DB + values = db_api.listener_get(workbook_name, id) - error = "Not implemented" - pecan.response.translatable_error = error - - raise wsme.exc.ClientSideError(unicode(error)) + if not values: + abort(404) + else: + return Listener.from_dict(values) @wsme_pecan.wsexpose(Listener, wtypes.text, wtypes.text, body=Listener) def put(self, workbook_name, id, listener): LOG.debug("Update listener [workbook_name=%s, id=%s, listener=%s]" % (workbook_name, id, listener)) - # TODO: modify the listener in DB + values = db_api.listener_update(workbook_name, id, listener.to_dict()) - error = "Not implemented" - pecan.response.translatable_error = error - - raise wsme.exc.ClientSideError(unicode(error)) - - @wsme_pecan.wsexpose(None, wtypes.text, wtypes.text, status_code=204) - def delete(self, workbook_name, id): - LOG.debug("Delete listener [workbook_name=%s, id=%s]" % - (workbook_name, id)) - - # TODO: delete the listener from DB - - error = "Not implemented" - pecan.response.translatable_error = error - - raise wsme.exc.ClientSideError(unicode(error)) + return Listener.from_dict(values) @wsme_pecan.wsexpose(Listener, wtypes.text, body=Listener, status_code=201) def post(self, workbook_name, listener): LOG.debug("Create listener [workbook_name=%s, listener=%s]" % (workbook_name, listener)) - # TODO: create listener in DB + values = db_api.listener_create(workbook_name, listener.to_dict()) - error = "Not implemented" - pecan.response.translatable_error = error + return Listener.from_dict(values) - raise wsme.exc.ClientSideError(unicode(error)) + @wsme_pecan.wsexpose(None, wtypes.text, wtypes.text, status_code=204) + def delete(self, workbook_name, id): + LOG.debug("Delete listener [workbook_name=%s, id=%s]" % + (workbook_name, id)) + + db_api.listener_delete(workbook_name, id) @wsme_pecan.wsexpose(Listeners, wtypes.text) def get_all(self, workbook_name): LOG.debug("Fetch listeners [workbook_name=%s]" % workbook_name) - listeners = [] - # TODO: fetch listeners from DB + listeners = [Listener.from_dict(values) + for values in db_api.listeners_get(workbook_name)] return Listeners(listeners=listeners) diff --git a/mistral/api/controllers/v1/root.py b/mistral/api/controllers/v1/root.py index 5c7fc644..e7fcf1e1 100644 --- a/mistral/api/controllers/v1/root.py +++ b/mistral/api/controllers/v1/root.py @@ -17,9 +17,10 @@ from wsme import types as wtypes import wsmeext.pecan as wsme_pecan from mistral.api.controllers.v1 import workbook +from mistral.api.controllers import resource -class RootResource(wtypes.Base): +class RootResource(resource.Resource): """Root resource for API version 1. It references all other resources belonging to the API. @@ -28,6 +29,7 @@ class RootResource(wtypes.Base): uri = wtypes.text # TODO: what else do we need here? + # TODO: we need to collect all the links from API v1.0 and provide them class Controller(object): diff --git a/mistral/api/controllers/v1/task.py b/mistral/api/controllers/v1/task.py new file mode 100644 index 00000000..10113873 --- /dev/null +++ b/mistral/api/controllers/v1/task.py @@ -0,0 +1,79 @@ +# 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 pecan import rest +from pecan import abort +from wsme import types as wtypes +import wsmeext.pecan as wsme_pecan + +from mistral.openstack.common import log as logging +from mistral.api.controllers import resource +from mistral.db import api as db_api + +LOG = logging.getLogger(__name__) + + +class Task(resource.Resource): + """Task resource.""" + + id = wtypes.text + workbook_name = wtypes.text + execution_id = wtypes.text + name = wtypes.text + description = wtypes.text + action = wtypes.text + state = wtypes.text + tags = [wtypes.text] + + +class Tasks(resource.Resource): + """A collection of tasks.""" + + tasks = [Task] + + +class TasksController(rest.RestController): + @wsme_pecan.wsexpose(Task, wtypes.text, wtypes.text, wtypes.text) + def get(self, workbook_name, execution_id, id): + LOG.debug("Fetch task [workbook_name=%s, execution_id=%s, id=%s]" % + (workbook_name, execution_id, id)) + + values = db_api.task_get(workbook_name, execution_id, id) + + if not values: + abort(404) + + return Task.from_dict(values) + + @wsme_pecan.wsexpose(Task, wtypes.text, wtypes.text, wtypes.text, + body=Task) + def put(self, workbook_name, execution_id, id, task): + LOG.debug("Update task " + "[workbook_name=%s, execution_id=%s, id=%s, task=%s]" % + (workbook_name, execution_id, id, task)) + + values = db_api.task_update(workbook_name, execution_id, id, + task.to_dict()) + + return Task.from_dict(values) + + @wsme_pecan.wsexpose(Tasks, wtypes.text, wtypes.text) + def get_all(self, workbook_name, execution_id): + LOG.debug("Fetch tasks [workbook_name=%s, execution_id=%s]" % + (workbook_name, execution_id)) + + tasks = [Task.from_dict(values) + for values in db_api.tasks_get(workbook_name, execution_id)] + + return Tasks(tasks=tasks) diff --git a/mistral/api/controllers/v1/workbook.py b/mistral/api/controllers/v1/workbook.py index 355e4344..6d3a0510 100644 --- a/mistral/api/controllers/v1/workbook.py +++ b/mistral/api/controllers/v1/workbook.py @@ -1,3 +1,5 @@ +# -*- coding: utf-8 -*- +# # Copyright 2013 - Mirantis, Inc. # # Licensed under the Apache License, Version 2.0 (the "License"); @@ -12,99 +14,77 @@ # See the License for the specific language governing permissions and # limitations under the License. -import pecan from pecan import rest -import wsme +from pecan import abort from wsme import types as wtypes import wsmeext.pecan as wsme_pecan +from mistral.api.controllers.v1 import workbook_definition from mistral.api.controllers.v1 import listener +from mistral.api.controllers.v1 import execution +from mistral.api.controllers import resource + from mistral.openstack.common import log as logging +from mistral.db import api as db_api + +LOG = logging.getLogger(__name__) -LOG = logging.getLogger("%s" % __name__) - - -class Workbook(wtypes.Base): +class Workbook(resource.Resource): """Workbook resource.""" name = wtypes.text description = wtypes.text tags = [wtypes.text] - def __str__(self): - return "Workbook [name='%s', description='%s', tags='%s']" % \ - (self.name, self.description, self.tags) - -class Workbooks(wtypes.Base): +class Workbooks(resource.Resource): """A collection of Workbooks.""" workbooks = [Workbook] - def __str__(self): - return "Workbooks [workbooks=%s]" % self.workbooks - class WorkbooksController(rest.RestController): - """Operations on collection of workbooks.""" + definition = workbook_definition.WorkbookDefinitionController() listeners = listener.ListenersController() - - #@pecan.expose() - #def _lookup(self, workbook_name, *remainder): - # # Standard Pecan delegation. - # return WorkbookController(workbook_name), remainder + executions = execution.ExecutionsController() @wsme_pecan.wsexpose(Workbook, wtypes.text) def get(self, name): LOG.debug("Fetch workbook [name=%s]" % name) - # TODO: fetch the workbook from the DB + values = db_api.workbook_get(name) - error = "Not implemented" - pecan.response.translatable_error = error - - raise wsme.exc.ClientSideError(unicode(error)) + if not values: + abort(404) + else: + return Workbook.from_dict(values) @wsme_pecan.wsexpose(Workbook, wtypes.text, body=Workbook) def put(self, name, workbook): LOG.debug("Update workbook [name=%s, workbook=%s]" % (name, workbook)) - # TODO: modify the workbook in DB - - error = "Not implemented" - pecan.response.translatable_error = error - - raise wsme.exc.ClientSideError(unicode(error)) - - @wsme_pecan.wsexpose(None, wtypes.text, status_code=204) - def delete(self, name): - LOG.debug("Delete workbook [name=%s]" % name) - - # TODO: delete the workbook from DB - - error = "Not implemented" - pecan.response.translatable_error = error - - raise wsme.exc.ClientSideError(unicode(error)) + return Workbook.from_dict(db_api.workbook_update(name, + workbook.to_dict())) @wsme_pecan.wsexpose(Workbook, body=Workbook, status_code=201) def post(self, workbook): LOG.debug("Create workbook [workbook=%s]" % workbook) - # TODO: create the listener in DB + return Workbook.from_dict(db_api.workbook_create(workbook.to_dict())) - error = "Not implemented" - pecan.response.translatable_error = error + @wsme_pecan.wsexpose(None, wtypes.text, status_code=204) + def delete(self, name): + LOG.debug("Delete workbook [name=%s]" % name) - raise wsme.exc.ClientSideError(unicode(error)) + db_api.workbook_delete(name) @wsme_pecan.wsexpose(Workbooks) def get_all(self): LOG.debug("Fetch workbooks.") - workbooks = [] - # TODO: fetch workbooks from DB + workbooks = [Workbook.from_dict(values) + for values in db_api.workbooks_get()] return Workbooks(workbooks=workbooks) diff --git a/mistral/api/controllers/v1/workbook_definition.py b/mistral/api/controllers/v1/workbook_definition.py new file mode 100644 index 00000000..b75ec4bf --- /dev/null +++ b/mistral/api/controllers/v1/workbook_definition.py @@ -0,0 +1,40 @@ +# 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 pecan import rest +from pecan import expose +from pecan import request + +from mistral.openstack.common import log as logging +from mistral.db import api as db_api + +LOG = logging.getLogger(__name__) + + +class WorkbookDefinitionController(rest.RestController): + @expose() + def get(self, workbook_name): + LOG.debug("Fetch workbook definition [workbook_name=%s]" % + workbook_name) + + return db_api.workbook_definition_get(workbook_name) + + @expose(content_type="text/plain") + def put(self, workbook_name): + text = request.text + + LOG.debug("Update workbook definition [workbook_name=%s, text=%s]" % + (workbook_name, text)) + + return db_api.workbook_definition_put(workbook_name, text) diff --git a/mistral/db/api.py b/mistral/db/api.py new file mode 100644 index 00000000..82ee6bf8 --- /dev/null +++ b/mistral/db/api.py @@ -0,0 +1,117 @@ +# -*- 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. + +# TODO: replace this module later with a real implementation + +# Workbooks + + +def workbook_get(name): + return {} + + +def workbook_create(values): + return values + + +def workbook_update(name, values): + return values + + +def workbook_delete(name): + pass + + +def workbooks_get(): + return [{}] + + +def workbook_definition_get(workbook_name): + return "" + + +def workbook_definition_put(workbook_name, text): + return text + + +# Executions + + +def execution_get(workbook_name, id): + return {} + + +def execution_create(workbook_name, values): + return values + + +def execution_update(workbook_name, id, values): + return values + + +def execution_delete(workbook_name, id): + pass + + +def executions_get(workbook_name): + return [{}] + + +# Tasks + +def task_get(workbook_name, execution_id, id): + return {} + + +def task_create(workbook_name, execution_id, values): + return values + + +def task_update(workbook_name, execution_id, id, values): + return values + + +def task_delete(workbook_name, execution_id, id): + pass + + +def tasks_get(workbook_name, execution_id): + return [{}] + + +# Listeners + + +def listener_get(workbook_name, id): + return {} + + +def listener_create(workbook_name, values): + values['id'] = 1 + + return values + + +def listener_update(workbook_name, id, values): + return values + + +def listener_delete(workbook_name, id): + pass + + +def listeners_get(workbook_name): + return [{}] diff --git a/mistral/tests/api/v1/controllers/test_executions.py b/mistral/tests/api/v1/controllers/test_executions.py new file mode 100644 index 00000000..ca42778b --- /dev/null +++ b/mistral/tests/api/v1/controllers/test_executions.py @@ -0,0 +1,77 @@ +# -*- 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. + +import mock + +from mistral.tests.api import base +from mistral.db import api as db_api + +# TODO: later we need additional tests verifying all the errors etc. + +EXECS = [ + { + 'id': "123", + 'workbook_name': "my_workbook", + 'target_task': 'my_task', + 'state': 'RUNNING' + } +] + + +class TestExecutionsController(base.FunctionalTest): + def test_get(self): + db_api.execution_get = mock.MagicMock(return_value=EXECS[0]) + + resp = self.app.get('/v1/workbooks/my_workbook/executions/123') + + self.assertEqual(resp.status_int, 200) + self.assertDictEqual(EXECS[0], resp.json) + + def test_put(self): + updated_exec = EXECS[0].copy() + updated_exec['state'] = 'STOPPED' + + db_api.execution_update = mock.MagicMock(return_value=updated_exec) + + resp = self.app.put_json('/v1/workbooks/my_workbook/executions/123', + dict(state='STOPPED')) + + self.assertEqual(resp.status_int, 200) + self.assertDictEqual(updated_exec, resp.json) + + def test_post(self): + db_api.execution_create = mock.MagicMock(return_value=EXECS[0]) + + resp = self.app.post_json('/v1/workbooks/my_workbook/executions', + EXECS[0]) + + self.assertEqual(resp.status_int, 201) + self.assertDictEqual(EXECS[0], resp.json) + + def test_delete(self): + resp = self.app.delete('/v1/workbooks/my_workbook/executions/123') + + self.assertEqual(resp.status_int, 204) + + def test_get_all(self): + db_api.executions_get = mock.MagicMock(return_value=EXECS) + + resp = self.app.get('/v1/workbooks/my_workbook/executions') + + self.assertEqual(resp.status_int, 200) + + self.assertEqual(len(resp.json), 1) + self.assertDictEqual(EXECS[0], resp.json['executions'][0]) diff --git a/mistral/tests/api/v1/controllers/test_listeners.py b/mistral/tests/api/v1/controllers/test_listeners.py new file mode 100644 index 00000000..e79c86f6 --- /dev/null +++ b/mistral/tests/api/v1/controllers/test_listeners.py @@ -0,0 +1,77 @@ +# -*- 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. + +import mock + +from mistral.tests.api import base +from mistral.db import api as db_api + +# TODO: later we need additional tests verifying all the errors etc. + +LISTENERS = [ + { + 'id': "1", + 'workbook_name': "my_workbook", + 'description': "My cool Mistral workbook", + 'webhook': "http://my.website.org" + } +] + + +class TestListenersController(base.FunctionalTest): + def test_get(self): + db_api.listener_get = mock.MagicMock(return_value=LISTENERS[0]) + + resp = self.app.get('/v1/workbooks/my_workbook/listeners/1') + + self.assertEqual(resp.status_int, 200) + self.assertDictEqual(LISTENERS[0], resp.json) + + def test_put(self): + updated_lsnr = LISTENERS[0].copy() + updated_lsnr['description'] = 'new description' + + db_api.listener_update = mock.MagicMock(return_value=updated_lsnr) + + resp = self.app.put_json('/v1/workbooks/my_workbook/listeners/1', + dict(description='new description')) + + self.assertEqual(resp.status_int, 200) + self.assertDictEqual(updated_lsnr, resp.json) + + def test_post(self): + db_api.listener_create = mock.MagicMock(return_value=LISTENERS[0]) + + resp = self.app.post_json('/v1/workbooks/my_workbook/listeners', + LISTENERS[0]) + + self.assertEqual(resp.status_int, 201) + self.assertDictEqual(LISTENERS[0], resp.json) + + def test_delete(self): + resp = self.app.delete('/v1/workbooks/my_workbook/listeners/1') + + self.assertEqual(resp.status_int, 204) + + def test_get_all(self): + db_api.listeners_get = mock.MagicMock(return_value=LISTENERS) + + resp = self.app.get('/v1/workbooks/my_workbook/listeners') + + self.assertEqual(resp.status_int, 200) + + self.assertEqual(len(resp.json), 1) + self.assertDictEqual(LISTENERS[0], resp.json['listeners'][0]) diff --git a/mistral/tests/api/v1/controllers/test_tasks.py b/mistral/tests/api/v1/controllers/test_tasks.py new file mode 100644 index 00000000..e822d9a4 --- /dev/null +++ b/mistral/tests/api/v1/controllers/test_tasks.py @@ -0,0 +1,68 @@ +# -*- 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. + +import mock + +from mistral.tests.api import base +from mistral.db import api as db_api + +# TODO: later we need additional tests verifying all the errors etc. + +TASKS = [ + { + 'id': "1", + 'workbook_name': "my_workbook", + 'execution_id': '123', + 'name': 'my_task', + 'description': 'My cool task', + 'action': 'my_action', + 'state': 'RUNNING', + 'tags': ['deployment', 'demo'] + } +] + + +class TestTasksController(base.FunctionalTest): + def test_get(self): + db_api.task_get = mock.MagicMock(return_value=TASKS[0]) + + resp = self.app.get('/v1/workbooks/my_workbook/executions/123/tasks/1') + + self.assertEqual(resp.status_int, 200) + self.assertDictEqual(TASKS[0], resp.json) + + def test_put(self): + updated_task = TASKS[0].copy() + updated_task['state'] = 'STOPPED' + + db_api.task_update = mock.MagicMock(return_value=updated_task) + + resp = self.app.put_json( + '/v1/workbooks/my_workbook/executions/123/tasks/1', + dict(state='STOPPED')) + + self.assertEqual(resp.status_int, 200) + self.assertDictEqual(updated_task, resp.json) + + def test_get_all(self): + db_api.tasks_get = mock.MagicMock(return_value=TASKS) + + resp = self.app.get('/v1/workbooks/my_workbook/executions/123/tasks') + + self.assertEqual(resp.status_int, 200) + + self.assertEqual(len(resp.json), 1) + self.assertDictEqual(TASKS[0], resp.json['tasks'][0]) diff --git a/mistral/tests/api/v1/controllers/test_workbook_definition.py b/mistral/tests/api/v1/controllers/test_workbook_definition.py new file mode 100644 index 00000000..48b128a6 --- /dev/null +++ b/mistral/tests/api/v1/controllers/test_workbook_definition.py @@ -0,0 +1,49 @@ +# -*- 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. + +import mock + +from mistral.tests.api import base +from mistral.db import api as db_api + +# TODO: later we need additional tests verifying all the errors etc. + +DEFINITION = "my definition" + + +class TestWorkbookDefinitionController(base.FunctionalTest): + def test_get(self): + db_api.workbook_definition_get =\ + mock.MagicMock(return_value=DEFINITION) + + resp = self.app.get('/v1/workbooks/my_workbook/definition', + headers={"Content-Type": "text/plain"}) + + self.assertEqual(resp.status_int, 200) + self.assertEqual(DEFINITION, resp.text) + + def test_put(self): + new_definition = "new definition" + + db_api.workbook_definition_update =\ + mock.MagicMock(return_value=new_definition) + + resp = self.app.put('/v1/workbooks/my_workbook/definition', + new_definition, + headers={"Content-Type": "text/plain"}) + + self.assertEqual(resp.status_int, 200) + self.assertEqual(new_definition, resp.body) diff --git a/mistral/tests/api/v1/controllers/test_workbooks.py b/mistral/tests/api/v1/controllers/test_workbooks.py index 59726350..5ef30356 100644 --- a/mistral/tests/api/v1/controllers/test_workbooks.py +++ b/mistral/tests/api/v1/controllers/test_workbooks.py @@ -14,21 +14,62 @@ # See the License for the specific language governing permissions and # limitations under the License. -from mistral.openstack.common import jsonutils +import mock + from mistral.tests.api import base +from mistral.db import api as db_api + +# TODO: later we need additional tests verifying all the errors etc. + +WORKBOOKS = [ + { + 'name': "my_workbook", + 'description': "My cool Mistral workbook", + 'tags': ['deployment', 'demo'] + } +] class TestWorkbooksController(base.FunctionalTest): + def test_get(self): + db_api.workbook_get = mock.MagicMock(return_value=WORKBOOKS[0]) + + resp = self.app.get('/v1/workbooks/my_workbook') + + self.assertEqual(resp.status_int, 200) + self.assertDictEqual(WORKBOOKS[0], resp.json) + + def test_put(self): + updated_workbook = WORKBOOKS[0].copy() + updated_workbook['description'] = 'new description' + + db_api.workbook_update = mock.MagicMock(return_value=updated_workbook) + + resp = self.app.put_json('/v1/workbooks/my_workbook', + dict(description='new description')) + + self.assertEqual(resp.status_int, 200) + self.assertDictEqual(updated_workbook, resp.json) + + def test_post(self): + db_api.workbook_create = mock.MagicMock(return_value=WORKBOOKS[0]) + + resp = self.app.post_json('/v1/workbooks', WORKBOOKS[0]) + + self.assertEqual(resp.status_int, 201) + self.assertDictEqual(WORKBOOKS[0], resp.json) + + def test_delete(self): + resp = self.app.delete('/v1/workbooks/my_workbook') + + self.assertEqual(resp.status_int, 204) def test_get_all(self): - resp = self.app.get('/v1/workbooks', - headers={'Accept': 'application/json'}) + db_api.workbooks_get = mock.MagicMock(return_value=WORKBOOKS) + + resp = self.app.get('/v1/workbooks') self.assertEqual(resp.status_int, 200) - data = jsonutils.loads(resp.body.decode()) - - print "json=%s" % data - - #self.assertEqual(data['name'], 'my_workbook') - #self.assertEqual(data['description'], 'My cool workbook') + self.assertEqual(len(resp.json), 1) + self.assertDictEqual(WORKBOOKS[0], resp.json['workbooks'][0])