New API v2.0
Introduced single endpoint for services. Old endpoints is still available. Added support for traverse feature for services. Added ability to change any attribute on any service with one call Change-Id: I06ad372ea6f15587d8a4e97d01820b4190172a59
This commit is contained in:
parent
898928e5fc
commit
42b65c2283
@ -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,
|
||||
|
25
muranoapi/api/v1/schemas.py
Normal file
25
muranoapi/api/v1/schemas.py
Normal file
@ -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"]
|
||||
}
|
85
muranoapi/api/v1/services.py
Normal file
85
muranoapi/api/v1/services.py
Normal file
@ -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 <EnvId: {0}>'.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 <EnvId: {0}, '
|
||||
'Body: {1}>'.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 <EnvId: {0}, '
|
||||
'Body: {1}>'.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())
|
@ -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
|
||||
|
69
muranoapi/db/services/core_services.py
Normal file
69
muranoapi/db/services/core_services.py
Normal file
@ -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
|
@ -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)
|
||||
|
@ -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'}])
|
||||
|
38
muranoapi/tests/common/utils_tests.py
Normal file
38
muranoapi/tests/common/utils_tests.py
Normal file
@ -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)
|
@ -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():
|
||||
|
Loading…
x
Reference in New Issue
Block a user