diff --git a/docs/installation/netbox.md b/docs/installation/netbox.md index fde5ab019..813291033 100644 --- a/docs/installation/netbox.md +++ b/docs/installation/netbox.md @@ -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): diff --git a/netbox/circuits/filters.py b/netbox/circuits/filters.py index 49d5e1b80..fc6628e87 100644 --- a/netbox/circuits/filters.py +++ b/netbox/circuits/filters.py @@ -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', diff --git a/netbox/circuits/forms.py b/netbox/circuits/forms.py index 7046b8ec3..847402c7d 100644 --- a/netbox/circuits/forms.py +++ b/netbox/circuits/forms.py @@ -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 # diff --git a/netbox/circuits/migrations/0003_provider_32bit_asn_support.py b/netbox/circuits/migrations/0003_provider_32bit_asn_support.py new file mode 100644 index 000000000..f1010064e --- /dev/null +++ b/netbox/circuits/migrations/0003_provider_32bit_asn_support.py @@ -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'), + ), + ] diff --git a/netbox/circuits/models.py b/netbox/circuits/models.py index d785eb1f2..c7c60378e 100644 --- a/netbox/circuits/models.py +++ b/netbox/circuits/models.py @@ -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') diff --git a/netbox/circuits/views.py b/netbox/circuits/views.py index 91be2ad54..790b8ba23 100644 --- a/netbox/circuits/views.py +++ b/netbox/circuits/views.py @@ -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' diff --git a/netbox/dcim/fields.py b/netbox/dcim/fields.py index e00b1115f..6b45f6e65 100644 --- a/netbox/dcim/fields.py +++ b/netbox/dcim/fields.py @@ -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' diff --git a/netbox/dcim/filters.py b/netbox/dcim/filters.py index 9712899a8..8872e5747 100644 --- a/netbox/dcim/filters.py +++ b/netbox/dcim/filters.py @@ -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(), diff --git a/netbox/dcim/forms.py b/netbox/dcim/forms.py index 89292717b..e3db78ec4 100644 --- a/netbox/dcim/forms.py +++ b/netbox/dcim/forms.py @@ -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', diff --git a/netbox/dcim/migrations/0009_site_32bit_asn_support.py b/netbox/dcim/migrations/0009_site_32bit_asn_support.py new file mode 100644 index 000000000..c93340cea --- /dev/null +++ b/netbox/dcim/migrations/0009_site_32bit_asn_support.py @@ -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'), + ), + ] diff --git a/netbox/dcim/models.py b/netbox/dcim/models.py index c51714a71..8bf97224c 100644 --- a/netbox/dcim/models.py +++ b/netbox/dcim/models.py @@ -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) diff --git a/netbox/dcim/views.py b/netbox/dcim/views.py index 92c8c029c..c97e2a3e1 100644 --- a/netbox/dcim/views.py +++ b/netbox/dcim/views.py @@ -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}), }) diff --git a/netbox/netbox/settings.py b/netbox/netbox/settings.py index 13bc4bda5..b4e1f1667 100644 --- a/netbox/netbox/settings.py +++ b/netbox/netbox/settings.py @@ -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', ], }, }, diff --git a/netbox/project-static/css/base.css b/netbox/project-static/css/base.css index b1a9e88c3..9f4bb5e24 100644 --- a/netbox/project-static/css/base.css +++ b/netbox/project-static/css/base.css @@ -2,6 +2,9 @@ * { margin: 0; } +html { + overflow-y: scroll; +} html, body { height: 100%; } diff --git a/netbox/project-static/js/forms.js b/netbox/project-static/js/forms.js index 4ba61f13a..fd510af3f 100644 --- a/netbox/project-static/js/forms.js +++ b/netbox/project-static/js/forms.js @@ -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) { diff --git a/netbox/templates/circuits/provider_list.html b/netbox/templates/circuits/provider_list.html index 2b5bfc68c..c6b81063b 100644 --- a/netbox/templates/circuits/provider_list.html +++ b/netbox/templates/circuits/provider_list.html @@ -14,8 +14,28 @@

Providers

-
+
{% include 'utilities/obj_table.html' with bulk_edit_url='circuits:provider_bulk_edit' bulk_delete_url='circuits:provider_bulk_delete' %}
+
+
+
+ Search +
+
+
+
+ + + + +
+
+
+
+ {% include 'inc/filter_panel.html' %} +
{% endblock %} diff --git a/netbox/templates/dcim/device_list.html b/netbox/templates/dcim/device_list.html index bae95b0ec..ee127cf48 100644 --- a/netbox/templates/dcim/device_list.html +++ b/netbox/templates/dcim/device_list.html @@ -25,6 +25,7 @@
+ Search
diff --git a/netbox/templates/dcim/devicetype.html b/netbox/templates/dcim/devicetype.html index f3ae5aa78..df567a102 100644 --- a/netbox/templates/dcim/devicetype.html +++ b/netbox/templates/dcim/devicetype.html @@ -42,7 +42,7 @@ - + @@ -54,7 +54,13 @@ - +
Manufacturer{{ devicetype.manufacturer }}{{ devicetype.manufacturer }}
Model Name
Full Depth{{ devicetype.is_full_depth|yesno|capfirst }} + {% if devicetype.is_full_depth %} + + {% else %} + + {% endif %} +
@@ -64,21 +70,70 @@
- - + + - - + + - - + + + + + +
Is a Console Server{{ devicetype.is_console_server|yesno|capfirst }} + {% if devicetype.is_console_server %} + + {% else %} + + {% endif %} + + Console Server
+ This device {% if devicetype.is_console_server %}has{% else %}does not have{% endif %} console server ports +
Is a PDU{{ devicetype.is_pdu|yesno|capfirst }} + {% if devicetype.is_pdu %} + + {% else %} + + {% endif %} + + PDU
+ This device {% if devicetype.is_pdu %}has{% else %}does not have{% endif %} power outlets +
Is a Network Device{{ devicetype.is_network_device|yesno|capfirst }} + {% if devicetype.is_network_device %} + + {% else %} + + {% endif %} + + Network Device
+ This device {% if devicetype.is_network_device %}has{% else %}does not have{% endif %} non-management network interfaces +
+ {% if devicetype.subdevice_role == True %} + + {% elif devicetype.subdevice_role == False %} + + {% else %} + + {% endif %} + + Parent/Child
+ {% if devicetype.subdevice_role == True %} + This device has device bays for mounting child devices + {% elif devicetype.subdevice_role == False %} + This device can only be mounted in a parent device + {% else %} + This device does not have device bays + {% endif %} +
{% 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' %}
{% if devicetype.is_parent_device %} diff --git a/netbox/templates/dcim/inc/devicetype_component_table.html b/netbox/templates/dcim/inc/devicetype_component_table.html index 55bed30e9..948c2c44c 100644 --- a/netbox/templates/dcim/inc/devicetype_component_table.html +++ b/netbox/templates/dcim/inc/devicetype_component_table.html @@ -4,7 +4,10 @@ {% csrf_token %}
{% render_table table 'table.html' %} diff --git a/netbox/templates/dcim/rack_list.html b/netbox/templates/dcim/rack_list.html index ec23d2736..811edb823 100644 --- a/netbox/templates/dcim/rack_list.html +++ b/netbox/templates/dcim/rack_list.html @@ -25,6 +25,7 @@
+ Search
diff --git a/netbox/templates/dcim/site_list.html b/netbox/templates/dcim/site_list.html index f3334ec6f..d35410443 100644 --- a/netbox/templates/dcim/site_list.html +++ b/netbox/templates/dcim/site_list.html @@ -21,6 +21,7 @@
+ Search
diff --git a/netbox/templates/inc/filter_panel.html b/netbox/templates/inc/filter_panel.html index 1a7b70704..32a7e21f8 100644 --- a/netbox/templates/inc/filter_panel.html +++ b/netbox/templates/inc/filter_panel.html @@ -2,6 +2,7 @@
+ Filter
diff --git a/netbox/templates/ipam/ipaddress_list.html b/netbox/templates/ipam/ipaddress_list.html index edcc140c3..ce09fc97c 100644 --- a/netbox/templates/ipam/ipaddress_list.html +++ b/netbox/templates/ipam/ipaddress_list.html @@ -26,6 +26,7 @@
+ Search
diff --git a/netbox/templates/ipam/prefix_list.html b/netbox/templates/ipam/prefix_list.html index 0b871f22a..81fd7b795 100644 --- a/netbox/templates/ipam/prefix_list.html +++ b/netbox/templates/ipam/prefix_list.html @@ -26,6 +26,7 @@
+ Search
diff --git a/netbox/templates/ipam/vlan_list.html b/netbox/templates/ipam/vlan_list.html index f9ed6d816..78ff2b47c 100644 --- a/netbox/templates/ipam/vlan_list.html +++ b/netbox/templates/ipam/vlan_list.html @@ -26,6 +26,7 @@
+ Search by ID