Browse Source

Merge branch 'develop' into develop-2.1

Conflicts:
	netbox/netbox/settings.py
Jeremy Stretch 7 years ago
parent
commit
ba8f48af65

+ 14 - 2
netbox/dcim/forms.py

@@ -13,8 +13,8 @@ from tenancy.forms import TenancyForm
 from tenancy.models import Tenant
 from utilities.forms import (
     APISelect, add_blank_choice, ArrayFieldSelectMultiple, BootstrapMixin, BulkEditForm, BulkEditNullBooleanSelect,
-    ChainedFieldsMixin, ChainedModelChoiceField, CommentField, CSVChoiceField, ExpandableNameField, FilterChoiceField,
-    FlexibleModelChoiceField, Livesearch, SelectWithDisabled, SmallTextarea, SlugField,
+    ChainedFieldsMixin, ChainedModelChoiceField, CommentField, ConfirmationForm, CSVChoiceField, ExpandableNameField,
+    FilterChoiceField, FlexibleModelChoiceField, Livesearch, SelectWithDisabled, SmallTextarea, SlugField,
     FilterTreeNodeMultipleChoiceField,
 )
 from .formfields import MACAddressFormField
@@ -1174,6 +1174,10 @@ class ConsoleServerPortConnectionForm(BootstrapMixin, ChainedFieldsMixin, forms.
         }
 
 
+class ConsoleServerPortBulkDisconnectForm(ConfirmationForm):
+    pk = forms.ModelMultipleChoiceField(queryset=ConsoleServerPort.objects.all(), widget=forms.MultipleHiddenInput)
+
+
 #
 # Power ports
 #
@@ -1431,6 +1435,10 @@ class PowerOutletConnectionForm(BootstrapMixin, ChainedFieldsMixin, forms.Form):
         }
 
 
+class PowerOutletBulkDisconnectForm(ConfirmationForm):
+    pk = forms.ModelMultipleChoiceField(queryset=PowerOutlet.objects.all(), widget=forms.MultipleHiddenInput)
+
+
 #
 # Interfaces
 #
@@ -1508,6 +1516,10 @@ class InterfaceBulkEditForm(BootstrapMixin, BulkEditForm):
             self.fields['lag'].choices = []
 
 
+class InterfaceBulkDisconnectForm(ConfirmationForm):
+    pk = forms.ModelMultipleChoiceField(queryset=Interface.objects.all(), widget=forms.MultipleHiddenInput)
+
+
 #
 # Interface connections
 #

+ 3 - 0
netbox/dcim/urls.py

@@ -139,6 +139,7 @@ urlpatterns = [
     # Console server ports
     url(r'^devices/console-server-ports/add/$', views.DeviceBulkAddConsoleServerPortView.as_view(), name='device_bulk_add_consoleserverport'),
     url(r'^devices/(?P<pk>\d+)/console-server-ports/add/$', views.ConsoleServerPortCreateView.as_view(), name='consoleserverport_add'),
+    url(r'^devices/(?P<pk>\d+)/console-server-ports/disconnect/$', views.ConsoleServerPortBulkDisconnectView.as_view(), name='consoleserverport_bulk_disconnect'),
     url(r'^devices/(?P<pk>\d+)/console-server-ports/delete/$', views.ConsoleServerPortBulkDeleteView.as_view(), name='consoleserverport_bulk_delete'),
     url(r'^console-server-ports/(?P<pk>\d+)/connect/$', views.consoleserverport_connect, name='consoleserverport_connect'),
     url(r'^console-server-ports/(?P<pk>\d+)/disconnect/$', views.consoleserverport_disconnect, name='consoleserverport_disconnect'),
@@ -157,6 +158,7 @@ urlpatterns = [
     # Power outlets
     url(r'^devices/power-outlets/add/$', views.DeviceBulkAddPowerOutletView.as_view(), name='device_bulk_add_poweroutlet'),
     url(r'^devices/(?P<pk>\d+)/power-outlets/add/$', views.PowerOutletCreateView.as_view(), name='poweroutlet_add'),
+    url(r'^devices/(?P<pk>\d+)/power-outlets/disconnect/$', views.PowerOutletBulkDisconnectView.as_view(), name='poweroutlet_bulk_disconnect'),
     url(r'^devices/(?P<pk>\d+)/power-outlets/delete/$', views.PowerOutletBulkDeleteView.as_view(), name='poweroutlet_bulk_delete'),
     url(r'^power-outlets/(?P<pk>\d+)/connect/$', views.poweroutlet_connect, name='poweroutlet_connect'),
     url(r'^power-outlets/(?P<pk>\d+)/disconnect/$', views.poweroutlet_disconnect, name='poweroutlet_disconnect'),
@@ -167,6 +169,7 @@ urlpatterns = [
     url(r'^devices/interfaces/add/$', views.DeviceBulkAddInterfaceView.as_view(), name='device_bulk_add_interface'),
     url(r'^devices/(?P<pk>\d+)/interfaces/add/$', views.InterfaceCreateView.as_view(), name='interface_add'),
     url(r'^devices/(?P<pk>\d+)/interfaces/edit/$', views.InterfaceBulkEditView.as_view(), name='interface_bulk_edit'),
+    url(r'^devices/(?P<pk>\d+)/interfaces/disconnect/$', views.InterfaceBulkDisconnectView.as_view(), name='interface_bulk_disconnect'),
     url(r'^devices/(?P<pk>\d+)/interfaces/delete/$', views.InterfaceBulkDeleteView.as_view(), name='interface_bulk_delete'),
     url(r'^devices/(?P<pk>\d+)/interface-connections/add/$', views.interfaceconnection_add, name='interfaceconnection_add'),
     url(r'^interface-connections/(?P<pk>\d+)/delete/$', views.interfaceconnection_delete, name='interfaceconnection_delete'),

+ 71 - 1
netbox/dcim/views.py

@@ -8,7 +8,7 @@ from django.contrib import messages
 from django.contrib.auth.decorators import permission_required
 from django.contrib.auth.mixins import PermissionRequiredMixin
 from django.core.paginator import EmptyPage, PageNotAnInteger
-from django.db.models import Count
+from django.db.models import Count, Q
 from django.http import HttpResponseRedirect
 from django.shortcuts import get_object_or_404, redirect, render
 from django.urls import reverse
@@ -141,6 +141,44 @@ class ComponentDeleteView(ObjectDeleteView):
         return obj.device.get_absolute_url()
 
 
+class BulkDisconnectView(View):
+    """
+    An extendable view for disconnection console/power/interface components in bulk.
+    """
+    model = None
+    form = None
+    template_name = 'dcim/bulk_disconnect.html'
+
+    def disconnect_objects(self, objects):
+        raise NotImplementedError()
+
+    def post(self, request, pk):
+
+        device = get_object_or_404(Device, pk=pk)
+        selected_objects = []
+
+        if '_confirm' in request.POST:
+            form = self.form(request.POST)
+            if form.is_valid():
+                count = self.disconnect_objects(form.cleaned_data['pk'])
+                messages.success(request, "Disconnected {} {} on {}".format(
+                    count, self.model._meta.verbose_name_plural, device
+                ))
+                return redirect(device.get_absolute_url())
+
+        else:
+            form = self.form(initial={'pk': request.POST.getlist('pk')})
+            selected_objects = self.model.objects.filter(pk__in=form.initial['pk'])
+
+        return render(request, self.template_name, {
+            'form': form,
+            'device': device,
+            'obj_type_plural': self.model._meta.verbose_name_plural,
+            'selected_objects': selected_objects,
+            'return_url': device.get_absolute_url(),
+        })
+
+
 #
 # Regions
 #
@@ -1159,6 +1197,15 @@ class ConsoleServerPortDeleteView(PermissionRequiredMixin, ComponentDeleteView):
     model = ConsoleServerPort
 
 
+class ConsoleServerPortBulkDisconnectView(PermissionRequiredMixin, BulkDisconnectView):
+    permission_required = 'dcim.change_consoleserverport'
+    model = ConsoleServerPort
+    form = forms.ConsoleServerPortBulkDisconnectForm
+
+    def disconnect_objects(self, cs_ports):
+        return ConsolePort.objects.filter(cs_port__in=cs_ports).update(cs_port=None, connection_status=None)
+
+
 class ConsoleServerPortBulkDeleteView(PermissionRequiredMixin, BulkDeleteView):
     permission_required = 'dcim.delete_consoleserverport'
     cls = ConsoleServerPort
@@ -1381,6 +1428,17 @@ class PowerOutletDeleteView(PermissionRequiredMixin, ComponentDeleteView):
     model = PowerOutlet
 
 
+class PowerOutletBulkDisconnectView(PermissionRequiredMixin, BulkDisconnectView):
+    permission_required = 'dcim.change_poweroutlet'
+    model = PowerOutlet
+    form = forms.PowerOutletBulkDisconnectForm
+
+    def disconnect_objects(self, power_outlets):
+        return PowerPort.objects.filter(power_outlet__in=power_outlets).update(
+            power_outlet=None, connection_status=None
+        )
+
+
 class PowerOutletBulkDeleteView(PermissionRequiredMixin, BulkDeleteView):
     permission_required = 'dcim.delete_poweroutlet'
     cls = PowerOutlet
@@ -1411,6 +1469,18 @@ class InterfaceDeleteView(PermissionRequiredMixin, ComponentDeleteView):
     model = Interface
 
 
+class InterfaceBulkDisconnectView(PermissionRequiredMixin, BulkDisconnectView):
+    permission_required = 'dcim.change_interface'
+    model = Interface
+    form = forms.InterfaceBulkDisconnectForm
+
+    def disconnect_objects(self, interfaces):
+        count, _ = InterfaceConnection.objects.filter(
+            Q(interface_a__in=interfaces) | Q(interface_b__in=interfaces)
+        ).delete()
+        return count
+
+
 class InterfaceBulkEditView(PermissionRequiredMixin, BulkEditView):
     permission_required = 'dcim.change_interface'
     cls = Interface

+ 2 - 1
netbox/extras/models.py

@@ -371,7 +371,8 @@ class TopologyMap(models.Model):
         # Add all circuits to the graph
         for termination in CircuitTermination.objects.filter(term_side='A', interface__device__in=devices):
             peer_termination = termination.get_peer_termination()
-            if peer_termination is not None and peer_termination.interface.device in devices:
+            if (peer_termination is not None and peer_termination.interface is not None and
+                    peer_termination.interface.device in devices):
                 graph.edge(termination.interface.device.name, peer_termination.interface.device.name, color='blue')
 
         return graph.pipe(format=img_format)

+ 8 - 1
netbox/ipam/forms.py

@@ -1,6 +1,7 @@
 from __future__ import unicode_literals
 
 from django import forms
+from django.core.exceptions import MultipleObjectsReturned
 from django.db.models import Count
 
 from dcim.models import Site, Rack, Device, Interface
@@ -301,6 +302,10 @@ class PrefixCSVForm(forms.ModelForm):
                     ))
                 else:
                     raise forms.ValidationError("Global VLAN {} not found in group {}".format(vlan_vid, vlan_group))
+            except MultipleObjectsReturned:
+                raise forms.ValidationError(
+                    "Multiple VLANs with VID {} found in group {}".format(vlan_vid, vlan_group)
+                )
         elif vlan_vid:
             try:
                 self.instance.vlan = VLAN.objects.get(site=site, group__isnull=True, vid=vlan_vid)
@@ -309,6 +314,8 @@ class PrefixCSVForm(forms.ModelForm):
                     raise forms.ValidationError("VLAN {} not found in site {}".format(vlan_vid, site))
                 else:
                     raise forms.ValidationError("Global VLAN {} not found".format(vlan_vid))
+            except MultipleObjectsReturned:
+                raise forms.ValidationError("Multiple VLANs with VID {} found".format(vlan_vid))
 
 
 class PrefixBulkEditForm(BootstrapMixin, CustomFieldBulkEditForm):
@@ -582,7 +589,7 @@ class IPAddressCSVForm(forms.ModelForm):
         }
     )
     status = CSVChoiceField(
-        choices=PREFIX_STATUS_CHOICES,
+        choices=IPADDRESS_STATUS_CHOICES,
         help_text='Operational status'
     )
     role = CSVChoiceField(

+ 11 - 2
netbox/project-static/js/livesearch.js

@@ -1,6 +1,7 @@
 $(document).ready(function() {
     var search_field = $('#id_livesearch');
     var real_field = $('#id_' + search_field.attr('data-field'));
+    var select_fields = $('#select select');
     var search_key = search_field.attr('data-key');
     var label = search_field.attr('data-label');
     if (!label) {
@@ -40,14 +41,22 @@ $(document).ready(function() {
         select: function(event, ui) {
             event.preventDefault();
             search_field.val(ui.item.label);
+            select_fields.val('');
+            select_fields.attr('disabled', 'disabled');
             real_field.empty();
             real_field.append($("<option></option>").attr('value', ui.item.value).text(ui.item.label));
             real_field.change();
             // Disable parent selection fields
-            $('select[filter-for="' + real_field.attr('name') + '"]').val('');
-            $('#select select').attr('disabled', 'disabled');
+            // $('select[filter-for="' + real_field.attr('name') + '"]').val('');
         },
         minLength: 4,
         delay: 500
     });
+
+    search_field.change(function() {
+        if (!search_field.val()) {
+            select_fields.removeAttr('disabled');
+            select_fields.val('');
+        }
+    });
 });

+ 13 - 0
netbox/templates/dcim/bulk_disconnect.html

@@ -0,0 +1,13 @@
+{% extends 'utilities/confirmation_form.html' %}
+{% load helpers %}
+
+{% block title %}Disconnect {{ obj_type_plural|bettertitle }}{% endblock %}
+
+{% block message %}
+    <p>Are you sure you want to disconnect all {{ selected_objects|length }} of these {{ obj_type_plural }} on <strong>{{ device }}</strong>?</p>
+    <ul>
+        {% for obj in selected_objects %}
+            <li>{{ obj }}</li>
+        {% endfor %}
+    </ul>
+{% endblock %}

+ 19 - 4
netbox/templates/dcim/device.html

@@ -424,12 +424,17 @@
                     <div class="panel-footer">
                         {% if interfaces and perms.dcim.change_interface %}
                             <button type="submit" name="_edit" formaction="{% url 'dcim:interface_bulk_edit' pk=device.pk %}" class="btn btn-warning btn-xs">
-                                <span class="glyphicon glyphicon-pencil" aria-hidden="true"></span> Edit selected
+                                <span class="glyphicon glyphicon-pencil" aria-hidden="true"></span> Edit
+                            </button>
+                        {% endif %}
+                        {% if interfaces and perms.dcim.delete_interfaceconnection %}
+                            <button type="submit" name="_disconnect" formaction="{% url 'dcim:interface_bulk_disconnect' pk=device.pk %}" class="btn btn-danger btn-xs">
+                                <span class="glyphicon glyphicon-resize-full" aria-hidden="true"></span> Disconnect
                             </button>
                         {% endif %}
                         {% if interfaces and perms.dcim.delete_interface %}
                             <button type="submit" name="_delete" formaction="{% url 'dcim:interface_bulk_delete' pk=device.pk %}" class="btn btn-danger btn-xs">
-                                <span class="glyphicon glyphicon-trash" aria-hidden="true"></span> Delete selected
+                                <span class="glyphicon glyphicon-trash" aria-hidden="true"></span> Delete
                             </button>
                         {% endif %}
                         {% if perms.dcim.add_interface %}
@@ -479,9 +484,14 @@
                 </table>
                 {% if perms.dcim.add_consoleserverport or perms.dcim.delete_consoleserverport %}
                     <div class="panel-footer">
+                        {% if cs_ports and perms.dcim.change_consoleport %}
+                            <button type="submit" name="_disconnect" formaction="{% url 'dcim:consoleserverport_bulk_disconnect' pk=device.pk %}" class="btn btn-danger btn-xs">
+                                <span class="glyphicon glyphicon-resize-full" aria-hidden="true"></span> Disconnect
+                            </button>
+                        {% endif %}
                         {% if cs_ports and perms.dcim.delete_consoleserverport %}
                             <button type="submit" class="btn btn-danger btn-xs">
-                                <span class="glyphicon glyphicon-trash" aria-hidden="true"></span> Delete selected
+                                <span class="glyphicon glyphicon-trash" aria-hidden="true"></span> Delete
                             </button>
                         {% endif %}
                         {% if perms.dcim.add_consoleserverport %}
@@ -531,9 +541,14 @@
                 </table>
                 {% if perms.dcim.add_poweroutlet or perms.dcim.delete_poweroutlet %}
                     <div class="panel-footer">
+                        {% if power_outlets and perms.dcim.change_powerport %}
+                            <button type="submit" name="_disconnect" formaction="{% url 'dcim:poweroutlet_bulk_disconnect' pk=device.pk %}" class="btn btn-danger btn-xs">
+                                <span class="glyphicon glyphicon-resize-full" aria-hidden="true"></span> Disconnect
+                            </button>
+                        {% endif %}
                         {% if power_outlets and perms.dcim.delete_poweroutlet %}
                             <button type="submit" class="btn btn-danger btn-xs">
-                                <span class="glyphicon glyphicon-trash" aria-hidden="true"></span> Delete selected
+                                <span class="glyphicon glyphicon-trash" aria-hidden="true"></span> Delete
                             </button>
                         {% endif %}
                         {% if perms.dcim.add_poweroutlet %}