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 <rktidwell85@gmail.com>
Co-Authored-By: Numan Siddique <nusiddiq@redhat.com>
Change-Id: Icabdd22577cfda0e1fbf6042e4b05b8080e54fdb
Partially-implements:  blueprint address-scopes
This commit is contained in:
vikram.choudhary 2015-06-09 19:55:59 +05:30 committed by Numan Siddique
parent f9787afc34
commit cbd95318ad
8 changed files with 540 additions and 3 deletions

View File

@ -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",

View File

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

View File

@ -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'))

View File

@ -1 +1 @@
599c6a226151
52c5312f6baf

View File

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

View File

@ -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):

View File

@ -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",

View File

@ -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']))