def _create_port_choose_fixed_ip(self, fixed_ips): # Neutron will try to allocate IPv4, IPv6, and IPv6 EUI-64 addresses. # We're most interested in the IPv4 address. An IPv4 vip can be # routable from IPv6. Creating a port by network can be used to manage # the dwindling, fragmented IPv4 address space. IPv6 has enough # addresses that a single subnet can always be created that's big # enough to allocate all vips. for fixed_ip in fixed_ips: ip_address = fixed_ip['ip_address'] ip = netaddr.IPAddress(ip_address) if ip.version == 4: return fixed_ip # An EUI-64 address isn't useful as a vip for fixed_ip in fixed_ips: ip_address = fixed_ip['ip_address'] ip = netaddr.IPAddress(ip_address) if ip.version == 6 and not ipv6_utils.is_eui64_address(ip_address): return fixed_ip for fixed_ip in fixed_ips: return fixed_ip
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', []) 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) getattr(db_port, 'fixed_ips') # refresh relationship before return 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']) context.session.refresh(db_port) 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 _allocate_specific_ip(self, session, ip_address, allocation_pool_id=None, auto_generated=False): """Remove an IP address from subnet's availability ranges. This method is supposed to be called from within a database transaction, otherwise atomicity and integrity might not be enforced and the operation might result in incosistent availability ranges for the subnet. :param session: database session :param ip_address: ip address to mark as allocated :param allocation_pool_id: identifier of the allocation pool from which the ip address has been extracted. If not specified this routine will scan all allocation pools. :param auto_generated: indicates whether ip was auto generated :returns: list of IP ranges as instances of IPAvailabilityRange """ # Return immediately for EUI-64 addresses. For this # class of subnets availability ranges do not apply if ipv6_utils.is_eui64_address(ip_address): return LOG.debug( "Removing %(ip_address)s from availability ranges for " "subnet id:%(subnet_id)s", { 'ip_address': ip_address, 'subnet_id': self.subnet_manager.neutron_id }) # Netaddr's IPRange and IPSet objects work very well even with very # large subnets, including IPv6 ones. final_ranges = [] ip_in_pools = False if allocation_pool_id: av_ranges = self.subnet_manager.list_ranges_by_allocation_pool( session, allocation_pool_id) else: av_ranges = self.subnet_manager.list_ranges_by_subnet_id(session) for db_range in av_ranges: initial_ip_set = netaddr.IPSet( netaddr.IPRange(db_range['first_ip'], db_range['last_ip'])) final_ip_set = initial_ip_set - netaddr.IPSet([ip_address]) if not final_ip_set: ip_in_pools = True # Range exhausted - bye bye if not self.subnet_manager.delete_range(session, db_range): raise db_exc.RetryRequest(ipam_exc.IPAllocationFailed) continue if initial_ip_set == final_ip_set: # IP address does not fall within the current range, move # to the next one final_ranges.append(db_range) continue ip_in_pools = True for new_range in final_ip_set.iter_ipranges(): # store new range in database # use netaddr.IPAddress format() method which is equivalent # to str(...) but also enables us to use different # representation formats (if needed) for IPv6. first_ip = netaddr.IPAddress(new_range.first) last_ip = netaddr.IPAddress(new_range.last) if (db_range['first_ip'] == first_ip.format() or db_range['last_ip'] == last_ip.format()): rows = self.subnet_manager.update_range(session, db_range, first_ip=first_ip, last_ip=last_ip) if not rows: raise db_exc.RetryRequest(ipam_exc.IPAllocationFailed) LOG.debug("Adjusted availability range for pool %s", db_range['allocation_pool_id']) final_ranges.append(db_range) else: new_ip_range = self.subnet_manager.create_range( session, db_range['allocation_pool_id'], first_ip.format(), last_ip.format()) LOG.debug("Created availability range for pool %s", new_ip_range['allocation_pool_id']) final_ranges.append(new_ip_range) # If ip is autogenerated it should be present in allocation pools, # so retry if it is not there if auto_generated and not ip_in_pools: raise db_exc.RetryRequest(ipam_exc.IPAllocationFailed) # Most callers might ignore this return value, which is however # useful for testing purposes LOG.debug( "Availability ranges for subnet id %(subnet_id)s " "modified: %(new_ranges)s", { 'subnet_id': self.subnet_manager.neutron_id, 'new_ranges': ", ".join([ "[%s; %s]" % (r['first_ip'], r['last_ip']) for r in final_ranges ]) }) return final_ranges
def _get_changed_ips_for_port(self, context, original_ips, new_ips, device_owner): """Calculate changes in IPs for the port.""" # Collect auto addressed subnet ids that has to be removed on update delete_subnet_ids = set(ip['subnet_id'] for ip in new_ips if ip.get('delete_subnet')) ips = [ ip for ip in new_ips if ip.get('subnet_id') not in delete_subnet_ids ] add_ips, prev_ips, remove_candidates = [], [], [] # Consider fixed_ips that specify a specific address first to see if # they already existed in original_ips or are completely new. orig_by_ip = {ip['ip_address']: ip for ip in original_ips} for ip in ips: if 'ip_address' not in ip: continue original = orig_by_ip.pop(ip['ip_address'], None) if original: prev_ips.append(original) else: add_ips.append(ip) # Consider fixed_ips that don't specify ip_address. Try to match them # up with originals to see if they can be reused. Create a new map of # the remaining, unmatched originals for this step. orig_by_subnet = collections.defaultdict(list) for ip in orig_by_ip.values(): orig_by_subnet[ip['subnet_id']].append(ip) for ip in ips: if 'ip_address' in ip: continue orig = orig_by_subnet.get(ip['subnet_id']) if not orig: add_ips.append(ip) continue # Try to match this new request up with an existing IP orig_ip = orig.pop() if ipv6_utils.is_eui64_address(orig_ip['ip_address']): # In case of EUI64 address, the prefix may have changed so # we want to make sure IPAM gets a chance to re-allocate # it. This is safe in general because EUI-64 addresses # always come out the same given the prefix doesn't change. add_ips.append(ip) remove_candidates.append(orig_ip) else: # Reuse the existing address on this subnet. prev_ips.append(orig_ip) # Iterate through any unclaimed original ips (orig_by_subnet) *and* the # remove_candidates with this compound chain. maybe_remove = itertools.chain( itertools.chain.from_iterable(orig_by_subnet.values()), remove_candidates) # Mark ip for removing if it is not found in new_ips # and subnet requires ip to be set manually. # For auto addressed subnet leave ip unchanged # unless it is explicitly marked for delete. remove_ips = [] for ip in maybe_remove: subnet_id = ip['subnet_id'] ip_required = self._is_ip_required_by_subnet( context, subnet_id, device_owner) if ip_required or subnet_id in delete_subnet_ids: remove_ips.append(ip) else: prev_ips.append(ip) return self.Changes(add=add_ips, original=prev_ips, remove=remove_ips)
def _test_eui_64(self, ips, expected): for ip in ips: self.assertEqual(expected, ipv6_utils.is_eui64_address(ip), "Error on %s" % ip)
def _get_changed_ips_for_port(self, context, original_ips, new_ips, device_owner): """Calculate changes in IPs for the port.""" # Collect auto addressed subnet ids that has to be removed on update delete_subnet_ids = set(ip['subnet_id'] for ip in new_ips if ip.get('delete_subnet')) ips = [ip for ip in new_ips if ip.get('subnet_id') not in delete_subnet_ids] add_ips, prev_ips, remove_candidates = [], [], [] # Consider fixed_ips that specify a specific address first to see if # they already existed in original_ips or are completely new. orig_by_ip = {ip['ip_address']: ip for ip in original_ips} for ip in ips: if 'ip_address' not in ip: continue original = orig_by_ip.pop(ip['ip_address'], None) if original: prev_ips.append(original) else: add_ips.append(ip) # Consider fixed_ips that don't specify ip_address. Try to match them # up with originals to see if they can be reused. Create a new map of # the remaining, unmatched originals for this step. orig_by_subnet = collections.defaultdict(list) for ip in orig_by_ip.values(): orig_by_subnet[ip['subnet_id']].append(ip) for ip in ips: if 'ip_address' in ip: continue orig = orig_by_subnet.get(ip['subnet_id']) if not orig: add_ips.append(ip) continue # Try to match this new request up with an existing IP orig_ip = orig.pop() if ipv6_utils.is_eui64_address(orig_ip['ip_address']): # In case of EUI64 address, the prefix may have changed so # we want to make sure IPAM gets a chance to re-allocate # it. This is safe in general because EUI-64 addresses # always come out the same given the prefix doesn't change. add_ips.append(ip) remove_candidates.append(orig_ip) else: # Reuse the existing address on this subnet. prev_ips.append(orig_ip) # Iterate through any unclaimed original ips (orig_by_subnet) *and* the # remove_candidates with this compound chain. maybe_remove = itertools.chain( itertools.chain.from_iterable(orig_by_subnet.values()), remove_candidates) # Mark ip for removing if it is not found in new_ips # and subnet requires ip to be set manually. # For auto addressed subnet leave ip unchanged # unless it is explicitly marked for delete. remove_ips = [] for ip in maybe_remove: subnet_id = ip['subnet_id'] ip_required = self._is_ip_required_by_subnet(context, subnet_id, device_owner) if ip_required or subnet_id in delete_subnet_ids: remove_ips.append(ip) else: prev_ips.append(ip) return self.Changes(add=add_ips, original=prev_ips, remove=remove_ips)
def _allocate_specific_ip(self, session, ip_address, allocation_pool_id=None, auto_generated=False): """Remove an IP address from subnet's availability ranges. This method is supposed to be called from within a database transaction, otherwise atomicity and integrity might not be enforced and the operation might result in incosistent availability ranges for the subnet. :param session: database session :param ip_address: ip address to mark as allocated :param allocation_pool_id: identifier of the allocation pool from which the ip address has been extracted. If not specified this routine will scan all allocation pools. :param auto_generated: indicates whether ip was auto generated :returns: list of IP ranges as instances of IPAvailabilityRange """ # Return immediately for EUI-64 addresses. For this # class of subnets availability ranges do not apply if ipv6_utils.is_eui64_address(ip_address): return LOG.debug("Removing %(ip_address)s from availability ranges for " "subnet id:%(subnet_id)s", {'ip_address': ip_address, 'subnet_id': self.subnet_manager.neutron_id}) # Netaddr's IPRange and IPSet objects work very well even with very # large subnets, including IPv6 ones. final_ranges = [] ip_in_pools = False if allocation_pool_id: av_ranges = self.subnet_manager.list_ranges_by_allocation_pool( session, allocation_pool_id) else: av_ranges = self.subnet_manager.list_ranges_by_subnet_id(session) for db_range in av_ranges: initial_ip_set = netaddr.IPSet(netaddr.IPRange( db_range['first_ip'], db_range['last_ip'])) final_ip_set = initial_ip_set - netaddr.IPSet([ip_address]) if not final_ip_set: ip_in_pools = True # Range exhausted - bye bye if not self.subnet_manager.delete_range(session, db_range): raise db_exc.RetryRequest(ipam_exc.IPAllocationFailed()) continue if initial_ip_set == final_ip_set: # IP address does not fall within the current range, move # to the next one final_ranges.append(db_range) continue ip_in_pools = True for new_range in final_ip_set.iter_ipranges(): # store new range in database # use netaddr.IPAddress format() method which is equivalent # to str(...) but also enables us to use different # representation formats (if needed) for IPv6. first_ip = netaddr.IPAddress(new_range.first) last_ip = netaddr.IPAddress(new_range.last) if (db_range['first_ip'] == first_ip.format() or db_range['last_ip'] == last_ip.format()): rows = self.subnet_manager.update_range( session, db_range, first_ip=first_ip, last_ip=last_ip) if not rows: raise db_exc.RetryRequest( ipam_exc.IPAllocationFailed()) LOG.debug("Adjusted availability range for pool %s", db_range['allocation_pool_id']) final_ranges.append(db_range) else: new_ip_range = self.subnet_manager.create_range( session, db_range['allocation_pool_id'], first_ip.format(), last_ip.format()) LOG.debug("Created availability range for pool %s", new_ip_range['allocation_pool_id']) final_ranges.append(new_ip_range) # If ip is autogenerated it should be present in allocation pools, # so retry if it is not there if auto_generated and not ip_in_pools: raise db_exc.RetryRequest(ipam_exc.IPAllocationFailed()) # Most callers might ignore this return value, which is however # useful for testing purposes LOG.debug("Availability ranges for subnet id %(subnet_id)s " "modified: %(new_ranges)s", {'subnet_id': self.subnet_manager.neutron_id, 'new_ranges': ", ".join(["[%s; %s]" % (r['first_ip'], r['last_ip']) for r in final_ranges])}) return final_ranges
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', []) 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) getattr(db_port, 'fixed_ips') # refresh relationship before return 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']) 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