diff --git a/mistralclient/__init__.py b/mistralclient/__init__.py new file mode 100644 index 00000000..e69de29b diff --git a/mistralclient/api/__init__.py b/mistralclient/api/__init__.py new file mode 100644 index 00000000..e69de29b diff --git a/mistralclient/api/base.py b/mistralclient/api/base.py new file mode 100644 index 00000000..4d4ba62e --- /dev/null +++ b/mistralclient/api/base.py @@ -0,0 +1,153 @@ +# -*- 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 json +import logging + +LOG = logging.getLogger(__name__) + + +class Resource(object): + resource_name = 'Something' + defaults = {} + + def __init__(self, manager, data): + self.manager = manager + self._data = data + self._set_defaults() + self._set_attributes() + + def _set_defaults(self): + for k, v in self.defaults.iteritems(): + if k not in self._data: + self._data[k] = v + + def _set_attributes(self): + for k, v in self._data.iteritems(): + try: + setattr(self, k, v) + except AttributeError: + # In this case we already defined the attribute on the class + pass + + def __str__(self): + vals = ", ".join(["%s='%s'" % (n, v) + for n, v in self._data.iteritems()]) + return "%s [%s]" % (self.resource_name, vals) + + +def _check_items(obj, searches): + try: + return all(getattr(obj, attr) == value for (attr, value) in searches) + except AttributeError: + return False + + +def extract_json(response, response_key): + if response_key is not None: + return get_json(response)[response_key] + else: + return get_json(response) + + +class ResourceManager(object): + resource_class = None + + def __init__(self, client): + self.client = client + + def find(self, **kwargs): + return [i for i in self.list() if _check_items(i, kwargs.items())] + + def _ensure_not_empty(self, **kwargs): + for name, value in kwargs.iteritems(): + if value is None or (isinstance(value, str) and len(value) == 0): + raise APIException('%s is missing field "%s"' % + (self.resource_class.__name__, name)) + + def _copy_if_defined(self, data, **kwargs): + for name, value in kwargs.iteritems(): + if value is not None: + data[name] = value + + def _create(self, url, data, response_key=None, dump_json=True): + if dump_json: + data = json.dumps(data) + + resp = self.client.http_client.post(url, data) + + if resp.status_code != 201: + self._raise_api_exception(resp) + + return self.resource_class(self, extract_json(resp, response_key)) + + def _update(self, url, data, response_key=None, dump_json=True): + if dump_json: + data = json.dumps(data) + + resp = self.client.http_client.put(url, data) + + if resp.status_code != 200: + self._raise_api_exception(resp) + + return self.resource_class(self, extract_json(resp, response_key)) + + def _list(self, url, response_key=None): + resp = self.client.http_client.get(url) + + if resp.status_code == 200: + return [self.resource_class(self, resource_data) + for resource_data in extract_json(resp, response_key)] + else: + self._raise_api_exception(resp) + + def _get(self, url, response_key=None): + resp = self.client.http_client.get(url) + + if resp.status_code == 200: + return self.resource_class(self, extract_json(resp, response_key)) + else: + self._raise_api_exception(resp) + + def _delete(self, url): + resp = self.client.http_client.delete(url) + + if resp.status_code != 204: + self._raise_api_exception(resp) + + def _plurify_resource_name(self): + return self.resource_class.resource_name + 's' + + def _raise_api_exception(self, resp): + error_data = get_json(resp) + raise APIException(error_data["error_message"]) + + +def get_json(response): + """This method provided backward compatibility with old versions + of requests library + + """ + json_field_or_function = getattr(response, 'json', None) + + if callable(json_field_or_function): + return response.json() + else: + return json.loads(response.content) + + +class APIException(Exception): + pass diff --git a/mistralclient/api/client.py b/mistralclient/api/client.py new file mode 100644 index 00000000..6cbcc436 --- /dev/null +++ b/mistralclient/api/client.py @@ -0,0 +1,45 @@ +# -*- 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 six + +from mistralclient.api import httpclient +from mistralclient.api import workbooks +from mistralclient.api import executions +from mistralclient.api import tasks +from mistralclient.api import listeners + + +class Client(object): + def __init__(self, mistral_url=None): + # TODO: add all required parameters for Keystone authentication + + if mistral_url and not isinstance(mistral_url, six.string_types): + raise RuntimeError('Mistral url should be a string.') + + if not mistral_url: + mistral_url = "http://localhost:8989/v1" + + # TODO: add Keystone authentication later + token = "TBD" + + self.http_client = httpclient.HTTPClient(mistral_url, token) + + # Create all resource managers. + self.workbooks = workbooks.WorkbookManager(self) + self.executions = executions.ExecutionManager(self) + self.tasks = tasks.TaskManager(self) + self.listeners = listeners.ListenerManager(self) diff --git a/mistralclient/api/executions.py b/mistralclient/api/executions.py new file mode 100644 index 00000000..7499489a --- /dev/null +++ b/mistralclient/api/executions.py @@ -0,0 +1,65 @@ +# -*- 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 mistralclient.api import base + + +class Execution(base.Resource): + resource_name = 'Execution' + + +class ExecutionManager(base.ResourceManager): + resource_class = Execution + + def create(self, workbook_name, target_task): + self._ensure_not_empty(workbook_name=workbook_name, + target_task=target_task) + + data = { + 'workbook_name': workbook_name, + 'target_task': target_task + } + + return self._create('/workbooks/%s/executions' % workbook_name, data) + + def update(self, workbook_name, id, state): + self._ensure_not_empty(workbook_name=workbook_name, id=id, + state=state) + + data = { + 'workbook_name': workbook_name, + 'id': id, + 'state': state + } + + return self._update('/workbooks/%s/executions/%s' % + (workbook_name, id), data) + + def list(self, workbook_name): + self._ensure_not_empty(workbook_name=workbook_name) + + return self._list('/workbooks/%s/executions' % workbook_name, + 'executions') + + def get(self, workbook_name, id): + self._ensure_not_empty(workbook_name=workbook_name, id=id) + + return self._get('/workbooks/%s/executions/%s' % (workbook_name, id)) + + def delete(self, workbook_name, id): + self._ensure_not_empty(workbook_name=workbook_name, id=id) + + self._delete('/workbooks/%s/executions/%s' % (workbook_name, id)) diff --git a/mistralclient/api/httpclient.py b/mistralclient/api/httpclient.py new file mode 100644 index 00000000..ef4dbc2e --- /dev/null +++ b/mistralclient/api/httpclient.py @@ -0,0 +1,55 @@ +# -*- 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 requests + + +class HTTPClient(object): + def __init__(self, base_url, token): + self.base_url = base_url + self.token = token + + def get(self, url, headers=None): + if not headers: + headers = {} + + headers['x-auth-token'] = self.token + + return requests.get(self.base_url + url, headers=headers) + + def post(self, url, body, headers=None): + if not headers: + headers = {'content-type': 'application/json'} + + headers['x-auth-token'] = self.token + + return requests.post(self.base_url + url, body, headers=headers) + + def put(self, url, body, headers=None): + if not headers: + headers = {'content-type': 'application/json'} + + headers['x-auth-token'] = self.token + + return requests.put(self.base_url + url, body, headers=headers) + + def delete(self, url, headers=None): + if not headers: + headers = {} + + headers['x-auth-token'] = self.token + + return requests.delete(self.base_url + url, headers=headers) diff --git a/mistralclient/api/listeners.py b/mistralclient/api/listeners.py new file mode 100644 index 00000000..92480c7b --- /dev/null +++ b/mistralclient/api/listeners.py @@ -0,0 +1,71 @@ +# -*- 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 mistralclient.api import base + + +class Listener(base.Resource): + resource_name = 'Listener' + + +class ListenerManager(base.ResourceManager): + resource_class = Listener + + def create(self, workbook_name, webhook, description=None, events=None): + # TODO(rakhmerov): need to describe what events is (data type) + + self._ensure_not_empty(workbook_name=workbook_name, + webhook=webhook) + data = { + 'workbook_name': workbook_name, + 'description': description, + 'webhook': webhook, + 'events': events + } + + return self._create('/workbooks/%s/listeners' % workbook_name, data) + + def update(self, workbook_name, id, webhook=None, description=None, + events=None): + #TODO: need to describe what events is + self._ensure_not_empty(workbook_name=workbook_name, id=id) + + data = { + 'id': id, + 'workbook_name': workbook_name, + 'description': description, + 'webhook': webhook, + 'events': events + } + + return self._update('/workbooks/%s/listeners/%s' % + (workbook_name, id), data) + + def list(self, workbook_name): + self._ensure_not_empty(workbook_name=workbook_name) + + return self._list('/workbooks/%s/listeners' % workbook_name, + 'listeners') + + def get(self, workbook_name, id): + self._ensure_not_empty(workbook_name=workbook_name, id=id) + + return self._get('/workbooks/%s/listeners/%s' % (workbook_name, id)) + + def delete(self, workbook_name, id): + self._ensure_not_empty(workbook_name=workbook_name, id=id) + + self._delete('/workbooks/%s/listeners/%s' % (workbook_name, id)) diff --git a/mistralclient/api/tasks.py b/mistralclient/api/tasks.py new file mode 100644 index 00000000..6da14f61 --- /dev/null +++ b/mistralclient/api/tasks.py @@ -0,0 +1,56 @@ +# -*- 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 mistralclient.api import base + + +class Task(base.Resource): + resource_name = 'Task' + + +class TaskManager(base.ResourceManager): + resource_class = Task + + def update(self, workbook_name, execution_id, id, state): + self._ensure_not_empty(workbook_name=workbook_name, + execution_id=execution_id, + id=id, + state=state) + + data = { + 'workbook_name': workbook_name, + 'execution_id': execution_id, + 'id': id, + 'state': state + } + + return self._update('/workbooks/%s/executions' % workbook_name, data) + + def list(self, workbook_name, execution_id): + self._ensure_not_empty(workbook_name=workbook_name, + execution_id=execution_id) + + return self._list('/workbooks/%s/executions/%s/tasks' % + (workbook_name, execution_id), + 'tasks') + + def get(self, workbook_name, execution_id, id): + self._ensure_not_empty(workbook_name=workbook_name, + execution_id=execution_id, + id=id) + + return self._get('/workbooks/%s/executions/%s/tasks/%s' % + (workbook_name, execution_id, id)) diff --git a/mistralclient/api/workbooks.py b/mistralclient/api/workbooks.py new file mode 100644 index 00000000..f55a919f --- /dev/null +++ b/mistralclient/api/workbooks.py @@ -0,0 +1,72 @@ +# -*- 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 mistralclient.api import base + + +class Workbook(base.Resource): + resource_name = 'Workbook' + + +class WorkbookManager(base.ResourceManager): + resource_class = Workbook + + def create(self, name, description=None, tags=None): + self._ensure_not_empty(name=name) + + data = { + 'name': name, + 'description': description, + 'tags': tags, + } + + return self._create('/workbooks', data) + + def update(self, name, description=None, tags=None): + self._ensure_not_empty(name=name) + + data = { + 'name': name, + 'description': description, + 'tags': tags, + } + + return self._update('/workbooks', data) + + def list(self): + return self._list('/workbooks', 'workbooks') + + def get(self, name): + self._ensure_not_empty(name=name) + + return self._get('/workbooks/%s' % name) + + def delete(self, name): + self._ensure_not_empty(name=name) + + self._delete('/workbooks/%s' % name) + + def upload_definition(self, name, text): + self._ensure_not_empty(name=name) + + self.client.http_client.put('/workbooks/%s/definition' % name, + text, + headers={'content-type': 'text/plain'}) + + def get_definition(self, name): + self._ensure_not_empty(name=name) + + return self.client.http_client.get('/workbooks/%s/definition' % name) diff --git a/mistralclient/tests/__init__.py b/mistralclient/tests/__init__.py new file mode 100644 index 00000000..e69de29b diff --git a/mistralclient/tests/base.py b/mistralclient/tests/base.py new file mode 100644 index 00000000..2f3977e9 --- /dev/null +++ b/mistralclient/tests/base.py @@ -0,0 +1,56 @@ +# -*- 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 unittest2 +import mock + +from mistralclient.api import client + + +class FakeResponse(object): + """Fake response for testing Mistral Client.""" + + def __init__(self, status_code, json_values={}): + self.status_code = status_code + self.json_values = json_values + + def json(self): + return self.json_values + + +class BaseClientTest(unittest2.TestCase): + def setUp(self): + self._client = client.Client() + self.workbooks = self._client.workbooks + self.executions = self._client.executions + self.tasks = self._client.tasks + self.listeners = self._client.listeners + + def mock_http_get(self, json, status_code=200): + self._client.http_client.get =\ + mock.MagicMock(return_value=FakeResponse(status_code, json)) + + def mock_http_post(self, json, status_code=201): + self._client.http_client.post =\ + mock.MagicMock(return_value=FakeResponse(status_code, json)) + + def mock_http_put(self, json, status_code=200): + self._client.http_client.put =\ + mock.MagicMock(return_value=FakeResponse(status_code, json)) + + def mock_http_delete(self, status_code=204): + self._client.http_client.delete =\ + mock.MagicMock(return_value=FakeResponse(status_code)) diff --git a/mistralclient/tests/test_executions.py b/mistralclient/tests/test_executions.py new file mode 100644 index 00000000..579b672c --- /dev/null +++ b/mistralclient/tests/test_executions.py @@ -0,0 +1,86 @@ +# -*- 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 mistralclient.tests import base + +# 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 TestExecutions(base.BaseClientTest): + + def test_create(self): + self.mock_http_post(json=EXECS[0]) + + wb = self.executions.create(EXECS[0]['workbook_name'], + EXECS[0]['target_task']) + + self.assertIsNotNone(wb) + self.assertEqual(EXECS[0]['id'], wb.id) + self.assertEqual(EXECS[0]['workbook_name'], wb.workbook_name) + self.assertEqual(EXECS[0]['target_task'], wb.target_task) + self.assertEqual(EXECS[0]['state'], wb.state) + + def test_update(self): + self.mock_http_put(json=EXECS[0]) + + ex = self.executions.update(EXECS[0]['workbook_name'], + EXECS[0]['id'], + EXECS[0]['state']) + + self.assertIsNotNone(ex) + self.assertEqual(EXECS[0]['id'], ex.id) + self.assertEqual(EXECS[0]['workbook_name'], ex.workbook_name) + self.assertEqual(EXECS[0]['target_task'], ex.target_task) + self.assertEqual(EXECS[0]['state'], ex.state) + + def test_list(self): + self.mock_http_get(json={'executions': EXECS}) + + executions = self.executions.list(EXECS[0]['workbook_name']) + + self.assertEqual(1, len(executions)) + + ex = executions[0] + + self.assertEqual(EXECS[0]['id'], ex.id) + self.assertEqual(EXECS[0]['workbook_name'], ex.workbook_name) + self.assertEqual(EXECS[0]['target_task'], ex.target_task) + self.assertEqual(EXECS[0]['state'], ex.state) + + def test_get(self): + self.mock_http_get(json=EXECS[0]) + + ex = self.executions.get(EXECS[0]['workbook_name'], EXECS[0]['id']) + + self.assertEqual(EXECS[0]['id'], ex.id) + self.assertEqual(EXECS[0]['workbook_name'], ex.workbook_name) + self.assertEqual(EXECS[0]['target_task'], ex.target_task) + self.assertEqual(EXECS[0]['state'], ex.state) + + def test_delete(self): + self.mock_http_delete(status_code=204) + + # Just make sure it doesn't throw any exceptions. + self.executions.delete(EXECS[0]['workbook_name'], EXECS[0]['id']) diff --git a/mistralclient/tests/test_listeners.py b/mistralclient/tests/test_listeners.py new file mode 100644 index 00000000..af5f0053 --- /dev/null +++ b/mistralclient/tests/test_listeners.py @@ -0,0 +1,88 @@ +# -*- 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 mistralclient.tests import base + +# 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 TestListeners(base.BaseClientTest): + def test_create(self): + self.mock_http_post(json=LISTENERS[0]) + + lsnr = self.listeners.create(LISTENERS[0]['workbook_name'], + LISTENERS[0]['webhook'], + LISTENERS[0]['description']) + + self.assertIsNotNone(lsnr) + self.assertEqual(LISTENERS[0]['id'], lsnr.id) + self.assertEqual(LISTENERS[0]['workbook_name'], lsnr.workbook_name) + self.assertEqual(LISTENERS[0]['webhook'], lsnr.webhook) + self.assertEqual(LISTENERS[0]['description'], lsnr.description) + + def test_update(self): + self.mock_http_put(json=LISTENERS[0]) + + lsnr = self.listeners.update(LISTENERS[0]['workbook_name'], + LISTENERS[0]['webhook'], + LISTENERS[0]['description']) + + self.assertIsNotNone(lsnr) + self.assertEqual(LISTENERS[0]['id'], lsnr.id) + self.assertEqual(LISTENERS[0]['workbook_name'], lsnr.workbook_name) + self.assertEqual(LISTENERS[0]['webhook'], lsnr.webhook) + self.assertEqual(LISTENERS[0]['description'], lsnr.description) + + def test_list(self): + self.mock_http_get(json={'listeners': LISTENERS}) + + listeners = self.listeners.list(LISTENERS[0]['workbook_name']) + + self.assertEqual(1, len(listeners)) + + lsnr = listeners[0] + + self.assertEqual(LISTENERS[0]['id'], lsnr.id) + self.assertEqual(LISTENERS[0]['workbook_name'], lsnr.workbook_name) + self.assertEqual(LISTENERS[0]['webhook'], lsnr.webhook) + self.assertEqual(LISTENERS[0]['description'], lsnr.description) + + def test_get(self): + self.mock_http_get(json=LISTENERS[0]) + + lsnr = self.listeners.get(LISTENERS[0]['workbook_name'], + LISTENERS[0]['id']) + + self.assertEqual(LISTENERS[0]['id'], lsnr.id) + self.assertEqual(LISTENERS[0]['workbook_name'], lsnr.workbook_name) + self.assertEqual(LISTENERS[0]['webhook'], lsnr.webhook) + self.assertEqual(LISTENERS[0]['description'], lsnr.description) + + def test_delete(self): + self.mock_http_delete(status_code=204) + + # Just make sure it doesn't throw any exceptions. + self.listeners.delete(LISTENERS[0]['workbook_name'], + LISTENERS[0]['id']) diff --git a/mistralclient/tests/test_tasks.py b/mistralclient/tests/test_tasks.py new file mode 100644 index 00000000..92ee35b1 --- /dev/null +++ b/mistralclient/tests/test_tasks.py @@ -0,0 +1,84 @@ +# -*- 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 mistralclient.tests import base + +# 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 TestTasks(base.BaseClientTest): + def test_update(self): + self.mock_http_put(json=TASKS[0]) + + task = self.tasks.update(TASKS[0]['workbook_name'], + TASKS[0]['execution_id'], + TASKS[0]['id'], + TASKS[0]['state']) + + self.assertIsNotNone(task) + self.assertEqual(TASKS[0]['id'], task.id) + self.assertEqual(TASKS[0]['workbook_name'], task.workbook_name) + self.assertEqual(TASKS[0]['execution_id'], task.execution_id) + self.assertEqual(TASKS[0]['description'], task.description) + self.assertEqual(TASKS[0]['action'], task.action) + self.assertEqual(TASKS[0]['state'], task.state) + self.assertEqual(TASKS[0]['tags'], task.tags) + + def test_list(self): + self.mock_http_get(json={'tasks': TASKS}) + + tasks = self.tasks.list(TASKS[0]['workbook_name'], + TASKS[0]['execution_id']) + + self.assertEqual(1, len(tasks)) + + task = tasks[0] + + self.assertEqual(TASKS[0]['id'], task.id) + self.assertEqual(TASKS[0]['workbook_name'], task.workbook_name) + self.assertEqual(TASKS[0]['execution_id'], task.execution_id) + self.assertEqual(TASKS[0]['description'], task.description) + self.assertEqual(TASKS[0]['action'], task.action) + self.assertEqual(TASKS[0]['state'], task.state) + self.assertEqual(TASKS[0]['tags'], task.tags) + + def test_get(self): + self.mock_http_get(json=TASKS[0]) + + task = self.tasks.get(TASKS[0]['workbook_name'], + TASKS[0]['execution_id'], + TASKS[0]['id']) + + self.assertEqual(TASKS[0]['id'], task.id) + self.assertEqual(TASKS[0]['workbook_name'], task.workbook_name) + self.assertEqual(TASKS[0]['execution_id'], task.execution_id) + self.assertEqual(TASKS[0]['description'], task.description) + self.assertEqual(TASKS[0]['action'], task.action) + self.assertEqual(TASKS[0]['state'], task.state) + self.assertEqual(TASKS[0]['tags'], task.tags) diff --git a/mistralclient/tests/test_workbooks.py b/mistralclient/tests/test_workbooks.py new file mode 100644 index 00000000..ef7ade9d --- /dev/null +++ b/mistralclient/tests/test_workbooks.py @@ -0,0 +1,121 @@ +# -*- 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 mistralclient.tests import base + +# TODO: later we need additional tests verifying all the errors etc. + +WORKBOOKS = [ + { + 'name': "my_workbook", + 'description': "My cool Mistral workbook", + 'tags': ['deployment', 'demo'] + } +] + +WB_DEF = """ +Service: + name: my_service + type: REST + parameters: + baseUrl: http://my.service.org + actions: + action1: + parameters: + url: servers + method: POST + task-parameters: + param1: + optional: false + param2: + optional: false +Workflow: + tasks: + task1: + action: my_service:create-vm + parameters: + param1: 1234 + param2: 42 +""" + + +class TestWorkbooks(base.BaseClientTest): + def test_create(self): + self.mock_http_post(json=WORKBOOKS[0]) + + wb = self.workbooks.create(WORKBOOKS[0]['name'], + WORKBOOKS[0]['description'], + WORKBOOKS[0]['tags']) + + self.assertIsNotNone(wb) + self.assertEqual(WORKBOOKS[0]['name'], wb.name) + self.assertEqual(WORKBOOKS[0]['description'], wb.description) + self.assertEqual(WORKBOOKS[0]['tags'], wb.tags) + + def test_update(self): + self.mock_http_put(json=WORKBOOKS[0]) + + wb = self.workbooks.update(WORKBOOKS[0]['name'], + WORKBOOKS[0]['description'], + WORKBOOKS[0]['tags']) + + self.assertIsNotNone(wb) + self.assertEqual(WORKBOOKS[0]['name'], wb.name) + self.assertEqual(WORKBOOKS[0]['description'], wb.description) + self.assertEqual(WORKBOOKS[0]['tags'], wb.tags) + + def test_list(self): + self.mock_http_get(json={'workbooks': WORKBOOKS}) + + workbooks = self.workbooks.list() + + self.assertEqual(1, len(workbooks)) + + wb = workbooks[0] + + self.assertEqual(WORKBOOKS[0]['name'], wb.name) + self.assertEqual(WORKBOOKS[0]['description'], wb.description) + self.assertEqual(WORKBOOKS[0]['tags'], wb.tags) + + def test_get(self): + self.mock_http_get(json=WORKBOOKS[0]) + + wb = self.workbooks.get(WORKBOOKS[0]['name']) + + self.assertIsNotNone(wb) + self.assertEqual(WORKBOOKS[0]['name'], wb.name) + self.assertEqual(WORKBOOKS[0]['description'], wb.description) + self.assertEqual(WORKBOOKS[0]['tags'], wb.tags) + + def test_delete(self): + self.mock_http_delete(status_code=204) + + # Just make sure it doesn't throw any exceptions. + self.workbooks.delete(WORKBOOKS[0]['name']) + + def test_upload_definition(self): + self.mock_http_put(None, status_code=200) + + # Just make sure it doesn't throw any exceptions. + self.workbooks.upload_definition("my_workbook", WB_DEF) + + def test_get_definition(self): + self._client.http_client.get = mock.MagicMock(return_value=WB_DEF) + + text = self.workbooks.get_definition("my_workbook") + + self.assertEqual(WB_DEF, text) diff --git a/requirements.txt b/requirements.txt index aa72446e..52f8ca9d 100644 --- a/requirements.txt +++ b/requirements.txt @@ -7,3 +7,4 @@ amqplib>=0.6.1 argparse croniter oslo.config>=1.2.0 +requests \ No newline at end of file