Repository: netbox-community/netbox

Issue:
Show available IP addresses
The current IPAM views only display addresses added to the system. It does not display available addresses within a subnet. It would be helpful to see all addresses within a subnet, noting each address' status. This could be similar to the "status" column on the /ipam/prefixes/ page, indicating "available", "reserved", or "active". Being able to filter the list based on status, and showing all addresses on a page rather than paginated would also be helpful.



Discussion:
I think is one of missing features, for example if is some Virtual System or not Racked System but have one IP. is more like IP Management combined with VLAN.

Great platform!!!

Any IP address not listed should be considered available, since it has not been defined. What benefit would explicitly listing every IP address have? And how would you approach this for IPv6?

When allocating IP addresses to systems, seeing what addresses are available is helpful. This is especially true when deploying several systems at once that we'd like to be consecutively addressed.  With our current IPAM (GestioIP) I can clearly see which addresses are unused, and how large the range is:
![2016-07-15_15h24_59](https://cloud.githubusercontent.com/assets/20360387/16890339/507cad88-4aa2-11e6-959a-455450abc445.png)

With NetBox we have to look closely to see what ranges are available by figuring out where the breaks are:
![2016-07-15_15h38_25](https://cloud.githubusercontent.com/assets/20360387/16890368/8d7f09d8-4aa2-11e6-9e41-c9c6791449ef.png)

The ability to quickly identify reserved addresses versus used and unused would be helpful.

An idea could be to group together IPs by block in order not to list all IPs (especially for IPv6). When aggregation is not possible, then list by smaller blocks or at the end individual IPs.

For my ISP usage, this would be a very very appreciated feature, in order to make choices when assigning IPs to customers.

So maybe between, say, .42 and .50, we'd inject a row reading "7 IPs available?" I think that would work fine for IPv4. We could employ a similar approach for IPv6 but I'll have to give it more thought.

Or maybe aggregate at the most.
For example (.42 and .50 are used but free between the both):
192.168.0.42/32: used
192.168.0.43/32: free
192.168.0.44/30: free
192.168.0.48/31: free
192.168.0.50/32: used

Don't know if it could work...

I think I'd prefer to just state the range, since unlike prefix aggregates IP addresses are atomic units. Generating aggregates to cover the space takes up more room on the page and can also make it more difficult to determine how much free space there is.

> So maybe between, say, .42 and .50, we'd inject a row reading "7 IPs available?"

I think this is a reasonable solution and reduces clutter.

I would also like to have this feature. And I agree with @jeremystretch 's range proposal, it will work both on IPv4 and IPv6. And this should be showed in a slightly different colour, light green or similar. 

I also think this feature should be extended to VLANs within the group to show available VLANs as ranges. (btw, I belive the sites with no VLAN group should have an implicit default group, even if the user is not creating one).

Otherwise great work with the project Jeremy, I think it really is adding features other IPAMs don't have, even though a few more features could be welcomed.

[phpipam](https://github.com/phpipam/phpipam) might be a source of [inspiration](http://demo.phpipam.net/subnets/1/33/). I think they do a great job with IPAM in general. Seems like netbox does a better job with DCIM and phpipam does a better job with IPAM, while they each try to cover both needs. It will be interesting to see where the two projects end up long term. 

Agreed, phpipam does handle that very well.

@alexjhart @anthraxau Could you elaborate on what you like about phpIPAM's view?

![phpipam1](https://cloud.githubusercontent.com/assets/13487278/17333982/95f30f70-58a2-11e6-8474-04bd4043641d.png)

IMO this is a confusing way to show IP addresses. Why does the free range appear in the hostname column? I was thinking of something like this, but more similar to how we show unallocated child prefixes.

![phpipam2](https://cloud.githubusercontent.com/assets/13487278/17334072/e183f5a8-58a2-11e6-83dc-61cf02dc93fb.png)

The block display, while visually appealing with small IPv4 networks, isn't very useful for prefixes larger than a /24 (and not at all with IPv6).

In the regards to the first image I just like how the dhcp range is grouped. You're right other than that it behaves incorrectly. 

The 2nd image is nice to see what's available at a quick glance but I agree that on a /22 or larger it would be painful I didn't even think of IPv6 :(

I've been using excel for a /22 and a few other prefixes so I'm feeling abit blessed with what you have currently. 

I'm not sure if other people do this but I tend to group similar devices in groups on the prefix for example routers .250-254 core switches .240-249 VMware stuff .100-.149 etc and I usually colour code them for easy navigation of my spreadsheet so that I can quickly glance other it and see any gaps that are available. I've noticed you do this for rack devices. Would be nice to be able to enable this for the ipam 

![image](https://cloud.githubusercontent.com/assets/216209/17337880/86795616-5899-11e6-8e7b-1dc8dc1f7bf9.png) 
I never considered the ranges to be under the hostname column. My eye accepted that as a range of available IPs and find the offset helpful in establishing the difference.  More important than which column data shows up under is the display and grouping of unused space. Having a place holder with a different row color and a range displayed of the available space is adequate. I also like similar types of ranges being grouped, like the dhcp range shown here, though that is less important to me than showing available space. It just plays into the argument you had for the next screenshot where doing things that help with large subnets is important. This could streamline the display of a lot of larger subnets. 

![image](https://cloud.githubusercontent.com/assets/216209/17338057/84ff0960-589a-11e6-8e77-229d7290e94c.png)
I could care less about this visual display and agree it isn't practical for larger subnets. If you did want to have something like this, maybe you would set a limit for max prefix size it is displayed for.

![image](https://cloud.githubusercontent.com/assets/216209/17338926/66076ed6-589e-11e6-9001-d6121d80bcd0.png)
I think you can take a similar approach to what you do with the child prefixes table. Here you have available subnets (though I didn't realize this at first, so maybe consider a different row color). Those are hyperlinked to a create new subnet form with the range autofilled, which I like. Though the workflow might be nicer if you used a popup rather than directing the user to a new page? 

![image](https://cloud.githubusercontent.com/assets/216209/17338870/1959b5c6-589e-11e6-8146-d2032178ff59.png)
Anyway, maybe something like this. I imagine that range there is hyperlinked and takes you to a new IP address form for the first IP in that range. By the way, it would be nice to be able to add a range of IPs, not just one at a time. Again, I think it would flow better if you handle the addition in a popup (similar to phpipam).

I like @anthraxau's idea of having color coded ranges. Maybe this can be accomplished with a tag #132  I imagine something similar to gmail labels where you have have multiple tags shown in the table which can have colors assigned to them, to make different things stand out more. That idea is also off topic though. 


Resolving patch:
diff --git a/netbox/ipam/tables.py b/netbox/ipam/tables.py
--- a/netbox/ipam/tables.py
+++ b/netbox/ipam/tables.py
@@ -39,6 +39,16 @@
 </span>
 """
 
+IPADDRESS_LINK = """
+{% if record.pk %}
+    <a href="{{ record.get_absolute_url }}">{{ record.address }}</a>
+{% elif perms.ipam.add_ipaddress %}
+    <a href="{% url 'ipam:ipaddress_add' %}?address={{ record.1 }}" class="btn btn-xs btn-success">{% if record.0 <= 65536 %}{{ record.0 }}{% else %}Lots of{% endif %} free IP{{ record.0|pluralize }}</a>
+{% else %}
+    {{ record.0 }}
+{% endif %}
+"""
+
 STATUS_LABEL = """
 {% if record.pk %}
     <span class="label label-{{ record.get_status_class }}">{{ record.get_status_display }}</span>
@@ -148,6 +158,9 @@ class PrefixTable(BaseTable):
     class Meta(BaseTable.Meta):
         model = Prefix
         fields = ('pk', 'prefix', 'status', 'vrf', 'tenant', 'site', 'role', 'description')
+        row_attrs = {
+            'class': lambda record: 'success' if not record.pk else '',
+        }
 
 
 class PrefixBriefTable(BaseTable):
@@ -169,7 +182,7 @@ class Meta(BaseTable.Meta):
 
 class IPAddressTable(BaseTable):
     pk = ToggleColumn()
-    address = tables.LinkColumn('ipam:ipaddress', args=[Accessor('pk')], verbose_name='IP Address')
+    address = tables.TemplateColumn(IPADDRESS_LINK, verbose_name='IP Address')
     vrf = tables.LinkColumn('ipam:vrf', args=[Accessor('vrf.pk')], default='Global', verbose_name='VRF')
     tenant = tables.TemplateColumn(TENANT_LINK, verbose_name='Tenant')
     device = tables.LinkColumn('dcim:device', args=[Accessor('interface.device.pk')], orderable=False,
@@ -180,6 +193,9 @@ class IPAddressTable(BaseTable):
     class Meta(BaseTable.Meta):
         model = IPAddress
         fields = ('pk', 'address', 'vrf', 'tenant', 'device', 'interface', 'description')
+        row_attrs = {
+            'class': lambda record: 'success' if not isinstance(record, IPAddress) else '',
+        }
 
 
 class IPAddressBriefTable(BaseTable):
diff --git a/netbox/ipam/views.py b/netbox/ipam/views.py
--- a/netbox/ipam/views.py
+++ b/netbox/ipam/views.py
@@ -1,4 +1,4 @@
-from netaddr import IPSet
+import netaddr
 from django_tables2 import RequestConfig
 
 from django.contrib.auth.mixins import PermissionRequiredMixin
@@ -21,7 +21,7 @@ def add_available_prefixes(parent, prefix_list):
     """
 
     # Find all unallocated space
-    available_prefixes = IPSet(parent) ^ IPSet([p.prefix for p in prefix_list])
+    available_prefixes = netaddr.IPSet(parent) ^ netaddr.IPSet([p.prefix for p in prefix_list])
     available_prefixes = [Prefix(prefix=p) for p in available_prefixes.iter_cidrs()]
 
     # Concatenate and sort complete list of children
@@ -31,6 +31,57 @@ def add_available_prefixes(parent, prefix_list):
     return prefix_list
 
 
+def add_available_ipaddresses(prefix, ipaddress_list):
+    """
+    Annotate ranges of available IP addresses within a given prefix.
+    """
+
+    output = []
+    prev_ip = None
+
+    # Ignore the "network address" for IPv4 prefixes larger than /31
+    if prefix.version == 4 and prefix.prefixlen < 31:
+        first_ip_in_prefix = netaddr.IPAddress(prefix.first + 1)
+    else:
+        first_ip_in_prefix = netaddr.IPAddress(prefix.first)
+
+    # Ignore the broadcast address for IPv4 prefixes larger than /31
+    if prefix.version == 4 and prefix.prefixlen < 31:
+        last_ip_in_prefix = netaddr.IPAddress(prefix.last - 1)
+    else:
+        last_ip_in_prefix = netaddr.IPAddress(prefix.last)
+
+    if not ipaddress_list:
+        return [(
+            int(last_ip_in_prefix - first_ip_in_prefix + 1),
+            '{}/{}'.format(first_ip_in_prefix, prefix.prefixlen)
+        )]
+
+    # Account for any available IPs before the first real IP
+    if ipaddress_list[0].address.ip > first_ip_in_prefix:
+        skipped_count = int(ipaddress_list[0].address.ip - first_ip_in_prefix)
+        first_skipped = '{}/{}'.format(first_ip_in_prefix, prefix.prefixlen)
+        output.append((skipped_count, first_skipped))
+
+    # Iterate through existing IPs and annotate free ranges
+    for ip in ipaddress_list:
+        if prev_ip:
+            skipped_count = int(ip.address.ip - prev_ip.address.ip - 1)
+            if skipped_count:
+                first_skipped = '{}/{}'.format(prev_ip.address.ip + 1, prefix.prefixlen)
+                output.append((skipped_count, first_skipped))
+        output.append(ip)
+        prev_ip = ip
+
+    # Include any remaining available IPs
+    if prev_ip.address.ip < last_ip_in_prefix:
+        skipped_count = int(last_ip_in_prefix - prev_ip.address.ip)
+        first_skipped = '{}/{}'.format(prev_ip.address.ip + 1, prefix.prefixlen)
+        output.append((skipped_count, first_skipped))
+
+    return output
+
+
 #
 # VRFs
 #
@@ -375,6 +426,7 @@ def prefix_ipaddresses(request, pk):
     # Find all IPAddresses belonging to this Prefix
     ipaddresses = IPAddress.objects.filter(vrf=prefix.vrf, address__net_contained_or_equal=str(prefix.prefix))\
         .select_related('vrf', 'interface__device', 'primary_ip4_for', 'primary_ip6_for')
+    ipaddresses = add_available_ipaddresses(prefix.prefix, ipaddresses)
 
     ip_table = tables.IPAddressTable(ipaddresses)
     ip_table.model = IPAddress


Test patch: