neutron/neutron/db/ipam_pluggable_backend.py

640 lines
29 KiB
Python

# Copyright (c) 2015 Infoblox Inc.
# 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 copy
import netaddr
from neutron_lib.api.definitions import portbindings
from neutron_lib import constants
from neutron_lib.db import api as db_api
from neutron_lib import exceptions as n_exc
from neutron_lib.objects import utils as obj_utils
from neutron_lib.plugins import constants as plugin_consts
from neutron_lib.plugins import directory
from oslo_db import exception as db_exc
from oslo_log import log as logging
from oslo_utils import excutils
from neutron.common import ipv6_utils
from neutron.db import ipam_backend_mixin
from neutron.ipam import driver
from neutron.ipam import exceptions as ipam_exc
from neutron.objects import ports as port_obj
from neutron.objects import subnet as obj_subnet
LOG = logging.getLogger(__name__)
def get_ip_update_not_allowed_device_owner_list():
l3plugin = directory.get_plugin(plugin_consts.L3)
# The following list is for IPAM to prevent direct update of port
# IP address. Currently it only has some L3 related types.
# L2 plugin can add the same list here, but for now it is not required.
return getattr(l3plugin, 'IP_UPDATE_NOT_ALLOWED_LIST', [])
def is_neutron_built_in_router(context, router_id):
l3plugin = directory.get_plugin(plugin_consts.L3)
return bool(l3plugin and
l3plugin.router_supports_scheduling(context, router_id))
class IpamPluggableBackend(ipam_backend_mixin.IpamBackendMixin):
def _get_failed_ips(self, all_ips, success_ips):
ips_list = (ip_dict['ip_address'] for ip_dict in success_ips)
return (ip_dict['ip_address'] for ip_dict in all_ips
if ip_dict['ip_address'] not in ips_list)
def _safe_rollback(self, func, *args, **kwargs):
"""Calls rollback actions and catch all exceptions.
All exceptions are catched and logged here to prevent rewriting
original exception that triggered rollback action.
"""
try:
func(*args, **kwargs)
except Exception as e:
LOG.warning("Revert failed with: %s", e)
def _ipam_deallocate_ips(self, context, ipam_driver, port, ips,
revert_on_fail=True):
"""Deallocate set of ips over IPAM.
If any single ip deallocation fails, tries to allocate deallocated
ip addresses with fixed ip request
"""
deallocated = []
try:
for ip in ips:
try:
ipam_subnet = ipam_driver.get_subnet(ip['subnet_id'])
ipam_subnet.deallocate(ip['ip_address'])
deallocated.append(ip)
except n_exc.SubnetNotFound:
LOG.debug("Subnet was not found on ip deallocation: %s",
ip)
except Exception:
with excutils.save_and_reraise_exception():
if not ipam_driver.needs_rollback():
return
LOG.debug("An exception occurred during IP deallocation.")
if revert_on_fail and deallocated:
LOG.debug("Reverting deallocation")
# In case of deadlock allocate fails with db error
# and rewrites original exception preventing db_retry
# wrappers from restarting entire api request.
self._safe_rollback(self._ipam_allocate_ips, context,
ipam_driver, port, deallocated,
revert_on_fail=False)
elif not revert_on_fail and ips:
addresses = ', '.join(self._get_failed_ips(ips,
deallocated))
LOG.error("IP deallocation failed on "
"external system for %s", addresses)
return deallocated
def _ipam_allocate_ips(self, context, ipam_driver, port, ips,
revert_on_fail=True):
"""Allocate set of ips over IPAM.
If any single ip allocation fails, tries to deallocate all
allocated ip addresses.
"""
allocated = []
factory = ipam_driver.get_address_request_factory()
# 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
ips.sort(key=lambda x: 'ip_address' not in x)
try:
for ip in ips:
# By default IP info is dict, used to allocate single ip
# from single subnet.
# IP info can be list, used to allocate single ip from
# multiple subnets
ip_list = [ip] if isinstance(ip, dict) else ip
subnets = [ip_dict['subnet_id'] for ip_dict in ip_list]
try:
ip_request = factory.get_request(context, port, ip_list[0])
ipam_allocator = ipam_driver.get_allocator(subnets)
ip_address, subnet_id = ipam_allocator.allocate(ip_request)
except ipam_exc.IpAddressGenerationFailureAllSubnets:
raise n_exc.IpAddressGenerationFailure(
net_id=port['network_id'])
allocated.append({'ip_address': ip_address,
'subnet_id': subnet_id})
except Exception:
with excutils.save_and_reraise_exception():
if not ipam_driver.needs_rollback():
return
LOG.debug("An exception occurred during IP allocation.")
if revert_on_fail and allocated:
LOG.debug("Reverting allocation")
# In case of deadlock deallocation fails with db error
# and rewrites original exception preventing db_retry
# wrappers from restarting entire api request.
self._safe_rollback(self._ipam_deallocate_ips, context,
ipam_driver, port, allocated,
revert_on_fail=False)
elif not revert_on_fail and ips:
addresses = ', '.join(self._get_failed_ips(ips,
allocated))
LOG.error("IP allocation failed on "
"external system for %s", addresses)
return allocated
def _ipam_update_allocation_pools(self, context, ipam_driver, subnet):
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 get_subnet(self, context, subnet_id):
ipam_driver = driver.Pool.get_instance(None, context)
return ipam_driver.get_subnet(subnet_id)
def allocate_ips_for_port_and_store(self, context, port, port_id):
# Make a copy of port dict to prevent changing
# incoming dict by adding 'id' to it.
# Deepcopy doesn't work correctly in this case, because copy of
# ATTR_NOT_SPECIFIED object happens. Address of copied object doesn't
# match original object, so 'is' check fails
# TODO(njohnston): Different behavior is required depending on whether
# a Port object is used or not; once conversion to OVO is complete only
# the first 'if' will be needed
if isinstance(port, port_obj.Port):
port_copy = {"port": self._make_port_dict(
port, process_extensions=False)}
elif 'port' in port:
port_copy = {'port': port['port'].copy()}
else:
port_copy = {'port': port.copy()}
port_copy['port']['id'] = port_id
network_id = port_copy['port']['network_id']
ips = []
try:
ips = self._allocate_ips_for_port(context, port_copy)
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)
return ips
except Exception:
with excutils.save_and_reraise_exception():
if ips:
ipam_driver = driver.Pool.get_instance(None, context)
if not ipam_driver.needs_rollback():
return
LOG.debug("An exception occurred during port creation. "
"Reverting IP allocation")
self._safe_rollback(self._ipam_deallocate_ips, context,
ipam_driver, port_copy['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']
fixed_configured = p['fixed_ips'] is not constants.ATTR_NOT_SPECIFIED
fixed_ips = p['fixed_ips'] if fixed_configured else []
subnets = self._ipam_get_subnets(
context,
network_id=p['network_id'],
host=p.get(portbindings.HOST_ID),
service_type=p.get('device_owner'),
fixed_configured=fixed_configured,
fixed_ips=fixed_ips,
distributed_service=self._is_distributed_service(p))
v4, v6_stateful, v6_stateless = self._classify_subnets(
context, subnets)
if fixed_configured:
ips = self._test_fixed_ips_for_port(context,
p["network_id"],
p['fixed_ips'],
p['device_owner'],
subnets)
else:
ips = []
version_subnets = [v4, v6_stateful]
for subnets in version_subnets:
if subnets:
ips.append([{'subnet_id': s['id']}
for s in subnets])
ips.extend(self._get_auto_address_ips(v6_stateless, p))
ipam_driver = driver.Pool.get_instance(None, context)
return self._ipam_allocate_ips(context, ipam_driver, p, ips)
def _get_auto_address_ips(self, v6_stateless_subnets, port,
exclude_subnet_ids=None):
exclude_subnet_ids = exclude_subnet_ids or []
ips = []
is_router_port = (
port['device_owner'] in constants.ROUTER_INTERFACE_OWNERS_SNAT)
if not is_router_port:
for subnet in v6_stateless_subnets:
if subnet['id'] not in exclude_subnet_ids:
# 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': port['mac_address']})
return ips
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_list = []
for fixed in fixed_ips:
fixed['device_owner'] = device_owner
subnet = self._get_subnet_for_fixed_ip(context, fixed,
subnets, network_id)
is_auto_addr_subnet = ipv6_utils.is_auto_address_subnet(subnet)
if ('ip_address' in fixed and
subnet['cidr'] != constants.PROVISIONAL_IPV6_PD_PREFIX):
if (is_auto_addr_subnet and device_owner not in
constants.ROUTER_INTERFACE_OWNERS):
raise ipam_exc.AllocationOnAutoAddressSubnet(
ip=fixed['ip_address'], subnet_id=subnet['id'])
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']})
return fixed_ip_list
def _check_ip_changed_by_version(self, context, ip_list, version):
for ip in ip_list:
ip_address = ip.get('ip_address')
subnet_id = ip.get('subnet_id')
if ip_address:
ip_addr = netaddr.IPAddress(ip_address)
if ip_addr.version == version:
return True
elif subnet_id:
subnet = obj_subnet.Subnet.get_object(context, id=subnet_id)
if subnet and subnet.ip_version == version:
return True
return False
def _update_ips_for_port(self, context, port, host,
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'])
not_allowed_list = get_ip_update_not_allowed_device_owner_list()
if (port['device_owner'] in not_allowed_list and
is_neutron_built_in_router(context, port['device_id'])):
ip_v4_changed = self._check_ip_changed_by_version(
context, changes.remove + changes.add,
constants.IP_VERSION_4)
if ip_v4_changed:
raise ipam_exc.IPAddressChangeNotAllowed(port_id=port['id'])
try:
subnets = self._ipam_get_subnets(
context, network_id=port['network_id'], host=host,
service_type=port.get('device_owner'), fixed_configured=True,
fixed_ips=changes.add + changes.original,
distributed_service=self._is_distributed_service(port))
except ipam_exc.DeferIpam:
subnets = []
# 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'], subnets)
if port['device_owner'] not in constants.ROUTER_INTERFACE_OWNERS:
to_add += self._update_ips_for_pd_subnet(
context, subnets, changes.add, mac)
ipam_driver = driver.Pool.get_instance(None, context)
if changes.remove:
removed = self._ipam_deallocate_ips(context, ipam_driver, port,
changes.remove)
v6_stateless = self._classify_subnets(
context, subnets)[2]
handled_subnet_ids = [ip['subnet_id'] for ip in
to_add + changes.original + changes.remove]
to_add.extend(self._get_auto_address_ips(
v6_stateless, port, handled_subnet_ids))
if to_add:
added = self._ipam_allocate_ips(context, ipam_driver,
port, to_add)
return self.Changes(add=added,
original=changes.original,
remove=removed)
@db_api.CONTEXT_WRITER
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))
obj_subnet.IPAllocationPool(
context, subnet_id=subnet['id'], start=first_ip,
end=last_ip).create()
def update_port_with_ips(self, context, host, db_port, new_port, new_mac):
changes = self.Changes(add=[], original=[], remove=[])
auto_assign_subnets = []
if new_mac:
original = self._make_port_dict(db_port, process_extensions=False)
if original.get('mac_address') != new_mac:
original_ips = original.get('fixed_ips', [])
# NOTE(hjensas): Only set the default for 'fixed_ips' in
# new_port if the original port or new_port actually have IPs.
# Setting the default to [] breaks deferred IP allocation.
# See Bug: https://bugs.launchpad.net/neutron/+bug/1811905
if original_ips or new_port.get('fixed_ips'):
new_ips = new_port.setdefault('fixed_ips', original_ips)
new_ips_subnets = [new_ip['subnet_id']
for new_ip in new_ips]
for orig_ip in original_ips:
if ipv6_utils.is_eui64_address(orig_ip.get('ip_address')):
subnet_to_delete = {}
subnet_to_delete['subnet_id'] = orig_ip['subnet_id']
subnet_to_delete['delete_subnet'] = True
auto_assign_subnets.append(subnet_to_delete)
try:
i = new_ips_subnets.index(orig_ip['subnet_id'])
new_ips[i] = subnet_to_delete
except ValueError:
new_ips.append(subnet_to_delete)
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,
host,
original["fixed_ips"],
new_port['fixed_ips'],
new_mac)
try:
# Expire the fixed_ips of db_port in current transaction, because
# it will be changed in the following operation and the latest
# data is expected.
context.session.expire(db_port, ['fixed_ips'])
# Check if the IPs need to be updated
network_id = db_port['network_id']
for ip in changes.remove:
self._delete_ip_allocation(context, network_id,
ip['subnet_id'], ip['ip_address'])
for ip in changes.add:
self._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)
if auto_assign_subnets:
port_copy = copy.deepcopy(original)
port_copy.update(new_port)
port_copy['fixed_ips'] = auto_assign_subnets
self.allocate_ips_for_port_and_store(
context, {'port': port_copy}, port_copy['id'])
getattr(db_port, 'fixed_ips') # refresh relationship before return
except Exception:
with excutils.save_and_reraise_exception():
if 'fixed_ips' in new_port:
ipam_driver = driver.Pool.get_instance(None, context)
if not ipam_driver.needs_rollback():
return
LOG.debug("An exception occurred during port update.")
if changes.add:
LOG.debug("Reverting IP allocation.")
self._safe_rollback(self._ipam_deallocate_ips,
context,
ipam_driver,
db_port,
changes.add,
revert_on_fail=False)
if changes.remove:
LOG.debug("Reverting IP deallocation.")
self._safe_rollback(self._ipam_allocate_ips,
context,
ipam_driver,
db_port,
changes.remove,
revert_on_fail=False)
return changes
def delete_port(self, context, id, port=None):
# Get fixed_ips list before port deletion
port = port or 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, subnet_obj=None):
subnet = subnet_obj or obj_subnet.Subnet.get_object(context, id=id)
old_segment_id = subnet.segment_id if subnet else None
if 'segment_id' in s:
self._validate_segment(
context, s['network_id'], s['segment_id'], action='update',
old_segment_id=old_segment_id)
# 'allocation_pools' is removed from 's' in
# _update_subnet_allocation_pools (ipam_backend_mixin),
# so create unchanged copy for ipam driver
subnet_copy = copy.deepcopy(s)
subnet, changes = super(IpamPluggableBackend, self).update_db_subnet(
context, id, s, old_pools, subnet_obj=subnet_obj)
ipam_driver = driver.Pool.get_instance(None, context)
# Set old allocation pools if no new pools are provided by user.
# Passing old pools allows to call ipam driver on each subnet update
# even if allocation pools are not changed. So custom ipam drivers
# are able to track other fields changes on subnet update.
if 'allocation_pools' not in subnet_copy:
subnet_copy['allocation_pools'] = old_pools
self._ipam_update_allocation_pools(context, ipam_driver, subnet_copy)
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."""
network_id = subnet['network_id']
with db_api.CONTEXT_READER.using(context):
ports = port_obj.Port.get_objects(
context, network_id=network_id,
device_owner=obj_utils.NotIn(
constants.ROUTER_INTERFACE_OWNERS_SNAT))
updated_ports = []
ipam_driver = driver.Pool.get_instance(None, context)
factory = ipam_driver.get_address_request_factory()
for port in ports:
# Find candidate subnets based on host_id and existing
# fixed_ips. This will filter subnets on other segments. Only
# allocate if this subnet is a valid candidate.
p = self._make_port_dict(port)
fixed_configured = (p['fixed_ips'] is not
constants.ATTR_NOT_SPECIFIED)
with db_api.CONTEXT_READER.using(context):
subnet_candidates = obj_subnet.Subnet.find_candidate_subnets(
context,
network_id,
p.get(portbindings.HOST_ID),
p.get('device_owner'),
fixed_configured,
p.get('fixed_ips'))
if subnet['id'] not in [s['id'] for s in subnet_candidates]:
continue
ip = {'subnet_id': subnet['id'],
'subnet_cidr': subnet['cidr'],
'eui64_address': True,
'mac': port.mac_address}
ip_request = factory.get_request(context, port, ip)
try:
with db_api.CONTEXT_WRITER.using(context):
ip_address = ipam_subnet.allocate(ip_request)
allocated = port_obj.IPAllocation(
context, network_id=network_id, port_id=port.id,
ip_address=ip_address, subnet_id=subnet['id'])
# 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; since OVO
# already opens a nested transaction, we don't need to do
# it explicitly here.
allocated.create()
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)
LOG.debug("Reverting IP allocation for %s", ip_address)
# Do not fail if reverting allocation was unsuccessful
with db_api.CONTEXT_WRITER.using(context):
try:
ipam_subnet.deallocate(ip_address)
except Exception:
LOG.debug("Reverting IP allocation failed for %s",
ip_address)
except ipam_exc.IpAddressAlreadyAllocated:
LOG.debug("Port %s got IPv6 auto-address in a concurrent "
"create or update port request. Ignoring.",
port.id)
return updated_ports
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, id=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'])
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)
obj_subnet.NetworkSubnetLock.lock_subnet(context, network.id,
subnet.id)
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():
if not ipam_driver.needs_rollback():
return
LOG.debug("An exception occurred during subnet creation. "
"Reverting subnet allocation.")
self._safe_rollback(self.delete_subnet,
context,
subnet_request.subnet_id)
return subnet, ipam_subnet