Associate subnets to segments through subnet API

Change-Id: Ia1084a94ac659332c126eb9d4787b04a89a4ba90
DocImpact: Need to add segment_id to API docs
Partially-Implements: blueprint routed-networks
This commit is contained in:
Carl Baldwin 2016-02-09 16:39:01 -07:00
parent 501b14f4ca
commit f494de47fc
15 changed files with 241 additions and 22 deletions

View File

@ -16,7 +16,9 @@
"default": "rule:admin_or_owner",
"create_subnet": "rule:admin_or_network_owner",
"create_subnet:segment_id": "rule:admin_only",
"get_subnet": "rule:admin_or_owner or rule:shared",
"get_subnet:segment_id": "rule:admin_only",
"update_subnet": "rule:admin_or_network_owner",
"delete_subnet": "rule:admin_or_network_owner",

View File

@ -32,7 +32,10 @@ from neutron.common import ipv6_utils
from neutron.common import utils as common_utils
from neutron.db import db_base_plugin_common
from neutron.db import models_v2
from neutron.db import segments_db
from neutron.extensions import segment
from neutron.ipam import utils as ipam_utils
from neutron.services.segments import exceptions as segment_exc
LOG = logging.getLogger(__name__)
@ -308,6 +311,25 @@ class IpamBackendMixin(db_base_plugin_common.DbBasePluginCommon):
msg = _('Exceeded maximum amount of fixed ips per port.')
raise exc.InvalidInput(error_message=msg)
def _validate_segment(self, context, network_id, segment_id):
query = context.session.query(models_v2.Subnet.segment_id)
query = query.filter(models_v2.Subnet.network_id == network_id)
associated_segments = set(row.segment_id for row in query)
if None in associated_segments and len(associated_segments) > 1:
raise segment_exc.SubnetsNotAllAssociatedWithSegments(
network_id=network_id)
if segment_id:
query = context.session.query(segments_db.NetworkSegment)
query = query.filter(segments_db.NetworkSegment.id == segment_id)
segment = query.one()
if segment.network_id != network_id:
raise segment_exc.NetworkIdsDontMatch(
subnet_network=network_id,
segment_id=segment_id)
if segment.is_dynamic:
raise segment_exc.SubnetCantAssociateToDynamicSegment()
def _get_subnet_for_fixed_ip(self, context, fixed, subnets):
# Subnets are all the subnets belonging to the same network.
if not subnets:
@ -447,7 +469,14 @@ class IpamBackendMixin(db_base_plugin_common.DbBasePluginCommon):
subnet_args['ip_version'])
subnet = models_v2.Subnet(**subnet_args)
context.session.add(subnet)
segment_id = subnet_args.get('segment_id')
try:
context.session.add(subnet)
context.session.flush()
except db_exc.DBReferenceError:
raise segment_exc.SegmentNotFound(segment_id=segment_id)
self._validate_segment(context, network['id'], segment_id)
# NOTE(changzhi) Store DNS nameservers with order into DB one
# by one when create subnet with DNS nameservers
if validators.is_attr_set(dns_nameservers):
@ -502,3 +531,17 @@ class IpamBackendMixin(db_base_plugin_common.DbBasePluginCommon):
else:
fixed_ip_list.append({'subnet_id': subnet['id']})
return fixed_ip_list
def _ipam_get_subnets(self, context, network_id, segment_id):
query = self._get_collection_query(context, models_v2.Subnet)
query = query.filter(models_v2.Subnet.network_id == network_id)
query = query.filter(models_v2.Subnet.segment_id == segment_id)
subnets = [self._make_subnet_dict(c, context=context) for c in query]
return subnets
def _make_subnet_args(self, detail, subnet, subnetpool_id):
args = super(IpamBackendMixin, self)._make_subnet_args(
detail, subnet, subnetpool_id)
if validators.is_attr_set(subnet.get(segment.SEGMENT_ID)):
args['segment_id'] = subnet[segment.SEGMENT_ID]
return args

View File

@ -318,8 +318,8 @@ class IpamNonPluggableBackend(ipam_backend_mixin.IpamBackendMixin):
added = []
changes = self._get_changed_ips_for_port(context, original_ips,
new_ips, device_owner)
net_id_filter = {'network_id': [network_id]}
subnets = self._get_subnets(context, filters=net_id_filter)
subnets = self._ipam_get_subnets(
context, network_id=network_id, segment_id=None)
# Check if the IP's to add are OK
to_add = self._test_fixed_ips_for_port(context, network_id,
changes.add, device_owner,
@ -351,8 +351,8 @@ class IpamNonPluggableBackend(ipam_backend_mixin.IpamBackendMixin):
a subnet_id then allocate an IP address accordingly.
"""
p = port['port']
net_id_filter = {'network_id': [p['network_id']]}
subnets = self._get_subnets(context, filters=net_id_filter)
subnets = self._ipam_get_subnets(
context, network_id=p['network_id'], segment_id=None)
v4, v6_stateful, v6_stateless = self._classify_subnets(
context, subnets)

View File

@ -203,8 +203,8 @@ class IpamPluggableBackend(ipam_backend_mixin.IpamBackendMixin):
a subnet_id then allocate an IP address accordingly.
"""
p = port['port']
net_id_filter = {'network_id': [p['network_id']]}
subnets = self._get_subnets(context, filters=net_id_filter)
subnets = self._ipam_get_subnets(
context, network_id=p['network_id'], segment_id=None)
v4, v6_stateful, v6_stateless = self._classify_subnets(
context, subnets)
@ -283,8 +283,8 @@ class IpamPluggableBackend(ipam_backend_mixin.IpamBackendMixin):
removed = []
changes = self._get_changed_ips_for_port(
context, original_ips, new_ips, port['device_owner'])
net_id_filter = {'network_id': [port['network_id']]}
subnets = self._get_subnets(context, filters=net_id_filter)
subnets = self._ipam_get_subnets(
context, network_id=port['network_id'], segment_id=None)
# Check if the IP's to add are OK
to_add = self._test_fixed_ips_for_port(
context, port['network_id'], changes.add,

View File

@ -1 +1 @@
89ab9a816d70
c879c5e1ee90

View File

@ -0,0 +1,30 @@
# Copyright (c) 2016 Hewlett Packard Enterprise Development Company, L.P.
#
# 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.
#
"""Add segment_id to subnet """
# revision identifiers, used by Alembic.
revision = 'c879c5e1ee90'
down_revision = '89ab9a816d70'
from alembic import op
import sqlalchemy as sa
def upgrade():
op.add_column('subnets',
sa.Column('segment_id', sa.String(length=36), nullable=True))
op.create_foreign_key(
None, 'subnets', 'networksegments', ['segment_id'], ['id'])

View File

@ -185,6 +185,8 @@ class Subnet(model_base.HasStandardAttributes, model_base.BASEV2,
name = sa.Column(sa.String(attr.NAME_MAX_LEN))
network_id = sa.Column(sa.String(36), sa.ForeignKey('networks.id'))
# Added by the segments service plugin
segment_id = sa.Column(sa.String(36), sa.ForeignKey('networksegments.id'))
subnetpool_id = sa.Column(sa.String(36), index=True)
# NOTE: Explicitly specify join conditions for the relationship because
# subnetpool_id in subnet might be 'prefix_delegation' when the IPv6 Prefix

View File

@ -63,6 +63,13 @@ RESOURCE_ATTRIBUTE_MAP = {
'convert_to': attributes.convert_to_int,
'is_visible': True},
},
attributes.SUBNETS: {
SEGMENT_ID: {'allow_post': True,
'allow_put': False,
'default': None,
'validate': {'type:uuid_or_none': None},
'is_visible': True, },
},
}

View File

@ -22,11 +22,21 @@ from oslo_log import helpers as log_helpers
from oslo_utils import uuidutils
from sqlalchemy.orm import exc
from neutron.api.v2 import attributes
from neutron.db import common_db_mixin
from neutron.db import segments_db as db
from neutron.services.segments import exceptions
def _extend_subnet_dict_binding(plugin, subnet_res, subnet_db):
subnet_res['segment_id'] = subnet_db.get('segment_id')
# Register dict extend functions for subnets
common_db_mixin.CommonDbMixin.register_dict_extend_funcs(
attributes.SUBNETS, [_extend_subnet_dict_binding])
class SegmentDbMixin(common_db_mixin.CommonDbMixin):
"""Mixin class to add segment."""

View File

@ -21,3 +21,18 @@ from neutron_lib import exceptions
class SegmentNotFound(exceptions.NotFound):
message = _("Segment %(segment_id)s could not be found.")
class SubnetsNotAllAssociatedWithSegments(exceptions.BadRequest):
message = _("All of the subnets on network '%(network_id)s' must either "
"all be associated with segments or all not associated with "
"any segment.")
class SubnetCantAssociateToDynamicSegment(exceptions.BadRequest):
message = _("A subnet cannot be associated with a dynamic segment.")
class NetworkIdsDontMatch(exceptions.BadRequest):
message = _("The subnet's network id, '%(subnet_network)s', doesn't match "
"the network_id of segment '%(segment_id)s'")

View File

@ -16,7 +16,9 @@
"default": "rule:admin_or_owner",
"create_subnet": "rule:admin_or_network_owner",
"create_subnet:segment_id": "rule:admin_only",
"get_subnet": "rule:admin_or_owner or rule:shared",
"get_subnet:segment_id": "rule:admin_only",
"update_subnet": "rule:admin_or_network_owner",
"delete_subnet": "rule:admin_or_network_owner",

View File

@ -44,6 +44,7 @@ from neutron.common import utils
from neutron import context
from neutron.db import api as db_api
from neutron.db import db_base_plugin_common
from neutron.db import ipam_backend_mixin
from neutron.db import ipam_non_pluggable_backend as non_ipam
from neutron.db import l3_db
from neutron.db import models_v2
@ -324,7 +325,7 @@ class NeutronDbPluginV2TestCase(testlib_api.WebTestCase):
if cidr:
data['subnet']['cidr'] = cidr
for arg in ('ip_version', 'tenant_id', 'subnetpool_id', 'prefixlen',
'enable_dhcp', 'allocation_pools',
'enable_dhcp', 'allocation_pools', 'segment_id',
'dns_nameservers', 'host_routes',
'shared', 'ipv6_ra_mode', 'ipv6_address_mode'):
# Arg must be present and not null (but can be false)
@ -449,11 +450,12 @@ class NeutronDbPluginV2TestCase(testlib_api.WebTestCase):
allocation_pools=None, ip_version=4, enable_dhcp=True,
dns_nameservers=None, host_routes=None, shared=None,
ipv6_ra_mode=None, ipv6_address_mode=None,
tenant_id=None, set_context=False):
tenant_id=None, set_context=False, segment_id=None):
res = self._create_subnet(fmt,
net_id=network['network']['id'],
cidr=cidr,
subnetpool_id=subnetpool_id,
segment_id=segment_id,
gateway_ip=gateway,
tenant_id=(tenant_id or
network['network']['tenant_id']),
@ -607,6 +609,7 @@ class NeutronDbPluginV2TestCase(testlib_api.WebTestCase):
gateway_ip=constants.ATTR_NOT_SPECIFIED,
cidr='10.0.0.0/24',
subnetpool_id=None,
segment_id=None,
fmt=None,
ip_version=4,
allocation_pools=None,
@ -631,6 +634,7 @@ class NeutronDbPluginV2TestCase(testlib_api.WebTestCase):
enable_dhcp,
dns_nameservers,
host_routes,
segment_id=segment_id,
shared=shared,
ipv6_ra_mode=ipv6_ra_mode,
ipv6_address_mode=ipv6_address_mode,
@ -1292,8 +1296,8 @@ fixed_ips=ip_address%%3D%s&fixed_ips=ip_address%%3D%s&fixed_ips=subnet_id%%3D%s
data = {'port': {'fixed_ips': [{'subnet_id':
subnet['subnet']['id']}]}}
# mock _get_subnets, to return this subnet
mock.patch.object(db_base_plugin_common.DbBasePluginCommon,
'_get_subnets',
mock.patch.object(ipam_backend_mixin.IpamBackendMixin,
'_ipam_get_subnets',
return_value=[subnet['subnet']]).start()
# Delete subnet, to mock the subnet as stale.
self._delete('subnets', subnet['subnet']['id'])

View File

@ -20,8 +20,8 @@ from oslo_config import cfg
from neutron.common import constants
from neutron.common import ipv6_utils
from neutron.db import db_base_plugin_common
from neutron.db import db_base_plugin_v2
from neutron.db import ipam_backend_mixin
from neutron.db import ipam_non_pluggable_backend as non_ipam
from neutron.db import models_v2
from neutron.tests import base
@ -143,8 +143,8 @@ class TestIpamNonPluggableBackend(base.BaseTestCase):
# were not actually created, so no ipam_subnet exists
cfg.CONF.set_override("ipam_driver", None)
plugin = db_base_plugin_v2.NeutronDbPluginV2()
with mock.patch.object(db_base_plugin_common.DbBasePluginCommon,
'_get_subnets') as get_subnets:
with mock.patch.object(ipam_backend_mixin.IpamBackendMixin,
'_ipam_get_subnets') as get_subnets:
with mock.patch.object(non_ipam.IpamNonPluggableBackend,
'_check_unique_ip') as check_unique:
context = mock.Mock()

View File

@ -582,7 +582,7 @@ class TestDbBasePluginIpam(test_db_base.NeutronDbPluginV2TestCase):
fixed_ips_mock = mock.Mock(return_value=changes.add)
mocks['ipam'] = ipam_pluggable_backend.IpamPluggableBackend()
mocks['ipam']._get_changed_ips_for_port = changes_mock
mocks['ipam']._get_subnets = mock.Mock()
mocks['ipam']._ipam_get_subnets = mock.Mock()
mocks['ipam']._test_fixed_ips_for_port = fixed_ips_mock
mocks['ipam']._update_ips_for_pd_subnet = mock.Mock(return_value=[])
@ -592,9 +592,8 @@ class TestDbBasePluginIpam(test_db_base.NeutronDbPluginV2TestCase):
mocks['ipam']._update_ips_for_port(context, port_dict,
original_ips, new_ips, mac)
mocks['driver'].get_address_request_factory.assert_called_once_with()
net_id_filter = {'network_id': [port_dict['network_id']]}
mocks['ipam']._get_subnets.assert_called_once_with(
context, filters=net_id_filter)
mocks['ipam']._ipam_get_subnets.assert_called_once_with(
context, network_id=port_dict['network_id'], segment_id=None)
# Validate port_dict is passed into address_factory
address_factory.get_request.assert_called_once_with(context,
port_dict,
@ -623,7 +622,7 @@ class TestDbBasePluginIpam(test_db_base.NeutronDbPluginV2TestCase):
get_subnets_mock = mock.Mock(return_value=subnets)
get_subnet_mock = mock.Mock(return_value=subnets[0])
mocks['ipam'] = ipam_pluggable_backend.IpamPluggableBackend()
mocks['ipam']._get_subnets = get_subnets_mock
mocks['ipam']._ipam_get_subnets = get_subnets_mock
mocks['ipam']._get_subnet = get_subnet_mock
mocks['ipam'].allocate_ips_for_port_and_store(context,

View File

@ -12,10 +12,14 @@
# License for the specific language governing permissions and limitations
# under the License.
from neutron_lib import constants
from oslo_utils import uuidutils
import webob.exc
from neutron.api.v2 import attributes
from neutron import context
from neutron.db import db_base_plugin_v2
from neutron.db import segments_db
from neutron.extensions import segment as ext_segment
from neutron.services.segments import db
from neutron.tests.unit.db import test_db_base_plugin_v2
@ -156,3 +160,104 @@ class TestSegment(SegmentTestCase):
segmentation_id=200)
res = self._list('segments')
self.assertEqual(2, len(res['segments']))
class TestSegmentSubnetAssociation(SegmentTestCase):
def test_basic_association(self):
with self.network() as network:
net = network['network']
segment = self._test_create_segment(network_id=net['id'])
segment_id = segment['segment']['id']
with self.subnet(network=network, segment_id=segment_id) as subnet:
subnet = subnet['subnet']
request = self.new_show_request('subnets', subnet['id'])
response = request.get_response(self.api)
res = self.deserialize(self.fmt, response)
self.assertEqual(segment_id,
res['subnet']['segment_id'])
def test_association_network_mismatch(self):
with self.network() as network1:
with self.network() as network2:
net = network1['network']
segment = self._test_create_segment(network_id=net['id'])
res = self._create_subnet(self.fmt,
net_id=network2['network']['id'],
tenant_id=network2['network']['tenant_id'],
gateway_ip=constants.ATTR_NOT_SPECIFIED,
cidr='10.0.0.0/24',
segment_id=segment['segment']['id'])
self.assertEqual(webob.exc.HTTPBadRequest.code, res.status_int)
def test_association_segment_not_found(self):
with self.network() as network:
net = network['network']
segment_id = uuidutils.generate_uuid()
res = self._create_subnet(self.fmt,
net_id=net['id'],
tenant_id=net['tenant_id'],
gateway_ip=constants.ATTR_NOT_SPECIFIED,
cidr='10.0.0.0/24',
segment_id=segment_id)
self.assertEqual(webob.exc.HTTPNotFound.code, res.status_int)
def test_only_some_subnets_associated_not_allowed(self):
with self.network() as network:
with self.subnet(network=network):
net = network['network']
segment = self._test_create_segment(network_id=net['id'])
res = self._create_subnet(self.fmt,
net_id=net['id'],
tenant_id=net['tenant_id'],
gateway_ip=constants.ATTR_NOT_SPECIFIED,
cidr='10.0.1.0/24',
segment_id=segment['segment']['id'])
self.assertEqual(webob.exc.HTTPBadRequest.code, res.status_int)
def test_association_to_dynamic_segment_not_allowed(self):
cxt = context.get_admin_context()
with self.network() as network:
net = network['network']
# Can't create a dynamic segment through the API
segment = {segments_db.NETWORK_TYPE: 'phys_net',
segments_db.PHYSICAL_NETWORK: 'net_type',
segments_db.SEGMENTATION_ID: 200}
segments_db.add_network_segment(cxt.session,
network_id=net['id'],
segment=segment,
is_dynamic=True)
res = self._create_subnet(self.fmt,
net_id=net['id'],
tenant_id=net['tenant_id'],
gateway_ip=constants.ATTR_NOT_SPECIFIED,
cidr='10.0.0.0/24',
segment_id=segment['id'])
self.assertEqual(webob.exc.HTTPBadRequest.code, res.status_int)
def test_port_create_with_segment_subnets(self):
with self.network() as network:
net = network['network']
segment = self._test_create_segment(network_id=net['id'])
segment_id = segment['segment']['id']
with self.subnet(network=network, segment_id=segment_id) as subnet:
subnet = subnet['subnet']
response = self._create_port(self.fmt,
net_id=net['id'],
tenant_id=net['tenant_id'])
res = self.deserialize(self.fmt, response)
# Don't allocate IPs in this case because it doesn't have binding info
self.assertEqual(0, len(res['port']['fixed_ips']))