Browse Source

#1246: Initial work on an API endpoint to retrieve available IPs for a prefix

Jeremy Stretch 7 years ago
parent
commit
d5bb37b552
3 changed files with 75 additions and 6 deletions
  1. 15 0
      netbox/ipam/api/serializers.py
  2. 32 0
      netbox/ipam/api/views.py
  3. 28 6
      netbox/ipam/models.py

+ 15 - 0
netbox/ipam/api/serializers.py

@@ -1,4 +1,5 @@
 from __future__ import unicode_literals
 from __future__ import unicode_literals
+from collections import OrderedDict
 
 
 from rest_framework import serializers
 from rest_framework import serializers
 from rest_framework.validators import UniqueTogetherValidator
 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
 # Services
 #
 #

+ 32 - 0
netbox/ipam/api/views.py

@@ -1,7 +1,12 @@
 from __future__ import unicode_literals
 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 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.models import Aggregate, IPAddress, Prefix, RIR, Role, Service, VLAN, VLANGroup, VRF
 from ipam import filters
 from ipam import filters
 from extras.api.views import CustomFieldModelViewSet
 from extras.api.views import CustomFieldModelViewSet
@@ -61,6 +66,33 @@ class PrefixViewSet(WritableSerializerMixin, CustomFieldModelViewSet):
     write_serializer_class = serializers.WritablePrefixSerializer
     write_serializer_class = serializers.WritablePrefixSerializer
     filter_class = filters.PrefixFilter
     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
 # IP addresses

+ 28 - 6
netbox/ipam/models.py

@@ -1,6 +1,5 @@
 from __future__ import unicode_literals
 from __future__ import unicode_literals
-
-from netaddr import IPNetwork, cidr_merge
+import netaddr
 
 
 from django.conf import settings
 from django.conf import settings
 from django.contrib.contenttypes.fields import GenericRelation
 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))
         child_prefixes = Prefix.objects.filter(prefix__net_contained_or_equal=str(self.prefix))
         # Remove overlapping prefixes from list of children
         # 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)
         children_size = float(0)
         for p in networks:
         for p in networks:
             children_size += p.size
             children_size += p.size
@@ -321,11 +320,34 @@ class Prefix(CreatedUpdatedModel, CustomFieldModel):
     def get_duplicates(self):
     def get_duplicates(self):
         return Prefix.objects.filter(vrf=self.vrf, prefix=str(self.prefix)).exclude(pk=self.pk)
         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):
     def get_utilization(self):
         """
         """
         Determine the utilization of the prefix and return it as a percentage.
         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
         prefix_size = self.prefix.size
         if self.family == 4 and self.prefix.prefixlen < 31 and not self.is_pool:
         if self.family == 4 and self.prefix.prefixlen < 31 and not self.is_pool:
             prefix_size -= 2
             prefix_size -= 2
@@ -335,11 +357,11 @@ class Prefix(CreatedUpdatedModel, CustomFieldModel):
     def new_subnet(self):
     def new_subnet(self):
         if self.family == 4:
         if self.family == 4:
             if self.prefix.prefixlen <= 30:
             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
             return None
         if self.family == 6:
         if self.family == 6:
             if self.prefix.prefixlen <= 126:
             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
             return None