Repository: netbox-community/netbox

Issue:
Add filter to container prefixes to show only avaliable child Prefixes
### NetBox version

v3.0.8

### Feature type

Change to existing functionality

### Proposed functionality

When looking at the child prefixes of a container prefix there are two views
1. "Show Available" Prefixes
2. "Hide Available" Prefixes

![image](https://user-images.githubusercontent.com/32685892/139167124-c750de64-c24c-4e96-9e92-ee4dd304bfe9.png)

This FR proposes a third option that filters only by available prefixes in the container. "Show Only Available"

This filter removes all currently allocated prefixes from the list, and only displays currently unassigned "available" prefixes within the container prefix

I should be able to put a PR in for this if accepted.

### Use case

We currently use the "Show Available Prefixes" filter to assign new child prefixes from the container. 
This works well when the container is mostly empty, but when you have a /20 container full of /31's that's 99% full you need to run through pages and pages of allocated prefixes to find the ones that are unallocated.

### Database changes

None - this functionality is currently available via the API

### External dependencies

None


Discussion:
@jeremystretch can i be assigned this?

Resolving patch:
diff --git a/netbox/ipam/utils.py b/netbox/ipam/utils.py
--- a/netbox/ipam/utils.py
+++ b/netbox/ipam/utils.py
@@ -4,20 +4,30 @@
 from .models import Prefix, VLAN
 
 
-def add_available_prefixes(parent, prefix_list):
+def add_requested_prefixes(parent, prefix_list, request):
     """
-    Create fake Prefix objects for all unallocated space within a prefix.
+    Return a list of requested prefixes using show_available, show_assigned filters.
+    If avalible prefixes are requested, create fake Prefix objects for all unallocated space within a prefix
     """
 
-    # Find all unallocated space
-    available_prefixes = netaddr.IPSet(parent) ^ netaddr.IPSet([p.prefix for p in prefix_list])
-    available_prefixes = [Prefix(prefix=p, status=None) for p in available_prefixes.iter_cidrs()]
+    child_prefixes = []
 
-    # Concatenate and sort complete list of children
-    prefix_list = list(prefix_list) + available_prefixes
-    prefix_list.sort(key=lambda p: p.prefix)
+    # Add available prefixes to the table if requested
+    if prefix_list and request.GET.get('show_available', 'true') == 'true':
 
-    return prefix_list
+        # Find all unallocated space, add fake Prefix objects to child_prefixes.
+        available_prefixes = netaddr.IPSet(parent) ^ netaddr.IPSet([p.prefix for p in prefix_list])
+        available_prefixes = [Prefix(prefix=p, status=None) for p in available_prefixes.iter_cidrs()]
+        child_prefixes = child_prefixes + available_prefixes
+
+    # Add assigned prefixes to the table if requested
+    if prefix_list and request.GET.get('show_assigned', 'true') == 'true':
+        child_prefixes = child_prefixes + list(prefix_list)
+
+    # Sort child prefixes after additions
+    child_prefixes.sort(key=lambda p: p.prefix)
+
+    return child_prefixes
 
 
 def add_available_ipaddresses(prefix, ipaddress_list, is_pool=False):
diff --git a/netbox/ipam/views.py b/netbox/ipam/views.py
--- a/netbox/ipam/views.py
+++ b/netbox/ipam/views.py
@@ -15,7 +15,7 @@
 from .constants import *
 from .models import *
 from .models import ASN
-from .utils import add_available_ipaddresses, add_available_prefixes, add_available_vlans
+from .utils import add_available_ipaddresses, add_requested_prefixes, add_available_vlans
 
 
 #
@@ -275,8 +275,8 @@ class AggregateView(generic.ObjectView):
     queryset = Aggregate.objects.all()
 
     def get_extra_context(self, request, instance):
-        # Find all child prefixes contained by this aggregate
-        child_prefixes = Prefix.objects.restrict(request.user, 'view').filter(
+        # Find all child prefixes contained in this aggregate
+        prefix_list = Prefix.objects.restrict(request.user, 'view').filter(
             prefix__net_contained_or_equal=str(instance.prefix)
         ).prefetch_related(
             'site', 'role'
@@ -284,9 +284,8 @@ def get_extra_context(self, request, instance):
             'prefix'
         )
 
-        # Add available prefixes to the table if requested
-        if request.GET.get('show_available', 'true') == 'true':
-            child_prefixes = add_available_prefixes(instance.prefix, child_prefixes)
+        # Return List of requested Prefixes
+        child_prefixes = add_requested_prefixes(instance.prefix, prefix_list, request)
 
         prefix_table = tables.PrefixTable(child_prefixes, exclude=('utilization',))
         if request.user.has_perm('ipam.change_prefix') or request.user.has_perm('ipam.delete_prefix'):
@@ -305,6 +304,7 @@ def get_extra_context(self, request, instance):
             'permissions': permissions,
             'bulk_querystring': f'within={instance.prefix}',
             'show_available': request.GET.get('show_available', 'true') == 'true',
+            'show_assigned': request.GET.get('show_assigned', 'true') == 'true',
         }
 
 
@@ -456,14 +456,13 @@ class PrefixPrefixesView(generic.ObjectView):
     template_name = 'ipam/prefix/prefixes.html'
 
     def get_extra_context(self, request, instance):
-        # Child prefixes table
-        child_prefixes = instance.get_child_prefixes().restrict(request.user, 'view').prefetch_related(
+        # Find all child prefixes contained in this prefix
+        prefix_list = instance.get_child_prefixes().restrict(request.user, 'view').prefetch_related(
             'site', 'vlan', 'role',
         )
 
-        # Add available prefixes to the table if requested
-        if child_prefixes and request.GET.get('show_available', 'true') == 'true':
-            child_prefixes = add_available_prefixes(instance.prefix, child_prefixes)
+        # Return List of requested Prefixes
+        child_prefixes = add_requested_prefixes(instance.prefix, prefix_list, request)
 
         table = tables.PrefixTable(child_prefixes, user=request.user, exclude=('utilization',))
         if request.user.has_perm('ipam.change_prefix') or request.user.has_perm('ipam.delete_prefix'):
@@ -485,6 +484,7 @@ def get_extra_context(self, request, instance):
             'active_tab': 'prefixes',
             'first_available_prefix': instance.get_first_available_prefix(),
             'show_available': request.GET.get('show_available', 'true') == 'true',
+            'show_assigned': request.GET.get('show_assigned', 'true') == 'true',
         }
 
 


Test patch: