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(), }