From e55e88fcaefa6691697c9ef6a718eeba45de0112 Mon Sep 17 00:00:00 2001 From: Dongfeng Huang Date: Sun, 25 Sep 2016 16:30:02 +0800 Subject: [PATCH] Implement resource routing features MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 1. What is the problem? In order to dispatch resource operation request to the proper bottom instance, we need a resource routing table, which maps a resource to the bottom instance which it’s located at. However, the resource routing APIs have not been implemented, so this patch carries out these features and performs some tests for them at the same time. 2. What is the solution to the problem? To implement the resource routing features, a new file named routing.py is created in the directory tricircle/api/controllers, then regular post and get functions will be implemented there. As for the unit test, a file named test_routing.py is added to the directory tricircle/tests/unit/api/controllers, conducting relevant tests for the features in file routing.py. 3. What the features need to be implemented to the Tricircle to realize the solution? Some main features like post, update, get and delete functions are implemented in the file routing.py, then relative test method for each of them is realized in the file test_routing.py. At last, these features are added to the search path in the file root.py for REST API call. Change-Id: I2c1073b8c8bd17eeb395afaa5ab7ea8d63609222 --- tricircle/api/controllers/root.py | 4 +- tricircle/api/controllers/routing.py | 247 +++++++++ tricircle/common/constants.py | 7 + tricircle/common/policy.py | 22 + tricircle/common/utils.py | 4 + tricircle/db/api.py | 28 + .../unit/api/controllers/test_routing.py | 477 ++++++++++++++++++ 7 files changed, 788 insertions(+), 1 deletion(-) create mode 100644 tricircle/api/controllers/routing.py create mode 100644 tricircle/tests/unit/api/controllers/test_routing.py diff --git a/tricircle/api/controllers/root.py b/tricircle/api/controllers/root.py index 9c81d844..e2cce1f5 100644 --- a/tricircle/api/controllers/root.py +++ b/tricircle/api/controllers/root.py @@ -19,6 +19,7 @@ import pecan from pecan import request from tricircle.api.controllers import pod +from tricircle.api.controllers import routing import tricircle.common.context as t_context @@ -77,7 +78,8 @@ class V1Controller(object): self.sub_controllers = { "pods": pod.PodsController(), - "bindings": pod.BindingsController() + "bindings": pod.BindingsController(), + "routings": routing.RoutingController() } for name, ctrl in self.sub_controllers.items(): diff --git a/tricircle/api/controllers/routing.py b/tricircle/api/controllers/routing.py new file mode 100644 index 00000000..f73601a9 --- /dev/null +++ b/tricircle/api/controllers/routing.py @@ -0,0 +1,247 @@ +# 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 pecan +from pecan import expose +from pecan import rest + +from oslo_log import log as logging + +from tricircle.common import constants +import tricircle.common.context as t_context +import tricircle.common.exceptions as t_exc +from tricircle.common.i18n import _ +from tricircle.common.i18n import _LE +from tricircle.common import policy +from tricircle.common import utils + +from tricircle.db import api as db_api + +LOG = logging.getLogger(__name__) + + +SUPPORTED_FILTERS = ['id', 'top_id', 'bottom_id', 'pod_id', 'project_id', + 'resource_type', 'created_at', 'updated_at'] + + +class RoutingController(rest.RestController): + + def __init__(self): + pass + + @expose(generic=True, template='json') + def post(self, **kw): + context = t_context.extract_context_from_environ() + + if not policy.enforce(context, policy.ADMIN_API_ROUTINGS_CREATE): + return utils.format_api_error( + 403, _("Unauthorized to create resource routing")) + + if 'routing' not in kw: + return utils.format_api_error( + 400, _("Request body not found")) + + routing = kw['routing'] + + for field in ('top_id', 'bottom_id', 'pod_id', + 'project_id', 'resource_type'): + value = routing.get(field) + if value is None or len(value.strip()) == 0: + return utils.format_api_error( + 400, _("Field %(field)s can not be empty") % { + 'field': field}) + + # verify the integrity: the pod_id and the project_id should be bound + pod_id = routing.get('pod_id').strip() + project_id = routing.get('project_id').strip() + bindings = db_api.list_pod_bindings(context, + [{'key': 'pod_id', + 'comparator': 'eq', + 'value': pod_id + }, + {'key': 'tenant_id', + 'comparator': 'eq', + 'value': project_id} + ], []) + if len(bindings) == 0: + return utils.format_api_error( + 400, _('The pod_id and project_id have not been bound')) + + # the resource type should be properly provisioned. + resource_type = routing.get('resource_type').strip() + if not constants.is_valid_resource_type(resource_type): + return utils.format_api_error( + 400, _('There is no such resource type')) + + try: + top_id = routing.get('top_id').strip() + bottom_id = routing.get('bottom_id').strip() + + routing = db_api.create_resource_mapping(context, top_id, + bottom_id, pod_id, + project_id, + resource_type) + if not routing: + return utils.format_api_error( + 409, _('Resource routing already exists')) + except Exception as e: + LOG.exception(_LE('Failed to create resource routing: ' + '%(exception)s '), {'exception': e}) + return utils.format_api_error( + 500, _('Failed to create resource routing')) + + return {'routing': routing} + + def _get_filters(self, params): + """Return a dictionary of query param filters from the request. + + :param params: the URI params coming from the wsgi layer + :return a dict of key/value filters + """ + filters = {} + for param in params: + if param in SUPPORTED_FILTERS: + # map filter name + filter_name = param + filters[filter_name] = params.get(param) + + return filters + + @expose(generic=True, template='json') + def get_all(self, **kwargs): + context = t_context.extract_context_from_environ() + + if not policy.enforce(context, policy.ADMIN_API_ROUTINGS_LIST): + return utils.format_api_error( + 403, _('Unauthorized to to show all resource routings')) + + filters = self._get_filters(kwargs) + filters = [{'key': key, + 'comparator': 'eq', + 'value': value} for key, value in filters.iteritems()] + + try: + return {'routings': db_api.list_resource_routings(context, + filters)} + except Exception as e: + LOG.exception(_LE('Failed to show all resource routings: ' + '%(exception)s '), {'exception': e}) + return utils.format_api_error( + 500, _('Failed to show all resource routings')) + + @expose(generic=True, template='json') + def get_one(self, _id): + context = t_context.extract_context_from_environ() + + if not policy.enforce(context, policy.ADMIN_API_ROUTINGS_SHOW): + return utils.format_api_error( + 403, _('Unauthorized to show the resource routing')) + + try: + return {'routing': db_api.get_resource_routing(context, _id)} + except t_exc.ResourceNotFound: + return utils.format_api_error( + 404, _('Resource routing not found')) + + @expose(generic=True, template='json') + def delete(self, _id): + context = t_context.extract_context_from_environ() + + if not policy.enforce(context, policy.ADMIN_API_ROUTINGS_DELETE): + return utils.format_api_error( + 403, _('Unauthorized to delete the resource routing')) + + try: + db_api.get_resource_routing(context, _id) + except t_exc.ResourceNotFound: + return utils.format_api_error(404, + _('Resource routing not found')) + try: + db_api.delete_resource_routing(context, _id) + pecan.response.status = 200 + return pecan.response + except Exception as e: + LOG.exception(_LE('Failed to delete the resource routing: ' + '%(exception)s '), {'exception': e}) + return utils.format_api_error( + 500, _('Failed to delete the resource routing')) + + @expose(generic=True, template='json') + def put(self, _id, **kw): + context = t_context.extract_context_from_environ() + + if not policy.enforce(context, policy.ADMIN_API_ROUTINGS_PUT): + return utils.format_api_error( + 403, _('Unauthorized to update resource routing')) + + try: + routing = db_api.get_resource_routing(context, _id) + except t_exc.ResourceNotFound: + return utils.format_api_error(404, + _('Resource routing not found')) + + if 'routing' not in kw: + return utils.format_api_error( + 400, _('Request body not found')) + + update_dict = kw['routing'] + + # values to be updated should not be empty + for field in update_dict: + value = update_dict.get(field) + if value is None or len(value.strip()) == 0: + return utils.format_api_error( + 400, _("Field %(field)s can not be empty") % { + 'field': field}) + + # the resource type should be properly provisioned. + if 'resource_type' in update_dict: + if not constants.is_valid_resource_type( + update_dict['resource_type']): + return utils.format_api_error( + 400, _('There is no such resource type')) + + # verify the integrity: the pod_id and project_id should be bound + if 'pod_id' in update_dict or 'project_id' in update_dict: + if 'pod_id' in update_dict: + pod_id = update_dict['pod_id'] + else: + pod_id = routing['pod_id'] + + if 'project_id' in update_dict: + project_id = update_dict['project_id'] + else: + project_id = routing['project_id'] + + bindings = db_api.list_pod_bindings(context, + [{'key': 'pod_id', + 'comparator': 'eq', + 'value': pod_id + }, + {'key': 'tenant_id', + 'comparator': 'eq', + 'value': project_id} + ], []) + if len(bindings) == 0: + return utils.format_api_error( + 400, _('The pod_id and project_id have not been ' + 'bound')) + + try: + routing_updated = db_api.update_resource_routing( + context, _id, update_dict) + return {'routing': routing_updated} + except Exception as e: + LOG.exception(_LE('Failed to update resource routing: ' + '%(exception)s '), {'exception': e}) + return utils.format_api_error( + 500, _('Failed to update resource routing')) diff --git a/tricircle/common/constants.py b/tricircle/common/constants.py index 267e07c0..90dbf20b 100644 --- a/tricircle/common/constants.py +++ b/tricircle/common/constants.py @@ -35,6 +35,13 @@ RT_PORT = 'port' RT_ROUTER = 'router' RT_SG = 'security_group' + +# check whether the resource type is properly provisioned. +def is_valid_resource_type(resource_type): + resource_type_table = [RT_NETWORK, RT_SUBNET, RT_PORT, RT_ROUTER, RT_SG] + return resource_type in resource_type_table + + # version list NOVA_VERSION_V21 = 'v2.1' CINDER_VERSION_V2 = 'v2' diff --git a/tricircle/common/policy.py b/tricircle/common/policy.py index b4ec0cdc..77b764f6 100644 --- a/tricircle/common/policy.py +++ b/tricircle/common/policy.py @@ -57,6 +57,12 @@ ADMIN_API_BINDINGS_DELETE = 'admin_api:bindings:delete' ADMIN_API_BINDINGS_SHOW = 'admin_api:bindings:show' ADMIN_API_BINDINGS_LIST = 'admin_api:bindings:list' +ADMIN_API_ROUTINGS_CREATE = 'admin_api:routings:create' +ADMIN_API_ROUTINGS_DELETE = 'admin_api:routings:delete' +ADMIN_API_ROUTINGS_PUT = 'admin_api:routings:put' +ADMIN_API_ROUTINGS_SHOW = 'admin_api:routings:show' +ADMIN_API_ROUTINGS_LIST = 'admin_api:routings:list' + tricircle_admin_api_policies = [ policy.RuleDefault(ADMIN_API_PODS_CREATE, 'rule:admin_api', @@ -83,6 +89,22 @@ tricircle_admin_api_policies = [ policy.RuleDefault(ADMIN_API_BINDINGS_LIST, 'rule:admin_api', description='List pod bindings'), + + policy.RuleDefault(ADMIN_API_ROUTINGS_CREATE, + 'rule:admin_api', + description='Create resource routing'), + policy.RuleDefault(ADMIN_API_ROUTINGS_DELETE, + 'rule:admin_api', + description='Delete resource routing'), + policy.RuleDefault(ADMIN_API_ROUTINGS_PUT, + 'rule:admin_api', + description='Update resource routing'), + policy.RuleDefault(ADMIN_API_ROUTINGS_SHOW, + 'rule:admin_api', + description='Show resource routing detail'), + policy.RuleDefault(ADMIN_API_ROUTINGS_LIST, + 'rule:admin_api', + description='List resource routings'), ] diff --git a/tricircle/common/utils.py b/tricircle/common/utils.py index ca310500..7a0277dd 100644 --- a/tricircle/common/utils.py +++ b/tricircle/common/utils.py @@ -159,6 +159,10 @@ def format_error(code, message, error_type=None): return {error_type: {'message': message, 'code': code}} +def format_api_error(code, message, error_type=None): + return format_error(code, message, error_type) + + def format_nova_error(code, message, error_type=None): return format_error(code, message, error_type) diff --git a/tricircle/db/api.py b/tricircle/db/api.py index 22772bc6..5628312f 100644 --- a/tricircle/db/api.py +++ b/tricircle/db/api.py @@ -102,6 +102,12 @@ def create_pod_binding(context, tenant_id, pod_id): 'is_binding': True}) +def list_pod_bindings(context, filters=None, sorts=None): + with context.session.begin(): + return core.query_resource(context, models.PodBinding, + filters or [], sorts or []) + + def delete_pod_service_configuration(context, config_id): with context.session.begin(): return core.delete_resource(context, models.PodServiceConfiguration, @@ -145,6 +151,28 @@ def create_resource_mapping(context, top_id, bottom_id, pod_id, project_id, context.session.close() +def list_resource_routings(context, filters=None, sorts=None): + with context.session.begin(): + return core.query_resource(context, models.ResourceRouting, + filters or [], sorts or []) + + +def get_resource_routing(context, id): + with context.session.begin(): + return core.get_resource(context, models.ResourceRouting, id) + + +def delete_resource_routing(context, id): + with context.session.begin(): + return core.delete_resource(context, models.ResourceRouting, id) + + +def update_resource_routing(context, id, update_dict): + with context.session.begin(): + return core.update_resource(context, models.ResourceRouting, id, + update_dict) + + def get_bottom_mappings_by_top_id(context, top_id, resource_type): """Get resource id and pod name on bottom diff --git a/tricircle/tests/unit/api/controllers/test_routing.py b/tricircle/tests/unit/api/controllers/test_routing.py new file mode 100644 index 00000000..fa6b1944 --- /dev/null +++ b/tricircle/tests/unit/api/controllers/test_routing.py @@ -0,0 +1,477 @@ +# 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 mock import patch +import unittest + +import pecan + +from tricircle.api.controllers import pod +from tricircle.api.controllers import routing +from tricircle.common import context +from tricircle.common import policy +from tricircle.db import api as db_api +from tricircle.db import core + +from oslo_log import log as logging + +LOG = logging.getLogger(__name__) + + +class FakeResponse(object): + def __new__(cls, code=500): + cls.status = code + cls.status_code = code + return super(FakeResponse, cls).__new__(cls) + + +class RoutingControllerTest(unittest.TestCase): + def setUp(self): + core.initialize() + core.ModelBase.metadata.create_all(core.get_engine()) + self.controller = routing.RoutingController() + self.context = context.get_admin_context() + policy.populate_default_rules() + + def _validate_error_code(self, res, code): + self.assertEqual(code, res[res.keys()[0]]['code']) + + @patch.object(pecan, 'response', new=FakeResponse) + @patch.object(context, 'extract_context_from_environ') + def test_post(self, mock_context): + mock_context.return_value = self.context + + # prepare the foreign key: pod_id, project_id + kw_pod = {'pod': {'pod_name': 'pod1', 'az_name': 'az1'}} + pod_id = pod.PodsController().post(**kw_pod)['pod']['pod_id'] + + kw_binding = {'pod_binding': {'tenant_id': '01', 'pod_id': pod_id}} + project_id = pod.BindingsController().post(**kw_binding)[ + 'pod_binding']['tenant_id'] + + kw_routing = {'routing': + {'top_id': '09fd7cc9-d169-4b5a-88e8-436ecf4d0bfe', + 'bottom_id': 'dc80f9de-abb7-4ec6-ab7a-94f8fd1e20ef', + 'pod_id': pod_id, + 'project_id': project_id, + 'resource_type': 'subnet' + }} + id = self.controller.post(**kw_routing)['routing']['id'] + routing = db_api.get_resource_routing(self.context, id) + self.assertEqual(routing['top_id'], + '09fd7cc9-d169-4b5a-88e8-436ecf4d0bfe') + self.assertEqual(routing['bottom_id'], + 'dc80f9de-abb7-4ec6-ab7a-94f8fd1e20ef') + self.assertEqual(routing['pod_id'], pod_id) + self.assertEqual(routing['project_id'], project_id) + self.assertEqual(routing['resource_type'], 'subnet') + + routings = db_api.list_resource_routings(self.context, + [{'key': 'top_id', + 'comparator': 'eq', + 'value': + '09fd7cc9-d169-4b5a-' + '88e8-436ecf4d0bfe' + }, + {'key': 'pod_id', + 'comparator': 'eq', + 'value': pod_id} + ], []) + self.assertEqual(len(routings), 1) + + # failure case, only admin can create resource routing + self.context.is_admin = False + res = self.controller.post(**kw_routing) + self._validate_error_code(res, 403) + + self.context.is_admin = True + + # failure case, request body not found + kw_routing1 = {'route': + {'top_id': '109fd7cc9-d169-4b5a-88e8-436ecf4d0bfe', + 'bottom_id': '2dc80f9de-abb7-4ec6-ab7a-94f8fd1e20ef', + 'pod_id': pod_id, + 'project_id': project_id, + 'resource_type': 'subnet' + }} + res = self.controller.post(**kw_routing1) + self._validate_error_code(res, 400) + + # failure case, top_id is not given + kw_routing2 = {'routing': + {'bottom_id': '2dc80f9de-abb7-4ec6-ab7a-94f8fd1e20ef', + 'pod_id': pod_id, + 'project_id': project_id, + 'resource_type': 'subnet' + }} + res = self.controller.post(**kw_routing2) + self._validate_error_code(res, 400) + + # failure case, top_id is empty + kw_routing3 = {'routing': + {'top_id': '', + 'bottom_id': '2dc80f9de-abb7-4ec6-ab7a-94f8fd1e20ef', + 'pod_id': pod_id, + 'project_id': project_id, + 'resource_type': 'subnet' + }} + res = self.controller.post(**kw_routing3) + self._validate_error_code(res, 400) + + # failure case, top_id is given value 'None' + kw_routing4 = {'routing': + {'top_id': None, + 'bottom_id': '2dc80f9de-abb7-4ec6-ab7a-94f8fd1e20ef', + 'pod_id': pod_id, + 'project_id': project_id, + 'resource_type': 'subnet' + }} + res = self.controller.post(**kw_routing4) + self._validate_error_code(res, 400) + + # failure case, the pod_id and the project_id should be bound + kw_pod2 = {'pod': {'pod_name': 'pod2', 'az_name': 'az1'}} + pod_id2 = pod.PodsController().post(**kw_pod2)['pod']['pod_id'] + + # the tenant_id binds with pod_id rather than pod_id2 + kw_binding2 = {'pod_binding': {'tenant_id': '02', 'pod_id': pod_id}} + project_id2 = pod.BindingsController().post(**kw_binding2)[ + 'pod_binding']['tenant_id'] + + kw_routing5 = {'routing': + {'top_id': '09fd7cc9-d169-4b5a-88e8-436ecf4d0bfe', + 'bottom_id': 'dc80f9de-abb7-4ec6-ab7a-94f8fd1e20ef', + 'pod_id': pod_id2, + 'project_id': project_id2, + 'resource_type': 'subnet' + }} + res = self.controller.post(**kw_routing5) + self._validate_error_code(res, 400) + + # failure case, wrong resource type + kw_routing6 = {'routing': + {'top_id': '09fd7cc9-d169-4b5a-88e8-436ecf4d0b09', + 'bottom_id': 'dc80f9de-abb7-4ec6-ab7a-94f8fd1e2031f', + 'pod_id': pod_id, + 'project_id': project_id, + 'resource_type': 'server' + }} + res = self.controller.post(**kw_routing6) + self._validate_error_code(res, 400) + + # failure case, the resource routing already exists + res = self.controller.post(**kw_routing) + self._validate_error_code(res, 409) + + @patch.object(pecan, 'response', new=FakeResponse) + @patch.object(context, 'extract_context_from_environ') + def test_get_one(self, mock_context): + mock_context.return_value = self.context + + # prepare the foreign key: pod_id, project_id + kw_pod = {'pod': {'pod_name': 'pod1', 'az_name': 'az1'}} + pod_id = pod.PodsController().post(**kw_pod)['pod']['pod_id'] + + kw_binding = {'pod_binding': {'tenant_id': '01', 'pod_id': pod_id}} + project_id = pod.BindingsController().post(**kw_binding)[ + 'pod_binding']['tenant_id'] + + kw_routing = {'routing': + {'top_id': '09fd7cc9-d169-4b5a-88e8-436ecf4d0bfe', + 'bottom_id': 'dc80f9de-abb7-4ec6-ab7a-94f8fd1e20ef', + 'pod_id': pod_id, + 'project_id': project_id, + 'resource_type': 'port' + }} + + id = self.controller.post(**kw_routing)['routing']['id'] + + routing = self.controller.get_one(id) + self.assertEqual(routing['routing']['top_id'], + '09fd7cc9-d169-4b5a-88e8-436ecf4d0bfe') + self.assertEqual(routing['routing']['bottom_id'], + 'dc80f9de-abb7-4ec6-ab7a-94f8fd1e20ef') + self.assertEqual(routing['routing']['pod_id'], pod_id) + self.assertEqual(routing['routing']['project_id'], + project_id) + self.assertEqual(routing['routing']['resource_type'], + 'port') + + # failure case, only admin can get resource routing + self.context.is_admin = False + res = self.controller.get_one(id) + self._validate_error_code(res, 403) + + self.context.is_admin = True + + # failure case, resource routing not found + res = self.controller.get_one(-123) + self._validate_error_code(res, 404) + + @patch.object(pecan, 'response', new=FakeResponse) + @patch.object(context, 'extract_context_from_environ') + def test_get_all(self, mock_context): + mock_context.return_value = self.context + + # prepare the foreign key: pod_id, project_id + kw_pod1 = {'pod': {'pod_name': 'pod1', 'az_name': 'az1'}} + pod_id1 = pod.PodsController().post(**kw_pod1)['pod']['pod_id'] + + kw_binding1 = {'pod_binding': {'tenant_id': '01', 'pod_id': pod_id1}} + project_id1 = pod.BindingsController().post(**kw_binding1)[ + 'pod_binding']['tenant_id'] + + kw_routing1 = {'routing': + {'top_id': 'c7f641c9-8462-4007-84b2-3035d8cfb7a3', + 'bottom_id': 'dc80f9de-abb7-4ec6-ab7a-94f8fd1e20ef', + 'pod_id': pod_id1, + 'project_id': project_id1, + 'resource_type': 'subnet' + }} + + # prepare the foreign key: pod_id, project_id + kw_pod2 = {'pod': {'pod_name': 'pod2', 'az_name': 'az1'}} + pod_id2 = pod.PodsController().post(**kw_pod2)['pod']['pod_id'] + + kw_binding2 = {'pod_binding': {'tenant_id': '02', 'pod_id': pod_id2}} + project_id2 = pod.BindingsController().post(**kw_binding2)[ + 'pod_binding']['tenant_id'] + + kw_routing2 = {'routing': + {'top_id': 'b669a2da-ca95-47db-a2a9-ba9e546d82ee', + 'bottom_id': 'fd72c010-6e62-4866-b999-6dcb718dd7b4', + 'pod_id': pod_id2, + 'project_id': project_id2, + 'resource_type': 'port' + }} + + self.controller.post(**kw_routing1) + self.controller.post(**kw_routing2) + + # no filters are applied to the routings, so all of the routings will + # be retrieved. + routings = self.controller.get_all() + actual = [(routing['top_id'], routing['pod_id']) + for routing in routings['routings']] + expect = [('c7f641c9-8462-4007-84b2-3035d8cfb7a3', pod_id1), + ('b669a2da-ca95-47db-a2a9-ba9e546d82ee', pod_id2)] + self.assertItemsEqual(expect, actual) + + # apply a resource type filter to the retrieved routings. + kw_filter1 = {'resource_type': 'port'} + routings = self.controller.get_all(**kw_filter1) + actual = [(routing['top_id'], routing['pod_id'], + routing['resource_type']) + for routing in routings['routings']] + expect = [('b669a2da-ca95-47db-a2a9-ba9e546d82ee', pod_id2, 'port')] + self.assertItemsEqual(expect, actual) + + # apply a filter and if it doesn't match with any of the retrieved + # routings, then all of them will be discarded and the method returns + # with [] + kw_filter2 = {'resource_type': 'port2'} + routings = self.controller.get_all(**kw_filter2) + self.assertEqual(routings['routings'], []) + + # apply an illegal filter and it won't take effect + kw_filter3 = {'resource': 'port'} + routings = self.controller.get_all(**kw_filter3) + actual = [(routing['top_id'], routing['pod_id']) + for routing in routings['routings']] + expect = [('c7f641c9-8462-4007-84b2-3035d8cfb7a3', pod_id1), + ('b669a2da-ca95-47db-a2a9-ba9e546d82ee', pod_id2)] + self.assertItemsEqual(expect, actual) + + # failure case, only admin can show all resource routings + self.context.is_admin = False + res = self.controller.get_all() + self._validate_error_code(res, 403) + + self.context.is_admin = True + + @patch.object(pecan, 'response', new=FakeResponse) + @patch.object(pecan, 'response', new=mock.Mock) + @patch.object(context, 'extract_context_from_environ') + def test_delete(self, mock_context): + mock_context.return_value = self.context + + # prepare the foreign key: pod_id, project_id + kw_pod = {'pod': {'pod_name': 'pod1', 'az_name': 'az1'}} + pod_id = pod.PodsController().post(**kw_pod)['pod']['pod_id'] + + kw_binding = {'pod_binding': {'tenant_id': '01', 'pod_id': pod_id}} + project_id = pod.BindingsController().post(**kw_binding)[ + 'pod_binding']['tenant_id'] + + kw_routing = {'routing': + {'top_id': '09fd7cc9-d169-4b5a-88e8-436ecf4d0bfe', + 'bottom_id': 'dc80f9de-abb7-4ec6-ab7a-94f8fd1e20ef', + 'pod_id': pod_id, + 'project_id': project_id, + 'resource_type': 'subnet' + }} + + routing = self.controller.post(**kw_routing) + id = routing['routing']['id'] + res = self.controller.delete(id) + self.assertEqual(res.status, 200) + + routings = db_api.list_resource_routings(self.context, + [{'key': 'top_id', + 'comparator': 'eq', + 'value': '09fd7cc9-d' + '169-4b5a-88e8-436ecf4d0bfe' + }, + {'key': 'pod_id', + 'comparator': 'eq', + 'value': pod_id + }], []) + self.assertEqual(len(routings), 0) + + # failure case, only admin can delete resource routing + self.context.is_admin = False + res = self.controller.delete(id) + self._validate_error_code(res, 403) + + self.context.is_admin = True + + # failure case, resource routing not found + res = self.controller.delete(-123) + self._validate_error_code(res, 404) + + @patch.object(pecan, 'response', new=FakeResponse) + @patch.object(pecan, 'response', new=mock.Mock) + @patch.object(context, 'extract_context_from_environ') + def test_put(self, mock_context): + mock_context.return_value = self.context + + # prepare the foreign key: pod_id, project_id + kw_pod1 = {'pod': {'pod_name': 'pod1', 'az_name': 'az1'}} + pod_id1 = pod.PodsController().post(**kw_pod1)['pod']['pod_id'] + + kw_binding1 = {'pod_binding': {'tenant_id': '01', 'pod_id': pod_id1}} + project_id1 = pod.BindingsController().post(**kw_binding1)[ + 'pod_binding']['tenant_id'] + + body = {'routing': + {'top_id': 'c7f641c9-8462-4007-84b2-3035d8cfb7a3', + 'bottom_id': 'dc80f9de-abb7-4ec6-ab7a-94f8fd1e20ef', + 'pod_id': pod_id1, + 'project_id': project_id1, + 'resource_type': 'router' + }} + + # both bottom_id and resource type have been changed + body_update1 = {'routing': + {'bottom_id': 'fd72c010-6e62-4866-b999-6dcb718dd7b4', + 'resource_type': 'port' + }} + + id = self.controller.post(**body)['routing']['id'] + routing = self.controller.put(id, **body_update1) + + self.assertEqual('port', + routing['routing']['resource_type']) + self.assertEqual('fd72c010-6e62-4866-b999-6dcb718dd7b4', + routing['routing']['bottom_id']) + self.assertEqual(pod_id1, routing['routing']['pod_id']) + + # failure case, only admin can update resource routing + self.context.is_admin = False + res = self.controller.put(id, **body_update1) + self._validate_error_code(res, 403) + + self.context.is_admin = True + + # failure case, request body not found + body_update2 = {'route': + {'bottom_id': 'fd72c010-6e62-4866-b999-6dcb718dd7b4', + 'resource_type': 'port' + }} + res = self.controller.put(id, **body_update2) + self._validate_error_code(res, 400) + + # failure case, wrong resource type + body_update3 = {'routing': + {'resource_type': 'volume'}} + res = self.controller.put(id, **body_update3) + self._validate_error_code(res, 400) + + # failure case, the value to be updated is empty + body_update4 = {'routing': + {'top_id': ''}} + res = self.controller.put(id, **body_update4) + self._validate_error_code(res, 400) + + # failure case, the value to be updated is None + body_update5 = {'routing': + {'top_id': None}} + res = self.controller.put(id, **body_update5) + self._validate_error_code(res, 400) + + # failure case, the value to be updated is not appropriate + res = self.controller.put(-123, **body_update1) + self._validate_error_code(res, 404) + + # failure case, the pod_id and the project_id should be bound + kw_pod2 = {'pod': {'pod_name': 'pod2', 'az_name': 'az1'}} + pod_id2 = pod.PodsController().post(**kw_pod2)['pod']['pod_id'] + + # only update pod_id, and the new pod id is pod_id2. pod_id2 + # has not been bound to tenant whose tenant_id is project_id1 + body_update6 = {'routing': + {'pod_id': pod_id2}} + + res = self.controller.put(id, **body_update6) + self._validate_error_code(res, 400) + + # failure case, the pod_id and the project_id should be bound + kw_binding2 = {'pod_binding': {'tenant_id': '02', 'pod_id': pod_id2}} + project_id2 = pod.BindingsController().post(**kw_binding2)[ + 'pod_binding']['tenant_id'] + + # only update project_id, and the new project id is project_id2, + # this tenant has not been bound to pod whose pod_id is pod_id1 + body_update7 = {'routing': + {'project_id': project_id2}} + res = self.controller.put(id, **body_update7) + self._validate_error_code(res, 400) + + # failure case, the pod_id and the project_id should be bound + # both the pod_id and project_id are specified, and the + # tenant1 bound to pod1, pod3, tenant2 bound to pod1, pod2 + # original routing: pod=pod1, tenant=tenant1 + # updated routing: pod=pod3, tenant=tenant2 + # this case should be failed + + # bind the tenant1 to pod3 + kw_pod3 = {'pod': {'pod_name': 'pod3', 'az_name': 'az1'}} + pod_id3 = pod.PodsController().post(**kw_pod3)['pod']['pod_id'] + + kw_binding3 = {'pod_binding': {'tenant_id': '01', 'pod_id': pod_id3}} + pod_id3 = pod.BindingsController().post(**kw_binding3)[ + 'pod_binding']['pod_id'] + + # bind the tenant2 to pod1 + kw_binding4 = {'pod_binding': {'tenant_id': '02', 'pod_id': pod_id1}} + project_id2 = pod.BindingsController().post(**kw_binding4)[ + 'pod_binding']['tenant_id'] + + body_update8 = {'routing': + {'pod_id': pod_id3, + 'project_id': project_id2}} + res = self.controller.put(id, **body_update8) + self._validate_error_code(res, 400) + + def tearDown(self): + core.ModelBase.metadata.drop_all(core.get_engine())