1
0
mirror of https://github.com/netbox-community/netbox.git synced 2024-05-10 07:54:54 +00:00

Merge pull request #694 from digitalocean/develop

Release v1.7.1
This commit is contained in:
Jeremy Stretch
2016-11-15 12:34:09 -05:00
committed by GitHub
12 changed files with 189 additions and 38 deletions

View File

@ -5,7 +5,7 @@ WORKDIR /opt/netbox
ARG BRANCH=master ARG BRANCH=master
ARG URL=https://github.com/digitalocean/netbox.git ARG URL=https://github.com/digitalocean/netbox.git
RUN git clone --depth 1 $URL -b $BRANCH . && \ RUN git clone --depth 1 $URL -b $BRANCH . && \
apt-get update -qq && apt-get install -y libldap2-dev libsasl2-dev libssl-dev && \ apt-get update -qq && apt-get install -y libldap2-dev libsasl2-dev libssl-dev graphviz && \
pip install gunicorn==17.5 && \ pip install gunicorn==17.5 && \
pip install django-auth-ldap && \ pip install django-auth-ldap && \
pip install -r requirements.txt pip install -r requirements.txt

View File

@ -268,6 +268,9 @@ class DeviceTypeBulkEditForm(BulkEditForm, BootstrapMixin):
manufacturer = forms.ModelChoiceField(queryset=Manufacturer.objects.all(), required=False) manufacturer = forms.ModelChoiceField(queryset=Manufacturer.objects.all(), required=False)
u_height = forms.IntegerField(min_value=1, required=False) u_height = forms.IntegerField(min_value=1, required=False)
class Meta:
nullable_fields = []
class DeviceTypeFilterForm(forms.Form, BootstrapMixin): class DeviceTypeFilterForm(forms.Form, BootstrapMixin):
manufacturer = FilterChoiceField(queryset=Manufacturer.objects.annotate(filter_count=Count('device_types')), manufacturer = FilterChoiceField(queryset=Manufacturer.objects.annotate(filter_count=Count('device_types')),
@ -1249,10 +1252,15 @@ class IPAddressForm(BootstrapMixin, CustomFieldForm):
self.fields['vrf'].empty_label = 'Global' self.fields['vrf'].empty_label = 'Global'
self.fields['interface'].queryset = device.interfaces.all() interfaces = device.interfaces.all()
self.fields['interface'].queryset = interfaces
self.fields['interface'].required = True self.fields['interface'].required = True
# If this device does not have any IP addresses assigned, default to setting the first IP as its primary # If this device has only one interface, select it by default.
if len(interfaces) == 1:
self.fields['interface'].initial = interfaces[0]
# If this device does not have any IP addresses assigned, default to setting the first IP as its primary.
if not IPAddress.objects.filter(interface__device=device).count(): if not IPAddress.objects.filter(interface__device=device).count():
self.fields['set_as_primary'].initial = True self.fields['set_as_primary'].initial = True

View File

@ -852,8 +852,7 @@ class Device(CreatedUpdatedModel, CustomFieldModel):
'face': "Must specify rack face when defining rack position." 'face': "Must specify rack face when defining rack position."
}) })
if self.device_type: try:
# Child devices cannot be assigned to a rack face/unit # Child devices cannot be assigned to a rack face/unit
if self.device_type.is_child_device and self.face is not None: if self.device_type.is_child_device and self.face is not None:
raise ValidationError({ raise ValidationError({
@ -880,6 +879,9 @@ class Device(CreatedUpdatedModel, CustomFieldModel):
except Rack.DoesNotExist: except Rack.DoesNotExist:
pass pass
except DeviceType.DoesNotExist:
pass
def save(self, *args, **kwargs): def save(self, *args, **kwargs):
is_new = not bool(self.pk) is_new = not bool(self.pk)

View File

@ -220,12 +220,11 @@ class PrefixFromCSVForm(forms.ModelForm):
self.add_error('vlan_vid', "Must specify site and/or VLAN group when assigning a VLAN.") self.add_error('vlan_vid', "Must specify site and/or VLAN group when assigning a VLAN.")
def save(self, *args, **kwargs): def save(self, *args, **kwargs):
m = super(PrefixFromCSVForm, self).save(commit=False)
# Assign Prefix status by name # Assign Prefix status by name
m.status = dict(self.fields['status_name'].choices)[self.cleaned_data['status_name']] self.instance.status = dict(self.fields['status_name'].choices)[self.cleaned_data['status_name']]
if kwargs.get('commit'):
m.save() return super(PrefixFromCSVForm, self).save(*args, **kwargs)
return m
class PrefixImportForm(BulkImportForm, BootstrapMixin): class PrefixImportForm(BulkImportForm, BootstrapMixin):
@ -391,7 +390,10 @@ class IPAddressFromCSVForm(forms.ModelForm):
if is_primary and not device: if is_primary and not device:
self.add_error('is_primary', "No device specified; cannot set as primary IP") self.add_error('is_primary', "No device specified; cannot set as primary IP")
def save(self, commit=True): def save(self, *args, **kwargs):
# Assign status by name
self.instance.status = dict(self.fields['status_name'].choices)[self.cleaned_data['status_name']]
# Set interface # Set interface
if self.cleaned_data['device'] and self.cleaned_data['interface_name']: if self.cleaned_data['device'] and self.cleaned_data['interface_name']:
@ -404,7 +406,7 @@ class IPAddressFromCSVForm(forms.ModelForm):
elif self.instance.address.version == 6: elif self.instance.address.version == 6:
self.instance.primary_ip6_for = self.cleaned_data['device'] self.instance.primary_ip6_for = self.cleaned_data['device']
return super(IPAddressFromCSVForm, self).save(commit=commit) return super(IPAddressFromCSVForm, self).save(*args, **kwargs)
class IPAddressImportForm(BulkImportForm, BootstrapMixin): class IPAddressImportForm(BulkImportForm, BootstrapMixin):

View File

@ -22,23 +22,33 @@ AF_CHOICES = (
(6, 'IPv6'), (6, 'IPv6'),
) )
PREFIX_STATUS_CONTAINER = 0
PREFIX_STATUS_ACTIVE = 1
PREFIX_STATUS_RESERVED = 2
PREFIX_STATUS_DEPRECATED = 3
PREFIX_STATUS_CHOICES = ( PREFIX_STATUS_CHOICES = (
(0, 'Container'), (PREFIX_STATUS_CONTAINER, 'Container'),
(1, 'Active'), (PREFIX_STATUS_ACTIVE, 'Active'),
(2, 'Reserved'), (PREFIX_STATUS_RESERVED, 'Reserved'),
(3, 'Deprecated') (PREFIX_STATUS_DEPRECATED, 'Deprecated')
) )
IPADDRESS_STATUS_ACTIVE = 1
IPADDRESS_STATUS_RESERVED = 2
IPADDRESS_STATUS_DHCP = 5
IPADDRESS_STATUS_CHOICES = ( IPADDRESS_STATUS_CHOICES = (
(1, 'Active'), (IPADDRESS_STATUS_ACTIVE, 'Active'),
(2, 'Reserved'), (IPADDRESS_STATUS_RESERVED, 'Reserved'),
(5, 'DHCP') (IPADDRESS_STATUS_DHCP, 'DHCP')
) )
VLAN_STATUS_ACTIVE = 1
VLAN_STATUS_RESERVED = 2
VLAN_STATUS_DEPRECATED = 3
VLAN_STATUS_CHOICES = ( VLAN_STATUS_CHOICES = (
(1, 'Active'), (VLAN_STATUS_ACTIVE, 'Active'),
(2, 'Reserved'), (VLAN_STATUS_RESERVED, 'Reserved'),
(3, 'Deprecated') (VLAN_STATUS_DEPRECATED, 'Deprecated')
) )
STATUS_CHOICE_CLASSES = { STATUS_CHOICE_CLASSES = {

View File

@ -6,6 +6,25 @@ from utilities.tables import BaseTable, ToggleColumn
from .models import Aggregate, IPAddress, Prefix, RIR, Role, VLAN, VLANGroup, VRF from .models import Aggregate, IPAddress, Prefix, RIR, Role, VLAN, VLANGroup, VRF
RIR_UTILIZATION = """
<div class="progress">
{% if record.stats.total %}
<div class="progress-bar" role="progressbar" style="width: {{ record.stats.percentages.active }}%;">
<span class="sr-only">{{ record.stats.percentages.active }}%</span>
</div>
<div class="progress-bar progress-bar-info" role="progressbar" style="width: {{ record.stats.percentages.reserved }}%;">
<span class="sr-only">{{ record.stats.percentages.reserved }}%</span>
</div>
<div class="progress-bar progress-bar-danger" role="progressbar" style="width: {{ record.stats.percentages.deprecated }}%;">
<span class="sr-only">{{ record.stats.percentages.deprecated }}%</span>
</div>
<div class="progress-bar progress-bar-success" role="progressbar" style="width: {{ record.stats.percentages.available }}%;">
<span class="sr-only">{{ record.stats.percentages.available }}%</span>
</div>
{% endif %}
</div>
"""
RIR_ACTIONS = """ RIR_ACTIONS = """
{% if perms.ipam.change_rir %} {% if perms.ipam.change_rir %}
<a href="{% url 'ipam:rir_edit' slug=record.slug %}" class="btn btn-xs btn-warning"><i class="glyphicon glyphicon-pencil" aria-hidden="true"></i></a> <a href="{% url 'ipam:rir_edit' slug=record.slug %}" class="btn btn-xs btn-warning"><i class="glyphicon glyphicon-pencil" aria-hidden="true"></i></a>
@ -108,12 +127,22 @@ class RIRTable(BaseTable):
pk = ToggleColumn() pk = ToggleColumn()
name = tables.LinkColumn(verbose_name='Name') name = tables.LinkColumn(verbose_name='Name')
aggregate_count = tables.Column(verbose_name='Aggregates') aggregate_count = tables.Column(verbose_name='Aggregates')
slug = tables.Column(verbose_name='Slug') stats_total = tables.Column(accessor='stats.total', verbose_name='Total',
footer=lambda table: sum(r.stats['total'] for r in table.data))
stats_active = tables.Column(accessor='stats.active', verbose_name='Active',
footer=lambda table: sum(r.stats['active'] for r in table.data))
stats_reserved = tables.Column(accessor='stats.reserved', verbose_name='Reserved',
footer=lambda table: sum(r.stats['reserved'] for r in table.data))
stats_deprecated = tables.Column(accessor='stats.deprecated', verbose_name='Deprecated',
footer=lambda table: sum(r.stats['deprecated'] for r in table.data))
stats_available = tables.Column(accessor='stats.available', verbose_name='Available',
footer=lambda table: sum(r.stats['available'] for r in table.data))
utilization = tables.TemplateColumn(template_code=RIR_UTILIZATION, verbose_name='Utilization')
actions = tables.TemplateColumn(template_code=RIR_ACTIONS, attrs={'td': {'class': 'text-right'}}, verbose_name='') actions = tables.TemplateColumn(template_code=RIR_ACTIONS, attrs={'td': {'class': 'text-right'}}, verbose_name='')
class Meta(BaseTable.Meta): class Meta(BaseTable.Meta):
model = RIR model = RIR
fields = ('pk', 'name', 'aggregate_count', 'slug', 'actions') fields = ('pk', 'name', 'aggregate_count', 'stats_total', 'stats_active', 'stats_reserved', 'stats_deprecated', 'stats_available', 'utilization', 'actions')
# #

View File

@ -1,5 +1,6 @@
import netaddr from collections import OrderedDict
from django_tables2 import RequestConfig from django_tables2 import RequestConfig
import netaddr
from django.contrib.auth.decorators import permission_required from django.contrib.auth.decorators import permission_required
from django.contrib.auth.mixins import PermissionRequiredMixin from django.contrib.auth.mixins import PermissionRequiredMixin
@ -16,7 +17,7 @@ from utilities.views import (
) )
from . import filters, forms, tables from . import filters, forms, tables
from .models import Aggregate, IPAddress, Prefix, RIR, Role, VLAN, VLANGroup, VRF from .models import Aggregate, IPAddress, PREFIX_STATUS_ACTIVE, PREFIX_STATUS_DEPRECATED, PREFIX_STATUS_RESERVED, Prefix, RIR, Role, VLAN, VLANGroup, VRF
def add_available_prefixes(parent, prefix_list): def add_available_prefixes(parent, prefix_list):
@ -157,6 +158,82 @@ class RIRListView(ObjectListView):
edit_permissions = ['ipam.change_rir', 'ipam.delete_rir'] edit_permissions = ['ipam.change_rir', 'ipam.delete_rir']
template_name = 'ipam/rir_list.html' template_name = 'ipam/rir_list.html'
def alter_queryset(self, request):
if request.GET.get('family') == '6':
family = 6
denominator = 2 ** 64 # Count /64s for IPv6 rather than individual IPs
else:
family = 4
denominator = 1
rirs = []
for rir in self.queryset:
stats = {
'total': 0,
'active': 0,
'reserved': 0,
'deprecated': 0,
'available': 0,
}
aggregate_list = Aggregate.objects.filter(family=family, rir=rir)
for aggregate in aggregate_list:
queryset = Prefix.objects.filter(prefix__net_contained_or_equal=str(aggregate.prefix))
# Find all consumed space for each prefix status (we ignore containers for this purpose).
active_prefixes = netaddr.cidr_merge([p.prefix for p in queryset.filter(status=PREFIX_STATUS_ACTIVE)])
reserved_prefixes = netaddr.cidr_merge([p.prefix for p in queryset.filter(status=PREFIX_STATUS_RESERVED)])
deprecated_prefixes = netaddr.cidr_merge([p.prefix for p in queryset.filter(status=PREFIX_STATUS_DEPRECATED)])
# Find all available prefixes by subtracting each of the existing prefix sets from the aggregate prefix.
available_prefixes = (
netaddr.IPSet([aggregate.prefix]) -
netaddr.IPSet(active_prefixes) -
netaddr.IPSet(reserved_prefixes) -
netaddr.IPSet(deprecated_prefixes)
)
# Add the size of each metric to the RIR total.
stats['total'] += aggregate.prefix.size / denominator
stats['active'] += netaddr.IPSet(active_prefixes).size / denominator
stats['reserved'] += netaddr.IPSet(reserved_prefixes).size / denominator
stats['deprecated'] += netaddr.IPSet(deprecated_prefixes).size / denominator
stats['available'] += available_prefixes.size / denominator
# Calculate the percentage of total space for each prefix status.
total = float(stats['total'])
stats['percentages'] = {
'active': float('{:.2f}'.format(stats['active'] / total * 100)) if total else 0,
'reserved': float('{:.2f}'.format(stats['reserved'] / total * 100)) if total else 0,
'deprecated': float('{:.2f}'.format(stats['deprecated'] / total * 100)) if total else 0,
}
stats['percentages']['available'] = (
100 -
stats['percentages']['active'] -
stats['percentages']['reserved'] -
stats['percentages']['deprecated']
)
rir.stats = stats
rirs.append(rir)
return rirs
def extra_context(self):
totals = {
'total': sum([rir.stats['total'] for rir in self.queryset]),
'active': sum([rir.stats['active'] for rir in self.queryset]),
'reserved': sum([rir.stats['reserved'] for rir in self.queryset]),
'deprecated': sum([rir.stats['deprecated'] for rir in self.queryset]),
'available': sum([rir.stats['available'] for rir in self.queryset]),
}
return {
'totals': totals,
}
class RIREditView(PermissionRequiredMixin, ObjectEditView): class RIREditView(PermissionRequiredMixin, ObjectEditView):
permission_required = 'ipam.change_rir' permission_required = 'ipam.change_rir'

View File

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

View File

@ -85,6 +85,9 @@ label.required {
th.pk, td.pk { th.pk, td.pk {
width: 30px; width: 30px;
} }
tfoot td {
font-weight: bold;
}
/* Paginator */ /* Paginator */
nav ul.pagination { nav ul.pagination {

View File

@ -237,16 +237,14 @@
{% for pp in power_ports %} {% for pp in power_ports %}
{% include 'dcim/inc/_powerport.html' %} {% include 'dcim/inc/_powerport.html' %}
{% empty %} {% empty %}
{% if not device.device_type.is_pdu %} <tr>
<tr> <td colspan="5" class="alert-warning">
<td colspan="5" class="alert-warning"> <i class="fa fa-fw fa-warning"></i> No power ports defined
<i class="fa fa-fw fa-warning"></i> No power ports defined {% if perms.dcim.add_powerport %}
{% if perms.dcim.add_powerport %} <a href="{% url 'dcim:powerport_add' pk=device.pk %}" class="btn btn-primary btn-xs pull-right"><span class="glyphicon glyphicon-plus" aria-hidden="true"></span></a>
<a href="{% url 'dcim:powerport_add' pk=device.pk %}" class="btn btn-primary btn-xs pull-right"><span class="glyphicon glyphicon-plus" aria-hidden="true"></span></a> {% endif %}
{% endif %} </td>
</td> </tr>
</tr>
{% endif %}
{% endfor %} {% endfor %}
</table> </table>
{% if perms.dcim.add_interface or perms.dcim.add_consoleport or perms.dcim.add_powerport %} {% if perms.dcim.add_interface or perms.dcim.add_consoleport or perms.dcim.add_powerport %}
@ -261,7 +259,7 @@
<span class="glyphicon glyphicon-plus" aria-hidden="true"></span> Add console port <span class="glyphicon glyphicon-plus" aria-hidden="true"></span> Add console port
</a> </a>
{% endif %} {% endif %}
{% if perms.dcim.add_powerport and not device.device_type.is_pdu %} {% if perms.dcim.add_powerport %}
<a href="{% url 'dcim:powerport_add' pk=device.pk %}" class="btn btn-xs btn-primary"> <a href="{% url 'dcim:powerport_add' pk=device.pk %}" class="btn btn-xs btn-primary">
<span class="glyphicon glyphicon-plus" aria-hidden="true"></span> Add power port <span class="glyphicon glyphicon-plus" aria-hidden="true"></span> Add power port
</a> </a>

View File

@ -1,10 +1,22 @@
{% extends '_base.html' %} {% extends '_base.html' %}
{% load humanize %}
{% load helpers %} {% load helpers %}
{% block title %}RIRs{% endblock %} {% block title %}RIRs{% endblock %}
{% block content %} {% block content %}
<div class="pull-right"> <div class="pull-right">
{% if request.GET.family == '6' %}
<a href="{% url 'ipam:rir_list' %}" class="btn btn-default">
<span class="fa fa-table" aria-hidden="true"></span>
IPv4 Stats
</a>
{% else %}
<a href="{% url 'ipam:rir_list' %}?family=6" class="btn btn-default">
<span class="fa fa-table" aria-hidden="true"></span>
IPv6 Stats
</a>
{% endif %}
{% if perms.ipam.add_rir %} {% if perms.ipam.add_rir %}
<a href="{% url 'ipam:rir_add' %}" class="btn btn-primary"> <a href="{% url 'ipam:rir_add' %}" class="btn btn-primary">
<span class="fa fa-plus" aria-hidden="true"></span> <span class="fa fa-plus" aria-hidden="true"></span>
@ -18,4 +30,7 @@
{% include 'utilities/obj_table.html' with bulk_delete_url='ipam:rir_bulk_delete' %} {% include 'utilities/obj_table.html' with bulk_delete_url='ipam:rir_bulk_delete' %}
</div> </div>
</div> </div>
{% if request.GET.family == '6' %}
<div class="pull-right text-muted"><strong>Note:</strong> Numbers shown indicate /64 prefixes.</div>
{% endif %}
{% endblock %} {% endblock %}

View File

@ -16,6 +16,13 @@
<input type="checkbox" name="_nullify" value="{{ field.name }}" /> Set null <input type="checkbox" name="_nullify" value="{{ field.name }}" /> Set null
</label> </label>
{% endif %} {% endif %}
{% if field.errors %}
<ul>
{% for error in field.errors %}
<li class="text-danger">{{ error }}</li>
{% endfor %}
</ul>
{% endif %}
</div> </div>
{% elif field|widget_type == 'textarea' %} {% elif field|widget_type == 'textarea' %}
<div class="col-md-12"> <div class="col-md-12">