model_forms.py 65 KB

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