diff --git a/netbox/dcim/models.py b/netbox/dcim/models.py index a09f720b4..33fa173cb 100644 --- a/netbox/dcim/models.py +++ b/netbox/dcim/models.py @@ -1011,6 +1011,14 @@ class Device(CreatedUpdatedModel, CustomFieldModel): raise ValidationError({ 'vc_position': "A device assigned to a virtual chassis must have its position defined." }) + try: + virtual_chassis = VirtualChassis.objects.filter(master=self.pk) + if self.virtual_chassis != virtual_chassis: + raise ValidationError( + "This device has been designated the master of a virtual chassis but is not assigned to it." + ) + except VirtualChassis.DoesNotExist: + pass def save(self, *args, **kwargs): @@ -1627,3 +1635,11 @@ class VirtualChassis(models.Model): def get_absolute_url(self): return self.master.get_absolute_url() + + def clean(self): + + # Validate master assignment + if self.master not in self.members.all(): + raise ValidationError({ + 'master': "The selected master is not assigned to this virtual chassis." + }) diff --git a/netbox/dcim/urls.py b/netbox/dcim/urls.py index 7c65f01b6..e7e1e41df 100644 --- a/netbox/dcim/urls.py +++ b/netbox/dcim/urls.py @@ -221,5 +221,6 @@ urlpatterns = [ url(r'^virtual-chassis/(?P\d+)/edit/$', views.VirtualChassisEditView.as_view(), name='virtualchassis_edit'), url(r'^virtual-chassis/(?P\d+)/delete/$', views.VirtualChassisDeleteView.as_view(), name='virtualchassis_delete'), url(r'^virtual-chassis/(?P\d+)/add-member/$', views.VirtualChassisAddMemberView.as_view(), name='virtualchassis_add_member'), + url(r'^virtual-chassis-members/(?P\d+)/delete/$', views.VirtualChassisRemoveMemberView.as_view(), name='virtualchassis_remove_member'), ] diff --git a/netbox/dcim/views.py b/netbox/dcim/views.py index 596449f85..8e1f61771 100644 --- a/netbox/dcim/views.py +++ b/netbox/dcim/views.py @@ -2172,7 +2172,7 @@ class VirtualChassisDeleteView(PermissionRequiredMixin, ObjectDeleteView): class VirtualChassisAddMemberView(PermissionRequiredMixin, GetReturnURLMixin, View): - permission_required = 'dcim.change_device' + permission_required = 'dcim.change_virtualchassis' def get(self, request, pk): @@ -2224,3 +2224,50 @@ class VirtualChassisAddMemberView(PermissionRequiredMixin, GetReturnURLMixin, Vi 'membership_form': membership_form, 'return_url': self.get_return_url(request, virtual_chassis), }) + + +class VirtualChassisRemoveMemberView(PermissionRequiredMixin, GetReturnURLMixin, View): + permission_required = 'dcim.change_virtualchassis' + + def get(self, request, pk): + + device = get_object_or_404(Device, pk=pk, virtual_chassis__isnull=False) + form = ConfirmationForm(initial=request.GET) + + return render(request, 'dcim/virtualchassis_remove_member.html', { + 'device': device, + 'form': form, + 'return_url': self.get_return_url(request, device), + }) + + def post(self, request, pk): + + device = get_object_or_404(Device, pk=pk, virtual_chassis__isnull=False) + form = ConfirmationForm(request.POST) + + # Protect master device from being removed + virtual_chassis = VirtualChassis.objects.filter(master=device).first() + if virtual_chassis is not None: + msg = 'Unable to remove master device {} from the virtual chassis.'.format(escape(device)) + messages.error(request, mark_safe(msg)) + return redirect(device.get_absolute_url()) + + if form.is_valid(): + + Device.objects.filter(pk=device.pk).update( + virtual_chassis=None, + vc_position=None, + vc_priority=None + ) + + msg = 'Removed {} from virtual chassis {}'.format(device, device.virtual_chassis) + messages.success(request, msg) + UserAction.objects.log_edit(request.user, device, msg) + + return redirect(self.get_return_url(request, device)) + + return render(request, 'dcim/virtualchassis_remove_member.html', { + 'device': device, + 'form': form, + 'return_url': self.get_return_url(request, device), + }) diff --git a/netbox/templates/dcim/virtualchassis_edit.html b/netbox/templates/dcim/virtualchassis_edit.html index 69051ac7a..1e68c39f6 100644 --- a/netbox/templates/dcim/virtualchassis_edit.html +++ b/netbox/templates/dcim/virtualchassis_edit.html @@ -31,6 +31,7 @@ Device Position Priority + @@ -38,11 +39,22 @@ {% for field in form.hidden_fields %} {{ field }} {% endfor %} - - {{ form.instance.name }} - {{ form.vc_position }} - {{ form.vc_priority }} - + {% with device=form.instance virtual_chassis=vc_form.instance %} + + + {{ device }} + + {{ form.vc_position }} + {{ form.vc_priority }} + + {% if virtual_chassis.pk %} + + + + {% endif %} + + + {% endwith %} {% endfor %} @@ -51,7 +63,11 @@
- + {% if vc_form.instance.pk %} + + {% else %} + + {% endif %} Cancel
diff --git a/netbox/templates/dcim/virtualchassis_remove_member.html b/netbox/templates/dcim/virtualchassis_remove_member.html new file mode 100644 index 000000000..0da7c1d1b --- /dev/null +++ b/netbox/templates/dcim/virtualchassis_remove_member.html @@ -0,0 +1,8 @@ +{% extends 'utilities/confirmation_form.html' %} +{% load form_helpers %} + +{% block title %}Remove Virtual Chassis Member?{% endblock %} + +{% block message %} +

Are you sure you want to remove {{ device }} from virtual chassis {{ device.virtual_chassis }}?

+{% endblock %}