Coverage for ipam/forms/model_forms.py: 47%
319 statements
« prev ^ index » next coverage.py v7.15.2, created at 2026-10-10 18:35 +0000
« prev ^ index » next coverage.py v7.15.2, created at 2026-10-10 18:35 +0000
1from django import forms
2from django.contrib.contenttypes.models import ContentType
3from django.core.exceptions import ValidationError
4from django.utils.safestring import mark_safe
5from django.utils.translation import gettext_lazy as _
7from dcim.forms.mixins import ScopedForm
8from dcim.models import Device, Interface, Site, SiteGroup
9from ipam.choices import *
10from ipam.constants import *
11from ipam.formfields import IPNetworkFormField
12from ipam.forms.fields import PortMappingField
13from ipam.models import *
14from netbox.forms import NetBoxModelForm, OrganizationalModelForm, PrimaryModelForm
15from tenancy.forms import TenancyForm
16from utilities.exceptions import PermissionsViolation
17from utilities.forms import GenericObjectFormMixin, add_blank_choice
18from utilities.forms.fields import (
19 ChoiceField,
20 DynamicModelChoiceField,
21 DynamicModelMultipleChoiceField,
22 GenericObjectChoiceField,
23 NumericRangeArrayField,
24 TypedChoiceField,
25)
26from utilities.forms.rendering import FieldSet, ObjectAttribute, TabbedGroups
27from utilities.forms.widgets import DatePicker
28from virtualization.models import VirtualMachine, VMInterface
30__all__ = (
31 'ASNForm',
32 'ASNRangeForm',
33 'AggregateForm',
34 'FHRPGroupAssignmentForm',
35 'FHRPGroupForm',
36 'IPAddressAssignForm',
37 'IPAddressBulkAddForm',
38 'IPAddressForm',
39 'IPRangeForm',
40 'PrefixBulkAddForm',
41 'PrefixForm',
42 'RIRForm',
43 'RoleForm',
44 'RouteTargetForm',
45 'ServiceCreateForm',
46 'ServiceForm',
47 'ServiceTemplateForm',
48 'VLANBulkAddForm',
49 'VLANForm',
50 'VLANGroupForm',
51 'VLANTranslationPolicyForm',
52 'VLANTranslationRuleForm',
53 'VRFForm',
54)
57class VRFForm(TenancyForm, PrimaryModelForm):
58 import_targets = DynamicModelMultipleChoiceField(
59 label=_('Import targets'),
60 queryset=RouteTarget.objects.all(),
61 required=False
62 )
63 export_targets = DynamicModelMultipleChoiceField(
64 label=_('Export targets'),
65 queryset=RouteTarget.objects.all(),
66 required=False
67 )
69 fieldsets = (
70 FieldSet('name', 'rd', 'enforce_unique', 'description', 'tags', name=_('VRF')),
71 FieldSet('import_targets', 'export_targets', name=_('Route Targets')),
72 FieldSet('tenant_group', 'tenant', name=_('Tenancy')),
73 )
75 class Meta:
76 model = VRF
77 fields = [
78 'name', 'rd', 'enforce_unique', 'import_targets', 'export_targets', 'tenant_group', 'tenant', 'description',
79 'owner', 'comments', 'tags',
80 ]
81 labels = {
82 'rd': "RD",
83 }
86class RouteTargetForm(TenancyForm, PrimaryModelForm):
87 fieldsets = (
88 FieldSet('name', 'description', 'tags', name=_('Route Target')),
89 FieldSet('tenant_group', 'tenant', name=_('Tenancy')),
90 )
92 class Meta:
93 model = RouteTarget
94 fields = [
95 'name', 'tenant_group', 'tenant', 'description', 'owner', 'comments', 'tags',
96 ]
99class RIRForm(OrganizationalModelForm):
100 fieldsets = (
101 FieldSet('name', 'slug', 'is_private', 'description', 'tags', name=_('RIR')),
102 )
104 class Meta:
105 model = RIR
106 fields = [
107 'name', 'slug', 'is_private', 'description', 'owner', 'comments', 'tags',
108 ]
111class AggregateForm(TenancyForm, PrimaryModelForm):
112 rir = DynamicModelChoiceField(
113 queryset=RIR.objects.all(),
114 label=_('RIR'),
115 quick_add=True
116 )
118 fieldsets = (
119 FieldSet('prefix', 'rir', 'date_added', 'description', 'tags', name=_('Aggregate')),
120 FieldSet('tenant_group', 'tenant', name=_('Tenancy')),
121 )
123 class Meta:
124 model = Aggregate
125 fields = [
126 'prefix', 'rir', 'date_added', 'tenant_group', 'tenant', 'description', 'owner', 'comments', 'tags',
127 ]
128 widgets = {
129 'date_added': DatePicker(),
130 }
133class ASNRangeForm(TenancyForm, OrganizationalModelForm):
134 rir = DynamicModelChoiceField(
135 queryset=RIR.objects.all(),
136 label=_('RIR'),
137 quick_add=True
138 )
139 fieldsets = (
140 FieldSet('name', 'slug', 'rir', 'start', 'end', 'description', 'tags', name=_('ASN Range')),
141 FieldSet('tenant_group', 'tenant', name=_('Tenancy')),
142 )
144 class Meta:
145 model = ASNRange
146 fields = [
147 'name', 'slug', 'rir', 'start', 'end', 'tenant_group', 'tenant', 'owner', 'description', 'comments', 'tags'
148 ]
151class ASNForm(TenancyForm, PrimaryModelForm):
152 rir = DynamicModelChoiceField(
153 queryset=RIR.objects.all(),
154 label=_('RIR'),
155 quick_add=True
156 )
157 role = DynamicModelChoiceField(
158 queryset=Role.objects.all(),
159 label=_('Role'),
160 required=False,
161 quick_add=True
162 )
163 sites = DynamicModelMultipleChoiceField(
164 queryset=Site.objects.all(),
165 label=_('Sites'),
166 required=False
167 )
169 fieldsets = (
170 FieldSet('asn', 'rir', 'role', 'sites', 'description', 'tags', name=_('ASN')),
171 FieldSet('tenant_group', 'tenant', name=_('Tenancy')),
172 )
174 class Meta:
175 model = ASN
176 fields = [
177 'asn', 'rir', 'role', 'sites', 'tenant_group', 'tenant', 'description', 'owner', 'comments', 'tags'
178 ]
179 widgets = {
180 'date_added': DatePicker(),
181 }
183 def __init__(self, data=None, instance=None, *args, **kwargs):
184 super().__init__(data=data, instance=instance, *args, **kwargs)
186 if self.instance and self.instance.pk is not None:
187 self.fields['sites'].initial = self.instance.sites.all().values_list('id', flat=True)
189 def save(self, *args, **kwargs):
190 instance = super().save(*args, **kwargs)
191 instance.sites.set(self.cleaned_data['sites'])
192 return instance
195class RoleForm(OrganizationalModelForm):
196 fieldsets = (
197 FieldSet('name', 'slug', 'weight', 'description', 'tags', name=_('Role')),
198 )
200 class Meta:
201 model = Role
202 fields = [
203 'name', 'slug', 'weight', 'description', 'owner', 'comments', 'tags',
204 ]
207class PrefixForm(TenancyForm, ScopedForm, PrimaryModelForm):
208 status = ChoiceField(
209 label=_('Status'),
210 choices=PrefixStatusChoices,
211 initial=PrefixStatusChoices.STATUS_ACTIVE,
212 help_text=_('Operational status of this prefix'),
213 )
214 vrf = DynamicModelChoiceField(
215 queryset=VRF.objects.all(),
216 required=False,
217 label=_('VRF')
218 )
219 vlan = DynamicModelChoiceField(
220 queryset=VLAN.objects.all(),
221 required=False,
222 selector=True,
223 query_params={
224 'available_at_site': '$scope_object_id',
225 },
226 label=_('VLAN'),
227 )
228 role = DynamicModelChoiceField(
229 label=_('Role'),
230 queryset=Role.objects.all(),
231 required=False,
232 quick_add=True
233 )
235 fieldsets = (
236 FieldSet(
237 'prefix', 'status', 'vrf', 'role', 'is_pool', 'mark_utilized', 'description', 'tags', name=_('Prefix')
238 ),
239 FieldSet('scope', name=_('Scope'), html_id='scope'),
240 FieldSet('vlan', name=_('VLAN Assignment')),
241 FieldSet('tenant_group', 'tenant', name=_('Tenancy')),
242 )
244 class Meta:
245 model = Prefix
246 fields = [
247 'prefix', 'vrf', 'vlan', 'status', 'role', 'is_pool', 'mark_utilized', 'tenant_group',
248 'tenant', 'description', 'owner', 'comments', 'tags',
249 ]
251 def __init__(self, *args, **kwargs):
252 super().__init__(*args, **kwargs)
254 # #18605: only filter the VLAN select list if the selected scope is a Site (or none is selected yet).
255 # #22588: a Site Group scope filters VLANs by the group's member sites instead.
256 if scope_field := self.fields.get('scope', None):
257 selected_model = scope_field.selected_model
258 if selected_model is SiteGroup:
259 self.fields['vlan'].widget.dynamic_params.clear()
260 self.fields['vlan'].widget.attrs.pop('data-dynamic-params', None)
261 self.fields['vlan'].widget.add_query_params({
262 'available_at_site_group': '$scope_object_id',
263 })
264 elif selected_model not in (None, Site):
265 self.fields['vlan'].widget.attrs.pop('data-dynamic-params', None)
268class PrefixBulkAddForm(PrefixForm):
269 """
270 Subclass of PrefixForm for bulk creation. The prefix field is inherited
271 but excluded from fieldsets — it is populated programmatically by BulkCreateView
272 from the expanded pattern.
273 """
275 fieldsets = (
276 FieldSet(
277 'status', 'vrf', 'role', 'is_pool', 'mark_utilized', 'description', 'tags', name=_('Prefix')
278 ),
279 FieldSet('scope', name=_('Scope'), html_id='scope'),
280 FieldSet('vlan', name=_('VLAN Assignment')),
281 FieldSet('tenant_group', 'tenant', name=_('Tenancy')),
282 )
285class IPRangeForm(TenancyForm, PrimaryModelForm):
286 status = ChoiceField(
287 label=_('Status'),
288 choices=IPRangeStatusChoices,
289 initial=IPRangeStatusChoices.STATUS_ACTIVE,
290 help_text=_('Operational status of this range'),
291 )
292 vrf = DynamicModelChoiceField(
293 queryset=VRF.objects.all(),
294 required=False,
295 label=_('VRF')
296 )
297 role = DynamicModelChoiceField(
298 label=_('Role'),
299 queryset=Role.objects.all(),
300 required=False,
301 quick_add=True
302 )
304 fieldsets = (
305 FieldSet(
306 'vrf', 'start_address', 'end_address', 'role', 'status', 'mark_populated', 'mark_utilized', 'description',
307 'tags', name=_('IP Range')
308 ),
309 FieldSet('tenant_group', 'tenant', name=_('Tenancy')),
310 )
312 class Meta:
313 model = IPRange
314 fields = [
315 'vrf', 'start_address', 'end_address', 'status', 'role', 'tenant_group', 'tenant', 'mark_populated',
316 'mark_utilized', 'description', 'owner', 'comments', 'tags',
317 ]
320class IPAddressForm(TenancyForm, PrimaryModelForm):
321 status = ChoiceField(
322 label=_('Status'),
323 choices=IPAddressStatusChoices,
324 initial=IPAddressStatusChoices.STATUS_ACTIVE,
325 help_text=_('The operational status of this IP'),
326 )
327 role = TypedChoiceField(
328 label=_('Role'),
329 choices=add_blank_choice(IPAddressRoleChoices),
330 required=False,
331 help_text=_('The functional role of this IP'),
332 )
333 interface = DynamicModelChoiceField(
334 queryset=Interface.objects.all(),
335 required=False,
336 context={
337 'parent': 'device',
338 },
339 selector=True,
340 label=_('Interface'),
341 )
342 vminterface = DynamicModelChoiceField(
343 queryset=VMInterface.objects.all(),
344 required=False,
345 context={
346 'parent': 'virtual_machine',
347 },
348 selector=True,
349 label=_('Interface'),
350 )
351 fhrpgroup = DynamicModelChoiceField(
352 queryset=FHRPGroup.objects.all(),
353 required=False,
354 selector=True,
355 label=_('FHRP Group')
356 )
357 vrf = DynamicModelChoiceField(
358 queryset=VRF.objects.all(),
359 required=False,
360 label=_('VRF')
361 )
362 nat_inside = DynamicModelChoiceField(
363 queryset=IPAddress.objects.all(),
364 required=False,
365 selector=True,
366 label=_('IP Address'),
367 )
368 primary_for_parent = forms.BooleanField(
369 required=False,
370 label=_('Make this the primary IP for the device/VM')
371 )
372 oob_for_parent = forms.BooleanField(
373 required=False,
374 label=_('Make this the out-of-band IP for the device')
375 )
377 fieldsets = (
378 FieldSet('address', 'status', 'role', 'vrf', 'dns_name', 'description', 'tags', name=_('IP Address')),
379 FieldSet('tenant_group', 'tenant', name=_('Tenancy')),
380 FieldSet(
381 TabbedGroups(
382 FieldSet('interface', name=_('Device')),
383 FieldSet('vminterface', name=_('Virtual Machine')),
384 FieldSet('fhrpgroup', name=_('FHRP Group')),
385 ),
386 'primary_for_parent', 'oob_for_parent', name=_('Assignment')
387 ),
388 FieldSet('nat_inside', name=_('NAT IP (Inside)')),
389 )
391 class Meta:
392 model = IPAddress
393 fields = [
394 'address', 'vrf', 'status', 'role', 'dns_name', 'primary_for_parent', 'oob_for_parent', 'nat_inside',
395 'tenant_group', 'tenant', 'description', 'owner', 'comments', 'tags',
396 ]
398 def __init__(self, *args, **kwargs):
400 # Initialize helper selectors
401 instance = kwargs.get('instance')
402 initial = kwargs.get('initial', {}).copy()
403 if instance:
404 if type(instance.assigned_object) is Interface:
405 initial['interface'] = instance.assigned_object
406 elif type(instance.assigned_object) is VMInterface:
407 initial['vminterface'] = instance.assigned_object
408 elif type(instance.assigned_object) is FHRPGroup:
409 initial['fhrpgroup'] = instance.assigned_object
410 kwargs['initial'] = initial
412 super().__init__(*args, **kwargs)
414 # Initialize parent object & fields if IP address is already assigned
415 if self.instance.pk and self.instance.assigned_object:
416 parent = getattr(self.instance.assigned_object, 'parent_object', None)
417 if parent and (
418 self.instance.address.version == 4 and parent.primary_ip4_id == self.instance.pk or
419 self.instance.address.version == 6 and parent.primary_ip6_id == self.instance.pk
420 ):
421 self.initial['primary_for_parent'] = True
423 if parent and getattr(parent, 'oob_ip_id', None) == self.instance.pk:
424 self.initial['oob_for_parent'] = True
426 if type(instance.assigned_object) is Interface:
427 self.fields['interface'].widget.add_query_params({
428 'device_id': instance.assigned_object.device.pk,
429 })
430 elif type(instance.assigned_object) is VMInterface:
431 self.fields['vminterface'].widget.add_query_params({
432 'virtual_machine_id': instance.assigned_object.virtual_machine.pk,
433 })
435 # Disable object assignment fields if the IP address is designated as primary or OOB
436 if self.initial.get('primary_for_parent') or self.initial.get('oob_for_parent'):
437 self.fields['interface'].disabled = True
438 self.fields['vminterface'].disabled = True
439 self.fields['fhrpgroup'].disabled = True
441 def clean(self):
442 super().clean()
444 # Handle object assignment
445 selected_objects = [
446 field for field in ('interface', 'vminterface', 'fhrpgroup') if self.cleaned_data[field]
447 ]
448 if len(selected_objects) > 1:
449 raise forms.ValidationError({
450 selected_objects[1]: _("An IP address can only be assigned to a single object.")
451 })
452 if selected_objects:
453 assigned_object = self.cleaned_data[selected_objects[0]]
454 if self.instance.pk and self.instance.assigned_object and assigned_object != self.instance.assigned_object:
455 if self.cleaned_data['primary_for_parent']:
456 raise ValidationError(
457 _("Cannot reassign primary IP address for the parent device/VM")
458 )
459 if self.cleaned_data['oob_for_parent']:
460 raise ValidationError(
461 _("Cannot reassign out-of-Band IP address for the parent device")
462 )
463 self.instance.assigned_object = assigned_object
464 else:
465 self.instance.assigned_object = None
467 # Primary IP assignment is only available if an interface has been assigned.
468 interface = self.cleaned_data.get('interface') or self.cleaned_data.get('vminterface')
469 if self.cleaned_data.get('primary_for_parent') and not interface:
470 self.add_error(
471 'primary_for_parent', _("Only IP addresses assigned to an interface can be designated as primary IPs.")
472 )
474 # OOB IP assignment is only available if device interface has been assigned.
475 interface = self.cleaned_data.get('interface')
476 if self.cleaned_data.get('oob_for_parent') and not interface:
477 self.add_error(
478 'oob_for_parent', _(
479 "Only IP addresses assigned to a device interface can be designated as the out-of-band IP for a "
480 "device."
481 )
482 )
484 def save(self, *args, **kwargs):
485 ipaddress = super().save(*args, **kwargs)
487 # Assign/clear this IPAddress as the primary for the associated Device/VirtualMachine.
488 interface = self.instance.assigned_object
489 if type(interface) in (Interface, VMInterface):
490 parent = interface.parent_object
491 parent.snapshot()
492 if self.cleaned_data['primary_for_parent']:
493 if ipaddress.address.version == 4:
494 parent.primary_ip4 = ipaddress
495 else:
496 parent.primary_ip6 = ipaddress
497 parent.save()
498 elif ipaddress.address.version == 4 and parent.primary_ip4 == ipaddress:
499 parent.primary_ip4 = None
500 parent.save()
501 elif ipaddress.address.version == 6 and parent.primary_ip6 == ipaddress:
502 parent.primary_ip6 = None
503 parent.save()
505 # Assign/clear this IPAddress as the OOB for the associated Device
506 if type(interface) is Interface:
507 parent = interface.parent_object
508 parent.snapshot()
509 if self.cleaned_data['oob_for_parent']:
510 parent.oob_ip = ipaddress
511 parent.save()
512 elif parent.oob_ip == ipaddress:
513 parent.oob_ip = None
514 parent.save()
516 return ipaddress
519class IPAddressBulkAddForm(TenancyForm, PrimaryModelForm):
520 status = ChoiceField(
521 label=_('Status'),
522 choices=IPAddressStatusChoices,
523 initial=IPAddressStatusChoices.STATUS_ACTIVE,
524 help_text=_('The operational status of this IP'),
525 )
526 role = TypedChoiceField(
527 label=_('Role'),
528 choices=add_blank_choice(IPAddressRoleChoices),
529 required=False,
530 help_text=_('The functional role of this IP'),
531 )
532 vrf = DynamicModelChoiceField(
533 queryset=VRF.objects.all(),
534 required=False,
535 label=_('VRF')
536 )
538 fieldsets = (
539 FieldSet('status', 'role', 'vrf', 'dns_name', 'description', 'tags', name=_('IP Address')),
540 FieldSet('tenant_group', 'tenant', name=_('Tenancy')),
541 )
543 class Meta:
544 model = IPAddress
545 fields = [
546 'address', 'vrf', 'status', 'role', 'dns_name', 'tenant_group', 'tenant', 'description', 'owner',
547 'comments', 'tags',
548 ]
551class IPAddressAssignForm(forms.Form):
552 vrf_id = DynamicModelChoiceField(
553 queryset=VRF.objects.all(),
554 required=False,
555 label=_('VRF')
556 )
557 q = forms.CharField(
558 required=False,
559 label=_('Search'),
560 )
563class FHRPGroupForm(PrimaryModelForm):
564 protocol = ChoiceField(
565 label=_('Protocol'),
566 choices=FHRPGroupProtocolChoices,
567 )
568 auth_type = TypedChoiceField(
569 label=_('Authentication type'),
570 choices=add_blank_choice(FHRPGroupAuthTypeChoices),
571 required=False,
572 )
574 # Optionally create a new IPAddress along with the FHRPGroup
575 ip_vrf = DynamicModelChoiceField(
576 queryset=VRF.objects.all(),
577 required=False,
578 label=_('VRF')
579 )
580 ip_address = IPNetworkFormField(
581 required=False,
582 label=_('Address')
583 )
584 ip_status = ChoiceField(
585 choices=add_blank_choice(IPAddressStatusChoices),
586 required=False,
587 label=_('Status')
588 )
590 fieldsets = (
591 FieldSet('protocol', 'group_id', 'name', 'description', 'tags', name=_('FHRP Group')),
592 FieldSet('auth_type', 'auth_key', name=_('Authentication')),
593 FieldSet('ip_vrf', 'ip_address', 'ip_status', name=_('Virtual IP Address'))
594 )
596 class Meta:
597 model = FHRPGroup
598 fields = (
599 'protocol', 'group_id', 'auth_type', 'auth_key', 'name', 'ip_vrf', 'ip_address', 'ip_status', 'description',
600 'owner', 'comments', 'tags',
601 )
603 def save(self, *args, **kwargs):
604 instance = super().save(*args, **kwargs)
605 user = getattr(instance, '_user', None) # Set under FHRPGroupEditView.alter_object()
607 # Check if we need to create a new IPAddress for the group
608 if self.cleaned_data.get('ip_address'):
609 ipaddress = IPAddress(
610 vrf=self.cleaned_data['ip_vrf'],
611 address=self.cleaned_data['ip_address'],
612 status=self.cleaned_data['ip_status'],
613 role=FHRP_PROTOCOL_ROLE_MAPPINGS.get(self.cleaned_data['protocol'], IPAddressRoleChoices.ROLE_VIP),
614 assigned_object=instance
615 )
616 ipaddress.save()
618 # Check that the new IPAddress conforms with any assigned object-level permissions
619 if not IPAddress.objects.restrict(user, 'add').filter(pk=ipaddress.pk).first():
620 raise PermissionsViolation()
622 return instance
624 def clean(self):
625 super().clean()
627 ip_vrf = self.cleaned_data.get('ip_vrf')
628 ip_address = self.cleaned_data.get('ip_address')
629 ip_status = self.cleaned_data.get('ip_status')
631 if ip_address:
632 ip_form = IPAddressForm({
633 'address': ip_address,
634 'vrf': ip_vrf,
635 'status': ip_status,
636 })
637 if not ip_form.is_valid():
638 self.errors.update({
639 f'ip_{field}': error for field, error in ip_form.errors.items()
640 })
643class FHRPGroupAssignmentForm(forms.ModelForm):
644 group = DynamicModelChoiceField(
645 label=_('Group'),
646 queryset=FHRPGroup.objects.all()
647 )
649 fieldsets = (
650 FieldSet(ObjectAttribute('interface'), 'group', 'priority'),
651 )
653 class Meta:
654 model = FHRPGroupAssignment
655 fields = ('group', 'priority')
657 def clean_group(self):
658 group = self.cleaned_data['group']
660 conflicting_assignments = FHRPGroupAssignment.objects.filter(
661 interface_type=self.instance.interface_type,
662 interface_id=self.instance.interface_id,
663 group=group
664 )
665 if self.instance.id:
666 conflicting_assignments = conflicting_assignments.exclude(id=self.instance.id)
668 if conflicting_assignments.exists():
669 raise forms.ValidationError(
670 _('Assignment already exists')
671 )
673 return group
676class VLANGroupForm(GenericObjectFormMixin, TenancyForm, OrganizationalModelForm):
677 vid_ranges = NumericRangeArrayField(
678 label=_('VLAN IDs')
679 )
680 scope = GenericObjectChoiceField(
681 label=_('Scope'),
682 content_type_queryset=ContentType.objects.filter(model__in=VLANGROUP_SCOPE_TYPES),
683 required=False,
684 selector=True,
685 hx_target_id='scope',
686 )
688 fieldsets = (
689 FieldSet('name', 'slug', 'description', 'tags', name=_('VLAN Group')),
690 FieldSet('vid_ranges', name=_('Child VLANs')),
691 FieldSet('scope', name=_('Scope'), html_id='scope'),
692 FieldSet('tenant_group', 'tenant', name=_('Tenancy')),
693 )
695 class Meta:
696 model = VLANGroup
697 fields = [
698 'name', 'slug', 'description', 'vid_ranges', 'tenant_group', 'tenant', 'owner', 'comments',
699 'tags',
700 ]
703class VLANForm(TenancyForm, PrimaryModelForm):
704 status = ChoiceField(
705 label=_('Status'),
706 choices=VLANStatusChoices,
707 initial=VLANStatusChoices.STATUS_ACTIVE,
708 help_text=_('Operational status of this VLAN'),
709 )
710 qinq_role = TypedChoiceField(
711 label=_('Q-in-Q role'),
712 choices=add_blank_choice(VLANQinQRoleChoices),
713 required=False,
714 help_text=_('Customer/service VLAN designation (for Q-in-Q/IEEE 802.1ad)'),
715 )
716 group = DynamicModelChoiceField(
717 queryset=VLANGroup.objects.all(),
718 required=False,
719 selector=True,
720 label=_('VLAN Group')
721 )
722 site = DynamicModelChoiceField(
723 label=_('Site'),
724 queryset=Site.objects.all(),
725 required=False,
726 null_option='None',
727 selector=True,
728 help_text=mark_safe(
729 '<span class="text-warning"><i class="mdi mdi-alert"></i> {text}</span>'.format(
730 text=_(
731 'The direct assignment of VLANs to a site is deprecated and will be removed in a future release. '
732 'Users are encouraged to utilize VLAN groups for this purpose.'
733 )
734 )
735 )
736 )
737 role = DynamicModelChoiceField(
738 label=_('Role'),
739 queryset=Role.objects.all(),
740 required=False,
741 quick_add=True
742 )
743 qinq_svlan = DynamicModelChoiceField(
744 label=_('Q-in-Q SVLAN'),
745 queryset=VLAN.objects.all(),
746 required=False,
747 query_params={
748 'qinq_role': VLANQinQRoleChoices.ROLE_SERVICE,
749 }
750 )
752 class Meta:
753 model = VLAN
754 fields = [
755 'site', 'group', 'vid', 'name', 'status', 'role', 'tenant_group', 'tenant', 'qinq_role', 'qinq_svlan',
756 'description', 'owner', 'comments', 'tags',
757 ]
760class VLANBulkAddForm(VLANForm):
761 """
762 Subclass of VLANForm for bulk creation.
764 The VID field is inherited but excluded from the visible fieldsets, as it is
765 populated programmatically by BulkCreateView from the expanded pattern.
766 """
767 fieldsets = (
768 FieldSet('group', 'site', 'name', 'status', 'role', 'description', 'tags', name=_('VLAN')),
769 FieldSet('qinq_role', 'qinq_svlan', name=_('Q-in-Q/802.1ad')),
770 FieldSet('tenant_group', 'tenant', name=_('Tenancy')),
771 )
773 def __init__(self, *args, **kwargs):
774 super().__init__(*args, **kwargs)
775 self.fields['name'].help_text = _(
776 'Use {vid} as a placeholder for the VLAN ID. Example: VLAN-{vid}.'
777 )
780class VLANTranslationPolicyForm(PrimaryModelForm):
782 fieldsets = (
783 FieldSet('name', 'description', 'tags', name=_('VLAN Translation Policy')),
784 )
786 class Meta:
787 model = VLANTranslationPolicy
788 fields = [
789 'name', 'description', 'owner', 'tags',
790 ]
793class VLANTranslationRuleForm(NetBoxModelForm):
794 policy = DynamicModelChoiceField(
795 label=_('Policy'),
796 queryset=VLANTranslationPolicy.objects.all(),
797 selector=True
798 )
800 fieldsets = (
801 FieldSet('policy', 'local_vid', 'remote_vid', 'description', 'tags', name=_('VLAN Translation Rule')),
802 )
804 class Meta:
805 model = VLANTranslationRule
806 fields = [
807 'policy', 'local_vid', 'remote_vid', 'description', 'tags',
808 ]
811class ServicePortMappingsMixin(forms.Form):
812 """
813 Adds a ``port_mappings`` field (protocol + ports rows) to a Service/ServiceTemplate form. The field
814 maps directly to the model's ``port_mappings`` ArrayField, so no custom save handling is required.
815 """
816 port_mappings = PortMappingField(
817 label=_('Port Mappings'),
818 help_text=_(
819 "One protocol per row, each with one or more port numbers. A range may be specified using a "
820 "hyphen (e.g. 80,443,8000-8010)."
821 ),
822 )
825class ServiceTemplateForm(ServicePortMappingsMixin, PrimaryModelForm):
826 fieldsets = (
827 FieldSet('name', 'port_mappings', 'description', 'tags', name=_('Application Service Template')),
828 )
830 class Meta:
831 model = ServiceTemplate
832 fields = ('name', 'port_mappings', 'description', 'owner', 'comments', 'tags')
835class ServiceForm(ServicePortMappingsMixin, GenericObjectFormMixin, PrimaryModelForm):
836 parent = GenericObjectChoiceField(
837 label=_('Parent'),
838 content_type_queryset=ContentType.objects.filter(SERVICE_ASSIGNMENT_MODELS),
839 required=True,
840 selector=True,
841 hx_target_id='service',
842 )
843 ipaddresses = DynamicModelMultipleChoiceField(
844 queryset=IPAddress.objects.all(),
845 required=False,
846 label=_('IP Addresses'),
847 )
849 fieldsets = (
850 FieldSet(
851 'parent', 'name', 'port_mappings',
852 'ipaddresses', 'description', 'tags', name=_('Application Service'),
853 html_id='service',
854 ),
855 )
857 class Meta:
858 model = Service
859 fields = [
860 'name', 'port_mappings', 'ipaddresses', 'description', 'owner', 'comments', 'tags',
861 ]
863 def __init__(self, *args, **kwargs):
864 super().__init__(*args, **kwargs)
866 # Filter the IP address selector to those belonging to the selected parent. The object subwidget is
867 # named "parent_object_id", so the dynamic param references "$parent_object_id".
868 parent_model = self.fields['parent'].selected_model
869 if parent_model is Device:
870 self.fields['ipaddresses'].widget.add_query_params({'device_id': '$parent_object_id'})
871 elif parent_model is VirtualMachine:
872 self.fields['ipaddresses'].widget.add_query_params({'virtual_machine_id': '$parent_object_id'})
873 elif parent_model is FHRPGroup:
874 self.fields['ipaddresses'].widget.add_query_params({'fhrpgroup_id': '$parent_object_id'})
877class ServiceCreateForm(ServiceForm):
878 service_template = DynamicModelChoiceField(
879 label=_('Application Service template'),
880 queryset=ServiceTemplate.objects.all(),
881 required=False
882 )
884 fieldsets = (
885 FieldSet(
886 'parent',
887 TabbedGroups(
888 FieldSet('service_template', name=_('From Template')),
889 FieldSet('name', 'port_mappings', name=_('Custom')),
890 ),
891 'ipaddresses', 'description', 'tags', name=_('Application Service'),
892 html_id='service',
893 ),
894 )
896 class Meta(ServiceForm.Meta):
897 fields = [
898 'service_template', 'name', 'port_mappings', 'ipaddresses', 'description', 'owner',
899 'comments', 'tags',
900 ]
902 def __init__(self, *args, **kwargs):
903 super().__init__(*args, **kwargs)
905 # Fields which may be populated from a ServiceTemplate are not required
906 for field in ('name', 'port_mappings'):
907 self.fields[field].required = False
908 self.fields[field].widget.is_required = False
910 def clean(self):
911 super().clean()
912 if self.cleaned_data['service_template']:
913 # Create a new Service from the specified template
914 service_template = self.cleaned_data['service_template']
915 self.cleaned_data['name'] = service_template.name
916 self.cleaned_data['port_mappings'] = list(service_template.port_mappings)
917 if not self.cleaned_data['description']:
918 self.cleaned_data['description'] = service_template.description
919 elif not self.cleaned_data.get('name') or not self.cleaned_data.get('port_mappings'):
920 raise forms.ValidationError(
921 _("Must specify name and port mapping(s) if not using an application service template.")
922 )