4148a347b3
With this we enable the deprecation warnings by default. Related-Blueprint: neutron-lib Change-Id: I5b9e53751dd164010e5bbeb15f534ac0fe2a5105
468 lines
22 KiB
Python
468 lines
22 KiB
Python
# Copyright (c) 2015 OpenStack Foundation.
|
|
# All Rights Reserved.
|
|
#
|
|
# Licensed under the Apache License, Version 2.0 (the "License"); you may
|
|
# not use this file except in compliance with the License. You may obtain
|
|
# a copy of the License at
|
|
#
|
|
# http://www.apache.org/licenses/LICENSE-2.0
|
|
#
|
|
# Unless required by applicable law or agreed to in writing, software
|
|
# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
|
|
# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
|
|
# License for the specific language governing permissions and limitations
|
|
# under the License.
|
|
|
|
import netaddr
|
|
from neutron_lib import constants
|
|
from neutron_lib import exceptions as n_exc
|
|
from oslo_db import exception as db_exc
|
|
from oslo_log import log as logging
|
|
from sqlalchemy import and_
|
|
from sqlalchemy import orm
|
|
from sqlalchemy.orm import exc
|
|
|
|
from neutron._i18n import _
|
|
from neutron.common import constants as n_const
|
|
from neutron.common import ipv6_utils
|
|
from neutron.db import ipam_backend_mixin
|
|
from neutron.db import models_v2
|
|
from neutron.ipam import requests as ipam_req
|
|
from neutron.ipam import subnet_alloc
|
|
|
|
LOG = logging.getLogger(__name__)
|
|
|
|
|
|
class IpamNonPluggableBackend(ipam_backend_mixin.IpamBackendMixin):
|
|
|
|
@staticmethod
|
|
def _generate_ip(context, subnets):
|
|
try:
|
|
return IpamNonPluggableBackend._try_generate_ip(context, subnets)
|
|
except n_exc.IpAddressGenerationFailure:
|
|
IpamNonPluggableBackend._rebuild_availability_ranges(context,
|
|
subnets)
|
|
|
|
return IpamNonPluggableBackend._try_generate_ip(context, subnets)
|
|
|
|
@staticmethod
|
|
def _try_generate_ip(context, subnets):
|
|
"""Generate an IP address.
|
|
|
|
The IP address will be generated from one of the subnets defined on
|
|
the network.
|
|
"""
|
|
range_qry = context.session.query(
|
|
models_v2.IPAvailabilityRange).join(
|
|
models_v2.IPAllocationPool).with_lockmode('update')
|
|
for subnet in subnets:
|
|
ip_range = range_qry.filter_by(subnet_id=subnet['id']).first()
|
|
if not ip_range:
|
|
LOG.debug("All IPs from subnet %(subnet_id)s (%(cidr)s) "
|
|
"allocated",
|
|
{'subnet_id': subnet['id'],
|
|
'cidr': subnet['cidr']})
|
|
continue
|
|
ip_address = ip_range['first_ip']
|
|
if ip_range['first_ip'] == ip_range['last_ip']:
|
|
# No more free indices on subnet => delete
|
|
LOG.debug("No more free IP's in slice. Deleting "
|
|
"allocation pool.")
|
|
context.session.delete(ip_range)
|
|
else:
|
|
# increment the first free
|
|
new_first_ip = str(netaddr.IPAddress(ip_address) + 1)
|
|
ip_range['first_ip'] = new_first_ip
|
|
LOG.debug("Allocated IP - %(ip_address)s from %(first_ip)s "
|
|
"to %(last_ip)s",
|
|
{'ip_address': ip_address,
|
|
'first_ip': ip_range['first_ip'],
|
|
'last_ip': ip_range['last_ip']})
|
|
return {'ip_address': ip_address,
|
|
'subnet_id': subnet['id']}
|
|
raise n_exc.IpAddressGenerationFailure(net_id=subnets[0]['network_id'])
|
|
|
|
@staticmethod
|
|
def _rebuild_availability_ranges(context, subnets):
|
|
"""Rebuild availability ranges.
|
|
|
|
This method is called only when there's no more IP available or by
|
|
_update_subnet_allocation_pools. Calling
|
|
_update_subnet_allocation_pools before calling this function deletes
|
|
the IPAllocationPools associated with the subnet that is updating,
|
|
which will result in deleting the IPAvailabilityRange too.
|
|
"""
|
|
ip_qry = context.session.query(
|
|
models_v2.IPAllocation).with_lockmode('update')
|
|
# PostgreSQL does not support select...for update with an outer join.
|
|
# No join is needed here.
|
|
pool_qry = context.session.query(
|
|
models_v2.IPAllocationPool).options(
|
|
orm.noload('available_ranges')).with_lockmode('update')
|
|
for subnet in sorted(subnets):
|
|
LOG.debug("Rebuilding availability ranges for subnet %s",
|
|
subnet)
|
|
|
|
# Create a set of all currently allocated addresses
|
|
ip_qry_results = ip_qry.filter_by(subnet_id=subnet['id'])
|
|
allocations = netaddr.IPSet([netaddr.IPAddress(i['ip_address'])
|
|
for i in ip_qry_results])
|
|
|
|
for pool in pool_qry.filter_by(subnet_id=subnet['id']):
|
|
# Create a set of all addresses in the pool
|
|
poolset = netaddr.IPSet(netaddr.IPRange(pool['first_ip'],
|
|
pool['last_ip']))
|
|
|
|
# Use set difference to find free addresses in the pool
|
|
available = poolset - allocations
|
|
|
|
# Generator compacts an ip set into contiguous ranges
|
|
def ipset_to_ranges(ipset):
|
|
first, last = None, None
|
|
for cidr in ipset.iter_cidrs():
|
|
if last and last + 1 != cidr.first:
|
|
yield netaddr.IPRange(first, last)
|
|
first = None
|
|
first, last = first if first else cidr.first, cidr.last
|
|
if first:
|
|
yield netaddr.IPRange(first, last)
|
|
|
|
# Write the ranges to the db
|
|
for ip_range in ipset_to_ranges(available):
|
|
available_range = models_v2.IPAvailabilityRange(
|
|
allocation_pool_id=pool['id'],
|
|
first_ip=str(netaddr.IPAddress(ip_range.first)),
|
|
last_ip=str(netaddr.IPAddress(ip_range.last)))
|
|
context.session.add(available_range)
|
|
|
|
@staticmethod
|
|
def _allocate_specific_ip(context, subnet_id, ip_address):
|
|
"""Allocate a specific IP address on the subnet."""
|
|
ip = int(netaddr.IPAddress(ip_address))
|
|
range_qry = context.session.query(
|
|
models_v2.IPAvailabilityRange).join(
|
|
models_v2.IPAllocationPool).with_lockmode('update')
|
|
results = range_qry.filter_by(subnet_id=subnet_id)
|
|
for ip_range in results:
|
|
first = int(netaddr.IPAddress(ip_range['first_ip']))
|
|
last = int(netaddr.IPAddress(ip_range['last_ip']))
|
|
if first <= ip <= last:
|
|
if first == last:
|
|
context.session.delete(ip_range)
|
|
return
|
|
elif first == ip:
|
|
new_first_ip = str(netaddr.IPAddress(ip_address) + 1)
|
|
ip_range['first_ip'] = new_first_ip
|
|
return
|
|
elif last == ip:
|
|
new_last_ip = str(netaddr.IPAddress(ip_address) - 1)
|
|
ip_range['last_ip'] = new_last_ip
|
|
return
|
|
else:
|
|
# Adjust the original range to end before ip_address
|
|
old_last_ip = ip_range['last_ip']
|
|
new_last_ip = str(netaddr.IPAddress(ip_address) - 1)
|
|
ip_range['last_ip'] = new_last_ip
|
|
|
|
# Create a new second range for after ip_address
|
|
new_first_ip = str(netaddr.IPAddress(ip_address) + 1)
|
|
new_ip_range = models_v2.IPAvailabilityRange(
|
|
allocation_pool_id=ip_range['allocation_pool_id'],
|
|
first_ip=new_first_ip,
|
|
last_ip=old_last_ip)
|
|
context.session.add(new_ip_range)
|
|
return
|
|
|
|
@staticmethod
|
|
def _check_unique_ip(context, network_id, subnet_id, ip_address):
|
|
"""Validate that the IP address on the subnet is not in use."""
|
|
ip_qry = context.session.query(models_v2.IPAllocation)
|
|
try:
|
|
ip_qry.filter_by(network_id=network_id,
|
|
subnet_id=subnet_id,
|
|
ip_address=ip_address).one()
|
|
except exc.NoResultFound:
|
|
return True
|
|
return False
|
|
|
|
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)
|
|
ip_range = models_v2.IPAvailabilityRange(
|
|
ipallocationpool=ip_pool,
|
|
first_ip=first_ip,
|
|
last_ip=last_ip)
|
|
context.session.add(ip_range)
|
|
|
|
def allocate_ips_for_port_and_store(self, context, port, port_id):
|
|
network_id = port['port']['network_id']
|
|
ips = self._allocate_ips_for_port(context, port)
|
|
if ips:
|
|
for ip in ips:
|
|
ip_address = ip['ip_address']
|
|
subnet_id = ip['subnet_id']
|
|
self._store_ip_allocation(context, ip_address, network_id,
|
|
subnet_id, port_id)
|
|
return ips
|
|
|
|
def update_port_with_ips(self, context, db_port, new_port, new_mac):
|
|
changes = self.Changes(add=[], original=[], remove=[])
|
|
# Check if the IPs need to be updated
|
|
network_id = db_port['network_id']
|
|
if 'fixed_ips' in new_port:
|
|
original = self._make_port_dict(db_port, process_extensions=False)
|
|
changes = self._update_ips_for_port(
|
|
context, network_id,
|
|
original["fixed_ips"], new_port['fixed_ips'],
|
|
original['mac_address'], db_port['device_owner'])
|
|
|
|
# Update ips if necessary
|
|
for ip in changes.add:
|
|
IpamNonPluggableBackend._store_ip_allocation(
|
|
context, ip['ip_address'], network_id,
|
|
ip['subnet_id'], db_port.id)
|
|
self._update_db_port(context, db_port, new_port, network_id, new_mac)
|
|
return changes
|
|
|
|
def _test_fixed_ips_for_port(self, context, network_id, fixed_ips,
|
|
device_owner, subnets):
|
|
"""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_set = []
|
|
for fixed in fixed_ips:
|
|
subnet = self._get_subnet_for_fixed_ip(context, fixed, subnets)
|
|
|
|
is_auto_addr_subnet = ipv6_utils.is_auto_address_subnet(subnet)
|
|
if ('ip_address' in fixed and
|
|
subnet['cidr'] != n_const.PROVISIONAL_IPV6_PD_PREFIX):
|
|
# Ensure that the IP's are unique
|
|
if not IpamNonPluggableBackend._check_unique_ip(
|
|
context, network_id,
|
|
subnet['id'], fixed['ip_address']):
|
|
raise n_exc.IpAddressInUse(net_id=network_id,
|
|
ip_address=fixed['ip_address'])
|
|
|
|
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_set.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_set.append({'subnet_id': subnet['id']})
|
|
|
|
self._validate_max_ips_per_port(fixed_ip_set, device_owner)
|
|
return fixed_ip_set
|
|
|
|
def _allocate_fixed_ips(self, context, fixed_ips, mac_address):
|
|
"""Allocate IP addresses according to the configured fixed_ips."""
|
|
ips = []
|
|
|
|
# we need to start with entries that asked for a specific IP in case
|
|
# those IPs happen to be next in the line for allocation for ones that
|
|
# didn't ask for a specific IP
|
|
fixed_ips.sort(key=lambda x: 'ip_address' not in x)
|
|
for fixed in fixed_ips:
|
|
subnet = self._get_subnet(context, fixed['subnet_id'])
|
|
is_auto_addr = ipv6_utils.is_auto_address_subnet(subnet)
|
|
if 'ip_address' in fixed:
|
|
if not is_auto_addr:
|
|
# Remove the IP address from the allocation pool
|
|
IpamNonPluggableBackend._allocate_specific_ip(
|
|
context, fixed['subnet_id'], fixed['ip_address'])
|
|
ips.append({'ip_address': fixed['ip_address'],
|
|
'subnet_id': fixed['subnet_id']})
|
|
# Only subnet ID is specified => need to generate IP
|
|
# from subnet
|
|
else:
|
|
if is_auto_addr:
|
|
ip_address = self._calculate_ipv6_eui64_addr(context,
|
|
subnet,
|
|
mac_address)
|
|
ips.append({'ip_address': ip_address.format(),
|
|
'subnet_id': subnet['id']})
|
|
else:
|
|
subnets = [subnet]
|
|
# IP address allocation
|
|
result = self._generate_ip(context, subnets)
|
|
ips.append({'ip_address': result['ip_address'],
|
|
'subnet_id': result['subnet_id']})
|
|
return ips
|
|
|
|
def _update_ips_for_port(self, context, network_id, original_ips,
|
|
new_ips, mac_address, device_owner):
|
|
"""Add or remove IPs from the port."""
|
|
added = []
|
|
changes = self._get_changed_ips_for_port(context, original_ips,
|
|
new_ips, device_owner)
|
|
net_id_filter = {'network_id': [network_id]}
|
|
subnets = self._get_subnets(context, filters=net_id_filter)
|
|
# Check if the IP's to add are OK
|
|
to_add = self._test_fixed_ips_for_port(context, network_id,
|
|
changes.add, device_owner,
|
|
subnets)
|
|
for ip in changes.remove:
|
|
LOG.debug("Port update. Hold %s", ip)
|
|
IpamNonPluggableBackend._delete_ip_allocation(context,
|
|
network_id,
|
|
ip['subnet_id'],
|
|
ip['ip_address'])
|
|
|
|
if to_add:
|
|
LOG.debug("Port update. Adding %s", to_add)
|
|
added = self._allocate_fixed_ips(context, to_add, mac_address)
|
|
return self.Changes(add=added,
|
|
original=changes.original,
|
|
remove=changes.remove)
|
|
|
|
def _allocate_ips_for_port(self, context, port):
|
|
"""Allocate IP addresses for the port.
|
|
|
|
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']
|
|
net_id_filter = {'network_id': [p['network_id']]}
|
|
subnets = self._get_subnets(context, filters=net_id_filter)
|
|
|
|
v4, v6_stateful, v6_stateless = self._classify_subnets(
|
|
context, subnets)
|
|
|
|
fixed_configured = p['fixed_ips'] is not constants.ATTR_NOT_SPECIFIED
|
|
if fixed_configured:
|
|
configured_ips = self._test_fixed_ips_for_port(context,
|
|
p["network_id"],
|
|
p['fixed_ips'],
|
|
p['device_owner'],
|
|
subnets)
|
|
ips = self._allocate_fixed_ips(context,
|
|
configured_ips,
|
|
p['mac_address'])
|
|
|
|
else:
|
|
ips = []
|
|
version_subnets = [v4, v6_stateful]
|
|
for subnets in version_subnets:
|
|
if subnets:
|
|
result = IpamNonPluggableBackend._generate_ip(context,
|
|
subnets)
|
|
ips.append({'ip_address': result['ip_address'],
|
|
'subnet_id': result['subnet_id']})
|
|
|
|
is_router_port = (
|
|
p['device_owner'] in constants.ROUTER_INTERFACE_OWNERS_SNAT)
|
|
if not is_router_port:
|
|
# IP addresses for IPv6 SLAAC and DHCPv6-stateless subnets
|
|
# are generated and implicitly included.
|
|
for subnet in v6_stateless:
|
|
ip_address = self._calculate_ipv6_eui64_addr(
|
|
context, subnet, p['mac_address'])
|
|
ips.append({'ip_address': ip_address.format(),
|
|
'subnet_id': subnet['id']})
|
|
|
|
return ips
|
|
|
|
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)))
|
|
updated_ports = []
|
|
for port in ports:
|
|
ip_address = self._calculate_ipv6_eui64_addr(
|
|
context, subnet, port['mac_address'])
|
|
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)
|
|
updated_ports.append(port['id'])
|
|
except db_exc.DBReferenceError:
|
|
LOG.debug("Port %s was deleted while updating it with an "
|
|
"IPv6 auto-address. Ignoring.", port['id'])
|
|
return updated_ports
|
|
|
|
def _calculate_ipv6_eui64_addr(self, context, subnet, mac_addr):
|
|
prefix = subnet['cidr']
|
|
network_id = subnet['network_id']
|
|
ip_address = ipv6_utils.get_ipv6_addr_by_EUI64(
|
|
prefix, mac_addr).format()
|
|
if not self._check_unique_ip(context, network_id,
|
|
subnet['id'], ip_address):
|
|
raise n_exc.IpAddressInUse(net_id=network_id,
|
|
ip_address=ip_address)
|
|
return ip_address
|
|
|
|
def allocate_subnet(self, context, network, subnet, subnetpool_id):
|
|
subnetpool = None
|
|
if subnetpool_id and not subnetpool_id == constants.IPV6_PD_POOL_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 constants.ATTR_NOT_SPECIFIED:
|
|
subnet['gateway_ip'] = self._gateway_ip_str(subnet,
|
|
subnet['cidr'])
|
|
# allocation_pools are converted to list of IPRanges
|
|
subnet['allocation_pools'] = self._prepare_allocation_pools(
|
|
subnet['allocation_pools'],
|
|
subnet['cidr'],
|
|
subnet['gateway_ip'])
|
|
|
|
subnet_request = ipam_req.SubnetRequestFactory.get_request(context,
|
|
subnet,
|
|
subnetpool)
|
|
|
|
if subnetpool_id and not subnetpool_id == constants.IPV6_PD_POOL_ID:
|
|
driver = subnet_alloc.SubnetAllocator(subnetpool, context)
|
|
ipam_subnet = driver.allocate_subnet(subnet_request)
|
|
subnet_request = ipam_subnet.get_details()
|
|
|
|
subnet = self._save_subnet(context,
|
|
network,
|
|
self._make_subnet_args(
|
|
subnet_request,
|
|
subnet,
|
|
subnetpool_id),
|
|
subnet['dns_nameservers'],
|
|
subnet['host_routes'],
|
|
subnet_request)
|
|
# 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
|