diff --git a/netbox/dcim/forms.py b/netbox/dcim/forms.py index 390911f7f..e05ffec50 100644 --- a/netbox/dcim/forms.py +++ b/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 # diff --git a/netbox/dcim/urls.py b/netbox/dcim/urls.py index 0935d8b96..53031ebbe 100644 --- a/netbox/dcim/urls.py +++ b/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\d+)/console-server-ports/add/$', views.ConsoleServerPortCreateView.as_view(), name='consoleserverport_add'), + url(r'^devices/(?P\d+)/console-server-ports/disconnect/$', views.ConsoleServerPortBulkDisconnectView.as_view(), name='consoleserverport_bulk_disconnect'), url(r'^devices/(?P\d+)/console-server-ports/delete/$', views.ConsoleServerPortBulkDeleteView.as_view(), name='consoleserverport_bulk_delete'), url(r'^console-server-ports/(?P\d+)/connect/$', views.consoleserverport_connect, name='consoleserverport_connect'), url(r'^console-server-ports/(?P\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\d+)/power-outlets/add/$', views.PowerOutletCreateView.as_view(), name='poweroutlet_add'), + url(r'^devices/(?P\d+)/power-outlets/disconnect/$', views.PowerOutletBulkDisconnectView.as_view(), name='poweroutlet_bulk_disconnect'), url(r'^devices/(?P\d+)/power-outlets/delete/$', views.PowerOutletBulkDeleteView.as_view(), name='poweroutlet_bulk_delete'), url(r'^power-outlets/(?P\d+)/connect/$', views.poweroutlet_connect, name='poweroutlet_connect'), url(r'^power-outlets/(?P\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\d+)/interfaces/add/$', views.InterfaceCreateView.as_view(), name='interface_add'), url(r'^devices/(?P\d+)/interfaces/edit/$', views.InterfaceBulkEditView.as_view(), name='interface_bulk_edit'), + url(r'^devices/(?P\d+)/interfaces/disconnect/$', views.InterfaceBulkDisconnectView.as_view(), name='interface_bulk_disconnect'), url(r'^devices/(?P\d+)/interfaces/delete/$', views.InterfaceBulkDeleteView.as_view(), name='interface_bulk_delete'), url(r'^devices/(?P\d+)/interface-connections/add/$', views.interfaceconnection_add, name='interfaceconnection_add'), url(r'^interface-connections/(?P\d+)/delete/$', views.interfaceconnection_delete, name='interfaceconnection_delete'), diff --git a/netbox/dcim/views.py b/netbox/dcim/views.py index 9dacfb421..e6b77cb59 100644 --- a/netbox/dcim/views.py +++ b/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 diff --git a/netbox/templates/dcim/bulk_disconnect.html b/netbox/templates/dcim/bulk_disconnect.html new file mode 100644 index 000000000..82cc86a7a --- /dev/null +++ b/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 %} +

Are you sure you want to disconnect all {{ selected_objects|length }} of these {{ obj_type_plural }} on {{ device }}?

+
    + {% for obj in selected_objects %} +
  • {{ obj }}
  • + {% endfor %} +
+{% endblock %} diff --git a/netbox/templates/dcim/device.html b/netbox/templates/dcim/device.html index bafcdd224..a6e5d1dbe 100644 --- a/netbox/templates/dcim/device.html +++ b/netbox/templates/dcim/device.html @@ -424,12 +424,17 @@