model_forms.py 75 KB

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