From: vikram.choudhary Date: Tue, 9 Jun 2015 14:25:59 +0000 (+0530) Subject: Support Basic Address Scope CRUD as extensions X-Git-Url: https://review.fuel-infra.org/gitweb?a=commitdiff_plain;h=cbd95318ad6c44e72a3aa163f7a399353c8b4458;p=openstack-build%2Fneutron-build.git Support Basic Address Scope CRUD as extensions This patch adds the support for basic address scope CRUD. Subsequent patches will be added to use this address scope on subnet pools. DocImpact APIImpact Co-Authored-By: Ryan Tidwell Co-Authored-By: Numan Siddique Change-Id: Icabdd22577cfda0e1fbf6042e4b05b8080e54fdb Partially-implements: blueprint address-scopes --- diff --git a/etc/policy.json b/etc/policy.json index 87f6b2668..eaf6d685f 100644 --- a/etc/policy.json +++ b/etc/policy.json @@ -9,6 +9,7 @@ "shared_firewalls": "field:firewalls:shared=True", "shared_firewall_policies": "field:firewall_policies:shared=True", "shared_subnetpools": "field:subnetpools:shared=True", + "shared_address_scopes": "field:address_scopes:shared=True", "external": "field:networks:router:external=True", "default": "rule:admin_or_owner", @@ -23,6 +24,13 @@ "update_subnetpool": "rule:admin_or_owner", "delete_subnetpool": "rule:admin_or_owner", + "create_address_scope": "", + "create_address_scope:shared": "rule:admin_only", + "get_address_scope": "rule:admin_or_owner or rule:shared_address_scopes", + "update_address_scope": "rule:admin_or_owner", + "update_address_scope:shared": "rule:admin_only", + "delete_address_scope": "rule:admin_or_owner", + "create_network": "", "get_network": "rule:admin_or_owner or rule:shared or rule:external or rule:context_is_advsvc", "get_network:router:external": "rule:regular_user", diff --git a/neutron/db/address_scope_db.py b/neutron/db/address_scope_db.py new file mode 100644 index 000000000..c5c7a8469 --- /dev/null +++ b/neutron/db/address_scope_db.py @@ -0,0 +1,105 @@ +# Copyright (c) 2015 Huawei Technologies Co.,LTD. +# +# 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 oslo_log import log as logging +from oslo_utils import uuidutils +import sqlalchemy as sa +from sqlalchemy.orm import exc + +from neutron.db import model_base +from neutron.db import models_v2 +from neutron.extensions import address_scope as ext_address_scope + +LOG = logging.getLogger(__name__) + + +class AddressScope(model_base.BASEV2, models_v2.HasId, models_v2.HasTenant): + """Represents a neutron address scope.""" + + __tablename__ = "address_scopes" + + name = sa.Column(sa.String(255), nullable=False) + shared = sa.Column(sa.Boolean, nullable=False) + + +class AddressScopeDbMixin(ext_address_scope.AddressScopePluginBase): + """Mixin class to add address scope to db_base_plugin_v2.""" + + __native_bulk_support = True + + def _make_address_scope_dict(self, address_scope, fields=None): + res = {'id': address_scope['id'], + 'name': address_scope['name'], + 'tenant_id': address_scope['tenant_id'], + 'shared': address_scope['shared']} + return self._fields(res, fields) + + def _get_address_scope(self, context, id): + try: + return self._get_by_id(context, AddressScope, id) + except exc.NoResultFound: + raise ext_address_scope.AddressScopeNotFound(address_scope_id=id) + + def create_address_scope(self, context, address_scope): + """Create a address scope.""" + a_s = address_scope['address_scope'] + tenant_id = self._get_tenant_id_for_create(context, a_s) + address_scope_id = a_s.get('id') or uuidutils.generate_uuid() + with context.session.begin(subtransactions=True): + pool_args = {'tenant_id': tenant_id, + 'id': address_scope_id, + 'name': a_s['name'], + 'shared': a_s['shared']} + address_scope = AddressScope(**pool_args) + context.session.add(address_scope) + + return self._make_address_scope_dict(address_scope) + + def update_address_scope(self, context, id, address_scope): + a_s = address_scope['address_scope'] + with context.session.begin(subtransactions=True): + address_scope = self._get_address_scope(context, id) + if address_scope.shared and not a_s.get('shared', True): + reason = _("Shared address scope can't be unshared") + raise ext_address_scope.AddressScopeUpdateError( + address_scope_id=id, reason=reason) + address_scope.update(a_s) + + return self._make_address_scope_dict(address_scope) + + def get_address_scope(self, context, id, fields=None): + address_scope = self._get_address_scope(context, id) + return self._make_address_scope_dict(address_scope, fields) + + def get_address_scopes(self, context, filters=None, fields=None, + sorts=None, limit=None, marker=None, + page_reverse=False): + marker_obj = self._get_marker_obj(context, 'addrscope', limit, marker) + collection = self._get_collection(context, AddressScope, + self._make_address_scope_dict, + filters=filters, fields=fields, + sorts=sorts, + limit=limit, + marker_obj=marker_obj, + page_reverse=page_reverse) + return collection + + def get_address_scopes_count(self, context, filters=None): + return self._get_collection_count(context, AddressScope, + filters=filters) + + def delete_address_scope(self, context, id): + with context.session.begin(subtransactions=True): + address_scope = self._get_address_scope(context, id) + context.session.delete(address_scope) diff --git a/neutron/db/migration/alembic_migrations/versions/52c5312f6baf_address_scopes.py b/neutron/db/migration/alembic_migrations/versions/52c5312f6baf_address_scopes.py new file mode 100644 index 000000000..9fa1466e5 --- /dev/null +++ b/neutron/db/migration/alembic_migrations/versions/52c5312f6baf_address_scopes.py @@ -0,0 +1,36 @@ +# Copyright (c) 2015 Red Hat, 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. +# + +"""Initial operations in support of address scopes + +""" + +# revision identifiers, used by Alembic. +revision = '52c5312f6baf' +down_revision = '599c6a226151' + +from alembic import op +import sqlalchemy as sa + + +def upgrade(): + op.create_table( + 'address_scopes', + sa.Column('id', sa.String(length=36), nullable=False), + sa.Column('name', sa.String(length=255), nullable=False), + sa.Column('tenant_id', sa.String(length=255), nullable=True, + index=True), + sa.Column('shared', sa.Boolean(), nullable=False), + sa.PrimaryKeyConstraint('id')) diff --git a/neutron/db/migration/alembic_migrations/versions/HEAD b/neutron/db/migration/alembic_migrations/versions/HEAD index 054926f3a..5d2bcdc22 100644 --- a/neutron/db/migration/alembic_migrations/versions/HEAD +++ b/neutron/db/migration/alembic_migrations/versions/HEAD @@ -1 +1 @@ -599c6a226151 +52c5312f6baf diff --git a/neutron/extensions/address_scope.py b/neutron/extensions/address_scope.py new file mode 100644 index 000000000..63829920b --- /dev/null +++ b/neutron/extensions/address_scope.py @@ -0,0 +1,138 @@ +# Copyright (c) 2015 Huawei Technologies Co.,LTD. +# +# 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 + +from neutron.api import extensions +from neutron.api.v2 import attributes as attr +from neutron.api.v2 import base +from neutron.common import exceptions as nexception +from neutron import manager +import six + +ADDRESS_SCOPE = 'address_scope' +ADDRESS_SCOPES = '%ss' % ADDRESS_SCOPE + + +# Attribute Map +RESOURCE_ATTRIBUTE_MAP = { + ADDRESS_SCOPES: { + 'id': {'allow_post': False, + 'allow_put': False, + 'validate': {'type:uuid': None}, + 'is_visible': True, + 'primary_key': True}, + 'name': {'allow_post': True, + 'allow_put': True, + 'default': '', + 'validate': {'type:string': attr.NAME_MAX_LEN}, + 'is_visible': True}, + 'tenant_id': {'allow_post': True, + 'allow_put': False, + 'validate': {'type:string': attr.TENANT_ID_MAX_LEN}, + 'required_by_policy': True, + 'is_visible': True}, + attr.SHARED: {'allow_post': True, + 'allow_put': True, + 'default': False, + 'convert_to': attr.convert_to_boolean, + 'is_visible': True, + 'required_by_policy': True, + 'enforce_policy': True}, + } +} + + +class AddressScopeNotFound(nexception.NotFound): + message = _("Address scope %(address_scope_id)s could not be found") + + +class AddressScopeDeleteError(nexception.BadRequest): + message = _("Unable to delete address scope %(address_scope_id)s : " + "%(reason)s") + + +class AddressScopeUpdateError(nexception.BadRequest): + message = _("Unable to update address scope %(address_scope_id)s : " + "%(reason)s") + + +class Address_scope(extensions.ExtensionDescriptor): + """Extension class supporting Address Scopes.""" + + @classmethod + def get_name(cls): + return "Address scope" + + @classmethod + def get_alias(cls): + return "address-scope" + + @classmethod + def get_description(cls): + return "Address scopes extension." + + @classmethod + def get_updated(cls): + return "2015-07-26T10:00:00-00:00" + + @classmethod + def get_resources(cls): + """Returns Ext Resources.""" + my_plurals = [(key, key[:-1]) for key in RESOURCE_ATTRIBUTE_MAP.keys()] + attr.PLURALS.update(dict(my_plurals)) + plugin = manager.NeutronManager.get_plugin() + collection_name = ADDRESS_SCOPES.replace('_', '-') + params = RESOURCE_ATTRIBUTE_MAP.get(ADDRESS_SCOPES, dict()) + controller = base.create_resource(collection_name, + ADDRESS_SCOPE, + plugin, params, allow_bulk=True, + allow_pagination=True, + allow_sorting=True) + + ex = extensions.ResourceExtension(collection_name, controller, + attr_map=params) + return [ex] + + def get_extended_resources(self, version): + return {} + + +@six.add_metaclass(abc.ABCMeta) +class AddressScopePluginBase(object): + + @abc.abstractmethod + def create_address_scope(self, context, adress_scope): + pass + + @abc.abstractmethod + def update_address_scope(self, context, id, address_scope): + pass + + @abc.abstractmethod + def get_address_scope(self, context, id, fields=None): + pass + + @abc.abstractmethod + def get_address_scopes(self, context, filters=None, fields=None, + sorts=None, limit=None, marker=None, + page_reverse=False): + pass + + @abc.abstractmethod + def delete_address_scope(self, context, id): + pass + + def get_address_scopes_count(self, context, filters=None): + raise NotImplementedError() diff --git a/neutron/plugins/ml2/plugin.py b/neutron/plugins/ml2/plugin.py index a56039d45..c61a717e3 100644 --- a/neutron/plugins/ml2/plugin.py +++ b/neutron/plugins/ml2/plugin.py @@ -44,6 +44,7 @@ from neutron.common import log as neutron_log from neutron.common import rpc as n_rpc from neutron.common import topics from neutron.common import utils +from neutron.db import address_scope_db from neutron.db import agents_db from neutron.db import agentschedulers_db from neutron.db import allowedaddresspairs_db as addr_pair_db @@ -88,7 +89,8 @@ class Ml2Plugin(db_base_plugin_v2.NeutronDbPluginV2, addr_pair_db.AllowedAddressPairsMixin, vlantransparent_db.Vlantransparent_db_mixin, extradhcpopt_db.ExtraDhcpOptMixin, - netmtu_db.Netmtu_db_mixin): + netmtu_db.Netmtu_db_mixin, + address_scope_db.AddressScopeDbMixin): """Implement the Neutron L2 abstractions using modules. @@ -112,7 +114,8 @@ class Ml2Plugin(db_base_plugin_v2.NeutronDbPluginV2, "dhcp_agent_scheduler", "multi-provider", "allowed-address-pairs", "extra_dhcp_opt", "subnet_allocation", - "net-mtu", "vlan-transparent"] + "net-mtu", "vlan-transparent", + "address-scope"] @property def supported_extension_aliases(self): diff --git a/neutron/tests/etc/policy.json b/neutron/tests/etc/policy.json index 87f6b2668..eaf6d685f 100644 --- a/neutron/tests/etc/policy.json +++ b/neutron/tests/etc/policy.json @@ -9,6 +9,7 @@ "shared_firewalls": "field:firewalls:shared=True", "shared_firewall_policies": "field:firewall_policies:shared=True", "shared_subnetpools": "field:subnetpools:shared=True", + "shared_address_scopes": "field:address_scopes:shared=True", "external": "field:networks:router:external=True", "default": "rule:admin_or_owner", @@ -23,6 +24,13 @@ "update_subnetpool": "rule:admin_or_owner", "delete_subnetpool": "rule:admin_or_owner", + "create_address_scope": "", + "create_address_scope:shared": "rule:admin_only", + "get_address_scope": "rule:admin_or_owner or rule:shared_address_scopes", + "update_address_scope": "rule:admin_or_owner", + "update_address_scope:shared": "rule:admin_only", + "delete_address_scope": "rule:admin_or_owner", + "create_network": "", "get_network": "rule:admin_or_owner or rule:shared or rule:external or rule:context_is_advsvc", "get_network:router:external": "rule:regular_user", diff --git a/neutron/tests/unit/extensions/test_address_scope.py b/neutron/tests/unit/extensions/test_address_scope.py new file mode 100644 index 000000000..df46e6bce --- /dev/null +++ b/neutron/tests/unit/extensions/test_address_scope.py @@ -0,0 +1,239 @@ +# Copyright (c) 2015 Red Hat, 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 contextlib + +import webob.exc + +from neutron.api.v2 import attributes as attr +from neutron import context +from neutron.db import address_scope_db +from neutron.db import db_base_plugin_v2 +from neutron.extensions import address_scope as ext_address_scope +from neutron.tests.unit.db import test_db_base_plugin_v2 + +DB_PLUGIN_KLASS = ('neutron.tests.unit.extensions.test_address_scope.' + 'AddressScopeTestPlugin') + + +class AddressScopeTestExtensionManager(object): + + def get_resources(self): + # Add the resources to the global attribute map + # This is done here as the setup process won't + # initialize the main API router which extends + # the global attribute map + attr.RESOURCE_ATTRIBUTE_MAP.update( + ext_address_scope.RESOURCE_ATTRIBUTE_MAP) + return ext_address_scope.Address_scope.get_resources() + + def get_actions(self): + return [] + + def get_request_extensions(self): + return [] + + +class AddressScopeTestCase(test_db_base_plugin_v2.NeutronDbPluginV2TestCase): + + def _create_address_scope(self, fmt, expected_res_status=None, + admin=False, **kwargs): + address_scope = {'address_scope': {}} + for k, v in kwargs.items(): + address_scope['address_scope'][k] = str(v) + + address_scope_req = self.new_create_request('address-scopes', + address_scope, fmt) + + if not admin: + neutron_context = context.Context('', kwargs.get('tenant_id', + self._tenant_id)) + address_scope_req.environ['neutron.context'] = neutron_context + + address_scope_res = address_scope_req.get_response(self.ext_api) + if expected_res_status: + self.assertEqual(address_scope_res.status_int, expected_res_status) + return address_scope_res + + def _make_address_scope(self, fmt, admin=False, **kwargs): + res = self._create_address_scope(fmt, admin=admin, **kwargs) + if res.status_int >= webob.exc.HTTPClientError.code: + raise webob.exc.HTTPClientError(code=res.status_int) + return self.deserialize(fmt, res) + + @contextlib.contextmanager + def address_scope(self, admin=False, **kwargs): + addr_scope = self._make_address_scope(self.fmt, admin, **kwargs) + yield addr_scope + + def _test_create_address_scope(self, admin=False, expected=None, **kwargs): + keys = kwargs.copy() + keys.setdefault('tenant_id', self._tenant_id) + with self.address_scope(admin=admin, **keys) as addr_scope: + self._validate_resource(addr_scope, keys, 'address_scope') + if expected: + self._compare_resource(addr_scope, expected, 'address_scope') + return addr_scope + + def _test_update_address_scope(self, addr_scope_id, data, admin=False, + expected=None, tenant_id=None): + update_req = self.new_update_request( + 'address-scopes', data, addr_scope_id) + if not admin: + neutron_context = context.Context('', tenant_id or self._tenant_id) + update_req.environ['neutron.context'] = neutron_context + + update_res = update_req.get_response(self.ext_api) + if expected: + addr_scope = self.deserialize(self.fmt, update_res) + self._compare_resource(addr_scope, expected, 'address_scope') + return addr_scope + + return update_res + + +class AddressScopeTestPlugin(db_base_plugin_v2.NeutronDbPluginV2, + address_scope_db.AddressScopeDbMixin): + __native_pagination_support = True + __native_sorting_support = True + + supported_extension_aliases = ["address-scope"] + + +class TestAddressScope(AddressScopeTestCase): + + def setUp(self): + plugin = DB_PLUGIN_KLASS + ext_mgr = AddressScopeTestExtensionManager() + super(TestAddressScope, self).setUp(plugin=plugin, ext_mgr=ext_mgr) + + def test_create_address_scope(self): + expected_addr_scope = {'name': 'foo-address-scope', + 'tenant_id': self._tenant_id, + 'shared': False} + self._test_create_address_scope(name='foo-address-scope', + expected=expected_addr_scope) + + def test_create_address_scope_empty_name(self): + expected_addr_scope = {'name': '', + 'tenant_id': self._tenant_id, + 'shared': False} + self._test_create_address_scope(name='', expected=expected_addr_scope) + + # no name specified + self._test_create_address_scope(expected=expected_addr_scope) + + def test_create_address_scope_shared_admin(self): + expected_addr_scope = {'name': 'foo-address-scope', 'shared': True} + self._test_create_address_scope(name='foo-address-scope', admin=True, + shared=True, + expected=expected_addr_scope) + + def test_created_address_scope_shared_non_admin(self): + res = self._create_address_scope(self.fmt, name='foo-address-scope', + tenant_id=self._tenant_id, + admin=False, shared=True) + self.assertEqual(webob.exc.HTTPForbidden.code, res.status_int) + + def test_created_address_scope_specify_id(self): + res = self._create_address_scope(self.fmt, name='foo-address-scope', + id='foo-id') + self.assertEqual(webob.exc.HTTPClientError.code, res.status_int) + + def test_delete_address_scope(self): + with self.address_scope(name='foo-address-scope') as addr_scope: + self._delete('address-scopes', addr_scope['address_scope']['id']) + self._show('address-scopes', addr_scope['address_scope']['id'], + expected_code=webob.exc.HTTPNotFound.code) + + def test_update_address_scope(self): + addr_scope = self._test_create_address_scope(name='foo-address-scope') + data = {'address_scope': {'name': 'bar-address-scope'}} + self._test_update_address_scope(addr_scope['address_scope']['id'], + data, expected=data['address_scope']) + + def test_update_address_scope_shared_true_admin(self): + addr_scope = self._test_create_address_scope(name='foo-address-scope') + data = {'address_scope': {'shared': True}} + self._test_update_address_scope(addr_scope['address_scope']['id'], + data, admin=True, + expected=data['address_scope']) + + def test_update_address_scope_shared_true_non_admin(self): + addr_scope = self._test_create_address_scope(name='foo-address-scope') + data = {'address_scope': {'shared': True}} + res = self._test_update_address_scope( + addr_scope['address_scope']['id'], data, admin=False) + self.assertEqual(webob.exc.HTTPForbidden.code, res.status_int) + + def test_update_address_scope_shared_false_admin(self): + addr_scope = self._test_create_address_scope(name='foo-address-scope', + admin=True, shared=True) + data = {'address_scope': {'shared': False}} + res = self._test_update_address_scope( + addr_scope['address_scope']['id'], data, admin=True) + self.assertEqual(webob.exc.HTTPClientError.code, res.status_int) + + def test_get_address_scope(self): + addr_scope = self._test_create_address_scope(name='foo-address-scope') + req = self.new_show_request('address-scopes', + addr_scope['address_scope']['id']) + res = self.deserialize(self.fmt, req.get_response(self.ext_api)) + self.assertEqual(addr_scope['address_scope']['id'], + res['address_scope']['id']) + + def test_get_address_scope_different_tenants_not_shared(self): + addr_scope = self._test_create_address_scope(name='foo-address-scope') + req = self.new_show_request('address-scopes', + addr_scope['address_scope']['id']) + neutron_context = context.Context('', 'not-the-owner') + req.environ['neutron.context'] = neutron_context + res = req.get_response(self.ext_api) + self.assertEqual(webob.exc.HTTPNotFound.code, res.status_int) + + def test_get_address_scope_different_tenants_shared(self): + addr_scope = self._test_create_address_scope(name='foo-address-scope', + shared=True, admin=True) + req = self.new_show_request('address-scopes', + addr_scope['address_scope']['id']) + neutron_context = context.Context('', 'test-tenant-2') + req.environ['neutron.context'] = neutron_context + res = self.deserialize(self.fmt, req.get_response(self.ext_api)) + self.assertEqual(addr_scope['address_scope']['id'], + res['address_scope']['id']) + + def test_list_address_scopes(self): + self._test_create_address_scope(name='foo-address-scope') + self._test_create_address_scope(name='bar-address-scope') + res = self._list('address-scopes') + self.assertEqual(2, len(res['address_scopes'])) + + def test_list_address_scopes_different_tenants_shared(self): + self._test_create_address_scope(name='foo-address-scope', shared=True, + admin=True) + admin_res = self._list('address-scopes') + mortal_res = self._list( + 'address-scopes', + neutron_context=context.Context('', 'not-the-owner')) + self.assertEqual(1, len(admin_res['address_scopes'])) + self.assertEqual(1, len(mortal_res['address_scopes'])) + + def test_list_address_scopes_different_tenants_not_shared(self): + self._test_create_address_scope(name='foo-address-scope') + admin_res = self._list('address-scopes') + mortal_res = self._list( + 'address-scopes', + neutron_context=context.Context('', 'not-the-owner')) + self.assertEqual(1, len(admin_res['address_scopes'])) + self.assertEqual(0, len(mortal_res['address_scopes']))