Basic subnetpool CRUD

Enable creating, reading, updating, and deleting subnet pools via REST API.
Includes required changes to REST, model, alembic migrations, and unit tests.
Subnet pools carry a list of IPv4 or IPv6 prefixes from which a subnet can be
allocated. This will enable tenants to request a subnet from a pool rather
than being forced to explicitly provide their own CIDR's for their subnets.
This change simply enables managing the lifecycle of a subnet pool and does
not yet enable allocation of subnet prefixes from a pool.

Subnet pools can have their prefix bounds (min, max, default), name, and
prefix list updated. Changes to prefix bounds do not alter existing
allocations and will not be blocked by existing allocations. Prefix lists can
only be appended to. Prefixes cannot be removed from the pool once added.

ApiImpact
Partially-Implements: blueprint subnet-allocation
Change-Id: I88c6b15aab258069758f1a9423d6616ceb4a33c4
This commit is contained in:
Ryan Tidwell 2015-01-15 13:17:17 -08:00
parent 4cda123a86
commit 6f610d2d87
11 changed files with 991 additions and 18 deletions

View File

@ -8,6 +8,7 @@
"shared": "field:networks:shared=True",
"shared_firewalls": "field:firewalls:shared=True",
"shared_firewall_policies": "field:firewall_policies:shared=True",
"shared_subnetpools": "field:subnetpools:shared=True",
"external": "field:networks:router:external=True",
"default": "rule:admin_or_owner",
@ -16,6 +17,12 @@
"update_subnet": "rule:admin_or_network_owner",
"delete_subnet": "rule:admin_or_network_owner",
"create_subnetpool": "",
"create_subnetpool:shared": "rule:admin_only",
"get_subnetpool": "rule:admin_or_owner or rule:shared_subnetpools",
"update_subnetpool": "rule:admin_or_owner",
"delete_subnetpool": "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

@ -651,6 +651,8 @@ PORT = 'port'
PORTS = '%ss' % PORT
SUBNET = 'subnet'
SUBNETS = '%ss' % SUBNET
SUBNETPOOL = 'subnetpool'
SUBNETPOOLS = '%ss' % SUBNETPOOL
# Note: a default of ATTR_NOT_SPECIFIED indicates that an
# attribute is not required, but will be generated by the plugin
# if it is not specified. Particularly, a value of ATTR_NOT_SPECIFIED
@ -812,6 +814,59 @@ RESOURCE_ATTRIBUTE_MAP = {
'is_visible': False,
'required_by_policy': True,
'enforce_policy': True},
},
SUBNETPOOLS: {
'id': {'allow_post': False,
'allow_put': False,
'validate': {'type:uuid': None},
'is_visible': True,
'primary_key': True},
'name': {'allow_post': True,
'allow_put': True,
'validate': {'type:not_empty_string': None},
'is_visible': True},
'tenant_id': {'allow_post': True,
'allow_put': False,
'validate': {'type:string': None},
'required_by_policy': True,
'is_visible': True},
'prefixes': {'allow_post': True,
'allow_put': True,
'validate': {'type:subnet_list': None},
'is_visible': True},
'ip_version': {'allow_post': False,
'allow_put': False,
'is_visible': True},
'allow_overlap': {'allow_post': True,
'allow_put': False,
'default': False,
'convert_to': convert_to_boolean,
'is_visible': True},
'default_prefixlen': {'allow_post': True,
'allow_put': True,
'validate': {'type:non_negative': None},
'convert_to': convert_to_int,
'default': ATTR_NOT_SPECIFIED,
'is_visible': True},
'min_prefixlen': {'allow_post': True,
'allow_put': True,
'default': ATTR_NOT_SPECIFIED,
'validate': {'type:non_negative': None},
'convert_to': convert_to_int,
'is_visible': True},
'max_prefixlen': {'allow_post': True,
'allow_put': True,
'default': ATTR_NOT_SPECIFIED,
'validate': {'type:non_negative': None},
'convert_to': convert_to_int,
'is_visible': True},
SHARED: {'allow_post': True,
'allow_put': False,
'default': False,
'convert_to': convert_to_boolean,
'is_visible': True,
'required_by_policy': True,
'enforce_policy': True},
}
}
@ -824,6 +879,7 @@ RESOURCE_FOREIGN_KEYS = {
PLURALS = {NETWORKS: NETWORK,
PORTS: PORT,
SUBNETS: SUBNET,
SUBNETPOOLS: SUBNETPOOL,
'dns_nameservers': 'dns_nameserver',
'host_routes': 'host_route',
'allocation_pools': 'allocation_pool',

View File

@ -33,6 +33,7 @@ LOG = logging.getLogger(__name__)
RESOURCES = {'network': 'networks',
'subnet': 'subnets',
'subnetpool': 'subnetpools',
'port': 'ports'}
SUB_RESOURCES = {}
COLLECTION_ACTIONS = ['index', 'create']

View File

@ -79,6 +79,10 @@ class SubnetNotFound(NotFound):
message = _("Subnet %(subnet_id)s could not be found")
class SubnetPoolNotFound(NotFound):
message = _("Subnet pool %(subnetpool_id)s could not be found")
class PortNotFound(NotFound):
message = _("Port %(port_id)s could not be found")
@ -397,3 +401,28 @@ class FirewallInternalDriverError(NeutronException):
raise this exception to the agent
"""
message = _("%(driver)s: Internal driver error.")
class MissingMinSubnetPoolPrefix(BadRequest):
message = _("Unspecified minimum subnet pool prefix")
class EmptySubnetPoolPrefixList(BadRequest):
message = _("Empty subnet pool prefix list")
class PrefixVersionMismatch(BadRequest):
message = _("Cannot mix IPv4 and IPv6 prefixes in a subnet pool")
class UnsupportedMinSubnetPoolPrefix(BadRequest):
message = _("Prefix '%(prefix)s' not supported in IPv%(version)s pool")
class IllegalSubnetPoolPrefixBounds(BadRequest):
message = _("Illegal prefix bounds: %(prefix_type)s=%(prefixlen)s, "
"%(base_prefix_type)s=%(base_prefixlen)s")
class IllegalSubnetPoolPrefixUpdate(BadRequest):
message = _("Illegal update to prefixes: %(msg)s")

View File

@ -34,6 +34,7 @@ from neutron.db import models_v2
from neutron.db import sqlalchemyutils
from neutron.extensions import l3
from neutron.i18n import _LE, _LI
from neutron.ipam import subnet_alloc
from neutron import manager
from neutron import neutron_plugin_base_v2
from neutron.openstack.common import uuidutils
@ -96,6 +97,16 @@ class NeutronDbPluginV2(neutron_plugin_base_v2.NeutronPluginBaseV2,
raise n_exc.SubnetNotFound(subnet_id=id)
return subnet
def _get_subnetpool(self, context, id):
try:
return self._get_by_id(context, models_v2.SubnetPool, id)
except exc.NoResultFound:
raise n_exc.SubnetPoolNotFound(subnetpool_id=id)
def _get_all_subnetpools(self, context):
# NOTE(tidwellr): see note in _get_all_subnets()
return context.session.query(models_v2.SubnetPool).all()
def _get_port(self, context, id):
try:
port = self._get_by_id(context, models_v2.Port, id)
@ -818,6 +829,23 @@ class NeutronDbPluginV2(neutron_plugin_base_v2.NeutronPluginBaseV2,
self._apply_dict_extend_functions(attributes.SUBNETS, res, subnet)
return self._fields(res, fields)
def _make_subnetpool_dict(self, subnetpool, fields=None):
default_prefixlen = str(subnetpool['default_prefixlen'])
min_prefixlen = str(subnetpool['min_prefixlen'])
max_prefixlen = str(subnetpool['max_prefixlen'])
res = {'id': subnetpool['id'],
'name': subnetpool['name'],
'tenant_id': subnetpool['tenant_id'],
'default_prefixlen': default_prefixlen,
'min_prefixlen': min_prefixlen,
'max_prefixlen': max_prefixlen,
'shared': subnetpool['shared'],
'allow_overlap': subnetpool['allow_overlap'],
'prefixes': [prefix['cidr']
for prefix in subnetpool['prefixes']],
'ip_version': subnetpool['ip_version']}
return self._fields(res, fields)
def _make_port_dict(self, port, fields=None,
process_extensions=True):
res = {"id": port["id"],
@ -1298,6 +1326,120 @@ class NeutronDbPluginV2(neutron_plugin_base_v2.NeutronPluginBaseV2,
return self._get_collection_count(context, models_v2.Subnet,
filters=filters)
def _create_subnetpool_prefix(self, context, cidr, subnetpool_id):
prefix_args = {'cidr': cidr, 'subnetpool_id': subnetpool_id}
subnetpool_prefix = models_v2.SubnetPoolPrefix(**prefix_args)
context.session.add(subnetpool_prefix)
def create_subnetpool(self, context, subnetpool):
"""Create a subnetpool"""
sp = subnetpool['subnetpool']
sp_reader = subnet_alloc.SubnetPoolReader(sp)
tenant_id = self._get_tenant_id_for_create(context, sp)
with context.session.begin(subtransactions=True):
pool_args = {'tenant_id': tenant_id,
'id': sp_reader.id,
'name': sp_reader.name,
'ip_version': sp_reader.ip_version,
'default_prefixlen':
sp_reader.default_prefixlen,
'min_prefixlen': sp_reader.min_prefixlen,
'max_prefixlen': sp_reader.max_prefixlen,
'shared': sp_reader.shared,
'allow_overlap': sp_reader.allow_overlap}
subnetpool = models_v2.SubnetPool(**pool_args)
context.session.add(subnetpool)
for prefix in sp_reader.prefixes:
self._create_subnetpool_prefix(context,
prefix,
subnetpool.id)
return self._make_subnetpool_dict(subnetpool)
def _update_subnetpool_prefixes(self, context, prefix_list, id):
with context.session.begin(subtransactions=True):
context.session.query(models_v2.SubnetPoolPrefix).filter_by(
subnetpool_id=id).delete()
for prefix in prefix_list:
model_prefix = models_v2.SubnetPoolPrefix(cidr=prefix,
subnetpool_id=id)
context.session.add(model_prefix)
def _updated_subnetpool_dict(self, model, new_pool):
updated = {}
new_prefixes = new_pool.get('prefixes', attributes.ATTR_NOT_SPECIFIED)
orig_prefixes = [str(x.cidr) for x in model['prefixes']]
if new_prefixes is not attributes.ATTR_NOT_SPECIFIED:
orig_set = netaddr.IPSet(orig_prefixes)
new_set = netaddr.IPSet(new_prefixes)
if not orig_set.issubset(new_set):
msg = _("Existing prefixes must be "
"a subset of the new prefixes")
raise n_exc.IllegalSubnetPoolPrefixUpdate(msg=msg)
new_set.compact()
updated['prefixes'] = [str(x.cidr) for x in new_set.iter_cidrs()]
else:
updated['prefixes'] = orig_prefixes
for key in ['id', 'name', 'ip_version', 'min_prefixlen',
'max_prefixlen', 'default_prefixlen', 'allow_overlap',
'shared']:
self._write_key(key, updated, model, new_pool)
return updated
def _write_key(self, key, update, orig, new_dict):
new_val = new_dict.get(key, attributes.ATTR_NOT_SPECIFIED)
if new_val is not attributes.ATTR_NOT_SPECIFIED:
update[key] = new_dict[key]
else:
update[key] = orig[key]
def update_subnetpool(self, context, id, subnetpool):
"""Update a subnetpool"""
new_sp = subnetpool['subnetpool']
with context.session.begin(subtransactions=True):
orig_sp = self._get_subnetpool(context, id)
updated = self._updated_subnetpool_dict(orig_sp, new_sp)
updated['tenant_id'] = orig_sp.tenant_id
reader = subnet_alloc.SubnetPoolReader(updated)
orig_sp.update(self._filter_non_model_columns(
reader.subnetpool,
models_v2.SubnetPool))
self._update_subnetpool_prefixes(context,
reader.prefixes,
id)
for key in ['min_prefixlen', 'max_prefixlen', 'default_prefixlen']:
updated['key'] = str(updated[key])
return updated
def get_subnetpool(self, context, id, fields=None):
"""Retrieve a subnetpool."""
subnetpool = self._get_subnetpool(context, id)
return self._make_subnetpool_dict(subnetpool, fields)
def get_subnetpools(self, context, filters=None, fields=None,
sorts=None, limit=None, marker=None,
page_reverse=False):
"""Retrieve list of subnetpools."""
marker_obj = self._get_marker_obj(context, 'subnetpool', limit, marker)
collection = self._get_collection(context, models_v2.SubnetPool,
self._make_subnetpool_dict,
filters=filters, fields=fields,
sorts=sorts,
limit=limit,
marker_obj=marker_obj,
page_reverse=page_reverse)
return collection
def delete_subnetpool(self, context, id):
"""Delete a subnetpool."""
with context.session.begin(subtransactions=True):
subnetpool = self._get_subnetpool(context, id)
context.session.delete(subnetpool)
def _check_mac_addr_update(self, context, port, new_mac, device_owner):
if (device_owner and device_owner.startswith('network:')):
raise n_exc.UnsupportedPortDeviceOwner(

View File

@ -0,0 +1,62 @@
# Copyright 2015 OpenStack Foundation
#
# 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 for subnetpools
Revision ID: 51c54792158e
Revises: 341ee8a4ccb5
Create Date: 2015-01-27 13:07:50.713838
"""
# revision identifiers, used by Alembic.
revision = '51c54792158e'
down_revision = '1955efc66455'
from alembic import op
import sqlalchemy as sa
def upgrade():
op.create_table('subnetpools',
sa.Column('tenant_id',
sa.String(length=255),
nullable=True,
index=True),
sa.Column('id', sa.String(length=36), nullable=False),
sa.Column('name', sa.String(length=255), nullable=True),
sa.Column('ip_version', sa.Integer(), nullable=False),
sa.Column('default_prefixlen',
sa.Integer(),
nullable=False),
sa.Column('min_prefixlen', sa.Integer(), nullable=False),
sa.Column('max_prefixlen', sa.Integer(), nullable=False),
sa.Column('shared', sa.Boolean(), nullable=False),
sa.Column('allow_overlap', sa.Boolean(), nullable=False),
sa.PrimaryKeyConstraint('id'))
op.create_table('subnetpoolprefixes',
sa.Column('cidr', sa.String(length=64), nullable=False),
sa.Column('subnetpool_id',
sa.String(length=36),
nullable=False),
sa.ForeignKeyConstraint(['subnetpool_id'],
['subnetpools.id'],
ondelete='CASCADE'),
sa.PrimaryKeyConstraint('cidr', 'subnetpool_id'))
def downgrade():
op.drop_table('subnetpoolprefixes')
op.drop_table('subnetpools')

View File

@ -1 +1 @@
1955efc66455
51c54792158e

View File

@ -209,6 +209,36 @@ class Subnet(model_base.BASEV2, HasId, HasTenant):
name='ipv6_address_modes'), nullable=True)
class SubnetPoolPrefix(model_base.BASEV2):
"""Represents a neutron subnet pool prefix
"""
__tablename__ = 'subnetpoolprefixes'
cidr = sa.Column(sa.String(64), nullable=False, primary_key=True)
subnetpool_id = sa.Column(sa.String(36),
sa.ForeignKey('subnetpools.id'),
nullable=False,
primary_key=True)
class SubnetPool(model_base.BASEV2, HasId, HasTenant):
"""Represents a neutron subnet pool.
"""
name = sa.Column(sa.String(255))
ip_version = sa.Column(sa.Integer, nullable=False)
default_prefixlen = sa.Column(sa.Integer, nullable=False)
min_prefixlen = sa.Column(sa.Integer, nullable=False)
max_prefixlen = sa.Column(sa.Integer, nullable=False)
shared = sa.Column(sa.Boolean, nullable=False)
allow_overlap = sa.Column(sa.Boolean, nullable=False)
prefixes = orm.relationship(SubnetPoolPrefix,
backref='subnetpools',
cascade='all, delete, delete-orphan',
lazy='joined')
class Network(model_base.BASEV2, HasId, HasTenant):
"""Represents a v2 neutron network."""

View File

@ -0,0 +1,188 @@
# Copyright (c) 2015 Hewlett-Packard Co.
# 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 netaddr
from neutron.api.v2 import attributes
from neutron.common import constants
from neutron.common import exceptions as n_exc
from neutron.openstack.common import uuidutils
class SubnetPoolReader(object):
'''Class to assist with reading a subnetpool, loading defaults, and
inferring IP version from prefix list. Provides a common way of
reading a stored model or a create request with defaultable attributes.
'''
MIN_PREFIX_TYPE = 'min'
MAX_PREFIX_TYPE = 'max'
DEFAULT_PREFIX_TYPE = 'default'
_sp_helper = None
def __init__(self, subnetpool):
self._read_prefix_list(subnetpool)
self._sp_helper = SubnetPoolHelper()
self._read_id(subnetpool)
self._read_prefix_bounds(subnetpool)
self._read_attrs(subnetpool,
['tenant_id', 'name', 'allow_overlap', 'shared'])
self.subnetpool = {'id': self.id,
'name': self.name,
'tenant_id': self.tenant_id,
'prefixes': self.prefixes,
'min_prefix': self.min_prefix,
'min_prefixlen': self.min_prefixlen,
'max_prefix': self.max_prefix,
'max_prefixlen': self.max_prefixlen,
'default_prefix': self.default_prefix,
'default_prefixlen': self.default_prefixlen,
'allow_overlap': self.allow_overlap,
'shared': self.shared}
def _read_attrs(self, subnetpool, keys):
for key in keys:
setattr(self, key, subnetpool[key])
def _ip_version_from_cidr(self, cidr):
return netaddr.IPNetwork(cidr).version
def _prefixlen_from_cidr(self, cidr):
return netaddr.IPNetwork(cidr).prefixlen
def _read_id(self, subnetpool):
id = subnetpool.get('id', attributes.ATTR_NOT_SPECIFIED)
if id is attributes.ATTR_NOT_SPECIFIED:
id = uuidutils.generate_uuid()
self.id = id
def _read_prefix_bounds(self, subnetpool):
ip_version = self.ip_version
default_min = self._sp_helper.default_min_prefixlen(ip_version)
default_max = self._sp_helper.default_max_prefixlen(ip_version)
self._read_prefix_bound(self.MIN_PREFIX_TYPE,
subnetpool,
default_min)
self._read_prefix_bound(self.MAX_PREFIX_TYPE,
subnetpool,
default_max)
self._read_prefix_bound(self.DEFAULT_PREFIX_TYPE,
subnetpool,
self.min_prefixlen)
self._sp_helper.validate_min_prefixlen(self.min_prefixlen,
self.max_prefixlen)
self._sp_helper.validate_max_prefixlen(self.max_prefixlen,
ip_version)
self._sp_helper.validate_default_prefixlen(self.min_prefixlen,
self.max_prefixlen,
self.default_prefixlen)
def _read_prefix_bound(self, type, subnetpool, default_bound=None):
prefixlen_attr = type + '_prefixlen'
prefix_attr = type + '_prefix'
prefixlen = subnetpool.get(prefixlen_attr,
attributes.ATTR_NOT_SPECIFIED)
wildcard = self._sp_helper.wildcard(self.ip_version)
if prefixlen is attributes.ATTR_NOT_SPECIFIED and default_bound:
prefixlen = default_bound
if prefixlen is not attributes.ATTR_NOT_SPECIFIED:
prefix_cidr = '/'.join((wildcard,
str(prefixlen)))
setattr(self, prefix_attr, prefix_cidr)
setattr(self, prefixlen_attr, prefixlen)
def _read_prefix_list(self, subnetpool):
prefix_list = subnetpool['prefixes']
if not prefix_list:
raise n_exc.EmptySubnetPoolPrefixList()
ip_version = None
for prefix in prefix_list:
if not ip_version:
ip_version = netaddr.IPNetwork(prefix).version
elif netaddr.IPNetwork(prefix).version != ip_version:
raise n_exc.PrefixVersionMismatch()
self.ip_version = ip_version
self.prefixes = self._compact_subnetpool_prefix_list(prefix_list)
def _compact_subnetpool_prefix_list(self, prefix_list):
"""Compact any overlapping prefixes in prefix_list and return the
result
"""
ip_set = netaddr.IPSet()
for prefix in prefix_list:
ip_set.add(netaddr.IPNetwork(prefix))
ip_set.compact()
return [str(x.cidr) for x in ip_set.iter_cidrs()]
class SubnetPoolHelper(object):
PREFIX_VERSION_INFO = {4: {'max_prefixlen': constants.IPv4_BITS,
'wildcard': '0.0.0.0',
'default_min_prefixlen': 8},
6: {'max_prefixlen': constants.IPv6_BITS,
'wildcard': '::',
'default_min_prefixlen': 64}}
def validate_min_prefixlen(self, min_prefixlen, max_prefixlen):
if min_prefixlen < 0:
raise n_exc.UnsupportedMinSubnetPoolPrefix(prefix=min_prefixlen,
version=4)
if min_prefixlen > max_prefixlen:
raise n_exc.IllegalSubnetPoolPrefixBounds(
prefix_type='min_prefixlen',
prefixlen=min_prefixlen,
base_prefix_type='max_prefixlen',
base_prefixlen=max_prefixlen)
def validate_max_prefixlen(self, prefixlen, ip_version):
max = self.PREFIX_VERSION_INFO[ip_version]['max_prefixlen']
if prefixlen > max:
raise n_exc.IllegalSubnetPoolPrefixBounds(
prefix_type='max_prefixlen',
prefixlen=prefixlen,
base_prefix_type='ip_version_max',
base_prefixlen=max)
def validate_default_prefixlen(self,
min_prefixlen,
max_prefixlen,
default_prefixlen):
if default_prefixlen < min_prefixlen:
raise n_exc.IllegalSubnetPoolPrefixBounds(
prefix_type='default_prefixlen',
prefixlen=default_prefixlen,
base_prefix_type='min_prefixlen',
base_prefixlen=min_prefixlen)
if default_prefixlen > max_prefixlen:
raise n_exc.IllegalSubnetPoolPrefixBounds(
prefix_type='default_prefixlen',
prefixlen=default_prefixlen,
base_prefix_type='max_prefixlen',
base_prefixlen=max_prefixlen)
def wildcard(self, ip_version):
return self.PREFIX_VERSION_INFO[ip_version]['wildcard']
def default_max_prefixlen(self, ip_version):
return self.PREFIX_VERSION_INFO[ip_version]['max_prefixlen']
def default_min_prefixlen(self, ip_version):
return self.PREFIX_VERSION_INFO[ip_version]['default_min_prefixlen']

View File

@ -127,6 +127,45 @@ class NeutronPluginBaseV2(object):
"""
pass
def create_subnetpool(self, context, subnetpool):
"""Create a subnet pool.
:param context: neutron api request context
:param subnetpool: Dictionary representing the subnetpool to create.
"""
raise NotImplementedError()
def update_subnetpool(self, context, id, subnetpool):
"""Update a subnet pool.
:param context: neutron api request context
:param subnetpool: Dictionary representing the subnetpool attributes
to update.
"""
raise NotImplementedError()
def get_subnetpool(self, context, id, fields=None):
"""Show a subnet pool.
:param context: neutron api request context
:param id: The UUID of the subnetpool to show.
"""
raise NotImplementedError()
def get_subnetpools(self, context, filters=None, fields=None,
sorts=None, limit=None, marker=None,
page_reverse=False):
"""Retrieve list of subnet pools."""
raise NotImplementedError()
def delete_subnetpool(self, context, id):
"""Delete a subnet pool.
:param context: neutron api request context
:param id: The UUID of the subnet pool to delete.
"""
raise NotImplementedError()
@abc.abstractmethod
def create_network(self, context, network):
"""Create a network.

View File

@ -18,6 +18,7 @@ import copy
import itertools
import mock
import netaddr
from oslo_config import cfg
from oslo_utils import importutils
from testtools import matchers
@ -357,6 +358,23 @@ class NeutronDbPluginV2TestCase(testlib_api.WebTestCase,
kwargs.update({'override': overrides})
return self._create_bulk(fmt, number, 'subnet', base_data, **kwargs)
def _create_subnetpool(self, fmt, prefixes,
expected_res_status=None, admin=False, **kwargs):
subnetpool = {'subnetpool': {'prefixes': prefixes}}
for k, v in kwargs.items():
subnetpool['subnetpool'][k] = str(v)
api = self._api_for_resource('subnetpools')
subnetpools_req = self.new_create_request('subnetpools',
subnetpool, fmt)
if not admin:
neutron_context = context.Context('', kwargs['tenant_id'])
subnetpools_req.environ['neutron.context'] = neutron_context
subnetpool_res = subnetpools_req.get_response(api)
if expected_res_status:
self.assertEqual(subnetpool_res.status_int, expected_res_status)
return subnetpool_res
def _create_port(self, fmt, net_id, expected_res_status=None,
arg_list=None, **kwargs):
data = {'port': {'network_id': net_id,
@ -447,6 +465,18 @@ class NeutronDbPluginV2TestCase(testlib_api.WebTestCase,
raise webob.exc.HTTPClientError(code=res.status_int)
return self.deserialize(fmt, res)
def _make_subnetpool(self, fmt, prefixes, admin=False, **kwargs):
res = self._create_subnetpool(fmt,
prefixes,
None,
admin,
**kwargs)
# Things can go wrong - raise HTTP exc with res code only
# so it can be caught by unit tests
if res.status_int >= webob.exc.HTTPClientError.code:
raise webob.exc.HTTPClientError(code=res.status_int)
return self.deserialize(fmt, res)
def _make_port(self, fmt, net_id, expected_res_status=None, **kwargs):
res = self._create_port(fmt, net_id, expected_res_status, **kwargs)
# Things can go wrong - raise HTTP exc with res code only
@ -456,7 +486,7 @@ class NeutronDbPluginV2TestCase(testlib_api.WebTestCase,
return self.deserialize(fmt, res)
def _api_for_resource(self, resource):
if resource in ['networks', 'subnets', 'ports']:
if resource in ['networks', 'subnets', 'ports', 'subnetpools']:
return self.api
else:
return self.ext_api
@ -538,8 +568,8 @@ class NeutronDbPluginV2TestCase(testlib_api.WebTestCase,
neutron_context=neutron_context,
query_params=query_params)
resource = resource.replace('-', '_')
self.assertEqual(sorted([i['id'] for i in res['%ss' % resource]]),
sorted([i[resource]['id'] for i in items]))
self.assertItemsEqual([i['id'] for i in res['%ss' % resource]],
[i[resource]['id'] for i in items])
@contextlib.contextmanager
def network(self, name='net1',
@ -578,6 +608,14 @@ class NeutronDbPluginV2TestCase(testlib_api.WebTestCase,
ipv6_address_mode=ipv6_address_mode)
yield subnet
@contextlib.contextmanager
def subnetpool(self, prefixes, admin=False, **kwargs):
subnetpool = self._make_subnetpool(self.fmt,
prefixes,
admin,
**kwargs)
yield subnetpool
@contextlib.contextmanager
def port(self, subnet=None, fmt=None, **kwargs):
with optional_ctx(subnet, self.subnet) as subnet_to_use:
@ -676,6 +714,27 @@ class NeutronDbPluginV2TestCase(testlib_api.WebTestCase,
expected_res.reverse()
self.assertEqual(expected_res, [n['id'] for n in item_res])
def _compare_resource(self, observed_res, expected_res, res_name):
'''
Compare the observed and expected resources (ie compare subnets)
'''
for k in expected_res:
self.assertIn(k, observed_res[res_name])
if isinstance(expected_res[k], list):
self.assertEqual(sorted(observed_res[res_name][k]),
sorted(expected_res[k]))
else:
self.assertEqual(observed_res[res_name][k], expected_res[k])
def _validate_resource(self, resource, keys, res_name):
for k in keys:
self.assertIn(k, resource[res_name])
if isinstance(keys[k], list):
self.assertEqual(sorted(resource[res_name][k]),
sorted(keys[k]))
else:
self.assertEqual(resource[res_name][k], keys[k])
class TestBasicGet(NeutronDbPluginV2TestCase):
@ -2535,22 +2594,10 @@ class TestSubnetsV2(NeutronDbPluginV2TestCase):
keys.setdefault('enable_dhcp', True)
with self.subnet(network=network, **keys) as subnet:
# verify the response has each key with the correct value
for k in keys:
self.assertIn(k, subnet['subnet'])
if isinstance(keys[k], list):
self.assertEqual(sorted(subnet['subnet'][k]),
sorted(keys[k]))
else:
self.assertEqual(subnet['subnet'][k], keys[k])
self._validate_resource(subnet, keys, 'subnet')
# verify the configured validations are correct
if expected:
for k in expected:
self.assertIn(k, subnet['subnet'])
if isinstance(expected[k], list):
self.assertEqual(sorted(subnet['subnet'][k]),
sorted(expected[k]))
else:
self.assertEqual(subnet['subnet'][k], expected[k])
self._compare_resource(subnet, expected, 'subnet')
self._delete('subnets', subnet['subnet']['id'])
return subnet
@ -4246,6 +4293,378 @@ class TestSubnetsV2(NeutronDbPluginV2TestCase):
expected_code=webob.exc.HTTPConflict.code)
class TestSubnetPoolsV2(NeutronDbPluginV2TestCase):
_POOL_NAME = 'test-pool'
def _test_create_subnetpool(self, prefixes, expected=None,
admin=False, **kwargs):
keys = kwargs.copy()
keys.setdefault('tenant_id', self._tenant_id)
with self.subnetpool(prefixes, admin, **keys) as subnetpool:
self._validate_resource(subnetpool, keys, 'subnetpool')
if expected:
self._compare_resource(subnetpool, expected, 'subnetpool')
return subnetpool
def _validate_default_prefix(self, prefix, subnetpool):
self.assertEqual(subnetpool['subnetpool']['default_prefixlen'], prefix)
def _validate_min_prefix(self, prefix, subnetpool):
self.assertEqual(subnetpool['subnetpool']['min_prefixlen'], prefix)
def _validate_max_prefix(self, prefix, subnetpool):
self.assertEqual(subnetpool['subnetpool']['max_prefixlen'], prefix)
def test_create_subnetpool_empty_prefix_list(self):
self.assertRaises(webob.exc.HTTPClientError,
self._test_create_subnetpool,
[],
name=self._POOL_NAME,
tenant_id=self._tenant_id,
min_prefixlen='21')
def test_create_subnetpool_ipv4_24_with_defaults(self):
subnet = netaddr.IPNetwork('10.10.10.0/24')
subnetpool = self._test_create_subnetpool([subnet.cidr],
name=self._POOL_NAME,
tenant_id=self._tenant_id,
min_prefixlen='21')
self._validate_default_prefix('21', subnetpool)
self._validate_min_prefix('21', subnetpool)
def test_create_subnetpool_ipv4_21_with_defaults(self):
subnet = netaddr.IPNetwork('10.10.10.0/21')
subnetpool = self._test_create_subnetpool([subnet.cidr],
name=self._POOL_NAME,
tenant_id=self._tenant_id,
min_prefixlen='21')
self._validate_default_prefix('21', subnetpool)
self._validate_min_prefix('21', subnetpool)
def test_create_subnetpool_ipv4_default_prefix_too_small(self):
subnet = netaddr.IPNetwork('10.10.10.0/21')
self.assertRaises(webob.exc.HTTPClientError,
self._test_create_subnetpool,
[subnet.cidr],
tenant_id=self._tenant_id,
name=self._POOL_NAME,
min_prefixlen='21',
default_prefixlen='20')
def test_create_subnetpool_ipv4_default_prefix_too_large(self):
subnet = netaddr.IPNetwork('10.10.10.0/21')
self.assertRaises(webob.exc.HTTPClientError,
self._test_create_subnetpool,
[subnet.cidr],
tenant_id=self._tenant_id,
name=self._POOL_NAME,
max_prefixlen=24,
default_prefixlen='32')
def test_create_subnetpool_ipv4_default_prefix_bounds(self):
subnet = netaddr.IPNetwork('10.10.10.0/21')
subnetpool = self._test_create_subnetpool([subnet.cidr],
tenant_id=self._tenant_id,
name=self._POOL_NAME)
self._validate_min_prefix('8', subnetpool)
self._validate_default_prefix('8', subnetpool)
self._validate_max_prefix('32', subnetpool)
def test_create_subnetpool_ipv6_default_prefix_bounds(self):
subnet = netaddr.IPNetwork('fe80::/48')
subnetpool = self._test_create_subnetpool([subnet.cidr],
tenant_id=self._tenant_id,
name=self._POOL_NAME)
self._validate_min_prefix('64', subnetpool)
self._validate_default_prefix('64', subnetpool)
self._validate_max_prefix('128', subnetpool)
def test_create_subnetpool_ipv4_supported_default_prefix(self):
subnet = netaddr.IPNetwork('10.10.10.0/21')
subnetpool = self._test_create_subnetpool([subnet.cidr],
tenant_id=self._tenant_id,
name=self._POOL_NAME,
min_prefixlen='21',
default_prefixlen='26')
self._validate_default_prefix('26', subnetpool)
def test_create_subnetpool_ipv4_supported_min_prefix(self):
subnet = netaddr.IPNetwork('10.10.10.0/24')
subnetpool = self._test_create_subnetpool([subnet.cidr],
tenant_id=self._tenant_id,
name=self._POOL_NAME,
min_prefixlen='26')
self._validate_min_prefix('26', subnetpool)
self._validate_default_prefix('26', subnetpool)
def test_create_subnetpool_ipv4_default_prefix_smaller_than_min(self):
subnet = netaddr.IPNetwork('10.10.10.0/21')
self.assertRaises(webob.exc.HTTPClientError,
self._test_create_subnetpool,
[subnet.cidr],
tenant_id=self._tenant_id,
name=self._POOL_NAME,
default_prefixlen='22',
min_prefixlen='23')
def test_create_subnetpool_mixed_ip_version(self):
subnet_v4 = netaddr.IPNetwork('10.10.10.0/21')
subnet_v6 = netaddr.IPNetwork('fe80::/48')
self.assertRaises(webob.exc.HTTPClientError,
self._test_create_subnetpool,
[subnet_v4.cidr, subnet_v6.cidr],
tenant_id=self._tenant_id,
name=self._POOL_NAME,
min_prefixlen='21')
def test_create_subnetpool_ipv6_with_defaults(self):
subnet = netaddr.IPNetwork('fe80::/48')
subnetpool = self._test_create_subnetpool([subnet.cidr],
tenant_id=self._tenant_id,
name=self._POOL_NAME,
min_prefixlen='48')
self._validate_default_prefix('48', subnetpool)
self._validate_min_prefix('48', subnetpool)
def test_get_subnetpool(self):
subnetpool = self._test_create_subnetpool(['10.10.10.0/24'],
tenant_id=self._tenant_id,
name=self._POOL_NAME,
min_prefixlen='24')
req = self.new_show_request('subnetpools',
subnetpool['subnetpool']['id'])
res = self.deserialize(self.fmt, req.get_response(self.api))
self.assertEqual(subnetpool['subnetpool']['id'],
res['subnetpool']['id'])
def test_get_subnetpool_different_tenants_not_shared(self):
subnetpool = self._test_create_subnetpool(['10.10.10.0/24'],
shared=False,
tenant_id=self._tenant_id,
name=self._POOL_NAME,
min_prefixlen='24')
req = self.new_show_request('subnetpools',
subnetpool['subnetpool']['id'])
neutron_context = context.Context('', 'not-the-owner')
req.environ['neutron.context'] = neutron_context
res = req.get_response(self.api)
self.assertEqual(res.status_int, 404)
def test_get_subnetpool_different_tenants_shared(self):
subnetpool = self._test_create_subnetpool(['10.10.10.0/24'],
None,
True,
name=self._POOL_NAME,
min_prefixlen='24',
shared=True)
req = self.new_show_request('subnetpools',
subnetpool['subnetpool']['id'])
neutron_context = context.Context('', self._tenant_id)
req.environ['neutron.context'] = neutron_context
res = self.deserialize(self.fmt, req.get_response(self.api))
self.assertEqual(subnetpool['subnetpool']['id'],
res['subnetpool']['id'])
def test_list_subnetpools_different_tenants_shared(self):
self._test_create_subnetpool(['10.10.10.0/24'],
None,
True,
name=self._POOL_NAME,
min_prefixlen='24',
shared=True)
admin_res = self._list('subnetpools')
mortal_res = self._list('subnetpools',
neutron_context=context.Context('', 'not-the-owner'))
self.assertEqual(len(admin_res['subnetpools']), 1)
self.assertEqual(len(mortal_res['subnetpools']), 1)
def test_list_subnetpools_different_tenants_not_shared(self):
self._test_create_subnetpool(['10.10.10.0/24'],
None,
True,
name=self._POOL_NAME,
min_prefixlen='24',
shared=False)
admin_res = self._list('subnetpools')
mortal_res = self._list('subnetpools',
neutron_context=context.Context('', 'not-the-owner'))
self.assertEqual(len(admin_res['subnetpools']), 1)
self.assertEqual(len(mortal_res['subnetpools']), 0)
def test_delete_subnetpool(self):
subnetpool = self._test_create_subnetpool(['10.10.10.0/24'],
tenant_id=self._tenant_id,
name=self._POOL_NAME,
min_prefixlen='24')
req = self.new_delete_request('subnetpools',
subnetpool['subnetpool']['id'])
res = req.get_response(self.api)
self.assertEqual(res.status_int, 204)
def test_delete_nonexistent_subnetpool(self):
req = self.new_delete_request('subnetpools',
'aaaaaaaa-aaaa-aaaa-aaaa-aaaaaaaaaaaa')
res = req.get_response(self._api_for_resource('subnetpools'))
self.assertEqual(res.status_int, 404)
def test_update_subnetpool_prefix_list_append(self):
initial_subnetpool = self._test_create_subnetpool(['10.10.8.0/21'],
tenant_id=self._tenant_id,
name=self._POOL_NAME,
min_prefixlen='24')
data = {'subnetpool': {'prefixes': ['10.10.8.0/21', '3.3.3.0/24',
'2.2.2.0/24']}}
req = self.new_update_request('subnetpools', data,
initial_subnetpool['subnetpool']['id'])
api = self._api_for_resource('subnetpools')
res = self.deserialize(self.fmt, req.get_response(api))
self.assertItemsEqual(res['subnetpool']['prefixes'],
['10.10.8.0/21', '3.3.3.0/24', '2.2.2.0/24'])
def test_update_subnetpool_prefix_list_compaction(self):
initial_subnetpool = self._test_create_subnetpool(['10.10.10.0/24'],
tenant_id=self._tenant_id,
name=self._POOL_NAME,
min_prefixlen='24')
data = {'subnetpool': {'prefixes': ['10.10.10.0/24',
'10.10.11.0/24']}}
req = self.new_update_request('subnetpools', data,
initial_subnetpool['subnetpool']['id'])
api = self._api_for_resource('subnetpools')
res = self.deserialize(self.fmt, req.get_response(api))
self.assertItemsEqual(res['subnetpool']['prefixes'],
['10.10.10.0/23'])
def test_illegal_subnetpool_prefix_list_update(self):
initial_subnetpool = self._test_create_subnetpool(['10.10.10.0/24'],
tenant_id=self._tenant_id,
name=self._POOL_NAME,
min_prefixlen='24')
data = {'subnetpool': {'prefixes': ['10.10.11.0/24']}}
req = self.new_update_request('subnetpools', data,
initial_subnetpool['subnetpool']['id'])
api = self._api_for_resource('subnetpools')
res = req.get_response(api)
self.assertEqual(res.status_int, 400)
def test_update_subnetpool_default_prefix(self):
initial_subnetpool = self._test_create_subnetpool(['10.10.8.0/21'],
tenant_id=self._tenant_id,
name=self._POOL_NAME,
min_prefixlen='24')
data = {'subnetpool': {'default_prefixlen': '26'}}
req = self.new_update_request('subnetpools', data,
initial_subnetpool['subnetpool']['id'])
api = self._api_for_resource('subnetpools')
res = self.deserialize(self.fmt, req.get_response(api))
self.assertEqual(res['subnetpool']['default_prefixlen'], 26)
def test_update_subnetpool_min_prefix(self):
initial_subnetpool = self._test_create_subnetpool(['10.10.10.0/24'],
tenant_id=self._tenant_id,
name=self._POOL_NAME,
min_prefixlen='24')
data = {'subnetpool': {'min_prefixlen': '21'}}
req = self.new_update_request('subnetpools', data,
initial_subnetpool['subnetpool']['id'])
res = self.deserialize(self.fmt, req.get_response(self.api))
self.assertEqual(res['subnetpool']['min_prefixlen'], 21)
def test_update_subnetpool_min_prefix_larger_than_max(self):
initial_subnetpool = self._test_create_subnetpool(['10.10.10.0/24'],
tenant_id=self._tenant_id,
name=self._POOL_NAME,
min_prefixlen='21',
max_prefixlen='24')
data = {'subnetpool': {'min_prefixlen': '28'}}
req = self.new_update_request('subnetpools', data,
initial_subnetpool['subnetpool']['id'])
res = req.get_response(self.api)
self.assertEqual(res.status_int, 400)
def test_update_subnetpool_max_prefix(self):
initial_subnetpool = self._test_create_subnetpool(['10.10.10.0/24'],
tenant_id=self._tenant_id,
name=self._POOL_NAME,
min_prefixlen='21',
max_prefixlen='24')
data = {'subnetpool': {'max_prefixlen': '26'}}
req = self.new_update_request('subnetpools', data,
initial_subnetpool['subnetpool']['id'])
res = self.deserialize(self.fmt, req.get_response(self.api))
self.assertEqual(res['subnetpool']['max_prefixlen'], 26)
def test_update_subnetpool_max_prefix_less_than_min(self):
initial_subnetpool = self._test_create_subnetpool(['10.10.10.0/24'],
tenant_id=self._tenant_id,
name=self._POOL_NAME,
min_prefixlen='24')
data = {'subnetpool': {'max_prefixlen': '21'}}
req = self.new_update_request('subnetpools', data,
initial_subnetpool['subnetpool']['id'])
res = req.get_response(self.api)
self.assertEqual(res.status_int, 400)
def test_update_subnetpool_max_prefix_less_than_default(self):
initial_subnetpool = self._test_create_subnetpool(['10.10.10.0/24'],
tenant_id=self._tenant_id,
name=self._POOL_NAME,
min_prefixlen='21',
default_prefixlen='24')
data = {'subnetpool': {'max_prefixlen': '22'}}
req = self.new_update_request('subnetpools', data,
initial_subnetpool['subnetpool']['id'])
res = req.get_response(self.api)
self.assertEqual(res.status_int, 400)
def test_update_subnetpool_default_prefix_less_than_min(self):
initial_subnetpool = self._test_create_subnetpool(['10.10.10.0/24'],
tenant_id=self._tenant_id,
name=self._POOL_NAME,
min_prefixlen='21')
data = {'subnetpool': {'default_prefixlen': '20'}}
req = self.new_update_request('subnetpools', data,
initial_subnetpool['subnetpool']['id'])
res = req.get_response(self.api)
self.assertEqual(res.status_int, 400)
def test_update_subnetpool_default_prefix_larger_than_max(self):
initial_subnetpool = self._test_create_subnetpool(['10.10.10.0/24'],
tenant_id=self._tenant_id,
name=self._POOL_NAME,
min_prefixlen='21',
max_prefixlen='24')
data = {'subnetpool': {'default_prefixlen': '28'}}
req = self.new_update_request('subnetpools', data,
initial_subnetpool['subnetpool']['id'])
res = req.get_response(self.api)
self.assertEqual(res.status_int, 400)
def test_update_subnetpool_prefix_list_mixed_ip_version(self):
initial_subnetpool = self._test_create_subnetpool(['10.10.10.0/24'],
tenant_id=self._tenant_id,
name=self._POOL_NAME,
min_prefixlen='24')
data = {'subnetpool': {'prefixes': ['fe80::/48']}}
req = self.new_update_request('subnetpools', data,
initial_subnetpool['subnetpool']['id'])
res = req.get_response(self.api)
self.assertEqual(res.status_int, 400)
class DbModelTestCase(base.BaseTestCase):
"""DB model tests."""
def test_repr(self):