diff --git a/netbox/dcim/models.py b/netbox/dcim/models.py
index b1c6b60b71e..a0e2b4e0fb5 100644
--- a/netbox/dcim/models.py
+++ b/netbox/dcim/models.py
@@ -1032,6 +1032,13 @@ class Meta:
def __unicode__(self):
return self.name
+ def clean(self):
+
+ if self.form_factor == IFACE_FF_VIRTUAL and self.is_connected:
+ raise ValidationError({'form_factor': "Virtual interfaces cannot be connected to another interface or "
+ "circuit. Disconnect the interface or choose a physical form "
+ "factor."})
+
@property
def is_physical(self):
return self.form_factor != IFACE_FF_VIRTUAL
diff --git a/netbox/extras/models.py b/netbox/extras/models.py
index 2f761868072..6a32f573876 100644
--- a/netbox/extras/models.py
+++ b/netbox/extras/models.py
@@ -18,9 +18,10 @@
)
EXPORTTEMPLATE_MODELS = [
- 'site', 'rack', 'device', 'consoleport', 'powerport', 'interfaceconnection',
- 'aggregate', 'prefix', 'ipaddress', 'vlan',
- 'provider', 'circuit'
+ 'site', 'rack', 'device', 'consoleport', 'powerport', 'interfaceconnection', # DCIM
+ 'aggregate', 'prefix', 'ipaddress', 'vlan', # IPAM
+ 'provider', 'circuit', # Circuits
+ 'tenant', # Tenants
]
ACTION_CREATE = 1
diff --git a/netbox/ipam/models.py b/netbox/ipam/models.py
index bd49feef10e..eebc43fad1d 100644
--- a/netbox/ipam/models.py
+++ b/netbox/ipam/models.py
@@ -5,6 +5,7 @@
from django.core.urlresolvers import reverse
from django.core.validators import MaxValueValidator, MinValueValidator
from django.db import models
+from django.db.models.expressions import RawSQL
from dcim.models import Interface
from tenancy.models import Tenant
@@ -295,6 +296,20 @@ def get_status_class(self):
return STATUS_CHOICE_CLASSES[self.status]
+class IPAddressManager(models.Manager):
+
+ def get_queryset(self):
+ """
+ By default, PostgreSQL will order INETs with shorter (larger) prefix lengths ahead of those with longer
+ (smaller) masks. This makes no sense when ordering IPs, which should be ordered solely by family and host
+ address. We can use HOST() to extract just the host portion of the address (ignoring its mask), but we must
+ then re-cast this value to INET() so that records will be ordered properly. We are essentially re-casting each
+ IP address as a /32 or /128.
+ """
+ qs = super(IPAddressManager, self).get_queryset()
+ return qs.annotate(host=RawSQL('INET(HOST(ipam_ipaddress.address))', [])).order_by('family', 'host')
+
+
class IPAddress(CreatedUpdatedModel):
"""
An IPAddress represents an individual IPv4 or IPv6 address and its mask. The mask length should match what is
@@ -317,6 +332,8 @@ class IPAddress(CreatedUpdatedModel):
null=True, verbose_name='NAT IP (inside)')
description = models.CharField(max_length=100, blank=True)
+ objects = IPAddressManager()
+
class Meta:
ordering = ['family', 'address']
verbose_name = 'IP address'
diff --git a/netbox/netbox/settings.py b/netbox/netbox/settings.py
index 67f808745f2..0384382b006 100644
--- a/netbox/netbox/settings.py
+++ b/netbox/netbox/settings.py
@@ -12,7 +12,7 @@
"the documentation.")
-VERSION = '1.5.1'
+VERSION = '1.5.2'
# Import local configuration
for setting in ['ALLOWED_HOSTS', 'DATABASE', 'SECRET_KEY']:
diff --git a/netbox/project-static/js/livesearch.js b/netbox/project-static/js/livesearch.js
index ed46fe1ff25..919fccf3b66 100644
--- a/netbox/project-static/js/livesearch.js
+++ b/netbox/project-static/js/livesearch.js
@@ -8,9 +8,15 @@ $(document).ready(function() {
}
// Update livesearch text when real field changes
- search_field.val(real_field.children('option:selected').text());
- real_field.change(function() {
+ if (real_field.val()) {
search_field.val(real_field.children('option:selected').text());
+ }
+ real_field.change(function() {
+ if (real_field.val()) {
+ search_field.val(real_field.children('option:selected').text());
+ } else {
+ search_field.val('');
+ }
});
search_field.autocomplete({
diff --git a/netbox/templates/circuits/circuit_list.html b/netbox/templates/circuits/circuit_list.html
index f30339ec6f6..db6861b2e8e 100644
--- a/netbox/templates/circuits/circuit_list.html
+++ b/netbox/templates/circuits/circuit_list.html
@@ -10,6 +10,10 @@
Add a circuit
+
+
+ Import circuits
+
{% endif %}
{% include 'inc/export_button.html' with obj_type='circuits' %}
diff --git a/netbox/templates/circuits/provider_list.html b/netbox/templates/circuits/provider_list.html
index 54dfdac93de..ca3dbfc09b3 100644
--- a/netbox/templates/circuits/provider_list.html
+++ b/netbox/templates/circuits/provider_list.html
@@ -9,6 +9,10 @@
Add a provider
+
+
+ Import providers
+
{% endif %}
{% include 'inc/export_button.html' with obj_type='providers' %}
diff --git a/netbox/templates/dcim/inc/_interface.html b/netbox/templates/dcim/inc/_interface.html
index a3d1b8e0422..a48200c302c 100644
--- a/netbox/templates/dcim/inc/_interface.html
+++ b/netbox/templates/dcim/inc/_interface.html
@@ -56,6 +56,10 @@
+ {% elif iface.circuit and perms.circuits.change_circuit %}
+
+
+
{% else %}
diff --git a/netbox/templates/ipam/aggregate_list.html b/netbox/templates/ipam/aggregate_list.html
index 52bca7219e5..aef7d84c19d 100644
--- a/netbox/templates/ipam/aggregate_list.html
+++ b/netbox/templates/ipam/aggregate_list.html
@@ -11,6 +11,10 @@
Add an aggregate
+
+
+ Import aggregates
+
{% endif %}
{% include 'inc/export_button.html' with obj_type='aggregates' %}
diff --git a/netbox/templates/tenancy/tenant_list.html b/netbox/templates/tenancy/tenant_list.html
index 24f796da346..529f01c7646 100644
--- a/netbox/templates/tenancy/tenant_list.html
+++ b/netbox/templates/tenancy/tenant_list.html
@@ -10,6 +10,10 @@
Add a tenant
+
+
+ Import tenants
+
{% endif %}
{% include 'inc/export_button.html' with obj_type='tenants' %}
diff --git a/netbox/tenancy/views.py b/netbox/tenancy/views.py
index ec220b9be2e..a1191545862 100644
--- a/netbox/tenancy/views.py
+++ b/netbox/tenancy/views.py
@@ -1,5 +1,5 @@
from django.contrib.auth.mixins import PermissionRequiredMixin
-from django.db.models import Count
+from django.db.models import Count, Q
from django.shortcuts import get_object_or_404, render
from circuits.models import Circuit
@@ -59,8 +59,14 @@ def tenant(request, slug):
'rack_count': Rack.objects.filter(tenant=tenant).count(),
'device_count': Device.objects.filter(tenant=tenant).count(),
'vrf_count': VRF.objects.filter(tenant=tenant).count(),
- 'prefix_count': Prefix.objects.filter(tenant=tenant).count(),
- 'ipaddress_count': IPAddress.objects.filter(tenant=tenant).count(),
+ 'prefix_count': Prefix.objects.filter(
+ Q(tenant=tenant) |
+ Q(tenant__isnull=True, vrf__tenant=tenant)
+ ).count(),
+ 'ipaddress_count': IPAddress.objects.filter(
+ Q(tenant=tenant) |
+ Q(tenant__isnull=True, vrf__tenant=tenant)
+ ).count(),
'vlan_count': VLAN.objects.filter(tenant=tenant).count(),
'circuit_count': Circuit.objects.filter(tenant=tenant).count(),
}