mirror of
https://github.com/netbox-community/netbox.git
synced 2025-07-24 09:28:38 -06:00
Closes #1944: Enable assigning VLANs to virtual machine interfaces
This commit is contained in:
parent
ae6848b194
commit
8bd268d81c
@ -14,11 +14,10 @@ from ipam.models import IPAddress, VLAN, VLANGroup
|
|||||||
from tenancy.forms import TenancyForm
|
from tenancy.forms import TenancyForm
|
||||||
from tenancy.models import Tenant
|
from tenancy.models import Tenant
|
||||||
from utilities.forms import (
|
from utilities.forms import (
|
||||||
AnnotatedMultipleChoiceField, APISelect, APISelectMultiple, add_blank_choice, ArrayFieldSelectMultiple,
|
AnnotatedMultipleChoiceField, APISelect, add_blank_choice, ArrayFieldSelectMultiple, BootstrapMixin, BulkEditForm,
|
||||||
BootstrapMixin, BulkEditForm, BulkEditNullBooleanSelect, ChainedFieldsMixin, ChainedModelChoiceField,
|
BulkEditNullBooleanSelect, ChainedFieldsMixin, ChainedModelChoiceField, CommentField, ComponentForm,
|
||||||
ChainedModelMultipleChoiceField, CommentField, ComponentForm, ConfirmationForm, CSVChoiceField, ExpandableNameField,
|
ConfirmationForm, CSVChoiceField, ExpandableNameField, FilterChoiceField, FilterTreeNodeMultipleChoiceField,
|
||||||
FilterChoiceField, FilterTreeNodeMultipleChoiceField, FlexibleModelChoiceField, Livesearch, SelectWithDisabled,
|
FlexibleModelChoiceField, Livesearch, SelectWithDisabled, SelectWithPK, SmallTextarea, SlugField,
|
||||||
SelectWithPK, SmallTextarea, SlugField,
|
|
||||||
)
|
)
|
||||||
from virtualization.models import Cluster
|
from virtualization.models import Cluster
|
||||||
from .constants import (
|
from .constants import (
|
||||||
@ -37,6 +36,12 @@ from .models import (
|
|||||||
|
|
||||||
DEVICE_BY_PK_RE = '{\d+\}'
|
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):
|
def get_device_by_name_or_pk(name):
|
||||||
"""
|
"""
|
||||||
@ -1657,7 +1662,7 @@ class InterfaceForm(BootstrapMixin, forms.ModelForm):
|
|||||||
class Meta:
|
class Meta:
|
||||||
model = Interface
|
model = Interface
|
||||||
fields = [
|
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',
|
'mode', 'untagged_vlan', 'tagged_vlans',
|
||||||
]
|
]
|
||||||
widgets = {
|
widgets = {
|
||||||
@ -1667,9 +1672,7 @@ class InterfaceForm(BootstrapMixin, forms.ModelForm):
|
|||||||
'mode': '802.1Q Mode',
|
'mode': '802.1Q Mode',
|
||||||
}
|
}
|
||||||
help_texts = {
|
help_texts = {
|
||||||
'mode': "Access: One untagged VLAN<br />"
|
'mode': INTERFACE_MODE_HELP_TEXT,
|
||||||
"Tagged: One untagged VLAN and/or one or more tagged VLANs<br />"
|
|
||||||
"Tagged All: Implies all VLANs are available (w/optional untagged VLAN)"
|
|
||||||
}
|
}
|
||||||
|
|
||||||
def __init__(self, *args, **kwargs):
|
def __init__(self, *args, **kwargs):
|
||||||
@ -1732,17 +1735,37 @@ class InterfaceAssignVLANsForm(BootstrapMixin, forms.ModelForm):
|
|||||||
if self.instance.untagged_vlan is not None:
|
if self.instance.untagged_vlan is not None:
|
||||||
assigned_vlans.append(self.instance.untagged_vlan.pk)
|
assigned_vlans.append(self.instance.untagged_vlan.pk)
|
||||||
|
|
||||||
# Initialize VLAN choices
|
# Compile VLAN choices
|
||||||
device = self.instance.device
|
vlan_choices = []
|
||||||
vlan_choices = [
|
|
||||||
('Global', [(vlan.pk, vlan) for vlan in VLAN.objects.filter(site=None).exclude(pk__in=assigned_vlans)]),
|
# Add global VLANs
|
||||||
(device.site.name, [(vlan.pk, vlan) for vlan in VLAN.objects.filter(site=device.site, group=None).exclude(pk__in=assigned_vlans)]),
|
global_vlans = VLAN.objects.filter(site=None, group=None).exclude(pk__in=assigned_vlans)
|
||||||
]
|
vlan_choices.append((
|
||||||
for group in VLANGroup.objects.filter(site=device.site):
|
'Global', [(vlan.pk, vlan) for vlan in global_vlans])
|
||||||
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)]
|
# 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
|
self.fields['vlans'].choices = vlan_choices
|
||||||
|
|
||||||
def clean(self):
|
def clean(self):
|
||||||
|
55
netbox/templates/dcim/inc/interface_vlans_table.html
Normal file
55
netbox/templates/dcim/inc/interface_vlans_table.html
Normal 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>
|
@ -19,59 +19,7 @@
|
|||||||
{% if obj.mode %}
|
{% if obj.mode %}
|
||||||
<div class="panel panel-default" id="vlans_panel">
|
<div class="panel panel-default" id="vlans_panel">
|
||||||
<div class="panel-heading"><strong>802.1Q VLANs</strong></div>
|
<div class="panel-heading"><strong>802.1Q VLANs</strong></div>
|
||||||
<table class="table panel-body">
|
{% include 'dcim/inc/interface_vlans_table.html' %}
|
||||||
<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>
|
|
||||||
<div class="panel-footer text-right">
|
<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 %}">
|
<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
|
<i class="glyphicon glyphicon-plus"></i> Add VLANs
|
||||||
|
53
netbox/templates/virtualization/interface_edit.html
Normal file
53
netbox/templates/virtualization/interface_edit.html
Normal 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 %}
|
@ -5,7 +5,8 @@ from django.core.exceptions import ValidationError
|
|||||||
from django.db.models import Count
|
from django.db.models import Count
|
||||||
from mptt.forms import TreeNodeChoiceField
|
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.formfields import MACAddressFormField
|
||||||
from dcim.models import Device, DeviceRole, Interface, Platform, Rack, Region, Site
|
from dcim.models import Device, DeviceRole, Interface, Platform, Rack, Region, Site
|
||||||
from extras.forms import CustomFieldBulkEditForm, CustomFieldForm, CustomFieldFilterForm
|
from extras.forms import CustomFieldBulkEditForm, CustomFieldForm, CustomFieldFilterForm
|
||||||
@ -414,11 +415,37 @@ class InterfaceForm(BootstrapMixin, forms.ModelForm):
|
|||||||
|
|
||||||
class Meta:
|
class Meta:
|
||||||
model = Interface
|
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 = {
|
widgets = {
|
||||||
'virtual_machine': forms.HiddenInput(),
|
'virtual_machine': forms.HiddenInput(),
|
||||||
'form_factor': 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):
|
class InterfaceCreateForm(ComponentForm):
|
||||||
|
@ -283,7 +283,6 @@ class VirtualMachine(CreatedUpdatedModel, CustomFieldModel):
|
|||||||
else:
|
else:
|
||||||
return None
|
return None
|
||||||
|
|
||||||
|
@property
|
||||||
def site(self):
|
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
|
return self.cluster.site
|
||||||
|
@ -329,6 +329,7 @@ class InterfaceEditView(PermissionRequiredMixin, ObjectEditView):
|
|||||||
permission_required = 'dcim.change_interface'
|
permission_required = 'dcim.change_interface'
|
||||||
model = Interface
|
model = Interface
|
||||||
model_form = forms.InterfaceForm
|
model_form = forms.InterfaceForm
|
||||||
|
template_name = 'virtualization/interface_edit.html'
|
||||||
|
|
||||||
|
|
||||||
class InterfaceDeleteView(PermissionRequiredMixin, ObjectDeleteView):
|
class InterfaceDeleteView(PermissionRequiredMixin, ObjectDeleteView):
|
||||||
|
Loading…
Reference in New Issue
Block a user