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
|
||||
#
|
||||
|
||||
class InterfaceForm(BootstrapMixin, forms.ModelForm, ChainedFieldsMixin):
|
||||
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 InterfaceForm(BootstrapMixin, forms.ModelForm):
|
||||
|
||||
class Meta:
|
||||
model = Interface
|
||||
fields = [
|
||||
'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 = {
|
||||
'device': forms.HiddenInput(),
|
||||
}
|
||||
labels = {
|
||||
'mode': '802.1Q Mode',
|
||||
}
|
||||
help_texts = {
|
||||
'mode': "Nullifying the mode will clear any associated VLANs."
|
||||
}
|
||||
|
||||
def __init__(self, *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
|
||||
)
|
||||
|
||||
# Limit the queryset for the site to only include the interface's device's site
|
||||
if device and device.site:
|
||||
self.fields['site'].queryset = Site.objects.filter(pk=device.site.id)
|
||||
self.fields['site'].initial = None
|
||||
else:
|
||||
self.fields['site'].queryset = Site.objects.none()
|
||||
self.fields['site'].initial = None
|
||||
def clean(self):
|
||||
|
||||
# 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.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,
|
||||
}
|
||||
super(InterfaceForm, self).clean()
|
||||
|
||||
self.fields['untagged_vlan'].queryset = VLAN.objects.filter(**filter_dict)
|
||||
self.fields['tagged_vlans'].queryset = VLAN.objects.filter(**filter_dict)
|
||||
# Validate VLAN assignments
|
||||
untagged_vlan = self.cleaned_data['untagged_vlan']
|
||||
tagged_vlans = self.cleaned_data['tagged_vlans']
|
||||
|
||||
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_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
|
||||
)
|
||||
|
||||
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."
|
||||
)
|
||||
class Meta:
|
||||
model = Interface
|
||||
fields = []
|
||||
|
||||
return self.cleaned_data['tagged_vlans']
|
||||
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:
|
||||
self.instance.untagged_vlan = self.cleaned_data['vlans'][0]
|
||||
|
||||
return super(InterfaceAssignVLANsForm, self).save(*args, **kwargs)
|
||||
|
||||
|
||||
class InterfaceCreateForm(ComponentForm, ChainedFieldsMixin):
|
||||
class InterfaceCreateForm(ComponentForm, forms.ModelForm):
|
||||
name_pattern = ExpandableNameField(label='Name')
|
||||
form_factor = forms.ChoiceField(choices=IFACE_FF_CHOICES)
|
||||
enabled = forms.BooleanField(required=False)
|
||||
@ -1790,50 +1758,6 @@ class InterfaceCreateForm(ComponentForm, ChainedFieldsMixin):
|
||||
)
|
||||
description = forms.CharField(max_length=100, 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):
|
||||
|
||||
@ -1851,39 +1775,6 @@ class InterfaceCreateForm(ComponentForm, ChainedFieldsMixin):
|
||||
else:
|
||||
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):
|
||||
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)
|
||||
})
|
||||
|
||||
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
|
||||
def parent(self):
|
||||
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'^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+)/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/rename/$', views.InterfaceBulkRenameView.as_view(), name='interface_bulk_rename'),
|
||||
|
||||
|
@ -1645,6 +1645,12 @@ class InterfaceEditView(PermissionRequiredMixin, ObjectEditView):
|
||||
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):
|
||||
permission_required = 'dcim.delete_interface'
|
||||
model = Interface
|
||||
|
@ -13,16 +13,58 @@
|
||||
{% render_field form.mtu %}
|
||||
{% render_field form.mgmt_only %}
|
||||
{% 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.site %}
|
||||
{% render_field form.vlan_group %}
|
||||
{% render_field form.untagged_vlan %}
|
||||
{% render_field form.tagged_vlans %}
|
||||
</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 %}
|
||||
|
Loading…
Reference in New Issue
Block a user