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 <rktidwell85@gmail.com>
Co-Authored-By: Numan Siddique <nusiddiq@redhat.com>
Change-Id: Icabdd22577cfda0e1fbf6042e4b05b8080e54fdb
Partially-implements: blueprint address-scopes
"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",
"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",
--- /dev/null
+# 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)
--- /dev/null
+# 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'))
-599c6a226151
+52c5312f6baf
--- /dev/null
+# 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()
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
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.
"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):
"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",
"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",
--- /dev/null
+# 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']))