mirror of
https://github.com/netbox-community/netbox.git
synced 2025-07-16 04:02:52 -06:00
Replaced tagged/untagged VLAN assignment widgets with a VLAN table; separate view for adding VLANs
This commit is contained in:
parent
546f17ab50
commit
7c043d9b4f
@ -1652,63 +1652,23 @@ class PowerOutletBulkDisconnectForm(ConfirmationForm):
|
|||||||
# Interfaces
|
# Interfaces
|
||||||
#
|
#
|
||||||
|
|
||||||
class InterfaceForm(BootstrapMixin, forms.ModelForm, ChainedFieldsMixin):
|
class InterfaceForm(BootstrapMixin, forms.ModelForm):
|
||||||
site = forms.ModelChoiceField(
|
|
||||||
queryset=Site.objects.all(),
|
|
||||||
required=False,
|
|
||||||
label='VLAN site',
|
|
||||||
widget=forms.Select(
|
|
||||||
attrs={'filter-for': 'vlan_group', 'nullable': 'true'},
|
|
||||||
)
|
|
||||||
)
|
|
||||||
vlan_group = ChainedModelChoiceField(
|
|
||||||
queryset=VLANGroup.objects.all(),
|
|
||||||
chains=(
|
|
||||||
('site', 'site'),
|
|
||||||
),
|
|
||||||
required=False,
|
|
||||||
label='VLAN group',
|
|
||||||
widget=APISelect(
|
|
||||||
attrs={'filter-for': 'untagged_vlan tagged_vlans', 'nullable': 'true'},
|
|
||||||
api_url='/api/ipam/vlan-groups/?site_id={{site}}',
|
|
||||||
)
|
|
||||||
)
|
|
||||||
untagged_vlan = ChainedModelChoiceField(
|
|
||||||
queryset=VLAN.objects.all(),
|
|
||||||
chains=(
|
|
||||||
('site', 'site'),
|
|
||||||
('group', 'vlan_group'),
|
|
||||||
),
|
|
||||||
required=False,
|
|
||||||
label='Untagged VLAN',
|
|
||||||
widget=APISelect(
|
|
||||||
api_url='/api/ipam/vlans/?site_id={{site}}&group_id={{vlan_group}}',
|
|
||||||
display_field='display_name'
|
|
||||||
)
|
|
||||||
)
|
|
||||||
tagged_vlans = ChainedModelMultipleChoiceField(
|
|
||||||
queryset=VLAN.objects.all(),
|
|
||||||
chains=(
|
|
||||||
('site', 'site'),
|
|
||||||
('group', 'vlan_group'),
|
|
||||||
),
|
|
||||||
required=False,
|
|
||||||
label='Tagged VLANs',
|
|
||||||
widget=APISelectMultiple(
|
|
||||||
api_url='/api/ipam/vlans/?site_id={{site}}&group_id={{vlan_group}}',
|
|
||||||
display_field='display_name'
|
|
||||||
)
|
|
||||||
)
|
|
||||||
|
|
||||||
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', 'mtu', 'mac_address', 'mgmt_only', 'description',
|
||||||
'mode', 'site', 'vlan_group', 'untagged_vlan', 'tagged_vlans',
|
'mode', 'untagged_vlan', 'tagged_vlans',
|
||||||
]
|
]
|
||||||
widgets = {
|
widgets = {
|
||||||
'device': forms.HiddenInput(),
|
'device': forms.HiddenInput(),
|
||||||
}
|
}
|
||||||
|
labels = {
|
||||||
|
'mode': '802.1Q Mode',
|
||||||
|
}
|
||||||
|
help_texts = {
|
||||||
|
'mode': "Nullifying the mode will clear any associated VLANs."
|
||||||
|
}
|
||||||
|
|
||||||
def __init__(self, *args, **kwargs):
|
def __init__(self, *args, **kwargs):
|
||||||
super(InterfaceForm, self).__init__(*args, **kwargs)
|
super(InterfaceForm, self).__init__(*args, **kwargs)
|
||||||
@ -1725,58 +1685,66 @@ class InterfaceForm(BootstrapMixin, forms.ModelForm, ChainedFieldsMixin):
|
|||||||
device__in=[self.instance.device, self.instance.device.get_vc_master()], form_factor=IFACE_FF_LAG
|
device__in=[self.instance.device, self.instance.device.get_vc_master()], form_factor=IFACE_FF_LAG
|
||||||
)
|
)
|
||||||
|
|
||||||
# Limit the queryset for the site to only include the interface's device's site
|
def clean(self):
|
||||||
if device and device.site:
|
|
||||||
self.fields['site'].queryset = Site.objects.filter(pk=device.site.id)
|
super(InterfaceForm, self).clean()
|
||||||
self.fields['site'].initial = None
|
|
||||||
|
# Validate VLAN assignments
|
||||||
|
untagged_vlan = self.cleaned_data['untagged_vlan']
|
||||||
|
tagged_vlans = self.cleaned_data['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))
|
||||||
|
|
||||||
|
class InterfaceAssignVLANsForm(BootstrapMixin, forms.ModelForm):
|
||||||
|
vlans = forms.MultipleChoiceField(
|
||||||
|
choices=[],
|
||||||
|
label='VLANs',
|
||||||
|
widget=forms.SelectMultiple(attrs={'size': 20})
|
||||||
|
)
|
||||||
|
tagged = forms.BooleanField(
|
||||||
|
required=False,
|
||||||
|
initial=True
|
||||||
|
)
|
||||||
|
|
||||||
|
class Meta:
|
||||||
|
model = Interface
|
||||||
|
fields = []
|
||||||
|
|
||||||
|
def __init__(self, *args, **kwargs):
|
||||||
|
|
||||||
|
super(InterfaceAssignVLANsForm, self).__init__(*args, **kwargs)
|
||||||
|
|
||||||
|
# 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)]),
|
||||||
|
]
|
||||||
|
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)]
|
||||||
|
))
|
||||||
|
self.fields['vlans'].choices = vlan_choices
|
||||||
|
|
||||||
|
def save(self, *args, **kwargs):
|
||||||
|
|
||||||
|
if self.cleaned_data['tagged']:
|
||||||
|
for vlan in self.cleaned_data['vlans']:
|
||||||
|
self.instance.tagged_vlans.add(vlan)
|
||||||
else:
|
else:
|
||||||
self.fields['site'].queryset = Site.objects.none()
|
self.instance.untagged_vlan = self.cleaned_data['vlans'][0]
|
||||||
self.fields['site'].initial = None
|
|
||||||
|
|
||||||
# Limit the initial vlan choices
|
return super(InterfaceAssignVLANsForm, self).save(*args, **kwargs)
|
||||||
if self.is_bound and self.data.get('vlan_group') and self.data.get('site'):
|
|
||||||
filter_dict = {
|
|
||||||
'group_id': self.data.get('vlan_group'),
|
|
||||||
'site_id': self.data.get('site'),
|
|
||||||
}
|
|
||||||
elif self.initial.get('untagged_vlan'):
|
|
||||||
filter_dict = {
|
|
||||||
'group_id': self.instance.untagged_vlan.group,
|
|
||||||
'site_id': self.instance.untagged_vlan.site,
|
|
||||||
}
|
|
||||||
elif self.initial.get('tagged_vlans'):
|
|
||||||
filter_dict = {
|
|
||||||
'group_id': self.instance.tagged_vlans.first().group,
|
|
||||||
'site_id': self.instance.tagged_vlans.first().site,
|
|
||||||
}
|
|
||||||
else:
|
|
||||||
filter_dict = {
|
|
||||||
'group_id': None,
|
|
||||||
'site_id': None,
|
|
||||||
}
|
|
||||||
|
|
||||||
self.fields['untagged_vlan'].queryset = VLAN.objects.filter(**filter_dict)
|
|
||||||
self.fields['tagged_vlans'].queryset = VLAN.objects.filter(**filter_dict)
|
|
||||||
|
|
||||||
def clean_tagged_vlans(self):
|
|
||||||
"""
|
|
||||||
Because tagged_vlans is a many-to-many relationship, validation must be done in the form
|
|
||||||
"""
|
|
||||||
if self.cleaned_data['mode'] == IFACE_MODE_ACCESS and self.cleaned_data['tagged_vlans']:
|
|
||||||
raise forms.ValidationError(
|
|
||||||
"An Access interface cannot have tagged VLANs."
|
|
||||||
)
|
|
||||||
|
|
||||||
if self.cleaned_data['mode'] == IFACE_MODE_TAGGED_ALL and self.cleaned_data['tagged_vlans']:
|
|
||||||
raise forms.ValidationError(
|
|
||||||
"Interface mode Tagged All implies all VLANs are tagged. "
|
|
||||||
"Do not select any tagged VLANs."
|
|
||||||
)
|
|
||||||
|
|
||||||
return self.cleaned_data['tagged_vlans']
|
|
||||||
|
|
||||||
|
|
||||||
class InterfaceCreateForm(ComponentForm, ChainedFieldsMixin):
|
class InterfaceCreateForm(ComponentForm, forms.ModelForm):
|
||||||
name_pattern = ExpandableNameField(label='Name')
|
name_pattern = ExpandableNameField(label='Name')
|
||||||
form_factor = forms.ChoiceField(choices=IFACE_FF_CHOICES)
|
form_factor = forms.ChoiceField(choices=IFACE_FF_CHOICES)
|
||||||
enabled = forms.BooleanField(required=False)
|
enabled = forms.BooleanField(required=False)
|
||||||
@ -1790,50 +1758,6 @@ class InterfaceCreateForm(ComponentForm, ChainedFieldsMixin):
|
|||||||
)
|
)
|
||||||
description = forms.CharField(max_length=100, required=False)
|
description = forms.CharField(max_length=100, required=False)
|
||||||
mode = forms.ChoiceField(choices=add_blank_choice(IFACE_MODE_CHOICES), required=False)
|
mode = forms.ChoiceField(choices=add_blank_choice(IFACE_MODE_CHOICES), required=False)
|
||||||
site = forms.ModelChoiceField(
|
|
||||||
queryset=Site.objects.all(),
|
|
||||||
required=False,
|
|
||||||
label='VLAN Site',
|
|
||||||
widget=forms.Select(
|
|
||||||
attrs={'filter-for': 'vlan_group', 'nullable': 'true'},
|
|
||||||
)
|
|
||||||
)
|
|
||||||
vlan_group = ChainedModelChoiceField(
|
|
||||||
queryset=VLANGroup.objects.all(),
|
|
||||||
chains=(
|
|
||||||
('site', 'site'),
|
|
||||||
),
|
|
||||||
required=False,
|
|
||||||
label='VLAN group',
|
|
||||||
widget=APISelect(
|
|
||||||
attrs={'filter-for': 'untagged_vlan tagged_vlans', 'nullable': 'true'},
|
|
||||||
api_url='/api/ipam/vlan-groups/?site_id={{site}}',
|
|
||||||
)
|
|
||||||
)
|
|
||||||
untagged_vlan = ChainedModelChoiceField(
|
|
||||||
queryset=VLAN.objects.all(),
|
|
||||||
chains=(
|
|
||||||
('site', 'site'),
|
|
||||||
('group', 'vlan_group'),
|
|
||||||
),
|
|
||||||
required=False,
|
|
||||||
label='Untagged VLAN',
|
|
||||||
widget=APISelect(
|
|
||||||
api_url='/api/ipam/vlans/?site_id={{site}}&group_id={{vlan_group}}',
|
|
||||||
)
|
|
||||||
)
|
|
||||||
tagged_vlans = ChainedModelMultipleChoiceField(
|
|
||||||
queryset=VLAN.objects.all(),
|
|
||||||
chains=(
|
|
||||||
('site', 'site'),
|
|
||||||
('group', 'vlan_group'),
|
|
||||||
),
|
|
||||||
required=False,
|
|
||||||
label='Tagged VLANs',
|
|
||||||
widget=APISelectMultiple(
|
|
||||||
api_url='/api/ipam/vlans/?site_id={{site}}&group_id={{vlan_group}}',
|
|
||||||
)
|
|
||||||
)
|
|
||||||
|
|
||||||
def __init__(self, *args, **kwargs):
|
def __init__(self, *args, **kwargs):
|
||||||
|
|
||||||
@ -1851,39 +1775,6 @@ class InterfaceCreateForm(ComponentForm, ChainedFieldsMixin):
|
|||||||
else:
|
else:
|
||||||
self.fields['lag'].queryset = Interface.objects.none()
|
self.fields['lag'].queryset = Interface.objects.none()
|
||||||
|
|
||||||
# Limit the queryset for the site to only include the interface's device's site
|
|
||||||
if self.parent is not None and self.parent.site:
|
|
||||||
self.fields['site'].queryset = Site.objects.filter(pk=self.parent.site.id)
|
|
||||||
self.fields['site'].initial = None
|
|
||||||
else:
|
|
||||||
self.fields['site'].queryset = Site.objects.none()
|
|
||||||
self.fields['site'].initial = None
|
|
||||||
|
|
||||||
# Limit the initial vlan choices
|
|
||||||
if self.is_bound and self.data.get('vlan_group') and self.data.get('site'):
|
|
||||||
filter_dict = {
|
|
||||||
'group_id': self.data.get('vlan_group'),
|
|
||||||
'site_id': self.data.get('site'),
|
|
||||||
}
|
|
||||||
elif self.initial.get('untagged_vlan'):
|
|
||||||
filter_dict = {
|
|
||||||
'group_id': self.untagged_vlan.group,
|
|
||||||
'site_id': self.untagged_vlan.site,
|
|
||||||
}
|
|
||||||
elif self.initial.get('tagged_vlans'):
|
|
||||||
filter_dict = {
|
|
||||||
'group_id': self.tagged_vlans.first().group,
|
|
||||||
'site_id': self.tagged_vlans.first().site,
|
|
||||||
}
|
|
||||||
else:
|
|
||||||
filter_dict = {
|
|
||||||
'group_id': None,
|
|
||||||
'site_id': None,
|
|
||||||
}
|
|
||||||
|
|
||||||
self.fields['untagged_vlan'].queryset = VLAN.objects.filter(**filter_dict)
|
|
||||||
self.fields['tagged_vlans'].queryset = VLAN.objects.filter(**filter_dict)
|
|
||||||
|
|
||||||
|
|
||||||
class InterfaceBulkEditForm(BootstrapMixin, BulkEditForm, ChainedFieldsMixin):
|
class InterfaceBulkEditForm(BootstrapMixin, BulkEditForm, ChainedFieldsMixin):
|
||||||
pk = forms.ModelMultipleChoiceField(queryset=Interface.objects.all(), widget=forms.MultipleHiddenInput)
|
pk = forms.ModelMultipleChoiceField(queryset=Interface.objects.all(), widget=forms.MultipleHiddenInput)
|
||||||
|
@ -1455,6 +1455,16 @@ class Interface(models.Model):
|
|||||||
"device/VM, or it must be global".format(self.untagged_vlan)
|
"device/VM, or it must be global".format(self.untagged_vlan)
|
||||||
})
|
})
|
||||||
|
|
||||||
|
def save(self, *args, **kwargs):
|
||||||
|
|
||||||
|
if self.mode is None:
|
||||||
|
self.untagged_vlan = None
|
||||||
|
self.tagged_vlans = []
|
||||||
|
elif self.mode is IFACE_MODE_ACCESS:
|
||||||
|
self.tagged_vlans = []
|
||||||
|
|
||||||
|
return super(Interface, self).save(*args, **kwargs)
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def parent(self):
|
def parent(self):
|
||||||
return self.device or self.virtual_machine
|
return self.device or self.virtual_machine
|
||||||
|
@ -185,6 +185,7 @@ urlpatterns = [
|
|||||||
url(r'^devices/(?P<pk>\d+)/interface-connections/add/$', views.InterfaceConnectionAddView.as_view(), name='interfaceconnection_add'),
|
url(r'^devices/(?P<pk>\d+)/interface-connections/add/$', views.InterfaceConnectionAddView.as_view(), name='interfaceconnection_add'),
|
||||||
url(r'^interface-connections/(?P<pk>\d+)/delete/$', views.InterfaceConnectionDeleteView.as_view(), name='interfaceconnection_delete'),
|
url(r'^interface-connections/(?P<pk>\d+)/delete/$', views.InterfaceConnectionDeleteView.as_view(), name='interfaceconnection_delete'),
|
||||||
url(r'^interfaces/(?P<pk>\d+)/edit/$', views.InterfaceEditView.as_view(), name='interface_edit'),
|
url(r'^interfaces/(?P<pk>\d+)/edit/$', views.InterfaceEditView.as_view(), name='interface_edit'),
|
||||||
|
url(r'^interfaces/(?P<pk>\d+)/assign-vlans/$', views.InterfaceAssignVLANsView.as_view(), name='interface_assign_vlans'),
|
||||||
url(r'^interfaces/(?P<pk>\d+)/delete/$', views.InterfaceDeleteView.as_view(), name='interface_delete'),
|
url(r'^interfaces/(?P<pk>\d+)/delete/$', views.InterfaceDeleteView.as_view(), name='interface_delete'),
|
||||||
url(r'^interfaces/rename/$', views.InterfaceBulkRenameView.as_view(), name='interface_bulk_rename'),
|
url(r'^interfaces/rename/$', views.InterfaceBulkRenameView.as_view(), name='interface_bulk_rename'),
|
||||||
|
|
||||||
|
@ -1645,6 +1645,12 @@ class InterfaceEditView(PermissionRequiredMixin, ObjectEditView):
|
|||||||
template_name = 'dcim/interface_edit.html'
|
template_name = 'dcim/interface_edit.html'
|
||||||
|
|
||||||
|
|
||||||
|
class InterfaceAssignVLANsView(PermissionRequiredMixin, ObjectEditView):
|
||||||
|
permission_required = 'dcim.change_interface'
|
||||||
|
model = Interface
|
||||||
|
model_form = forms.InterfaceAssignVLANsForm
|
||||||
|
|
||||||
|
|
||||||
class InterfaceDeleteView(PermissionRequiredMixin, ObjectDeleteView):
|
class InterfaceDeleteView(PermissionRequiredMixin, ObjectDeleteView):
|
||||||
permission_required = 'dcim.delete_interface'
|
permission_required = 'dcim.delete_interface'
|
||||||
model = Interface
|
model = Interface
|
||||||
|
@ -13,16 +13,58 @@
|
|||||||
{% render_field form.mtu %}
|
{% render_field form.mtu %}
|
||||||
{% render_field form.mgmt_only %}
|
{% render_field form.mgmt_only %}
|
||||||
{% render_field form.description %}
|
{% render_field form.description %}
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<div class="panel panel-default">
|
|
||||||
<div class="panel-heading"><strong>802.1Q Encapsulation</strong></div>
|
|
||||||
<div class="panel-body">
|
|
||||||
{% render_field form.mode %}
|
{% render_field form.mode %}
|
||||||
{% render_field form.site %}
|
|
||||||
{% render_field form.vlan_group %}
|
|
||||||
{% render_field form.untagged_vlan %}
|
|
||||||
{% render_field form.tagged_vlans %}
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
{% with interface=form.instance %}
|
||||||
|
{% if interface.mode %}
|
||||||
|
<div class="panel panel-default">
|
||||||
|
<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 interface.untagged_vlan %}
|
||||||
|
<tr>
|
||||||
|
<td>{{ interface.untagged_vlan.vid }}</td>
|
||||||
|
<td>{{ interface.untagged_vlan.name }}</td>
|
||||||
|
<td>
|
||||||
|
<input type="radio" name="untagged_vlan" value="{{ interface.untagged_vlan.pk }}" checked="true" />
|
||||||
|
</td>
|
||||||
|
<td>
|
||||||
|
<input type="checkbox" name="tagged_vlans" value="{{ interface.untagged_vlan.pk }}" />
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
{% endif %}
|
||||||
|
{% for vlan in interface.tagged_vlans.all %}
|
||||||
|
<tr>
|
||||||
|
<td>{{ vlan.vid }}</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 interface.untagged_vlan and not interface.tagged_vlans.exists %}
|
||||||
|
<tr>
|
||||||
|
<td colspan="4">
|
||||||
|
<span class="text-muted">No VLANs assigned</span>
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
{% endif %}
|
||||||
|
</table>
|
||||||
|
<div class="panel-footer text-right">
|
||||||
|
<a href="{% url 'dcim:interface_assign_vlans' pk=interface.pk %}?return_url={% url 'dcim:interface_edit' pk=interface.pk %}" class="btn btn-primary btn-xs">
|
||||||
|
<i class="glyphicon glyphicon-plus"></i> Add VLANs
|
||||||
|
</a>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
{% endif %}
|
||||||
|
{% endwith %}
|
||||||
{% endblock %}
|
{% endblock %}
|
||||||
|
Loading…
Reference in New Issue
Block a user