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())