Merge pull request #474 from digitalocean/develop

Release v1.5.2
This commit is contained in:
Jeremy Stretch 2016-08-16 09:33:57 -04:00 committed by GitHub
commit 58e3d5ae09
11 changed files with 66 additions and 9 deletions

View File

@ -1032,6 +1032,13 @@ class Interface(models.Model):
def __unicode__(self): def __unicode__(self):
return self.name 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 @property
def is_physical(self): def is_physical(self):
return self.form_factor != IFACE_FF_VIRTUAL return self.form_factor != IFACE_FF_VIRTUAL

View File

@ -18,9 +18,10 @@ GRAPH_TYPE_CHOICES = (
) )
EXPORTTEMPLATE_MODELS = [ EXPORTTEMPLATE_MODELS = [
'site', 'rack', 'device', 'consoleport', 'powerport', 'interfaceconnection', 'site', 'rack', 'device', 'consoleport', 'powerport', 'interfaceconnection', # DCIM
'aggregate', 'prefix', 'ipaddress', 'vlan', 'aggregate', 'prefix', 'ipaddress', 'vlan', # IPAM
'provider', 'circuit' 'provider', 'circuit', # Circuits
'tenant', # Tenants
] ]
ACTION_CREATE = 1 ACTION_CREATE = 1

View File

@ -5,6 +5,7 @@ from django.core.exceptions import ValidationError
from django.core.urlresolvers import reverse from django.core.urlresolvers import reverse
from django.core.validators import MaxValueValidator, MinValueValidator from django.core.validators import MaxValueValidator, MinValueValidator
from django.db import models from django.db import models
from django.db.models.expressions import RawSQL
from dcim.models import Interface from dcim.models import Interface
from tenancy.models import Tenant from tenancy.models import Tenant
@ -295,6 +296,20 @@ class Prefix(CreatedUpdatedModel):
return STATUS_CHOICE_CLASSES[self.status] 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): class IPAddress(CreatedUpdatedModel):
""" """
An IPAddress represents an individual IPv4 or IPv6 address and its mask. The mask length should match what is 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)') null=True, verbose_name='NAT IP (inside)')
description = models.CharField(max_length=100, blank=True) description = models.CharField(max_length=100, blank=True)
objects = IPAddressManager()
class Meta: class Meta:
ordering = ['family', 'address'] ordering = ['family', 'address']
verbose_name = 'IP address' verbose_name = 'IP address'

View File

@ -12,7 +12,7 @@ except ImportError:
"the documentation.") "the documentation.")
VERSION = '1.5.1' VERSION = '1.5.2'
# Import local configuration # Import local configuration
for setting in ['ALLOWED_HOSTS', 'DATABASE', 'SECRET_KEY']: for setting in ['ALLOWED_HOSTS', 'DATABASE', 'SECRET_KEY']:

View File

@ -8,9 +8,15 @@ $(document).ready(function() {
} }
// Update livesearch text when real field changes // Update livesearch text when real field changes
search_field.val(real_field.children('option:selected').text()); if (real_field.val()) {
real_field.change(function() {
search_field.val(real_field.children('option:selected').text()); 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({ search_field.autocomplete({

View File

@ -10,6 +10,10 @@
<span class="fa fa-plus" aria-hidden="true"></span> <span class="fa fa-plus" aria-hidden="true"></span>
Add a circuit Add a circuit
</a> </a>
<a href="{% url 'circuits:circuit_import' %}" class="btn btn-info">
<span class="fa fa-download" aria-hidden="true"></span>
Import circuits
</a>
{% endif %} {% endif %}
{% include 'inc/export_button.html' with obj_type='circuits' %} {% include 'inc/export_button.html' with obj_type='circuits' %}
</div> </div>

View File

@ -9,6 +9,10 @@
<span class="fa fa-plus" aria-hidden="true"></span> <span class="fa fa-plus" aria-hidden="true"></span>
Add a provider Add a provider
</a> </a>
<a href="{% url 'circuits:provider_import' %}" class="btn btn-info">
<span class="fa fa-download" aria-hidden="true"></span>
Import providers
</a>
{% endif %} {% endif %}
{% include 'inc/export_button.html' with obj_type='providers' %} {% include 'inc/export_button.html' with obj_type='providers' %}
</div> </div>

View File

@ -56,6 +56,10 @@
<a href="{% url 'dcim:interfaceconnection_delete' pk=iface.connection.pk %}?device={{ device.pk }}" class="btn btn-danger btn-xs" title="Delete connection"> <a href="{% url 'dcim:interfaceconnection_delete' pk=iface.connection.pk %}?device={{ device.pk }}" class="btn btn-danger btn-xs" title="Delete connection">
<i class="glyphicon glyphicon-remove" aria-hidden="true"></i> <i class="glyphicon glyphicon-remove" aria-hidden="true"></i>
</a> </a>
{% elif iface.circuit and perms.circuits.change_circuit %}
<a href="{% url 'circuits:circuit_edit' pk=iface.circuit.pk %}" class="btn btn-danger btn-xs" title="Edit circuit">
<i class="glyphicon glyphicon-remove" aria-hidden="true"></i>
</a>
{% else %} {% else %}
<a href="{% url 'dcim:interfaceconnection_add' pk=device.pk %}?interface={{ iface.pk }}" class="btn btn-success btn-xs" title="Connect"> <a href="{% url 'dcim:interfaceconnection_add' pk=device.pk %}?interface={{ iface.pk }}" class="btn btn-success btn-xs" title="Connect">
<i class="glyphicon glyphicon-plus" aria-hidden="true"></i> <i class="glyphicon glyphicon-plus" aria-hidden="true"></i>

View File

@ -11,6 +11,10 @@
<span class="fa fa-plus" aria-hidden="true"></span> <span class="fa fa-plus" aria-hidden="true"></span>
Add an aggregate Add an aggregate
</a> </a>
<a href="{% url 'ipam:aggregate_import' %}" class="btn btn-info">
<span class="fa fa-download" aria-hidden="true"></span>
Import aggregates
</a>
{% endif %} {% endif %}
{% include 'inc/export_button.html' with obj_type='aggregates' %} {% include 'inc/export_button.html' with obj_type='aggregates' %}
</div> </div>

View File

@ -10,6 +10,10 @@
<span class="fa fa-plus" aria-hidden="true"></span> <span class="fa fa-plus" aria-hidden="true"></span>
Add a tenant Add a tenant
</a> </a>
<a href="{% url 'tenancy:tenant_import' %}" class="btn btn-info">
<span class="fa fa-download" aria-hidden="true"></span>
Import tenants
</a>
{% endif %} {% endif %}
{% include 'inc/export_button.html' with obj_type='tenants' %} {% include 'inc/export_button.html' with obj_type='tenants' %}
</div> </div>

View File

@ -1,5 +1,5 @@
from django.contrib.auth.mixins import PermissionRequiredMixin 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 django.shortcuts import get_object_or_404, render
from circuits.models import Circuit from circuits.models import Circuit
@ -59,8 +59,14 @@ def tenant(request, slug):
'rack_count': Rack.objects.filter(tenant=tenant).count(), 'rack_count': Rack.objects.filter(tenant=tenant).count(),
'device_count': Device.objects.filter(tenant=tenant).count(), 'device_count': Device.objects.filter(tenant=tenant).count(),
'vrf_count': VRF.objects.filter(tenant=tenant).count(), 'vrf_count': VRF.objects.filter(tenant=tenant).count(),
'prefix_count': Prefix.objects.filter(tenant=tenant).count(), 'prefix_count': Prefix.objects.filter(
'ipaddress_count': IPAddress.objects.filter(tenant=tenant).count(), 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(), 'vlan_count': VLAN.objects.filter(tenant=tenant).count(),
'circuit_count': Circuit.objects.filter(tenant=tenant).count(), 'circuit_count': Circuit.objects.filter(tenant=tenant).count(),
} }