Merge pull request #299 from digitalocean/develop

Release v1.2.2
This commit is contained in:
Jeremy Stretch 2016-07-14 15:21:40 -04:00 committed by GitHub
commit 4e64e1ea95
25 changed files with 227 additions and 19 deletions

View File

@ -112,6 +112,9 @@ Generate a random secret key of at least 50 alphanumeric characters. This key mu
You may use the script located at `netbox/generate_secret_key.py` to generate a suitable key.
!!! note
In the case of a highly available installation with multiple web servers, `SECRET_KEY` must be identical among all servers in order to maintain a persistent user session state.
# Run Database Migrations
Before NetBox can run, we need to install the database schema. This is done by running `./manage.py migrate` from the `netbox` directory (`/opt/netbox/netbox/` in our example):

View File

@ -1,9 +1,40 @@
import django_filters
from django.db.models import Q
from dcim.models import Site
from .models import Provider, Circuit, CircuitType
class ProviderFilter(django_filters.FilterSet):
q = django_filters.MethodFilter(
action='search',
label='Search',
)
site_id = django_filters.ModelMultipleChoiceFilter(
name='circuits__site',
queryset=Site.objects.all(),
label='Site',
)
site = django_filters.ModelMultipleChoiceFilter(
name='circuits__site',
queryset=Site.objects.all(),
to_field_name='slug',
label='Site (slug)',
)
class Meta:
model = Provider
fields = ['q', 'name', 'account', 'asn']
def search(self, queryset, value):
value = value.strip()
return queryset.filter(
Q(name__icontains=value) |
Q(account__icontains=value)
)
class CircuitFilter(django_filters.FilterSet):
q = django_filters.MethodFilter(
action='search',

View File

@ -59,6 +59,16 @@ class ProviderBulkDeleteForm(ConfirmationForm):
pk = forms.ModelMultipleChoiceField(queryset=Provider.objects.all(), widget=forms.MultipleHiddenInput)
def provider_site_choices():
site_choices = Site.objects.all()
return [(s.slug, s.name) for s in site_choices]
class ProviderFilterForm(forms.Form, BootstrapMixin):
site = forms.MultipleChoiceField(required=False, choices=provider_site_choices,
widget=forms.SelectMultiple(attrs={'size': 8}))
#
# Circuit types
#

View File

@ -0,0 +1,21 @@
# -*- coding: utf-8 -*-
# Generated by Django 1.9.7 on 2016-07-13 19:24
from __future__ import unicode_literals
import dcim.fields
from django.db import migrations
class Migration(migrations.Migration):
dependencies = [
('circuits', '0002_auto_20160622_1821'),
]
operations = [
migrations.AlterField(
model_name='provider',
name='asn',
field=dcim.fields.ASNField(blank=True, null=True, verbose_name=b'ASN'),
),
]

View File

@ -1,6 +1,7 @@
from django.core.urlresolvers import reverse
from django.db import models
from dcim.fields import ASNField
from dcim.models import Site, Interface
from utilities.models import CreatedUpdatedModel
@ -12,7 +13,7 @@ class Provider(CreatedUpdatedModel):
"""
name = models.CharField(max_length=50, unique=True)
slug = models.SlugField(unique=True)
asn = models.PositiveIntegerField(blank=True, null=True, verbose_name='ASN')
asn = ASNField(blank=True, null=True, verbose_name='ASN')
account = models.CharField(max_length=30, blank=True, verbose_name='Account number')
portal_url = models.URLField(blank=True, verbose_name='Portal')
noc_contact = models.TextField(blank=True, verbose_name='NOC contact')

View File

@ -16,6 +16,8 @@ from .models import Circuit, CircuitType, Provider
class ProviderListView(ObjectListView):
queryset = Provider.objects.annotate(count_circuits=Count('circuits'))
filter = filters.ProviderFilter
filter_form = forms.ProviderFilterForm
table = tables.ProviderTable
edit_permissions = ['circuits.change_provider', 'circuits.delete_provider']
template_name = 'circuits/provider_list.html'

View File

@ -1,11 +1,20 @@
from netaddr import EUI, mac_unix_expanded
from django.core.exceptions import ValidationError
from django.core.validators import MinValueValidator, MaxValueValidator
from django.db import models
from .formfields import MACAddressFormField
class ASNField(models.BigIntegerField):
description = "32-bit ASN field"
default_validators = [
MinValueValidator(1),
MaxValueValidator(4294967295),
]
class mac_unix_expanded_uppercase(mac_unix_expanded):
word_fmt = '%.2X'

View File

@ -122,6 +122,11 @@ class DeviceFilter(django_filters.FilterSet):
to_field_name='slug',
label='Site name (slug)',
)
rack_group_id = django_filters.ModelMultipleChoiceFilter(
name='rack__group',
queryset=RackGroup.objects.all(),
label='Rack group (ID)',
)
rack_id = django_filters.ModelMultipleChoiceFilter(
name='rack',
queryset=Rack.objects.all(),

View File

@ -186,7 +186,7 @@ def rack_group_choices():
class RackFilterForm(forms.Form, BootstrapMixin):
site = forms.MultipleChoiceField(required=False, choices=rack_site_choices,
widget=forms.SelectMultiple(attrs={'size': 8}))
group_id = forms.MultipleChoiceField(required=False, choices=rack_group_choices,
group_id = forms.MultipleChoiceField(required=False, choices=rack_group_choices, label='Rack Group',
widget=forms.SelectMultiple(attrs={'size': 8}))
@ -502,6 +502,11 @@ def device_site_choices():
return [(s.slug, '{} ({})'.format(s.name, s.device_count)) for s in site_choices]
def device_rack_group_choices():
group_choices = RackGroup.objects.select_related('site').annotate(device_count=Count('racks__devices'))
return [(g.pk, '{} ({})'.format(g, g.device_count)) for g in group_choices]
def device_role_choices():
role_choices = DeviceRole.objects.annotate(device_count=Count('devices'))
return [(r.slug, '{} ({})'.format(r.name, r.device_count)) for r in role_choices]
@ -520,6 +525,8 @@ def device_platform_choices():
class DeviceFilterForm(forms.Form, BootstrapMixin):
site = forms.MultipleChoiceField(required=False, choices=device_site_choices,
widget=forms.SelectMultiple(attrs={'size': 8}))
rack_group_id = forms.MultipleChoiceField(required=False, choices=device_rack_group_choices, label='Rack Group',
widget=forms.SelectMultiple(attrs={'size': 8}))
role = forms.MultipleChoiceField(required=False, choices=device_role_choices,
widget=forms.SelectMultiple(attrs={'size': 8}))
device_type_id = forms.MultipleChoiceField(required=False, choices=device_type_choices, label='Type',

View File

@ -0,0 +1,21 @@
# -*- coding: utf-8 -*-
# Generated by Django 1.9.7 on 2016-07-13 19:24
from __future__ import unicode_literals
import dcim.fields
from django.db import migrations
class Migration(migrations.Migration):
dependencies = [
('dcim', '0008_device_remove_primary_ip'),
]
operations = [
migrations.AlterField(
model_name='site',
name='asn',
field=dcim.fields.ASNField(blank=True, null=True, verbose_name=b'ASN'),
),
]

View File

@ -11,7 +11,7 @@ from extras.rpc import RPC_CLIENTS
from utilities.fields import NullableCharField
from utilities.models import CreatedUpdatedModel
from .fields import MACAddressField
from .fields import ASNField, MACAddressField
RACK_FACE_FRONT = 0
RACK_FACE_REAR = 1
@ -145,7 +145,7 @@ class Site(CreatedUpdatedModel):
name = models.CharField(max_length=50, unique=True)
slug = models.SlugField(unique=True)
facility = models.CharField(max_length=50, blank=True)
asn = models.PositiveIntegerField(blank=True, null=True, verbose_name='ASN')
asn = ASNField(blank=True, null=True, verbose_name='ASN')
physical_address = models.CharField(max_length=200, blank=True)
shipping_address = models.CharField(max_length=200, blank=True)
comments = models.TextField(blank=True)

View File

@ -273,7 +273,10 @@ def devicetype(request, pk):
poweroutlet_table = tables.PowerOutletTemplateTable(
natsorted(PowerOutletTemplate.objects.filter(device_type=devicetype), key=attrgetter('name'))
)
interface_table = tables.InterfaceTemplateTable(InterfaceTemplate.objects.filter(device_type=devicetype))
mgmt_interface_table = tables.InterfaceTemplateTable(InterfaceTemplate.objects.filter(device_type=devicetype,
mgmt_only=True))
interface_table = tables.InterfaceTemplateTable(InterfaceTemplate.objects.filter(device_type=devicetype,
mgmt_only=False))
devicebay_table = tables.DeviceBayTemplateTable(
natsorted(DeviceBayTemplate.objects.filter(device_type=devicetype), key=attrgetter('name'))
)
@ -282,6 +285,7 @@ def devicetype(request, pk):
consoleserverport_table.base_columns['pk'].visible = True
powerport_table.base_columns['pk'].visible = True
poweroutlet_table.base_columns['pk'].visible = True
mgmt_interface_table.base_columns['pk'].visible = True
interface_table.base_columns['pk'].visible = True
devicebay_table.base_columns['pk'].visible = True
@ -291,6 +295,7 @@ def devicetype(request, pk):
'consoleserverport_table': consoleserverport_table,
'powerport_table': powerport_table,
'poweroutlet_table': poweroutlet_table,
'mgmt_interface_table': mgmt_interface_table,
'interface_table': interface_table,
'devicebay_table': devicebay_table,
})
@ -348,7 +353,7 @@ class ComponentTemplateCreateView(View):
return render(request, 'dcim/component_template_add.html', {
'devicetype': devicetype,
'component_type': self.model._meta.verbose_name,
'form': self.form(),
'form': self.form(initial=request.GET),
'cancel_url': reverse('dcim:devicetype', kwargs={'pk': devicetype.pk}),
})

View File

@ -12,7 +12,7 @@ except ImportError:
"the documentation.")
VERSION = '1.2.1'
VERSION = '1.2.2'
# Import local configuration
for setting in ['ALLOWED_HOSTS', 'DATABASE', 'SECRET_KEY']:
@ -138,7 +138,6 @@ TEMPLATES = [
'django.contrib.auth.context_processors.auth',
'django.contrib.messages.context_processors.messages',
'utilities.context_processors.settings',
'django.core.context_processors.request',
],
},
},

View File

@ -2,6 +2,9 @@
* {
margin: 0;
}
html {
overflow-y: scroll;
}
html, body {
height: 100%;
}

View File

@ -1,9 +1,15 @@
$(document).ready(function() {
// "Select all" checkbox in a table header
$('th input:checkbox').click(function (event) {
$('th input:checkbox[name=_all]').click(function (event) {
$(this).parents('table').find('td input:checkbox').prop('checked', $(this).prop('checked'));
});
// Uncheck the "select all" checkbox if an item is unchecked
$('input:checkbox[name=pk]').click(function (event) {
if (!$(this).attr('checked')) {
$(this).parents('table').find('input:checkbox[name=_all]').prop('checked', false);
}
});
// Slugify
function slugify(s, num_chars) {

View File

@ -14,8 +14,28 @@
</div>
<h1>Providers</h1>
<div class="row">
<div class="col-md-12">
<div class="col-md-9">
{% include 'utilities/obj_table.html' with bulk_edit_url='circuits:provider_bulk_edit' bulk_delete_url='circuits:provider_bulk_delete' %}
</div>
<div class="col-md-3">
<div class="panel panel-default">
<div class="panel-heading">
<strong>Search</strong>
</div>
<div class="panel-body">
<form action="{% url 'circuits:provider_list' %}" method="get">
<div class="input-group">
<input type="text" name="q" class="form-control" placeholder="Name" {% if request.GET.q %}value="{{ request.GET.q }}" {% endif %}/>
<span class="input-group-btn">
<button type="submit" class="btn btn-primary">
<span class="glyphicon glyphicon-search" aria-hidden="true"></span>
</button>
</span>
</div>
</form>
</div>
</div>
{% include 'inc/filter_panel.html' %}
</div>
</div>
{% endblock %}

View File

@ -25,6 +25,7 @@
<div class="col-md-3">
<div class="panel panel-default">
<div class="panel-heading">
<span class="glyphicon glyphicon-search" aria-hidden="true"></span>
<strong>Search</strong>
</div>
<div class="panel-body">

View File

@ -42,7 +42,7 @@
<table class="table table-hover panel-body">
<tr>
<td>Manufacturer</td>
<td>{{ devicetype.manufacturer }}</td>
<td><a href="{% url 'dcim:devicetype_list' %}?manufacturer={{ devicetype.manufacturer.slug }}">{{ devicetype.manufacturer }}</a></td>
</tr>
<tr>
<td>Model Name</td>
@ -54,7 +54,13 @@
</tr>
<tr>
<td>Full Depth</td>
<td>{{ devicetype.is_full_depth|yesno|capfirst }}</td>
<td>
{% if devicetype.is_full_depth %}
<i class="glyphicon glyphicon-ok text-success" title="Yes"></i>
{% else %}
<i class="glyphicon glyphicon-remove text-danger" title="No"></i>
{% endif %}
</td>
</tr>
</table>
</div>
@ -64,21 +70,70 @@
</div>
<table class="table table-hover panel-body">
<tr>
<td>Is a Console Server</td>
<td>{{ devicetype.is_console_server|yesno|capfirst }}</td>
<td class="text-right">
{% if devicetype.is_console_server %}
<i class="glyphicon glyphicon-ok text-success" title="Yes"></i>
{% else %}
<i class="glyphicon glyphicon-remove text-danger" title="No"></i>
{% endif %}
</td>
<td>
<strong>Console Server</strong><br />
<small class="text-muted">This device {% if devicetype.is_console_server %}has{% else %}does not have{% endif %} console server ports</small>
</td>
</tr>
<tr>
<td>Is a PDU</td>
<td>{{ devicetype.is_pdu|yesno|capfirst }}</td>
<td class="text-right">
{% if devicetype.is_pdu %}
<i class="glyphicon glyphicon-ok text-success" title="Yes"></i>
{% else %}
<i class="glyphicon glyphicon-remove text-danger" title="No"></i>
{% endif %}
</td>
<td>
<strong>PDU</strong><br />
<small class="text-muted">This device {% if devicetype.is_pdu %}has{% else %}does not have{% endif %} power outlets</small>
</td>
</tr>
<tr>
<td>Is a Network Device</td>
<td>{{ devicetype.is_network_device|yesno|capfirst }}</td>
<td class="text-right">
{% if devicetype.is_network_device %}
<i class="glyphicon glyphicon-ok text-success" title="Yes"></i>
{% else %}
<i class="glyphicon glyphicon-remove text-danger" title="No"></i>
{% endif %}
</td>
<td>
<strong>Network Device</strong><br />
<small class="text-muted">This device {% if devicetype.is_network_device %}has{% else %}does not have{% endif %} non-management network interfaces</small>
</td>
</tr>
<tr>
<td class="text-right">
{% if devicetype.subdevice_role == True %}
<label class="label label-primary">Parent</label>
{% elif devicetype.subdevice_role == False %}
<label class="label label-info">Child</label>
{% else %}
<label class="label label-default">None</label>
{% endif %}
</td>
<td>
<strong>Parent/Child</strong><br />
{% if devicetype.subdevice_role == True %}
<small class="text-muted">This device has device bays for mounting child devices</small>
{% elif devicetype.subdevice_role == False %}
<small class="text-muted">This device can only be mounted in a parent device</small>
{% else %}
<small class="text-muted">This device does not have device bays</small>
{% endif %}
</td>
</tr>
</table>
</div>
{% include 'dcim/inc/devicetype_component_table.html' with table=consoleport_table title='Console Ports' add_url='dcim:devicetype_add_consoleport' delete_url='dcim:devicetype_delete_consoleport' %}
{% include 'dcim/inc/devicetype_component_table.html' with table=powerport_table title='Power Ports' add_url='dcim:devicetype_add_powerport' delete_url='dcim:devicetype_delete_powerport' %}
{% include 'dcim/inc/devicetype_component_table.html' with table=mgmt_interface_table title='Management Interfaces' add_url='dcim:devicetype_add_interface' add_url_extra='?mgmt_only=1' delete_url='dcim:devicetype_delete_interface' %}
</div>
<div class="col-md-6">
{% if devicetype.is_parent_device %}

View File

@ -4,7 +4,10 @@
{% csrf_token %}
<div class="panel panel-default">
<div class="panel-heading">
<a href="{% url add_url pk=devicetype.pk %}" class="btn btn-primary btn-xs pull-right"><span class="glyphicon glyphicon-plus" aria-hidden="true"></span> Add {{ title }}</a>
<a href="{% url add_url pk=devicetype.pk %}{{ add_url_extra }}" class="btn btn-primary btn-xs pull-right">
<span class="glyphicon glyphicon-plus" aria-hidden="true"></span>
Add {{ title }}
</a>
<strong>{{ title }}</strong>
</div>
{% render_table table 'table.html' %}

View File

@ -25,6 +25,7 @@
<div class="col-md-3">
<div class="panel panel-default">
<div class="panel-heading">
<span class="glyphicon glyphicon-search" aria-hidden="true"></span>
<strong>Search</strong>
</div>
<div class="panel-body">

View File

@ -21,6 +21,7 @@
<div class="col-md-3">
<div class="panel panel-default">
<div class="panel-heading">
<span class="glyphicon glyphicon-search" aria-hidden="true"></span>
<strong>Search</strong>
</div>
<div class="panel-body">

View File

@ -2,6 +2,7 @@
<div class="panel panel-default">
<div class="panel-heading">
<span class="glyphicon glyphicon-filter" aria-hidden="true"></span>
<strong>Filter</strong>
</div>
<div class="panel-body">

View File

@ -26,6 +26,7 @@
<div class="col-md-3">
<div class="panel panel-default">
<div class="panel-heading">
<span class="glyphicon glyphicon-search" aria-hidden="true"></span>
<strong>Search</strong>
</div>
<div class="panel-body">

View File

@ -26,6 +26,7 @@
<div class="col-md-3">
<div class="panel panel-default">
<div class="panel-heading">
<span class="glyphicon glyphicon-search" aria-hidden="true"></span>
<strong>Search</strong>
</div>
<div class="panel-body">

View File

@ -26,6 +26,7 @@
<div class="col-md-3">
<div class="panel panel-default">
<div class="panel-heading">
<span class="glyphicon glyphicon-search" aria-hidden="true"></span>
<strong>Search by ID</strong>
</div>
<div class="panel-body">