Fixes #27
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:
@@ -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},
|
||||
|
||||
@@ -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
|
||||
|
||||
|
||||
|
||||
@@ -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):
|
||||
|
||||
@@ -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):
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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)
|
||||
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)
|
||||
|
||||
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))
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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):
|
||||
|
||||
@@ -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):
|
||||
|
||||
@@ -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
|
||||
|
||||
Reference in New Issue
Block a user