diff --git a/netbox/dcim/apps.py b/netbox/dcim/apps.py index fb1f4ee39..ef3158508 100644 --- a/netbox/dcim/apps.py +++ b/netbox/dcim/apps.py @@ -6,3 +6,6 @@ from django.apps import AppConfig class DCIMConfig(AppConfig): name = "dcim" verbose_name = "DCIM" + + def ready(self): + import dcim.signals diff --git a/netbox/dcim/forms.py b/netbox/dcim/forms.py index 456ca0bc5..dacd29293 100644 --- a/netbox/dcim/forms.py +++ b/netbox/dcim/forms.py @@ -2276,38 +2276,54 @@ class VirtualChassisForm(BootstrapMixin, forms.ModelForm): fields = ['master', 'domain'] -# class VCAddMemberForm(BootstrapMixin, ChainedFieldsMixin, forms.Form): -# site = forms.ModelChoiceField( -# queryset=Site.objects.all(), -# label='Site', -# required=False, -# widget=forms.Select( -# attrs={'filter-for': 'rack'} -# ) -# ) -# rack = ChainedModelChoiceField( -# queryset=Rack.objects.all(), -# chains=( -# ('site', 'site'), -# ), -# label='Rack', -# required=False, -# widget=APISelect( -# api_url='/api/dcim/racks/?site_id={{site}}', -# attrs={'filter-for': 'device', 'nullable': 'true'} -# ) -# ) -# device = ChainedModelChoiceField( -# queryset=Device.objects.all(), -# chains=( -# ('site', 'site'), -# ('rack', 'rack'), -# ), -# label='Device', -# widget=APISelect( -# api_url='/api/dcim/devices/?site_id={{site}}&rack_id={{rack}}', -# display_field='display_name' -# ) -# ) -# vc_position = forms.IntegerField(label='Position') -# vc_priority = forms.IntegerField(required=False, label='Priority') +class VCMemberSelectForm(BootstrapMixin, ChainedFieldsMixin, forms.Form): + site = forms.ModelChoiceField( + queryset=Site.objects.all(), + label='Site', + required=False, + widget=forms.Select( + attrs={'filter-for': 'rack'} + ) + ) + rack = ChainedModelChoiceField( + queryset=Rack.objects.all(), + chains=( + ('site', 'site'), + ), + label='Rack', + required=False, + widget=APISelect( + api_url='/api/dcim/racks/?site_id={{site}}', + attrs={'filter-for': 'device', 'nullable': 'true'} + ) + ) + device = ChainedModelChoiceField( + queryset=Device.objects.all(), + chains=( + ('site', 'site'), + ('rack', 'rack'), + ), + label='Device', + widget=APISelect( + api_url='/api/dcim/devices/?site_id={{site}}&rack_id={{rack}}', + display_field='display_name' + ) + ) + + +class DeviceVCMembershipForm(forms.ModelForm): + + class Meta: + model = Device + fields = ['vc_position', 'vc_priority'] + labels = { + 'vc_position': 'Position', + 'vc_priority': 'Priority', + } + + def clean_vc_position(self): + vc_position = self.cleaned_data['vc_position'] + if Device.objects.filter(virtual_chassis=self.instance.virtual_chassis, vc_position=vc_position).exists(): + raise forms.ValidationError("A virtual chassis member already exists in this position.") + + return vc_position \ No newline at end of file diff --git a/netbox/dcim/models.py b/netbox/dcim/models.py index 6f585e4cf..a09f720b4 100644 --- a/netbox/dcim/models.py +++ b/netbox/dcim/models.py @@ -1006,6 +1006,12 @@ class Device(CreatedUpdatedModel, CustomFieldModel): 'cluster': "The assigned cluster belongs to a different site ({})".format(self.cluster.site) }) + # Validate virtual chassis assignment + if self.virtual_chassis and not self.vc_position: + raise ValidationError({ + 'vc_position': "A device assigned to a virtual chassis must have its position defined." + }) + def save(self, *args, **kwargs): is_new = not bool(self.pk) diff --git a/netbox/dcim/signals.py b/netbox/dcim/signals.py new file mode 100644 index 000000000..a19563ac7 --- /dev/null +++ b/netbox/dcim/signals.py @@ -0,0 +1,14 @@ +from __future__ import unicode_literals + +from django.db.models.signals import pre_delete +from django.dispatch import receiver + +from .models import Device, VirtualChassis + + +@receiver(pre_delete, sender=VirtualChassis) +def clear_virtualchassis_members(instance, **kwargs): + """ + When a VirtualChassis is deleted, nullify the vc_position and vc_priority fields of its prior members. + """ + Device.objects.filter(virtual_chassis=instance.pk).update(vc_position=None, vc_priority=None) diff --git a/netbox/dcim/urls.py b/netbox/dcim/urls.py index 962217d69..7c65f01b6 100644 --- a/netbox/dcim/urls.py +++ b/netbox/dcim/urls.py @@ -220,6 +220,6 @@ urlpatterns = [ url(r'^virtual-chassis/add/$', views.VirtualChassisCreateView.as_view(), name='virtualchassis_add'), 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/(?P\d+)/add-member/$', views.VirtualChassisAddMemberView.as_view(), name='virtualchassis_add_member'), ] diff --git a/netbox/dcim/views.py b/netbox/dcim/views.py index 17b090b35..596449f85 100644 --- a/netbox/dcim/views.py +++ b/netbox/dcim/views.py @@ -2158,7 +2158,7 @@ class VirtualChassisEditView(PermissionRequiredMixin, GetReturnURLMixin, View): return redirect(vc_form.cleaned_data['master'].get_absolute_url()) - return render(request, 'dcim/virtualchassis_add.html', { + return render(request, 'dcim/virtualchassis_edit.html', { 'vc_form': vc_form, 'formset': formset, 'return_url': self.get_return_url(request, virtual_chassis), @@ -2169,3 +2169,58 @@ class VirtualChassisDeleteView(PermissionRequiredMixin, ObjectDeleteView): permission_required = 'dcim.delete_virtualchassis' model = VirtualChassis default_return_url = 'dcim:device_list' + + +class VirtualChassisAddMemberView(PermissionRequiredMixin, GetReturnURLMixin, View): + permission_required = 'dcim.change_device' + + def get(self, request, pk): + + virtual_chassis = get_object_or_404(VirtualChassis, pk=pk) + + initial_data = {k: request.GET[k] for k in request.GET} + member_select_form = forms.VCMemberSelectForm(initial=initial_data) + membership_form = forms.DeviceVCMembershipForm(initial=initial_data) + + return render(request, 'dcim/virtualchassis_add_member.html', { + 'virtual_chassis': virtual_chassis, + 'member_select_form': member_select_form, + 'membership_form': membership_form, + 'return_url': self.get_return_url(request, virtual_chassis), + }) + + def post(self, request, pk): + + virtual_chassis = get_object_or_404(VirtualChassis, pk=pk) + + member_select_form = forms.VCMemberSelectForm(request.POST) + + if member_select_form.is_valid(): + + device = member_select_form.cleaned_data['device'] + device.virtual_chassis = virtual_chassis + data = {k: request.POST[k] for k in ['vc_position', 'vc_priority']} + membership_form = forms.DeviceVCMembershipForm(data, instance=device) + + if membership_form.is_valid(): + + membership_form.save() + msg = 'Added member {}'.format(device.get_absolute_url(), escape(device)) + messages.success(request, mark_safe(msg)) + UserAction.objects.log_edit(request.user, device, msg) + + if '_addanother' in request.POST: + return redirect(request.get_full_path()) + + return redirect(self.get_return_url(request, device)) + + else: + + membership_form = forms.DeviceVCMembershipForm(request.POST) + + return render(request, 'dcim/virtualchassis_add_member.html', { + 'virtual_chassis': virtual_chassis, + 'member_select_form': member_select_form, + 'membership_form': membership_form, + 'return_url': self.get_return_url(request, virtual_chassis), + }) diff --git a/netbox/templates/dcim/virtualchassis_add_member.html b/netbox/templates/dcim/virtualchassis_add_member.html new file mode 100644 index 000000000..cef1a2a2e --- /dev/null +++ b/netbox/templates/dcim/virtualchassis_add_member.html @@ -0,0 +1,35 @@ +{% extends '_base.html' %} +{% load form_helpers %} + +{% block content %} +
+ {% csrf_token %} +
+
+

{% block title %}Add New Member to Virtual Chassis {{ virtual_chassis }}{% endblock %}

+ {% if membership_form.non_field_errors %} +
+
Errors
+
+ {{ membership_form.non_field_errors }} +
+
+ {% endif %} +
+
Add New Member
+
+ {% render_form member_select_form %} + {% render_form membership_form %} +
+
+
+
+
+
+ + + Cancel +
+
+
+{% endblock %} diff --git a/netbox/templates/dcim/virtualchassis_edit.html b/netbox/templates/dcim/virtualchassis_edit.html index f564c8060..69051ac7a 100644 --- a/netbox/templates/dcim/virtualchassis_edit.html +++ b/netbox/templates/dcim/virtualchassis_edit.html @@ -35,8 +35,8 @@ {% for form in formset %} - {% for hidden in form.hidden_fields %} - {{ hidden }} + {% for field in form.hidden_fields %} + {{ field }} {% endfor %} {{ form.instance.name }}