From 1afd7fc22acc36ae40af6b573940f02ea970a20f Mon Sep 17 00:00:00 2001 From: Akihiro Motoki Date: Thu, 24 Dec 2015 06:34:52 +0900 Subject: [PATCH] Make VPN endpoint groups aware from API There is no way to check whether the VPN endpoint groups feature is available without sending an API request at now. This commit split VPN endpoint groups feature as an extension. By doing so, API consumers including Horizon can know the feature is available through Neutron extension list API. Note that this commit touches only the API layer and does not touch the DB layer because the endpoint groups feature is the first citizen of VPNaaS API. Closes-Bug: #1515670 Change-Id: I82a3ab60b5a50eee2b4a84b54829a323cd2b3fb1 --- neutron_vpnaas/db/vpn/vpn_db.py | 5 +- .../extensions/vpn_endpoint_groups.py | 121 +++++++++++ neutron_vpnaas/extensions/vpnaas.py | 48 ----- neutron_vpnaas/services/vpn/plugin.py | 4 +- .../extensions/test_vpn_endpoint_groups.py | 196 ++++++++++++++++++ .../tests/unit/extensions/test_vpnaas.py | 144 ------------- 6 files changed, 324 insertions(+), 194 deletions(-) create mode 100644 neutron_vpnaas/extensions/vpn_endpoint_groups.py create mode 100644 neutron_vpnaas/tests/unit/extensions/test_vpn_endpoint_groups.py diff --git a/neutron_vpnaas/db/vpn/vpn_db.py b/neutron_vpnaas/db/vpn/vpn_db.py index b0132ebb3..88e096c55 100644 --- a/neutron_vpnaas/db/vpn/vpn_db.py +++ b/neutron_vpnaas/db/vpn/vpn_db.py @@ -34,13 +34,16 @@ from sqlalchemy.orm import exc from neutron_vpnaas._i18n import _LW from neutron_vpnaas.db.vpn import vpn_models from neutron_vpnaas.db.vpn import vpn_validator +from neutron_vpnaas.extensions import vpn_endpoint_groups from neutron_vpnaas.extensions import vpnaas from neutron_vpnaas.services.vpn.common import constants as v_constants LOG = logging.getLogger(__name__) -class VPNPluginDb(vpnaas.VPNPluginBase, base_db.CommonDbMixin): +class VPNPluginDb(vpnaas.VPNPluginBase, + vpn_endpoint_groups.VPNEndpointGroupsPluginBase, + base_db.CommonDbMixin): """VPN plugin database class using SQLAlchemy models.""" def _get_validator(self): diff --git a/neutron_vpnaas/extensions/vpn_endpoint_groups.py b/neutron_vpnaas/extensions/vpn_endpoint_groups.py new file mode 100644 index 000000000..d48d23137 --- /dev/null +++ b/neutron_vpnaas/extensions/vpn_endpoint_groups.py @@ -0,0 +1,121 @@ +# (c) Copyright 2015 NEC Corporation, All Rights Reserved. +# +# 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 abc + +import six + +from neutron.api import extensions +from neutron.api.v2 import attributes as attr +from neutron.api.v2 import resource_helper +from neutron.plugins.common import constants as nconstants + +from neutron_vpnaas.services.vpn.common import constants + + +RESOURCE_ATTRIBUTE_MAP = { + + 'endpoint_groups': { + 'id': {'allow_post': False, 'allow_put': False, + 'validate': {'type:uuid': None}, + 'is_visible': True, + 'primary_key': True}, + 'tenant_id': {'allow_post': True, 'allow_put': False, + 'validate': {'type:string': attr.TENANT_ID_MAX_LEN}, + 'required_by_policy': True, + 'is_visible': True}, + 'name': {'allow_post': True, 'allow_put': True, + 'validate': {'type:string': attr.NAME_MAX_LEN}, + 'is_visible': True, 'default': ''}, + 'description': {'allow_post': True, 'allow_put': True, + 'validate': {'type:string': attr.DESCRIPTION_MAX_LEN}, + 'is_visible': True, 'default': ''}, + 'type': {'allow_post': True, 'allow_put': False, + 'validate': { + 'type:values': constants.VPN_SUPPORTED_ENDPOINT_TYPES, + }, + 'is_visible': True}, + 'endpoints': {'allow_post': True, 'allow_put': False, + 'convert_to': attr.convert_to_list, + 'is_visible': True}, + }, +} + + +class Vpn_endpoint_groups(extensions.ExtensionDescriptor): + + @classmethod + def get_name(cls): + return "VPN Endpoint Groups" + + @classmethod + def get_alias(cls): + return "vpn-endpoint-groups" + + @classmethod + def get_description(cls): + return "VPN endpoint groups support" + + @classmethod + def get_updated(cls): + return "2015-08-04T10:00:00-00:00" + + @classmethod + def get_resources(cls): + plural_mappings = resource_helper.build_plural_mappings( + {}, RESOURCE_ATTRIBUTE_MAP) + attr.PLURALS.update(plural_mappings) + return resource_helper.build_resource_info(plural_mappings, + RESOURCE_ATTRIBUTE_MAP, + nconstants.VPN, + register_quota=True, + translate_name=True) + + def get_required_extensions(self): + return ["vpnaas"] + + def update_attributes_map(self, attributes): + super(Vpn_endpoint_groups, self).update_attributes_map( + attributes, extension_attrs_map=RESOURCE_ATTRIBUTE_MAP) + + def get_extended_resources(self, version): + if version == "2.0": + return RESOURCE_ATTRIBUTE_MAP + else: + return {} + + +@six.add_metaclass(abc.ABCMeta) +class VPNEndpointGroupsPluginBase(object): + + @abc.abstractmethod + def create_endpoint_group(self, context, endpoint_group): + pass + + @abc.abstractmethod + def update_endpoint_group(self, context, endpoint_group_id, + endpoint_group): + pass + + @abc.abstractmethod + def delete_endpoint_group(self, context, endpoint_group_id): + pass + + @abc.abstractmethod + def get_endpoint_group(self, context, endpoint_group_id, fields=None): + pass + + @abc.abstractmethod + def get_endpoint_groups(self, context, filters=None, fields=None): + pass diff --git a/neutron_vpnaas/extensions/vpnaas.py b/neutron_vpnaas/extensions/vpnaas.py index 4210d2f83..59d2b43e6 100644 --- a/neutron_vpnaas/extensions/vpnaas.py +++ b/neutron_vpnaas/extensions/vpnaas.py @@ -24,8 +24,6 @@ from neutron.common import exceptions as nexception from neutron.plugins.common import constants as nconstants from neutron.services import service_base -from neutron_vpnaas.services.vpn.common import constants - class VPNServiceNotFound(nexception.NotFound): message = _("VPNService %(vpnservice_id)s could not be found") @@ -423,31 +421,6 @@ RESOURCE_ATTRIBUTE_MAP = { 'validate': {'type:values': vpn_supported_pfs}, 'is_visible': True} }, - - 'endpoint_groups': { - 'id': {'allow_post': False, 'allow_put': False, - 'validate': {'type:uuid': None}, - 'is_visible': True, - 'primary_key': True}, - 'tenant_id': {'allow_post': True, 'allow_put': False, - 'validate': {'type:string': attr.TENANT_ID_MAX_LEN}, - 'required_by_policy': True, - 'is_visible': True}, - 'name': {'allow_post': True, 'allow_put': True, - 'validate': {'type:string': attr.NAME_MAX_LEN}, - 'is_visible': True, 'default': ''}, - 'description': {'allow_post': True, 'allow_put': True, - 'validate': {'type:string': attr.DESCRIPTION_MAX_LEN}, - 'is_visible': True, 'default': ''}, - 'type': {'allow_post': True, 'allow_put': False, - 'validate': { - 'type:values': constants.VPN_SUPPORTED_ENDPOINT_TYPES, - }, - 'is_visible': True}, - 'endpoints': {'allow_post': True, 'allow_put': False, - 'convert_to': attr.convert_to_list, - 'is_visible': True}, - }, } @@ -595,24 +568,3 @@ class VPNPluginBase(service_base.ServicePluginBase): @abc.abstractmethod def delete_ipsecpolicy(self, context, ipsecpolicy_id): pass - - @abc.abstractmethod - def create_endpoint_group(self, context, endpoint_group): - pass - - @abc.abstractmethod - def update_endpoint_group(self, context, endpoint_group_id, - endpoint_group): - pass - - @abc.abstractmethod - def delete_endpoint_group(self, context, endpoint_group_id): - pass - - @abc.abstractmethod - def get_endpoint_group(self, context, endpoint_group_id, fields=None): - pass - - @abc.abstractmethod - def get_endpoint_groups(self, context, filters=None, fields=None): - pass diff --git a/neutron_vpnaas/services/vpn/plugin.py b/neutron_vpnaas/services/vpn/plugin.py index 95702ab35..aa8de0e6d 100644 --- a/neutron_vpnaas/services/vpn/plugin.py +++ b/neutron_vpnaas/services/vpn/plugin.py @@ -40,7 +40,9 @@ class VPNPlugin(vpn_db.VPNPluginDb): Most DB related works are implemented in class vpn_db.VPNPluginDb. """ - supported_extension_aliases = ["vpnaas", "service-type"] + supported_extension_aliases = ["vpnaas", + "vpn-endpoint-groups", + "service-type"] path_prefix = "/vpn" diff --git a/neutron_vpnaas/tests/unit/extensions/test_vpn_endpoint_groups.py b/neutron_vpnaas/tests/unit/extensions/test_vpn_endpoint_groups.py new file mode 100644 index 000000000..4ad40af9c --- /dev/null +++ b/neutron_vpnaas/tests/unit/extensions/test_vpn_endpoint_groups.py @@ -0,0 +1,196 @@ +# (c) Copyright 2015 NEC Corporation, All Rights Reserved. +# +# 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 copy + +import mock +from oslo_utils import uuidutils +from webob import exc + +from neutron.plugins.common import constants as nconstants +from neutron.tests.unit.api.v2 import test_base as test_api_v2 + +from neutron_vpnaas.extensions import vpn_endpoint_groups +from neutron_vpnaas.extensions import vpnaas +from neutron_vpnaas.services.vpn.common import constants +from neutron_vpnaas.tests import base + +_uuid = uuidutils.generate_uuid +_get_path = test_api_v2._get_path + + +class VpnEndpointGroupsTestPlugin( + vpnaas.VPNPluginBase, + vpn_endpoint_groups.VPNEndpointGroupsPluginBase): + pass + + +class VpnEndpointGroupsTestCase(base.ExtensionTestCase): + + fmt = 'json' + + def setUp(self): + super(VpnEndpointGroupsTestCase, self).setUp() + plural_mappings = {'endpoint_group': 'endpoint-groups'} + self._setUpExtension( + 'neutron_vpnaas.tests.unit.extensions.test_vpn_endpoint_groups.' + 'VpnEndpointGroupsTestPlugin', + nconstants.VPN, + vpn_endpoint_groups.RESOURCE_ATTRIBUTE_MAP, + vpn_endpoint_groups.Vpn_endpoint_groups, + 'vpn', plural_mappings=plural_mappings, + use_quota=True) + + def helper_test_endpoint_group_create(self, data): + """Check that the endpoint_group_create works. + + Uses passed in endpoint group information, which specifies an + endpoint type and values. + """ + data['endpoint_group'].update({'tenant_id': _uuid(), + 'name': 'my endpoint group', + 'description': 'my description'}) + return_value = copy.copy(data['endpoint_group']) + return_value.update({'id': _uuid()}) + + instance = self.plugin.return_value + instance.create_endpoint_group.return_value = return_value + res = self.api.post(_get_path('vpn/endpoint-groups', fmt=self.fmt), + self.serialize(data), + content_type='application/%s' % self.fmt) + instance.create_endpoint_group.assert_called_with( + mock.ANY, endpoint_group=data) + self.assertEqual(exc.HTTPCreated.code, res.status_int) + res = self.deserialize(res) + self.assertIn('endpoint_group', res) + self.assertEqual(res['endpoint_group'], return_value) + + def test_create_cidr_endpoint_group_create(self): + """Test creation of CIDR type endpoint group.""" + data = {'endpoint_group': + {'type': constants.CIDR_ENDPOINT, + 'endpoints': ['10.10.10.0/24', '20.20.20.0/24']}} + self.helper_test_endpoint_group_create(data) + + def test_create_subnet_endpoint_group_create(self): + """Test creation of subnet type endpoint group.""" + data = {'endpoint_group': + {'type': constants.SUBNET_ENDPOINT, + 'endpoints': [_uuid(), _uuid()]}} + self.helper_test_endpoint_group_create(data) + + def test_create_vlan_endpoint_group_create(self): + """Test creation of VLAN type endpoint group.""" + data = {'endpoint_group': + {'type': constants.VLAN_ENDPOINT, + 'endpoints': ['100', '200', '300', '400']}} + self.helper_test_endpoint_group_create(data) + + def test_get_endpoint_group(self): + """Test show for endpoint group.""" + endpoint_group_id = _uuid() + return_value = {'id': endpoint_group_id, + 'tenant_id': _uuid(), + 'name': 'my-endpoint-group', + 'description': 'my endpoint group', + 'type': constants.CIDR_ENDPOINT, + 'endpoints': ['10.10.10.0/24']} + + instance = self.plugin.return_value + instance.get_endpoint_group.return_value = return_value + + res = self.api.get(_get_path('vpn/endpoint-groups', + id=endpoint_group_id, + fmt=self.fmt)) + + instance.get_endpoint_group.assert_called_with(mock.ANY, + endpoint_group_id, + fields=mock.ANY) + self.assertEqual(res.status_int, exc.HTTPOk.code) + res = self.deserialize(res) + self.assertIn('endpoint_group', res) + self.assertEqual(res['endpoint_group'], return_value) + + def test_endpoint_group_list(self): + """Test listing all endpoint groups.""" + return_value = [{'id': _uuid(), + 'tenant_id': _uuid(), + 'name': 'my-endpoint-group', + 'description': 'my endpoint group', + 'type': constants.CIDR_ENDPOINT, + 'endpoints': ['10.10.10.0/24']}, + {'id': _uuid(), + 'tenant_id': _uuid(), + 'name': 'another-endpoint-group', + 'description': 'second endpoint group', + 'type': constants.VLAN_ENDPOINT, + 'endpoints': ['100', '200', '300']}] + + instance = self.plugin.return_value + instance.get_endpoint_groups.return_value = return_value + + res = self.api.get(_get_path('vpn/endpoint-groups', fmt=self.fmt)) + + instance.get_endpoint_groups.assert_called_with(mock.ANY, + fields=mock.ANY, + filters=mock.ANY) + self.assertEqual(res.status_int, exc.HTTPOk.code) + + def test_endpoint_group_delete(self): + """Test deleting an endpoint group.""" + self._test_entity_delete('endpoint_group') + + def test_endpoint_group_update(self): + """Test updating endpoint_group.""" + endpoint_group_id = _uuid() + update_data = {'endpoint_group': {'description': 'new description'}} + return_value = {'id': endpoint_group_id, + 'tenant_id': _uuid(), + 'name': 'my-endpoint-group', + 'description': 'new_description', + 'type': constants.CIDR_ENDPOINT, + 'endpoints': ['10.10.10.0/24']} + + instance = self.plugin.return_value + instance.update_endpoint_group.return_value = return_value + + res = self.api.put(_get_path('vpn/endpoint-groups', + id=endpoint_group_id, + fmt=self.fmt), + self.serialize(update_data)) + + instance.update_endpoint_group.assert_called_with( + mock.ANY, endpoint_group_id, endpoint_group=update_data) + self.assertEqual(res.status_int, exc.HTTPOk.code) + res = self.deserialize(res) + self.assertIn('endpoint_group', res) + self.assertEqual(res['endpoint_group'], return_value) + + def test_fail_updating_endpoints_in_endpoint_group(self): + """Test fails to update the endpoints in an endpoint group. + + This documents that we are not allowing endpoints to be updated + (currently), as doing so, implies that the connection using the + enclosing endpoint group would also need to be updated. For now, + a new endpoint group can be created, and the connection can be + updated to point to the new endpoint group. + """ + endpoint_group_id = _uuid() + update_data = {'endpoint_group': {'endpoints': ['10.10.10.0/24']}} + res = self.api.put(_get_path('vpn/endpoint-groups', + id=endpoint_group_id, + fmt=self.fmt), + params=self.serialize(update_data), + expect_errors=True) + self.assertEqual(exc.HTTPBadRequest.code, res.status_int) diff --git a/neutron_vpnaas/tests/unit/extensions/test_vpnaas.py b/neutron_vpnaas/tests/unit/extensions/test_vpnaas.py index c31a83b4d..da2db9106 100644 --- a/neutron_vpnaas/tests/unit/extensions/test_vpnaas.py +++ b/neutron_vpnaas/tests/unit/extensions/test_vpnaas.py @@ -21,7 +21,6 @@ from oslo_utils import uuidutils from webob import exc from neutron_vpnaas.extensions import vpnaas -from neutron_vpnaas.services.vpn.common import constants from neutron_vpnaas.tests import base _uuid = uuidutils.generate_uuid @@ -567,146 +566,3 @@ class VpnaasExtensionTestCase(base.ExtensionTestCase): def test_ipsec_site_connection_delete(self): """Test case to delete a ipsec_site_connection.""" self._test_entity_delete('ipsec_site_connection') - - def helper_test_endpoint_group_create(self, data): - """Check that the endpoint_group_create works. - - Uses passed in endpoint group information, which specifies an - endpoint type and values. - """ - data['endpoint_group'].update({'tenant_id': _uuid(), - 'name': 'my endpoint group', - 'description': 'my description'}) - return_value = copy.copy(data['endpoint_group']) - return_value.update({'id': _uuid()}) - - instance = self.plugin.return_value - instance.create_endpoint_group.return_value = return_value - res = self.api.post(_get_path('vpn/endpoint-groups', fmt=self.fmt), - self.serialize(data), - content_type='application/%s' % self.fmt) - instance.create_endpoint_group.assert_called_with( - mock.ANY, endpoint_group=data) - self.assertEqual(exc.HTTPCreated.code, res.status_int) - res = self.deserialize(res) - self.assertIn('endpoint_group', res) - self.assertEqual(res['endpoint_group'], return_value) - - def test_create_cidr_endpoint_group_create(self): - """Test creation of CIDR type endpoint group.""" - data = {'endpoint_group': - {'type': constants.CIDR_ENDPOINT, - 'endpoints': ['10.10.10.0/24', '20.20.20.0/24']}} - self.helper_test_endpoint_group_create(data) - - def test_create_subnet_endpoint_group_create(self): - """Test creation of subnet type endpoint group.""" - data = {'endpoint_group': - {'type': constants.SUBNET_ENDPOINT, - 'endpoints': [_uuid(), _uuid()]}} - self.helper_test_endpoint_group_create(data) - - def test_create_vlan_endpoint_group_create(self): - """Test creation of VLAN type endpoint group.""" - data = {'endpoint_group': - {'type': constants.VLAN_ENDPOINT, - 'endpoints': ['100', '200', '300', '400']}} - self.helper_test_endpoint_group_create(data) - - def test_get_endpoint_group(self): - """Test show for endpoint group.""" - endpoint_group_id = _uuid() - return_value = {'id': endpoint_group_id, - 'tenant_id': _uuid(), - 'name': 'my-endpoint-group', - 'description': 'my endpoint group', - 'type': constants.CIDR_ENDPOINT, - 'endpoints': ['10.10.10.0/24']} - - instance = self.plugin.return_value - instance.get_endpoint_group.return_value = return_value - - res = self.api.get(_get_path('vpn/endpoint-groups', - id=endpoint_group_id, - fmt=self.fmt)) - - instance.get_endpoint_group.assert_called_with(mock.ANY, - endpoint_group_id, - fields=mock.ANY) - self.assertEqual(res.status_int, exc.HTTPOk.code) - res = self.deserialize(res) - self.assertIn('endpoint_group', res) - self.assertEqual(res['endpoint_group'], return_value) - - def test_endpoint_group_list(self): - """Test listing all endpoint groups.""" - return_value = [{'id': _uuid(), - 'tenant_id': _uuid(), - 'name': 'my-endpoint-group', - 'description': 'my endpoint group', - 'type': constants.CIDR_ENDPOINT, - 'endpoints': ['10.10.10.0/24']}, - {'id': _uuid(), - 'tenant_id': _uuid(), - 'name': 'another-endpoint-group', - 'description': 'second endpoint group', - 'type': constants.VLAN_ENDPOINT, - 'endpoints': ['100', '200', '300']}] - - instance = self.plugin.return_value - instance.get_endpoint_groups.return_value = return_value - - res = self.api.get(_get_path('vpn/endpoint-groups', fmt=self.fmt)) - - instance.get_endpoint_groups.assert_called_with(mock.ANY, - fields=mock.ANY, - filters=mock.ANY) - self.assertEqual(res.status_int, exc.HTTPOk.code) - - def test_endpoint_group_delete(self): - """Test deleting an endpoint group.""" - self._test_entity_delete('endpoint_group') - - def test_endpoint_group_update(self): - """Test updating endpoint_group.""" - endpoint_group_id = _uuid() - update_data = {'endpoint_group': {'description': 'new description'}} - return_value = {'id': endpoint_group_id, - 'tenant_id': _uuid(), - 'name': 'my-endpoint-group', - 'description': 'new_description', - 'type': constants.CIDR_ENDPOINT, - 'endpoints': ['10.10.10.0/24']} - - instance = self.plugin.return_value - instance.update_endpoint_group.return_value = return_value - - res = self.api.put(_get_path('vpn/endpoint-groups', - id=endpoint_group_id, - fmt=self.fmt), - self.serialize(update_data)) - - instance.update_endpoint_group.assert_called_with( - mock.ANY, endpoint_group_id, endpoint_group=update_data) - self.assertEqual(res.status_int, exc.HTTPOk.code) - res = self.deserialize(res) - self.assertIn('endpoint_group', res) - self.assertEqual(res['endpoint_group'], return_value) - - def test_fail_updating_endpoints_in_endpoint_group(self): - """Test fails to update the endpoints in an endpoint group. - - This documents that we are not allowing endpoints to be updated - (currently), as doing so, implies that the connection using the - enclosing endpoint group would also need to be updated. For now, - a new endpoint group can be created, and the connection can be - updated to point to the new endpoint group. - """ - endpoint_group_id = _uuid() - update_data = {'endpoint_group': {'endpoints': ['10.10.10.0/24']}} - res = self.api.put(_get_path('vpn/endpoint-groups', - id=endpoint_group_id, - fmt=self.fmt), - params=self.serialize(update_data), - expect_errors=True) - self.assertEqual(exc.HTTPBadRequest.code, res.status_int)