Implements basic segmented networking by providing a new "segment_id"
column. This column is used for lookups when assigning IP addresses out
of "shared" networks. If one attempts to create a port on a shared
network without providing the segment_id, ambiguity exceptions are
raised. Additionally, extends subnets to allow a segment_id to be passed
when the subnet is created.
This commit is contained in:
Matt Dietz
2013-12-08 15:31:40 +00:00
parent a21af5f589
commit 3d87fff9b7
10 changed files with 176 additions and 34 deletions

View File

@@ -19,6 +19,7 @@ from neutron.api import extensions
EXTENDED_ATTRIBUTES_2_0 = {
'subnets': {
"segment_id": {"allow_post": True, "default": False},
"enable_dhcp": {'allow_post': False, 'allow_put': False,
'default': False,
'is_visible': True},

View File

@@ -51,7 +51,7 @@ for _name, klass in inspect.getmembers(models, inspect.isclass):
def _listify(filters):
for key in ["name", "network_id", "id", "device_id", "tenant_id",
"mac_address", "shared", "version"]:
"subnet_id", "mac_address", "shared", "version"]:
if key in filters:
if not filters[key]:
continue
@@ -74,6 +74,9 @@ def _model_query(context, model, filters, fields=None):
if filters.get("mac_address"):
model_filters.append(model.mac_address.in_(filters["mac_address"]))
if filters.get("segment_id"):
model_filters.append(model.segment_id == filters["segment_id"])
if filters.get("id"):
model_filters.append(model.id.in_(filters["id"]))
@@ -84,8 +87,7 @@ def _model_query(context, model, filters, fields=None):
model_filters.append(model.deallocated_at <= reuse)
if filters.get("subnet_id"):
model_filters.append(model.subnet_id ==
filters["subnet_id"])
model_filters.append(model.subnet_id.in_(filters["subnet_id"]))
if filters.get("deallocated"):
model_filters.append(model.deallocated == filters["deallocated"])
@@ -381,6 +383,8 @@ def subnet_find_allocation_counts(context, net_id, **filters):
query = query.filter(models.Subnet.network_id == net_id)
if "ip_version" in filters:
query = query.filter(models.Subnet.ip_version == filters["ip_version"])
if "segment_id" in filters:
query = query.filter(models.Subnet.segment_id == filters["segment_id"])
return query

View File

@@ -201,6 +201,7 @@ class Subnet(BASEV2, models.HasId, IsHazTags):
network_id = sa.Column(sa.String(36), sa.ForeignKey('quark_networks.id'))
_cidr = sa.Column(sa.String(64), nullable=False)
tenant_id = sa.Column(sa.String(255), index=True)
segment_id = sa.Column(sa.String(255), index=True)
@hybrid.hybrid_property
def cidr(self):

View File

@@ -21,8 +21,8 @@ class RouteNotFound(exceptions.NotFound):
message = _("Route %(route_id)s not found.")
class AmbiguousNetworkId(exceptions.NeutronException):
message = _("Segment ID required for network %(net_id)s.")
class AmbiguousNetworkId(exceptions.InvalidInput):
msg = _("Segment ID required for network %(net_id)s.")
class AmbigiousLswitchCount(exceptions.NeutronException):

View File

@@ -78,7 +78,8 @@ class QuarkIpam(object):
raise exceptions.MacAddressGenerationFailure(net_id=net_id)
def attempt_to_reallocate_ip(self, context, net_id, port_id, reuse_after,
version=None, ip_address=None):
version=None, ip_address=None,
segment_id=None):
version = version or [4, 6]
elevated = context.elevated()
@@ -87,10 +88,26 @@ class QuarkIpam(object):
# is really wrong)
for times in xrange(3):
with context.session.begin(subtransactions=True):
address = db_api.ip_address_find(
elevated, network_id=net_id, reuse_after=reuse_after,
deallocated=True, scope=db_api.ONE, ip_address=ip_address,
lock_mode=True, version=version, order_by="address")
sub_ids = []
if segment_id:
subnets = db_api.subnet_find(elevated, network_id=net_id,
segment_id=segment_id)
sub_ids = [s["id"] for s in subnets]
if not sub_ids:
raise exceptions.IpAddressGenerationFailure(
net_id=net_id)
ip_kwargs = {
"network_id": net_id, "reuse_after": reuse_after,
"deallocated": True, "scope": db_api.ONE,
"ip_address": ip_address, "lock_mode": True,
"version": version, "order_by": "address"}
if sub_ids:
ip_kwargs["subnet_id"] = sub_ids
address = db_api.ip_address_find(elevated, **ip_kwargs)
if address:
#NOTE(mdietz): We should always be in the CIDR but we've
@@ -138,7 +155,7 @@ class QuarkIpam(object):
return next_ip
def allocate_ip_address(self, context, net_id, port_id, reuse_after,
version=None, ip_address=None):
segment_id=None, version=None, ip_address=None):
elevated = context.elevated()
if ip_address:
ip_address = netaddr.IPAddress(ip_address)
@@ -147,14 +164,15 @@ class QuarkIpam(object):
realloc_ips = self.attempt_to_reallocate_ip(context, net_id,
port_id, reuse_after,
version=None,
ip_address=None)
ip_address=None,
segment_id=segment_id)
if self.is_strategy_satisfied(realloc_ips):
return realloc_ips
new_addresses.extend(realloc_ips)
with context.session.begin(subtransactions=True):
subnets = self._choose_available_subnet(
elevated, net_id, version, ip_address=ip_address,
reallocated_ips=realloc_ips)
elevated, net_id, version, segment_id=segment_id,
ip_address=ip_address, reallocated_ips=realloc_ips)
for subnet in subnets:
ip_policy_rules = models.IPPolicy.get_ip_policy_rule_set(
subnet)
@@ -224,8 +242,10 @@ class QuarkIpam(object):
db_api.mac_address_update(context, mac, deallocated=True,
deallocated_at=timeutils.utcnow())
def select_subnet(self, context, net_id, ip_address, **filters):
def select_subnet(self, context, net_id, ip_address, segment_id,
**filters):
subnets = db_api.subnet_find_allocation_counts(context, net_id,
segment_id=segment_id,
scope=db_api.ALL,
**filters)
for subnet, ips_in_subnet in subnets:
@@ -247,11 +267,13 @@ class QuarkIpamANY(QuarkIpam):
return "ANY"
def _choose_available_subnet(self, context, net_id, version=None,
ip_address=None, reallocated_ips=None):
segment_id=None, ip_address=None,
reallocated_ips=None):
filters = {}
if version:
filters["ip_version"] = version
subnet = self.select_subnet(context, net_id, ip_address, **filters)
subnet = self.select_subnet(context, net_id, ip_address, segment_id,
**filters)
if subnet:
return [subnet]
raise exceptions.IpAddressGenerationFailure(net_id=net_id)
@@ -273,17 +295,19 @@ class QuarkIpamBOTH(QuarkIpam):
def attempt_to_reallocate_ip(self, context, net_id, port_id,
reuse_after, version=None,
ip_address=None):
ip_address=None, segment_id=None):
both_versions = []
with context.session.begin(subtransactions=True):
for ver in (4, 6):
address = super(QuarkIpamBOTH, self).attempt_to_reallocate_ip(
context, net_id, port_id, reuse_after, ver, ip_address)
context, net_id, port_id, reuse_after, ver, ip_address,
segment_id)
both_versions.extend(address)
return both_versions
def _choose_available_subnet(self, context, net_id, version=None,
ip_address=None, reallocated_ips=None):
segment_id=None, ip_address=None,
reallocated_ips=None):
both_subnet_versions = []
need_versions = [4, 6]
for i in reallocated_ips:
@@ -292,7 +316,8 @@ class QuarkIpamBOTH(QuarkIpam):
filters = {}
for ver in need_versions:
filters["ip_version"] = ver
sub = self.select_subnet(context, net_id, ip_address, **filters)
sub = self.select_subnet(context, net_id, ip_address, segment_id,
**filters)
if sub:
both_subnet_versions.append(sub)
@@ -308,9 +333,10 @@ class QuarkIpamBOTHREQ(QuarkIpamBOTH):
return "BOTH_REQUIRED"
def _choose_available_subnet(self, context, net_id, version=None,
ip_address=None, reallocated_ips=None):
segment_id=None, ip_address=None,
reallocated_ips=None):
subnets = super(QuarkIpamBOTHREQ, self)._choose_available_subnet(
context, net_id, version, ip_address, reallocated_ips)
context, net_id, version, segment_id, ip_address, reallocated_ips)
if len(reallocated_ips) + len(subnets) < 2:
raise exceptions.IpAddressGenerationFailure(net_id=net_id)

View File

@@ -23,6 +23,7 @@ from oslo.config import cfg
from quark.db import api as db_api
from quark.drivers import registry
from quark import exceptions as q_exc
from quark import ipam
from quark import network_strategy
from quark import plugin_views as v
@@ -55,18 +56,19 @@ def create_port(context, port):
with context.session.begin():
port_id = uuidutils.generate_uuid()
net = db_api.network_find(context, id=net_id,
segment_id=segment_id, scope=db_api.ONE)
net = db_api.network_find(context, id=net_id, scope=db_api.ONE)
if not net:
# Maybe it's a tenant network
net = db_api.network_find(context, id=net_id, scope=db_api.ONE)
if not net:
raise exceptions.NetworkNotFound(net_id=net_id)
raise exceptions.NetworkNotFound(net_id=net_id)
if not STRATEGY.is_parent_network(net_id):
# We don't honor segmented networks when they aren't "shared"
segment_id = None
quota.QUOTAS.limit_check(
context, context.tenant_id,
ports_per_network=len(net.get('ports', [])) + 1)
else:
if not segment_id:
raise q_exc.AmbiguousNetworkId(net_id=net_id)
ipam_driver = ipam.IPAM_REGISTRY.get_strategy(net["ipam_strategy"])
if fixed_ips:
@@ -79,10 +81,11 @@ def create_port(context, port):
msg="subnet_id and ip_address required")
addresses.extend(ipam_driver.allocate_ip_address(
context, net["id"], port_id, CONF.QUARK.ipam_reuse_after,
ip_address=ip_address))
segment_id=segment_id, ip_address=ip_address))
else:
addresses.extend(ipam_driver.allocate_ip_address(
context, net["id"], port_id, CONF.QUARK.ipam_reuse_after))
context, net["id"], port_id, CONF.QUARK.ipam_reuse_after,
segment_id=segment_id))
group_ids, security_groups = v.make_security_group_list(
context, port["port"].pop("security_groups", None))

View File

@@ -99,6 +99,10 @@ def create_subnet(context, subnet):
dns_ips = utils.pop_param(sub_attrs, "dns_nameservers", [])
host_routes = utils.pop_param(sub_attrs, "host_routes", [])
allocation_pools = utils.pop_param(sub_attrs, "allocation_pools", None)
if not context.is_admin and "segment_id" in sub_attrs:
sub_attrs.pop("segment_id")
sub_attrs["network"] = net
new_subnet = db_api.subnet_create(context, **sub_attrs)

View File

@@ -23,6 +23,7 @@ from neutron.extensions import securitygroup as sg_ext
from quark.db import api as quark_db_api
from quark.db import models
from quark import exceptions as q_exc
from quark import network_strategy
from quark.plugin_modules import ports as quark_ports
from quark.tests import test_quark_plugin
@@ -185,6 +186,30 @@ class TestQuarkCreatePort(test_quark_plugin.TestQuarkPlugin):
for key in expected.keys():
self.assertEqual(result[key], expected[key])
def test_create_port_segment_id_on_unshared_net_ignored(self):
network = dict(id=1)
mac = dict(address="AA:BB:CC:DD:EE:FF")
port_name = "foobar"
ip = dict()
port = dict(port=dict(mac_address=mac["address"], network_id=1,
tenant_id=self.context.tenant_id, device_id=2,
segment_id="cell01", name=port_name))
expected = {'status': "ACTIVE",
'name': port_name,
'device_owner': None,
'mac_address': mac["address"],
'network_id': network["id"],
'tenant_id': self.context.tenant_id,
'admin_state_up': None,
'fixed_ips': [],
'device_id': 2}
with self._stubs(port=port["port"], network=network, addr=ip,
mac=mac) as port_create:
result = self.plugin.create_port(self.context, port)
self.assertTrue(port_create.called)
for key in expected.keys():
self.assertEqual(result[key], expected[key])
def test_create_port_mac_address_not_specified(self):
network = dict(id=1)
mac = dict(address="AA:BB:CC:DD:EE:FF")
@@ -532,6 +557,7 @@ class TestQuarkCreatePortOnSharedNetworks(test_quark_plugin.TestQuarkPlugin):
port = dict(port=dict(mac_address=mac["address"],
network_id="public_network",
tenant_id=self.context.tenant_id, device_id=2,
segment_id="cell01",
name=port_name))
with self._stubs(port=port["port"], network=network, addr=ip, mac=mac):
try:
@@ -539,6 +565,19 @@ class TestQuarkCreatePortOnSharedNetworks(test_quark_plugin.TestQuarkPlugin):
except Exception:
self.fail("create_port raised OverQuota")
def test_create_port_shared_net_no_segment_id_fails(self):
network = dict(id=1, ports=[models.Port()])
mac = dict(address="AA:BB:CC:DD:EE:FF")
port_name = "foobar"
ip = dict()
port = dict(port=dict(mac_address=mac["address"],
network_id="public_network",
tenant_id=self.context.tenant_id, device_id=2,
name=port_name))
with self._stubs(port=port["port"], network=network, addr=ip, mac=mac):
with self.assertRaises(q_exc.AmbiguousNetworkId):
self.plugin.create_port(self.context, port)
class TestQuarkGetPortCount(test_quark_plugin.TestQuarkPlugin):
def test_get_port_count(self):

View File

@@ -306,6 +306,41 @@ class TestQuarkCreateSubnet(test_quark_plugin.TestQuarkPlugin):
else:
self.assertEqual(res[key], subnet["subnet"][key])
def test_create_subnet_not_admin_segment_id_ignored(self):
routes = [dict(cidr="0.0.0.0/0", gateway="0.0.0.0")]
subnet = dict(
subnet=dict(network_id=1,
tenant_id=self.context.tenant_id, ip_version=4,
cidr="172.16.0.0/24", gateway_ip="0.0.0.0",
dns_nameservers=neutron_attrs.ATTR_NOT_SPECIFIED,
host_routes=neutron_attrs.ATTR_NOT_SPECIFIED,
enable_dhcp=None))
network = dict(network_id=1)
with self._stubs(
subnet=subnet["subnet"],
network=network,
routes=routes
) as (subnet_create, dns_create, route_create):
dns_nameservers = subnet["subnet"].pop("dns_nameservers")
host_routes = subnet["subnet"].pop("host_routes")
subnet_request = copy.deepcopy(subnet)
subnet_request["subnet"]["dns_nameservers"] = dns_nameservers
subnet_request["subnet"]["host_routes"] = host_routes
subnet_request["subnet"]["segment_id"] = "cell01"
res = self.plugin.create_subnet(self.context,
subnet_request)
self.assertEqual(subnet_create.call_count, 1)
self.assertTrue("segment_id" not in subnet_create.called_with)
self.assertEqual(dns_create.call_count, 0)
self.assertEqual(route_create.call_count, 1)
for key in subnet["subnet"].keys():
if key == "host_routes":
self.assertEqual(res[key][0]["destination"], "0.0.0.0/0")
self.assertEqual(res[key][0]["nexthop"], "0.0.0.0")
else:
self.assertEqual(res[key], subnet["subnet"][key])
def test_create_subnet_no_network_fails(self):
subnet = dict(subnet=dict(network_id=1))
with self._stubs(subnet=dict(), network=None):

View File

@@ -225,10 +225,13 @@ class QuarkIpamTestBothIpAllocation(QuarkIpamBaseTest):
self.context.session.add = mock.Mock()
with contextlib.nested(
mock.patch("%s.ip_address_find" % db_mod),
mock.patch("%s.subnet_find_allocation_counts" % db_mod)
) as (addr_find, subnet_find):
mock.patch("%s.subnet_find_allocation_counts" % db_mod),
mock.patch("%s.subnet_find" % db_mod)
) as (addr_find, subnet_alloc_find, subnet_find):
addr_find.side_effect = addresses
subnet_find.side_effect = subnets
if subnets and len(subnets[0]):
subnet_find.return_value = [subnets[0][0][0]]
subnet_alloc_find.side_effect = subnets
yield
def test_allocate_new_ip_address_two_empty_subnets(self):
@@ -292,6 +295,32 @@ class QuarkIpamTestBothIpAllocation(QuarkIpamBaseTest):
self.assertEqual(address[1]["address"], 0)
self.assertEqual(address[1]["version"], 6)
def test_reallocate_deallocated_v4_ip_shared_net(self):
subnet6 = dict(id=1, first_ip=self.v6_fip, last_ip=self.v6_lip,
cidr="feed::/104", ip_version=6,
next_auto_assign_ip=0, network=dict(ip_policy=None),
ip_policy=None)
address = models.IPAddress()
address["address"] = 4
address["version"] = 4
address["subnet"] = models.Subnet(cidr="0.0.0.0/24")
with self._stubs(subnets=[[(subnet6, 0)]],
addresses=[address, None, None]):
address = self.ipam.allocate_ip_address(self.context, 0, 0, 0,
segment_id="cell01")
self.assertEqual(len(address), 2)
self.assertEqual(address[0]["address"], 4)
self.assertEqual(address[0]["version"], 4)
self.assertEqual(address[1]["address"], 0)
self.assertEqual(address[1]["version"], 6)
def test_reallocate_deallocated_v4_ip_shared_net_no_subs_raises(self):
with self._stubs(subnets=[],
addresses=[None]):
with self.assertRaises(exceptions.IpAddressGenerationFailure):
self.ipam.allocate_ip_address(self.context, 0, 0, 0,
segment_id="cell01")
def test_reallocate_deallocated_v4_ip_no_avail_subnets(self):
address = models.IPAddress()
address["address"] = 4