Example #1
0
 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 _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
Example #3
0
    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
Example #4
0
    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)
Example #6
0
 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)
Example #8
0
    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
Example #10
0
 def _test_eui_64(self, ips, expected):
     for ip in ips:
         self.assertEqual(expected, ipv6_utils.is_eui64_address(ip),
                          "Error on %s" % ip)