diff --git a/muranoapi/api/v1/router.py b/muranoapi/api/v1/router.py index 017102f9..a981a892 100644 --- a/muranoapi/api/v1/router.py +++ b/muranoapi/api/v1/router.py @@ -14,7 +14,7 @@ import routes from muranoapi.openstack.common import wsgi -from muranoapi.api.v1 import environments +from muranoapi.api.v1 import environments, services from muranoapi.api.v1 import sessions from muranoapi.api.v1 import active_directories from muranoapi.api.v1 import webservers @@ -29,6 +29,43 @@ class API(wsgi.Router): return cls(routes.Mapper()) def __init__(self, mapper): + services_resource = services.create_resource() + mapper.connect('/environments/{environment_id}/services', + controller=services_resource, + action='get', + conditions={'method': ['GET']}, path='') + mapper.connect('/environments/{environment_id}/services/{path:.*?}', + controller=services_resource, + action='get', + conditions={'method': ['GET']}, path='') + + mapper.connect('/environments/{environment_id}/services', + controller=services_resource, + action='post', + conditions={'method': ['POST']}, path='') + mapper.connect('/environments/{environment_id}/services/{path:.*?}', + controller=services_resource, + action='post', + conditions={'method': ['POST']}, path='') + + mapper.connect('/environments/{environment_id}/services', + controller=services_resource, + action='put', + conditions={'method': ['PUT']}, path='') + mapper.connect('/environments/{environment_id}/services/{path:.*?}', + controller=services_resource, + action='put', + conditions={'method': ['PUT']}, path='') + + mapper.connect('/environments/{environment_id}/services', + controller=services_resource, + action='delete', + conditions={'method': ['DELETE']}, path='') + mapper.connect('/environments/{environment_id}/services/{path:.*?}', + controller=services_resource, + action='delete', + conditions={'method': ['DELETE']}, path='') + environments_resource = environments.create_resource() mapper.connect('/environments', controller=environments_resource, diff --git a/muranoapi/api/v1/schemas.py b/muranoapi/api/v1/schemas.py new file mode 100644 index 00000000..55999437 --- /dev/null +++ b/muranoapi/api/v1/schemas.py @@ -0,0 +1,25 @@ +# Copyright (c) 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:write detailed schema +ENV_SCHEMA = { + "$schema": "http://json-schema.org/draft-04/schema#", + + "type": "object", + "properties": { + "id": {"type": "string"}, + "name": {"type": "string"} + }, + "required": ["id"] +} diff --git a/muranoapi/api/v1/services.py b/muranoapi/api/v1/services.py new file mode 100644 index 00000000..65b96a7d --- /dev/null +++ b/muranoapi/api/v1/services.py @@ -0,0 +1,85 @@ +# Copyright (c) 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 functools import wraps + +from webob.exc import HTTPNotFound + +from muranoapi import utils +from muranoapi.db.services.core_services import CoreServices +from muranoapi.openstack.common import wsgi +from muranoapi.openstack.common import log as logging + +log = logging.getLogger(__name__) + + +def normalize_path(f): + @wraps(f) + def f_normalize_path(*args, **kwargs): + if 'path' in kwargs: + if kwargs['path']: + kwargs['path'] = '/services/' + kwargs['path'] + else: + kwargs['path'] = '/services' + return f(*args, **kwargs) + + return f_normalize_path + + +class Controller(object): + @normalize_path + def get(self, request, environment_id, path): + log.debug(_('Services:Get '.format(environment_id))) + + session_id = None + if hasattr(request, 'context') and request.context.session: + session_id = request.context.session + + try: + result = CoreServices.get_data(environment_id, path, session_id) + except (KeyError, ValueError): + raise HTTPNotFound + return result + + @utils.verify_session + @normalize_path + def post(self, request, environment_id, path, body): + log.debug(_('Services:Post '.format(environment_id, body))) + + post_data = CoreServices.post_data + session_id = request.context.session + try: + result = post_data(environment_id, session_id, body, path) + except (KeyError, ValueError): + raise HTTPNotFound + return result + + @utils.verify_session + @normalize_path + def put(self, request, environment_id, path, body): + log.debug(_('Services:Put '.format(environment_id, body))) + + put_data = CoreServices.put_data + session_id = request.context.session + + try: + result = put_data(environment_id, session_id, body, path) + except (KeyError, ValueError): + raise HTTPNotFound + return result + + +def create_resource(): + return wsgi.Resource(Controller()) diff --git a/muranoapi/common/utils.py b/muranoapi/common/utils.py index 1923d71f..bf3be030 100644 --- a/muranoapi/common/utils.py +++ b/muranoapi/common/utils.py @@ -13,6 +13,8 @@ # under the License. import eventlet +from jsonschema import validate +from muranoapi.common.uuidutils import generate_uuid import types from collections import deque from functools import wraps @@ -95,6 +97,17 @@ class TraverseHelper(object): node.append(value) +def auto_id(value): + if isinstance(value, types.DictionaryType): + value['id'] = generate_uuid() + for k, v in value.iteritems(): + value[k] = auto_id(v) + if isinstance(value, types.ListType): + for item in value: + auto_id(item) + return value + + def retry(ExceptionToCheck, tries=4, delay=3, backoff=2): """Retry calling the decorated function using an exponential backoff. @@ -151,3 +164,14 @@ def handle(f): log.exception(e) return f_handle + + +def validate_body(schema): + def deco_validate_body(f): + @wraps(f) + def f_validate_body(*args, **kwargs): + if 'body' in kwargs: + validate(kwargs['body'], schema) + return f(*args, **kwargs) + return f_validate_body + return deco_validate_body diff --git a/muranoapi/db/services/core_services.py b/muranoapi/db/services/core_services.py new file mode 100644 index 00000000..7874eccd --- /dev/null +++ b/muranoapi/db/services/core_services.py @@ -0,0 +1,69 @@ +# Copyright (c) 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 muranoapi.common.utils import TraverseHelper, auto_id +from muranoapi.db.services.environments import EnvironmentServices +from muranoapi.openstack.common import timeutils + + +class CoreServices(object): + @staticmethod + def get_data(environment_id, path, session_id=None): + get_description = EnvironmentServices.get_environment_description + + env_description = get_description(environment_id, session_id) + + if not 'services' in env_description: + return [] + + result = TraverseHelper.get(path, env_description) + return result if result else [] + + @staticmethod + def post_data(environment_id, session_id, data, path): + get_description = EnvironmentServices.get_environment_description + save_description = EnvironmentServices.save_environment_description + + env_description = get_description(environment_id, session_id) + if not 'services' in env_description: + env_description['services'] = [] + + data = auto_id(data) + + if path == '/services': + data['created'] = str(timeutils.utcnow()) + data['updated'] = str(timeutils.utcnow()) + + for idx, unit in enumerate(data['units']): + unit['name'] = data['name'] + '_instance_' + str(idx) + + TraverseHelper.insert(path, data, env_description) + save_description(session_id, env_description) + + return data + + @staticmethod + def put_data(environment_id, session_id, data, path): + get_description = EnvironmentServices.get_environment_description + save_description = EnvironmentServices.save_environment_description + + env_description = get_description(environment_id, session_id) + + TraverseHelper.update(path, data, env_description) + if path == '/services': + data['updated'] = str(timeutils.utcnow()) + + save_description(session_id, env_description) + + return data diff --git a/muranoapi/db/services/environments.py b/muranoapi/db/services/environments.py index eadece48..66efcff3 100644 --- a/muranoapi/db/services/environments.py +++ b/muranoapi/db/services/environments.py @@ -16,6 +16,8 @@ from collections import namedtuple from amqplib.client_0_8 import Message import anyjson import eventlet +from jsonschema import validate +from muranoapi.api.v1.schemas import ENV_SCHEMA from muranoapi.common import config from muranoapi.db.models import Session, Environment from muranoapi.db.session import get_session @@ -174,5 +176,6 @@ class EnvironmentServices(object): unit = get_session() session = unit.query(Session).get(session_id) + validate(environment, ENV_SCHEMA) session.description = environment session.save(unit) diff --git a/muranoapi/tests/common/traverse_helper_tests.py b/muranoapi/tests/common/traverse_helper_tests.py index f4c25a16..7cc06eeb 100644 --- a/muranoapi/tests/common/traverse_helper_tests.py +++ b/muranoapi/tests/common/traverse_helper_tests.py @@ -11,7 +11,7 @@ # 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 unittest +import unittest2 as unittest from muranoapi.common.utils import TraverseHelper @@ -19,7 +19,7 @@ class TraverseHelperTests(unittest.TestCase): def test_simple_root_get(self): source = {"attr": True} value = TraverseHelper.get('/', source) - self.assertEqual(value, {"attr": True}) + self.assertDictEqual(value, {"attr": True}) def test_simple_attribute_get(self): source = {"attr": True} @@ -61,10 +61,31 @@ class TraverseHelperTests(unittest.TestCase): source = {"attr": [1, 2, 3]} TraverseHelper.insert('/attr', 4, source) value = TraverseHelper.get('/attr', source) - self.assertEqual(value, [1, 2, 3, 4]) + self.assertListEqual(value, [1, 2, 3, 4]) def test_adding_item_to_list(self): source = {"obj": {"attr": [1, 2, 3]}} TraverseHelper.insert('/obj/attr', 4, source) value = TraverseHelper.get('/obj/attr', source) - self.assertEqual(value, [1, 2, 3, 4]) + self.assertListEqual(value, [1, 2, 3, 4]) + + @unittest.skip + def test_simple_attribute_remove(self): + source = {"attr1": False, "attr2": True} + TraverseHelper.remove('/attr1', source) + value = TraverseHelper.get('/', source) + self.assertEqual(value, {"attr2": True}) + + @unittest.skip + def test_nested_attribute_remove_from_object(self): + source = {"obj": {"attr1": False, "attr2": True}} + TraverseHelper.remove('/obj/attr1', source) + value = TraverseHelper.get('/obj', source) + self.assertDictEqual(value, {"attr2": True}) + + @unittest.skip + def test_nested_attribute_remove_from_list(self): + source = {"obj": [{"id": 'id1'}, {"id": 'id2'}]} + TraverseHelper.remove('/obj/id1', source) + value = TraverseHelper.get('/', source) + self.assertListEqual(value, [{"id": 'id2'}]) diff --git a/muranoapi/tests/common/utils_tests.py b/muranoapi/tests/common/utils_tests.py new file mode 100644 index 00000000..c99e19c5 --- /dev/null +++ b/muranoapi/tests/common/utils_tests.py @@ -0,0 +1,38 @@ +# Copyright (c) 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 as unittest +from muranoapi.common.utils import auto_id + + +class AutoIdTests(unittest.TestCase): + def test_simple_dict(self): + source = {"attr": True} + value = auto_id(source) + self.assertIn('id', value) + + def test_nested_lists(self): + source = {"attr": True, "obj": {"attr": False}} + value = auto_id(source) + self.assertIn('id', value) + + def test_list_with_ints(self): + source = [0, 1, 2, 3] + value = auto_id(source) + self.assertListEqual(value, source) + + def test_list_with_dicts(self): + source = [{"attr": True}, {"attr": False}] + value = auto_id(source) + for item in value: + self.assertIn('id', item) diff --git a/muranoapi/tests/sanity_tests.py b/muranoapi/tests/sanity_tests.py index 16ca3614..8e8cc4c8 100644 --- a/muranoapi/tests/sanity_tests.py +++ b/muranoapi/tests/sanity_tests.py @@ -18,8 +18,8 @@ from mock import MagicMock import muranoapi.api.v1.router as router -def my_mock(link, controller, action, conditions): - return [link, controller, action, conditions] +def my_mock(link, controller, action, conditions, path=''): + return [link, controller, action, conditions, path] def func_mock():