mirror of
https://github.com/netbox-community/netbox.git
synced 2025-07-25 01:48:38 -06:00
Improved validation and workflow
This commit is contained in:
parent
7c043d9b4f
commit
70625a5cb0
@ -1667,7 +1667,9 @@ class InterfaceForm(BootstrapMixin, forms.ModelForm):
|
|||||||
'mode': '802.1Q Mode',
|
'mode': '802.1Q Mode',
|
||||||
}
|
}
|
||||||
help_texts = {
|
help_texts = {
|
||||||
'mode': "Nullifying the mode will clear any associated VLANs."
|
'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)"
|
||||||
}
|
}
|
||||||
|
|
||||||
def __init__(self, *args, **kwargs):
|
def __init__(self, *args, **kwargs):
|
||||||
@ -1693,13 +1695,20 @@ class InterfaceForm(BootstrapMixin, forms.ModelForm):
|
|||||||
untagged_vlan = self.cleaned_data['untagged_vlan']
|
untagged_vlan = self.cleaned_data['untagged_vlan']
|
||||||
tagged_vlans = self.cleaned_data['tagged_vlans']
|
tagged_vlans = self.cleaned_data['tagged_vlans']
|
||||||
|
|
||||||
|
# A VLAN cannot be both tagged and untagged
|
||||||
|
if untagged_vlan and untagged_vlan in tagged_vlans:
|
||||||
|
raise forms.ValidationError("VLAN {} cannot be both tagged and untagged.".format(untagged_vlan))
|
||||||
|
|
||||||
|
# Untagged interfaces cannot be assigned tagged VLANs
|
||||||
if self.cleaned_data['mode'] == IFACE_MODE_ACCESS and tagged_vlans:
|
if self.cleaned_data['mode'] == IFACE_MODE_ACCESS and tagged_vlans:
|
||||||
raise forms.ValidationError({
|
raise forms.ValidationError({
|
||||||
'mode': "An access interface cannot have tagged VLANs assigned."
|
'mode': "An access interface cannot have tagged VLANs assigned."
|
||||||
})
|
})
|
||||||
|
|
||||||
if untagged_vlan and untagged_vlan in tagged_vlans:
|
# Remove all tagged VLAN assignments from "tagged all" interfaces
|
||||||
raise forms.ValidationError("VLAN {} cannot be both tagged and untagged.".format(untagged_vlan))
|
elif self.cleaned_data['mode'] == IFACE_MODE_TAGGED_ALL:
|
||||||
|
self.cleaned_data['tagged_vlans'] = []
|
||||||
|
|
||||||
|
|
||||||
class InterfaceAssignVLANsForm(BootstrapMixin, forms.ModelForm):
|
class InterfaceAssignVLANsForm(BootstrapMixin, forms.ModelForm):
|
||||||
vlans = forms.MultipleChoiceField(
|
vlans = forms.MultipleChoiceField(
|
||||||
@ -1720,31 +1729,51 @@ class InterfaceAssignVLANsForm(BootstrapMixin, forms.ModelForm):
|
|||||||
|
|
||||||
super(InterfaceAssignVLANsForm, self).__init__(*args, **kwargs)
|
super(InterfaceAssignVLANsForm, self).__init__(*args, **kwargs)
|
||||||
|
|
||||||
|
if self.instance.mode == IFACE_MODE_ACCESS:
|
||||||
|
self.initial['tagged'] = False
|
||||||
|
|
||||||
|
# Find all VLANs already assigned to the interface for exclusion from the list
|
||||||
|
assigned_vlans = [v.pk for v in self.instance.tagged_vlans.all()]
|
||||||
|
if self.instance.untagged_vlan is not None:
|
||||||
|
assigned_vlans.append(self.instance.untagged_vlan.pk)
|
||||||
|
|
||||||
# Initialize VLAN choices
|
# Initialize VLAN choices
|
||||||
device = self.instance.device
|
device = self.instance.device
|
||||||
vlan_choices = [
|
vlan_choices = [
|
||||||
('Global', [(vlan.pk, vlan) for vlan in VLAN.objects.filter(site=None)]),
|
('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)]),
|
(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):
|
for group in VLANGroup.objects.filter(site=device.site):
|
||||||
vlan_choices.append((
|
vlan_choices.append((
|
||||||
'{} / {}'.format(group.site.name, group.name),
|
'{} / {}'.format(group.site.name, group.name),
|
||||||
[(vlan.pk, vlan) for vlan in VLAN.objects.filter(group=group)]
|
[(vlan.pk, vlan) for vlan in VLAN.objects.filter(group=group).exclude(pk__in=assigned_vlans)]
|
||||||
))
|
))
|
||||||
self.fields['vlans'].choices = vlan_choices
|
self.fields['vlans'].choices = vlan_choices
|
||||||
|
|
||||||
|
def clean(self):
|
||||||
|
|
||||||
|
super(InterfaceAssignVLANsForm, self).clean()
|
||||||
|
|
||||||
|
# Only untagged VLANs permitted on an access interface
|
||||||
|
if self.instance.mode == IFACE_MODE_ACCESS and len(self.cleaned_data['vlans']) > 1:
|
||||||
|
raise forms.ValidationError("Only one VLAN may be assigned to an access interface.")
|
||||||
|
|
||||||
|
# 'tagged' is required if more than one VLAN is selected
|
||||||
|
if not self.cleaned_data['tagged'] and len(self.cleaned_data['vlans']) > 1:
|
||||||
|
raise forms.ValidationError("Only one untagged VLAN may be selected.")
|
||||||
|
|
||||||
def save(self, *args, **kwargs):
|
def save(self, *args, **kwargs):
|
||||||
|
|
||||||
if self.cleaned_data['tagged']:
|
if self.cleaned_data['tagged']:
|
||||||
for vlan in self.cleaned_data['vlans']:
|
for vlan in self.cleaned_data['vlans']:
|
||||||
self.instance.tagged_vlans.add(vlan)
|
self.instance.tagged_vlans.add(vlan)
|
||||||
else:
|
else:
|
||||||
self.instance.untagged_vlan = self.cleaned_data['vlans'][0]
|
self.instance.untagged_vlan_id = self.cleaned_data['vlans'][0]
|
||||||
|
|
||||||
return super(InterfaceAssignVLANsForm, self).save(*args, **kwargs)
|
return super(InterfaceAssignVLANsForm, self).save(*args, **kwargs)
|
||||||
|
|
||||||
|
|
||||||
class InterfaceCreateForm(ComponentForm, forms.ModelForm):
|
class InterfaceCreateForm(ComponentForm, forms.Form):
|
||||||
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)
|
||||||
|
@ -1457,11 +1457,13 @@ class Interface(models.Model):
|
|||||||
|
|
||||||
def save(self, *args, **kwargs):
|
def save(self, *args, **kwargs):
|
||||||
|
|
||||||
|
# Remove untagged VLAN assignment for non-802.1Q interfaces
|
||||||
if self.mode is None:
|
if self.mode is None:
|
||||||
self.untagged_vlan = None
|
self.untagged_vlan = None
|
||||||
self.tagged_vlans = []
|
|
||||||
elif self.mode is IFACE_MODE_ACCESS:
|
# Only "tagged" interfaces may have tagged VLANs assigned. ("tagged all" implies all VLANs are assigned.)
|
||||||
self.tagged_vlans = []
|
if self.mode is not IFACE_MODE_TAGGED:
|
||||||
|
self.tagged_vlans.clear()
|
||||||
|
|
||||||
return super(Interface, self).save(*args, **kwargs)
|
return super(Interface, self).save(*args, **kwargs)
|
||||||
|
|
||||||
|
@ -16,55 +16,93 @@
|
|||||||
{% render_field form.mode %}
|
{% render_field form.mode %}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
{% with interface=form.instance %}
|
{% if obj.mode %}
|
||||||
{% if interface.mode %}
|
<div class="panel panel-default" id="vlans_panel">
|
||||||
<div class="panel panel-default">
|
<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">
|
||||||
<table class="table panel-body">
|
<tr>
|
||||||
|
<th>VID</th>
|
||||||
|
<th>Name</th>
|
||||||
|
<th>Untagged</th>
|
||||||
|
<th>Tagged</th>
|
||||||
|
</tr>
|
||||||
|
{% if obj.untagged_vlan %}
|
||||||
<tr>
|
<tr>
|
||||||
<th>VID</th>
|
<td>
|
||||||
<th>Name</th>
|
<a href="{{ obj.untagged_vlan.get_absolute_url }}">{{ obj.untagged_vlan.vid }}</a>
|
||||||
<th>Untagged</th>
|
</td>
|
||||||
<th>Tagged</th>
|
<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>
|
</tr>
|
||||||
{% if interface.untagged_vlan %}
|
{% endif %}
|
||||||
<tr>
|
{% for vlan in obj.tagged_vlans.all %}
|
||||||
<td>{{ interface.untagged_vlan.vid }}</td>
|
<tr>
|
||||||
<td>{{ interface.untagged_vlan.name }}</td>
|
<td>
|
||||||
<td>
|
<a href="{{ vlan.get_absolute_url }}">{{ vlan.vid }}</a>
|
||||||
<input type="radio" name="untagged_vlan" value="{{ interface.untagged_vlan.pk }}" checked="true" />
|
</td>
|
||||||
</td>
|
<td>{{ vlan.name }}</td>
|
||||||
<td>
|
<td>
|
||||||
<input type="checkbox" name="tagged_vlans" value="{{ interface.untagged_vlan.pk }}" />
|
<input type="radio" name="untagged_vlan" value="{{ vlan.pk }}" />
|
||||||
</td>
|
</td>
|
||||||
</tr>
|
<td>
|
||||||
{% endif %}
|
<input type="checkbox" name="tagged_vlans" value="{{ vlan.pk }}" checked="true" />
|
||||||
{% for vlan in interface.tagged_vlans.all %}
|
</td>
|
||||||
<tr>
|
</tr>
|
||||||
<td>{{ vlan.vid }}</td>
|
{% endfor %}
|
||||||
<td>{{ vlan.name }}</td>
|
{% if not obj.untagged_vlan and not obj.tagged_vlans.exists %}
|
||||||
<td>
|
<tr>
|
||||||
<input type="radio" name="untagged_vlan" value="{{ vlan.pk }}" />
|
<td colspan="4" class="text-muted text-center">
|
||||||
</td>
|
No VLANs assigned
|
||||||
<td>
|
</td>
|
||||||
<input type="checkbox" name="tagged_vlans" value="{{ vlan.pk }}" checked="true" />
|
</tr>
|
||||||
</td>
|
{% else %}
|
||||||
</tr>
|
<tr>
|
||||||
{% endfor %}
|
<td colspan="2"></td>
|
||||||
{% if not interface.untagged_vlan and not interface.tagged_vlans.exists %}
|
<td>
|
||||||
<tr>
|
<a href="#" id="clear_untagged_vlan" class="btn btn-warning btn-xs">Clear</a>
|
||||||
<td colspan="4">
|
</td>
|
||||||
<span class="text-muted">No VLANs assigned</span>
|
<td>
|
||||||
</td>
|
<a href="#" id="clear_tagged_vlans" class="btn btn-warning btn-xs">Clear All</a>
|
||||||
</tr>
|
</td>
|
||||||
{% endif %}
|
</tr>
|
||||||
</table>
|
{% endif %}
|
||||||
<div class="panel-footer text-right">
|
</table>
|
||||||
<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">
|
<div class="panel-footer text-right">
|
||||||
<i class="glyphicon glyphicon-plus"></i> Add VLANs
|
<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>
|
<i class="glyphicon glyphicon-plus"></i> Add VLANs
|
||||||
</div>
|
</a>
|
||||||
</div>
|
</div>
|
||||||
{% endif %}
|
</div>
|
||||||
{% endwith %}
|
{% 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 'dcim: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 %}
|
{% endblock %}
|
||||||
|
@ -31,13 +31,15 @@
|
|||||||
</div>
|
</div>
|
||||||
<div class="row">
|
<div class="row">
|
||||||
<div class="col-md-6 col-md-offset-3 text-right">
|
<div class="col-md-6 col-md-offset-3 text-right">
|
||||||
{% if obj.pk %}
|
{% block buttons %}
|
||||||
<button type="submit" name="_update" class="btn btn-primary">Update</button>
|
{% if obj.pk %}
|
||||||
{% else %}
|
<button type="submit" name="_update" class="btn btn-primary">Update</button>
|
||||||
<button type="submit" name="_create" class="btn btn-primary">Create</button>
|
{% else %}
|
||||||
<button type="submit" name="_addanother" class="btn btn-primary">Create and Add Another</button>
|
<button type="submit" name="_create" class="btn btn-primary">Create</button>
|
||||||
{% endif %}
|
<button type="submit" name="_addanother" class="btn btn-primary">Create and Add Another</button>
|
||||||
<a href="{{ return_url }}" class="btn btn-default">Cancel</a>
|
{% endif %}
|
||||||
|
<a href="{{ return_url }}" class="btn btn-default">Cancel</a>
|
||||||
|
{% endblock %}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</form>
|
</form>
|
||||||
|
Loading…
Reference in New Issue
Block a user