Working on REST API

* Added installation/running info into README.md
* Added pecan controllers for:
  * executions
  * tasks
  * workbook definitions
* Refactored controllers
* Added simple functional tests for workbooks

Implements: blueprint mistral-rest-api
Change-Id: I604b62b8b0bda5f5363b1fefedcb9ffb7447bd1d
This commit is contained in:
Renat Akhmerov 2013-11-29 12:04:12 +07:00
parent 039e899d8b
commit 9288a60915
16 changed files with 804 additions and 126 deletions

View File

@ -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*

View File

@ -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

View File

@ -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

View File

@ -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]

View File

@ -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)

View File

@ -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)

View File

@ -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):

View File

@ -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)

View File

@ -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)

View File

@ -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)

117
mistral/db/api.py Normal file
View File

@ -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 [{}]

View File

@ -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])

View File

@ -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])

View File

@ -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])

View File

@ -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)

View File

@ -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])