Closes #1944: Enable assigning VLANs to virtual machine interfaces

This commit is contained in:
Jeremy Stretch 2018-03-14 14:53:28 -04:00
parent ae6848b194
commit 8bd268d81c
7 changed files with 183 additions and 77 deletions

View File

@ -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):

View File

@ -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>

View File

@ -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

View File

@ -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 %}

View File

@ -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):

View File

@ -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

View File

@ -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):