Merge "Add Pluggable IPAM Backend Part 2"
This commit is contained in:
commit
20f099150f
@ -45,6 +45,9 @@ DEVICE_OWNER_LOADBALANCERV2 = "neutron:LOADBALANCERV2"
|
|||||||
# DEVICE_OWNER_ROUTER_HA_INTF is a special case and so is not included.
|
# DEVICE_OWNER_ROUTER_HA_INTF is a special case and so is not included.
|
||||||
ROUTER_INTERFACE_OWNERS = (DEVICE_OWNER_ROUTER_INTF,
|
ROUTER_INTERFACE_OWNERS = (DEVICE_OWNER_ROUTER_INTF,
|
||||||
DEVICE_OWNER_DVR_INTERFACE)
|
DEVICE_OWNER_DVR_INTERFACE)
|
||||||
|
ROUTER_INTERFACE_OWNERS_SNAT = (DEVICE_OWNER_ROUTER_INTF,
|
||||||
|
DEVICE_OWNER_DVR_INTERFACE,
|
||||||
|
DEVICE_OWNER_ROUTER_SNAT)
|
||||||
L3_AGENT_MODE_DVR = 'dvr'
|
L3_AGENT_MODE_DVR = 'dvr'
|
||||||
L3_AGENT_MODE_DVR_SNAT = 'dvr_snat'
|
L3_AGENT_MODE_DVR_SNAT = 'dvr_snat'
|
||||||
L3_AGENT_MODE_LEGACY = 'legacy'
|
L3_AGENT_MODE_LEGACY = 'legacy'
|
||||||
|
@ -36,6 +36,7 @@ from neutron import context as ctx
|
|||||||
from neutron.db import api as db_api
|
from neutron.db import api as db_api
|
||||||
from neutron.db import db_base_plugin_common
|
from neutron.db import db_base_plugin_common
|
||||||
from neutron.db import ipam_non_pluggable_backend
|
from neutron.db import ipam_non_pluggable_backend
|
||||||
|
from neutron.db import ipam_pluggable_backend
|
||||||
from neutron.db import models_v2
|
from neutron.db import models_v2
|
||||||
from neutron.db import rbac_db_models as rbac_db
|
from neutron.db import rbac_db_models as rbac_db
|
||||||
from neutron.db import sqlalchemyutils
|
from neutron.db import sqlalchemyutils
|
||||||
@ -101,7 +102,10 @@ class NeutronDbPluginV2(db_base_plugin_common.DbBasePluginCommon,
|
|||||||
self.nova_notifier.record_port_status_changed)
|
self.nova_notifier.record_port_status_changed)
|
||||||
|
|
||||||
def set_ipam_backend(self):
|
def set_ipam_backend(self):
|
||||||
self.ipam = ipam_non_pluggable_backend.IpamNonPluggableBackend()
|
if cfg.CONF.ipam_driver:
|
||||||
|
self.ipam = ipam_pluggable_backend.IpamPluggableBackend()
|
||||||
|
else:
|
||||||
|
self.ipam = ipam_non_pluggable_backend.IpamNonPluggableBackend()
|
||||||
|
|
||||||
def _validate_host_route(self, route, ip_version):
|
def _validate_host_route(self, route, ip_version):
|
||||||
try:
|
try:
|
||||||
@ -470,10 +474,10 @@ class NeutronDbPluginV2(db_base_plugin_common.DbBasePluginCommon,
|
|||||||
|
|
||||||
with context.session.begin(subtransactions=True):
|
with context.session.begin(subtransactions=True):
|
||||||
network = self._get_network(context, s["network_id"])
|
network = self._get_network(context, s["network_id"])
|
||||||
subnet = self.ipam.allocate_subnet(context,
|
subnet, ipam_subnet = self.ipam.allocate_subnet(context,
|
||||||
network,
|
network,
|
||||||
s,
|
s,
|
||||||
subnetpool_id)
|
subnetpool_id)
|
||||||
if hasattr(network, 'external') and network.external:
|
if hasattr(network, 'external') and network.external:
|
||||||
self._update_router_gw_ports(context,
|
self._update_router_gw_ports(context,
|
||||||
network,
|
network,
|
||||||
@ -481,7 +485,8 @@ class NeutronDbPluginV2(db_base_plugin_common.DbBasePluginCommon,
|
|||||||
# If this subnet supports auto-addressing, then update any
|
# If this subnet supports auto-addressing, then update any
|
||||||
# internal ports on the network with addresses for this subnet.
|
# internal ports on the network with addresses for this subnet.
|
||||||
if ipv6_utils.is_auto_address_subnet(subnet):
|
if ipv6_utils.is_auto_address_subnet(subnet):
|
||||||
self.ipam.add_auto_addrs_on_network_ports(context, subnet)
|
self.ipam.add_auto_addrs_on_network_ports(context, subnet,
|
||||||
|
ipam_subnet)
|
||||||
return self._make_subnet_dict(subnet, context=context)
|
return self._make_subnet_dict(subnet, context=context)
|
||||||
|
|
||||||
def _get_subnetpool_id(self, subnet):
|
def _get_subnetpool_id(self, subnet):
|
||||||
@ -561,21 +566,24 @@ class NeutronDbPluginV2(db_base_plugin_common.DbBasePluginCommon,
|
|||||||
s['ip_version'] = db_subnet.ip_version
|
s['ip_version'] = db_subnet.ip_version
|
||||||
s['cidr'] = db_subnet.cidr
|
s['cidr'] = db_subnet.cidr
|
||||||
s['id'] = db_subnet.id
|
s['id'] = db_subnet.id
|
||||||
|
s['tenant_id'] = db_subnet.tenant_id
|
||||||
self._validate_subnet(context, s, cur_subnet=db_subnet)
|
self._validate_subnet(context, s, cur_subnet=db_subnet)
|
||||||
|
db_pools = [netaddr.IPRange(p['first_ip'], p['last_ip'])
|
||||||
|
for p in db_subnet.allocation_pools]
|
||||||
|
|
||||||
|
range_pools = None
|
||||||
|
if s.get('allocation_pools') is not None:
|
||||||
|
# Convert allocation pools to IPRange to simplify future checks
|
||||||
|
range_pools = self.ipam.pools_to_ip_range(s['allocation_pools'])
|
||||||
|
s['allocation_pools'] = range_pools
|
||||||
|
|
||||||
if s.get('gateway_ip') is not None:
|
if s.get('gateway_ip') is not None:
|
||||||
if s.get('allocation_pools') is not None:
|
pools = range_pools if range_pools is not None else db_pools
|
||||||
allocation_pools = [{'start': p['start'], 'end': p['end']}
|
self.ipam.validate_gw_out_of_pools(s["gateway_ip"], pools)
|
||||||
for p in s['allocation_pools']]
|
|
||||||
else:
|
|
||||||
allocation_pools = [{'start': p['first_ip'],
|
|
||||||
'end': p['last_ip']}
|
|
||||||
for p in db_subnet.allocation_pools]
|
|
||||||
self.ipam.validate_gw_out_of_pools(s["gateway_ip"],
|
|
||||||
allocation_pools)
|
|
||||||
|
|
||||||
with context.session.begin(subtransactions=True):
|
with context.session.begin(subtransactions=True):
|
||||||
subnet, changes = self.ipam.update_db_subnet(context, id, s)
|
subnet, changes = self.ipam.update_db_subnet(context, id, s,
|
||||||
|
db_pools)
|
||||||
result = self._make_subnet_dict(subnet, context=context)
|
result = self._make_subnet_dict(subnet, context=context)
|
||||||
# Keep up with fields that changed
|
# Keep up with fields that changed
|
||||||
result.update(changes)
|
result.update(changes)
|
||||||
@ -654,6 +662,9 @@ class NeutronDbPluginV2(db_base_plugin_common.DbBasePluginCommon,
|
|||||||
raise n_exc.SubnetInUse(subnet_id=id)
|
raise n_exc.SubnetInUse(subnet_id=id)
|
||||||
|
|
||||||
context.session.delete(subnet)
|
context.session.delete(subnet)
|
||||||
|
# Delete related ipam subnet manually,
|
||||||
|
# since there is no FK relationship
|
||||||
|
self.ipam.delete_subnet(context, id)
|
||||||
|
|
||||||
def get_subnet(self, context, id, fields=None):
|
def get_subnet(self, context, id, fields=None):
|
||||||
subnet = self._get_subnet(context, id)
|
subnet = self._get_subnet(context, id)
|
||||||
|
@ -52,6 +52,24 @@ class IpamBackendMixin(db_base_plugin_common.DbBasePluginCommon):
|
|||||||
return str(netaddr.IPNetwork(cidr_net).network + 1)
|
return str(netaddr.IPNetwork(cidr_net).network + 1)
|
||||||
return subnet.get('gateway_ip')
|
return subnet.get('gateway_ip')
|
||||||
|
|
||||||
|
@staticmethod
|
||||||
|
def pools_to_ip_range(ip_pools):
|
||||||
|
ip_range_pools = []
|
||||||
|
for ip_pool in ip_pools:
|
||||||
|
try:
|
||||||
|
ip_range_pools.append(netaddr.IPRange(ip_pool['start'],
|
||||||
|
ip_pool['end']))
|
||||||
|
except netaddr.AddrFormatError:
|
||||||
|
LOG.info(_LI("Found invalid IP address in pool: "
|
||||||
|
"%(start)s - %(end)s:"),
|
||||||
|
{'start': ip_pool['start'],
|
||||||
|
'end': ip_pool['end']})
|
||||||
|
raise n_exc.InvalidAllocationPool(pool=ip_pool)
|
||||||
|
return ip_range_pools
|
||||||
|
|
||||||
|
def delete_subnet(self, context, subnet_id):
|
||||||
|
pass
|
||||||
|
|
||||||
def validate_pools_with_subnetpool(self, subnet):
|
def validate_pools_with_subnetpool(self, subnet):
|
||||||
"""Verifies that allocation pools are set correctly
|
"""Verifies that allocation pools are set correctly
|
||||||
|
|
||||||
@ -140,22 +158,23 @@ class IpamBackendMixin(db_base_plugin_common.DbBasePluginCommon):
|
|||||||
def _update_subnet_allocation_pools(self, context, subnet_id, s):
|
def _update_subnet_allocation_pools(self, context, subnet_id, s):
|
||||||
context.session.query(models_v2.IPAllocationPool).filter_by(
|
context.session.query(models_v2.IPAllocationPool).filter_by(
|
||||||
subnet_id=subnet_id).delete()
|
subnet_id=subnet_id).delete()
|
||||||
new_pools = [models_v2.IPAllocationPool(first_ip=p['start'],
|
pools = ((netaddr.IPAddress(p.first, p.version).format(),
|
||||||
last_ip=p['end'],
|
netaddr.IPAddress(p.last, p.version).format())
|
||||||
|
for p in s['allocation_pools'])
|
||||||
|
new_pools = [models_v2.IPAllocationPool(first_ip=p[0],
|
||||||
|
last_ip=p[1],
|
||||||
subnet_id=subnet_id)
|
subnet_id=subnet_id)
|
||||||
for p in s['allocation_pools']]
|
for p in pools]
|
||||||
context.session.add_all(new_pools)
|
context.session.add_all(new_pools)
|
||||||
# Call static method with self to redefine in child
|
# Call static method with self to redefine in child
|
||||||
# (non-pluggable backend)
|
# (non-pluggable backend)
|
||||||
self._rebuild_availability_ranges(context, [s])
|
self._rebuild_availability_ranges(context, [s])
|
||||||
# Gather new pools for result:
|
# Gather new pools for result
|
||||||
result_pools = [{'start': pool['start'],
|
result_pools = [{'start': p[0], 'end': p[1]} for p in pools]
|
||||||
'end': pool['end']}
|
|
||||||
for pool in s['allocation_pools']]
|
|
||||||
del s['allocation_pools']
|
del s['allocation_pools']
|
||||||
return result_pools
|
return result_pools
|
||||||
|
|
||||||
def update_db_subnet(self, context, subnet_id, s):
|
def update_db_subnet(self, context, subnet_id, s, oldpools):
|
||||||
changes = {}
|
changes = {}
|
||||||
if "dns_nameservers" in s:
|
if "dns_nameservers" in s:
|
||||||
changes['dns_nameservers'] = (
|
changes['dns_nameservers'] = (
|
||||||
@ -239,38 +258,23 @@ class IpamBackendMixin(db_base_plugin_common.DbBasePluginCommon):
|
|||||||
LOG.debug("Performing IP validity checks on allocation pools")
|
LOG.debug("Performing IP validity checks on allocation pools")
|
||||||
ip_sets = []
|
ip_sets = []
|
||||||
for ip_pool in ip_pools:
|
for ip_pool in ip_pools:
|
||||||
try:
|
start_ip = netaddr.IPAddress(ip_pool.first, ip_pool.version)
|
||||||
start_ip = netaddr.IPAddress(ip_pool['start'])
|
end_ip = netaddr.IPAddress(ip_pool.last, ip_pool.version)
|
||||||
end_ip = netaddr.IPAddress(ip_pool['end'])
|
|
||||||
except netaddr.AddrFormatError:
|
|
||||||
LOG.info(_LI("Found invalid IP address in pool: "
|
|
||||||
"%(start)s - %(end)s:"),
|
|
||||||
{'start': ip_pool['start'],
|
|
||||||
'end': ip_pool['end']})
|
|
||||||
raise n_exc.InvalidAllocationPool(pool=ip_pool)
|
|
||||||
if (start_ip.version != subnet.version or
|
if (start_ip.version != subnet.version or
|
||||||
end_ip.version != subnet.version):
|
end_ip.version != subnet.version):
|
||||||
LOG.info(_LI("Specified IP addresses do not match "
|
LOG.info(_LI("Specified IP addresses do not match "
|
||||||
"the subnet IP version"))
|
"the subnet IP version"))
|
||||||
raise n_exc.InvalidAllocationPool(pool=ip_pool)
|
raise n_exc.InvalidAllocationPool(pool=ip_pool)
|
||||||
if end_ip < start_ip:
|
|
||||||
LOG.info(_LI("Start IP (%(start)s) is greater than end IP "
|
|
||||||
"(%(end)s)"),
|
|
||||||
{'start': ip_pool['start'], 'end': ip_pool['end']})
|
|
||||||
raise n_exc.InvalidAllocationPool(pool=ip_pool)
|
|
||||||
if start_ip < subnet_first_ip or end_ip > subnet_last_ip:
|
if start_ip < subnet_first_ip or end_ip > subnet_last_ip:
|
||||||
LOG.info(_LI("Found pool larger than subnet "
|
LOG.info(_LI("Found pool larger than subnet "
|
||||||
"CIDR:%(start)s - %(end)s"),
|
"CIDR:%(start)s - %(end)s"),
|
||||||
{'start': ip_pool['start'],
|
{'start': start_ip, 'end': end_ip})
|
||||||
'end': ip_pool['end']})
|
|
||||||
raise n_exc.OutOfBoundsAllocationPool(
|
raise n_exc.OutOfBoundsAllocationPool(
|
||||||
pool=ip_pool,
|
pool=ip_pool,
|
||||||
subnet_cidr=subnet_cidr)
|
subnet_cidr=subnet_cidr)
|
||||||
# Valid allocation pool
|
# Valid allocation pool
|
||||||
# Create an IPSet for it for easily verifying overlaps
|
# Create an IPSet for it for easily verifying overlaps
|
||||||
ip_sets.append(netaddr.IPSet(netaddr.IPRange(
|
ip_sets.append(netaddr.IPSet(ip_pool.cidrs()))
|
||||||
ip_pool['start'],
|
|
||||||
ip_pool['end']).cidrs()))
|
|
||||||
|
|
||||||
LOG.debug("Checking for overlaps among allocation pools "
|
LOG.debug("Checking for overlaps among allocation pools "
|
||||||
"and gateway ip")
|
"and gateway ip")
|
||||||
@ -291,22 +295,54 @@ class IpamBackendMixin(db_base_plugin_common.DbBasePluginCommon):
|
|||||||
pool_2=r_range,
|
pool_2=r_range,
|
||||||
subnet_cidr=subnet_cidr)
|
subnet_cidr=subnet_cidr)
|
||||||
|
|
||||||
|
def _validate_max_ips_per_port(self, fixed_ip_list):
|
||||||
|
if len(fixed_ip_list) > cfg.CONF.max_fixed_ips_per_port:
|
||||||
|
msg = _('Exceeded maximim amount of fixed ips per port')
|
||||||
|
raise n_exc.InvalidInput(error_message=msg)
|
||||||
|
|
||||||
|
def _get_subnet_for_fixed_ip(self, context, fixed, network_id):
|
||||||
|
if 'subnet_id' in fixed:
|
||||||
|
subnet = self._get_subnet(context, fixed['subnet_id'])
|
||||||
|
if subnet['network_id'] != network_id:
|
||||||
|
msg = (_("Failed to create port on network %(network_id)s"
|
||||||
|
", because fixed_ips included invalid subnet "
|
||||||
|
"%(subnet_id)s") %
|
||||||
|
{'network_id': network_id,
|
||||||
|
'subnet_id': fixed['subnet_id']})
|
||||||
|
raise n_exc.InvalidInput(error_message=msg)
|
||||||
|
# Ensure that the IP is valid on the subnet
|
||||||
|
if ('ip_address' in fixed and
|
||||||
|
not ipam_utils.check_subnet_ip(subnet['cidr'],
|
||||||
|
fixed['ip_address'])):
|
||||||
|
raise n_exc.InvalidIpForSubnet(ip_address=fixed['ip_address'])
|
||||||
|
return subnet
|
||||||
|
|
||||||
|
if 'ip_address' not in fixed:
|
||||||
|
msg = _('IP allocation requires subnet_id or ip_address')
|
||||||
|
raise n_exc.InvalidInput(error_message=msg)
|
||||||
|
|
||||||
|
filter = {'network_id': [network_id]}
|
||||||
|
subnets = self._get_subnets(context, filters=filter)
|
||||||
|
|
||||||
|
for subnet in subnets:
|
||||||
|
if ipam_utils.check_subnet_ip(subnet['cidr'],
|
||||||
|
fixed['ip_address']):
|
||||||
|
return subnet
|
||||||
|
raise n_exc.InvalidIpForNetwork(ip_address=fixed['ip_address'])
|
||||||
|
|
||||||
def _prepare_allocation_pools(self, allocation_pools, cidr, gateway_ip):
|
def _prepare_allocation_pools(self, allocation_pools, cidr, gateway_ip):
|
||||||
"""Returns allocation pools represented as list of IPRanges"""
|
"""Returns allocation pools represented as list of IPRanges"""
|
||||||
if not attributes.is_attr_set(allocation_pools):
|
if not attributes.is_attr_set(allocation_pools):
|
||||||
return ipam_utils.generate_pools(cidr, gateway_ip)
|
return ipam_utils.generate_pools(cidr, gateway_ip)
|
||||||
|
|
||||||
self._validate_allocation_pools(allocation_pools, cidr)
|
ip_range_pools = self.pools_to_ip_range(allocation_pools)
|
||||||
|
self._validate_allocation_pools(ip_range_pools, cidr)
|
||||||
if gateway_ip:
|
if gateway_ip:
|
||||||
self.validate_gw_out_of_pools(gateway_ip, allocation_pools)
|
self.validate_gw_out_of_pools(gateway_ip, ip_range_pools)
|
||||||
return [netaddr.IPRange(p['start'], p['end'])
|
return ip_range_pools
|
||||||
for p in allocation_pools]
|
|
||||||
|
|
||||||
def validate_gw_out_of_pools(self, gateway_ip, pools):
|
def validate_gw_out_of_pools(self, gateway_ip, pools):
|
||||||
for allocation_pool in pools:
|
for pool_range in pools:
|
||||||
pool_range = netaddr.IPRange(
|
|
||||||
allocation_pool['start'],
|
|
||||||
allocation_pool['end'])
|
|
||||||
if netaddr.IPAddress(gateway_ip) in pool_range:
|
if netaddr.IPAddress(gateway_ip) in pool_range:
|
||||||
raise n_exc.GatewayConflictWithAllocationPools(
|
raise n_exc.GatewayConflictWithAllocationPools(
|
||||||
pool=pool_range,
|
pool=pool_range,
|
||||||
|
@ -14,7 +14,6 @@
|
|||||||
# under the License.
|
# under the License.
|
||||||
|
|
||||||
import netaddr
|
import netaddr
|
||||||
from oslo_config import cfg
|
|
||||||
from oslo_db import exception as db_exc
|
from oslo_db import exception as db_exc
|
||||||
from oslo_log import log as logging
|
from oslo_log import log as logging
|
||||||
from sqlalchemy import and_
|
from sqlalchemy import and_
|
||||||
@ -29,7 +28,6 @@ from neutron.db import ipam_backend_mixin
|
|||||||
from neutron.db import models_v2
|
from neutron.db import models_v2
|
||||||
from neutron.ipam import requests as ipam_req
|
from neutron.ipam import requests as ipam_req
|
||||||
from neutron.ipam import subnet_alloc
|
from neutron.ipam import subnet_alloc
|
||||||
from neutron.ipam import utils as ipam_utils
|
|
||||||
|
|
||||||
LOG = logging.getLogger(__name__)
|
LOG = logging.getLogger(__name__)
|
||||||
|
|
||||||
@ -242,49 +240,17 @@ class IpamNonPluggableBackend(ipam_backend_mixin.IpamBackendMixin):
|
|||||||
"""
|
"""
|
||||||
fixed_ip_set = []
|
fixed_ip_set = []
|
||||||
for fixed in fixed_ips:
|
for fixed in fixed_ips:
|
||||||
found = False
|
subnet = self._get_subnet_for_fixed_ip(context, fixed, network_id)
|
||||||
if 'subnet_id' not in fixed:
|
|
||||||
if 'ip_address' not in fixed:
|
|
||||||
msg = _('IP allocation requires subnet_id or ip_address')
|
|
||||||
raise n_exc.InvalidInput(error_message=msg)
|
|
||||||
|
|
||||||
filter = {'network_id': [network_id]}
|
|
||||||
subnets = self._get_subnets(context, filters=filter)
|
|
||||||
for subnet in subnets:
|
|
||||||
if ipam_utils.check_subnet_ip(subnet['cidr'],
|
|
||||||
fixed['ip_address']):
|
|
||||||
found = True
|
|
||||||
subnet_id = subnet['id']
|
|
||||||
break
|
|
||||||
if not found:
|
|
||||||
raise n_exc.InvalidIpForNetwork(
|
|
||||||
ip_address=fixed['ip_address'])
|
|
||||||
else:
|
|
||||||
subnet = self._get_subnet(context, fixed['subnet_id'])
|
|
||||||
if subnet['network_id'] != network_id:
|
|
||||||
msg = (_("Failed to create port on network %(network_id)s"
|
|
||||||
", because fixed_ips included invalid subnet "
|
|
||||||
"%(subnet_id)s") %
|
|
||||||
{'network_id': network_id,
|
|
||||||
'subnet_id': fixed['subnet_id']})
|
|
||||||
raise n_exc.InvalidInput(error_message=msg)
|
|
||||||
subnet_id = subnet['id']
|
|
||||||
|
|
||||||
is_auto_addr_subnet = ipv6_utils.is_auto_address_subnet(subnet)
|
is_auto_addr_subnet = ipv6_utils.is_auto_address_subnet(subnet)
|
||||||
if 'ip_address' in fixed:
|
if 'ip_address' in fixed:
|
||||||
# Ensure that the IP's are unique
|
# Ensure that the IP's are unique
|
||||||
if not IpamNonPluggableBackend._check_unique_ip(
|
if not IpamNonPluggableBackend._check_unique_ip(
|
||||||
context, network_id,
|
context, network_id,
|
||||||
subnet_id, fixed['ip_address']):
|
subnet['id'], fixed['ip_address']):
|
||||||
raise n_exc.IpAddressInUse(net_id=network_id,
|
raise n_exc.IpAddressInUse(net_id=network_id,
|
||||||
ip_address=fixed['ip_address'])
|
ip_address=fixed['ip_address'])
|
||||||
|
|
||||||
# Ensure that the IP is valid on the subnet
|
|
||||||
if (not found and
|
|
||||||
not ipam_utils.check_subnet_ip(subnet['cidr'],
|
|
||||||
fixed['ip_address'])):
|
|
||||||
raise n_exc.InvalidIpForSubnet(
|
|
||||||
ip_address=fixed['ip_address'])
|
|
||||||
if (is_auto_addr_subnet and
|
if (is_auto_addr_subnet and
|
||||||
device_owner not in
|
device_owner not in
|
||||||
constants.ROUTER_INTERFACE_OWNERS):
|
constants.ROUTER_INTERFACE_OWNERS):
|
||||||
@ -292,23 +258,20 @@ class IpamNonPluggableBackend(ipam_backend_mixin.IpamBackendMixin):
|
|||||||
"assigned to a port on subnet %(id)s since the "
|
"assigned to a port on subnet %(id)s since the "
|
||||||
"subnet is configured for automatic addresses") %
|
"subnet is configured for automatic addresses") %
|
||||||
{'address': fixed['ip_address'],
|
{'address': fixed['ip_address'],
|
||||||
'id': subnet_id})
|
'id': subnet['id']})
|
||||||
raise n_exc.InvalidInput(error_message=msg)
|
raise n_exc.InvalidInput(error_message=msg)
|
||||||
fixed_ip_set.append({'subnet_id': subnet_id,
|
fixed_ip_set.append({'subnet_id': subnet['id'],
|
||||||
'ip_address': fixed['ip_address']})
|
'ip_address': fixed['ip_address']})
|
||||||
else:
|
else:
|
||||||
# A scan for auto-address subnets on the network is done
|
# A scan for auto-address subnets on the network is done
|
||||||
# separately so that all such subnets (not just those
|
# separately so that all such subnets (not just those
|
||||||
# listed explicitly here by subnet ID) are associated
|
# listed explicitly here by subnet ID) are associated
|
||||||
# with the port.
|
# with the port.
|
||||||
if (device_owner in constants.ROUTER_INTERFACE_OWNERS or
|
if (device_owner in constants.ROUTER_INTERFACE_OWNERS_SNAT or
|
||||||
device_owner == constants.DEVICE_OWNER_ROUTER_SNAT or
|
|
||||||
not is_auto_addr_subnet):
|
not is_auto_addr_subnet):
|
||||||
fixed_ip_set.append({'subnet_id': subnet_id})
|
fixed_ip_set.append({'subnet_id': subnet['id']})
|
||||||
|
|
||||||
if len(fixed_ip_set) > cfg.CONF.max_fixed_ips_per_port:
|
self._validate_max_ips_per_port(fixed_ip_set)
|
||||||
msg = _('Exceeded maximim amount of fixed ips per port')
|
|
||||||
raise n_exc.InvalidInput(error_message=msg)
|
|
||||||
return fixed_ip_set
|
return fixed_ip_set
|
||||||
|
|
||||||
def _allocate_fixed_ips(self, context, fixed_ips, mac_address):
|
def _allocate_fixed_ips(self, context, fixed_ips, mac_address):
|
||||||
@ -382,8 +345,7 @@ class IpamNonPluggableBackend(ipam_backend_mixin.IpamBackendMixin):
|
|||||||
net_id_filter = {'network_id': [p['network_id']]}
|
net_id_filter = {'network_id': [p['network_id']]}
|
||||||
subnets = self._get_subnets(context, filters=net_id_filter)
|
subnets = self._get_subnets(context, filters=net_id_filter)
|
||||||
is_router_port = (
|
is_router_port = (
|
||||||
p['device_owner'] in constants.ROUTER_INTERFACE_OWNERS or
|
p['device_owner'] in constants.ROUTER_INTERFACE_OWNERS_SNAT)
|
||||||
p['device_owner'] == constants.DEVICE_OWNER_ROUTER_SNAT)
|
|
||||||
|
|
||||||
fixed_configured = p['fixed_ips'] is not attributes.ATTR_NOT_SPECIFIED
|
fixed_configured = p['fixed_ips'] is not attributes.ATTR_NOT_SPECIFIED
|
||||||
if fixed_configured:
|
if fixed_configured:
|
||||||
@ -431,17 +393,16 @@ class IpamNonPluggableBackend(ipam_backend_mixin.IpamBackendMixin):
|
|||||||
|
|
||||||
return ips
|
return ips
|
||||||
|
|
||||||
def add_auto_addrs_on_network_ports(self, context, subnet):
|
def add_auto_addrs_on_network_ports(self, context, subnet, ipam_subnet):
|
||||||
"""For an auto-address subnet, add addrs for ports on the net."""
|
"""For an auto-address subnet, add addrs for ports on the net."""
|
||||||
with context.session.begin(subtransactions=True):
|
with context.session.begin(subtransactions=True):
|
||||||
network_id = subnet['network_id']
|
network_id = subnet['network_id']
|
||||||
port_qry = context.session.query(models_v2.Port)
|
port_qry = context.session.query(models_v2.Port)
|
||||||
for port in port_qry.filter(
|
ports = port_qry.filter(
|
||||||
and_(models_v2.Port.network_id == network_id,
|
and_(models_v2.Port.network_id == network_id,
|
||||||
models_v2.Port.device_owner !=
|
|
||||||
constants.DEVICE_OWNER_ROUTER_SNAT,
|
|
||||||
~models_v2.Port.device_owner.in_(
|
~models_v2.Port.device_owner.in_(
|
||||||
constants.ROUTER_INTERFACE_OWNERS))):
|
constants.ROUTER_INTERFACE_OWNERS_SNAT)))
|
||||||
|
for port in ports:
|
||||||
ip_address = self._calculate_ipv6_eui64_addr(
|
ip_address = self._calculate_ipv6_eui64_addr(
|
||||||
context, subnet, port['mac_address'])
|
context, subnet, port['mac_address'])
|
||||||
allocated = models_v2.IPAllocation(network_id=network_id,
|
allocated = models_v2.IPAllocation(network_id=network_id,
|
||||||
@ -505,4 +466,6 @@ class IpamNonPluggableBackend(ipam_backend_mixin.IpamBackendMixin):
|
|||||||
subnet['dns_nameservers'],
|
subnet['dns_nameservers'],
|
||||||
subnet['host_routes'],
|
subnet['host_routes'],
|
||||||
subnet_request)
|
subnet_request)
|
||||||
return subnet
|
# ipam_subnet is not expected to be allocated for non pluggable ipam,
|
||||||
|
# so just return None for it (second element in returned tuple)
|
||||||
|
return subnet, None
|
||||||
|
@ -13,13 +13,22 @@
|
|||||||
# License for the specific language governing permissions and limitations
|
# License for the specific language governing permissions and limitations
|
||||||
# under the License.
|
# under the License.
|
||||||
|
|
||||||
|
import netaddr
|
||||||
|
from oslo_db import exception as db_exc
|
||||||
from oslo_log import log as logging
|
from oslo_log import log as logging
|
||||||
from oslo_utils import excutils
|
from oslo_utils import excutils
|
||||||
|
from sqlalchemy import and_
|
||||||
|
|
||||||
|
from neutron.api.v2 import attributes
|
||||||
|
from neutron.common import constants
|
||||||
from neutron.common import exceptions as n_exc
|
from neutron.common import exceptions as n_exc
|
||||||
|
from neutron.common import ipv6_utils
|
||||||
from neutron.db import ipam_backend_mixin
|
from neutron.db import ipam_backend_mixin
|
||||||
|
from neutron.db import models_v2
|
||||||
from neutron.i18n import _LE
|
from neutron.i18n import _LE
|
||||||
|
from neutron.ipam import driver
|
||||||
from neutron.ipam import exceptions as ipam_exc
|
from neutron.ipam import exceptions as ipam_exc
|
||||||
|
from neutron.ipam import requests as ipam_req
|
||||||
|
|
||||||
|
|
||||||
LOG = logging.getLogger(__name__)
|
LOG = logging.getLogger(__name__)
|
||||||
@ -110,7 +119,6 @@ class IpamPluggableBackend(ipam_backend_mixin.IpamBackendMixin):
|
|||||||
ip_address, ip_subnet = self._ipam_allocate_single_ip(
|
ip_address, ip_subnet = self._ipam_allocate_single_ip(
|
||||||
context, ipam_driver, port, ip_list)
|
context, ipam_driver, port, ip_list)
|
||||||
allocated.append({'ip_address': ip_address,
|
allocated.append({'ip_address': ip_address,
|
||||||
'subnet_cidr': ip_subnet['subnet_cidr'],
|
|
||||||
'subnet_id': ip_subnet['subnet_id']})
|
'subnet_id': ip_subnet['subnet_id']})
|
||||||
except Exception:
|
except Exception:
|
||||||
with excutils.save_and_reraise_exception():
|
with excutils.save_and_reraise_exception():
|
||||||
@ -127,3 +135,317 @@ class IpamPluggableBackend(ipam_backend_mixin.IpamBackendMixin):
|
|||||||
"external system for %s"), addresses)
|
"external system for %s"), addresses)
|
||||||
|
|
||||||
return allocated
|
return allocated
|
||||||
|
|
||||||
|
def _ipam_update_allocation_pools(self, context, ipam_driver, subnet):
|
||||||
|
self._validate_allocation_pools(subnet['allocation_pools'],
|
||||||
|
subnet['cidr'])
|
||||||
|
|
||||||
|
factory = ipam_driver.get_subnet_request_factory()
|
||||||
|
subnet_request = factory.get_request(context, subnet, None)
|
||||||
|
|
||||||
|
ipam_driver.update_subnet(subnet_request)
|
||||||
|
|
||||||
|
def delete_subnet(self, context, subnet_id):
|
||||||
|
ipam_driver = driver.Pool.get_instance(None, context)
|
||||||
|
ipam_driver.remove_subnet(subnet_id)
|
||||||
|
|
||||||
|
def allocate_ips_for_port_and_store(self, context, port, port_id):
|
||||||
|
network_id = port['port']['network_id']
|
||||||
|
ips = []
|
||||||
|
try:
|
||||||
|
ips = self._allocate_ips_for_port(context, port)
|
||||||
|
for ip in ips:
|
||||||
|
ip_address = ip['ip_address']
|
||||||
|
subnet_id = ip['subnet_id']
|
||||||
|
IpamPluggableBackend._store_ip_allocation(
|
||||||
|
context, ip_address, network_id,
|
||||||
|
subnet_id, port_id)
|
||||||
|
except Exception:
|
||||||
|
with excutils.save_and_reraise_exception():
|
||||||
|
if ips:
|
||||||
|
LOG.debug("An exception occurred during port creation."
|
||||||
|
"Reverting IP allocation")
|
||||||
|
ipam_driver = driver.Pool.get_instance(None, context)
|
||||||
|
self._ipam_deallocate_ips(context, ipam_driver,
|
||||||
|
port['port'], ips,
|
||||||
|
revert_on_fail=False)
|
||||||
|
|
||||||
|
def _allocate_ips_for_port(self, context, port):
|
||||||
|
"""Allocate IP addresses for the port. IPAM version.
|
||||||
|
|
||||||
|
If port['fixed_ips'] is set to 'ATTR_NOT_SPECIFIED', allocate IP
|
||||||
|
addresses for the port. If port['fixed_ips'] contains an IP address or
|
||||||
|
a subnet_id then allocate an IP address accordingly.
|
||||||
|
"""
|
||||||
|
p = port['port']
|
||||||
|
ips = []
|
||||||
|
v6_stateless = []
|
||||||
|
net_id_filter = {'network_id': [p['network_id']]}
|
||||||
|
subnets = self._get_subnets(context, filters=net_id_filter)
|
||||||
|
is_router_port = (
|
||||||
|
p['device_owner'] in constants.ROUTER_INTERFACE_OWNERS_SNAT)
|
||||||
|
|
||||||
|
fixed_configured = p['fixed_ips'] is not attributes.ATTR_NOT_SPECIFIED
|
||||||
|
if fixed_configured:
|
||||||
|
ips = self._test_fixed_ips_for_port(context,
|
||||||
|
p["network_id"],
|
||||||
|
p['fixed_ips'],
|
||||||
|
p['device_owner'])
|
||||||
|
# For ports that are not router ports, implicitly include all
|
||||||
|
# auto-address subnets for address association.
|
||||||
|
if not is_router_port:
|
||||||
|
v6_stateless += [subnet for subnet in subnets
|
||||||
|
if ipv6_utils.is_auto_address_subnet(subnet)]
|
||||||
|
else:
|
||||||
|
# Split into v4, v6 stateless and v6 stateful subnets
|
||||||
|
v4 = []
|
||||||
|
v6_stateful = []
|
||||||
|
for subnet in subnets:
|
||||||
|
if subnet['ip_version'] == 4:
|
||||||
|
v4.append(subnet)
|
||||||
|
else:
|
||||||
|
if ipv6_utils.is_auto_address_subnet(subnet):
|
||||||
|
if not is_router_port:
|
||||||
|
v6_stateless.append(subnet)
|
||||||
|
else:
|
||||||
|
v6_stateful.append(subnet)
|
||||||
|
|
||||||
|
version_subnets = [v4, v6_stateful]
|
||||||
|
for subnets in version_subnets:
|
||||||
|
if subnets:
|
||||||
|
ips.append([{'subnet_id': s['id']}
|
||||||
|
for s in subnets])
|
||||||
|
|
||||||
|
for subnet in v6_stateless:
|
||||||
|
# IP addresses for IPv6 SLAAC and DHCPv6-stateless subnets
|
||||||
|
# are implicitly included.
|
||||||
|
ips.append({'subnet_id': subnet['id'],
|
||||||
|
'subnet_cidr': subnet['cidr'],
|
||||||
|
'eui64_address': True,
|
||||||
|
'mac': p['mac_address']})
|
||||||
|
ipam_driver = driver.Pool.get_instance(None, context)
|
||||||
|
return self._ipam_allocate_ips(context, ipam_driver, p, ips)
|
||||||
|
|
||||||
|
def _test_fixed_ips_for_port(self, context, network_id, fixed_ips,
|
||||||
|
device_owner):
|
||||||
|
"""Test fixed IPs for port.
|
||||||
|
|
||||||
|
Check that configured subnets are valid prior to allocating any
|
||||||
|
IPs. Include the subnet_id in the result if only an IP address is
|
||||||
|
configured.
|
||||||
|
|
||||||
|
:raises: InvalidInput, IpAddressInUse, InvalidIpForNetwork,
|
||||||
|
InvalidIpForSubnet
|
||||||
|
"""
|
||||||
|
fixed_ip_list = []
|
||||||
|
for fixed in fixed_ips:
|
||||||
|
subnet = self._get_subnet_for_fixed_ip(context, fixed, network_id)
|
||||||
|
|
||||||
|
is_auto_addr_subnet = ipv6_utils.is_auto_address_subnet(subnet)
|
||||||
|
if 'ip_address' in fixed:
|
||||||
|
if (is_auto_addr_subnet and device_owner not in
|
||||||
|
constants.ROUTER_INTERFACE_OWNERS):
|
||||||
|
msg = (_("IPv6 address %(address)s can not be directly "
|
||||||
|
"assigned to a port on subnet %(id)s since the "
|
||||||
|
"subnet is configured for automatic addresses") %
|
||||||
|
{'address': fixed['ip_address'],
|
||||||
|
'id': subnet['id']})
|
||||||
|
raise n_exc.InvalidInput(error_message=msg)
|
||||||
|
fixed_ip_list.append({'subnet_id': subnet['id'],
|
||||||
|
'ip_address': fixed['ip_address']})
|
||||||
|
else:
|
||||||
|
# A scan for auto-address subnets on the network is done
|
||||||
|
# separately so that all such subnets (not just those
|
||||||
|
# listed explicitly here by subnet ID) are associated
|
||||||
|
# with the port.
|
||||||
|
if (device_owner in constants.ROUTER_INTERFACE_OWNERS_SNAT or
|
||||||
|
not is_auto_addr_subnet):
|
||||||
|
fixed_ip_list.append({'subnet_id': subnet['id']})
|
||||||
|
|
||||||
|
self._validate_max_ips_per_port(fixed_ip_list)
|
||||||
|
return fixed_ip_list
|
||||||
|
|
||||||
|
def _update_ips_for_port(self, context, port,
|
||||||
|
original_ips, new_ips, mac):
|
||||||
|
"""Add or remove IPs from the port. IPAM version"""
|
||||||
|
added = []
|
||||||
|
removed = []
|
||||||
|
changes = self._get_changed_ips_for_port(
|
||||||
|
context, original_ips, new_ips, port['device_owner'])
|
||||||
|
# Check if the IP's to add are OK
|
||||||
|
to_add = self._test_fixed_ips_for_port(
|
||||||
|
context, port['network_id'], changes.add,
|
||||||
|
port['device_owner'])
|
||||||
|
|
||||||
|
ipam_driver = driver.Pool.get_instance(None, context)
|
||||||
|
if changes.remove:
|
||||||
|
removed = self._ipam_deallocate_ips(context, ipam_driver, port,
|
||||||
|
changes.remove)
|
||||||
|
if to_add:
|
||||||
|
added = self._ipam_allocate_ips(context, ipam_driver,
|
||||||
|
changes, to_add)
|
||||||
|
return self.Changes(add=added,
|
||||||
|
original=changes.original,
|
||||||
|
remove=removed)
|
||||||
|
|
||||||
|
def save_allocation_pools(self, context, subnet, allocation_pools):
|
||||||
|
for pool in allocation_pools:
|
||||||
|
first_ip = str(netaddr.IPAddress(pool.first, pool.version))
|
||||||
|
last_ip = str(netaddr.IPAddress(pool.last, pool.version))
|
||||||
|
ip_pool = models_v2.IPAllocationPool(subnet=subnet,
|
||||||
|
first_ip=first_ip,
|
||||||
|
last_ip=last_ip)
|
||||||
|
context.session.add(ip_pool)
|
||||||
|
|
||||||
|
def update_port_with_ips(self, context, db_port, new_port, new_mac):
|
||||||
|
changes = self.Changes(add=[], original=[], remove=[])
|
||||||
|
|
||||||
|
if 'fixed_ips' in new_port:
|
||||||
|
original = self._make_port_dict(db_port,
|
||||||
|
process_extensions=False)
|
||||||
|
changes = self._update_ips_for_port(context,
|
||||||
|
db_port,
|
||||||
|
original["fixed_ips"],
|
||||||
|
new_port['fixed_ips'],
|
||||||
|
new_mac)
|
||||||
|
try:
|
||||||
|
# Check if the IPs need to be updated
|
||||||
|
network_id = db_port['network_id']
|
||||||
|
for ip in changes.add:
|
||||||
|
self._store_ip_allocation(
|
||||||
|
context, ip['ip_address'], network_id,
|
||||||
|
ip['subnet_id'], db_port.id)
|
||||||
|
for ip in changes.remove:
|
||||||
|
self._delete_ip_allocation(context, network_id,
|
||||||
|
ip['subnet_id'], ip['ip_address'])
|
||||||
|
self._update_db_port(context, db_port, new_port, network_id,
|
||||||
|
new_mac)
|
||||||
|
except Exception:
|
||||||
|
with excutils.save_and_reraise_exception():
|
||||||
|
if 'fixed_ips' in new_port:
|
||||||
|
LOG.debug("An exception occurred during port update.")
|
||||||
|
ipam_driver = driver.Pool.get_instance(None, context)
|
||||||
|
if changes.add:
|
||||||
|
LOG.debug("Reverting IP allocation.")
|
||||||
|
self._ipam_deallocate_ips(context, ipam_driver,
|
||||||
|
db_port, changes.add,
|
||||||
|
revert_on_fail=False)
|
||||||
|
if changes.remove:
|
||||||
|
LOG.debug("Reverting IP deallocation.")
|
||||||
|
self._ipam_allocate_ips(context, ipam_driver,
|
||||||
|
db_port, changes.remove,
|
||||||
|
revert_on_fail=False)
|
||||||
|
return changes
|
||||||
|
|
||||||
|
def delete_port(self, context, id):
|
||||||
|
# Get fixed_ips list before port deletion
|
||||||
|
port = self._get_port(context, id)
|
||||||
|
ipam_driver = driver.Pool.get_instance(None, context)
|
||||||
|
|
||||||
|
super(IpamPluggableBackend, self).delete_port(context, id)
|
||||||
|
# Deallocating ips via IPAM after port is deleted locally.
|
||||||
|
# So no need to do rollback actions on remote server
|
||||||
|
# in case of fail to delete port locally
|
||||||
|
self._ipam_deallocate_ips(context, ipam_driver, port,
|
||||||
|
port['fixed_ips'])
|
||||||
|
|
||||||
|
def update_db_subnet(self, context, id, s, old_pools):
|
||||||
|
ipam_driver = driver.Pool.get_instance(None, context)
|
||||||
|
if "allocation_pools" in s:
|
||||||
|
self._ipam_update_allocation_pools(context, ipam_driver, s)
|
||||||
|
|
||||||
|
try:
|
||||||
|
subnet, changes = super(IpamPluggableBackend,
|
||||||
|
self).update_db_subnet(context, id,
|
||||||
|
s, old_pools)
|
||||||
|
except Exception:
|
||||||
|
with excutils.save_and_reraise_exception():
|
||||||
|
if "allocation_pools" in s and old_pools:
|
||||||
|
LOG.error(
|
||||||
|
_LE("An exception occurred during subnet update."
|
||||||
|
"Reverting allocation pool changes"))
|
||||||
|
s['allocation_pools'] = old_pools
|
||||||
|
self._ipam_update_allocation_pools(context, ipam_driver, s)
|
||||||
|
return subnet, changes
|
||||||
|
|
||||||
|
def add_auto_addrs_on_network_ports(self, context, subnet, ipam_subnet):
|
||||||
|
"""For an auto-address subnet, add addrs for ports on the net."""
|
||||||
|
with context.session.begin(subtransactions=True):
|
||||||
|
network_id = subnet['network_id']
|
||||||
|
port_qry = context.session.query(models_v2.Port)
|
||||||
|
ports = port_qry.filter(
|
||||||
|
and_(models_v2.Port.network_id == network_id,
|
||||||
|
~models_v2.Port.device_owner.in_(
|
||||||
|
constants.ROUTER_INTERFACE_OWNERS_SNAT)))
|
||||||
|
for port in ports:
|
||||||
|
ip_request = ipam_req.AutomaticAddressRequest(
|
||||||
|
prefix=subnet['cidr'],
|
||||||
|
mac=port['mac_address'])
|
||||||
|
ip_address = ipam_subnet.allocate(ip_request)
|
||||||
|
allocated = models_v2.IPAllocation(network_id=network_id,
|
||||||
|
port_id=port['id'],
|
||||||
|
ip_address=ip_address,
|
||||||
|
subnet_id=subnet['id'])
|
||||||
|
try:
|
||||||
|
# Do the insertion of each IP allocation entry within
|
||||||
|
# the context of a nested transaction, so that the entry
|
||||||
|
# is rolled back independently of other entries whenever
|
||||||
|
# the corresponding port has been deleted.
|
||||||
|
with context.session.begin_nested():
|
||||||
|
context.session.add(allocated)
|
||||||
|
except db_exc.DBReferenceError:
|
||||||
|
LOG.debug("Port %s was deleted while updating it with an "
|
||||||
|
"IPv6 auto-address. Ignoring.", port['id'])
|
||||||
|
LOG.debug("Reverting IP allocation for %s", ip_address)
|
||||||
|
# Do not fail if reverting allocation was unsuccessful
|
||||||
|
try:
|
||||||
|
ipam_subnet.deallocate(ip_address)
|
||||||
|
except Exception:
|
||||||
|
LOG.debug("Reverting IP allocation failed for %s",
|
||||||
|
ip_address)
|
||||||
|
|
||||||
|
def allocate_subnet(self, context, network, subnet, subnetpool_id):
|
||||||
|
subnetpool = None
|
||||||
|
|
||||||
|
if subnetpool_id:
|
||||||
|
subnetpool = self._get_subnetpool(context, subnetpool_id)
|
||||||
|
self._validate_ip_version_with_subnetpool(subnet, subnetpool)
|
||||||
|
|
||||||
|
# gateway_ip and allocation pools should be validated or generated
|
||||||
|
# only for specific request
|
||||||
|
if subnet['cidr'] is not attributes.ATTR_NOT_SPECIFIED:
|
||||||
|
subnet['gateway_ip'] = self._gateway_ip_str(subnet,
|
||||||
|
subnet['cidr'])
|
||||||
|
subnet['allocation_pools'] = self._prepare_allocation_pools(
|
||||||
|
subnet['allocation_pools'],
|
||||||
|
subnet['cidr'],
|
||||||
|
subnet['gateway_ip'])
|
||||||
|
|
||||||
|
ipam_driver = driver.Pool.get_instance(subnetpool, context)
|
||||||
|
subnet_factory = ipam_driver.get_subnet_request_factory()
|
||||||
|
subnet_request = subnet_factory.get_request(context, subnet,
|
||||||
|
subnetpool)
|
||||||
|
ipam_subnet = ipam_driver.allocate_subnet(subnet_request)
|
||||||
|
# get updated details with actually allocated subnet
|
||||||
|
subnet_request = ipam_subnet.get_details()
|
||||||
|
|
||||||
|
try:
|
||||||
|
subnet = self._save_subnet(context,
|
||||||
|
network,
|
||||||
|
self._make_subnet_args(
|
||||||
|
subnet_request,
|
||||||
|
subnet,
|
||||||
|
subnetpool_id),
|
||||||
|
subnet['dns_nameservers'],
|
||||||
|
subnet['host_routes'],
|
||||||
|
subnet_request)
|
||||||
|
except Exception:
|
||||||
|
# Note(pbondar): Third-party ipam servers can't rely
|
||||||
|
# on transaction rollback, so explicit rollback call needed.
|
||||||
|
# IPAM part rolled back in exception handling
|
||||||
|
# and subnet part is rolled back by transaction rollback.
|
||||||
|
with excutils.save_and_reraise_exception():
|
||||||
|
LOG.debug("An exception occurred during subnet creation."
|
||||||
|
"Reverting subnet allocation.")
|
||||||
|
self.delete_subnet(context, subnet_request.subnet_id)
|
||||||
|
return subnet, ipam_subnet
|
||||||
|
@ -148,14 +148,3 @@ class Subnet(object):
|
|||||||
|
|
||||||
:returns: An instance of SpecificSubnetRequest with the subnet detail.
|
:returns: An instance of SpecificSubnetRequest with the subnet detail.
|
||||||
"""
|
"""
|
||||||
|
|
||||||
@abc.abstractmethod
|
|
||||||
def associate_neutron_subnet(self, subnet_id):
|
|
||||||
"""Associate the IPAM subnet with a neutron subnet.
|
|
||||||
|
|
||||||
This operation should be performed to attach a neutron subnet to the
|
|
||||||
current subnet instance. In some cases IPAM subnets may be created
|
|
||||||
independently of neutron subnets and associated at a later stage.
|
|
||||||
|
|
||||||
:param subnet_id: neutron subnet identifier.
|
|
||||||
"""
|
|
||||||
|
@ -54,11 +54,18 @@ class IpamSubnetManager(object):
|
|||||||
session.add(ipam_subnet)
|
session.add(ipam_subnet)
|
||||||
return self._ipam_subnet_id
|
return self._ipam_subnet_id
|
||||||
|
|
||||||
def associate_neutron_id(self, session, neutron_subnet_id):
|
@classmethod
|
||||||
session.query(db_models.IpamSubnet).filter_by(
|
def delete(cls, session, neutron_subnet_id):
|
||||||
id=self._ipam_subnet_id).update(
|
"""Delete IPAM subnet.
|
||||||
{'neutron_subnet_id': neutron_subnet_id})
|
|
||||||
self._neutron_subnet_id = neutron_subnet_id
|
IPAM subnet no longer has foreign key to neutron subnet,
|
||||||
|
so need to perform delete manually
|
||||||
|
|
||||||
|
:param session: database sesssion
|
||||||
|
:param neutron_subnet_id: neutron subnet id associated with ipam subnet
|
||||||
|
"""
|
||||||
|
return session.query(db_models.IpamSubnet).filter_by(
|
||||||
|
neutron_subnet_id=neutron_subnet_id).delete()
|
||||||
|
|
||||||
def create_pool(self, session, pool_start, pool_end):
|
def create_pool(self, session, pool_start, pool_end):
|
||||||
"""Create an allocation pool and availability ranges for the subnet.
|
"""Create an allocation pool and availability ranges for the subnet.
|
||||||
|
@ -56,7 +56,7 @@ class NeutronDbSubnet(ipam_base.Subnet):
|
|||||||
ipam_subnet_id = uuidutils.generate_uuid()
|
ipam_subnet_id = uuidutils.generate_uuid()
|
||||||
subnet_manager = ipam_db_api.IpamSubnetManager(
|
subnet_manager = ipam_db_api.IpamSubnetManager(
|
||||||
ipam_subnet_id,
|
ipam_subnet_id,
|
||||||
None)
|
subnet_request.subnet_id)
|
||||||
# Create subnet resource
|
# Create subnet resource
|
||||||
session = ctx.session
|
session = ctx.session
|
||||||
subnet_manager.create(session)
|
subnet_manager.create(session)
|
||||||
@ -76,8 +76,7 @@ class NeutronDbSubnet(ipam_base.Subnet):
|
|||||||
allocation_pools=pools,
|
allocation_pools=pools,
|
||||||
gateway_ip=subnet_request.gateway_ip,
|
gateway_ip=subnet_request.gateway_ip,
|
||||||
tenant_id=subnet_request.tenant_id,
|
tenant_id=subnet_request.tenant_id,
|
||||||
subnet_id=subnet_request.subnet_id,
|
subnet_id=subnet_request.subnet_id)
|
||||||
subnet_id_not_set=True)
|
|
||||||
|
|
||||||
@classmethod
|
@classmethod
|
||||||
def load(cls, neutron_subnet_id, ctx):
|
def load(cls, neutron_subnet_id, ctx):
|
||||||
@ -88,7 +87,7 @@ class NeutronDbSubnet(ipam_base.Subnet):
|
|||||||
ipam_subnet = ipam_db_api.IpamSubnetManager.load_by_neutron_subnet_id(
|
ipam_subnet = ipam_db_api.IpamSubnetManager.load_by_neutron_subnet_id(
|
||||||
ctx.session, neutron_subnet_id)
|
ctx.session, neutron_subnet_id)
|
||||||
if not ipam_subnet:
|
if not ipam_subnet:
|
||||||
LOG.error(_LE("Unable to retrieve IPAM subnet as the referenced "
|
LOG.error(_LE("IPAM subnet referenced to "
|
||||||
"Neutron subnet %s does not exist"),
|
"Neutron subnet %s does not exist"),
|
||||||
neutron_subnet_id)
|
neutron_subnet_id)
|
||||||
raise n_exc.SubnetNotFound(subnet_id=neutron_subnet_id)
|
raise n_exc.SubnetNotFound(subnet_id=neutron_subnet_id)
|
||||||
@ -113,7 +112,7 @@ class NeutronDbSubnet(ipam_base.Subnet):
|
|||||||
|
|
||||||
def __init__(self, internal_id, ctx, cidr=None,
|
def __init__(self, internal_id, ctx, cidr=None,
|
||||||
allocation_pools=None, gateway_ip=None, tenant_id=None,
|
allocation_pools=None, gateway_ip=None, tenant_id=None,
|
||||||
subnet_id=None, subnet_id_not_set=False):
|
subnet_id=None):
|
||||||
# NOTE: In theory it could have been possible to grant the IPAM
|
# NOTE: In theory it could have been possible to grant the IPAM
|
||||||
# driver direct access to the database. While this is possible,
|
# driver direct access to the database. While this is possible,
|
||||||
# it would have led to duplicate code and/or non-trivial
|
# it would have led to duplicate code and/or non-trivial
|
||||||
@ -124,7 +123,7 @@ class NeutronDbSubnet(ipam_base.Subnet):
|
|||||||
self._pools = allocation_pools
|
self._pools = allocation_pools
|
||||||
self._gateway_ip = gateway_ip
|
self._gateway_ip = gateway_ip
|
||||||
self._tenant_id = tenant_id
|
self._tenant_id = tenant_id
|
||||||
self._subnet_id = None if subnet_id_not_set else subnet_id
|
self._subnet_id = subnet_id
|
||||||
self.subnet_manager = ipam_db_api.IpamSubnetManager(internal_id,
|
self.subnet_manager = ipam_db_api.IpamSubnetManager(internal_id,
|
||||||
self._subnet_id)
|
self._subnet_id)
|
||||||
self._context = ctx
|
self._context = ctx
|
||||||
@ -363,17 +362,6 @@ class NeutronDbSubnet(ipam_base.Subnet):
|
|||||||
self._tenant_id, self.subnet_manager.neutron_id,
|
self._tenant_id, self.subnet_manager.neutron_id,
|
||||||
self._cidr, self._gateway_ip, self._pools)
|
self._cidr, self._gateway_ip, self._pools)
|
||||||
|
|
||||||
def associate_neutron_subnet(self, subnet_id):
|
|
||||||
"""Set neutron identifier for this subnet"""
|
|
||||||
session = self._context.session
|
|
||||||
if self._subnet_id:
|
|
||||||
raise
|
|
||||||
# IPAMSubnet does not have foreign key to Subnet,
|
|
||||||
# so need verify subnet existence.
|
|
||||||
NeutronDbSubnet._fetch_subnet(self._context, subnet_id)
|
|
||||||
self.subnet_manager.associate_neutron_id(session, subnet_id)
|
|
||||||
self._subnet_id = subnet_id
|
|
||||||
|
|
||||||
|
|
||||||
class NeutronDbPool(subnet_alloc.SubnetAllocator):
|
class NeutronDbPool(subnet_alloc.SubnetAllocator):
|
||||||
"""Subnet pools backed by Neutron Database.
|
"""Subnet pools backed by Neutron Database.
|
||||||
@ -429,10 +417,16 @@ class NeutronDbPool(subnet_alloc.SubnetAllocator):
|
|||||||
subnet.update_allocation_pools(subnet_request.allocation_pools)
|
subnet.update_allocation_pools(subnet_request.allocation_pools)
|
||||||
return subnet
|
return subnet
|
||||||
|
|
||||||
def remove_subnet(self, subnet):
|
def remove_subnet(self, subnet_id):
|
||||||
"""Remove data structures for a given subnet.
|
"""Remove data structures for a given subnet.
|
||||||
|
|
||||||
All the IPAM-related data are cleared when a subnet is deleted thanks
|
IPAM-related data has no foreign key relationships to neutron subnet,
|
||||||
to cascaded foreign key relationships.
|
so removing ipam subnet manually
|
||||||
"""
|
"""
|
||||||
pass
|
count = ipam_db_api.IpamSubnetManager.delete(self._context.session,
|
||||||
|
subnet_id)
|
||||||
|
if count < 1:
|
||||||
|
LOG.error(_LE("IPAM subnet referenced to "
|
||||||
|
"Neutron subnet %s does not exist"),
|
||||||
|
subnet_id)
|
||||||
|
raise n_exc.SubnetNotFound(subnet_id=subnet_id)
|
||||||
|
@ -193,9 +193,6 @@ class IpamSubnet(driver.Subnet):
|
|||||||
def get_details(self):
|
def get_details(self):
|
||||||
return self._req
|
return self._req
|
||||||
|
|
||||||
def associate_neutron_subnet(self, subnet_id):
|
|
||||||
pass
|
|
||||||
|
|
||||||
|
|
||||||
class SubnetPoolReader(object):
|
class SubnetPoolReader(object):
|
||||||
'''Class to assist with reading a subnetpool, loading defaults, and
|
'''Class to assist with reading a subnetpool, loading defaults, and
|
||||||
|
@ -24,6 +24,7 @@ from neutron import context
|
|||||||
from neutron.db import db_base_plugin_v2 as base_plugin
|
from neutron.db import db_base_plugin_v2 as base_plugin
|
||||||
from neutron.db import model_base
|
from neutron.db import model_base
|
||||||
from neutron.db import models_v2
|
from neutron.db import models_v2
|
||||||
|
from neutron.ipam.drivers.neutrondb_ipam import db_models as ipam_models
|
||||||
from neutron.tests import base
|
from neutron.tests import base
|
||||||
from neutron.tests.common import base as common_base
|
from neutron.tests.common import base as common_base
|
||||||
|
|
||||||
@ -47,9 +48,13 @@ class IpamTestCase(object):
|
|||||||
Base class for tests that aim to test ip allocation.
|
Base class for tests that aim to test ip allocation.
|
||||||
"""
|
"""
|
||||||
|
|
||||||
def configure_test(self):
|
def configure_test(self, use_pluggable_ipam=False):
|
||||||
model_base.BASEV2.metadata.create_all(self.engine)
|
model_base.BASEV2.metadata.create_all(self.engine)
|
||||||
cfg.CONF.set_override('notify_nova_on_port_status_changes', False)
|
cfg.CONF.set_override('notify_nova_on_port_status_changes', False)
|
||||||
|
if use_pluggable_ipam:
|
||||||
|
self._turn_on_pluggable_ipam()
|
||||||
|
else:
|
||||||
|
self._turn_off_pluggable_ipam()
|
||||||
self.plugin = base_plugin.NeutronDbPluginV2()
|
self.plugin = base_plugin.NeutronDbPluginV2()
|
||||||
self.cxt = get_admin_test_context(self.engine.url)
|
self.cxt = get_admin_test_context(self.engine.url)
|
||||||
self.addCleanup(self.cxt._session.close)
|
self.addCleanup(self.cxt._session.close)
|
||||||
@ -60,6 +65,16 @@ class IpamTestCase(object):
|
|||||||
self._create_network()
|
self._create_network()
|
||||||
self._create_subnet()
|
self._create_subnet()
|
||||||
|
|
||||||
|
def _turn_off_pluggable_ipam(self):
|
||||||
|
cfg.CONF.set_override('ipam_driver', None)
|
||||||
|
self.ip_availability_range = models_v2.IPAvailabilityRange
|
||||||
|
|
||||||
|
def _turn_on_pluggable_ipam(self):
|
||||||
|
cfg.CONF.set_override('ipam_driver', 'internal')
|
||||||
|
DB_PLUGIN_KLASS = 'neutron.db.db_base_plugin_v2.NeutronDbPluginV2'
|
||||||
|
self.setup_coreplugin(DB_PLUGIN_KLASS)
|
||||||
|
self.ip_availability_range = ipam_models.IpamAvailabilityRange
|
||||||
|
|
||||||
def result_set_to_dicts(self, resultset, keys):
|
def result_set_to_dicts(self, resultset, keys):
|
||||||
dicts = []
|
dicts = []
|
||||||
for item in resultset:
|
for item in resultset:
|
||||||
@ -75,7 +90,7 @@ class IpamTestCase(object):
|
|||||||
|
|
||||||
def assert_ip_avail_range_matches(self, expected):
|
def assert_ip_avail_range_matches(self, expected):
|
||||||
result_set = self.cxt.session.query(
|
result_set = self.cxt.session.query(
|
||||||
models_v2.IPAvailabilityRange).all()
|
self.ip_availability_range).all()
|
||||||
keys = ['first_ip', 'last_ip']
|
keys = ['first_ip', 'last_ip']
|
||||||
actual = self.result_set_to_dicts(result_set, keys)
|
actual = self.result_set_to_dicts(result_set, keys)
|
||||||
self.assertEqual(expected, actual)
|
self.assertEqual(expected, actual)
|
||||||
@ -218,3 +233,19 @@ class TestIpamPsql(common_base.PostgreSQLTestCase,
|
|||||||
def setUp(self):
|
def setUp(self):
|
||||||
super(TestIpamPsql, self).setUp()
|
super(TestIpamPsql, self).setUp()
|
||||||
self.configure_test()
|
self.configure_test()
|
||||||
|
|
||||||
|
|
||||||
|
class TestPluggableIpamMySql(common_base.MySQLTestCase,
|
||||||
|
base.BaseTestCase, IpamTestCase):
|
||||||
|
|
||||||
|
def setUp(self):
|
||||||
|
super(TestPluggableIpamMySql, self).setUp()
|
||||||
|
self.configure_test(use_pluggable_ipam=True)
|
||||||
|
|
||||||
|
|
||||||
|
class TestPluggableIpamPsql(common_base.PostgreSQLTestCase,
|
||||||
|
base.BaseTestCase, IpamTestCase):
|
||||||
|
|
||||||
|
def setUp(self):
|
||||||
|
super(TestPluggableIpamPsql, self).setUp()
|
||||||
|
self.configure_test(use_pluggable_ipam=True)
|
||||||
|
@ -3278,6 +3278,9 @@ class TestSubnetsV2(NeutronDbPluginV2TestCase):
|
|||||||
[{'start': '10.0.0.2', 'end': '10.0.0.254'},
|
[{'start': '10.0.0.2', 'end': '10.0.0.254'},
|
||||||
{'end': '10.0.0.254'}],
|
{'end': '10.0.0.254'}],
|
||||||
None,
|
None,
|
||||||
|
[{'start': '10.0.0.200', 'end': '10.0.3.20'}],
|
||||||
|
[{'start': '10.0.2.250', 'end': '10.0.3.5'}],
|
||||||
|
[{'start': '10.0.2.10', 'end': '10.0.2.5'}],
|
||||||
[{'start': '10.0.0.2', 'end': '10.0.0.3'},
|
[{'start': '10.0.0.2', 'end': '10.0.0.3'},
|
||||||
{'start': '10.0.0.2', 'end': '10.0.0.3'}]]
|
{'start': '10.0.0.2', 'end': '10.0.0.3'}]]
|
||||||
tenant_id = network['network']['tenant_id']
|
tenant_id = network['network']['tenant_id']
|
||||||
@ -3816,7 +3819,7 @@ class TestSubnetsV2(NeutronDbPluginV2TestCase):
|
|||||||
return orig(s, instance)
|
return orig(s, instance)
|
||||||
mock.patch.object(orm.Session, 'add',
|
mock.patch.object(orm.Session, 'add',
|
||||||
new=db_ref_err_for_ipalloc).start()
|
new=db_ref_err_for_ipalloc).start()
|
||||||
mock.patch.object(non_ipam.IpamNonPluggableBackend,
|
mock.patch.object(db_base_plugin_common.DbBasePluginCommon,
|
||||||
'_get_subnet',
|
'_get_subnet',
|
||||||
return_value=mock.Mock()).start()
|
return_value=mock.Mock()).start()
|
||||||
# Add an IPv6 auto-address subnet to the network
|
# Add an IPv6 auto-address subnet to the network
|
||||||
|
@ -15,18 +15,49 @@
|
|||||||
|
|
||||||
import mock
|
import mock
|
||||||
import netaddr
|
import netaddr
|
||||||
|
import webob.exc
|
||||||
|
|
||||||
|
from oslo_config import cfg
|
||||||
from oslo_utils import uuidutils
|
from oslo_utils import uuidutils
|
||||||
|
|
||||||
from neutron.common import exceptions as n_exc
|
from neutron.common import exceptions as n_exc
|
||||||
from neutron.common import ipv6_utils
|
from neutron.common import ipv6_utils
|
||||||
|
from neutron.db import ipam_backend_mixin
|
||||||
from neutron.db import ipam_pluggable_backend
|
from neutron.db import ipam_pluggable_backend
|
||||||
from neutron.ipam import requests as ipam_req
|
from neutron.ipam import requests as ipam_req
|
||||||
from neutron.tests.unit.db import test_db_base_plugin_v2 as test_db_base
|
from neutron.tests.unit.db import test_db_base_plugin_v2 as test_db_base
|
||||||
|
|
||||||
|
|
||||||
|
class UseIpamMixin(object):
|
||||||
|
|
||||||
|
def setUp(self):
|
||||||
|
cfg.CONF.set_override("ipam_driver", 'internal')
|
||||||
|
super(UseIpamMixin, self).setUp()
|
||||||
|
|
||||||
|
|
||||||
|
class TestIpamHTTPResponse(UseIpamMixin, test_db_base.TestV2HTTPResponse):
|
||||||
|
pass
|
||||||
|
|
||||||
|
|
||||||
|
class TestIpamPorts(UseIpamMixin, test_db_base.TestPortsV2):
|
||||||
|
pass
|
||||||
|
|
||||||
|
|
||||||
|
class TestIpamNetworks(UseIpamMixin, test_db_base.TestNetworksV2):
|
||||||
|
pass
|
||||||
|
|
||||||
|
|
||||||
|
class TestIpamSubnets(UseIpamMixin, test_db_base.TestSubnetsV2):
|
||||||
|
pass
|
||||||
|
|
||||||
|
|
||||||
|
class TestIpamSubnetPool(UseIpamMixin, test_db_base.TestSubnetPoolsV2):
|
||||||
|
pass
|
||||||
|
|
||||||
|
|
||||||
class TestDbBasePluginIpam(test_db_base.NeutronDbPluginV2TestCase):
|
class TestDbBasePluginIpam(test_db_base.NeutronDbPluginV2TestCase):
|
||||||
def setUp(self):
|
def setUp(self):
|
||||||
|
cfg.CONF.set_override("ipam_driver", 'internal')
|
||||||
super(TestDbBasePluginIpam, self).setUp()
|
super(TestDbBasePluginIpam, self).setUp()
|
||||||
self.tenant_id = uuidutils.generate_uuid()
|
self.tenant_id = uuidutils.generate_uuid()
|
||||||
self.subnet_id = uuidutils.generate_uuid()
|
self.subnet_id = uuidutils.generate_uuid()
|
||||||
@ -56,6 +87,11 @@ class TestDbBasePluginIpam(test_db_base.NeutronDbPluginV2TestCase):
|
|||||||
mocks['ipam'] = ipam_pluggable_backend.IpamPluggableBackend()
|
mocks['ipam'] = ipam_pluggable_backend.IpamPluggableBackend()
|
||||||
return mocks
|
return mocks
|
||||||
|
|
||||||
|
def _prepare_mocks_with_pool_mock(self, pool_mock):
|
||||||
|
mocks = self._prepare_mocks()
|
||||||
|
pool_mock.get_instance.return_value = mocks['driver']
|
||||||
|
return mocks
|
||||||
|
|
||||||
def _get_allocate_mock(self, auto_ip='10.0.0.2',
|
def _get_allocate_mock(self, auto_ip='10.0.0.2',
|
||||||
fail_ip='127.0.0.1',
|
fail_ip='127.0.0.1',
|
||||||
error_message='SomeError'):
|
error_message='SomeError'):
|
||||||
@ -233,3 +269,225 @@ class TestDbBasePluginIpam(test_db_base.NeutronDbPluginV2TestCase):
|
|||||||
self._validate_allocate_calls(ips[:-1], mocks)
|
self._validate_allocate_calls(ips[:-1], mocks)
|
||||||
# Deallocate should be called for the first ip only
|
# Deallocate should be called for the first ip only
|
||||||
mocks['subnet'].deallocate.assert_called_once_with(auto_ip)
|
mocks['subnet'].deallocate.assert_called_once_with(auto_ip)
|
||||||
|
|
||||||
|
@mock.patch('neutron.ipam.driver.Pool')
|
||||||
|
def test_create_subnet_over_ipam(self, pool_mock):
|
||||||
|
mocks = self._prepare_mocks_with_pool_mock(pool_mock)
|
||||||
|
cidr = '192.168.0.0/24'
|
||||||
|
allocation_pools = [{'start': '192.168.0.2', 'end': '192.168.0.254'}]
|
||||||
|
with self.subnet(allocation_pools=allocation_pools,
|
||||||
|
cidr=cidr):
|
||||||
|
pool_mock.get_instance.assert_called_once_with(None, mock.ANY)
|
||||||
|
assert mocks['driver'].allocate_subnet.called
|
||||||
|
request = mocks['driver'].allocate_subnet.call_args[0][0]
|
||||||
|
self.assertEqual(ipam_req.SpecificSubnetRequest, type(request))
|
||||||
|
self.assertEqual(netaddr.IPNetwork(cidr), request.subnet_cidr)
|
||||||
|
|
||||||
|
@mock.patch('neutron.ipam.driver.Pool')
|
||||||
|
def test_create_subnet_over_ipam_with_rollback(self, pool_mock):
|
||||||
|
mocks = self._prepare_mocks_with_pool_mock(pool_mock)
|
||||||
|
mocks['driver'].allocate_subnet.side_effect = ValueError
|
||||||
|
cidr = '10.0.2.0/24'
|
||||||
|
with self.network() as network:
|
||||||
|
self._create_subnet(self.fmt, network['network']['id'],
|
||||||
|
cidr, expected_res_status=500)
|
||||||
|
|
||||||
|
pool_mock.get_instance.assert_called_once_with(None, mock.ANY)
|
||||||
|
assert mocks['driver'].allocate_subnet.called
|
||||||
|
request = mocks['driver'].allocate_subnet.call_args[0][0]
|
||||||
|
self.assertEqual(ipam_req.SpecificSubnetRequest, type(request))
|
||||||
|
self.assertEqual(netaddr.IPNetwork(cidr), request.subnet_cidr)
|
||||||
|
# Verify no subnet was created for network
|
||||||
|
req = self.new_show_request('networks', network['network']['id'])
|
||||||
|
res = req.get_response(self.api)
|
||||||
|
net = self.deserialize(self.fmt, res)
|
||||||
|
self.assertEqual(0, len(net['network']['subnets']))
|
||||||
|
|
||||||
|
@mock.patch('neutron.ipam.driver.Pool')
|
||||||
|
def test_ipam_subnet_deallocated_if_create_fails(self, pool_mock):
|
||||||
|
mocks = self._prepare_mocks_with_pool_mock(pool_mock)
|
||||||
|
cidr = '10.0.2.0/24'
|
||||||
|
with mock.patch.object(
|
||||||
|
ipam_backend_mixin.IpamBackendMixin, '_save_subnet',
|
||||||
|
side_effect=ValueError), self.network() as network:
|
||||||
|
self._create_subnet(self.fmt, network['network']['id'],
|
||||||
|
cidr, expected_res_status=500)
|
||||||
|
pool_mock.get_instance.assert_any_call(None, mock.ANY)
|
||||||
|
self.assertEqual(2, pool_mock.get_instance.call_count)
|
||||||
|
assert mocks['driver'].allocate_subnet.called
|
||||||
|
request = mocks['driver'].allocate_subnet.call_args[0][0]
|
||||||
|
self.assertEqual(ipam_req.SpecificSubnetRequest, type(request))
|
||||||
|
self.assertEqual(netaddr.IPNetwork(cidr), request.subnet_cidr)
|
||||||
|
# Verify remove ipam subnet was called
|
||||||
|
mocks['driver'].remove_subnet.assert_called_once_with(
|
||||||
|
self.subnet_id)
|
||||||
|
|
||||||
|
@mock.patch('neutron.ipam.driver.Pool')
|
||||||
|
def test_update_subnet_over_ipam(self, pool_mock):
|
||||||
|
mocks = self._prepare_mocks_with_pool_mock(pool_mock)
|
||||||
|
cidr = '10.0.0.0/24'
|
||||||
|
allocation_pools = [{'start': '10.0.0.2', 'end': '10.0.0.254'}]
|
||||||
|
with self.subnet(allocation_pools=allocation_pools,
|
||||||
|
cidr=cidr) as subnet:
|
||||||
|
data = {'subnet': {'allocation_pools': [
|
||||||
|
{'start': '10.0.0.10', 'end': '10.0.0.20'},
|
||||||
|
{'start': '10.0.0.30', 'end': '10.0.0.40'}]}}
|
||||||
|
req = self.new_update_request('subnets', data,
|
||||||
|
subnet['subnet']['id'])
|
||||||
|
res = req.get_response(self.api)
|
||||||
|
self.assertEqual(res.status_code, 200)
|
||||||
|
|
||||||
|
pool_mock.get_instance.assert_any_call(None, mock.ANY)
|
||||||
|
self.assertEqual(2, pool_mock.get_instance.call_count)
|
||||||
|
assert mocks['driver'].update_subnet.called
|
||||||
|
request = mocks['driver'].update_subnet.call_args[0][0]
|
||||||
|
self.assertEqual(ipam_req.SpecificSubnetRequest, type(request))
|
||||||
|
self.assertEqual(netaddr.IPNetwork(cidr), request.subnet_cidr)
|
||||||
|
|
||||||
|
ip_ranges = [netaddr.IPRange(p['start'],
|
||||||
|
p['end']) for p in data['subnet']['allocation_pools']]
|
||||||
|
self.assertEqual(ip_ranges, request.allocation_pools)
|
||||||
|
|
||||||
|
@mock.patch('neutron.ipam.driver.Pool')
|
||||||
|
def test_delete_subnet_over_ipam(self, pool_mock):
|
||||||
|
mocks = self._prepare_mocks_with_pool_mock(pool_mock)
|
||||||
|
gateway_ip = '10.0.0.1'
|
||||||
|
cidr = '10.0.0.0/24'
|
||||||
|
res = self._create_network(fmt=self.fmt, name='net',
|
||||||
|
admin_state_up=True)
|
||||||
|
network = self.deserialize(self.fmt, res)
|
||||||
|
subnet = self._make_subnet(self.fmt, network, gateway_ip,
|
||||||
|
cidr, ip_version=4)
|
||||||
|
req = self.new_delete_request('subnets', subnet['subnet']['id'])
|
||||||
|
res = req.get_response(self.api)
|
||||||
|
self.assertEqual(res.status_int, webob.exc.HTTPNoContent.code)
|
||||||
|
|
||||||
|
pool_mock.get_instance.assert_any_call(None, mock.ANY)
|
||||||
|
self.assertEqual(2, pool_mock.get_instance.call_count)
|
||||||
|
mocks['driver'].remove_subnet.assert_called_once_with(
|
||||||
|
subnet['subnet']['id'])
|
||||||
|
|
||||||
|
@mock.patch('neutron.ipam.driver.Pool')
|
||||||
|
def test_delete_subnet_over_ipam_with_rollback(self, pool_mock):
|
||||||
|
mocks = self._prepare_mocks_with_pool_mock(pool_mock)
|
||||||
|
mocks['driver'].remove_subnet.side_effect = ValueError
|
||||||
|
gateway_ip = '10.0.0.1'
|
||||||
|
cidr = '10.0.0.0/24'
|
||||||
|
res = self._create_network(fmt=self.fmt, name='net',
|
||||||
|
admin_state_up=True)
|
||||||
|
network = self.deserialize(self.fmt, res)
|
||||||
|
subnet = self._make_subnet(self.fmt, network, gateway_ip,
|
||||||
|
cidr, ip_version=4)
|
||||||
|
req = self.new_delete_request('subnets', subnet['subnet']['id'])
|
||||||
|
res = req.get_response(self.api)
|
||||||
|
self.assertEqual(res.status_int, webob.exc.HTTPServerError.code)
|
||||||
|
|
||||||
|
pool_mock.get_instance.assert_any_call(None, mock.ANY)
|
||||||
|
self.assertEqual(2, pool_mock.get_instance.call_count)
|
||||||
|
mocks['driver'].remove_subnet.assert_called_once_with(
|
||||||
|
subnet['subnet']['id'])
|
||||||
|
# Verify subnet was recreated after failed ipam call
|
||||||
|
subnet_req = self.new_show_request('subnets',
|
||||||
|
subnet['subnet']['id'])
|
||||||
|
raw_res = subnet_req.get_response(self.api)
|
||||||
|
sub_res = self.deserialize(self.fmt, raw_res)
|
||||||
|
self.assertIn(sub_res['subnet']['cidr'], cidr)
|
||||||
|
self.assertIn(sub_res['subnet']['gateway_ip'],
|
||||||
|
gateway_ip)
|
||||||
|
|
||||||
|
@mock.patch('neutron.ipam.driver.Pool')
|
||||||
|
def test_create_port_ipam(self, pool_mock):
|
||||||
|
mocks = self._prepare_mocks_with_pool_mock(pool_mock)
|
||||||
|
auto_ip = '10.0.0.2'
|
||||||
|
expected_calls = [{'ip_address': ''}]
|
||||||
|
mocks['subnet'].allocate.side_effect = self._get_allocate_mock(
|
||||||
|
auto_ip=auto_ip)
|
||||||
|
with self.subnet() as subnet:
|
||||||
|
with self.port(subnet=subnet) as port:
|
||||||
|
ips = port['port']['fixed_ips']
|
||||||
|
self.assertEqual(1, len(ips))
|
||||||
|
self.assertEqual(ips[0]['ip_address'], auto_ip)
|
||||||
|
self.assertEqual(ips[0]['subnet_id'], subnet['subnet']['id'])
|
||||||
|
self._validate_allocate_calls(expected_calls, mocks)
|
||||||
|
|
||||||
|
@mock.patch('neutron.ipam.driver.Pool')
|
||||||
|
def test_create_port_ipam_with_rollback(self, pool_mock):
|
||||||
|
mocks = self._prepare_mocks_with_pool_mock(pool_mock)
|
||||||
|
mocks['subnet'].allocate.side_effect = ValueError
|
||||||
|
with self.network() as network:
|
||||||
|
with self.subnet(network=network):
|
||||||
|
net_id = network['network']['id']
|
||||||
|
data = {
|
||||||
|
'port': {'network_id': net_id,
|
||||||
|
'tenant_id': network['network']['tenant_id']}}
|
||||||
|
port_req = self.new_create_request('ports', data)
|
||||||
|
res = port_req.get_response(self.api)
|
||||||
|
self.assertEqual(res.status_int,
|
||||||
|
webob.exc.HTTPServerError.code)
|
||||||
|
|
||||||
|
# verify no port left after failure
|
||||||
|
req = self.new_list_request('ports', self.fmt,
|
||||||
|
"network_id=%s" % net_id)
|
||||||
|
res = self.deserialize(self.fmt, req.get_response(self.api))
|
||||||
|
self.assertEqual(0, len(res['ports']))
|
||||||
|
|
||||||
|
@mock.patch('neutron.ipam.driver.Pool')
|
||||||
|
def test_update_port_ipam(self, pool_mock):
|
||||||
|
mocks = self._prepare_mocks_with_pool_mock(pool_mock)
|
||||||
|
auto_ip = '10.0.0.2'
|
||||||
|
new_ip = '10.0.0.15'
|
||||||
|
expected_calls = [{'ip_address': ip} for ip in ['', new_ip]]
|
||||||
|
mocks['subnet'].allocate.side_effect = self._get_allocate_mock(
|
||||||
|
auto_ip=auto_ip)
|
||||||
|
with self.subnet() as subnet:
|
||||||
|
with self.port(subnet=subnet) as port:
|
||||||
|
ips = port['port']['fixed_ips']
|
||||||
|
self.assertEqual(1, len(ips))
|
||||||
|
self.assertEqual(ips[0]['ip_address'], auto_ip)
|
||||||
|
# Update port with another new ip
|
||||||
|
data = {"port": {"fixed_ips": [{
|
||||||
|
'subnet_id': subnet['subnet']['id'],
|
||||||
|
'ip_address': new_ip}]}}
|
||||||
|
req = self.new_update_request('ports', data,
|
||||||
|
port['port']['id'])
|
||||||
|
res = self.deserialize(self.fmt, req.get_response(self.api))
|
||||||
|
ips = res['port']['fixed_ips']
|
||||||
|
self.assertEqual(1, len(ips))
|
||||||
|
self.assertEqual(new_ip, ips[0]['ip_address'])
|
||||||
|
|
||||||
|
# Allocate should be called for the first two networks
|
||||||
|
self._validate_allocate_calls(expected_calls, mocks)
|
||||||
|
# Deallocate should be called for the first ip only
|
||||||
|
mocks['subnet'].deallocate.assert_called_once_with(auto_ip)
|
||||||
|
|
||||||
|
@mock.patch('neutron.ipam.driver.Pool')
|
||||||
|
def test_delete_port_ipam(self, pool_mock):
|
||||||
|
mocks = self._prepare_mocks_with_pool_mock(pool_mock)
|
||||||
|
auto_ip = '10.0.0.2'
|
||||||
|
mocks['subnet'].allocate.side_effect = self._get_allocate_mock(
|
||||||
|
auto_ip=auto_ip)
|
||||||
|
with self.subnet() as subnet:
|
||||||
|
with self.port(subnet=subnet) as port:
|
||||||
|
ips = port['port']['fixed_ips']
|
||||||
|
self.assertEqual(1, len(ips))
|
||||||
|
self.assertEqual(ips[0]['ip_address'], auto_ip)
|
||||||
|
req = self.new_delete_request('ports', port['port']['id'])
|
||||||
|
res = req.get_response(self.api)
|
||||||
|
|
||||||
|
self.assertEqual(res.status_int, webob.exc.HTTPNoContent.code)
|
||||||
|
mocks['subnet'].deallocate.assert_called_once_with(auto_ip)
|
||||||
|
|
||||||
|
def test_recreate_port_ipam(self):
|
||||||
|
ip = '10.0.0.2'
|
||||||
|
with self.subnet() as subnet:
|
||||||
|
with self.port(subnet=subnet) as port:
|
||||||
|
ips = port['port']['fixed_ips']
|
||||||
|
self.assertEqual(1, len(ips))
|
||||||
|
self.assertEqual(ips[0]['ip_address'], ip)
|
||||||
|
req = self.new_delete_request('ports', port['port']['id'])
|
||||||
|
res = req.get_response(self.api)
|
||||||
|
self.assertEqual(res.status_int, webob.exc.HTTPNoContent.code)
|
||||||
|
with self.port(subnet=subnet, fixed_ips=ips) as port:
|
||||||
|
ips = port['port']['fixed_ips']
|
||||||
|
self.assertEqual(1, len(ips))
|
||||||
|
self.assertEqual(ips[0]['ip_address'], ip)
|
||||||
|
@ -43,12 +43,18 @@ class TestIpamSubnetManager(testlib_api.SqlTestCase):
|
|||||||
id=self.ipam_subnet_id).all()
|
id=self.ipam_subnet_id).all()
|
||||||
self.assertEqual(1, len(subnets))
|
self.assertEqual(1, len(subnets))
|
||||||
|
|
||||||
def test_associate_neutron_id(self):
|
def test_remove(self):
|
||||||
self.subnet_manager.associate_neutron_id(self.ctx.session,
|
count = db_api.IpamSubnetManager.delete(self.ctx.session,
|
||||||
'test-id')
|
self.neutron_subnet_id)
|
||||||
subnet = self.ctx.session.query(db_models.IpamSubnet).filter_by(
|
self.assertEqual(1, count)
|
||||||
id=self.ipam_subnet_id).first()
|
subnets = self.ctx.session.query(db_models.IpamSubnet).filter_by(
|
||||||
self.assertEqual('test-id', subnet['neutron_subnet_id'])
|
id=self.ipam_subnet_id).all()
|
||||||
|
self.assertEqual(0, len(subnets))
|
||||||
|
|
||||||
|
def test_remove_non_existent_subnet(self):
|
||||||
|
count = db_api.IpamSubnetManager.delete(self.ctx.session,
|
||||||
|
'non-existent')
|
||||||
|
self.assertEqual(0, count)
|
||||||
|
|
||||||
def _create_pools(self, pools):
|
def _create_pools(self, pools):
|
||||||
db_pools = []
|
db_pools = []
|
||||||
|
@ -144,8 +144,7 @@ class TestNeutronDbIpamPool(testlib_api.SqlTestCase,
|
|||||||
def test_update_subnet_pools(self):
|
def test_update_subnet_pools(self):
|
||||||
cidr = '10.0.0.0/24'
|
cidr = '10.0.0.0/24'
|
||||||
subnet, subnet_req = self._prepare_specific_subnet_request(cidr)
|
subnet, subnet_req = self._prepare_specific_subnet_request(cidr)
|
||||||
ipam_subnet = self.ipam_pool.allocate_subnet(subnet_req)
|
self.ipam_pool.allocate_subnet(subnet_req)
|
||||||
ipam_subnet.associate_neutron_subnet(subnet['id'])
|
|
||||||
allocation_pools = [netaddr.IPRange('10.0.0.100', '10.0.0.150'),
|
allocation_pools = [netaddr.IPRange('10.0.0.100', '10.0.0.150'),
|
||||||
netaddr.IPRange('10.0.0.200', '10.0.0.250')]
|
netaddr.IPRange('10.0.0.200', '10.0.0.250')]
|
||||||
update_subnet_req = ipam_req.SpecificSubnetRequest(
|
update_subnet_req = ipam_req.SpecificSubnetRequest(
|
||||||
@ -162,8 +161,7 @@ class TestNeutronDbIpamPool(testlib_api.SqlTestCase,
|
|||||||
def test_get_subnet(self):
|
def test_get_subnet(self):
|
||||||
cidr = '10.0.0.0/24'
|
cidr = '10.0.0.0/24'
|
||||||
subnet, subnet_req = self._prepare_specific_subnet_request(cidr)
|
subnet, subnet_req = self._prepare_specific_subnet_request(cidr)
|
||||||
ipam_subnet = self.ipam_pool.allocate_subnet(subnet_req)
|
self.ipam_pool.allocate_subnet(subnet_req)
|
||||||
ipam_subnet.associate_neutron_subnet(subnet['id'])
|
|
||||||
# Retrieve the subnet
|
# Retrieve the subnet
|
||||||
ipam_subnet = self.ipam_pool.get_subnet(subnet['id'])
|
ipam_subnet = self.ipam_pool.get_subnet(subnet['id'])
|
||||||
self._verify_ipam_subnet_details(
|
self._verify_ipam_subnet_details(
|
||||||
@ -176,6 +174,30 @@ class TestNeutronDbIpamPool(testlib_api.SqlTestCase,
|
|||||||
self.ipam_pool.get_subnet,
|
self.ipam_pool.get_subnet,
|
||||||
'boo')
|
'boo')
|
||||||
|
|
||||||
|
def test_remove_ipam_subnet(self):
|
||||||
|
cidr = '10.0.0.0/24'
|
||||||
|
subnet, subnet_req = self._prepare_specific_subnet_request(cidr)
|
||||||
|
self.ipam_pool.allocate_subnet(subnet_req)
|
||||||
|
# Remove ipam subnet by neutron subnet id
|
||||||
|
self.ipam_pool.remove_subnet(subnet['id'])
|
||||||
|
|
||||||
|
def test_remove_non_existent_subnet_fails(self):
|
||||||
|
self.assertRaises(n_exc.SubnetNotFound,
|
||||||
|
self.ipam_pool.remove_subnet,
|
||||||
|
'non-existent-id')
|
||||||
|
|
||||||
|
def test_get_details_for_invalid_subnet_id_fails(self):
|
||||||
|
cidr = '10.0.0.0/24'
|
||||||
|
subnet_req = ipam_req.SpecificSubnetRequest(
|
||||||
|
self._tenant_id,
|
||||||
|
'non-existent-id',
|
||||||
|
cidr)
|
||||||
|
self.ipam_pool.allocate_subnet(subnet_req)
|
||||||
|
# Neutron subnet does not exist, so get_subnet should fail
|
||||||
|
self.assertRaises(n_exc.SubnetNotFound,
|
||||||
|
self.ipam_pool.get_subnet,
|
||||||
|
'non-existent-id')
|
||||||
|
|
||||||
|
|
||||||
class TestNeutronDbIpamSubnet(testlib_api.SqlTestCase,
|
class TestNeutronDbIpamSubnet(testlib_api.SqlTestCase,
|
||||||
TestNeutronDbIpamMixin):
|
TestNeutronDbIpamMixin):
|
||||||
@ -214,7 +236,6 @@ class TestNeutronDbIpamSubnet(testlib_api.SqlTestCase,
|
|||||||
gateway_ip=subnet['gateway_ip'],
|
gateway_ip=subnet['gateway_ip'],
|
||||||
allocation_pools=allocation_pool_ranges)
|
allocation_pools=allocation_pool_ranges)
|
||||||
ipam_subnet = self.ipam_pool.allocate_subnet(subnet_req)
|
ipam_subnet = self.ipam_pool.allocate_subnet(subnet_req)
|
||||||
ipam_subnet.associate_neutron_subnet(subnet['id'])
|
|
||||||
return ipam_subnet, subnet
|
return ipam_subnet, subnet
|
||||||
|
|
||||||
def setUp(self):
|
def setUp(self):
|
||||||
@ -314,7 +335,7 @@ class TestNeutronDbIpamSubnet(testlib_api.SqlTestCase,
|
|||||||
subnet = self._create_subnet(
|
subnet = self._create_subnet(
|
||||||
self.plugin, self.ctx, self.net_id, cidr)
|
self.plugin, self.ctx, self.net_id, cidr)
|
||||||
subnet_req = ipam_req.SpecificSubnetRequest(
|
subnet_req = ipam_req.SpecificSubnetRequest(
|
||||||
'tenant_id', subnet, cidr, gateway_ip=subnet['gateway_ip'])
|
'tenant_id', subnet['id'], cidr, gateway_ip=subnet['gateway_ip'])
|
||||||
ipam_subnet = self.ipam_pool.allocate_subnet(subnet_req)
|
ipam_subnet = self.ipam_pool.allocate_subnet(subnet_req)
|
||||||
with self.ctx.session.begin():
|
with self.ctx.session.begin():
|
||||||
ranges = ipam_subnet._allocate_specific_ip(
|
ranges = ipam_subnet._allocate_specific_ip(
|
||||||
@ -416,28 +437,10 @@ class TestNeutronDbIpamSubnet(testlib_api.SqlTestCase,
|
|||||||
# This test instead might be made to pass, but for the wrong reasons!
|
# This test instead might be made to pass, but for the wrong reasons!
|
||||||
pass
|
pass
|
||||||
|
|
||||||
def _test_allocate_subnet(self, subnet_id):
|
|
||||||
subnet_req = ipam_req.SpecificSubnetRequest(
|
|
||||||
'tenant_id', subnet_id, '192.168.0.0/24')
|
|
||||||
return self.ipam_pool.allocate_subnet(subnet_req)
|
|
||||||
|
|
||||||
def test_allocate_subnet_for_non_existent_subnet_pass(self):
|
def test_allocate_subnet_for_non_existent_subnet_pass(self):
|
||||||
# This test should pass because neutron subnet is not checked
|
# This test should pass because ipam subnet is no longer
|
||||||
# until associate neutron subnet step
|
# have foreign key relationship with neutron subnet.
|
||||||
|
# Creating ipam subnet before neutron subnet is a valid case.
|
||||||
subnet_req = ipam_req.SpecificSubnetRequest(
|
subnet_req = ipam_req.SpecificSubnetRequest(
|
||||||
'tenant_id', 'meh', '192.168.0.0/24')
|
'tenant_id', 'meh', '192.168.0.0/24')
|
||||||
self.ipam_pool.allocate_subnet(subnet_req)
|
self.ipam_pool.allocate_subnet(subnet_req)
|
||||||
|
|
||||||
def test_associate_neutron_subnet(self):
|
|
||||||
ipam_subnet, subnet = self._create_and_allocate_ipam_subnet(
|
|
||||||
'192.168.0.0/24', ip_version=4)
|
|
||||||
details = ipam_subnet.get_details()
|
|
||||||
self.assertEqual(subnet['id'], details.subnet_id)
|
|
||||||
|
|
||||||
def test_associate_non_existing_neutron_subnet_fails(self):
|
|
||||||
subnet_req = ipam_req.SpecificSubnetRequest(
|
|
||||||
'tenant_id', 'meh', '192.168.0.0/24')
|
|
||||||
ipam_subnet = self.ipam_pool.allocate_subnet(subnet_req)
|
|
||||||
self.assertRaises(n_exc.SubnetNotFound,
|
|
||||||
ipam_subnet.associate_neutron_subnet,
|
|
||||||
'meh')
|
|
||||||
|
@ -193,6 +193,8 @@ class KeyStoneInfo(object):
|
|||||||
|
|
||||||
class ContrailPluginTestCase(test_plugin.NeutronDbPluginV2TestCase):
|
class ContrailPluginTestCase(test_plugin.NeutronDbPluginV2TestCase):
|
||||||
_plugin_name = ('%s.NeutronPluginContrailCoreV2' % CONTRAIL_PKG_PATH)
|
_plugin_name = ('%s.NeutronPluginContrailCoreV2' % CONTRAIL_PKG_PATH)
|
||||||
|
_fetch = ('neutron.ipam.drivers.neutrondb_ipam.driver.NeutronDbSubnet'
|
||||||
|
'._fetch_subnet')
|
||||||
|
|
||||||
def setUp(self, plugin=None, ext_mgr=None):
|
def setUp(self, plugin=None, ext_mgr=None):
|
||||||
if 'v6' in self._testMethodName:
|
if 'v6' in self._testMethodName:
|
||||||
@ -201,6 +203,7 @@ class ContrailPluginTestCase(test_plugin.NeutronDbPluginV2TestCase):
|
|||||||
self.skipTest("OpenContrail Plugin does not support subnet pools.")
|
self.skipTest("OpenContrail Plugin does not support subnet pools.")
|
||||||
cfg.CONF.keystone_authtoken = KeyStoneInfo()
|
cfg.CONF.keystone_authtoken = KeyStoneInfo()
|
||||||
mock.patch('requests.post').start().side_effect = FAKE_SERVER.request
|
mock.patch('requests.post').start().side_effect = FAKE_SERVER.request
|
||||||
|
mock.patch(self._fetch).start().side_effect = FAKE_SERVER._get_subnet
|
||||||
super(ContrailPluginTestCase, self).setUp(self._plugin_name)
|
super(ContrailPluginTestCase, self).setUp(self._plugin_name)
|
||||||
|
|
||||||
|
|
||||||
|
1
tox.ini
1
tox.ini
@ -158,7 +158,6 @@ commands = python -m testtools.run \
|
|||||||
neutron.tests.unit.scheduler.test_dhcp_agent_scheduler \
|
neutron.tests.unit.scheduler.test_dhcp_agent_scheduler \
|
||||||
neutron.tests.unit.db.test_ipam_backend_mixin \
|
neutron.tests.unit.db.test_ipam_backend_mixin \
|
||||||
neutron.tests.unit.db.test_l3_dvr_db \
|
neutron.tests.unit.db.test_l3_dvr_db \
|
||||||
neutron.tests.unit.db.test_ipam_pluggable_backend \
|
|
||||||
neutron.tests.unit.db.test_migration \
|
neutron.tests.unit.db.test_migration \
|
||||||
neutron.tests.unit.db.test_agents_db \
|
neutron.tests.unit.db.test_agents_db \
|
||||||
neutron.tests.unit.db.test_dvr_mac_db \
|
neutron.tests.unit.db.test_dvr_mac_db \
|
||||||
|
Loading…
x
Reference in New Issue
Block a user