bulk_edit.py 39 KB

1234567891011121314151617181920212223242526272829303132333435363738394041424344454647484950515253545556575859606162636465666768697071727374757677787980818283848586878889909192939495969798991001011021031041051061071081091101111121131141151161171181191201211221231241251261271281291301311321331341351361371381391401411421431441451461471481491501511521531541551561571581591601611621631641651661671681691701711721731741751761771781791801811821831841851861871881891901911921931941951961971981992002012022032042052062072082092102112122132142152162172182192202212222232242252262272282292302312322332342352362372382392402412422432442452462472482492502512522532542552562572582592602612622632642652662672682692702712722732742752762772782792802812822832842852862872882892902912922932942952962972982993003013023033043053063073083093103113123133143153163173183193203213223233243253263273283293303313323333343353363373383393403413423433443453463473483493503513523533543553563573583593603613623633643653663673683693703713723733743753763773783793803813823833843853863873883893903913923933943953963973983994004014024034044054064074084094104114124134144154164174184194204214224234244254264274284294304314324334344354364374384394404414424434444454464474484494504514524534544554564574584594604614624634644654664674684694704714724734744754764774784794804814824834844854864874884894904914924934944954964974984995005015025035045055065075085095105115125135145155165175185195205215225235245255265275285295305315325335345355365375385395405415425435445455465475485495505515525535545555565575585595605615625635645655665675685695705715725735745755765775785795805815825835845855865875885895905915925935945955965975985996006016026036046056066076086096106116126136146156166176186196206216226236246256266276286296306316326336346356366376386396406416426436446456466476486496506516526536546556566576586596606616626636646656666676686696706716726736746756766776786796806816826836846856866876886896906916926936946956966976986997007017027037047057067077087097107117127137147157167177187197207217227237247257267277287297307317327337347357367377387397407417427437447457467477487497507517527537547557567577587597607617627637647657667677687697707717727737747757767777787797807817827837847857867877887897907917927937947957967977987998008018028038048058068078088098108118128138148158168178188198208218228238248258268278288298308318328338348358368378388398408418428438448458468478488498508518528538548558568578588598608618628638648658668678688698708718728738748758768778788798808818828838848858868878888898908918928938948958968978988999009019029039049059069079089099109119129139149159169179189199209219229239249259269279289299309319329339349359369379389399409419429439449459469479489499509519529539549559569579589599609619629639649659669679689699709719729739749759769779789799809819829839849859869879889899909919929939949959969979989991000100110021003100410051006100710081009101010111012101310141015101610171018101910201021102210231024102510261027102810291030103110321033103410351036103710381039104010411042104310441045104610471048104910501051105210531054105510561057105810591060106110621063106410651066106710681069107010711072107310741075107610771078107910801081108210831084108510861087108810891090109110921093109410951096109710981099110011011102110311041105110611071108110911101111111211131114111511161117111811191120112111221123112411251126112711281129113011311132113311341135113611371138113911401141114211431144114511461147114811491150115111521153115411551156115711581159116011611162116311641165116611671168116911701171117211731174117511761177117811791180118111821183118411851186118711881189119011911192119311941195119611971198119912001201120212031204120512061207120812091210121112121213121412151216121712181219122012211222122312241225122612271228122912301231123212331234123512361237123812391240124112421243124412451246124712481249125012511252125312541255125612571258125912601261126212631264126512661267126812691270127112721273127412751276127712781279128012811282128312841285128612871288128912901291129212931294129512961297129812991300130113021303130413051306130713081309131013111312131313141315131613171318131913201321132213231324132513261327132813291330133113321333133413351336133713381339134013411342134313441345134613471348134913501351135213531354135513561357135813591360136113621363136413651366136713681369137013711372137313741375137613771378137913801381138213831384138513861387138813891390139113921393139413951396139713981399140014011402140314041405140614071408140914101411141214131414141514161417
  1. from django import forms
  2. from django.contrib.auth.models import User
  3. from django.utils.translation import gettext as _
  4. from timezone_field import TimeZoneFormField
  5. from dcim.choices import *
  6. from dcim.constants import *
  7. from dcim.models import *
  8. from extras.models import ConfigTemplate
  9. from ipam.models import ASN, VLAN, VLANGroup, VRF
  10. from netbox.forms import NetBoxModelBulkEditForm
  11. from tenancy.models import Tenant
  12. from utilities.forms import (
  13. add_blank_choice, BulkEditForm, BulkEditNullBooleanSelect, ColorField, CommentField, DynamicModelChoiceField,
  14. DynamicModelMultipleChoiceField, form_from_model, SelectSpeedWidget,
  15. )
  16. __all__ = (
  17. 'CableBulkEditForm',
  18. 'ConsolePortBulkEditForm',
  19. 'ConsolePortTemplateBulkEditForm',
  20. 'ConsoleServerPortBulkEditForm',
  21. 'ConsoleServerPortTemplateBulkEditForm',
  22. 'DeviceBayBulkEditForm',
  23. 'DeviceBayTemplateBulkEditForm',
  24. 'DeviceBulkEditForm',
  25. 'DeviceRoleBulkEditForm',
  26. 'DeviceTypeBulkEditForm',
  27. 'FrontPortBulkEditForm',
  28. 'FrontPortTemplateBulkEditForm',
  29. 'InterfaceBulkEditForm',
  30. 'InterfaceTemplateBulkEditForm',
  31. 'InventoryItemBulkEditForm',
  32. 'InventoryItemRoleBulkEditForm',
  33. 'InventoryItemTemplateBulkEditForm',
  34. 'LocationBulkEditForm',
  35. 'ManufacturerBulkEditForm',
  36. 'ModuleBulkEditForm',
  37. 'ModuleBayBulkEditForm',
  38. 'ModuleBayTemplateBulkEditForm',
  39. 'ModuleTypeBulkEditForm',
  40. 'PlatformBulkEditForm',
  41. 'PowerFeedBulkEditForm',
  42. 'PowerOutletBulkEditForm',
  43. 'PowerOutletTemplateBulkEditForm',
  44. 'PowerPanelBulkEditForm',
  45. 'PowerPortBulkEditForm',
  46. 'PowerPortTemplateBulkEditForm',
  47. 'RackBulkEditForm',
  48. 'RackReservationBulkEditForm',
  49. 'RackRoleBulkEditForm',
  50. 'RearPortBulkEditForm',
  51. 'RearPortTemplateBulkEditForm',
  52. 'RegionBulkEditForm',
  53. 'SiteBulkEditForm',
  54. 'SiteGroupBulkEditForm',
  55. 'VirtualChassisBulkEditForm',
  56. 'VirtualDeviceContextBulkEditForm'
  57. )
  58. class RegionBulkEditForm(NetBoxModelBulkEditForm):
  59. parent = DynamicModelChoiceField(
  60. queryset=Region.objects.all(),
  61. required=False
  62. )
  63. description = forms.CharField(
  64. max_length=200,
  65. required=False
  66. )
  67. model = Region
  68. fieldsets = (
  69. (None, ('parent', 'description')),
  70. )
  71. nullable_fields = ('parent', 'description')
  72. class SiteGroupBulkEditForm(NetBoxModelBulkEditForm):
  73. parent = DynamicModelChoiceField(
  74. queryset=SiteGroup.objects.all(),
  75. required=False
  76. )
  77. description = forms.CharField(
  78. max_length=200,
  79. required=False
  80. )
  81. model = SiteGroup
  82. fieldsets = (
  83. (None, ('parent', 'description')),
  84. )
  85. nullable_fields = ('parent', 'description')
  86. class SiteBulkEditForm(NetBoxModelBulkEditForm):
  87. status = forms.ChoiceField(
  88. choices=add_blank_choice(SiteStatusChoices),
  89. required=False,
  90. initial=''
  91. )
  92. region = DynamicModelChoiceField(
  93. queryset=Region.objects.all(),
  94. required=False
  95. )
  96. group = DynamicModelChoiceField(
  97. queryset=SiteGroup.objects.all(),
  98. required=False
  99. )
  100. tenant = DynamicModelChoiceField(
  101. queryset=Tenant.objects.all(),
  102. required=False
  103. )
  104. asns = DynamicModelMultipleChoiceField(
  105. queryset=ASN.objects.all(),
  106. label=_('ASNs'),
  107. required=False
  108. )
  109. contact_name = forms.CharField(
  110. max_length=50,
  111. required=False
  112. )
  113. contact_phone = forms.CharField(
  114. max_length=20,
  115. required=False
  116. )
  117. contact_email = forms.EmailField(
  118. required=False,
  119. label=_('Contact E-mail')
  120. )
  121. time_zone = TimeZoneFormField(
  122. choices=add_blank_choice(TimeZoneFormField().choices),
  123. required=False
  124. )
  125. description = forms.CharField(
  126. max_length=200,
  127. required=False
  128. )
  129. comments = CommentField(
  130. widget=forms.Textarea,
  131. label='Comments'
  132. )
  133. model = Site
  134. fieldsets = (
  135. (None, ('status', 'region', 'group', 'tenant', 'asns', 'time_zone', 'description')),
  136. )
  137. nullable_fields = (
  138. 'region', 'group', 'tenant', 'asns', 'time_zone', 'description', 'comments',
  139. )
  140. class LocationBulkEditForm(NetBoxModelBulkEditForm):
  141. site = DynamicModelChoiceField(
  142. queryset=Site.objects.all(),
  143. required=False
  144. )
  145. parent = DynamicModelChoiceField(
  146. queryset=Location.objects.all(),
  147. required=False,
  148. query_params={
  149. 'site_id': '$site'
  150. }
  151. )
  152. status = forms.ChoiceField(
  153. choices=add_blank_choice(LocationStatusChoices),
  154. required=False,
  155. initial=''
  156. )
  157. tenant = DynamicModelChoiceField(
  158. queryset=Tenant.objects.all(),
  159. required=False
  160. )
  161. description = forms.CharField(
  162. max_length=200,
  163. required=False
  164. )
  165. model = Location
  166. fieldsets = (
  167. (None, ('site', 'parent', 'status', 'tenant', 'description')),
  168. )
  169. nullable_fields = ('parent', 'tenant', 'description')
  170. class RackRoleBulkEditForm(NetBoxModelBulkEditForm):
  171. color = ColorField(
  172. required=False
  173. )
  174. description = forms.CharField(
  175. max_length=200,
  176. required=False
  177. )
  178. model = RackRole
  179. fieldsets = (
  180. (None, ('color', 'description')),
  181. )
  182. nullable_fields = ('color', 'description')
  183. class RackBulkEditForm(NetBoxModelBulkEditForm):
  184. region = DynamicModelChoiceField(
  185. queryset=Region.objects.all(),
  186. required=False,
  187. initial_params={
  188. 'sites': '$site'
  189. }
  190. )
  191. site_group = DynamicModelChoiceField(
  192. queryset=SiteGroup.objects.all(),
  193. required=False,
  194. initial_params={
  195. 'sites': '$site'
  196. }
  197. )
  198. site = DynamicModelChoiceField(
  199. queryset=Site.objects.all(),
  200. required=False,
  201. query_params={
  202. 'region_id': '$region',
  203. 'group_id': '$site_group',
  204. }
  205. )
  206. location = DynamicModelChoiceField(
  207. queryset=Location.objects.all(),
  208. required=False,
  209. query_params={
  210. 'site_id': '$site'
  211. }
  212. )
  213. tenant = DynamicModelChoiceField(
  214. queryset=Tenant.objects.all(),
  215. required=False
  216. )
  217. status = forms.ChoiceField(
  218. choices=add_blank_choice(RackStatusChoices),
  219. required=False,
  220. initial=''
  221. )
  222. role = DynamicModelChoiceField(
  223. queryset=RackRole.objects.all(),
  224. required=False
  225. )
  226. serial = forms.CharField(
  227. max_length=50,
  228. required=False,
  229. label=_('Serial Number')
  230. )
  231. asset_tag = forms.CharField(
  232. max_length=50,
  233. required=False
  234. )
  235. type = forms.ChoiceField(
  236. choices=add_blank_choice(RackTypeChoices),
  237. required=False
  238. )
  239. width = forms.ChoiceField(
  240. choices=add_blank_choice(RackWidthChoices),
  241. required=False
  242. )
  243. u_height = forms.IntegerField(
  244. required=False,
  245. label=_('Height (U)')
  246. )
  247. desc_units = forms.NullBooleanField(
  248. required=False,
  249. widget=BulkEditNullBooleanSelect,
  250. label=_('Descending units')
  251. )
  252. outer_width = forms.IntegerField(
  253. required=False,
  254. min_value=1
  255. )
  256. outer_depth = forms.IntegerField(
  257. required=False,
  258. min_value=1
  259. )
  260. outer_unit = forms.ChoiceField(
  261. choices=add_blank_choice(RackDimensionUnitChoices),
  262. required=False
  263. )
  264. mounting_depth = forms.IntegerField(
  265. required=False,
  266. min_value=1
  267. )
  268. weight = forms.DecimalField(
  269. min_value=0,
  270. required=False
  271. )
  272. max_weight = forms.IntegerField(
  273. min_value=0,
  274. required=False
  275. )
  276. weight_unit = forms.ChoiceField(
  277. choices=add_blank_choice(WeightUnitChoices),
  278. required=False,
  279. initial=''
  280. )
  281. description = forms.CharField(
  282. max_length=200,
  283. required=False
  284. )
  285. comments = CommentField(
  286. widget=forms.Textarea,
  287. label='Comments'
  288. )
  289. model = Rack
  290. fieldsets = (
  291. ('Rack', ('status', 'role', 'tenant', 'serial', 'asset_tag', 'description')),
  292. ('Location', ('region', 'site_group', 'site', 'location')),
  293. ('Hardware', (
  294. 'type', 'width', 'u_height', 'desc_units', 'outer_width', 'outer_depth', 'outer_unit', 'mounting_depth',
  295. )),
  296. ('Weight', ('weight', 'max_weight', 'weight_unit')),
  297. )
  298. nullable_fields = (
  299. 'location', 'tenant', 'role', 'serial', 'asset_tag', 'outer_width', 'outer_depth', 'outer_unit', 'weight',
  300. 'max_weight', 'weight_unit', 'description', 'comments',
  301. )
  302. class RackReservationBulkEditForm(NetBoxModelBulkEditForm):
  303. user = forms.ModelChoiceField(
  304. queryset=User.objects.order_by(
  305. 'username'
  306. ),
  307. required=False
  308. )
  309. tenant = DynamicModelChoiceField(
  310. queryset=Tenant.objects.all(),
  311. required=False
  312. )
  313. description = forms.CharField(
  314. max_length=200,
  315. required=False
  316. )
  317. comments = CommentField(
  318. widget=forms.Textarea,
  319. label='Comments'
  320. )
  321. model = RackReservation
  322. fieldsets = (
  323. (None, ('user', 'tenant', 'description')),
  324. )
  325. nullable_fields = ('comments',)
  326. class ManufacturerBulkEditForm(NetBoxModelBulkEditForm):
  327. description = forms.CharField(
  328. max_length=200,
  329. required=False
  330. )
  331. model = Manufacturer
  332. fieldsets = (
  333. (None, ('description',)),
  334. )
  335. nullable_fields = ('description',)
  336. class DeviceTypeBulkEditForm(NetBoxModelBulkEditForm):
  337. manufacturer = DynamicModelChoiceField(
  338. queryset=Manufacturer.objects.all(),
  339. required=False
  340. )
  341. default_platform = DynamicModelChoiceField(
  342. queryset=Platform.objects.all(),
  343. required=False
  344. )
  345. part_number = forms.CharField(
  346. required=False
  347. )
  348. u_height = forms.IntegerField(
  349. min_value=1,
  350. required=False
  351. )
  352. is_full_depth = forms.NullBooleanField(
  353. required=False,
  354. widget=BulkEditNullBooleanSelect(),
  355. label=_('Is full depth')
  356. )
  357. airflow = forms.ChoiceField(
  358. choices=add_blank_choice(DeviceAirflowChoices),
  359. required=False
  360. )
  361. weight = forms.DecimalField(
  362. min_value=0,
  363. required=False
  364. )
  365. weight_unit = forms.ChoiceField(
  366. choices=add_blank_choice(WeightUnitChoices),
  367. required=False,
  368. initial=''
  369. )
  370. description = forms.CharField(
  371. max_length=200,
  372. required=False
  373. )
  374. comments = CommentField(
  375. widget=forms.Textarea,
  376. label='Comments'
  377. )
  378. model = DeviceType
  379. fieldsets = (
  380. ('Device Type', ('manufacturer', 'default_platform', 'part_number', 'u_height', 'is_full_depth', 'airflow', 'description')),
  381. ('Weight', ('weight', 'weight_unit')),
  382. )
  383. nullable_fields = ('part_number', 'airflow', 'weight', 'weight_unit', 'description', 'comments')
  384. class ModuleTypeBulkEditForm(NetBoxModelBulkEditForm):
  385. manufacturer = DynamicModelChoiceField(
  386. queryset=Manufacturer.objects.all(),
  387. required=False
  388. )
  389. part_number = forms.CharField(
  390. required=False
  391. )
  392. weight = forms.DecimalField(
  393. min_value=0,
  394. required=False
  395. )
  396. weight_unit = forms.ChoiceField(
  397. choices=add_blank_choice(WeightUnitChoices),
  398. required=False,
  399. initial=''
  400. )
  401. description = forms.CharField(
  402. max_length=200,
  403. required=False
  404. )
  405. comments = CommentField(
  406. widget=forms.Textarea,
  407. label='Comments'
  408. )
  409. model = ModuleType
  410. fieldsets = (
  411. ('Module Type', ('manufacturer', 'part_number', 'description')),
  412. ('Weight', ('weight', 'weight_unit')),
  413. )
  414. nullable_fields = ('part_number', 'weight', 'weight_unit', 'description', 'comments')
  415. class DeviceRoleBulkEditForm(NetBoxModelBulkEditForm):
  416. color = ColorField(
  417. required=False
  418. )
  419. vm_role = forms.NullBooleanField(
  420. required=False,
  421. widget=BulkEditNullBooleanSelect,
  422. label=_('VM role')
  423. )
  424. config_template = DynamicModelChoiceField(
  425. queryset=ConfigTemplate.objects.all(),
  426. required=False
  427. )
  428. description = forms.CharField(
  429. max_length=200,
  430. required=False
  431. )
  432. model = DeviceRole
  433. fieldsets = (
  434. (None, ('color', 'vm_role', 'config_template', 'description')),
  435. )
  436. nullable_fields = ('color', 'config_template', 'description')
  437. class PlatformBulkEditForm(NetBoxModelBulkEditForm):
  438. manufacturer = DynamicModelChoiceField(
  439. queryset=Manufacturer.objects.all(),
  440. required=False
  441. )
  442. napalm_driver = forms.CharField(
  443. max_length=50,
  444. required=False
  445. )
  446. config_template = DynamicModelChoiceField(
  447. queryset=ConfigTemplate.objects.all(),
  448. required=False
  449. )
  450. description = forms.CharField(
  451. max_length=200,
  452. required=False
  453. )
  454. model = Platform
  455. fieldsets = (
  456. (None, ('manufacturer', 'config_template', 'napalm_driver', 'description')),
  457. )
  458. nullable_fields = ('manufacturer', 'config_template', 'napalm_driver', 'description')
  459. class DeviceBulkEditForm(NetBoxModelBulkEditForm):
  460. manufacturer = DynamicModelChoiceField(
  461. queryset=Manufacturer.objects.all(),
  462. required=False
  463. )
  464. device_type = DynamicModelChoiceField(
  465. queryset=DeviceType.objects.all(),
  466. required=False,
  467. query_params={
  468. 'manufacturer_id': '$manufacturer'
  469. }
  470. )
  471. device_role = DynamicModelChoiceField(
  472. queryset=DeviceRole.objects.all(),
  473. required=False
  474. )
  475. site = DynamicModelChoiceField(
  476. queryset=Site.objects.all(),
  477. required=False
  478. )
  479. location = DynamicModelChoiceField(
  480. queryset=Location.objects.all(),
  481. required=False,
  482. query_params={
  483. 'site_id': '$site'
  484. }
  485. )
  486. tenant = DynamicModelChoiceField(
  487. queryset=Tenant.objects.all(),
  488. required=False
  489. )
  490. platform = DynamicModelChoiceField(
  491. queryset=Platform.objects.all(),
  492. required=False
  493. )
  494. status = forms.ChoiceField(
  495. choices=add_blank_choice(DeviceStatusChoices),
  496. required=False
  497. )
  498. airflow = forms.ChoiceField(
  499. choices=add_blank_choice(DeviceAirflowChoices),
  500. required=False
  501. )
  502. serial = forms.CharField(
  503. max_length=50,
  504. required=False,
  505. label=_('Serial Number')
  506. )
  507. description = forms.CharField(
  508. max_length=200,
  509. required=False
  510. )
  511. config_template = DynamicModelChoiceField(
  512. queryset=ConfigTemplate.objects.all(),
  513. required=False
  514. )
  515. comments = CommentField(
  516. widget=forms.Textarea,
  517. label='Comments'
  518. )
  519. model = Device
  520. fieldsets = (
  521. ('Device', ('device_role', 'status', 'tenant', 'platform', 'description')),
  522. ('Location', ('site', 'location')),
  523. ('Hardware', ('manufacturer', 'device_type', 'airflow', 'serial')),
  524. ('Configuration', ('config_template',)),
  525. )
  526. nullable_fields = (
  527. 'location', 'tenant', 'platform', 'serial', 'airflow', 'description', 'comments',
  528. )
  529. class ModuleBulkEditForm(NetBoxModelBulkEditForm):
  530. manufacturer = DynamicModelChoiceField(
  531. queryset=Manufacturer.objects.all(),
  532. required=False
  533. )
  534. module_type = DynamicModelChoiceField(
  535. queryset=ModuleType.objects.all(),
  536. required=False,
  537. query_params={
  538. 'manufacturer_id': '$manufacturer'
  539. }
  540. )
  541. status = forms.ChoiceField(
  542. choices=add_blank_choice(ModuleStatusChoices),
  543. required=False,
  544. initial=''
  545. )
  546. serial = forms.CharField(
  547. max_length=50,
  548. required=False,
  549. label=_('Serial Number')
  550. )
  551. description = forms.CharField(
  552. max_length=200,
  553. required=False
  554. )
  555. comments = CommentField(
  556. widget=forms.Textarea,
  557. label='Comments'
  558. )
  559. model = Module
  560. fieldsets = (
  561. (None, ('manufacturer', 'module_type', 'status', 'serial', 'description')),
  562. )
  563. nullable_fields = ('serial', 'description', 'comments')
  564. class CableBulkEditForm(NetBoxModelBulkEditForm):
  565. type = forms.ChoiceField(
  566. choices=add_blank_choice(CableTypeChoices),
  567. required=False,
  568. initial=''
  569. )
  570. status = forms.ChoiceField(
  571. choices=add_blank_choice(LinkStatusChoices),
  572. required=False,
  573. initial=''
  574. )
  575. tenant = DynamicModelChoiceField(
  576. queryset=Tenant.objects.all(),
  577. required=False
  578. )
  579. label = forms.CharField(
  580. max_length=100,
  581. required=False
  582. )
  583. color = ColorField(
  584. required=False
  585. )
  586. length = forms.DecimalField(
  587. min_value=0,
  588. required=False
  589. )
  590. length_unit = forms.ChoiceField(
  591. choices=add_blank_choice(CableLengthUnitChoices),
  592. required=False,
  593. initial=''
  594. )
  595. description = forms.CharField(
  596. max_length=200,
  597. required=False
  598. )
  599. comments = CommentField(
  600. widget=forms.Textarea,
  601. label='Comments'
  602. )
  603. model = Cable
  604. fieldsets = (
  605. (None, ('type', 'status', 'tenant', 'label', 'description')),
  606. ('Attributes', ('color', 'length', 'length_unit')),
  607. )
  608. nullable_fields = (
  609. 'type', 'status', 'tenant', 'label', 'color', 'length', 'description', 'comments',
  610. )
  611. class VirtualChassisBulkEditForm(NetBoxModelBulkEditForm):
  612. domain = forms.CharField(
  613. max_length=30,
  614. required=False
  615. )
  616. description = forms.CharField(
  617. max_length=200,
  618. required=False
  619. )
  620. comments = CommentField(
  621. widget=forms.Textarea,
  622. label='Comments'
  623. )
  624. model = VirtualChassis
  625. fieldsets = (
  626. (None, ('domain', 'description')),
  627. )
  628. nullable_fields = ('domain', 'description', 'comments')
  629. class PowerPanelBulkEditForm(NetBoxModelBulkEditForm):
  630. region = DynamicModelChoiceField(
  631. queryset=Region.objects.all(),
  632. required=False,
  633. initial_params={
  634. 'sites': '$site'
  635. }
  636. )
  637. site_group = DynamicModelChoiceField(
  638. queryset=SiteGroup.objects.all(),
  639. required=False,
  640. initial_params={
  641. 'sites': '$site'
  642. }
  643. )
  644. site = DynamicModelChoiceField(
  645. queryset=Site.objects.all(),
  646. required=False,
  647. query_params={
  648. 'region_id': '$region',
  649. 'group_id': '$site_group',
  650. }
  651. )
  652. location = DynamicModelChoiceField(
  653. queryset=Location.objects.all(),
  654. required=False,
  655. query_params={
  656. 'site_id': '$site'
  657. }
  658. )
  659. description = forms.CharField(
  660. max_length=200,
  661. required=False
  662. )
  663. comments = CommentField(
  664. widget=forms.Textarea,
  665. label='Comments'
  666. )
  667. model = PowerPanel
  668. fieldsets = (
  669. (None, ('region', 'site_group', 'site', 'location', 'description')),
  670. )
  671. nullable_fields = ('location', 'description', 'comments')
  672. class PowerFeedBulkEditForm(NetBoxModelBulkEditForm):
  673. power_panel = DynamicModelChoiceField(
  674. queryset=PowerPanel.objects.all(),
  675. required=False
  676. )
  677. rack = DynamicModelChoiceField(
  678. queryset=Rack.objects.all(),
  679. required=False,
  680. )
  681. status = forms.ChoiceField(
  682. choices=add_blank_choice(PowerFeedStatusChoices),
  683. required=False,
  684. initial=''
  685. )
  686. type = forms.ChoiceField(
  687. choices=add_blank_choice(PowerFeedTypeChoices),
  688. required=False,
  689. initial=''
  690. )
  691. supply = forms.ChoiceField(
  692. choices=add_blank_choice(PowerFeedSupplyChoices),
  693. required=False,
  694. initial=''
  695. )
  696. phase = forms.ChoiceField(
  697. choices=add_blank_choice(PowerFeedPhaseChoices),
  698. required=False,
  699. initial=''
  700. )
  701. voltage = forms.IntegerField(
  702. required=False
  703. )
  704. amperage = forms.IntegerField(
  705. required=False
  706. )
  707. max_utilization = forms.IntegerField(
  708. required=False
  709. )
  710. mark_connected = forms.NullBooleanField(
  711. required=False,
  712. widget=BulkEditNullBooleanSelect
  713. )
  714. description = forms.CharField(
  715. max_length=200,
  716. required=False
  717. )
  718. comments = CommentField(
  719. widget=forms.Textarea,
  720. label=_('Comments')
  721. )
  722. model = PowerFeed
  723. fieldsets = (
  724. (None, ('power_panel', 'rack', 'status', 'type', 'mark_connected', 'description')),
  725. ('Power', ('supply', 'phase', 'voltage', 'amperage', 'max_utilization'))
  726. )
  727. nullable_fields = ('location', 'description', 'comments')
  728. #
  729. # Device component templates
  730. #
  731. class ConsolePortTemplateBulkEditForm(BulkEditForm):
  732. pk = forms.ModelMultipleChoiceField(
  733. queryset=ConsolePortTemplate.objects.all(),
  734. widget=forms.MultipleHiddenInput()
  735. )
  736. label = forms.CharField(
  737. max_length=64,
  738. required=False
  739. )
  740. type = forms.ChoiceField(
  741. choices=add_blank_choice(ConsolePortTypeChoices),
  742. required=False
  743. )
  744. nullable_fields = ('label', 'type', 'description')
  745. class ConsoleServerPortTemplateBulkEditForm(BulkEditForm):
  746. pk = forms.ModelMultipleChoiceField(
  747. queryset=ConsoleServerPortTemplate.objects.all(),
  748. widget=forms.MultipleHiddenInput()
  749. )
  750. label = forms.CharField(
  751. max_length=64,
  752. required=False
  753. )
  754. type = forms.ChoiceField(
  755. choices=add_blank_choice(ConsolePortTypeChoices),
  756. required=False
  757. )
  758. description = forms.CharField(
  759. required=False
  760. )
  761. nullable_fields = ('label', 'type', 'description')
  762. class PowerPortTemplateBulkEditForm(BulkEditForm):
  763. pk = forms.ModelMultipleChoiceField(
  764. queryset=PowerPortTemplate.objects.all(),
  765. widget=forms.MultipleHiddenInput()
  766. )
  767. label = forms.CharField(
  768. max_length=64,
  769. required=False
  770. )
  771. type = forms.ChoiceField(
  772. choices=add_blank_choice(PowerPortTypeChoices),
  773. required=False
  774. )
  775. maximum_draw = forms.IntegerField(
  776. min_value=1,
  777. required=False,
  778. help_text=_("Maximum power draw (watts)")
  779. )
  780. allocated_draw = forms.IntegerField(
  781. min_value=1,
  782. required=False,
  783. help_text=_("Allocated power draw (watts)")
  784. )
  785. description = forms.CharField(
  786. required=False
  787. )
  788. nullable_fields = ('label', 'type', 'maximum_draw', 'allocated_draw', 'description')
  789. class PowerOutletTemplateBulkEditForm(BulkEditForm):
  790. pk = forms.ModelMultipleChoiceField(
  791. queryset=PowerOutletTemplate.objects.all(),
  792. widget=forms.MultipleHiddenInput()
  793. )
  794. device_type = forms.ModelChoiceField(
  795. queryset=DeviceType.objects.all(),
  796. required=False,
  797. disabled=True,
  798. widget=forms.HiddenInput()
  799. )
  800. label = forms.CharField(
  801. max_length=64,
  802. required=False
  803. )
  804. type = forms.ChoiceField(
  805. choices=add_blank_choice(PowerOutletTypeChoices),
  806. required=False
  807. )
  808. power_port = forms.ModelChoiceField(
  809. queryset=PowerPortTemplate.objects.all(),
  810. required=False
  811. )
  812. feed_leg = forms.ChoiceField(
  813. choices=add_blank_choice(PowerOutletFeedLegChoices),
  814. required=False
  815. )
  816. description = forms.CharField(
  817. required=False
  818. )
  819. nullable_fields = ('label', 'type', 'power_port', 'feed_leg', 'description')
  820. def __init__(self, *args, **kwargs):
  821. super().__init__(*args, **kwargs)
  822. # Limit power_port queryset to PowerPortTemplates which belong to the parent DeviceType
  823. if 'device_type' in self.initial:
  824. device_type = DeviceType.objects.filter(pk=self.initial['device_type']).first()
  825. self.fields['power_port'].queryset = PowerPortTemplate.objects.filter(device_type=device_type)
  826. else:
  827. self.fields['power_port'].choices = ()
  828. self.fields['power_port'].widget.attrs['disabled'] = True
  829. class InterfaceTemplateBulkEditForm(BulkEditForm):
  830. pk = forms.ModelMultipleChoiceField(
  831. queryset=InterfaceTemplate.objects.all(),
  832. widget=forms.MultipleHiddenInput()
  833. )
  834. label = forms.CharField(
  835. max_length=64,
  836. required=False
  837. )
  838. type = forms.ChoiceField(
  839. choices=add_blank_choice(InterfaceTypeChoices),
  840. required=False
  841. )
  842. enabled = forms.NullBooleanField(
  843. required=False,
  844. widget=BulkEditNullBooleanSelect
  845. )
  846. mgmt_only = forms.NullBooleanField(
  847. required=False,
  848. widget=BulkEditNullBooleanSelect,
  849. label=_('Management only')
  850. )
  851. description = forms.CharField(
  852. required=False
  853. )
  854. poe_mode = forms.ChoiceField(
  855. choices=add_blank_choice(InterfacePoEModeChoices),
  856. required=False,
  857. initial='',
  858. label=_('PoE mode')
  859. )
  860. poe_type = forms.ChoiceField(
  861. choices=add_blank_choice(InterfacePoETypeChoices),
  862. required=False,
  863. initial='',
  864. label=_('PoE type')
  865. )
  866. nullable_fields = ('label', 'description', 'poe_mode', 'poe_type')
  867. class FrontPortTemplateBulkEditForm(BulkEditForm):
  868. pk = forms.ModelMultipleChoiceField(
  869. queryset=FrontPortTemplate.objects.all(),
  870. widget=forms.MultipleHiddenInput()
  871. )
  872. label = forms.CharField(
  873. max_length=64,
  874. required=False
  875. )
  876. type = forms.ChoiceField(
  877. choices=add_blank_choice(PortTypeChoices),
  878. required=False
  879. )
  880. color = ColorField(
  881. required=False
  882. )
  883. description = forms.CharField(
  884. required=False
  885. )
  886. nullable_fields = ('description',)
  887. class RearPortTemplateBulkEditForm(BulkEditForm):
  888. pk = forms.ModelMultipleChoiceField(
  889. queryset=RearPortTemplate.objects.all(),
  890. widget=forms.MultipleHiddenInput()
  891. )
  892. label = forms.CharField(
  893. max_length=64,
  894. required=False
  895. )
  896. type = forms.ChoiceField(
  897. choices=add_blank_choice(PortTypeChoices),
  898. required=False
  899. )
  900. color = ColorField(
  901. required=False
  902. )
  903. description = forms.CharField(
  904. required=False
  905. )
  906. nullable_fields = ('description',)
  907. class ModuleBayTemplateBulkEditForm(BulkEditForm):
  908. pk = forms.ModelMultipleChoiceField(
  909. queryset=ModuleBayTemplate.objects.all(),
  910. widget=forms.MultipleHiddenInput()
  911. )
  912. label = forms.CharField(
  913. max_length=64,
  914. required=False
  915. )
  916. description = forms.CharField(
  917. required=False
  918. )
  919. nullable_fields = ('label', 'position', 'description')
  920. class DeviceBayTemplateBulkEditForm(BulkEditForm):
  921. pk = forms.ModelMultipleChoiceField(
  922. queryset=DeviceBayTemplate.objects.all(),
  923. widget=forms.MultipleHiddenInput()
  924. )
  925. label = forms.CharField(
  926. max_length=64,
  927. required=False
  928. )
  929. description = forms.CharField(
  930. required=False
  931. )
  932. nullable_fields = ('label', 'description')
  933. class InventoryItemTemplateBulkEditForm(BulkEditForm):
  934. pk = forms.ModelMultipleChoiceField(
  935. queryset=InventoryItemTemplate.objects.all(),
  936. widget=forms.MultipleHiddenInput()
  937. )
  938. label = forms.CharField(
  939. max_length=64,
  940. required=False
  941. )
  942. description = forms.CharField(
  943. required=False
  944. )
  945. role = DynamicModelChoiceField(
  946. queryset=InventoryItemRole.objects.all(),
  947. required=False
  948. )
  949. manufacturer = DynamicModelChoiceField(
  950. queryset=Manufacturer.objects.all(),
  951. required=False
  952. )
  953. nullable_fields = ('label', 'role', 'manufacturer', 'part_id', 'description')
  954. #
  955. # Device components
  956. #
  957. class ComponentBulkEditForm(NetBoxModelBulkEditForm):
  958. device = forms.ModelChoiceField(
  959. queryset=Device.objects.all(),
  960. required=False,
  961. disabled=True,
  962. widget=forms.HiddenInput()
  963. )
  964. module = forms.ModelChoiceField(
  965. queryset=Module.objects.all(),
  966. required=False
  967. )
  968. def __init__(self, *args, **kwargs):
  969. super().__init__(*args, **kwargs)
  970. # Limit module queryset to Modules which belong to the parent Device
  971. if 'device' in self.initial:
  972. device = Device.objects.filter(pk=self.initial['device']).first()
  973. self.fields['module'].queryset = Module.objects.filter(device=device)
  974. else:
  975. self.fields['module'].choices = ()
  976. self.fields['module'].widget.attrs['disabled'] = True
  977. class ConsolePortBulkEditForm(
  978. form_from_model(ConsolePort, ['label', 'type', 'speed', 'mark_connected', 'description']),
  979. ComponentBulkEditForm
  980. ):
  981. mark_connected = forms.NullBooleanField(
  982. required=False,
  983. widget=BulkEditNullBooleanSelect
  984. )
  985. model = ConsolePort
  986. fieldsets = (
  987. (None, ('module', 'type', 'label', 'speed', 'description', 'mark_connected')),
  988. )
  989. nullable_fields = ('module', 'label', 'description')
  990. class ConsoleServerPortBulkEditForm(
  991. form_from_model(ConsoleServerPort, ['label', 'type', 'speed', 'mark_connected', 'description']),
  992. ComponentBulkEditForm
  993. ):
  994. mark_connected = forms.NullBooleanField(
  995. required=False,
  996. widget=BulkEditNullBooleanSelect
  997. )
  998. model = ConsoleServerPort
  999. fieldsets = (
  1000. (None, ('module', 'type', 'label', 'speed', 'description', 'mark_connected')),
  1001. )
  1002. nullable_fields = ('module', 'label', 'description')
  1003. class PowerPortBulkEditForm(
  1004. form_from_model(PowerPort, ['label', 'type', 'maximum_draw', 'allocated_draw', 'mark_connected', 'description']),
  1005. ComponentBulkEditForm
  1006. ):
  1007. mark_connected = forms.NullBooleanField(
  1008. required=False,
  1009. widget=BulkEditNullBooleanSelect
  1010. )
  1011. model = PowerPort
  1012. fieldsets = (
  1013. (None, ('module', 'type', 'label', 'description', 'mark_connected')),
  1014. ('Power', ('maximum_draw', 'allocated_draw')),
  1015. )
  1016. nullable_fields = ('module', 'label', 'description')
  1017. class PowerOutletBulkEditForm(
  1018. form_from_model(PowerOutlet, ['label', 'type', 'feed_leg', 'power_port', 'mark_connected', 'description']),
  1019. ComponentBulkEditForm
  1020. ):
  1021. mark_connected = forms.NullBooleanField(
  1022. required=False,
  1023. widget=BulkEditNullBooleanSelect
  1024. )
  1025. model = PowerOutlet
  1026. fieldsets = (
  1027. (None, ('module', 'type', 'label', 'description', 'mark_connected')),
  1028. ('Power', ('feed_leg', 'power_port')),
  1029. )
  1030. nullable_fields = ('module', 'label', 'type', 'feed_leg', 'power_port', 'description')
  1031. def __init__(self, *args, **kwargs):
  1032. super().__init__(*args, **kwargs)
  1033. # Limit power_port queryset to PowerPorts which belong to the parent Device
  1034. if 'device' in self.initial:
  1035. device = Device.objects.filter(pk=self.initial['device']).first()
  1036. self.fields['power_port'].queryset = PowerPort.objects.filter(device=device)
  1037. else:
  1038. self.fields['power_port'].choices = ()
  1039. self.fields['power_port'].widget.attrs['disabled'] = True
  1040. class InterfaceBulkEditForm(
  1041. form_from_model(Interface, [
  1042. 'label', 'type', 'parent', 'bridge', 'lag', 'speed', 'duplex', 'mac_address', 'wwn', 'mtu', 'mgmt_only',
  1043. 'mark_connected', 'description', 'mode', 'rf_role', 'rf_channel', 'rf_channel_frequency', 'rf_channel_width',
  1044. 'tx_power',
  1045. ]),
  1046. ComponentBulkEditForm
  1047. ):
  1048. enabled = forms.NullBooleanField(
  1049. required=False,
  1050. widget=BulkEditNullBooleanSelect
  1051. )
  1052. parent = DynamicModelChoiceField(
  1053. queryset=Interface.objects.all(),
  1054. required=False
  1055. )
  1056. bridge = DynamicModelChoiceField(
  1057. queryset=Interface.objects.all(),
  1058. required=False
  1059. )
  1060. lag = DynamicModelChoiceField(
  1061. queryset=Interface.objects.all(),
  1062. required=False,
  1063. query_params={
  1064. 'type': 'lag',
  1065. },
  1066. label=_('LAG')
  1067. )
  1068. speed = forms.IntegerField(
  1069. required=False,
  1070. widget=SelectSpeedWidget(),
  1071. label=_('Speed')
  1072. )
  1073. mgmt_only = forms.NullBooleanField(
  1074. required=False,
  1075. widget=BulkEditNullBooleanSelect,
  1076. label=_('Management only')
  1077. )
  1078. poe_mode = forms.ChoiceField(
  1079. choices=add_blank_choice(InterfacePoEModeChoices),
  1080. required=False,
  1081. initial='',
  1082. label=_('PoE mode')
  1083. )
  1084. poe_type = forms.ChoiceField(
  1085. choices=add_blank_choice(InterfacePoETypeChoices),
  1086. required=False,
  1087. initial='',
  1088. label=_('PoE type')
  1089. )
  1090. mark_connected = forms.NullBooleanField(
  1091. required=False,
  1092. widget=BulkEditNullBooleanSelect
  1093. )
  1094. mode = forms.ChoiceField(
  1095. choices=add_blank_choice(InterfaceModeChoices),
  1096. required=False,
  1097. initial=''
  1098. )
  1099. vlan_group = DynamicModelChoiceField(
  1100. queryset=VLANGroup.objects.all(),
  1101. required=False,
  1102. label=_('VLAN group')
  1103. )
  1104. untagged_vlan = DynamicModelChoiceField(
  1105. queryset=VLAN.objects.all(),
  1106. required=False,
  1107. query_params={
  1108. 'group_id': '$vlan_group',
  1109. },
  1110. label=_('Untagged VLAN')
  1111. )
  1112. tagged_vlans = DynamicModelMultipleChoiceField(
  1113. queryset=VLAN.objects.all(),
  1114. required=False,
  1115. query_params={
  1116. 'group_id': '$vlan_group',
  1117. },
  1118. label=_('Tagged VLANs')
  1119. )
  1120. vrf = DynamicModelChoiceField(
  1121. queryset=VRF.objects.all(),
  1122. required=False,
  1123. label=_('VRF')
  1124. )
  1125. model = Interface
  1126. fieldsets = (
  1127. (None, ('module', 'type', 'label', 'speed', 'duplex', 'description')),
  1128. ('Addressing', ('vrf', 'mac_address', 'wwn')),
  1129. ('Operation', ('mtu', 'tx_power', 'enabled', 'mgmt_only', 'mark_connected')),
  1130. ('PoE', ('poe_mode', 'poe_type')),
  1131. ('Related Interfaces', ('parent', 'bridge', 'lag')),
  1132. ('802.1Q Switching', ('mode', 'vlan_group', 'untagged_vlan', 'tagged_vlans')),
  1133. ('Wireless', ('rf_role', 'rf_channel', 'rf_channel_frequency', 'rf_channel_width')),
  1134. )
  1135. nullable_fields = (
  1136. 'module', 'label', 'parent', 'bridge', 'lag', 'speed', 'duplex', 'mac_address', 'wwn', 'mtu', 'description',
  1137. 'poe_mode', 'poe_type', 'mode', 'rf_channel', 'rf_channel_frequency', 'rf_channel_width', 'tx_power',
  1138. 'vlan_group', 'untagged_vlan', 'tagged_vlans', 'vrf',
  1139. )
  1140. def __init__(self, *args, **kwargs):
  1141. super().__init__(*args, **kwargs)
  1142. if 'device' in self.initial:
  1143. device = Device.objects.filter(pk=self.initial['device']).first()
  1144. # Restrict parent/bridge/LAG interface assignment by device
  1145. self.fields['parent'].widget.add_query_param('device_id', device.pk)
  1146. self.fields['bridge'].widget.add_query_param('device_id', device.pk)
  1147. self.fields['lag'].widget.add_query_param('device_id', device.pk)
  1148. # Limit VLAN choices by device
  1149. self.fields['untagged_vlan'].widget.add_query_param('available_on_device', device.pk)
  1150. self.fields['tagged_vlans'].widget.add_query_param('available_on_device', device.pk)
  1151. else:
  1152. # See #4523
  1153. if 'pk' in self.initial:
  1154. site = None
  1155. interfaces = Interface.objects.filter(pk__in=self.initial['pk']).prefetch_related('device__site')
  1156. # Check interface sites. First interface should set site, further interfaces will either continue the
  1157. # loop or reset back to no site and break the loop.
  1158. for interface in interfaces:
  1159. if site is None:
  1160. site = interface.device.site
  1161. elif interface.device.site is not site:
  1162. site = None
  1163. break
  1164. if site is not None:
  1165. self.fields['untagged_vlan'].widget.add_query_param('site_id', site.pk)
  1166. self.fields['tagged_vlans'].widget.add_query_param('site_id', site.pk)
  1167. self.fields['parent'].choices = ()
  1168. self.fields['parent'].widget.attrs['disabled'] = True
  1169. self.fields['bridge'].choices = ()
  1170. self.fields['bridge'].widget.attrs['disabled'] = True
  1171. self.fields['lag'].choices = ()
  1172. self.fields['lag'].widget.attrs['disabled'] = True
  1173. def clean(self):
  1174. super().clean()
  1175. if not self.cleaned_data['mode']:
  1176. if self.cleaned_data['untagged_vlan']:
  1177. raise forms.ValidationError({'untagged_vlan': "Interface mode must be specified to assign VLANs"})
  1178. elif self.cleaned_data['tagged_vlans']:
  1179. raise forms.ValidationError({'tagged_vlans': "Interface mode must be specified to assign VLANs"})
  1180. # Untagged interfaces cannot be assigned tagged VLANs
  1181. elif self.cleaned_data['mode'] == InterfaceModeChoices.MODE_ACCESS and self.cleaned_data['tagged_vlans']:
  1182. raise forms.ValidationError({
  1183. 'mode': "An access interface cannot have tagged VLANs assigned."
  1184. })
  1185. # Remove all tagged VLAN assignments from "tagged all" interfaces
  1186. elif self.cleaned_data['mode'] == InterfaceModeChoices.MODE_TAGGED_ALL:
  1187. self.cleaned_data['tagged_vlans'] = []
  1188. class FrontPortBulkEditForm(
  1189. form_from_model(FrontPort, ['label', 'type', 'color', 'mark_connected', 'description']),
  1190. ComponentBulkEditForm
  1191. ):
  1192. model = FrontPort
  1193. fieldsets = (
  1194. (None, ('module', 'type', 'label', 'color', 'description', 'mark_connected')),
  1195. )
  1196. nullable_fields = ('module', 'label', 'description', 'color')
  1197. class RearPortBulkEditForm(
  1198. form_from_model(RearPort, ['label', 'type', 'color', 'mark_connected', 'description']),
  1199. ComponentBulkEditForm
  1200. ):
  1201. model = RearPort
  1202. fieldsets = (
  1203. (None, ('module', 'type', 'label', 'color', 'description', 'mark_connected')),
  1204. )
  1205. nullable_fields = ('module', 'label', 'description', 'color')
  1206. class ModuleBayBulkEditForm(
  1207. form_from_model(ModuleBay, ['label', 'position', 'description']),
  1208. NetBoxModelBulkEditForm
  1209. ):
  1210. model = ModuleBay
  1211. fieldsets = (
  1212. (None, ('label', 'position', 'description')),
  1213. )
  1214. nullable_fields = ('label', 'position', 'description')
  1215. class DeviceBayBulkEditForm(
  1216. form_from_model(DeviceBay, ['label', 'description']),
  1217. NetBoxModelBulkEditForm
  1218. ):
  1219. model = DeviceBay
  1220. fieldsets = (
  1221. (None, ('label', 'description')),
  1222. )
  1223. nullable_fields = ('label', 'description')
  1224. class InventoryItemBulkEditForm(
  1225. form_from_model(InventoryItem, ['label', 'role', 'manufacturer', 'part_id', 'description']),
  1226. NetBoxModelBulkEditForm
  1227. ):
  1228. device = DynamicModelChoiceField(
  1229. queryset=Device.objects.all(),
  1230. required=False
  1231. )
  1232. role = DynamicModelChoiceField(
  1233. queryset=InventoryItemRole.objects.all(),
  1234. required=False
  1235. )
  1236. manufacturer = DynamicModelChoiceField(
  1237. queryset=Manufacturer.objects.all(),
  1238. required=False
  1239. )
  1240. model = InventoryItem
  1241. fieldsets = (
  1242. (None, ('device', 'label', 'role', 'manufacturer', 'part_id', 'description')),
  1243. )
  1244. nullable_fields = ('label', 'role', 'manufacturer', 'part_id', 'description')
  1245. #
  1246. # Device component roles
  1247. #
  1248. class InventoryItemRoleBulkEditForm(NetBoxModelBulkEditForm):
  1249. color = ColorField(
  1250. required=False
  1251. )
  1252. description = forms.CharField(
  1253. max_length=200,
  1254. required=False
  1255. )
  1256. model = InventoryItemRole
  1257. fieldsets = (
  1258. (None, ('color', 'description')),
  1259. )
  1260. nullable_fields = ('color', 'description')
  1261. class VirtualDeviceContextBulkEditForm(NetBoxModelBulkEditForm):
  1262. device = DynamicModelChoiceField(
  1263. queryset=Device.objects.all(),
  1264. required=False
  1265. )
  1266. status = forms.ChoiceField(
  1267. required=False,
  1268. choices=add_blank_choice(VirtualDeviceContextStatusChoices)
  1269. )
  1270. tenant = DynamicModelChoiceField(
  1271. queryset=Tenant.objects.all(),
  1272. required=False
  1273. )
  1274. model = VirtualDeviceContext
  1275. fieldsets = (
  1276. (None, ('device', 'status', 'tenant')),
  1277. )
  1278. nullable_fields = ('device', 'tenant', )