Coverage for dcim/forms/model_forms.py: 63%
702 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.validators import EMPTY_VALUES
4from django.utils.html import format_html
5from django.utils.translation import gettext_lazy as _
6from timezone_field import TimeZoneFormField
8from dcim.choices import *
9from dcim.constants import *
10from dcim.forms.mixins import FrontPortFormMixin
11from dcim.models import *
12from extras.models import ConfigTemplate
13from ipam.choices import VLANQinQRoleChoices
14from ipam.models import ASN, VLAN, VRF, IPAddress, VLANGroup, VLANTranslationPolicy
15from netbox.choices import WeightUnitChoices
16from netbox.forms import NestedGroupModelForm, NetBoxModelForm, OrganizationalModelForm, PrimaryModelForm
17from netbox.forms.mixins import ChangelogMessageMixin, OwnerMixin
18from tenancy.forms import TenancyForm
19from users.models import User
20from utilities.choices import Choice
21from utilities.forms import add_blank_choice, get_field_value
22from utilities.forms.fields import (
23 ChoiceField,
24 DynamicModelChoiceField,
25 DynamicModelMultipleChoiceField,
26 JSONField,
27 NumericArrayField,
28 SlugField,
29 TypedChoiceField,
30)
31from utilities.forms.rendering import FieldSet, InlineFields, M2MAddRemoveFields, TabbedGroups
32from utilities.forms.widgets import (
33 APISelect,
34 ClearableFileInput,
35 ClearableSelect,
36 DatePicker,
37 HTMXSelect,
38 NumberWithOptions,
39 SelectWithPK,
40)
41from utilities.jsonschema import JSONSchemaProperty
42from virtualization.models import Cluster, VMInterface
43from wireless.choices import WirelessChannelChoices, WirelessRoleChoices
44from wireless.models import WirelessLAN, WirelessLANGroup
46from .common import InterfaceCommonForm, ModuleCommonForm
48__all__ = (
49 'CableBundleForm',
50 'CableForm',
51 'ConsolePortForm',
52 'ConsolePortTemplateForm',
53 'ConsoleServerPortForm',
54 'ConsoleServerPortTemplateForm',
55 'CoolingFeedForm',
56 'CoolingIntakeForm',
57 'CoolingIntakeTemplateForm',
58 'CoolingOutflowForm',
59 'CoolingOutflowTemplateForm',
60 'CoolingSourceForm',
61 'DeviceBayForm',
62 'DeviceBayTemplateForm',
63 'DeviceForm',
64 'DeviceRoleForm',
65 'DeviceTypeForm',
66 'DeviceVCMembershipForm',
67 'FrontPortForm',
68 'FrontPortTemplateForm',
69 'InterfaceForm',
70 'InterfaceTemplateForm',
71 'InventoryItemForm',
72 'InventoryItemRoleForm',
73 'InventoryItemTemplateForm',
74 'LocationForm',
75 'MACAddressForm',
76 'ManufacturerForm',
77 'ModuleBayForm',
78 'ModuleBayTemplateForm',
79 'ModuleBayTypeForm',
80 'ModuleForm',
81 'ModuleTypeForm',
82 'ModuleTypeProfileForm',
83 'PlatformForm',
84 'PopulateDeviceBayForm',
85 'PowerFeedForm',
86 'PowerOutletForm',
87 'PowerOutletTemplateForm',
88 'PowerPanelForm',
89 'PowerPortForm',
90 'PowerPortTemplateForm',
91 'RackForm',
92 'RackGroupForm',
93 'RackReservationForm',
94 'RackRoleForm',
95 'RackTypeForm',
96 'RearPortForm',
97 'RearPortTemplateForm',
98 'RegionForm',
99 'SiteForm',
100 'SiteGroupForm',
101 'VCMemberSelectForm',
102 'VirtualChassisForm',
103 'VirtualDeviceContextForm'
104)
107class RegionForm(NestedGroupModelForm):
108 parent = DynamicModelChoiceField(
109 label=_('Parent'),
110 queryset=Region.objects.all(),
111 required=False
112 )
114 fieldsets = (
115 FieldSet('parent', 'name', 'slug', 'description', 'tags'),
116 )
118 class Meta:
119 model = Region
120 fields = (
121 'parent', 'name', 'slug', 'description', 'owner', 'tags', 'comments',
122 )
125class SiteGroupForm(NestedGroupModelForm):
126 parent = DynamicModelChoiceField(
127 label=_('Parent'),
128 queryset=SiteGroup.objects.all(),
129 required=False
130 )
132 fieldsets = (
133 FieldSet('parent', 'name', 'slug', 'description', 'tags'),
134 )
136 class Meta:
137 model = SiteGroup
138 fields = (
139 'parent', 'name', 'slug', 'description', 'owner', 'comments', 'tags',
140 )
143class SiteForm(TenancyForm, PrimaryModelForm):
144 status = ChoiceField(
145 label=_('Status'),
146 choices=SiteStatusChoices,
147 initial=SiteStatusChoices.STATUS_ACTIVE,
148 )
149 region = DynamicModelChoiceField(
150 label=_('Region'),
151 queryset=Region.objects.all(),
152 required=False,
153 quick_add=True
154 )
155 group = DynamicModelChoiceField(
156 label=_('Group'),
157 queryset=SiteGroup.objects.all(),
158 required=False,
159 quick_add=True
160 )
161 asns = DynamicModelMultipleChoiceField(
162 queryset=ASN.objects.all(),
163 label=_('ASNs'),
164 required=False
165 )
166 add_asns = DynamicModelMultipleChoiceField(
167 queryset=ASN.objects.all(),
168 label=_('Add ASNs'),
169 required=False
170 )
171 remove_asns = DynamicModelMultipleChoiceField(
172 queryset=ASN.objects.all(),
173 label=_('Remove ASNs'),
174 required=False
175 )
176 slug = SlugField()
177 time_zone = TimeZoneFormField(
178 label=_('Time zone'),
179 choices=add_blank_choice(TimeZoneFormField().choices),
180 required=False
181 )
183 fieldsets = (
184 FieldSet(
185 'name', 'slug', 'status', 'region', 'group', 'facility', M2MAddRemoveFields('asns'), 'time_zone',
186 'description', 'tags',
187 name=_('Site')
188 ),
189 FieldSet('tenant_group', 'tenant', name=_('Tenancy')),
190 FieldSet('physical_address', 'shipping_address', 'latitude', 'longitude', name=_('Contact Info')),
191 )
193 class Meta:
194 model = Site
195 fields = (
196 'name', 'slug', 'status', 'region', 'group', 'tenant_group', 'tenant', 'facility', 'time_zone',
197 'description', 'physical_address', 'shipping_address', 'latitude', 'longitude', 'owner', 'comments', 'tags',
198 )
199 widgets = {
200 'physical_address': forms.Textarea(
201 attrs={
202 'rows': 3,
203 }
204 ),
205 'shipping_address': forms.Textarea(
206 attrs={
207 'rows': 3,
208 }
209 ),
210 }
212 def __init__(self, *args, **kwargs):
213 super().__init__(*args, **kwargs)
214 if self.instance.pk and (count := self.instance.asns.count()) >= M2MAddRemoveFields.THRESHOLD:
215 # Add/remove mode for large M2M sets
216 self.fields.pop('asns')
217 self.fields['add_asns'].widget.add_query_param('site_id__n', self.instance.pk)
218 self.fields['remove_asns'].widget.add_query_param('site_id', self.instance.pk)
219 self.fields['remove_asns'].help_text = _("{count} ASNs currently assigned").format(count=count)
220 else:
221 # Simple mode for new objects or small M2M sets
222 self.fields.pop('add_asns')
223 self.fields.pop('remove_asns')
224 if self.instance.pk:
225 self.initial['asns'] = list(self.instance.asns.values_list('pk', flat=True))
228class LocationForm(TenancyForm, NestedGroupModelForm):
229 status = ChoiceField(
230 label=_('Status'),
231 choices=LocationStatusChoices,
232 initial=LocationStatusChoices.STATUS_ACTIVE,
233 )
234 site = DynamicModelChoiceField(
235 label=_('Site'),
236 queryset=Site.objects.all(),
237 selector=True
238 )
239 parent = DynamicModelChoiceField(
240 label=_('Parent'),
241 queryset=Location.objects.all(),
242 required=False,
243 query_params={
244 'site_id': '$site'
245 }
246 )
248 fieldsets = (
249 FieldSet('site', 'parent', 'name', 'slug', 'status', 'facility', 'description', 'tags', name=_('Location')),
250 FieldSet('tenant_group', 'tenant', name=_('Tenancy')),
251 )
253 class Meta:
254 model = Location
255 fields = (
256 'site', 'parent', 'name', 'slug', 'status', 'description', 'tenant_group', 'tenant', 'facility', 'owner',
257 'comments', 'tags',
258 )
261class RackGroupForm(OrganizationalModelForm):
262 fieldsets = (
263 FieldSet('name', 'slug', 'description', 'tags', name=_('Rack Group')),
264 )
266 class Meta:
267 model = RackGroup
268 fields = [
269 'name', 'slug', 'description', 'owner', 'comments', 'tags',
270 ]
273class RackRoleForm(OrganizationalModelForm):
274 fieldsets = (
275 FieldSet('name', 'slug', 'color', 'description', 'tags', name=_('Rack Role')),
276 )
278 class Meta:
279 model = RackRole
280 fields = [
281 'name', 'slug', 'color', 'description', 'owner', 'comments', 'tags',
282 ]
285class RackTypeForm(PrimaryModelForm):
286 form_factor = ChoiceField(
287 label=_('Form factor'),
288 choices=RackFormFactorChoices,
289 )
290 outer_unit = TypedChoiceField(
291 label=_('Outer unit'),
292 choices=add_blank_choice(RackDimensionUnitChoices),
293 required=False,
294 )
295 weight_unit = TypedChoiceField(
296 label=_('Weight unit'),
297 choices=add_blank_choice(WeightUnitChoices),
298 required=False,
299 )
300 manufacturer = DynamicModelChoiceField(
301 label=_('Manufacturer'),
302 queryset=Manufacturer.objects.all(),
303 quick_add=True
304 )
305 slug = SlugField(
306 label=_('Slug'),
307 slug_source='model'
308 )
310 fieldsets = (
311 FieldSet('manufacturer', 'model', 'slug', 'description', 'form_factor', 'tags', name=_('Rack Type')),
312 FieldSet(
313 'width', 'u_height',
314 InlineFields('outer_width', 'outer_height', 'outer_depth', 'outer_unit', label=_('Outer Dimensions')),
315 InlineFields('weight', 'max_weight', 'weight_unit', label=_('Weight')),
316 'mounting_depth', name=_('Dimensions')
317 ),
318 FieldSet('starting_unit', 'desc_units', name=_('Numbering')),
319 FieldSet('cooling_capability', 'cooling_capacity', name=_('Cooling')),
320 )
322 class Meta:
323 model = RackType
324 fields = [
325 'manufacturer', 'model', 'slug', 'form_factor', 'width', 'u_height', 'starting_unit', 'desc_units',
326 'outer_width', 'outer_height', 'outer_depth', 'outer_unit', 'mounting_depth', 'weight', 'max_weight',
327 'weight_unit', 'cooling_capability', 'cooling_capacity', 'description', 'owner', 'comments', 'tags',
328 ]
331class RackForm(TenancyForm, PrimaryModelForm):
332 status = ChoiceField(
333 label=_('Status'),
334 choices=RackStatusChoices,
335 initial=RackStatusChoices.STATUS_ACTIVE,
336 )
337 form_factor = TypedChoiceField(
338 label=_('Form factor'),
339 choices=add_blank_choice(RackFormFactorChoices),
340 required=False,
341 )
342 outer_unit = TypedChoiceField(
343 label=_('Outer unit'),
344 choices=add_blank_choice(RackDimensionUnitChoices),
345 required=False,
346 )
347 airflow = TypedChoiceField(
348 label=_('Airflow'),
349 choices=add_blank_choice(RackAirflowChoices),
350 required=False,
351 )
352 weight_unit = TypedChoiceField(
353 label=_('Weight unit'),
354 choices=add_blank_choice(WeightUnitChoices),
355 required=False,
356 )
357 site = DynamicModelChoiceField(
358 label=_('Site'),
359 queryset=Site.objects.all(),
360 selector=True
361 )
362 location = DynamicModelChoiceField(
363 label=_('Location'),
364 queryset=Location.objects.all(),
365 required=False,
366 query_params={
367 'site_id': '$site'
368 }
369 )
370 group = DynamicModelChoiceField(
371 label=_('Rack Group'),
372 queryset=RackGroup.objects.all(),
373 required=False
374 )
375 role = DynamicModelChoiceField(
376 label=_('Role'),
377 queryset=RackRole.objects.all(),
378 required=False
379 )
380 rack_type = DynamicModelChoiceField(
381 label=_('Rack Type'),
382 queryset=RackType.objects.all(),
383 required=False,
384 selector=True,
385 help_text=_("Select a pre-defined rack type, or set physical characteristics below."),
386 )
388 fieldsets = (
389 FieldSet(
390 'site', 'location', 'group', 'name', 'status', 'role', 'rack_type', 'description', 'airflow', 'tags',
391 name=_('Rack')
392 ),
393 FieldSet('cooling_capability', 'cooling_capacity', name=_('Cooling')),
394 FieldSet('facility_id', 'serial', 'asset_tag', name=_('Inventory Control')),
395 FieldSet('tenant_group', 'tenant', name=_('Tenancy')),
396 )
398 class Meta:
399 model = Rack
400 fields = [
401 'site', 'location', 'group', 'name', 'facility_id', 'tenant_group', 'tenant', 'status', 'role', 'serial',
402 'asset_tag', 'rack_type', 'form_factor', 'width', 'u_height', 'starting_unit', 'desc_units', 'outer_width',
403 'outer_height', 'outer_depth', 'outer_unit', 'mounting_depth', 'airflow', 'cooling_capability',
404 'cooling_capacity', 'weight', 'max_weight', 'weight_unit', 'description', 'owner', 'comments', 'tags',
405 ]
407 def __init__(self, *args, **kwargs):
408 super().__init__(*args, **kwargs)
410 # Mimic HTMXSelect()
411 self.fields['rack_type'].widget.attrs.update({
412 'hx-get': '.',
413 'hx-include': '#form_fields',
414 'hx-target': '#form_fields',
415 })
417 # Omit RackType-defined fields if rack_type is set
418 if get_field_value(self, 'rack_type'):
419 for field_name in Rack.RACKTYPE_FIELDS:
420 del self.fields[field_name]
421 else:
422 # The form_factor, width, and outer_* fields are deprecated on the Rack model and will be removed in
423 # NetBox v5.0. Their values should instead be defined on an assigned rack type. (See #22593.)
424 deprecation_warning = format_html(
425 '<span class="text-warning"><i class="mdi mdi-alert"></i> {}</span>',
426 _(
427 'Deprecated and will be removed in NetBox v5.0. Assign a rack type to define this attribute '
428 'instead.'
429 )
430 )
431 self.fields['form_factor'].help_text = deprecation_warning
432 self.fields['width'].help_text = deprecation_warning
434 self.fieldsets = (
435 *self.fieldsets,
436 FieldSet(
437 'form_factor',
438 'width',
439 'starting_unit',
440 'u_height',
441 InlineFields(
442 'outer_width', 'outer_height', 'outer_depth', 'outer_unit',
443 label=_('Outer Dimensions'),
444 help_text=deprecation_warning,
445 ),
446 InlineFields(
447 'weight', 'max_weight', 'weight_unit', label=_('Weight'),
448 ),
449 'mounting_depth',
450 'desc_units',
451 name=_('Dimensions')
452 ),
453 )
456class RackReservationForm(TenancyForm, PrimaryModelForm):
457 status = ChoiceField(
458 label=_('Status'),
459 choices=RackReservationStatusChoices,
460 initial=RackReservationStatusChoices.STATUS_ACTIVE,
461 )
462 rack = DynamicModelChoiceField(
463 label=_('Rack'),
464 queryset=Rack.objects.all(),
465 selector=True
466 )
467 units = NumericArrayField(
468 label=_('Units'),
469 base_field=forms.IntegerField(),
470 help_text=_("Comma-separated list of numeric unit IDs. A range may be specified using a hyphen.")
471 )
472 user = forms.ModelChoiceField(
473 label=_('User'),
474 queryset=User.objects.order_by('username')
475 )
477 fieldsets = (
478 FieldSet('rack', 'units', 'status', 'user', 'description', 'tags', name=_('Reservation')),
479 FieldSet('tenant_group', 'tenant', name=_('Tenancy')),
480 )
482 class Meta:
483 model = RackReservation
484 fields = [
485 'rack', 'units', 'status', 'user', 'tenant_group', 'tenant', 'description', 'owner', 'comments', 'tags',
486 ]
489class ManufacturerForm(OrganizationalModelForm):
490 fieldsets = (
491 FieldSet('name', 'slug', 'description', 'tags', name=_('Manufacturer')),
492 )
494 class Meta:
495 model = Manufacturer
496 fields = [
497 'name', 'slug', 'description', 'owner', 'comments', 'tags',
498 ]
501class DeviceTypeForm(PrimaryModelForm):
502 subdevice_role = TypedChoiceField(
503 label=_('Parent/child status'),
504 choices=add_blank_choice(SubdeviceRoleChoices),
505 required=False,
506 help_text=_(
507 'Parent devices house child devices in device bays. Leave blank if this device type is neither a '
508 'parent nor a child.'
509 ),
510 )
511 airflow = TypedChoiceField(
512 label=_('Airflow'),
513 choices=add_blank_choice(DeviceAirflowChoices),
514 required=False,
515 )
516 weight_unit = TypedChoiceField(
517 label=_('Weight unit'),
518 choices=add_blank_choice(WeightUnitChoices),
519 required=False,
520 )
521 manufacturer = DynamicModelChoiceField(
522 label=_('Manufacturer'),
523 queryset=Manufacturer.objects.all(),
524 quick_add=True
525 )
526 default_platform = DynamicModelChoiceField(
527 label=_('Default platform'),
528 queryset=Platform.objects.all(),
529 required=False,
530 selector=True,
531 query_params={
532 'manufacturer_id': ['$manufacturer', 'null'],
533 }
534 )
535 slug = SlugField(
536 label=_('Slug'),
537 slug_source='model'
538 )
540 fieldsets = (
541 FieldSet('manufacturer', 'model', 'slug', 'default_platform', 'description', 'tags', name=_('Device Type')),
542 FieldSet(
543 'u_height', 'exclude_from_utilization', 'is_full_depth', 'part_number', 'subdevice_role',
544 'weight', 'weight_unit', name=_('Chassis')
545 ),
546 FieldSet('cooling_method', 'airflow', name=_('Cooling')),
547 FieldSet('end_of_life', name=_('Lifecycle')),
548 FieldSet('front_image', 'rear_image', name=_('Images')),
549 )
551 class Meta:
552 model = DeviceType
553 fields = [
554 'manufacturer', 'model', 'slug', 'default_platform', 'part_number', 'u_height', 'exclude_from_utilization',
555 'is_full_depth', 'subdevice_role', 'airflow', 'cooling_method', 'weight', 'weight_unit', 'end_of_life',
556 'front_image',
557 'rear_image', 'description', 'owner', 'comments', 'tags',
558 ]
559 widgets = {
560 'front_image': ClearableFileInput(attrs={
561 'accept': DEVICETYPE_IMAGE_FORMATS
562 }),
563 'rear_image': ClearableFileInput(attrs={
564 'accept': DEVICETYPE_IMAGE_FORMATS
565 }),
566 'end_of_life': DatePicker(),
567 }
570class ModuleBayTypeForm(PrimaryModelForm):
571 manufacturer = DynamicModelChoiceField(
572 label=_('Manufacturer'),
573 queryset=Manufacturer.objects.all(),
574 required=False,
575 )
576 slug = SlugField(
577 slug_source='name',
578 )
580 fieldsets = (
581 FieldSet('name', 'slug', 'manufacturer', 'color', 'description', 'tags', name=_('Module Bay Type')),
582 )
584 class Meta:
585 model = ModuleBayType
586 fields = [
587 'name', 'slug', 'manufacturer', 'color', 'description', 'owner', 'comments', 'tags',
588 ]
591class ModuleTypeProfileForm(PrimaryModelForm):
592 schema = JSONField(
593 label=_('Schema'),
594 required=False,
595 help_text=_("Enter a valid JSON schema to define supported attributes.")
596 )
598 fieldsets = (
599 FieldSet('name', 'description', 'schema', 'tags', name=_('Profile')),
600 )
602 class Meta:
603 model = ModuleTypeProfile
604 fields = [
605 'name', 'description', 'schema', 'owner', 'comments', 'tags',
606 ]
609class ModuleTypeForm(PrimaryModelForm):
610 airflow = TypedChoiceField(
611 label=_('Airflow'),
612 choices=add_blank_choice(ModuleAirflowChoices),
613 required=False,
614 )
615 weight_unit = TypedChoiceField(
616 label=_('Weight unit'),
617 choices=add_blank_choice(WeightUnitChoices),
618 required=False,
619 )
620 profile = forms.ModelChoiceField(
621 queryset=ModuleTypeProfile.objects.all(),
622 label=_('Profile'),
623 required=False,
624 widget=HTMXSelect(hx_target_id='profile-attributes')
625 )
626 manufacturer = DynamicModelChoiceField(
627 label=_('Manufacturer'),
628 queryset=Manufacturer.objects.all()
629 )
630 module_bay_types = DynamicModelMultipleChoiceField(
631 label=_('Module bay types'),
632 queryset=ModuleBayType.objects.all(),
633 required=False,
634 )
636 @property
637 def fieldsets(self):
638 return [
639 FieldSet('manufacturer', 'model', 'part_number', 'description', 'tags', name=_('Module Type')),
640 FieldSet('weight', 'weight_unit', name=_('Hardware')),
641 FieldSet('cooling_method', 'airflow', name=_('Cooling')),
642 FieldSet('module_bay_types', name=_('Bay Type Compatibility')),
643 FieldSet('end_of_life', name=_('Lifecycle')),
644 FieldSet('profile', *self.attr_fields, name=_('Profile & Attributes'), html_id='profile-attributes')
645 ]
647 class Meta:
648 model = ModuleType
649 fields = [
650 'profile', 'manufacturer', 'model', 'part_number', 'description', 'cooling_method', 'airflow', 'weight',
651 'weight_unit', 'module_bay_types', 'end_of_life', 'owner', 'comments', 'tags',
652 ]
653 widgets = {
654 'end_of_life': DatePicker(),
655 }
657 def __init__(self, *args, **kwargs):
658 super().__init__(*args, **kwargs)
660 # Track profile-specific attribute fields
661 self.attr_fields = []
663 # Retrieve assigned ModuleTypeProfile, if any
664 if not (profile_id := get_field_value(self, 'profile')):
665 return
666 if not (profile := ModuleTypeProfile.objects.filter(pk=profile_id).first()):
667 return
669 # Extend form with fields for profile attributes
670 for attr, form_field in self._get_attr_form_fields(profile).items():
671 field_name = f'attr_{attr}'
672 self.attr_fields.append(field_name)
673 self.fields[field_name] = form_field
674 if self.instance.attribute_data:
675 self.fields[field_name].initial = self.instance.attribute_data.get(attr)
677 @staticmethod
678 def _get_attr_form_fields(profile):
679 """
680 Return a dictionary mapping of attribute names to form fields, suitable for extending
681 the form per the selected ModuleTypeProfile.
682 """
683 if not profile.schema:
684 return {}
686 properties = profile.schema.get('properties', {})
687 required_fields = profile.schema.get('required', [])
689 attr_fields = {}
690 for name, options in properties.items():
691 prop = JSONSchemaProperty(**options)
692 attr_fields[name] = prop.to_form_field(name, required=name in required_fields)
694 return dict(sorted(attr_fields.items()))
696 def _post_clean(self):
698 # Compile attribute data from the individual form fields
699 if self.cleaned_data.get('profile'):
700 self.instance.attribute_data = {
701 name[5:]: self.cleaned_data[name] # Remove the attr_ prefix
702 for name in self.attr_fields
703 if self.cleaned_data.get(name) not in EMPTY_VALUES
704 }
706 return super()._post_clean()
709class DeviceRoleForm(NestedGroupModelForm):
710 config_template = DynamicModelChoiceField(
711 label=_('Config template'),
712 queryset=ConfigTemplate.objects.all(),
713 required=False
714 )
715 parent = DynamicModelChoiceField(
716 label=_('Parent'),
717 queryset=DeviceRole.objects.all(),
718 required=False,
719 )
721 fieldsets = (
722 FieldSet(
723 'name', 'slug', 'parent', 'color', 'vm_role', 'config_template', 'description',
724 'tags', name=_('Device Role')
725 ),
726 )
728 class Meta:
729 model = DeviceRole
730 fields = [
731 'name', 'slug', 'parent', 'color', 'vm_role', 'config_template', 'description', 'owner', 'comments', 'tags',
732 ]
735class PlatformForm(NestedGroupModelForm):
736 parent = DynamicModelChoiceField(
737 label=_('Parent'),
738 queryset=Platform.objects.all(),
739 required=False,
740 )
741 manufacturer = DynamicModelChoiceField(
742 label=_('Manufacturer'),
743 queryset=Manufacturer.objects.all(),
744 required=False,
745 quick_add=True
746 )
747 config_template = DynamicModelChoiceField(
748 label=_('Config template'),
749 queryset=ConfigTemplate.objects.all(),
750 required=False
751 )
752 slug = SlugField(
753 label=_('Slug'),
754 max_length=64
755 )
757 fieldsets = (
758 FieldSet(
759 'name', 'slug', 'parent', 'manufacturer', 'config_template', 'description', 'tags', name=_('Platform'),
760 ),
761 )
763 class Meta:
764 model = Platform
765 fields = [
766 'name', 'slug', 'parent', 'manufacturer', 'config_template', 'description', 'owner', 'comments', 'tags',
767 ]
770class DeviceForm(TenancyForm, PrimaryModelForm):
771 status = ChoiceField(
772 label=_('Status'),
773 choices=DeviceStatusChoices,
774 initial=DeviceStatusChoices.STATUS_ACTIVE,
775 )
776 airflow = TypedChoiceField(
777 label=_('Airflow'),
778 choices=add_blank_choice(DeviceAirflowChoices),
779 required=False,
780 )
781 site = DynamicModelChoiceField(
782 label=_('Site'),
783 queryset=Site.objects.all(),
784 selector=True
785 )
786 location = DynamicModelChoiceField(
787 label=_('Location'),
788 queryset=Location.objects.all(),
789 required=False,
790 query_params={
791 'site_id': '$site'
792 },
793 initial_params={
794 'racks': '$rack'
795 }
796 )
797 rack = DynamicModelChoiceField(
798 label=_('Rack'),
799 queryset=Rack.objects.all(),
800 required=False,
801 query_params={
802 'site_id': '$site',
803 'location_id': '$location',
804 }
805 )
806 position = forms.DecimalField(
807 label=_('Position'),
808 required=False,
809 help_text=_("The lowest-numbered unit occupied by the device"),
810 localize=True,
811 widget=APISelect(
812 api_url='/api/dcim/racks/{{rack}}/elevation/',
813 attrs={
814 'ts-disabled-field': 'device',
815 'data-dynamic-params': '[{"fieldName":"face","queryParam":"face"}]'
816 },
817 )
818 )
819 face = TypedChoiceField(
820 label=_('Face'),
821 choices=add_blank_choice(DeviceFaceChoices),
822 required=False,
823 widget=ClearableSelect(
824 requires_fields=['rack']
825 )
826 )
827 device_type = DynamicModelChoiceField(
828 label=_('Device type'),
829 queryset=DeviceType.objects.all(),
830 context={
831 'parent': 'manufacturer',
832 },
833 selector=True
834 )
835 role = DynamicModelChoiceField(
836 label=_('Device role'),
837 queryset=DeviceRole.objects.all(),
838 quick_add=True
839 )
840 platform = DynamicModelChoiceField(
841 label=_('Platform'),
842 queryset=Platform.objects.all(),
843 required=False,
844 selector=True,
845 query_params={
846 'available_for_device_type': '$device_type',
847 }
848 )
849 cluster = DynamicModelChoiceField(
850 label=_('Cluster'),
851 queryset=Cluster.objects.all(),
852 required=False,
853 selector=True,
854 query_params={
855 'site_id': ['$site', 'null']
856 },
857 )
858 local_context_data = JSONField(
859 required=False,
860 label='',
861 widget=forms.Textarea(attrs={'aria-label': _('Local config context data')})
862 )
863 virtual_chassis = DynamicModelChoiceField(
864 label=_('Virtual chassis'),
865 queryset=VirtualChassis.objects.all(),
866 required=False,
867 context={
868 'parent': 'master',
869 },
870 selector=True
871 )
872 vc_position = forms.IntegerField(
873 required=False,
874 label=_('Position'),
875 help_text=_("The position in the virtual chassis this device is identified by")
876 )
877 vc_priority = forms.IntegerField(
878 required=False,
879 label=_('Priority'),
880 help_text=_("The priority of the device in the virtual chassis")
881 )
882 config_template = DynamicModelChoiceField(
883 label=_('Config template'),
884 queryset=ConfigTemplate.objects.all(),
885 required=False
886 )
888 class Meta:
889 model = Device
890 fields = [
891 'name', 'role', 'device_type', 'serial', 'asset_tag', 'site', 'rack', 'location', 'position', 'face',
892 'latitude', 'longitude', 'status', 'airflow', 'cooling_method', 'platform', 'primary_ip4', 'primary_ip6',
893 'oob_ip', 'cluster',
894 'tenant_group', 'tenant', 'virtual_chassis', 'vc_position', 'vc_priority', 'description', 'config_template',
895 'owner', 'comments', 'tags', 'local_context_data',
896 ]
898 def __init__(self, *args, **kwargs):
899 super().__init__(*args, **kwargs)
901 if self.instance.pk:
903 # Compile list of choices for primary IPv4 and IPv6 addresses
904 oob_ip_choices = [(None, '---------')]
905 for family in [4, 6]:
906 ip_choices = [(None, '---------')]
908 # Gather PKs of all interfaces belonging to this Device or a peer VirtualChassis member
909 interface_ids = self.instance.vc_interfaces(if_master=False).values_list('pk', flat=True)
911 # Collect interface IPs
912 interface_ips = IPAddress.objects.filter(
913 address__family=family,
914 assigned_object_type=ContentType.objects.get_for_model(Interface),
915 assigned_object_id__in=interface_ids
916 ).prefetch_related('assigned_object')
917 if interface_ips:
918 ip_list = [(ip.id, f'{ip.address} ({ip.assigned_object})') for ip in interface_ips]
919 ip_choices.append(('Interface IPs', ip_list))
920 oob_ip_choices.extend(ip_list)
921 # Collect NAT IPs
922 nat_ips = IPAddress.objects.prefetch_related('nat_inside').filter(
923 address__family=family,
924 nat_inside__assigned_object_type=ContentType.objects.get_for_model(Interface),
925 nat_inside__assigned_object_id__in=interface_ids
926 ).prefetch_related('assigned_object')
927 if nat_ips:
928 ip_list = [(ip.id, f'{ip.address} (NAT)') for ip in nat_ips]
929 ip_choices.append(('NAT IPs', ip_list))
930 self.fields['primary_ip{}'.format(family)].choices = ip_choices
931 self.fields['oob_ip'].choices = oob_ip_choices
933 # If editing an existing device, exclude it from the list of occupied rack units. This ensures that a device
934 # can be flipped from one face to another.
935 self.fields['position'].widget.add_query_param('exclude', self.instance.pk)
937 # Disable rack assignment if this is a child device installed in a parent device
938 if self.instance.device_type.is_child_device and hasattr(self.instance, 'parent_bay'):
939 self.fields['site'].disabled = True
940 self.fields['rack'].disabled = True
941 self.initial['site'] = self.instance.parent_bay.device.site_id
942 self.initial['rack'] = self.instance.parent_bay.device.rack_id
944 else:
946 # An object that doesn't exist yet can't have any IPs assigned to it
947 self.fields['primary_ip4'].choices = []
948 self.fields['primary_ip4'].widget.attrs['readonly'] = True
949 self.fields['primary_ip6'].choices = []
950 self.fields['primary_ip6'].widget.attrs['readonly'] = True
951 self.fields['oob_ip'].choices = []
952 self.fields['oob_ip'].widget.attrs['readonly'] = True
954 # Rack position
955 position = self.data.get('position') or self.initial.get('position')
956 if position:
957 self.fields['position'].widget.choices = [(position, f'U{position}')]
960class ModuleForm(ModuleCommonForm, PrimaryModelForm):
961 status = ChoiceField(
962 label=_('Status'),
963 choices=ModuleStatusChoices,
964 initial=ModuleStatusChoices.STATUS_ACTIVE,
965 )
966 device = DynamicModelChoiceField(
967 label=_('Device'),
968 queryset=Device.objects.all(),
969 initial_params={
970 'modulebays': '$module_bay'
971 }
972 )
973 module_bay = DynamicModelChoiceField(
974 label=_('Module bay'),
975 queryset=ModuleBay.objects.all(),
976 query_params={
977 'device_id': '$device',
978 },
979 context={
980 'disabled': '_occupied',
981 },
982 )
983 module_type = DynamicModelChoiceField(
984 label=_('Module type'),
985 queryset=ModuleType.objects.all(),
986 context={
987 'parent': 'manufacturer',
988 },
989 selector=True
990 )
991 replicate_components = forms.BooleanField(
992 label=_('Replicate components'),
993 required=False,
994 initial=True,
995 help_text=_("Automatically populate components associated with this module type")
996 )
997 adopt_components = forms.BooleanField(
998 label=_('Adopt components'),
999 required=False,
1000 initial=False,
1001 help_text=_("Adopt already existing components")
1002 )
1004 fieldsets = (
1005 FieldSet('device', 'module_bay', 'module_type', 'status', 'description', 'tags', name=_('Module')),
1006 FieldSet('serial', 'asset_tag', 'replicate_components', 'adopt_components', name=_('Hardware')),
1007 )
1009 class Meta:
1010 model = Module
1011 fields = [
1012 'device', 'module_bay', 'module_type', 'status', 'serial', 'asset_tag', 'tags', 'replicate_components',
1013 'adopt_components', 'description', 'owner', 'comments',
1014 ]
1016 def __init__(self, *args, **kwargs):
1017 super().__init__(*args, **kwargs)
1019 if self.instance.pk:
1020 self.fields['replicate_components'].initial = False
1021 self.fields['replicate_components'].disabled = True
1022 self.fields['adopt_components'].initial = False
1023 self.fields['adopt_components'].disabled = True
1026def get_termination_type_choices():
1027 return add_blank_choice([
1028 Choice(f'{ct.app_label}.{ct.model}', ct.model_class()._meta.verbose_name.title())
1029 for ct in ContentType.objects.filter(CABLE_TERMINATION_MODELS)
1030 ])
1033class CableBundleForm(PrimaryModelForm):
1035 fieldsets = (
1036 FieldSet('name', 'description', 'tags', name=_('Cable Bundle')),
1037 )
1039 class Meta:
1040 model = CableBundle
1041 fields = ['name', 'description', 'owner', 'comments', 'tags']
1044class CableForm(TenancyForm, PrimaryModelForm):
1045 type = TypedChoiceField(
1046 label=_('Type'),
1047 choices=add_blank_choice(CableTypeChoices),
1048 required=False,
1049 )
1050 status = ChoiceField(
1051 label=_('Status'),
1052 choices=LinkStatusChoices,
1053 initial=LinkStatusChoices.STATUS_CONNECTED,
1054 )
1055 profile = ChoiceField(
1056 label=_('Profile'),
1057 choices=add_blank_choice(CableProfileChoices),
1058 required=False,
1059 )
1060 length_unit = TypedChoiceField(
1061 label=_('Length unit'),
1062 choices=add_blank_choice(CableLengthUnitChoices),
1063 required=False,
1064 )
1065 a_terminations_type = ChoiceField(
1066 choices=get_termination_type_choices,
1067 required=False,
1068 widget=HTMXSelect(hx_target_id='cable-side-a'),
1069 label=_('Type')
1070 )
1071 b_terminations_type = ChoiceField(
1072 choices=get_termination_type_choices,
1073 required=False,
1074 widget=HTMXSelect(hx_target_id='cable-side-b'),
1075 label=_('Type')
1076 )
1077 bundle = DynamicModelChoiceField(
1078 queryset=CableBundle.objects.all(),
1079 required=False,
1080 label=_('Bundle'),
1081 )
1083 class Meta:
1084 model = Cable
1085 fields = [
1086 'a_terminations_type', 'b_terminations_type', 'type', 'status', 'profile', 'tenant_group', 'tenant',
1087 'bundle', 'label', 'color', 'length', 'length_unit', 'description', 'owner', 'comments', 'tags',
1088 ]
1091class PowerPanelForm(PrimaryModelForm):
1092 site = DynamicModelChoiceField(
1093 label=_('Site'),
1094 queryset=Site.objects.all(),
1095 selector=True
1096 )
1097 location = DynamicModelChoiceField(
1098 label=_('Location'),
1099 queryset=Location.objects.all(),
1100 required=False,
1101 query_params={
1102 'site_id': '$site'
1103 }
1104 )
1106 fieldsets = (
1107 FieldSet('site', 'location', 'name', 'description', 'tags', name=_('Power Panel')),
1108 )
1110 class Meta:
1111 model = PowerPanel
1112 fields = [
1113 'site', 'location', 'name', 'description', 'owner', 'comments', 'tags',
1114 ]
1117class PowerFeedForm(TenancyForm, PrimaryModelForm):
1118 status = ChoiceField(
1119 label=_('Status'),
1120 choices=PowerFeedStatusChoices,
1121 initial=PowerFeedStatusChoices.STATUS_ACTIVE,
1122 )
1123 type = ChoiceField(
1124 label=_('Type'),
1125 choices=PowerFeedTypeChoices,
1126 initial=PowerFeedTypeChoices.TYPE_PRIMARY,
1127 )
1128 supply = ChoiceField(
1129 label=_('Supply'),
1130 choices=PowerFeedSupplyChoices,
1131 initial=PowerFeedSupplyChoices.SUPPLY_AC,
1132 )
1133 phase = ChoiceField(
1134 label=_('Phase'),
1135 choices=PowerFeedPhaseChoices,
1136 initial=PowerFeedPhaseChoices.PHASE_SINGLE,
1137 )
1138 power_panel = DynamicModelChoiceField(
1139 label=_('Power panel'),
1140 queryset=PowerPanel.objects.all(),
1141 selector=True,
1142 quick_add=True
1143 )
1144 rack = DynamicModelChoiceField(
1145 label=_('Rack'),
1146 queryset=Rack.objects.all(),
1147 required=False,
1148 selector=True
1149 )
1151 fieldsets = (
1152 FieldSet(
1153 'power_panel', 'rack', 'name', 'status', 'type', 'description', 'mark_connected', 'tags',
1154 name=_('Power Feed')
1155 ),
1156 FieldSet('supply', 'voltage', 'amperage', 'phase', 'max_utilization', name=_('Characteristics')),
1157 FieldSet('tenant_group', 'tenant', name=_('Tenancy')),
1158 )
1160 class Meta:
1161 model = PowerFeed
1162 fields = [
1163 'power_panel', 'rack', 'name', 'status', 'type', 'mark_connected', 'supply', 'phase', 'voltage', 'amperage',
1164 'max_utilization', 'tenant_group', 'tenant', 'description', 'owner', 'comments', 'tags'
1165 ]
1168#
1169# Cooling
1170#
1172class CoolingSourceForm(PrimaryModelForm):
1173 site = DynamicModelChoiceField(
1174 label=_('Site'),
1175 queryset=Site.objects.all(),
1176 selector=True
1177 )
1178 location = DynamicModelChoiceField(
1179 label=_('Location'),
1180 queryset=Location.objects.all(),
1181 required=False,
1182 query_params={
1183 'site_id': '$site'
1184 }
1185 )
1187 fieldsets = (
1188 FieldSet('site', 'location', 'name', 'type', 'status', 'description', 'tags', name=_('Cooling Source')),
1189 FieldSet('fluid_type', 'cooling_capacity', name=_('Characteristics')),
1190 )
1192 class Meta:
1193 model = CoolingSource
1194 fields = [
1195 'site', 'location', 'name', 'type', 'status', 'fluid_type', 'cooling_capacity', 'description', 'owner',
1196 'comments', 'tags',
1197 ]
1200class CoolingFeedForm(TenancyForm, PrimaryModelForm):
1201 cooling_source = DynamicModelChoiceField(
1202 label=_('Cooling source'),
1203 queryset=CoolingSource.objects.all(),
1204 selector=True,
1205 quick_add=True
1206 )
1207 rack = DynamicModelChoiceField(
1208 label=_('Rack'),
1209 queryset=Rack.objects.all(),
1210 required=False,
1211 selector=True
1212 )
1214 fieldsets = (
1215 FieldSet(
1216 'cooling_source', 'rack', 'name', 'status', 'description', 'tags',
1217 name=_('Cooling Feed')
1218 ),
1219 FieldSet(
1220 'cooling_capacity',
1221 InlineFields('max_flow', 'max_flow_unit', label=_('Max flow')),
1222 name=_('Characteristics')
1223 ),
1224 FieldSet('tenant_group', 'tenant', name=_('Tenancy')),
1225 )
1227 class Meta:
1228 model = CoolingFeed
1229 fields = [
1230 'cooling_source', 'rack', 'name', 'status', 'cooling_capacity',
1231 'max_flow', 'max_flow_unit', 'tenant_group', 'tenant', 'description', 'owner', 'comments',
1232 'tags',
1233 ]
1236#
1237# Virtual chassis
1238#
1240class VirtualChassisForm(PrimaryModelForm):
1241 master = forms.ModelChoiceField(
1242 label=_('Master'),
1243 queryset=Device.objects.all(),
1244 required=False,
1245 widget=SelectWithPK(),
1246 )
1248 class Meta:
1249 model = VirtualChassis
1250 fields = [
1251 'name', 'domain', 'master', 'description', 'owner', 'comments', 'tags',
1252 ]
1254 def __init__(self, *args, **kwargs):
1255 super().__init__(*args, **kwargs)
1257 self.fields['master'].queryset = Device.objects.filter(virtual_chassis=self.instance)
1260class DeviceVCMembershipForm(forms.ModelForm):
1261 class Meta:
1262 model = Device
1263 fields = [
1264 'vc_position', 'vc_priority',
1265 ]
1266 labels = {
1267 'vc_position': 'Position',
1268 'vc_priority': 'Priority',
1269 }
1271 def __init__(self, validate_vc_position=False, *args, **kwargs):
1272 super().__init__(*args, **kwargs)
1274 # Require VC position (only required when the Device is a VirtualChassis member)
1275 self.fields['vc_position'].required = True
1277 # Add bootstrap classes to form elements.
1278 self.fields['vc_position'].widget.attrs = {'class': 'form-control'}
1279 self.fields['vc_priority'].widget.attrs = {'class': 'form-control'}
1281 # Validation of vc_position is optional. This is only required when adding a new member to an existing
1282 # VirtualChassis. Otherwise, vc_position validation is handled by BaseVCMemberFormSet.
1283 self.validate_vc_position = validate_vc_position
1285 def clean_vc_position(self):
1286 vc_position = self.cleaned_data['vc_position']
1288 if self.validate_vc_position:
1289 conflicting_members = Device.objects.filter(
1290 virtual_chassis=self.instance.virtual_chassis,
1291 vc_position=vc_position
1292 )
1293 if conflicting_members.exists():
1294 raise forms.ValidationError(
1295 'A virtual chassis member already exists in position {}.'.format(vc_position)
1296 )
1298 return vc_position
1301class VCMemberSelectForm(forms.Form):
1302 device = DynamicModelChoiceField(
1303 label=_('Device'),
1304 queryset=Device.objects.all(),
1305 query_params={
1306 'virtual_chassis_id': 'null',
1307 },
1308 selector=True
1309 )
1311 def clean_device(self):
1312 device = self.cleaned_data['device']
1313 if device.virtual_chassis is not None:
1314 raise forms.ValidationError(
1315 f"Device {device} is already assigned to a virtual chassis."
1316 )
1317 return device
1320#
1321# Device component templates
1322#
1324class ComponentTemplateForm(ChangelogMessageMixin, forms.ModelForm):
1325 device_type = DynamicModelChoiceField(
1326 label=_('Device type'),
1327 queryset=DeviceType.objects.all(),
1328 context={
1329 'parent': 'manufacturer',
1330 }
1331 )
1333 def __init__(self, *args, **kwargs):
1334 super().__init__(*args, **kwargs)
1336 # Disable reassignment of DeviceType when editing an existing instance
1337 if self.instance.pk:
1338 self.fields['device_type'].disabled = True
1341class ModularComponentTemplateForm(ComponentTemplateForm):
1342 device_type = DynamicModelChoiceField(
1343 label=_('Device type'),
1344 queryset=DeviceType.objects.all(),
1345 required=False,
1346 context={
1347 'parent': 'manufacturer',
1348 }
1349 )
1350 module_type = DynamicModelChoiceField(
1351 label=_('Module type'),
1352 queryset=ModuleType.objects.all(),
1353 required=False,
1354 context={
1355 'parent': 'manufacturer',
1356 }
1357 )
1359 fieldsets = (
1360 FieldSet(
1361 TabbedGroups(
1362 FieldSet('device_type', name=_('Device Type')),
1363 FieldSet('module_type', name=_('Module Type')),
1364 ),
1365 'name', 'label', 'type', 'description'
1366 ),
1367 )
1369 def __init__(self, *args, **kwargs):
1370 super().__init__(*args, **kwargs)
1372 # Disable reassignment of ModuleType when editing an existing instance
1373 if self.instance.pk:
1374 self.fields['module_type'].disabled = True
1376 # Components attached to a module need to present this standardized substitution help text.
1377 self.fields['name'].help_text = _(
1378 "Alphanumeric ranges are supported for bulk creation. Mixed cases and types within a single range are not "
1379 "supported (example: <code>[ge,xe]-0/0/[0-9]</code>). The token <code>{module}</code>, if present, will be "
1380 "automatically replaced with the position value when creating a new module. "
1381 "The token <code>{vc_position}</code> will be replaced with the device's Virtual Chassis position "
1382 "(use <code>{vc_position:1}</code> to specify a fallback (default is 0))"
1383 )
1386class ConsolePortTemplateForm(ModularComponentTemplateForm):
1387 type = TypedChoiceField(
1388 label=_('Type'),
1389 choices=add_blank_choice(ConsolePortTypeChoices),
1390 required=False,
1391 )
1393 class Meta:
1394 model = ConsolePortTemplate
1395 fields = [
1396 'device_type', 'module_type', 'name', 'label', 'type', 'description',
1397 ]
1400class ConsoleServerPortTemplateForm(ModularComponentTemplateForm):
1401 type = TypedChoiceField(
1402 label=_('Type'),
1403 choices=add_blank_choice(ConsolePortTypeChoices),
1404 required=False,
1405 )
1407 class Meta:
1408 model = ConsoleServerPortTemplate
1409 fields = [
1410 'device_type', 'module_type', 'name', 'label', 'type', 'description',
1411 ]
1414class PowerPortTemplateForm(ModularComponentTemplateForm):
1415 type = TypedChoiceField(
1416 label=_('Type'),
1417 choices=add_blank_choice(PowerPortTypeChoices),
1418 required=False,
1419 )
1420 fieldsets = (
1421 FieldSet(
1422 TabbedGroups(
1423 FieldSet('device_type', name=_('Device Type')),
1424 FieldSet('module_type', name=_('Module Type')),
1425 ),
1426 'name', 'label', 'type', 'maximum_draw', 'allocated_draw', 'description',
1427 ),
1428 )
1430 class Meta:
1431 model = PowerPortTemplate
1432 fields = [
1433 'device_type', 'module_type', 'name', 'label', 'type', 'maximum_draw', 'allocated_draw', 'description',
1434 ]
1437class PowerOutletTemplateForm(ModularComponentTemplateForm):
1438 type = TypedChoiceField(
1439 label=_('Type'),
1440 choices=add_blank_choice(PowerOutletTypeChoices),
1441 required=False,
1442 )
1443 feed_leg = TypedChoiceField(
1444 label=_('Feed leg'),
1445 choices=add_blank_choice(PowerOutletFeedLegChoices),
1446 required=False,
1447 help_text=_('Phase (for three-phase feeds)'),
1448 )
1449 power_port = DynamicModelChoiceField(
1450 label=_('Power port'),
1451 queryset=PowerPortTemplate.objects.all(),
1452 required=False,
1453 query_params={
1454 'device_type_id': '$device_type',
1455 }
1456 )
1458 fieldsets = (
1459 FieldSet(
1460 TabbedGroups(
1461 FieldSet('device_type', name=_('Device Type')),
1462 FieldSet('module_type', name=_('Module Type')),
1463 ),
1464 'name', 'label', 'type', 'color', 'power_port', 'feed_leg', 'description',
1465 ),
1466 )
1468 class Meta:
1469 model = PowerOutletTemplate
1470 fields = [
1471 'device_type', 'module_type', 'name', 'label', 'type', 'color', 'power_port', 'feed_leg', 'description',
1472 ]
1475class CoolingIntakeTemplateForm(ModularComponentTemplateForm):
1476 fieldsets = (
1477 FieldSet(
1478 TabbedGroups(
1479 FieldSet('device_type', name=_('Device Type')),
1480 FieldSet('module_type', name=_('Module Type')),
1481 ),
1482 'name', 'label', 'type',
1483 InlineFields('diameter', 'diameter_unit', label=_('Diameter')),
1484 InlineFields('max_flow', 'max_flow_unit', label=_('Max flow')),
1485 'description',
1486 ),
1487 )
1489 class Meta:
1490 model = CoolingIntakeTemplate
1491 fields = [
1492 'device_type', 'module_type', 'name', 'label', 'type', 'diameter', 'diameter_unit',
1493 'max_flow', 'max_flow_unit', 'description',
1494 ]
1497class CoolingOutflowTemplateForm(ModularComponentTemplateForm):
1498 cooling_intake = DynamicModelChoiceField(
1499 label=_('Cooling intake'),
1500 queryset=CoolingIntakeTemplate.objects.all(),
1501 required=False,
1502 query_params={
1503 'device_type_id': '$device_type',
1504 }
1505 )
1507 fieldsets = (
1508 FieldSet(
1509 TabbedGroups(
1510 FieldSet('device_type', name=_('Device Type')),
1511 FieldSet('module_type', name=_('Module Type')),
1512 ),
1513 'name', 'label', 'type',
1514 InlineFields('diameter', 'diameter_unit', label=_('Diameter')),
1515 'cooling_intake', 'description',
1516 ),
1517 )
1519 class Meta:
1520 model = CoolingOutflowTemplate
1521 fields = [
1522 'device_type', 'module_type', 'name', 'label', 'type', 'diameter', 'diameter_unit',
1523 'cooling_intake', 'description',
1524 ]
1527class InterfaceTemplateForm(ModularComponentTemplateForm):
1528 type = ChoiceField(
1529 label=_('Type'),
1530 choices=InterfaceTypeChoices,
1531 )
1532 poe_mode = TypedChoiceField(
1533 label=_('PoE mode'),
1534 choices=add_blank_choice(InterfacePoEModeChoices),
1535 required=False,
1536 )
1537 poe_type = TypedChoiceField(
1538 label=_('PoE type'),
1539 choices=add_blank_choice(InterfacePoETypeChoices),
1540 required=False,
1541 )
1542 rf_role = TypedChoiceField(
1543 label=_('Wireless role'),
1544 choices=add_blank_choice(WirelessRoleChoices),
1545 required=False,
1546 )
1547 parent = DynamicModelChoiceField(
1548 label=_('Parent'),
1549 queryset=InterfaceTemplate.objects.all(),
1550 required=False,
1551 query_params={
1552 'device_type_id': '$device_type',
1553 'module_type_id': '$module_type',
1554 }
1555 )
1556 bridge = DynamicModelChoiceField(
1557 label=_('Bridge'),
1558 queryset=InterfaceTemplate.objects.all(),
1559 required=False,
1560 query_params={
1561 'device_type_id': '$device_type',
1562 'module_type_id': '$module_type',
1563 }
1564 )
1566 fieldsets = (
1567 FieldSet(
1568 TabbedGroups(
1569 FieldSet('device_type', name=_('Device Type')),
1570 FieldSet('module_type', name=_('Module Type')),
1571 ),
1572 'name', 'label', 'type', 'channels', 'channel_id', 'enabled', 'mgmt_only', 'description', 'parent',
1573 'bridge',
1574 ),
1575 FieldSet('poe_mode', 'poe_type', name=_('PoE')),
1576 FieldSet('rf_role', name=_('Wireless')),
1577 )
1579 class Meta:
1580 model = InterfaceTemplate
1581 fields = [
1582 'device_type', 'module_type', 'name', 'label', 'type', 'channels', 'channel_id', 'mgmt_only', 'enabled',
1583 'description', 'poe_mode', 'poe_type', 'parent', 'bridge', 'rf_role',
1584 ]
1587class FrontPortTemplateForm(FrontPortFormMixin, ModularComponentTemplateForm):
1588 type = ChoiceField(
1589 label=_('Type'),
1590 choices=PortTypeChoices,
1591 )
1592 fieldsets = (
1593 FieldSet(
1594 TabbedGroups(
1595 FieldSet('device_type', name=_('Device Type')),
1596 FieldSet('module_type', name=_('Module Type')),
1597 ),
1598 'name', 'label', 'type', 'color', 'positions', 'rear_ports', 'description',
1599 ),
1600 )
1602 port_mapping_model = PortTemplateMapping
1603 rear_port_model = RearPortTemplate
1605 class Meta:
1606 model = FrontPortTemplate
1607 fields = [
1608 'device_type', 'module_type', 'name', 'label', 'type', 'color', 'positions', 'description',
1609 ]
1611 def __init__(self, *args, **kwargs):
1612 super().__init__(*args, **kwargs)
1614 # Populate rear port choices based on parent DeviceType or ModuleType
1615 if device_type_id := self.data.get('device_type') or self.initial.get('device_type'):
1616 parent_filter = Q(device_type=device_type_id)
1617 elif module_type_id := self.data.get('module_type') or self.initial.get('module_type'):
1618 parent_filter = Q(module_type=module_type_id)
1619 else:
1620 return
1621 self.fields['rear_ports'].choices = self._get_rear_port_choices(parent_filter, self.instance)
1623 # Set initial rear port mappings
1624 if self.instance.pk:
1625 self.initial['rear_ports'] = [
1626 f'{mapping.rear_port_id}:{mapping.rear_port_position}'
1627 for mapping in PortTemplateMapping.objects.filter(front_port_id=self.instance.pk)
1628 ]
1631class RearPortTemplateForm(ModularComponentTemplateForm):
1632 type = ChoiceField(
1633 label=_('Type'),
1634 choices=PortTypeChoices,
1635 )
1636 fieldsets = (
1637 FieldSet(
1638 TabbedGroups(
1639 FieldSet('device_type', name=_('Device Type')),
1640 FieldSet('module_type', name=_('Module Type')),
1641 ),
1642 'name', 'label', 'type', 'color', 'positions', 'description',
1643 ),
1644 )
1646 class Meta:
1647 model = RearPortTemplate
1648 fields = [
1649 'device_type', 'module_type', 'name', 'label', 'type', 'color', 'positions', 'description',
1650 ]
1653class ModuleBayTemplateForm(ModularComponentTemplateForm):
1654 module_bay_types = DynamicModelMultipleChoiceField(
1655 label=_('Module bay types'),
1656 queryset=ModuleBayType.objects.all(),
1657 required=False,
1658 )
1660 fieldsets = (
1661 FieldSet(
1662 TabbedGroups(
1663 FieldSet('device_type', name=_('Device Type')),
1664 FieldSet('module_type', name=_('Module Type')),
1665 ),
1666 'name', 'label', 'position', 'enabled', 'description', 'module_bay_types',
1667 ),
1668 )
1670 class Meta:
1671 model = ModuleBayTemplate
1672 fields = [
1673 'device_type', 'module_type', 'name', 'label', 'position', 'enabled', 'description', 'module_bay_types',
1674 ]
1677class DeviceBayTemplateForm(ComponentTemplateForm):
1678 fieldsets = (
1679 FieldSet('device_type', 'name', 'label', 'enabled', 'description'),
1680 )
1682 class Meta:
1683 model = DeviceBayTemplate
1684 fields = [
1685 'device_type', 'name', 'label', 'enabled', 'description',
1686 ]
1689class InventoryItemTemplateForm(ComponentTemplateForm):
1690 parent = DynamicModelChoiceField(
1691 label=_('Parent'),
1692 queryset=InventoryItemTemplate.objects.all(),
1693 required=False,
1694 query_params={
1695 'device_type_id': '$device_type'
1696 }
1697 )
1698 role = DynamicModelChoiceField(
1699 label=_('Role'),
1700 queryset=InventoryItemRole.objects.all(),
1701 required=False
1702 )
1703 manufacturer = DynamicModelChoiceField(
1704 label=_('Manufacturer'),
1705 queryset=Manufacturer.objects.all(),
1706 required=False
1707 )
1709 # Assigned component selectors
1710 consoleporttemplate = DynamicModelChoiceField(
1711 queryset=ConsolePortTemplate.objects.all(),
1712 required=False,
1713 query_params={
1714 'device_type_id': '$device_type'
1715 },
1716 label=_('Console port template')
1717 )
1718 consoleserverporttemplate = DynamicModelChoiceField(
1719 queryset=ConsoleServerPortTemplate.objects.all(),
1720 required=False,
1721 query_params={
1722 'device_type_id': '$device_type'
1723 },
1724 label=_('Console server port template')
1725 )
1726 frontporttemplate = DynamicModelChoiceField(
1727 queryset=FrontPortTemplate.objects.all(),
1728 required=False,
1729 query_params={
1730 'device_type_id': '$device_type'
1731 },
1732 label=_('Front port template')
1733 )
1734 interfacetemplate = DynamicModelChoiceField(
1735 queryset=InterfaceTemplate.objects.all(),
1736 required=False,
1737 query_params={
1738 'device_type_id': '$device_type'
1739 },
1740 label=_('Interface template')
1741 )
1742 poweroutlettemplate = DynamicModelChoiceField(
1743 queryset=PowerOutletTemplate.objects.all(),
1744 required=False,
1745 query_params={
1746 'device_type_id': '$device_type'
1747 },
1748 label=_('Power outlet template')
1749 )
1750 powerporttemplate = DynamicModelChoiceField(
1751 queryset=PowerPortTemplate.objects.all(),
1752 required=False,
1753 query_params={
1754 'device_type_id': '$device_type'
1755 },
1756 label=_('Power port template')
1757 )
1758 rearporttemplate = DynamicModelChoiceField(
1759 queryset=RearPortTemplate.objects.all(),
1760 required=False,
1761 query_params={
1762 'device_type_id': '$device_type'
1763 },
1764 label=_('Rear port template')
1765 )
1767 fieldsets = (
1768 FieldSet(
1769 'device_type', 'parent', 'name', 'label', 'role', 'manufacturer', 'part_id', 'description',
1770 ),
1771 FieldSet(
1772 TabbedGroups(
1773 FieldSet('interfacetemplate', name=_('Interface')),
1774 FieldSet('consoleporttemplate', name=_('Console Port')),
1775 FieldSet('consoleserverporttemplate', name=_('Console Server Port')),
1776 FieldSet('frontporttemplate', name=_('Front Port')),
1777 FieldSet('rearporttemplate', name=_('Rear Port')),
1778 FieldSet('powerporttemplate', name=_('Power Port')),
1779 FieldSet('poweroutlettemplate', name=_('Power Outlet')),
1780 ),
1781 name=_('Component Assignment')
1782 )
1783 )
1785 class Meta:
1786 model = InventoryItemTemplate
1787 fields = [
1788 'device_type', 'parent', 'name', 'label', 'role', 'manufacturer', 'part_id', 'description',
1789 ]
1791 def __init__(self, *args, **kwargs):
1792 instance = kwargs.get('instance')
1793 initial = kwargs.get('initial', {}).copy()
1794 component_type = initial.get('component_type')
1795 component_id = initial.get('component_id')
1797 if instance:
1798 # When editing set the initial value for component selection
1799 for component_model in ContentType.objects.filter(MODULAR_COMPONENT_TEMPLATE_MODELS):
1800 if type(instance.component) is component_model.model_class():
1801 initial[component_model.model] = instance.component
1802 break
1803 elif component_type and component_id:
1804 # When adding the InventoryItem from a component page
1805 content_type = ContentType.objects.filter(
1806 MODULAR_COMPONENT_TEMPLATE_MODELS
1807 ).filter(pk=component_type).first()
1808 if content_type:
1809 if component := content_type.model_class().objects.filter(pk=component_id).first():
1810 initial[content_type.model] = component
1812 kwargs['initial'] = initial
1814 super().__init__(*args, **kwargs)
1816 def clean(self):
1817 super().clean()
1819 # Handle object assignment
1820 selected_objects = [
1821 field for field in (
1822 'consoleporttemplate', 'consoleserverporttemplate', 'frontporttemplate', 'interfacetemplate',
1823 'poweroutlettemplate', 'powerporttemplate', 'rearporttemplate'
1824 ) if self.cleaned_data[field]
1825 ]
1826 if len(selected_objects) > 1:
1827 raise forms.ValidationError(_("An InventoryItem can only be assigned to a single component."))
1828 if selected_objects:
1829 self.instance.component = self.cleaned_data[selected_objects[0]]
1830 else:
1831 self.instance.component = None
1834#
1835# Device components
1836#
1838class DeviceComponentForm(OwnerMixin, NetBoxModelForm):
1839 device = DynamicModelChoiceField(
1840 label=_('Device'),
1841 queryset=Device.objects.all(),
1842 selector=True
1843 )
1845 def __init__(self, *args, **kwargs):
1846 super().__init__(*args, **kwargs)
1848 # Disable reassignment of Device when editing an existing instance
1849 if self.instance.pk:
1850 self.fields['device'].disabled = True
1853class ModularDeviceComponentForm(DeviceComponentForm):
1854 module = DynamicModelChoiceField(
1855 label=_('Module'),
1856 queryset=Module.objects.all(),
1857 required=False,
1858 query_params={
1859 'device_id': '$device',
1860 }
1861 )
1864class ConsolePortForm(ModularDeviceComponentForm):
1865 type = TypedChoiceField(
1866 label=_('Type'),
1867 choices=add_blank_choice(ConsolePortTypeChoices),
1868 required=False,
1869 help_text=_('Physical port type'),
1870 )
1871 fieldsets = (
1872 FieldSet(
1873 'device', 'module', 'name', 'label', 'type', 'speed', 'mark_connected', 'description', 'tags',
1874 ),
1875 )
1877 class Meta:
1878 model = ConsolePort
1879 fields = [
1880 'device', 'module', 'name', 'label', 'type', 'speed', 'mark_connected', 'description', 'owner', 'tags',
1881 ]
1884class ConsoleServerPortForm(ModularDeviceComponentForm):
1885 type = TypedChoiceField(
1886 label=_('Type'),
1887 choices=add_blank_choice(ConsolePortTypeChoices),
1888 required=False,
1889 help_text=_('Physical port type'),
1890 )
1891 fieldsets = (
1892 FieldSet(
1893 'device', 'module', 'name', 'label', 'type', 'speed', 'mark_connected', 'description', 'tags',
1894 ),
1895 )
1897 class Meta:
1898 model = ConsoleServerPort
1899 fields = [
1900 'device', 'module', 'name', 'label', 'type', 'speed', 'mark_connected', 'description', 'owner', 'tags',
1901 ]
1904class PowerPortForm(ModularDeviceComponentForm):
1905 type = TypedChoiceField(
1906 label=_('Type'),
1907 choices=add_blank_choice(PowerPortTypeChoices),
1908 required=False,
1909 help_text=_('Physical port type'),
1910 )
1911 fieldsets = (
1912 FieldSet(
1913 'device', 'module', 'name', 'label', 'type', 'maximum_draw', 'allocated_draw', 'mark_connected',
1914 'description', 'tags',
1915 ),
1916 )
1918 class Meta:
1919 model = PowerPort
1920 fields = [
1921 'device', 'module', 'name', 'label', 'type', 'maximum_draw', 'allocated_draw', 'mark_connected',
1922 'description', 'owner', 'tags',
1923 ]
1926class PowerOutletForm(ModularDeviceComponentForm):
1927 type = TypedChoiceField(
1928 label=_('Type'),
1929 choices=add_blank_choice(PowerOutletTypeChoices),
1930 required=False,
1931 help_text=_('Physical port type'),
1932 )
1933 status = ChoiceField(
1934 label=_('Status'),
1935 choices=PowerOutletStatusChoices,
1936 initial=PowerOutletStatusChoices.STATUS_ENABLED,
1937 )
1938 feed_leg = TypedChoiceField(
1939 label=_('Feed leg'),
1940 choices=add_blank_choice(PowerOutletFeedLegChoices),
1941 required=False,
1942 help_text=_('Phase (for three-phase feeds)'),
1943 )
1944 power_port = DynamicModelChoiceField(
1945 label=_('Power port'),
1946 queryset=PowerPort.objects.all(),
1947 required=False,
1948 query_params={
1949 'device_id': '$device',
1950 }
1951 )
1953 fieldsets = (
1954 FieldSet(
1955 'device', 'module', 'name', 'label', 'type', 'status', 'color', 'power_port', 'feed_leg', 'mark_connected',
1956 'description', 'tags',
1957 ),
1958 )
1960 class Meta:
1961 model = PowerOutlet
1962 fields = [
1963 'device', 'module', 'name', 'label', 'type', 'status', 'color', 'power_port', 'feed_leg', 'mark_connected',
1964 'description', 'owner', 'tags',
1965 ]
1968class CoolingIntakeForm(ModularDeviceComponentForm):
1969 cooling_outflow = DynamicModelChoiceField(
1970 label=_('Cooling outflow'),
1971 queryset=CoolingOutflow.objects.all(),
1972 required=False,
1973 selector=True
1974 )
1976 fieldsets = (
1977 FieldSet(
1978 'device', 'module', 'name', 'label', 'type',
1979 InlineFields('diameter', 'diameter_unit', label=_('Diameter')),
1980 InlineFields('max_flow', 'max_flow_unit', label=_('Max flow')),
1981 'cooling_outflow', 'description', 'tags',
1982 ),
1983 )
1985 class Meta:
1986 model = CoolingIntake
1987 fields = [
1988 'device', 'module', 'name', 'label', 'type', 'diameter', 'diameter_unit', 'max_flow',
1989 'max_flow_unit', 'cooling_outflow', 'description', 'owner', 'tags',
1990 ]
1993class CoolingOutflowForm(ModularDeviceComponentForm):
1994 cooling_intake = DynamicModelChoiceField(
1995 label=_('Cooling intake'),
1996 queryset=CoolingIntake.objects.all(),
1997 required=False,
1998 query_params={
1999 'device_id': '$device',
2000 }
2001 )
2003 fieldsets = (
2004 FieldSet(
2005 'device', 'module', 'name', 'label', 'type',
2006 InlineFields('diameter', 'diameter_unit', label=_('Diameter')),
2007 'cooling_intake', 'description', 'tags',
2008 ),
2009 )
2011 class Meta:
2012 model = CoolingOutflow
2013 fields = [
2014 'device', 'module', 'name', 'label', 'type', 'diameter', 'diameter_unit', 'cooling_intake', 'description',
2015 'owner', 'tags',
2016 ]
2019class InterfaceForm(InterfaceCommonForm, ModularDeviceComponentForm):
2020 type = ChoiceField(
2021 label=_('Type'),
2022 choices=InterfaceTypeChoices,
2023 )
2024 duplex = TypedChoiceField(
2025 label=_('Duplex'),
2026 choices=add_blank_choice(InterfaceDuplexChoices),
2027 required=False,
2028 )
2029 poe_mode = TypedChoiceField(
2030 label=_('PoE mode'),
2031 choices=add_blank_choice(InterfacePoEModeChoices),
2032 required=False,
2033 )
2034 poe_type = TypedChoiceField(
2035 label=_('PoE type'),
2036 choices=add_blank_choice(InterfacePoETypeChoices),
2037 required=False,
2038 )
2039 mode = TypedChoiceField(
2040 label=_('802.1Q Mode'),
2041 choices=add_blank_choice(InterfaceModeChoices),
2042 required=False,
2043 help_text=_('IEEE 802.1Q tagging strategy'),
2044 widget=HTMXSelect(hx_target_id='dot1q-switching'),
2045 )
2046 rf_role = TypedChoiceField(
2047 label=_('Wireless role'),
2048 choices=add_blank_choice(WirelessRoleChoices),
2049 required=False,
2050 )
2051 rf_channel = TypedChoiceField(
2052 label=_('Wireless channel'),
2053 choices=add_blank_choice(WirelessChannelChoices),
2054 required=False,
2055 )
2056 vdcs = DynamicModelMultipleChoiceField(
2057 queryset=VirtualDeviceContext.objects.all(),
2058 required=False,
2059 label=_('Virtual device contexts'),
2060 initial_params={
2061 'interfaces': '$parent',
2062 },
2063 query_params={
2064 'device_id': '$device',
2065 }
2066 )
2067 parent = DynamicModelChoiceField(
2068 queryset=Interface.objects.all(),
2069 required=False,
2070 label=_('Parent interface'),
2071 query_params={
2072 'virtual_chassis_member_id': '$device',
2073 }
2074 )
2075 bridge = DynamicModelChoiceField(
2076 queryset=Interface.objects.all(),
2077 required=False,
2078 label=_('Bridged interface'),
2079 query_params={
2080 'virtual_chassis_member_id': '$device',
2081 }
2082 )
2083 lag = DynamicModelChoiceField(
2084 queryset=Interface.objects.all(),
2085 required=False,
2086 label=_('LAG interface'),
2087 query_params={
2088 'virtual_chassis_member_id': '$device',
2089 'type': 'lag',
2090 }
2091 )
2092 wireless_lan_group = DynamicModelChoiceField(
2093 queryset=WirelessLANGroup.objects.all(),
2094 required=False,
2095 label=_('Wireless LAN group')
2096 )
2097 wireless_lans = DynamicModelMultipleChoiceField(
2098 queryset=WirelessLAN.objects.all(),
2099 required=False,
2100 label=_('Wireless LANs'),
2101 query_params={
2102 'group_id': '$wireless_lan_group',
2103 }
2104 )
2105 vlan_group = DynamicModelChoiceField(
2106 queryset=VLANGroup.objects.all(),
2107 required=False,
2108 label=_('VLAN group'),
2109 help_text=_("Filter VLANs available for assignment by group.")
2110 )
2111 untagged_vlan = DynamicModelChoiceField(
2112 queryset=VLAN.objects.all(),
2113 required=False,
2114 label=_('Untagged VLAN'),
2115 query_params={
2116 'group_id': '$vlan_group',
2117 'available_on_device': '$device',
2118 }
2119 )
2120 tagged_vlans = DynamicModelMultipleChoiceField(
2121 queryset=VLAN.objects.all(),
2122 required=False,
2123 label=_('Tagged VLANs'),
2124 query_params={
2125 'group_id': '$vlan_group',
2126 'available_on_device': '$device',
2127 }
2128 )
2129 qinq_svlan = DynamicModelChoiceField(
2130 queryset=VLAN.objects.all(),
2131 required=False,
2132 label=_('Q-in-Q Service VLAN'),
2133 query_params={
2134 'group_id': '$vlan_group',
2135 'available_on_device': '$device',
2136 'qinq_role': VLANQinQRoleChoices.ROLE_SERVICE,
2137 }
2138 )
2139 vrf = DynamicModelChoiceField(
2140 queryset=VRF.objects.all(),
2141 required=False,
2142 label=_('VRF')
2143 )
2144 wwn = forms.CharField(
2145 empty_value=None,
2146 required=False,
2147 label=_('WWN')
2148 )
2149 vlan_translation_policy = DynamicModelChoiceField(
2150 queryset=VLANTranslationPolicy.objects.all(),
2151 required=False,
2152 label=_('VLAN Translation Policy')
2153 )
2155 fieldsets = (
2156 FieldSet(
2157 'device', 'module', 'name', 'label', 'type', 'channels', 'speed', 'duplex', 'description', 'tags',
2158 name=_('Interface')
2159 ),
2160 FieldSet('vrf', 'mac_address', 'wwn', name=_('Addressing')),
2161 FieldSet('vdcs', 'mtu', 'tx_power', 'enabled', 'mgmt_only', 'mark_connected', name=_('Operation')),
2162 FieldSet('parent', 'channel_id', 'bridge', 'lag', name=_('Related Interfaces')),
2163 FieldSet('poe_mode', 'poe_type', name=_('PoE')),
2164 FieldSet(
2165 'mode', 'vlan_group', 'untagged_vlan', 'tagged_vlans', 'qinq_svlan', 'vlan_translation_policy',
2166 name=_('802.1Q Switching'),
2167 html_id='dot1q-switching',
2168 ),
2169 FieldSet(
2170 'rf_role', 'rf_channel', 'rf_channel_frequency', 'rf_channel_width', 'wireless_lan_group', 'wireless_lans',
2171 name=_('Wireless')
2172 ),
2173 )
2175 class Meta:
2176 model = Interface
2177 fields = [
2178 'device', 'module', 'vdcs', 'name', 'label', 'type', 'channels', 'channel_id', 'speed', 'duplex',
2179 'enabled', 'parent', 'bridge', 'lag', 'wwn', 'mtu', 'mgmt_only', 'mark_connected', 'description',
2180 'poe_mode', 'poe_type', 'mode', 'rf_role', 'rf_channel', 'rf_channel_frequency', 'rf_channel_width',
2181 'tx_power', 'wireless_lans', 'untagged_vlan', 'tagged_vlans', 'qinq_svlan', 'vlan_translation_policy',
2182 'vrf', 'owner', 'tags',
2183 ]
2184 widgets = {
2185 'speed': NumberWithOptions(
2186 options=InterfaceSpeedChoices
2187 ),
2188 }
2191class FrontPortForm(FrontPortFormMixin, ModularDeviceComponentForm):
2192 type = ChoiceField(
2193 label=_('Type'),
2194 choices=PortTypeChoices,
2195 )
2196 fieldsets = (
2197 FieldSet(
2198 'device', 'module', 'name', 'label', 'type', 'color', 'positions', 'rear_ports', 'mark_connected',
2199 'description', 'tags',
2200 ),
2201 )
2203 port_mapping_model = PortMapping
2204 rear_port_model = RearPort
2206 class Meta:
2207 model = FrontPort
2208 fields = [
2209 'device', 'module', 'name', 'label', 'type', 'color', 'positions', 'mark_connected', 'description', 'owner',
2210 'tags',
2211 ]
2213 def __init__(self, *args, **kwargs):
2214 super().__init__(*args, **kwargs)
2216 # Populate rear port choices
2217 if device_id := self.data.get('device') or self.initial.get('device'):
2218 parent_filter = Q(device=device_id)
2219 else:
2220 return
2221 self.fields['rear_ports'].choices = self._get_rear_port_choices(parent_filter, self.instance)
2223 # Set initial rear port mappings
2224 if self.instance.pk:
2225 self.initial['rear_ports'] = [
2226 f'{mapping.rear_port_id}:{mapping.rear_port_position}'
2227 for mapping in PortMapping.objects.filter(front_port_id=self.instance.pk)
2228 ]
2231class RearPortForm(ModularDeviceComponentForm):
2232 type = ChoiceField(
2233 label=_('Type'),
2234 choices=PortTypeChoices,
2235 )
2236 fieldsets = (
2237 FieldSet(
2238 'device', 'module', 'name', 'label', 'type', 'color', 'positions', 'mark_connected', 'description', 'tags',
2239 ),
2240 )
2242 class Meta:
2243 model = RearPort
2244 fields = [
2245 'device', 'module', 'name', 'label', 'type', 'color', 'positions', 'mark_connected', 'description', 'owner',
2246 'tags',
2247 ]
2250class ModuleBayForm(ModularDeviceComponentForm):
2251 module_bay_types = DynamicModelMultipleChoiceField(
2252 label=_('Module bay types'),
2253 queryset=ModuleBayType.objects.all(),
2254 required=False,
2255 )
2257 fieldsets = (
2258 FieldSet('device', 'module', 'name', 'label', 'position', 'enabled', 'description', 'module_bay_types', 'tags'),
2259 )
2261 class Meta:
2262 model = ModuleBay
2263 fields = [
2264 'device', 'module', 'name', 'label', 'position', 'enabled', 'description', 'module_bay_types', 'owner',
2265 'tags',
2266 ]
2269class DeviceBayForm(DeviceComponentForm):
2270 fieldsets = (
2271 FieldSet('device', 'name', 'label', 'enabled', 'description', 'tags',),
2272 )
2274 class Meta:
2275 model = DeviceBay
2276 fields = [
2277 'device', 'name', 'label', 'enabled', 'description', 'owner', 'tags',
2278 ]
2281class PopulateDeviceBayForm(forms.Form):
2282 installed_device = forms.ModelChoiceField(
2283 queryset=Device.objects.all(),
2284 label=_('Child Device'),
2285 help_text=_("Child devices must first be created and assigned to the site and rack of the parent device.")
2286 )
2288 def __init__(self, device_bay, *args, **kwargs):
2289 super().__init__(*args, **kwargs)
2291 self.fields['installed_device'].queryset = Device.objects.filter(
2292 site=device_bay.device.site,
2293 rack=device_bay.device.rack,
2294 parent_bay__isnull=True,
2295 device_type__u_height=0,
2296 device_type__subdevice_role=SubdeviceRoleChoices.ROLE_CHILD
2297 ).exclude(pk=device_bay.device.pk)
2300class InventoryItemForm(DeviceComponentForm):
2301 status = ChoiceField(
2302 label=_('Status'),
2303 choices=InventoryItemStatusChoices,
2304 initial=InventoryItemStatusChoices.STATUS_ACTIVE,
2305 )
2306 parent = DynamicModelChoiceField(
2307 label=_('Parent'),
2308 queryset=InventoryItem.objects.all(),
2309 required=False,
2310 query_params={
2311 'device_id': '$device'
2312 }
2313 )
2314 role = DynamicModelChoiceField(
2315 label=_('Role'),
2316 queryset=InventoryItemRole.objects.all(),
2317 required=False
2318 )
2319 manufacturer = DynamicModelChoiceField(
2320 label=_('Manufacturer'),
2321 queryset=Manufacturer.objects.all(),
2322 required=False
2323 )
2325 # Assigned component selectors
2326 consoleport = DynamicModelChoiceField(
2327 queryset=ConsolePort.objects.all(),
2328 required=False,
2329 query_params={
2330 'device_id': '$device'
2331 },
2332 label=_('Console port')
2333 )
2334 consoleserverport = DynamicModelChoiceField(
2335 queryset=ConsoleServerPort.objects.all(),
2336 required=False,
2337 query_params={
2338 'device_id': '$device'
2339 },
2340 label=_('Console server port')
2341 )
2342 frontport = DynamicModelChoiceField(
2343 queryset=FrontPort.objects.all(),
2344 required=False,
2345 query_params={
2346 'device_id': '$device'
2347 },
2348 label=_('Front port')
2349 )
2350 interface = DynamicModelChoiceField(
2351 queryset=Interface.objects.all(),
2352 required=False,
2353 query_params={
2354 'device_id': '$device'
2355 },
2356 label=_('Interface')
2357 )
2358 poweroutlet = DynamicModelChoiceField(
2359 queryset=PowerOutlet.objects.all(),
2360 required=False,
2361 query_params={
2362 'device_id': '$device'
2363 },
2364 label=_('Power outlet')
2365 )
2366 powerport = DynamicModelChoiceField(
2367 queryset=PowerPort.objects.all(),
2368 required=False,
2369 query_params={
2370 'device_id': '$device'
2371 },
2372 label=_('Power port')
2373 )
2374 rearport = DynamicModelChoiceField(
2375 queryset=RearPort.objects.all(),
2376 required=False,
2377 query_params={
2378 'device_id': '$device'
2379 },
2380 label=_('Rear port')
2381 )
2383 fieldsets = (
2384 FieldSet(
2385 'device', 'parent', 'name', 'label', 'status', 'role', 'description', 'tags',
2386 name=_('Inventory Item')
2387 ),
2388 FieldSet('manufacturer', 'part_id', 'serial', 'asset_tag', name=_('Hardware')),
2389 FieldSet(
2390 TabbedGroups(
2391 FieldSet('interface', name=_('Interface')),
2392 FieldSet('consoleport', name=_('Console Port')),
2393 FieldSet('consoleserverport', name=_('Console Server Port')),
2394 FieldSet('frontport', name=_('Front Port')),
2395 FieldSet('rearport', name=_('Rear Port')),
2396 FieldSet('powerport', name=_('Power Port')),
2397 FieldSet('poweroutlet', name=_('Power Outlet')),
2398 ),
2399 name=_('Component Assignment')
2400 )
2401 )
2403 class Meta:
2404 model = InventoryItem
2405 fields = [
2406 'device', 'parent', 'name', 'label', 'role', 'manufacturer', 'part_id', 'serial', 'asset_tag',
2407 'status', 'description', 'owner', 'tags',
2408 ]
2410 def __init__(self, *args, **kwargs):
2411 instance = kwargs.get('instance')
2412 initial = kwargs.get('initial', {}).copy()
2413 component_type = initial.get('component_type')
2414 component_id = initial.get('component_id')
2416 if instance:
2417 # When editing set the initial value for component selection
2418 for component_model in ContentType.objects.filter(MODULAR_COMPONENT_MODELS):
2419 if type(instance.component) is component_model.model_class():
2420 initial[component_model.model] = instance.component
2421 break
2422 elif component_type and component_id:
2423 # When adding the InventoryItem from a component page
2424 if content_type := ContentType.objects.filter(MODULAR_COMPONENT_MODELS).filter(pk=component_type).first():
2425 if component := content_type.model_class().objects.filter(pk=component_id).first():
2426 initial[content_type.model] = component
2428 kwargs['initial'] = initial
2430 super().__init__(*args, **kwargs)
2432 # Specifically allow editing the device of IntentoryItems
2433 if self.instance.pk:
2434 self.fields['device'].disabled = False
2436 def clean(self):
2437 super().clean()
2439 # Handle object assignment
2440 selected_objects = [
2441 field for field in (
2442 'consoleport', 'consoleserverport', 'frontport', 'interface', 'poweroutlet', 'powerport', 'rearport'
2443 ) if self.cleaned_data[field]
2444 ]
2445 if len(selected_objects) > 1:
2446 raise forms.ValidationError(_("An InventoryItem can only be assigned to a single component."))
2447 if selected_objects:
2448 self.instance.component = self.cleaned_data[selected_objects[0]]
2449 else:
2450 self.instance.component = None
2453class InventoryItemRoleForm(OrganizationalModelForm):
2454 fieldsets = (
2455 FieldSet('name', 'slug', 'color', 'description', 'tags', name=_('Inventory Item Role')),
2456 )
2458 class Meta:
2459 model = InventoryItemRole
2460 fields = [
2461 'name', 'slug', 'color', 'description', 'owner', 'comments', 'tags',
2462 ]
2465class VirtualDeviceContextForm(TenancyForm, PrimaryModelForm):
2466 status = ChoiceField(
2467 label=_('Status'),
2468 choices=VirtualDeviceContextStatusChoices,
2469 )
2470 device = DynamicModelChoiceField(
2471 label=_('Device'),
2472 queryset=Device.objects.all(),
2473 selector=True
2474 )
2475 primary_ip4 = DynamicModelChoiceField(
2476 queryset=IPAddress.objects.all(),
2477 label=_('Primary IPv4'),
2478 required=False,
2479 query_params={
2480 'device_id': '$device',
2481 'family': '4',
2482 }
2483 )
2484 primary_ip6 = DynamicModelChoiceField(
2485 queryset=IPAddress.objects.all(),
2486 label=_('Primary IPv6'),
2487 required=False,
2488 query_params={
2489 'device_id': '$device',
2490 'family': '6',
2491 }
2492 )
2494 fieldsets = (
2495 FieldSet(
2496 'device', 'name', 'status', 'identifier', 'primary_ip4', 'primary_ip6', 'tags',
2497 name=_('Virtual Device Context')
2498 ),
2499 FieldSet('tenant_group', 'tenant', name=_('Tenancy'))
2500 )
2502 class Meta:
2503 model = VirtualDeviceContext
2504 fields = [
2505 'device', 'name', 'status', 'identifier', 'primary_ip4', 'primary_ip6', 'tenant_group', 'tenant', 'owner',
2506 'comments', 'tags'
2507 ]
2510#
2511# Addressing
2512#
2514class MACAddressForm(PrimaryModelForm):
2515 mac_address = forms.CharField(
2516 required=True,
2517 label=_('MAC address')
2518 )
2519 interface = DynamicModelChoiceField(
2520 label=_('Interface'),
2521 queryset=Interface.objects.all(),
2522 required=False,
2523 selector=True,
2524 context={
2525 'parent': 'device',
2526 },
2527 )
2528 vminterface = DynamicModelChoiceField(
2529 label=_('VM Interface'),
2530 queryset=VMInterface.objects.all(),
2531 required=False,
2532 selector=True,
2533 context={
2534 'parent': 'virtual_machine',
2535 },
2536 )
2538 fieldsets = (
2539 FieldSet(
2540 'mac_address', 'description', 'tags',
2541 ),
2542 FieldSet(
2543 TabbedGroups(
2544 FieldSet('interface', name=_('Device')),
2545 FieldSet('vminterface', name=_('Virtual Machine')),
2546 ),
2547 ),
2548 )
2550 class Meta:
2551 model = MACAddress
2552 fields = [
2553 'mac_address', 'interface', 'vminterface', 'description', 'owner', 'comments', 'tags',
2554 ]
2556 def __init__(self, *args, **kwargs):
2558 # Initialize helper selectors
2559 instance = kwargs.get('instance')
2560 initial = kwargs.get('initial', {}).copy()
2561 if instance:
2562 if type(instance.assigned_object) is Interface:
2563 initial['interface'] = instance.assigned_object
2564 elif type(instance.assigned_object) is VMInterface:
2565 initial['vminterface'] = instance.assigned_object
2566 kwargs['initial'] = initial
2568 super().__init__(*args, **kwargs)
2570 if instance and instance.assigned_object and instance.assigned_object.primary_mac_address:
2571 if instance.assigned_object.primary_mac_address.pk == instance.pk:
2572 self.fields['interface'].disabled = True
2573 self.fields['vminterface'].disabled = True
2575 def clean(self):
2576 super().clean()
2578 # Handle object assignment
2579 selected_objects = [
2580 field for field in ('interface', 'vminterface') if self.cleaned_data[field]
2581 ]
2582 if len(selected_objects) > 1:
2583 raise forms.ValidationError({
2584 selected_objects[1]: _("A MAC address can only be assigned to a single object.")
2585 })
2586 if selected_objects:
2587 self.instance.assigned_object = self.cleaned_data[selected_objects[0]]
2588 else:
2589 self.instance.assigned_object = None