Browse Source

Closes #1944: Enable assigning VLANs to virtual machine interfaces

Jeremy Stretch 7 years ago
parent
commit
8bd268d81c

+ 43 - 20
netbox/dcim/forms.py

@@ -14,11 +14,10 @@ from ipam.models import IPAddress, VLAN, VLANGroup
 from tenancy.forms import TenancyForm
 from tenancy.models import Tenant
 from utilities.forms import (
-    AnnotatedMultipleChoiceField, APISelect, APISelectMultiple, add_blank_choice, ArrayFieldSelectMultiple,
-    BootstrapMixin, BulkEditForm, BulkEditNullBooleanSelect, ChainedFieldsMixin, ChainedModelChoiceField,
-    ChainedModelMultipleChoiceField, CommentField, ComponentForm, ConfirmationForm, CSVChoiceField, ExpandableNameField,
-    FilterChoiceField, FilterTreeNodeMultipleChoiceField, FlexibleModelChoiceField, Livesearch, SelectWithDisabled,
-    SelectWithPK, SmallTextarea, SlugField,
+    AnnotatedMultipleChoiceField, APISelect, add_blank_choice, ArrayFieldSelectMultiple, BootstrapMixin, BulkEditForm,
+    BulkEditNullBooleanSelect, ChainedFieldsMixin, ChainedModelChoiceField, CommentField, ComponentForm,
+    ConfirmationForm, CSVChoiceField, ExpandableNameField, FilterChoiceField, FilterTreeNodeMultipleChoiceField,
+    FlexibleModelChoiceField, Livesearch, SelectWithDisabled, SelectWithPK, SmallTextarea, SlugField,
 )
 from virtualization.models import Cluster
 from .constants import (
@@ -37,6 +36,12 @@ from .models import (
 
 DEVICE_BY_PK_RE = '{\d+\}'
 
+INTERFACE_MODE_HELP_TEXT = """
+Access: One untagged VLAN<br />
+Tagged: One untagged VLAN and/or one or more tagged VLANs<br />
+Tagged All: Implies all VLANs are available (w/optional untagged VLAN)
+"""
+
 
 def get_device_by_name_or_pk(name):
     """
@@ -1657,7 +1662,7 @@ class InterfaceForm(BootstrapMixin, forms.ModelForm):
     class Meta:
         model = Interface
         fields = [
-            'device', 'name', 'form_factor', 'enabled', 'lag', 'mtu', 'mac_address', 'mgmt_only', 'description',
+            'device', 'name', 'form_factor', 'enabled', 'lag', 'mac_address', 'mtu', 'mgmt_only', 'description',
             'mode', 'untagged_vlan', 'tagged_vlans',
         ]
         widgets = {
@@ -1667,9 +1672,7 @@ class InterfaceForm(BootstrapMixin, forms.ModelForm):
             'mode': '802.1Q Mode',
         }
         help_texts = {
-            'mode': "Access: One untagged VLAN<br />"
-                    "Tagged: One untagged VLAN and/or one or more tagged VLANs<br />"
-                    "Tagged All: Implies all VLANs are available (w/optional untagged VLAN)"
+            'mode': INTERFACE_MODE_HELP_TEXT,
         }
 
     def __init__(self, *args, **kwargs):
@@ -1732,17 +1735,37 @@ class InterfaceAssignVLANsForm(BootstrapMixin, forms.ModelForm):
         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).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).exclude(pk__in=assigned_vlans)]
-            ))
+        # Compile VLAN choices
+        vlan_choices = []
+
+        # Add global VLANs
+        global_vlans = VLAN.objects.filter(site=None, group=None).exclude(pk__in=assigned_vlans)
+        vlan_choices.append((
+            'Global', [(vlan.pk, vlan) for vlan in global_vlans])
+        )
+
+        # Add grouped global VLANs
+        for group in VLANGroup.objects.filter(site=None):
+            global_group_vlans = VLAN.objects.filter(group=group).exclude(pk__in=assigned_vlans)
+            vlan_choices.append(
+                (group.name, [(vlan.pk, vlan) for vlan in global_group_vlans])
+            )
+
+        parent = self.instance.parent
+        if parent is not None:
+
+            # Add site VLANs
+            site_vlans = VLAN.objects.filter(site=parent.site, group=None).exclude(pk__in=assigned_vlans)
+            vlan_choices.append((parent.site.name, [(vlan.pk, vlan) for vlan in site_vlans]))
+
+            # Add grouped site VLANs
+            for group in VLANGroup.objects.filter(site=parent.site):
+                site_group_vlans = VLAN.objects.filter(group=group).exclude(pk__in=assigned_vlans)
+                vlan_choices.append((
+                    '{} / {}'.format(group.site.name, group.name),
+                    [(vlan.pk, vlan) for vlan in site_group_vlans]
+                ))
+
         self.fields['vlans'].choices = vlan_choices
 
     def clean(self):

+ 55 - 0
netbox/templates/dcim/inc/interface_vlans_table.html

@@ -0,0 +1,55 @@
+<table class="table panel-body">
+    <tr>
+        <th>VID</th>
+        <th>Name</th>
+        <th>Untagged</th>
+        <th>Tagged</th>
+    </tr>
+    {% with tagged_vlans=obj.tagged_vlans.all %}
+        {% if obj.untagged_vlan and obj.untagged_vlan not in tagged_vlans %}
+            <tr>
+                <td>
+                    <a href="{{ obj.untagged_vlan.get_absolute_url }}">{{ obj.untagged_vlan.vid }}</a>
+                </td>
+                <td>{{ obj.untagged_vlan.name }}</td>
+                <td>
+                    <input type="radio" name="untagged_vlan" value="{{ obj.untagged_vlan.pk }}" checked="checked" />
+                </td>
+                <td>
+                    <input type="checkbox" name="tagged_vlans" value="{{ obj.untagged_vlan.pk }}" />
+                </td>
+            </tr>
+        {% endif %}
+        {% for vlan in tagged_vlans %}
+            <tr>
+                <td>
+                    <a href="{{ vlan.get_absolute_url }}">{{ vlan.vid }}</a>
+                </td>
+                <td>{{ vlan.name }}</td>
+                <td>
+                    <input type="radio" name="untagged_vlan" value="{{ vlan.pk }}"{% if vlan == obj.untagged_vlan %} checked="checked"{% endif %} />
+                </td>
+                <td>
+                    <input type="checkbox" name="tagged_vlans" value="{{ vlan.pk }}" checked="checked" />
+                </td>
+            </tr>
+        {% endfor %}
+        {% if not obj.untagged_vlan and not tagged_vlans %}
+            <tr>
+                <td colspan="4" class="text-muted text-center">
+                    No VLANs assigned
+                </td>
+            </tr>
+        {% else %}
+            <tr>
+                <td colspan="2"></td>
+                <td>
+                    <a href="#" id="clear_untagged_vlan" class="btn btn-warning btn-xs">Clear</a>
+                </td>
+                <td>
+                    <a href="#" id="clear_tagged_vlans" class="btn btn-warning btn-xs">Clear All</a>
+                </td>
+            </tr>
+        {% endif %}
+    {% endwith %}
+</table>

+ 1 - 53
netbox/templates/dcim/interface_edit.html

@@ -19,59 +19,7 @@
     {% if obj.mode %}
         <div class="panel panel-default" id="vlans_panel">
             <div class="panel-heading"><strong>802.1Q VLANs</strong></div>
-            <table class="table panel-body">
-                <tr>
-                    <th>VID</th>
-                    <th>Name</th>
-                    <th>Untagged</th>
-                    <th>Tagged</th>
-                </tr>
-                {% if obj.untagged_vlan %}
-                    <tr>
-                        <td>
-                            <a href="{{ obj.untagged_vlan.get_absolute_url }}">{{ obj.untagged_vlan.vid }}</a>
-                        </td>
-                        <td>{{ obj.untagged_vlan.name }}</td>
-                        <td>
-                            <input type="radio" name="untagged_vlan" value="{{ obj.untagged_vlan.pk }}" checked="true" />
-                        </td>
-                        <td>
-                            <input type="checkbox" name="tagged_vlans" value="{{ obj.untagged_vlan.pk }}" />
-                        </td>
-                    </tr>
-                {% endif %}
-                {% for vlan in obj.tagged_vlans.all %}
-                    <tr>
-                        <td>
-                            <a href="{{ vlan.get_absolute_url }}">{{ vlan.vid }}</a>
-                        </td>
-                        <td>{{ vlan.name }}</td>
-                        <td>
-                            <input type="radio" name="untagged_vlan" value="{{ vlan.pk }}" />
-                        </td>
-                        <td>
-                            <input type="checkbox" name="tagged_vlans" value="{{ vlan.pk }}" checked="true" />
-                        </td>
-                    </tr>
-                {% endfor %}
-                {% if not obj.untagged_vlan and not obj.tagged_vlans.exists %}
-                    <tr>
-                        <td colspan="4" class="text-muted text-center">
-                            No VLANs assigned
-                        </td>
-                    </tr>
-                {% else %}
-                    <tr>
-                        <td colspan="2"></td>
-                        <td>
-                            <a href="#" id="clear_untagged_vlan" class="btn btn-warning btn-xs">Clear</a>
-                        </td>
-                        <td>
-                            <a href="#" id="clear_tagged_vlans" class="btn btn-warning btn-xs">Clear All</a>
-                        </td>
-                    </tr>
-                {% endif %}
-            </table>
+            {% include 'dcim/inc/interface_vlans_table.html' %}
             <div class="panel-footer text-right">
                 <a href="{% url 'dcim:interface_assign_vlans' pk=obj.pk %}?return_url={% url 'dcim:interface_edit' pk=obj.pk %}" class="btn btn-primary btn-xs{% if form.instance.mode == 100 and form.instance.untagged_vlan %} disabled{% endif %}">
                     <i class="glyphicon glyphicon-plus"></i> Add VLANs

+ 53 - 0
netbox/templates/virtualization/interface_edit.html

@@ -0,0 +1,53 @@
+{% extends 'utilities/obj_edit.html' %}
+{% load form_helpers %}
+
+{% block form %}
+    <div class="panel panel-default">
+        <div class="panel-heading"><strong>Interface</strong></div>
+        <div class="panel-body">
+            {% render_field form.name %}
+            {% render_field form.enabled %}
+            {% render_field form.mac_address %}
+            {% render_field form.mtu %}
+            {% render_field form.description %}
+            {% render_field form.mode %}
+        </div>
+    </div>
+    {% if obj.mode %}
+        <div class="panel panel-default" id="vlans_panel">
+            <div class="panel-heading"><strong>802.1Q VLANs</strong></div>
+            {% include 'dcim/inc/interface_vlans_table.html' %}
+            <div class="panel-footer text-right">
+                <a href="{% url 'dcim:interface_assign_vlans' pk=obj.pk %}?return_url={% url 'virtualization:interface_edit' pk=obj.pk %}" class="btn btn-primary btn-xs{% if form.instance.mode == 100 and form.instance.untagged_vlan %} disabled{% endif %}">
+                    <i class="glyphicon glyphicon-plus"></i> Add VLANs
+                </a>
+            </div>
+        </div>
+    {% endif %}
+{% endblock %}
+
+{% block buttons %}
+    {% if obj.pk %}
+        <button type="submit" name="_update" class="btn btn-primary">Update</button>
+        <button type="submit" formaction="?return_url={% url 'virtualization:interface_edit' pk=obj.pk %}" class="btn btn-primary">Update and Continue Editing</button>
+    {% else %}
+        <button type="submit" name="_create" class="btn btn-primary">Create</button>
+        <button type="submit" name="_addanother" class="btn btn-primary">Create and Add Another</button>
+    {% endif %}
+    <a href="{{ return_url }}" class="btn btn-default">Cancel</a>
+{% endblock %}
+
+{% block javascript %}
+    <script type="text/javascript">
+        $(document).ready(function() {
+            $('#clear_untagged_vlan').click(function () {
+                $('input[name="untagged_vlan"]').prop("checked", false);
+                return false;
+            });
+            $('#clear_tagged_vlans').click(function () {
+                $('input[name="tagged_vlans"]').prop("checked", false);
+                return false;
+            });
+        });
+    </script>
+{% endblock %}

+ 29 - 2
netbox/virtualization/forms.py

@@ -5,7 +5,8 @@ from django.core.exceptions import ValidationError
 from django.db.models import Count
 from mptt.forms import TreeNodeChoiceField
 
-from dcim.constants import IFACE_FF_VIRTUAL
+from dcim.constants import IFACE_FF_VIRTUAL, IFACE_MODE_ACCESS, IFACE_MODE_TAGGED_ALL
+from dcim.forms import INTERFACE_MODE_HELP_TEXT
 from dcim.formfields import MACAddressFormField
 from dcim.models import Device, DeviceRole, Interface, Platform, Rack, Region, Site
 from extras.forms import CustomFieldBulkEditForm, CustomFieldForm, CustomFieldFilterForm
@@ -414,11 +415,37 @@ class InterfaceForm(BootstrapMixin, forms.ModelForm):
 
     class Meta:
         model = Interface
-        fields = ['virtual_machine', 'name', 'form_factor', 'enabled', 'mac_address', 'mtu', 'description']
+        fields = [
+            'virtual_machine', 'name', 'form_factor', 'enabled', 'mac_address', 'mtu', 'description', 'mode',
+            'untagged_vlan', 'tagged_vlans',
+        ]
         widgets = {
             'virtual_machine': forms.HiddenInput(),
             'form_factor': forms.HiddenInput(),
         }
+        labels = {
+            'mode': '802.1Q Mode',
+        }
+        help_texts = {
+            'mode': INTERFACE_MODE_HELP_TEXT,
+        }
+
+    def clean(self):
+
+        super(InterfaceForm, self).clean()
+
+        # Validate VLAN assignments
+        tagged_vlans = self.cleaned_data['tagged_vlans']
+
+        # 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."
+            })
+
+        # Remove all tagged VLAN assignments from "tagged all" interfaces
+        elif self.cleaned_data['mode'] == IFACE_MODE_TAGGED_ALL:
+            self.cleaned_data['tagged_vlans'] = []
 
 
 class InterfaceCreateForm(ComponentForm):

+ 1 - 2
netbox/virtualization/models.py

@@ -283,7 +283,6 @@ class VirtualMachine(CreatedUpdatedModel, CustomFieldModel):
         else:
             return None
 
+    @property
     def site(self):
-        # used when a child compent (eg Interface) needs to know its parent's site but
-        # the parent could be either a device or a virtual machine
         return self.cluster.site

+ 1 - 0
netbox/virtualization/views.py

@@ -329,6 +329,7 @@ class InterfaceEditView(PermissionRequiredMixin, ObjectEditView):
     permission_required = 'dcim.change_interface'
     model = Interface
     model_form = forms.InterfaceForm
+    template_name = 'virtualization/interface_edit.html'
 
 
 class InterfaceDeleteView(PermissionRequiredMixin, ObjectDeleteView):