model_forms.py 63 KB

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