diff --git a/netbox/ipam/api/serializers.py b/netbox/ipam/api/serializers.py index e02e384ed..5a7d96352 100644 --- a/netbox/ipam/api/serializers.py +++ b/netbox/ipam/api/serializers.py @@ -1,4 +1,5 @@ from __future__ import unicode_literals +from collections import OrderedDict from rest_framework import serializers from rest_framework.validators import UniqueTogetherValidator @@ -268,6 +269,20 @@ class WritableIPAddressSerializer(CustomFieldModelSerializer): ] +class AvailableIPSerializer(serializers.Serializer): + + def to_representation(self, instance): + if self.context.get('vrf'): + vrf = NestedVRFSerializer(self.context['vrf'], context={'request': self.context['request']}).data + else: + vrf = None + return OrderedDict([ + ('family', self.context['prefix'].version), + ('address', '{}/{}'.format(instance, self.context['prefix'].prefixlen)), + ('vrf', vrf), + ]) + + # # Services # diff --git a/netbox/ipam/api/views.py b/netbox/ipam/api/views.py index 74d26dca1..0bb6411f8 100644 --- a/netbox/ipam/api/views.py +++ b/netbox/ipam/api/views.py @@ -1,7 +1,12 @@ from __future__ import unicode_literals +from rest_framework.decorators import detail_route +from rest_framework.response import Response from rest_framework.viewsets import ModelViewSet +from django.conf import settings +from django.shortcuts import get_object_or_404 + from ipam.models import Aggregate, IPAddress, Prefix, RIR, Role, Service, VLAN, VLANGroup, VRF from ipam import filters from extras.api.views import CustomFieldModelViewSet @@ -61,6 +66,33 @@ class PrefixViewSet(WritableSerializerMixin, CustomFieldModelViewSet): write_serializer_class = serializers.WritablePrefixSerializer filter_class = filters.PrefixFilter + @detail_route(url_path='available-ips') + def available_ips(self, request, pk=None): + """ + A convenience method for returning available IP addresses within a prefix. By default, the number of IPs + returned will be equivalent to PAGINATE_COUNT. An arbitrary limit (up to MAX_PAGE_SIZE, if set) may be passed, + however results will not be paginated. + """ + prefix = get_object_or_404(Prefix, pk=pk) + + # Determine the maximum amount of IPs to return + try: + limit = int(request.query_params.get('limit', settings.PAGINATE_COUNT)) + except ValueError: + limit = settings.PAGINATE_COUNT + if settings.MAX_PAGE_SIZE: + limit = min(limit, settings.MAX_PAGE_SIZE) + + # Calculate available IPs within the prefix + ip_list = list(prefix.get_available_ips())[:limit] + serializer = serializers.AvailableIPSerializer(ip_list, many=True, context={ + 'request': request, + 'prefix': prefix.prefix, + 'vrf': prefix.vrf, + }) + + return Response(serializer.data) + # # IP addresses diff --git a/netbox/ipam/models.py b/netbox/ipam/models.py index 57ad939ed..8b23f3f42 100644 --- a/netbox/ipam/models.py +++ b/netbox/ipam/models.py @@ -1,6 +1,5 @@ from __future__ import unicode_literals - -from netaddr import IPNetwork, cidr_merge +import netaddr from django.conf import settings from django.contrib.contenttypes.fields import GenericRelation @@ -161,7 +160,7 @@ class Aggregate(CreatedUpdatedModel, CustomFieldModel): """ child_prefixes = Prefix.objects.filter(prefix__net_contained_or_equal=str(self.prefix)) # Remove overlapping prefixes from list of children - networks = cidr_merge([c.prefix for c in child_prefixes]) + networks = netaddr.cidr_merge([c.prefix for c in child_prefixes]) children_size = float(0) for p in networks: children_size += p.size @@ -321,11 +320,34 @@ class Prefix(CreatedUpdatedModel, CustomFieldModel): def get_duplicates(self): return Prefix.objects.filter(vrf=self.vrf, prefix=str(self.prefix)).exclude(pk=self.pk) + def get_child_ips(self): + """ + Return all IPAddresses within this Prefix. + """ + return IPAddress.objects.filter(address__net_contained_or_equal=str(self.prefix), vrf=self.vrf) + + def get_available_ips(self): + """ + Return all available IPs within this prefix as an IPSet. + """ + prefix = netaddr.IPSet(self.prefix) + child_ips = netaddr.IPSet([ip.address for ip in self.get_child_ips()]) + available_ips = prefix - child_ips + + # Remove unusable IPs from non-pool prefixes + if not self.is_pool: + available_ips -= netaddr.IPSet([ + netaddr.IPAddress(self.prefix.first), + netaddr.IPAddress(self.prefix.last), + ]) + + return available_ips + def get_utilization(self): """ Determine the utilization of the prefix and return it as a percentage. """ - child_count = IPAddress.objects.filter(address__net_contained_or_equal=str(self.prefix), vrf=self.vrf).count() + child_count = self.get_child_ips().count() prefix_size = self.prefix.size if self.family == 4 and self.prefix.prefixlen < 31 and not self.is_pool: prefix_size -= 2 @@ -335,11 +357,11 @@ class Prefix(CreatedUpdatedModel, CustomFieldModel): def new_subnet(self): if self.family == 4: if self.prefix.prefixlen <= 30: - return IPNetwork('{}/{}'.format(self.prefix.network, self.prefix.prefixlen + 1)) + return netaddr.IPNetwork('{}/{}'.format(self.prefix.network, self.prefix.prefixlen + 1)) return None if self.family == 6: if self.prefix.prefixlen <= 126: - return IPNetwork('{}/{}'.format(self.prefix.network, self.prefix.prefixlen + 1)) + return netaddr.IPNetwork('{}/{}'.format(self.prefix.network, self.prefix.prefixlen + 1)) return None