Merge "New API v2.0"
This commit is contained in:
commit
169bf1316c
@ -14,7 +14,7 @@
|
|||||||
|
|
||||||
import routes
|
import routes
|
||||||
from muranoapi.openstack.common import wsgi
|
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 sessions
|
||||||
from muranoapi.api.v1 import active_directories
|
from muranoapi.api.v1 import active_directories
|
||||||
from muranoapi.api.v1 import webservers
|
from muranoapi.api.v1 import webservers
|
||||||
@ -29,6 +29,43 @@ class API(wsgi.Router):
|
|||||||
return cls(routes.Mapper())
|
return cls(routes.Mapper())
|
||||||
|
|
||||||
def __init__(self, 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()
|
environments_resource = environments.create_resource()
|
||||||
mapper.connect('/environments',
|
mapper.connect('/environments',
|
||||||
controller=environments_resource,
|
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.
|
# under the License.
|
||||||
|
|
||||||
import eventlet
|
import eventlet
|
||||||
|
from jsonschema import validate
|
||||||
|
from muranoapi.common.uuidutils import generate_uuid
|
||||||
import types
|
import types
|
||||||
from collections import deque
|
from collections import deque
|
||||||
from functools import wraps
|
from functools import wraps
|
||||||
@ -95,6 +97,17 @@ class TraverseHelper(object):
|
|||||||
node.append(value)
|
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):
|
def retry(ExceptionToCheck, tries=4, delay=3, backoff=2):
|
||||||
"""Retry calling the decorated function using an exponential backoff.
|
"""Retry calling the decorated function using an exponential backoff.
|
||||||
|
|
||||||
@ -151,3 +164,14 @@ def handle(f):
|
|||||||
log.exception(e)
|
log.exception(e)
|
||||||
|
|
||||||
return f_handle
|
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
|
from amqplib.client_0_8 import Message
|
||||||
import anyjson
|
import anyjson
|
||||||
import eventlet
|
import eventlet
|
||||||
|
from jsonschema import validate
|
||||||
|
from muranoapi.api.v1.schemas import ENV_SCHEMA
|
||||||
from muranoapi.common import config
|
from muranoapi.common import config
|
||||||
from muranoapi.db.models import Session, Environment
|
from muranoapi.db.models import Session, Environment
|
||||||
from muranoapi.db.session import get_session
|
from muranoapi.db.session import get_session
|
||||||
@ -174,5 +176,6 @@ class EnvironmentServices(object):
|
|||||||
unit = get_session()
|
unit = get_session()
|
||||||
session = unit.query(Session).get(session_id)
|
session = unit.query(Session).get(session_id)
|
||||||
|
|
||||||
|
validate(environment, ENV_SCHEMA)
|
||||||
session.description = environment
|
session.description = environment
|
||||||
session.save(unit)
|
session.save(unit)
|
||||||
|
@ -11,7 +11,7 @@
|
|||||||
# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
|
# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
|
||||||
# License for the specific language governing permissions and limitations
|
# License for the specific language governing permissions and limitations
|
||||||
# under the License.
|
# under the License.
|
||||||
import unittest
|
import unittest2 as unittest
|
||||||
from muranoapi.common.utils import TraverseHelper
|
from muranoapi.common.utils import TraverseHelper
|
||||||
|
|
||||||
|
|
||||||
@ -19,7 +19,7 @@ class TraverseHelperTests(unittest.TestCase):
|
|||||||
def test_simple_root_get(self):
|
def test_simple_root_get(self):
|
||||||
source = {"attr": True}
|
source = {"attr": True}
|
||||||
value = TraverseHelper.get('/', source)
|
value = TraverseHelper.get('/', source)
|
||||||
self.assertEqual(value, {"attr": True})
|
self.assertDictEqual(value, {"attr": True})
|
||||||
|
|
||||||
def test_simple_attribute_get(self):
|
def test_simple_attribute_get(self):
|
||||||
source = {"attr": True}
|
source = {"attr": True}
|
||||||
@ -61,10 +61,31 @@ class TraverseHelperTests(unittest.TestCase):
|
|||||||
source = {"attr": [1, 2, 3]}
|
source = {"attr": [1, 2, 3]}
|
||||||
TraverseHelper.insert('/attr', 4, source)
|
TraverseHelper.insert('/attr', 4, source)
|
||||||
value = TraverseHelper.get('/attr', 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):
|
def test_adding_item_to_list(self):
|
||||||
source = {"obj": {"attr": [1, 2, 3]}}
|
source = {"obj": {"attr": [1, 2, 3]}}
|
||||||
TraverseHelper.insert('/obj/attr', 4, source)
|
TraverseHelper.insert('/obj/attr', 4, source)
|
||||||
value = TraverseHelper.get('/obj/attr', 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
|
import muranoapi.api.v1.router as router
|
||||||
|
|
||||||
|
|
||||||
def my_mock(link, controller, action, conditions):
|
def my_mock(link, controller, action, conditions, path=''):
|
||||||
return [link, controller, action, conditions]
|
return [link, controller, action, conditions, path]
|
||||||
|
|
||||||
|
|
||||||
def func_mock():
|
def func_mock():
|
||||||
|
Loading…
Reference in New Issue
Block a user