Parcourir la source

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

Jeremy Stretch il y a 7 ans
Parent
commit
d5bb37b552
3 fichiers modifiés avec 75 ajouts et 6 suppressions
  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 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
 #

+ 32 - 0
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

+ 28 - 6
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