model_forms.py 82 KB

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