model_forms.py 61 KB

1234567891011121314151617181920212223242526272829303132333435363738394041424344454647484950515253545556575859606162636465666768697071727374757677787980818283848586878889909192939495969798991001011021031041051061071081091101111121131141151161171181191201211221231241251261271281291301311321331341351361371381391401411421431441451461471481491501511521531541551561571581591601611621631641651661671681691701711721731741751761771781791801811821831841851861871881891901911921931941951961971981992002012022032042052062072082092102112122132142152162172182192202212222232242252262272282292302312322332342352362372382392402412422432442452462472482492502512522532542552562572582592602612622632642652662672682692702712722732742752762772782792802812822832842852862872882892902912922932942952962972982993003013023033043053063073083093103113123133143153163173183193203213223233243253263273283293303313323333343353363373383393403413423433443453463473483493503513523533543553563573583593603613623633643653663673683693703713723733743753763773783793803813823833843853863873883893903913923933943953963973983994004014024034044054064074084094104114124134144154164174184194204214224234244254264274284294304314324334344354364374384394404414424434444454464474484494504514524534544554564574584594604614624634644654664674684694704714724734744754764774784794804814824834844854864874884894904914924934944954964974984995005015025035045055065075085095105115125135145155165175185195205215225235245255265275285295305315325335345355365375385395405415425435445455465475485495505515525535545555565575585595605615625635645655665675685695705715725735745755765775785795805815825835845855865875885895905915925935945955965975985996006016026036046056066076086096106116126136146156166176186196206216226236246256266276286296306316326336346356366376386396406416426436446456466476486496506516526536546556566576586596606616626636646656666676686696706716726736746756766776786796806816826836846856866876886896906916926936946956966976986997007017027037047057067077087097107117127137147157167177187197207217227237247257267277287297307317327337347357367377387397407417427437447457467477487497507517527537547557567577587597607617627637647657667677687697707717727737747757767777787797807817827837847857867877887897907917927937947957967977987998008018028038048058068078088098108118128138148158168178188198208218228238248258268278288298308318328338348358368378388398408418428438448458468478488498508518528538548558568578588598608618628638648658668678688698708718728738748758768778788798808818828838848858868878888898908918928938948958968978988999009019029039049059069079089099109119129139149159169179189199209219229239249259269279289299309319329339349359369379389399409419429439449459469479489499509519529539549559569579589599609619629639649659669679689699709719729739749759769779789799809819829839849859869879889899909919929939949959969979989991000100110021003100410051006100710081009101010111012101310141015101610171018101910201021102210231024102510261027102810291030103110321033103410351036103710381039104010411042104310441045104610471048104910501051105210531054105510561057105810591060106110621063106410651066106710681069107010711072107310741075107610771078107910801081108210831084108510861087108810891090109110921093109410951096109710981099110011011102110311041105110611071108110911101111111211131114111511161117111811191120112111221123112411251126112711281129113011311132113311341135113611371138113911401141114211431144114511461147114811491150115111521153115411551156115711581159116011611162116311641165116611671168116911701171117211731174117511761177117811791180118111821183118411851186118711881189119011911192119311941195119611971198119912001201120212031204120512061207120812091210121112121213121412151216121712181219122012211222122312241225122612271228122912301231123212331234123512361237123812391240124112421243124412451246124712481249125012511252125312541255125612571258125912601261126212631264126512661267126812691270127112721273127412751276127712781279128012811282128312841285128612871288128912901291129212931294129512961297129812991300130113021303130413051306130713081309131013111312131313141315131613171318131913201321132213231324132513261327132813291330133113321333133413351336133713381339134013411342134313441345134613471348134913501351135213531354135513561357135813591360136113621363136413651366136713681369137013711372137313741375137613771378137913801381138213831384138513861387138813891390139113921393139413951396139713981399140014011402140314041405140614071408140914101411141214131414141514161417141814191420142114221423142414251426142714281429143014311432143314341435143614371438143914401441144214431444144514461447144814491450145114521453145414551456145714581459146014611462146314641465146614671468146914701471147214731474147514761477147814791480148114821483148414851486148714881489149014911492149314941495149614971498149915001501150215031504150515061507150815091510151115121513151415151516151715181519152015211522152315241525152615271528152915301531153215331534153515361537153815391540154115421543154415451546154715481549155015511552155315541555155615571558155915601561156215631564156515661567156815691570157115721573157415751576157715781579158015811582158315841585158615871588158915901591159215931594159515961597159815991600160116021603160416051606160716081609161016111612161316141615161616171618161916201621162216231624162516261627162816291630163116321633163416351636163716381639164016411642164316441645164616471648164916501651165216531654165516561657165816591660166116621663166416651666166716681669167016711672167316741675167616771678167916801681168216831684168516861687168816891690169116921693169416951696169716981699170017011702170317041705170617071708170917101711171217131714171517161717171817191720172117221723172417251726172717281729173017311732173317341735173617371738173917401741174217431744174517461747174817491750175117521753175417551756175717581759176017611762176317641765176617671768176917701771177217731774177517761777177817791780178117821783178417851786178717881789179017911792179317941795179617971798179918001801180218031804180518061807180818091810181118121813181418151816181718181819182018211822182318241825182618271828182918301831183218331834183518361837183818391840184118421843184418451846184718481849185018511852185318541855185618571858185918601861186218631864186518661867186818691870187118721873187418751876187718781879188018811882188318841885188618871888188918901891189218931894189518961897189818991900190119021903190419051906190719081909191019111912191319141915191619171918191919201921192219231924192519261927192819291930193119321933193419351936193719381939194019411942
  1. from django import forms
  2. from django.contrib.contenttypes.models import ContentType
  3. from django.core.validators import EMPTY_VALUES
  4. from django.utils.translation import gettext_lazy as _
  5. from timezone_field import TimeZoneFormField
  6. from dcim.choices import *
  7. from dcim.constants import *
  8. from dcim.models import *
  9. from extras.models import ConfigTemplate
  10. from ipam.choices import VLANQinQRoleChoices
  11. from ipam.models import ASN, IPAddress, VLAN, VLANGroup, VLANTranslationPolicy, VRF
  12. from netbox.forms import NestedGroupModelForm, NetBoxModelForm, OrganizationalModelForm, PrimaryModelForm
  13. from netbox.forms.mixins import ChangelogMessageMixin, OwnerMixin
  14. from tenancy.forms import TenancyForm
  15. from users.models import User
  16. from utilities.forms import add_blank_choice, get_field_value
  17. from utilities.forms.fields import (
  18. DynamicModelChoiceField, DynamicModelMultipleChoiceField, JSONField, NumericArrayField, SlugField,
  19. )
  20. from utilities.forms.rendering import FieldSet, InlineFields, TabbedGroups
  21. from utilities.forms.widgets import APISelect, ClearableFileInput, HTMXSelect, NumberWithOptions, SelectWithPK
  22. from utilities.jsonschema import JSONSchemaProperty
  23. from virtualization.models import Cluster, VMInterface
  24. from wireless.models import WirelessLAN, WirelessLANGroup
  25. from .common import InterfaceCommonForm, ModuleCommonForm
  26. __all__ = (
  27. 'CableForm',
  28. 'ConsolePortForm',
  29. 'ConsolePortTemplateForm',
  30. 'ConsoleServerPortForm',
  31. 'ConsoleServerPortTemplateForm',
  32. 'DeviceBayForm',
  33. 'DeviceBayTemplateForm',
  34. 'DeviceForm',
  35. 'DeviceRoleForm',
  36. 'DeviceTypeForm',
  37. 'DeviceVCMembershipForm',
  38. 'FrontPortForm',
  39. 'FrontPortTemplateForm',
  40. 'InterfaceForm',
  41. 'InterfaceTemplateForm',
  42. 'InventoryItemForm',
  43. 'InventoryItemRoleForm',
  44. 'InventoryItemTemplateForm',
  45. 'LocationForm',
  46. 'MACAddressForm',
  47. 'ManufacturerForm',
  48. 'ModuleForm',
  49. 'ModuleBayForm',
  50. 'ModuleBayTemplateForm',
  51. 'ModuleTypeForm',
  52. 'ModuleTypeProfileForm',
  53. 'PlatformForm',
  54. 'PopulateDeviceBayForm',
  55. 'PowerFeedForm',
  56. 'PowerOutletForm',
  57. 'PowerOutletTemplateForm',
  58. 'PowerPanelForm',
  59. 'PowerPortForm',
  60. 'PowerPortTemplateForm',
  61. 'RackForm',
  62. 'RackReservationForm',
  63. 'RackRoleForm',
  64. 'RackTypeForm',
  65. 'RearPortForm',
  66. 'RearPortTemplateForm',
  67. 'RegionForm',
  68. 'SiteForm',
  69. 'SiteGroupForm',
  70. 'VCMemberSelectForm',
  71. 'VirtualChassisForm',
  72. 'VirtualDeviceContextForm'
  73. )
  74. class RegionForm(NestedGroupModelForm):
  75. parent = DynamicModelChoiceField(
  76. label=_('Parent'),
  77. queryset=Region.objects.all(),
  78. required=False
  79. )
  80. fieldsets = (
  81. FieldSet('parent', 'name', 'slug', 'description', 'tags'),
  82. )
  83. class Meta:
  84. model = Region
  85. fields = (
  86. 'parent', 'name', 'slug', 'description', 'owner', 'tags', 'comments',
  87. )
  88. class SiteGroupForm(NestedGroupModelForm):
  89. parent = DynamicModelChoiceField(
  90. label=_('Parent'),
  91. queryset=SiteGroup.objects.all(),
  92. required=False
  93. )
  94. fieldsets = (
  95. FieldSet('parent', 'name', 'slug', 'description', 'tags'),
  96. )
  97. class Meta:
  98. model = SiteGroup
  99. fields = (
  100. 'parent', 'name', 'slug', 'description', 'owner', 'comments', 'tags',
  101. )
  102. class SiteForm(TenancyForm, PrimaryModelForm):
  103. region = DynamicModelChoiceField(
  104. label=_('Region'),
  105. queryset=Region.objects.all(),
  106. required=False,
  107. quick_add=True
  108. )
  109. group = DynamicModelChoiceField(
  110. label=_('Group'),
  111. queryset=SiteGroup.objects.all(),
  112. required=False,
  113. quick_add=True
  114. )
  115. asns = DynamicModelMultipleChoiceField(
  116. queryset=ASN.objects.all(),
  117. label=_('ASNs'),
  118. required=False
  119. )
  120. slug = SlugField()
  121. time_zone = TimeZoneFormField(
  122. label=_('Time zone'),
  123. choices=add_blank_choice(TimeZoneFormField().choices),
  124. required=False
  125. )
  126. fieldsets = (
  127. FieldSet(
  128. 'name', 'slug', 'status', 'region', 'group', 'facility', 'asns', 'time_zone', 'description', 'tags',
  129. name=_('Site')
  130. ),
  131. FieldSet('tenant_group', 'tenant', name=_('Tenancy')),
  132. FieldSet('physical_address', 'shipping_address', 'latitude', 'longitude', name=_('Contact Info')),
  133. )
  134. class Meta:
  135. model = Site
  136. fields = (
  137. 'name', 'slug', 'status', 'region', 'group', 'tenant_group', 'tenant', 'facility', 'asns', 'time_zone',
  138. 'description', 'physical_address', 'shipping_address', 'latitude', 'longitude', 'owner', 'comments', 'tags',
  139. )
  140. widgets = {
  141. 'physical_address': forms.Textarea(
  142. attrs={
  143. 'rows': 3,
  144. }
  145. ),
  146. 'shipping_address': forms.Textarea(
  147. attrs={
  148. 'rows': 3,
  149. }
  150. ),
  151. }
  152. class LocationForm(TenancyForm, NestedGroupModelForm):
  153. site = DynamicModelChoiceField(
  154. label=_('Site'),
  155. queryset=Site.objects.all(),
  156. selector=True
  157. )
  158. parent = DynamicModelChoiceField(
  159. label=_('Parent'),
  160. queryset=Location.objects.all(),
  161. required=False,
  162. query_params={
  163. 'site_id': '$site'
  164. }
  165. )
  166. fieldsets = (
  167. FieldSet('site', 'parent', 'name', 'slug', 'status', 'facility', 'description', 'tags', name=_('Location')),
  168. FieldSet('tenant_group', 'tenant', name=_('Tenancy')),
  169. )
  170. class Meta:
  171. model = Location
  172. fields = (
  173. 'site', 'parent', 'name', 'slug', 'status', 'description', 'tenant_group', 'tenant', 'facility', 'owner',
  174. 'comments', 'tags',
  175. )
  176. class RackRoleForm(OrganizationalModelForm):
  177. fieldsets = (
  178. FieldSet('name', 'slug', 'color', 'description', 'tags', name=_('Rack Role')),
  179. )
  180. class Meta:
  181. model = RackRole
  182. fields = [
  183. 'name', 'slug', 'color', 'description', 'owner', 'tags',
  184. ]
  185. class RackTypeForm(PrimaryModelForm):
  186. manufacturer = DynamicModelChoiceField(
  187. label=_('Manufacturer'),
  188. queryset=Manufacturer.objects.all(),
  189. quick_add=True
  190. )
  191. slug = SlugField(
  192. label=_('Slug'),
  193. slug_source='model'
  194. )
  195. fieldsets = (
  196. FieldSet('manufacturer', 'model', 'slug', 'description', 'form_factor', 'tags', name=_('Rack Type')),
  197. FieldSet(
  198. 'width', 'u_height',
  199. InlineFields('outer_width', 'outer_height', 'outer_depth', 'outer_unit', label=_('Outer Dimensions')),
  200. InlineFields('weight', 'max_weight', 'weight_unit', label=_('Weight')),
  201. 'mounting_depth', name=_('Dimensions')
  202. ),
  203. FieldSet('starting_unit', 'desc_units', name=_('Numbering')),
  204. )
  205. class Meta:
  206. model = RackType
  207. fields = [
  208. 'manufacturer', 'model', 'slug', 'form_factor', 'width', 'u_height', 'starting_unit', 'desc_units',
  209. 'outer_width', 'outer_height', 'outer_depth', 'outer_unit', 'mounting_depth', 'weight', 'max_weight',
  210. 'weight_unit', 'description', 'owner', 'comments', 'tags',
  211. ]
  212. class RackForm(TenancyForm, PrimaryModelForm):
  213. site = DynamicModelChoiceField(
  214. label=_('Site'),
  215. queryset=Site.objects.all(),
  216. selector=True
  217. )
  218. location = DynamicModelChoiceField(
  219. label=_('Location'),
  220. queryset=Location.objects.all(),
  221. required=False,
  222. query_params={
  223. 'site_id': '$site'
  224. }
  225. )
  226. role = DynamicModelChoiceField(
  227. label=_('Role'),
  228. queryset=RackRole.objects.all(),
  229. required=False
  230. )
  231. rack_type = DynamicModelChoiceField(
  232. label=_('Rack Type'),
  233. queryset=RackType.objects.all(),
  234. required=False,
  235. selector=True,
  236. help_text=_("Select a pre-defined rack type, or set physical characteristics below."),
  237. )
  238. fieldsets = (
  239. FieldSet(
  240. 'site', 'location', 'name', 'status', 'role', 'rack_type', 'description', 'airflow', 'tags',
  241. name=_('Rack')
  242. ),
  243. FieldSet('facility_id', 'serial', 'asset_tag', name=_('Inventory Control')),
  244. FieldSet('tenant_group', 'tenant', name=_('Tenancy')),
  245. )
  246. class Meta:
  247. model = Rack
  248. fields = [
  249. 'site', 'location', 'name', 'facility_id', 'tenant_group', 'tenant', 'status', 'role', 'serial',
  250. 'asset_tag', 'rack_type', 'form_factor', 'width', 'u_height', 'starting_unit', 'desc_units', 'outer_width',
  251. 'outer_height', 'outer_depth', 'outer_unit', 'mounting_depth', 'airflow', 'weight', 'max_weight',
  252. 'weight_unit', 'description', 'owner', 'comments', 'tags',
  253. ]
  254. def __init__(self, *args, **kwargs):
  255. super().__init__(*args, **kwargs)
  256. # Mimic HTMXSelect()
  257. self.fields['rack_type'].widget.attrs.update({
  258. 'hx-get': '.',
  259. 'hx-include': '#form_fields',
  260. 'hx-target': '#form_fields',
  261. })
  262. # Omit RackType-defined fields if rack_type is set
  263. if get_field_value(self, 'rack_type'):
  264. for field_name in Rack.RACKTYPE_FIELDS:
  265. del self.fields[field_name]
  266. else:
  267. self.fieldsets = (
  268. *self.fieldsets,
  269. FieldSet(
  270. 'form_factor', 'width', 'starting_unit', 'u_height',
  271. InlineFields('outer_width', 'outer_height', 'outer_depth', 'outer_unit',
  272. label=_('Outer Dimensions')),
  273. InlineFields('weight', 'max_weight', 'weight_unit', label=_('Weight')),
  274. 'mounting_depth', 'desc_units', name=_('Dimensions')
  275. ),
  276. )
  277. class RackReservationForm(TenancyForm, PrimaryModelForm):
  278. rack = DynamicModelChoiceField(
  279. label=_('Rack'),
  280. queryset=Rack.objects.all(),
  281. selector=True
  282. )
  283. units = NumericArrayField(
  284. label=_('Units'),
  285. base_field=forms.IntegerField(),
  286. help_text=_("Comma-separated list of numeric unit IDs. A range may be specified using a hyphen.")
  287. )
  288. user = forms.ModelChoiceField(
  289. label=_('User'),
  290. queryset=User.objects.order_by('username')
  291. )
  292. fieldsets = (
  293. FieldSet('rack', 'units', 'status', 'user', 'description', 'tags', name=_('Reservation')),
  294. FieldSet('tenant_group', 'tenant', name=_('Tenancy')),
  295. )
  296. class Meta:
  297. model = RackReservation
  298. fields = [
  299. 'rack', 'units', 'status', 'user', 'tenant_group', 'tenant', 'description', 'owner', 'comments', 'tags',
  300. ]
  301. class ManufacturerForm(OrganizationalModelForm):
  302. fieldsets = (
  303. FieldSet('name', 'slug', 'description', 'tags', name=_('Manufacturer')),
  304. )
  305. class Meta:
  306. model = Manufacturer
  307. fields = [
  308. 'name', 'slug', 'description', 'owner', 'tags',
  309. ]
  310. class DeviceTypeForm(PrimaryModelForm):
  311. manufacturer = DynamicModelChoiceField(
  312. label=_('Manufacturer'),
  313. queryset=Manufacturer.objects.all(),
  314. quick_add=True
  315. )
  316. default_platform = DynamicModelChoiceField(
  317. label=_('Default platform'),
  318. queryset=Platform.objects.all(),
  319. required=False,
  320. selector=True,
  321. query_params={
  322. 'manufacturer_id': ['$manufacturer', 'null'],
  323. }
  324. )
  325. slug = SlugField(
  326. label=_('Slug'),
  327. slug_source='model'
  328. )
  329. fieldsets = (
  330. FieldSet('manufacturer', 'model', 'slug', 'default_platform', 'description', 'tags', name=_('Device Type')),
  331. FieldSet(
  332. 'u_height', 'exclude_from_utilization', 'is_full_depth', 'part_number', 'subdevice_role', 'airflow',
  333. 'weight', 'weight_unit', name=_('Chassis')
  334. ),
  335. FieldSet('front_image', 'rear_image', name=_('Images')),
  336. )
  337. class Meta:
  338. model = DeviceType
  339. fields = [
  340. 'manufacturer', 'model', 'slug', 'default_platform', 'part_number', 'u_height', 'exclude_from_utilization',
  341. 'is_full_depth', 'subdevice_role', 'airflow', 'weight', 'weight_unit', 'front_image', 'rear_image',
  342. 'description', 'owner', 'comments', 'tags',
  343. ]
  344. widgets = {
  345. 'front_image': ClearableFileInput(attrs={
  346. 'accept': DEVICETYPE_IMAGE_FORMATS
  347. }),
  348. 'rear_image': ClearableFileInput(attrs={
  349. 'accept': DEVICETYPE_IMAGE_FORMATS
  350. }),
  351. }
  352. class ModuleTypeProfileForm(PrimaryModelForm):
  353. schema = JSONField(
  354. label=_('Schema'),
  355. required=False,
  356. help_text=_("Enter a valid JSON schema to define supported attributes.")
  357. )
  358. fieldsets = (
  359. FieldSet('name', 'description', 'schema', 'tags', name=_('Profile')),
  360. )
  361. class Meta:
  362. model = ModuleTypeProfile
  363. fields = [
  364. 'name', 'description', 'schema', 'owner', 'comments', 'tags',
  365. ]
  366. class ModuleTypeForm(PrimaryModelForm):
  367. profile = forms.ModelChoiceField(
  368. queryset=ModuleTypeProfile.objects.all(),
  369. label=_('Profile'),
  370. required=False,
  371. widget=HTMXSelect()
  372. )
  373. manufacturer = DynamicModelChoiceField(
  374. label=_('Manufacturer'),
  375. queryset=Manufacturer.objects.all()
  376. )
  377. @property
  378. def fieldsets(self):
  379. return [
  380. FieldSet('manufacturer', 'model', 'part_number', 'description', 'tags', name=_('Module Type')),
  381. FieldSet('airflow', 'weight', 'weight_unit', name=_('Hardware')),
  382. FieldSet('profile', *self.attr_fields, name=_('Profile & Attributes'))
  383. ]
  384. class Meta:
  385. model = ModuleType
  386. fields = [
  387. 'profile', 'manufacturer', 'model', 'part_number', 'description', 'airflow', 'weight', 'weight_unit',
  388. 'owner', 'comments', 'tags',
  389. ]
  390. def __init__(self, *args, **kwargs):
  391. super().__init__(*args, **kwargs)
  392. # Track profile-specific attribute fields
  393. self.attr_fields = []
  394. # Retrieve assigned ModuleTypeProfile, if any
  395. if not (profile_id := get_field_value(self, 'profile')):
  396. return
  397. if not (profile := ModuleTypeProfile.objects.filter(pk=profile_id).first()):
  398. return
  399. # Extend form with fields for profile attributes
  400. for attr, form_field in self._get_attr_form_fields(profile).items():
  401. field_name = f'attr_{attr}'
  402. self.attr_fields.append(field_name)
  403. self.fields[field_name] = form_field
  404. if self.instance.attribute_data:
  405. self.fields[field_name].initial = self.instance.attribute_data.get(attr)
  406. @staticmethod
  407. def _get_attr_form_fields(profile):
  408. """
  409. Return a dictionary mapping of attribute names to form fields, suitable for extending
  410. the form per the selected ModuleTypeProfile.
  411. """
  412. if not profile.schema:
  413. return {}
  414. properties = profile.schema.get('properties', {})
  415. required_fields = profile.schema.get('required', [])
  416. attr_fields = {}
  417. for name, options in properties.items():
  418. prop = JSONSchemaProperty(**options)
  419. attr_fields[name] = prop.to_form_field(name, required=name in required_fields)
  420. return dict(sorted(attr_fields.items()))
  421. def _post_clean(self):
  422. # Compile attribute data from the individual form fields
  423. if self.cleaned_data.get('profile'):
  424. self.instance.attribute_data = {
  425. name[5:]: self.cleaned_data[name] # Remove the attr_ prefix
  426. for name in self.attr_fields
  427. if self.cleaned_data.get(name) not in EMPTY_VALUES
  428. }
  429. return super()._post_clean()
  430. class DeviceRoleForm(NestedGroupModelForm):
  431. config_template = DynamicModelChoiceField(
  432. label=_('Config template'),
  433. queryset=ConfigTemplate.objects.all(),
  434. required=False
  435. )
  436. parent = DynamicModelChoiceField(
  437. label=_('Parent'),
  438. queryset=DeviceRole.objects.all(),
  439. required=False,
  440. )
  441. fieldsets = (
  442. FieldSet(
  443. 'name', 'slug', 'parent', 'color', 'vm_role', 'config_template', 'description',
  444. 'tags', name=_('Device Role')
  445. ),
  446. )
  447. class Meta:
  448. model = DeviceRole
  449. fields = [
  450. 'name', 'slug', 'parent', 'color', 'vm_role', 'config_template', 'description', 'owner', 'comments', 'tags',
  451. ]
  452. class PlatformForm(NestedGroupModelForm):
  453. parent = DynamicModelChoiceField(
  454. label=_('Parent'),
  455. queryset=Platform.objects.all(),
  456. required=False,
  457. )
  458. manufacturer = DynamicModelChoiceField(
  459. label=_('Manufacturer'),
  460. queryset=Manufacturer.objects.all(),
  461. required=False,
  462. quick_add=True
  463. )
  464. config_template = DynamicModelChoiceField(
  465. label=_('Config template'),
  466. queryset=ConfigTemplate.objects.all(),
  467. required=False
  468. )
  469. slug = SlugField(
  470. label=_('Slug'),
  471. max_length=64
  472. )
  473. fieldsets = (
  474. FieldSet(
  475. 'name', 'slug', 'parent', 'manufacturer', 'config_template', 'description', 'tags', name=_('Platform'),
  476. ),
  477. )
  478. class Meta:
  479. model = Platform
  480. fields = [
  481. 'name', 'slug', 'parent', 'manufacturer', 'config_template', 'description', 'owner', 'comments', 'tags',
  482. ]
  483. class DeviceForm(TenancyForm, PrimaryModelForm):
  484. site = DynamicModelChoiceField(
  485. label=_('Site'),
  486. queryset=Site.objects.all(),
  487. selector=True
  488. )
  489. location = DynamicModelChoiceField(
  490. label=_('Location'),
  491. queryset=Location.objects.all(),
  492. required=False,
  493. query_params={
  494. 'site_id': '$site'
  495. },
  496. initial_params={
  497. 'racks': '$rack'
  498. }
  499. )
  500. rack = DynamicModelChoiceField(
  501. label=_('Rack'),
  502. queryset=Rack.objects.all(),
  503. required=False,
  504. query_params={
  505. 'site_id': '$site',
  506. 'location_id': '$location',
  507. }
  508. )
  509. position = forms.DecimalField(
  510. label=_('Position'),
  511. required=False,
  512. help_text=_("The lowest-numbered unit occupied by the device"),
  513. localize=True,
  514. widget=APISelect(
  515. api_url='/api/dcim/racks/{{rack}}/elevation/',
  516. attrs={
  517. 'ts-disabled-field': 'device',
  518. 'data-dynamic-params': '[{"fieldName":"face","queryParam":"face"}]'
  519. },
  520. )
  521. )
  522. device_type = DynamicModelChoiceField(
  523. label=_('Device type'),
  524. queryset=DeviceType.objects.all(),
  525. context={
  526. 'parent': 'manufacturer',
  527. },
  528. selector=True
  529. )
  530. role = DynamicModelChoiceField(
  531. label=_('Device role'),
  532. queryset=DeviceRole.objects.all(),
  533. quick_add=True
  534. )
  535. platform = DynamicModelChoiceField(
  536. label=_('Platform'),
  537. queryset=Platform.objects.all(),
  538. required=False,
  539. selector=True,
  540. query_params={
  541. 'available_for_device_type': '$device_type',
  542. }
  543. )
  544. cluster = DynamicModelChoiceField(
  545. label=_('Cluster'),
  546. queryset=Cluster.objects.all(),
  547. required=False,
  548. selector=True,
  549. query_params={
  550. 'site_id': ['$site', 'null']
  551. },
  552. )
  553. local_context_data = JSONField(
  554. required=False,
  555. label=''
  556. )
  557. virtual_chassis = DynamicModelChoiceField(
  558. label=_('Virtual chassis'),
  559. queryset=VirtualChassis.objects.all(),
  560. required=False,
  561. context={
  562. 'parent': 'master',
  563. },
  564. selector=True
  565. )
  566. vc_position = forms.IntegerField(
  567. required=False,
  568. label=_('Position'),
  569. help_text=_("The position in the virtual chassis this device is identified by")
  570. )
  571. vc_priority = forms.IntegerField(
  572. required=False,
  573. label=_('Priority'),
  574. help_text=_("The priority of the device in the virtual chassis")
  575. )
  576. config_template = DynamicModelChoiceField(
  577. label=_('Config template'),
  578. queryset=ConfigTemplate.objects.all(),
  579. required=False
  580. )
  581. class Meta:
  582. model = Device
  583. fields = [
  584. 'name', 'role', 'device_type', 'serial', 'asset_tag', 'site', 'rack', 'location', 'position', 'face',
  585. 'latitude', 'longitude', 'status', 'airflow', 'platform', 'primary_ip4', 'primary_ip6', 'oob_ip', 'cluster',
  586. 'tenant_group', 'tenant', 'virtual_chassis', 'vc_position', 'vc_priority', 'description', 'config_template',
  587. 'owner', 'comments', 'tags', 'local_context_data',
  588. ]
  589. def __init__(self, *args, **kwargs):
  590. super().__init__(*args, **kwargs)
  591. if self.instance.pk:
  592. # Compile list of choices for primary IPv4 and IPv6 addresses
  593. oob_ip_choices = [(None, '---------')]
  594. for family in [4, 6]:
  595. ip_choices = [(None, '---------')]
  596. # Gather PKs of all interfaces belonging to this Device or a peer VirtualChassis member
  597. interface_ids = self.instance.vc_interfaces(if_master=False).values_list('pk', flat=True)
  598. # Collect interface IPs
  599. interface_ips = IPAddress.objects.filter(
  600. address__family=family,
  601. assigned_object_type=ContentType.objects.get_for_model(Interface),
  602. assigned_object_id__in=interface_ids
  603. ).prefetch_related('assigned_object')
  604. if interface_ips:
  605. ip_list = [(ip.id, f'{ip.address} ({ip.assigned_object})') for ip in interface_ips]
  606. ip_choices.append(('Interface IPs', ip_list))
  607. oob_ip_choices.extend(ip_list)
  608. # Collect NAT IPs
  609. nat_ips = IPAddress.objects.prefetch_related('nat_inside').filter(
  610. address__family=family,
  611. nat_inside__assigned_object_type=ContentType.objects.get_for_model(Interface),
  612. nat_inside__assigned_object_id__in=interface_ids
  613. ).prefetch_related('assigned_object')
  614. if nat_ips:
  615. ip_list = [(ip.id, f'{ip.address} (NAT)') for ip in nat_ips]
  616. ip_choices.append(('NAT IPs', ip_list))
  617. self.fields['primary_ip{}'.format(family)].choices = ip_choices
  618. self.fields['oob_ip'].choices = oob_ip_choices
  619. # If editing an existing device, exclude it from the list of occupied rack units. This ensures that a device
  620. # can be flipped from one face to another.
  621. self.fields['position'].widget.add_query_param('exclude', self.instance.pk)
  622. # Disable rack assignment if this is a child device installed in a parent device
  623. if self.instance.device_type.is_child_device and hasattr(self.instance, 'parent_bay'):
  624. self.fields['site'].disabled = True
  625. self.fields['rack'].disabled = True
  626. self.initial['site'] = self.instance.parent_bay.device.site_id
  627. self.initial['rack'] = self.instance.parent_bay.device.rack_id
  628. else:
  629. # An object that doesn't exist yet can't have any IPs assigned to it
  630. self.fields['primary_ip4'].choices = []
  631. self.fields['primary_ip4'].widget.attrs['readonly'] = True
  632. self.fields['primary_ip6'].choices = []
  633. self.fields['primary_ip6'].widget.attrs['readonly'] = True
  634. self.fields['oob_ip'].choices = []
  635. self.fields['oob_ip'].widget.attrs['readonly'] = True
  636. # Rack position
  637. position = self.data.get('position') or self.initial.get('position')
  638. if position:
  639. self.fields['position'].widget.choices = [(position, f'U{position}')]
  640. class ModuleForm(ModuleCommonForm, PrimaryModelForm):
  641. device = DynamicModelChoiceField(
  642. label=_('Device'),
  643. queryset=Device.objects.all(),
  644. initial_params={
  645. 'modulebays': '$module_bay'
  646. }
  647. )
  648. module_bay = DynamicModelChoiceField(
  649. label=_('Module bay'),
  650. queryset=ModuleBay.objects.all(),
  651. query_params={
  652. 'device_id': '$device'
  653. },
  654. context={
  655. 'disabled': 'installed_module',
  656. },
  657. )
  658. module_type = DynamicModelChoiceField(
  659. label=_('Module type'),
  660. queryset=ModuleType.objects.all(),
  661. context={
  662. 'parent': 'manufacturer',
  663. },
  664. selector=True
  665. )
  666. replicate_components = forms.BooleanField(
  667. label=_('Replicate components'),
  668. required=False,
  669. initial=True,
  670. help_text=_("Automatically populate components associated with this module type")
  671. )
  672. adopt_components = forms.BooleanField(
  673. label=_('Adopt components'),
  674. required=False,
  675. initial=False,
  676. help_text=_("Adopt already existing components")
  677. )
  678. fieldsets = (
  679. FieldSet('device', 'module_bay', 'module_type', 'status', 'description', 'tags', name=_('Module')),
  680. FieldSet('serial', 'asset_tag', 'replicate_components', 'adopt_components', name=_('Hardware')),
  681. )
  682. class Meta:
  683. model = Module
  684. fields = [
  685. 'device', 'module_bay', 'module_type', 'status', 'serial', 'asset_tag', 'tags', 'replicate_components',
  686. 'adopt_components', 'description', 'owner', 'comments',
  687. ]
  688. def __init__(self, *args, **kwargs):
  689. super().__init__(*args, **kwargs)
  690. if self.instance.pk:
  691. self.fields['device'].disabled = True
  692. self.fields['replicate_components'].initial = False
  693. self.fields['replicate_components'].disabled = True
  694. self.fields['adopt_components'].initial = False
  695. self.fields['adopt_components'].disabled = True
  696. def get_termination_type_choices():
  697. return add_blank_choice([
  698. (f'{ct.app_label}.{ct.model}', ct.model_class()._meta.verbose_name.title())
  699. for ct in ContentType.objects.filter(CABLE_TERMINATION_MODELS)
  700. ])
  701. class CableForm(TenancyForm, PrimaryModelForm):
  702. a_terminations_type = forms.ChoiceField(
  703. choices=get_termination_type_choices,
  704. required=False,
  705. widget=HTMXSelect(),
  706. label=_('Type')
  707. )
  708. b_terminations_type = forms.ChoiceField(
  709. choices=get_termination_type_choices,
  710. required=False,
  711. widget=HTMXSelect(),
  712. label=_('Type')
  713. )
  714. class Meta:
  715. model = Cable
  716. fields = [
  717. 'a_terminations_type', 'b_terminations_type', 'type', 'status', 'profile', 'tenant_group', 'tenant',
  718. 'label', 'color', 'length', 'length_unit', 'description', 'owner', 'comments', 'tags',
  719. ]
  720. class PowerPanelForm(PrimaryModelForm):
  721. site = DynamicModelChoiceField(
  722. label=_('Site'),
  723. queryset=Site.objects.all(),
  724. selector=True
  725. )
  726. location = DynamicModelChoiceField(
  727. label=_('Location'),
  728. queryset=Location.objects.all(),
  729. required=False,
  730. query_params={
  731. 'site_id': '$site'
  732. }
  733. )
  734. fieldsets = (
  735. FieldSet('site', 'location', 'name', 'description', 'tags', name=_('Power Panel')),
  736. )
  737. class Meta:
  738. model = PowerPanel
  739. fields = [
  740. 'site', 'location', 'name', 'description', 'owner', 'comments', 'tags',
  741. ]
  742. class PowerFeedForm(TenancyForm, PrimaryModelForm):
  743. power_panel = DynamicModelChoiceField(
  744. label=_('Power panel'),
  745. queryset=PowerPanel.objects.all(),
  746. selector=True,
  747. quick_add=True
  748. )
  749. rack = DynamicModelChoiceField(
  750. label=_('Rack'),
  751. queryset=Rack.objects.all(),
  752. required=False,
  753. selector=True
  754. )
  755. fieldsets = (
  756. FieldSet(
  757. 'power_panel', 'rack', 'name', 'status', 'type', 'description', 'mark_connected', 'tags',
  758. name=_('Power Feed')
  759. ),
  760. FieldSet('supply', 'voltage', 'amperage', 'phase', 'max_utilization', name=_('Characteristics')),
  761. FieldSet('tenant_group', 'tenant', name=_('Tenancy')),
  762. )
  763. class Meta:
  764. model = PowerFeed
  765. fields = [
  766. 'power_panel', 'rack', 'name', 'status', 'type', 'mark_connected', 'supply', 'phase', 'voltage', 'amperage',
  767. 'max_utilization', 'tenant_group', 'tenant', 'description', 'owner', 'comments', 'tags'
  768. ]
  769. #
  770. # Virtual chassis
  771. #
  772. class VirtualChassisForm(PrimaryModelForm):
  773. master = forms.ModelChoiceField(
  774. label=_('Master'),
  775. queryset=Device.objects.all(),
  776. required=False,
  777. )
  778. class Meta:
  779. model = VirtualChassis
  780. fields = [
  781. 'name', 'domain', 'master', 'description', 'owner', 'comments', 'tags',
  782. ]
  783. widgets = {
  784. 'master': SelectWithPK(),
  785. }
  786. def __init__(self, *args, **kwargs):
  787. super().__init__(*args, **kwargs)
  788. self.fields['master'].queryset = Device.objects.filter(virtual_chassis=self.instance)
  789. class DeviceVCMembershipForm(forms.ModelForm):
  790. class Meta:
  791. model = Device
  792. fields = [
  793. 'vc_position', 'vc_priority',
  794. ]
  795. labels = {
  796. 'vc_position': 'Position',
  797. 'vc_priority': 'Priority',
  798. }
  799. def __init__(self, validate_vc_position=False, *args, **kwargs):
  800. super().__init__(*args, **kwargs)
  801. # Require VC position (only required when the Device is a VirtualChassis member)
  802. self.fields['vc_position'].required = True
  803. # Add bootstrap classes to form elements.
  804. self.fields['vc_position'].widget.attrs = {'class': 'form-control'}
  805. self.fields['vc_priority'].widget.attrs = {'class': 'form-control'}
  806. # Validation of vc_position is optional. This is only required when adding a new member to an existing
  807. # VirtualChassis. Otherwise, vc_position validation is handled by BaseVCMemberFormSet.
  808. self.validate_vc_position = validate_vc_position
  809. def clean_vc_position(self):
  810. vc_position = self.cleaned_data['vc_position']
  811. if self.validate_vc_position:
  812. conflicting_members = Device.objects.filter(
  813. virtual_chassis=self.instance.virtual_chassis,
  814. vc_position=vc_position
  815. )
  816. if conflicting_members.exists():
  817. raise forms.ValidationError(
  818. 'A virtual chassis member already exists in position {}.'.format(vc_position)
  819. )
  820. return vc_position
  821. class VCMemberSelectForm(forms.Form):
  822. device = DynamicModelChoiceField(
  823. label=_('Device'),
  824. queryset=Device.objects.all(),
  825. query_params={
  826. 'virtual_chassis_id': 'null',
  827. },
  828. selector=True
  829. )
  830. def clean_device(self):
  831. device = self.cleaned_data['device']
  832. if device.virtual_chassis is not None:
  833. raise forms.ValidationError(
  834. f"Device {device} is already assigned to a virtual chassis."
  835. )
  836. return device
  837. #
  838. # Device component templates
  839. #
  840. class ComponentTemplateForm(ChangelogMessageMixin, forms.ModelForm):
  841. device_type = DynamicModelChoiceField(
  842. label=_('Device type'),
  843. queryset=DeviceType.objects.all(),
  844. context={
  845. 'parent': 'manufacturer',
  846. }
  847. )
  848. def __init__(self, *args, **kwargs):
  849. super().__init__(*args, **kwargs)
  850. # Disable reassignment of DeviceType when editing an existing instance
  851. if self.instance.pk:
  852. self.fields['device_type'].disabled = True
  853. class ModularComponentTemplateForm(ComponentTemplateForm):
  854. device_type = DynamicModelChoiceField(
  855. label=_('Device type'),
  856. queryset=DeviceType.objects.all(),
  857. required=False,
  858. context={
  859. 'parent': 'manufacturer',
  860. }
  861. )
  862. module_type = DynamicModelChoiceField(
  863. label=_('Module type'),
  864. queryset=ModuleType.objects.all(),
  865. required=False,
  866. context={
  867. 'parent': 'manufacturer',
  868. }
  869. )
  870. fieldsets = (
  871. FieldSet(
  872. TabbedGroups(
  873. FieldSet('device_type', name=_('Device Type')),
  874. FieldSet('module_type', name=_('Module Type')),
  875. ),
  876. 'name', 'label', 'type', 'description'
  877. ),
  878. )
  879. def __init__(self, *args, **kwargs):
  880. super().__init__(*args, **kwargs)
  881. # Disable reassignment of ModuleType when editing an existing instance
  882. if self.instance.pk:
  883. self.fields['module_type'].disabled = True
  884. # Components attached to a module need to present this standardized substitution help text.
  885. self.fields['name'].help_text = _(
  886. "Alphanumeric ranges are supported for bulk creation. Mixed cases and types within a single range are not "
  887. "supported (example: <code>[ge,xe]-0/0/[0-9]</code>). The token <code>{module}</code>, if present, will be "
  888. "automatically replaced with the position value when creating a new module."
  889. )
  890. class ConsolePortTemplateForm(ModularComponentTemplateForm):
  891. class Meta:
  892. model = ConsolePortTemplate
  893. fields = [
  894. 'device_type', 'module_type', 'name', 'label', 'type', 'description',
  895. ]
  896. class ConsoleServerPortTemplateForm(ModularComponentTemplateForm):
  897. class Meta:
  898. model = ConsoleServerPortTemplate
  899. fields = [
  900. 'device_type', 'module_type', 'name', 'label', 'type', 'description',
  901. ]
  902. class PowerPortTemplateForm(ModularComponentTemplateForm):
  903. fieldsets = (
  904. FieldSet(
  905. TabbedGroups(
  906. FieldSet('device_type', name=_('Device Type')),
  907. FieldSet('module_type', name=_('Module Type')),
  908. ),
  909. 'name', 'label', 'type', 'maximum_draw', 'allocated_draw', 'description',
  910. ),
  911. )
  912. class Meta:
  913. model = PowerPortTemplate
  914. fields = [
  915. 'device_type', 'module_type', 'name', 'label', 'type', 'maximum_draw', 'allocated_draw', 'description',
  916. ]
  917. class PowerOutletTemplateForm(ModularComponentTemplateForm):
  918. power_port = DynamicModelChoiceField(
  919. label=_('Power port'),
  920. queryset=PowerPortTemplate.objects.all(),
  921. required=False,
  922. query_params={
  923. 'device_type_id': '$device_type',
  924. }
  925. )
  926. fieldsets = (
  927. FieldSet(
  928. TabbedGroups(
  929. FieldSet('device_type', name=_('Device Type')),
  930. FieldSet('module_type', name=_('Module Type')),
  931. ),
  932. 'name', 'label', 'type', 'color', 'power_port', 'feed_leg', 'description',
  933. ),
  934. )
  935. class Meta:
  936. model = PowerOutletTemplate
  937. fields = [
  938. 'device_type', 'module_type', 'name', 'label', 'type', 'color', 'power_port', 'feed_leg', 'description',
  939. ]
  940. class InterfaceTemplateForm(ModularComponentTemplateForm):
  941. bridge = DynamicModelChoiceField(
  942. label=_('Bridge'),
  943. queryset=InterfaceTemplate.objects.all(),
  944. required=False,
  945. query_params={
  946. 'device_type_id': '$device_type',
  947. 'module_type_id': '$module_type',
  948. }
  949. )
  950. fieldsets = (
  951. FieldSet(
  952. TabbedGroups(
  953. FieldSet('device_type', name=_('Device Type')),
  954. FieldSet('module_type', name=_('Module Type')),
  955. ),
  956. 'name', 'label', 'type', 'enabled', 'mgmt_only', 'description', 'bridge',
  957. ),
  958. FieldSet('poe_mode', 'poe_type', name=_('PoE')),
  959. FieldSet('rf_role', name=_('Wireless')),
  960. )
  961. class Meta:
  962. model = InterfaceTemplate
  963. fields = [
  964. 'device_type', 'module_type', 'name', 'label', 'type', 'mgmt_only', 'enabled', 'description', 'poe_mode',
  965. 'poe_type', 'bridge', 'rf_role',
  966. ]
  967. class FrontPortTemplateForm(ModularComponentTemplateForm):
  968. rear_port = DynamicModelChoiceField(
  969. label=_('Rear port'),
  970. queryset=RearPortTemplate.objects.all(),
  971. required=False,
  972. query_params={
  973. 'device_type_id': '$device_type',
  974. 'module_type_id': '$module_type',
  975. }
  976. )
  977. fieldsets = (
  978. FieldSet(
  979. TabbedGroups(
  980. FieldSet('device_type', name=_('Device Type')),
  981. FieldSet('module_type', name=_('Module Type')),
  982. ),
  983. 'name', 'label', 'type', 'color', 'rear_port', 'rear_port_position', 'description',
  984. ),
  985. )
  986. class Meta:
  987. model = FrontPortTemplate
  988. fields = [
  989. 'device_type', 'module_type', 'name', 'label', 'type', 'color', 'rear_port', 'rear_port_position',
  990. 'description',
  991. ]
  992. class RearPortTemplateForm(ModularComponentTemplateForm):
  993. fieldsets = (
  994. FieldSet(
  995. TabbedGroups(
  996. FieldSet('device_type', name=_('Device Type')),
  997. FieldSet('module_type', name=_('Module Type')),
  998. ),
  999. 'name', 'label', 'type', 'color', 'positions', 'description',
  1000. ),
  1001. )
  1002. class Meta:
  1003. model = RearPortTemplate
  1004. fields = [
  1005. 'device_type', 'module_type', 'name', 'label', 'type', 'color', 'positions', 'description',
  1006. ]
  1007. class ModuleBayTemplateForm(ModularComponentTemplateForm):
  1008. fieldsets = (
  1009. FieldSet(
  1010. TabbedGroups(
  1011. FieldSet('device_type', name=_('Device Type')),
  1012. FieldSet('module_type', name=_('Module Type')),
  1013. ),
  1014. 'name', 'label', 'position', 'description',
  1015. ),
  1016. )
  1017. class Meta:
  1018. model = ModuleBayTemplate
  1019. fields = [
  1020. 'device_type', 'module_type', 'name', 'label', 'position', 'description',
  1021. ]
  1022. class DeviceBayTemplateForm(ComponentTemplateForm):
  1023. fieldsets = (
  1024. FieldSet('device_type', 'name', 'label', 'description'),
  1025. )
  1026. class Meta:
  1027. model = DeviceBayTemplate
  1028. fields = [
  1029. 'device_type', 'name', 'label', 'description',
  1030. ]
  1031. class InventoryItemTemplateForm(ComponentTemplateForm):
  1032. parent = DynamicModelChoiceField(
  1033. label=_('Parent'),
  1034. queryset=InventoryItemTemplate.objects.all(),
  1035. required=False,
  1036. query_params={
  1037. 'device_type_id': '$device_type'
  1038. }
  1039. )
  1040. role = DynamicModelChoiceField(
  1041. label=_('Role'),
  1042. queryset=InventoryItemRole.objects.all(),
  1043. required=False
  1044. )
  1045. manufacturer = DynamicModelChoiceField(
  1046. label=_('Manufacturer'),
  1047. queryset=Manufacturer.objects.all(),
  1048. required=False
  1049. )
  1050. # Assigned component selectors
  1051. consoleporttemplate = DynamicModelChoiceField(
  1052. queryset=ConsolePortTemplate.objects.all(),
  1053. required=False,
  1054. query_params={
  1055. 'device_type_id': '$device_type'
  1056. },
  1057. label=_('Console port template')
  1058. )
  1059. consoleserverporttemplate = DynamicModelChoiceField(
  1060. queryset=ConsoleServerPortTemplate.objects.all(),
  1061. required=False,
  1062. query_params={
  1063. 'device_type_id': '$device_type'
  1064. },
  1065. label=_('Console server port template')
  1066. )
  1067. frontporttemplate = DynamicModelChoiceField(
  1068. queryset=FrontPortTemplate.objects.all(),
  1069. required=False,
  1070. query_params={
  1071. 'device_type_id': '$device_type'
  1072. },
  1073. label=_('Front port template')
  1074. )
  1075. interfacetemplate = DynamicModelChoiceField(
  1076. queryset=InterfaceTemplate.objects.all(),
  1077. required=False,
  1078. query_params={
  1079. 'device_type_id': '$device_type'
  1080. },
  1081. label=_('Interface template')
  1082. )
  1083. poweroutlettemplate = DynamicModelChoiceField(
  1084. queryset=PowerOutletTemplate.objects.all(),
  1085. required=False,
  1086. query_params={
  1087. 'device_type_id': '$device_type'
  1088. },
  1089. label=_('Power outlet template')
  1090. )
  1091. powerporttemplate = DynamicModelChoiceField(
  1092. queryset=PowerPortTemplate.objects.all(),
  1093. required=False,
  1094. query_params={
  1095. 'device_type_id': '$device_type'
  1096. },
  1097. label=_('Power port template')
  1098. )
  1099. rearporttemplate = DynamicModelChoiceField(
  1100. queryset=RearPortTemplate.objects.all(),
  1101. required=False,
  1102. query_params={
  1103. 'device_type_id': '$device_type'
  1104. },
  1105. label=_('Rear port template')
  1106. )
  1107. fieldsets = (
  1108. FieldSet(
  1109. 'device_type', 'parent', 'name', 'label', 'role', 'manufacturer', 'part_id', 'description',
  1110. ),
  1111. FieldSet(
  1112. TabbedGroups(
  1113. FieldSet('interfacetemplate', name=_('Interface')),
  1114. FieldSet('consoleporttemplate', name=_('Console Port')),
  1115. FieldSet('consoleserverporttemplate', name=_('Console Server Port')),
  1116. FieldSet('frontporttemplate', name=_('Front Port')),
  1117. FieldSet('rearporttemplate', name=_('Rear Port')),
  1118. FieldSet('powerporttemplate', name=_('Power Port')),
  1119. FieldSet('poweroutlettemplate', name=_('Power Outlet')),
  1120. ),
  1121. name=_('Component Assignment')
  1122. )
  1123. )
  1124. class Meta:
  1125. model = InventoryItemTemplate
  1126. fields = [
  1127. 'device_type', 'parent', 'name', 'label', 'role', 'manufacturer', 'part_id', 'description',
  1128. ]
  1129. def __init__(self, *args, **kwargs):
  1130. instance = kwargs.get('instance')
  1131. initial = kwargs.get('initial', {}).copy()
  1132. component_type = initial.get('component_type')
  1133. component_id = initial.get('component_id')
  1134. if instance:
  1135. # When editing set the initial value for component selection
  1136. for component_model in ContentType.objects.filter(MODULAR_COMPONENT_TEMPLATE_MODELS):
  1137. if type(instance.component) is component_model.model_class():
  1138. initial[component_model.model] = instance.component
  1139. break
  1140. elif component_type and component_id:
  1141. # When adding the InventoryItem from a component page
  1142. content_type = ContentType.objects.filter(
  1143. MODULAR_COMPONENT_TEMPLATE_MODELS
  1144. ).filter(pk=component_type).first()
  1145. if content_type:
  1146. if component := content_type.model_class().objects.filter(pk=component_id).first():
  1147. initial[content_type.model] = component
  1148. kwargs['initial'] = initial
  1149. super().__init__(*args, **kwargs)
  1150. def clean(self):
  1151. super().clean()
  1152. # Handle object assignment
  1153. selected_objects = [
  1154. field for field in (
  1155. 'consoleporttemplate', 'consoleserverporttemplate', 'frontporttemplate', 'interfacetemplate',
  1156. 'poweroutlettemplate', 'powerporttemplate', 'rearporttemplate'
  1157. ) if self.cleaned_data[field]
  1158. ]
  1159. if len(selected_objects) > 1:
  1160. raise forms.ValidationError(_("An InventoryItem can only be assigned to a single component."))
  1161. elif selected_objects:
  1162. self.instance.component = self.cleaned_data[selected_objects[0]]
  1163. else:
  1164. self.instance.component = None
  1165. #
  1166. # Device components
  1167. #
  1168. class DeviceComponentForm(OwnerMixin, NetBoxModelForm):
  1169. device = DynamicModelChoiceField(
  1170. label=_('Device'),
  1171. queryset=Device.objects.all(),
  1172. selector=True
  1173. )
  1174. def __init__(self, *args, **kwargs):
  1175. super().__init__(*args, **kwargs)
  1176. # Disable reassignment of Device when editing an existing instance
  1177. if self.instance.pk:
  1178. self.fields['device'].disabled = True
  1179. class ModularDeviceComponentForm(DeviceComponentForm):
  1180. module = DynamicModelChoiceField(
  1181. label=_('Module'),
  1182. queryset=Module.objects.all(),
  1183. required=False,
  1184. query_params={
  1185. 'device_id': '$device',
  1186. }
  1187. )
  1188. class ConsolePortForm(ModularDeviceComponentForm):
  1189. fieldsets = (
  1190. FieldSet(
  1191. 'device', 'module', 'name', 'label', 'type', 'speed', 'mark_connected', 'description', 'tags',
  1192. ),
  1193. )
  1194. class Meta:
  1195. model = ConsolePort
  1196. fields = [
  1197. 'device', 'module', 'name', 'label', 'type', 'speed', 'mark_connected', 'description', 'owner', 'tags',
  1198. ]
  1199. class ConsoleServerPortForm(ModularDeviceComponentForm):
  1200. fieldsets = (
  1201. FieldSet(
  1202. 'device', 'module', 'name', 'label', 'type', 'speed', 'mark_connected', 'description', 'tags',
  1203. ),
  1204. )
  1205. class Meta:
  1206. model = ConsoleServerPort
  1207. fields = [
  1208. 'device', 'module', 'name', 'label', 'type', 'speed', 'mark_connected', 'description', 'owner', 'tags',
  1209. ]
  1210. class PowerPortForm(ModularDeviceComponentForm):
  1211. fieldsets = (
  1212. FieldSet(
  1213. 'device', 'module', 'name', 'label', 'type', 'maximum_draw', 'allocated_draw', 'mark_connected',
  1214. 'description', 'tags',
  1215. ),
  1216. )
  1217. class Meta:
  1218. model = PowerPort
  1219. fields = [
  1220. 'device', 'module', 'name', 'label', 'type', 'maximum_draw', 'allocated_draw', 'mark_connected',
  1221. 'description', 'owner', 'tags',
  1222. ]
  1223. class PowerOutletForm(ModularDeviceComponentForm):
  1224. power_port = DynamicModelChoiceField(
  1225. label=_('Power port'),
  1226. queryset=PowerPort.objects.all(),
  1227. required=False,
  1228. query_params={
  1229. 'device_id': '$device',
  1230. }
  1231. )
  1232. fieldsets = (
  1233. FieldSet(
  1234. 'device', 'module', 'name', 'label', 'type', 'status', 'color', 'power_port', 'feed_leg', 'mark_connected',
  1235. 'description', 'owner', 'tags',
  1236. ),
  1237. )
  1238. class Meta:
  1239. model = PowerOutlet
  1240. fields = [
  1241. 'device', 'module', 'name', 'label', 'type', 'status', 'color', 'power_port', 'feed_leg', 'mark_connected',
  1242. 'description', 'tags',
  1243. ]
  1244. class InterfaceForm(InterfaceCommonForm, ModularDeviceComponentForm):
  1245. vdcs = DynamicModelMultipleChoiceField(
  1246. queryset=VirtualDeviceContext.objects.all(),
  1247. required=False,
  1248. label=_('Virtual device contexts'),
  1249. initial_params={
  1250. 'interfaces': '$parent',
  1251. },
  1252. query_params={
  1253. 'device_id': '$device',
  1254. }
  1255. )
  1256. parent = DynamicModelChoiceField(
  1257. queryset=Interface.objects.all(),
  1258. required=False,
  1259. label=_('Parent interface'),
  1260. query_params={
  1261. 'virtual_chassis_member_id': '$device',
  1262. }
  1263. )
  1264. bridge = DynamicModelChoiceField(
  1265. queryset=Interface.objects.all(),
  1266. required=False,
  1267. label=_('Bridged interface'),
  1268. query_params={
  1269. 'virtual_chassis_member_id': '$device',
  1270. }
  1271. )
  1272. lag = DynamicModelChoiceField(
  1273. queryset=Interface.objects.all(),
  1274. required=False,
  1275. label=_('LAG interface'),
  1276. query_params={
  1277. 'virtual_chassis_member_id': '$device',
  1278. 'type': 'lag',
  1279. }
  1280. )
  1281. wireless_lan_group = DynamicModelChoiceField(
  1282. queryset=WirelessLANGroup.objects.all(),
  1283. required=False,
  1284. label=_('Wireless LAN group')
  1285. )
  1286. wireless_lans = DynamicModelMultipleChoiceField(
  1287. queryset=WirelessLAN.objects.all(),
  1288. required=False,
  1289. label=_('Wireless LANs'),
  1290. query_params={
  1291. 'group_id': '$wireless_lan_group',
  1292. }
  1293. )
  1294. vlan_group = DynamicModelChoiceField(
  1295. queryset=VLANGroup.objects.all(),
  1296. required=False,
  1297. label=_('VLAN group'),
  1298. help_text=_("Filter VLANs available for assignment by group.")
  1299. )
  1300. untagged_vlan = DynamicModelChoiceField(
  1301. queryset=VLAN.objects.all(),
  1302. required=False,
  1303. label=_('Untagged VLAN'),
  1304. query_params={
  1305. 'group_id': '$vlan_group',
  1306. 'available_on_device': '$device',
  1307. }
  1308. )
  1309. tagged_vlans = DynamicModelMultipleChoiceField(
  1310. queryset=VLAN.objects.all(),
  1311. required=False,
  1312. label=_('Tagged VLANs'),
  1313. query_params={
  1314. 'group_id': '$vlan_group',
  1315. 'available_on_device': '$device',
  1316. }
  1317. )
  1318. qinq_svlan = DynamicModelChoiceField(
  1319. queryset=VLAN.objects.all(),
  1320. required=False,
  1321. label=_('Q-in-Q Service VLAN'),
  1322. query_params={
  1323. 'group_id': '$vlan_group',
  1324. 'available_on_device': '$device',
  1325. 'qinq_role': VLANQinQRoleChoices.ROLE_SERVICE,
  1326. }
  1327. )
  1328. vrf = DynamicModelChoiceField(
  1329. queryset=VRF.objects.all(),
  1330. required=False,
  1331. label=_('VRF')
  1332. )
  1333. primary_mac_address = DynamicModelChoiceField(
  1334. queryset=MACAddress.objects.all(),
  1335. label=_('Primary MAC address'),
  1336. required=False,
  1337. quick_add=True,
  1338. quick_add_params={'interface': '$pk'}
  1339. )
  1340. wwn = forms.CharField(
  1341. empty_value=None,
  1342. required=False,
  1343. label=_('WWN')
  1344. )
  1345. vlan_translation_policy = DynamicModelChoiceField(
  1346. queryset=VLANTranslationPolicy.objects.all(),
  1347. required=False,
  1348. label=_('VLAN Translation Policy')
  1349. )
  1350. fieldsets = (
  1351. FieldSet(
  1352. 'device', 'module', 'name', 'label', 'type', 'speed', 'duplex', 'description', 'tags', name=_('Interface')
  1353. ),
  1354. FieldSet('vrf', 'primary_mac_address', 'wwn', name=_('Addressing')),
  1355. FieldSet('vdcs', 'mtu', 'tx_power', 'enabled', 'mgmt_only', 'mark_connected', name=_('Operation')),
  1356. FieldSet('parent', 'bridge', 'lag', name=_('Related Interfaces')),
  1357. FieldSet('poe_mode', 'poe_type', name=_('PoE')),
  1358. FieldSet(
  1359. 'mode', 'vlan_group', 'untagged_vlan', 'tagged_vlans', 'qinq_svlan', 'vlan_translation_policy',
  1360. name=_('802.1Q Switching')
  1361. ),
  1362. FieldSet(
  1363. 'rf_role', 'rf_channel', 'rf_channel_frequency', 'rf_channel_width', 'wireless_lan_group', 'wireless_lans',
  1364. name=_('Wireless')
  1365. ),
  1366. )
  1367. class Meta:
  1368. model = Interface
  1369. fields = [
  1370. 'device', 'module', 'vdcs', 'name', 'label', 'type', 'speed', 'duplex', 'enabled', 'parent', 'bridge',
  1371. 'lag', 'wwn', 'mtu', 'mgmt_only', 'mark_connected', 'description', 'poe_mode', 'poe_type', 'mode',
  1372. 'rf_role', 'rf_channel', 'rf_channel_frequency', 'rf_channel_width', 'tx_power', 'wireless_lans',
  1373. 'untagged_vlan', 'tagged_vlans', 'qinq_svlan', 'vlan_translation_policy', 'vrf', 'primary_mac_address',
  1374. 'owner', 'tags',
  1375. ]
  1376. widgets = {
  1377. 'speed': NumberWithOptions(
  1378. options=InterfaceSpeedChoices
  1379. ),
  1380. 'mode': HTMXSelect(),
  1381. }
  1382. labels = {
  1383. 'mode': '802.1Q Mode',
  1384. }
  1385. class FrontPortForm(ModularDeviceComponentForm):
  1386. rear_port = DynamicModelChoiceField(
  1387. queryset=RearPort.objects.all(),
  1388. query_params={
  1389. 'device_id': '$device',
  1390. }
  1391. )
  1392. fieldsets = (
  1393. FieldSet(
  1394. 'device', 'module', 'name', 'label', 'type', 'color', 'rear_port', 'rear_port_position', 'mark_connected',
  1395. 'description', 'tags',
  1396. ),
  1397. )
  1398. class Meta:
  1399. model = FrontPort
  1400. fields = [
  1401. 'device', 'module', 'name', 'label', 'type', 'color', 'rear_port', 'rear_port_position', 'mark_connected',
  1402. 'description', 'owner', 'tags',
  1403. ]
  1404. class RearPortForm(ModularDeviceComponentForm):
  1405. fieldsets = (
  1406. FieldSet(
  1407. 'device', 'module', 'name', 'label', 'type', 'color', 'positions', 'mark_connected', 'description', 'tags',
  1408. ),
  1409. )
  1410. class Meta:
  1411. model = RearPort
  1412. fields = [
  1413. 'device', 'module', 'name', 'label', 'type', 'color', 'positions', 'mark_connected', 'description', 'owner',
  1414. 'tags',
  1415. ]
  1416. class ModuleBayForm(ModularDeviceComponentForm):
  1417. fieldsets = (
  1418. FieldSet('device', 'module', 'name', 'label', 'position', 'description', 'tags',),
  1419. )
  1420. class Meta:
  1421. model = ModuleBay
  1422. fields = [
  1423. 'device', 'module', 'name', 'label', 'position', 'description', 'owner', 'tags',
  1424. ]
  1425. class DeviceBayForm(DeviceComponentForm):
  1426. fieldsets = (
  1427. FieldSet('device', 'name', 'label', 'description', 'tags',),
  1428. )
  1429. class Meta:
  1430. model = DeviceBay
  1431. fields = [
  1432. 'device', 'name', 'label', 'description', 'owner', 'tags',
  1433. ]
  1434. class PopulateDeviceBayForm(forms.Form):
  1435. installed_device = forms.ModelChoiceField(
  1436. queryset=Device.objects.all(),
  1437. label=_('Child Device'),
  1438. help_text=_("Child devices must first be created and assigned to the site and rack of the parent device.")
  1439. )
  1440. def __init__(self, device_bay, *args, **kwargs):
  1441. super().__init__(*args, **kwargs)
  1442. self.fields['installed_device'].queryset = Device.objects.filter(
  1443. site=device_bay.device.site,
  1444. rack=device_bay.device.rack,
  1445. parent_bay__isnull=True,
  1446. device_type__u_height=0,
  1447. device_type__subdevice_role=SubdeviceRoleChoices.ROLE_CHILD
  1448. ).exclude(pk=device_bay.device.pk)
  1449. class InventoryItemForm(DeviceComponentForm):
  1450. parent = DynamicModelChoiceField(
  1451. label=_('Parent'),
  1452. queryset=InventoryItem.objects.all(),
  1453. required=False,
  1454. query_params={
  1455. 'device_id': '$device'
  1456. }
  1457. )
  1458. role = DynamicModelChoiceField(
  1459. label=_('Role'),
  1460. queryset=InventoryItemRole.objects.all(),
  1461. required=False
  1462. )
  1463. manufacturer = DynamicModelChoiceField(
  1464. label=_('Manufacturer'),
  1465. queryset=Manufacturer.objects.all(),
  1466. required=False
  1467. )
  1468. # Assigned component selectors
  1469. consoleport = DynamicModelChoiceField(
  1470. queryset=ConsolePort.objects.all(),
  1471. required=False,
  1472. query_params={
  1473. 'device_id': '$device'
  1474. },
  1475. label=_('Console port')
  1476. )
  1477. consoleserverport = DynamicModelChoiceField(
  1478. queryset=ConsoleServerPort.objects.all(),
  1479. required=False,
  1480. query_params={
  1481. 'device_id': '$device'
  1482. },
  1483. label=_('Console server port')
  1484. )
  1485. frontport = DynamicModelChoiceField(
  1486. queryset=FrontPort.objects.all(),
  1487. required=False,
  1488. query_params={
  1489. 'device_id': '$device'
  1490. },
  1491. label=_('Front port')
  1492. )
  1493. interface = DynamicModelChoiceField(
  1494. queryset=Interface.objects.all(),
  1495. required=False,
  1496. query_params={
  1497. 'device_id': '$device'
  1498. },
  1499. label=_('Interface')
  1500. )
  1501. poweroutlet = DynamicModelChoiceField(
  1502. queryset=PowerOutlet.objects.all(),
  1503. required=False,
  1504. query_params={
  1505. 'device_id': '$device'
  1506. },
  1507. label=_('Power outlet')
  1508. )
  1509. powerport = DynamicModelChoiceField(
  1510. queryset=PowerPort.objects.all(),
  1511. required=False,
  1512. query_params={
  1513. 'device_id': '$device'
  1514. },
  1515. label=_('Power port')
  1516. )
  1517. rearport = DynamicModelChoiceField(
  1518. queryset=RearPort.objects.all(),
  1519. required=False,
  1520. query_params={
  1521. 'device_id': '$device'
  1522. },
  1523. label=_('Rear port')
  1524. )
  1525. fieldsets = (
  1526. FieldSet(
  1527. 'device', 'parent', 'name', 'label', 'status', 'role', 'description', 'tags',
  1528. name=_('Inventory Item')
  1529. ),
  1530. FieldSet('manufacturer', 'part_id', 'serial', 'asset_tag', name=_('Hardware')),
  1531. FieldSet(
  1532. TabbedGroups(
  1533. FieldSet('interface', name=_('Interface')),
  1534. FieldSet('consoleport', name=_('Console Port')),
  1535. FieldSet('consoleserverport', name=_('Console Server Port')),
  1536. FieldSet('frontport', name=_('Front Port')),
  1537. FieldSet('rearport', name=_('Rear Port')),
  1538. FieldSet('powerport', name=_('Power Port')),
  1539. FieldSet('poweroutlet', name=_('Power Outlet')),
  1540. ),
  1541. name=_('Component Assignment')
  1542. )
  1543. )
  1544. class Meta:
  1545. model = InventoryItem
  1546. fields = [
  1547. 'device', 'parent', 'name', 'label', 'role', 'manufacturer', 'part_id', 'serial', 'asset_tag',
  1548. 'status', 'description', 'owner', 'tags',
  1549. ]
  1550. def __init__(self, *args, **kwargs):
  1551. instance = kwargs.get('instance')
  1552. initial = kwargs.get('initial', {}).copy()
  1553. component_type = initial.get('component_type')
  1554. component_id = initial.get('component_id')
  1555. if instance:
  1556. # When editing set the initial value for component selection
  1557. for component_model in ContentType.objects.filter(MODULAR_COMPONENT_MODELS):
  1558. if type(instance.component) is component_model.model_class():
  1559. initial[component_model.model] = instance.component
  1560. break
  1561. elif component_type and component_id:
  1562. # When adding the InventoryItem from a component page
  1563. if content_type := ContentType.objects.filter(MODULAR_COMPONENT_MODELS).filter(pk=component_type).first():
  1564. if component := content_type.model_class().objects.filter(pk=component_id).first():
  1565. initial[content_type.model] = component
  1566. kwargs['initial'] = initial
  1567. super().__init__(*args, **kwargs)
  1568. # Specifically allow editing the device of IntentoryItems
  1569. if self.instance.pk:
  1570. self.fields['device'].disabled = False
  1571. def clean(self):
  1572. super().clean()
  1573. # Handle object assignment
  1574. selected_objects = [
  1575. field for field in (
  1576. 'consoleport', 'consoleserverport', 'frontport', 'interface', 'poweroutlet', 'powerport', 'rearport'
  1577. ) if self.cleaned_data[field]
  1578. ]
  1579. if len(selected_objects) > 1:
  1580. raise forms.ValidationError(_("An InventoryItem can only be assigned to a single component."))
  1581. elif selected_objects:
  1582. self.instance.component = self.cleaned_data[selected_objects[0]]
  1583. else:
  1584. self.instance.component = None
  1585. class InventoryItemRoleForm(OrganizationalModelForm):
  1586. fieldsets = (
  1587. FieldSet('name', 'slug', 'color', 'description', 'tags', name=_('Inventory Item Role')),
  1588. )
  1589. class Meta:
  1590. model = InventoryItemRole
  1591. fields = [
  1592. 'name', 'slug', 'color', 'description', 'owner', 'tags',
  1593. ]
  1594. class VirtualDeviceContextForm(TenancyForm, PrimaryModelForm):
  1595. device = DynamicModelChoiceField(
  1596. label=_('Device'),
  1597. queryset=Device.objects.all(),
  1598. selector=True
  1599. )
  1600. primary_ip4 = DynamicModelChoiceField(
  1601. queryset=IPAddress.objects.all(),
  1602. label=_('Primary IPv4'),
  1603. required=False,
  1604. query_params={
  1605. 'device_id': '$device',
  1606. 'family': '4',
  1607. }
  1608. )
  1609. primary_ip6 = DynamicModelChoiceField(
  1610. queryset=IPAddress.objects.all(),
  1611. label=_('Primary IPv6'),
  1612. required=False,
  1613. query_params={
  1614. 'device_id': '$device',
  1615. 'family': '6',
  1616. }
  1617. )
  1618. fieldsets = (
  1619. FieldSet(
  1620. 'device', 'name', 'status', 'identifier', 'primary_ip4', 'primary_ip6', 'tags',
  1621. name=_('Virtual Device Context')
  1622. ),
  1623. FieldSet('tenant_group', 'tenant', name=_('Tenancy'))
  1624. )
  1625. class Meta:
  1626. model = VirtualDeviceContext
  1627. fields = [
  1628. 'device', 'name', 'status', 'identifier', 'primary_ip4', 'primary_ip6', 'tenant_group', 'tenant', 'owner',
  1629. 'comments', 'tags'
  1630. ]
  1631. #
  1632. # Addressing
  1633. #
  1634. class MACAddressForm(PrimaryModelForm):
  1635. mac_address = forms.CharField(
  1636. required=True,
  1637. label=_('MAC address')
  1638. )
  1639. interface = DynamicModelChoiceField(
  1640. label=_('Interface'),
  1641. queryset=Interface.objects.all(),
  1642. required=False,
  1643. selector=True,
  1644. context={
  1645. 'parent': 'device',
  1646. },
  1647. )
  1648. vminterface = DynamicModelChoiceField(
  1649. label=_('VM Interface'),
  1650. queryset=VMInterface.objects.all(),
  1651. required=False,
  1652. selector=True,
  1653. context={
  1654. 'parent': 'virtual_machine',
  1655. },
  1656. )
  1657. fieldsets = (
  1658. FieldSet(
  1659. 'mac_address', 'description', 'tags',
  1660. ),
  1661. FieldSet(
  1662. TabbedGroups(
  1663. FieldSet('interface', name=_('Device')),
  1664. FieldSet('vminterface', name=_('Virtual Machine')),
  1665. ),
  1666. ),
  1667. )
  1668. class Meta:
  1669. model = MACAddress
  1670. fields = [
  1671. 'mac_address', 'interface', 'vminterface', 'description', 'owner', 'tags',
  1672. ]
  1673. def __init__(self, *args, **kwargs):
  1674. # Initialize helper selectors
  1675. instance = kwargs.get('instance')
  1676. initial = kwargs.get('initial', {}).copy()
  1677. if instance:
  1678. if type(instance.assigned_object) is Interface:
  1679. initial['interface'] = instance.assigned_object
  1680. elif type(instance.assigned_object) is VMInterface:
  1681. initial['vminterface'] = instance.assigned_object
  1682. kwargs['initial'] = initial
  1683. super().__init__(*args, **kwargs)
  1684. if instance and instance.assigned_object and instance.assigned_object.primary_mac_address:
  1685. if instance.assigned_object.primary_mac_address.pk == instance.pk:
  1686. self.fields['interface'].disabled = True
  1687. self.fields['vminterface'].disabled = True
  1688. def clean(self):
  1689. super().clean()
  1690. # Handle object assignment
  1691. selected_objects = [
  1692. field for field in ('interface', 'vminterface') if self.cleaned_data[field]
  1693. ]
  1694. if len(selected_objects) > 1:
  1695. raise forms.ValidationError({
  1696. selected_objects[1]: _("A MAC address can only be assigned to a single object.")
  1697. })
  1698. elif selected_objects:
  1699. self.instance.assigned_object = self.cleaned_data[selected_objects[0]]
  1700. else:
  1701. self.instance.assigned_object = None