diff --git a/netbox/dcim/forms.py b/netbox/dcim/forms.py index 0a62b460e..794c310c4 100644 --- a/netbox/dcim/forms.py +++ b/netbox/dcim/forms.py @@ -1667,7 +1667,9 @@ class InterfaceForm(BootstrapMixin, forms.ModelForm): 'mode': '802.1Q Mode', } help_texts = { - 'mode': "Nullifying the mode will clear any associated VLANs." + 'mode': "Access: One untagged VLAN
" + "Tagged: One untagged VLAN and/or one or more tagged VLANs
" + "Tagged All: Implies all VLANs are available (w/optional untagged VLAN)" } def __init__(self, *args, **kwargs): @@ -1693,13 +1695,20 @@ class InterfaceForm(BootstrapMixin, forms.ModelForm): untagged_vlan = self.cleaned_data['untagged_vlan'] tagged_vlans = self.cleaned_data['tagged_vlans'] + # A VLAN cannot be both tagged and untagged + if untagged_vlan and untagged_vlan in tagged_vlans: + raise forms.ValidationError("VLAN {} cannot be both tagged and untagged.".format(untagged_vlan)) + + # Untagged interfaces cannot be assigned tagged VLANs if self.cleaned_data['mode'] == IFACE_MODE_ACCESS and tagged_vlans: raise forms.ValidationError({ 'mode': "An access interface cannot have tagged VLANs assigned." }) - if untagged_vlan and untagged_vlan in tagged_vlans: - raise forms.ValidationError("VLAN {} cannot be both tagged and untagged.".format(untagged_vlan)) + # Remove all tagged VLAN assignments from "tagged all" interfaces + elif self.cleaned_data['mode'] == IFACE_MODE_TAGGED_ALL: + self.cleaned_data['tagged_vlans'] = [] + class InterfaceAssignVLANsForm(BootstrapMixin, forms.ModelForm): vlans = forms.MultipleChoiceField( @@ -1720,31 +1729,51 @@ class InterfaceAssignVLANsForm(BootstrapMixin, forms.ModelForm): super(InterfaceAssignVLANsForm, self).__init__(*args, **kwargs) + if self.instance.mode == IFACE_MODE_ACCESS: + self.initial['tagged'] = False + + # Find all VLANs already assigned to the interface for exclusion from the list + assigned_vlans = [v.pk for v in self.instance.tagged_vlans.all()] + if self.instance.untagged_vlan is not None: + assigned_vlans.append(self.instance.untagged_vlan.pk) + # Initialize VLAN choices device = self.instance.device vlan_choices = [ - ('Global', [(vlan.pk, vlan) for vlan in VLAN.objects.filter(site=None)]), - (device.site.name, [(vlan.pk, vlan) for vlan in VLAN.objects.filter(site=device.site, group=None)]), + ('Global', [(vlan.pk, vlan) for vlan in VLAN.objects.filter(site=None).exclude(pk__in=assigned_vlans)]), + (device.site.name, [(vlan.pk, vlan) for vlan in VLAN.objects.filter(site=device.site, group=None).exclude(pk__in=assigned_vlans)]), ] for group in VLANGroup.objects.filter(site=device.site): vlan_choices.append(( '{} / {}'.format(group.site.name, group.name), - [(vlan.pk, vlan) for vlan in VLAN.objects.filter(group=group)] + [(vlan.pk, vlan) for vlan in VLAN.objects.filter(group=group).exclude(pk__in=assigned_vlans)] )) self.fields['vlans'].choices = vlan_choices + def clean(self): + + super(InterfaceAssignVLANsForm, self).clean() + + # Only untagged VLANs permitted on an access interface + if self.instance.mode == IFACE_MODE_ACCESS and len(self.cleaned_data['vlans']) > 1: + raise forms.ValidationError("Only one VLAN may be assigned to an access interface.") + + # 'tagged' is required if more than one VLAN is selected + if not self.cleaned_data['tagged'] and len(self.cleaned_data['vlans']) > 1: + raise forms.ValidationError("Only one untagged VLAN may be selected.") + def save(self, *args, **kwargs): if self.cleaned_data['tagged']: for vlan in self.cleaned_data['vlans']: self.instance.tagged_vlans.add(vlan) else: - self.instance.untagged_vlan = self.cleaned_data['vlans'][0] + self.instance.untagged_vlan_id = self.cleaned_data['vlans'][0] return super(InterfaceAssignVLANsForm, self).save(*args, **kwargs) -class InterfaceCreateForm(ComponentForm, forms.ModelForm): +class InterfaceCreateForm(ComponentForm, forms.Form): name_pattern = ExpandableNameField(label='Name') form_factor = forms.ChoiceField(choices=IFACE_FF_CHOICES) enabled = forms.BooleanField(required=False) diff --git a/netbox/dcim/models.py b/netbox/dcim/models.py index 3d8e41068..cb5a40c88 100644 --- a/netbox/dcim/models.py +++ b/netbox/dcim/models.py @@ -1457,11 +1457,13 @@ class Interface(models.Model): def save(self, *args, **kwargs): + # Remove untagged VLAN assignment for non-802.1Q interfaces if self.mode is None: self.untagged_vlan = None - self.tagged_vlans = [] - elif self.mode is IFACE_MODE_ACCESS: - self.tagged_vlans = [] + + # Only "tagged" interfaces may have tagged VLANs assigned. ("tagged all" implies all VLANs are assigned.) + if self.mode is not IFACE_MODE_TAGGED: + self.tagged_vlans.clear() return super(Interface, self).save(*args, **kwargs) diff --git a/netbox/templates/dcim/interface_edit.html b/netbox/templates/dcim/interface_edit.html index 071276e38..cef4a772b 100644 --- a/netbox/templates/dcim/interface_edit.html +++ b/netbox/templates/dcim/interface_edit.html @@ -16,55 +16,93 @@ {% render_field form.mode %} - {% with interface=form.instance %} - {% if interface.mode %} -
-
802.1Q VLANs
- + {% if obj.mode %} +
+
802.1Q VLANs
+
+ + + + + + + {% if obj.untagged_vlan %} - - - - + + + + - {% if interface.untagged_vlan %} - - - - - - - {% endif %} - {% for vlan in interface.tagged_vlans.all %} - - - - - - - {% endfor %} - {% if not interface.untagged_vlan and not interface.tagged_vlans.exists %} - - - - {% endif %} -
VIDNameUntaggedTagged
VIDNameUntaggedTagged + {{ obj.untagged_vlan.vid }} + {{ obj.untagged_vlan.name }} + + + +
{{ interface.untagged_vlan.vid }}{{ interface.untagged_vlan.name }} - - - -
{{ vlan.vid }}{{ vlan.name }} - - - -
- No VLANs assigned -
- + {% endif %} + {% for vlan in obj.tagged_vlans.all %} + + + {{ vlan.vid }} + + {{ vlan.name }} + + + + + + + + {% endfor %} + {% if not obj.untagged_vlan and not obj.tagged_vlans.exists %} + + + No VLANs assigned + + + {% else %} + + + + Clear + + + Clear All + + + {% endif %} + + - {% endif %} - {% endwith %} +
+ {% endif %} +{% endblock %} + +{% block buttons %} + {% if obj.pk %} + + + {% else %} + + + {% endif %} + Cancel +{% endblock %} + +{% block javascript %} + {% endblock %} diff --git a/netbox/templates/utilities/obj_edit.html b/netbox/templates/utilities/obj_edit.html index 2b24208fd..16acc32ed 100644 --- a/netbox/templates/utilities/obj_edit.html +++ b/netbox/templates/utilities/obj_edit.html @@ -31,13 +31,15 @@
- {% if obj.pk %} - - {% else %} - - - {% endif %} - Cancel + {% block buttons %} + {% if obj.pk %} + + {% else %} + + + {% endif %} + Cancel + {% endblock %}