Explorar o código

Merge pull request #299 from digitalocean/develop

Release v1.2.2
Jeremy Stretch %!s(int64=9) %!d(string=hai) anos
pai
achega
4e64e1ea95

+ 3 - 0
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):

+ 31 - 0
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',

+ 10 - 0
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
 #

+ 21 - 0
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'),
+        ),
+    ]

+ 2 - 1
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')

+ 2 - 0
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'

+ 9 - 0
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'
 

+ 5 - 0
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(),

+ 8 - 1
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',

+ 21 - 0
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'),
+        ),
+    ]

+ 2 - 2
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)

+ 7 - 2
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}),
         })
 

+ 1 - 2
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',
             ],
         },
     },

+ 3 - 0
netbox/project-static/css/base.css

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

+ 7 - 1
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) {

+ 21 - 1
netbox/templates/circuits/provider_list.html

@@ -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 %}

+ 1 - 0
netbox/templates/dcim/device_list.html

@@ -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">

+ 63 - 8
netbox/templates/dcim/devicetype.html

@@ -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 %}

+ 4 - 1
netbox/templates/dcim/inc/devicetype_component_table.html

@@ -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' %}

+ 1 - 0
netbox/templates/dcim/rack_list.html

@@ -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">

+ 1 - 0
netbox/templates/dcim/site_list.html

@@ -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">

+ 1 - 0
netbox/templates/inc/filter_panel.html

@@ -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">

+ 1 - 0
netbox/templates/ipam/ipaddress_list.html

@@ -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">

+ 1 - 0
netbox/templates/ipam/prefix_list.html

@@ -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">

+ 1 - 0
netbox/templates/ipam/vlan_list.html

@@ -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">