model_forms.py 45 KB

1234567891011121314151617181920212223242526272829303132333435363738394041424344454647484950515253545556575859606162636465666768697071727374757677787980818283848586878889909192939495969798991001011021031041051061071081091101111121131141151161171181191201211221231241251261271281291301311321331341351361371381391401411421431441451461471481491501511521531541551561571581591601611621631641651661671681691701711721731741751761771781791801811821831841851861871881891901911921931941951961971981992002012022032042052062072082092102112122132142152162172182192202212222232242252262272282292302312322332342352362372382392402412422432442452462472482492502512522532542552562572582592602612622632642652662672682692702712722732742752762772782792802812822832842852862872882892902912922932942952962972982993003013023033043053063073083093103113123133143153163173183193203213223233243253263273283293303313323333343353363373383393403413423433443453463473483493503513523533543553563573583593603613623633643653663673683693703713723733743753763773783793803813823833843853863873883893903913923933943953963973983994004014024034044054064074084094104114124134144154164174184194204214224234244254264274284294304314324334344354364374384394404414424434444454464474484494504514524534544554564574584594604614624634644654664674684694704714724734744754764774784794804814824834844854864874884894904914924934944954964974984995005015025035045055065075085095105115125135145155165175185195205215225235245255265275285295305315325335345355365375385395405415425435445455465475485495505515525535545555565575585595605615625635645655665675685695705715725735745755765775785795805815825835845855865875885895905915925935945955965975985996006016026036046056066076086096106116126136146156166176186196206216226236246256266276286296306316326336346356366376386396406416426436446456466476486496506516526536546556566576586596606616626636646656666676686696706716726736746756766776786796806816826836846856866876886896906916926936946956966976986997007017027037047057067077087097107117127137147157167177187197207217227237247257267277287297307317327337347357367377387397407417427437447457467477487497507517527537547557567577587597607617627637647657667677687697707717727737747757767777787797807817827837847857867877887897907917927937947957967977987998008018028038048058068078088098108118128138148158168178188198208218228238248258268278288298308318328338348358368378388398408418428438448458468478488498508518528538548558568578588598608618628638648658668678688698708718728738748758768778788798808818828838848858868878888898908918928938948958968978988999009019029039049059069079089099109119129139149159169179189199209219229239249259269279289299309319329339349359369379389399409419429439449459469479489499509519529539549559569579589599609619629639649659669679689699709719729739749759769779789799809819829839849859869879889899909919929939949959969979989991000100110021003100410051006100710081009101010111012101310141015101610171018101910201021102210231024102510261027102810291030103110321033103410351036103710381039104010411042104310441045104610471048104910501051105210531054105510561057105810591060106110621063106410651066106710681069107010711072107310741075107610771078107910801081108210831084108510861087108810891090109110921093109410951096109710981099110011011102110311041105110611071108110911101111111211131114111511161117111811191120112111221123112411251126112711281129113011311132113311341135113611371138113911401141114211431144114511461147114811491150115111521153115411551156115711581159116011611162116311641165116611671168116911701171117211731174117511761177117811791180118111821183118411851186118711881189119011911192119311941195119611971198119912001201120212031204120512061207120812091210121112121213121412151216121712181219122012211222122312241225122612271228122912301231123212331234123512361237123812391240124112421243124412451246124712481249125012511252125312541255125612571258125912601261126212631264126512661267126812691270127112721273127412751276127712781279128012811282128312841285128612871288128912901291129212931294129512961297129812991300130113021303130413051306130713081309131013111312131313141315131613171318131913201321132213231324132513261327132813291330133113321333133413351336133713381339134013411342134313441345134613471348134913501351135213531354135513561357135813591360136113621363136413651366136713681369137013711372137313741375137613771378137913801381138213831384138513861387138813891390139113921393139413951396139713981399140014011402140314041405140614071408140914101411141214131414141514161417141814191420142114221423142414251426142714281429143014311432143314341435143614371438143914401441144214431444144514461447144814491450145114521453145414551456145714581459146014611462146314641465146614671468146914701471147214731474147514761477147814791480148114821483148414851486148714881489149014911492149314941495149614971498149915001501150215031504150515061507150815091510151115121513151415151516151715181519152015211522152315241525152615271528
  1. from django import forms
  2. from django.contrib.auth import get_user_model
  3. from django.contrib.contenttypes.models import ContentType
  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.models import *
  9. from extras.models import ConfigTemplate
  10. from ipam.models import ASN, IPAddress, VLAN, VLANGroup, VRF
  11. from netbox.forms import NetBoxModelForm
  12. from tenancy.forms import TenancyForm
  13. from utilities.forms import add_blank_choice
  14. from utilities.forms.fields import (
  15. CommentField, ContentTypeChoiceField, DynamicModelChoiceField, DynamicModelMultipleChoiceField, JSONField,
  16. NumericArrayField, SlugField,
  17. )
  18. from utilities.forms.rendering import InlineFields
  19. from utilities.forms.widgets import APISelect, ClearableFileInput, HTMXSelect, NumberWithOptions, SelectWithPK
  20. from virtualization.models import Cluster
  21. from wireless.models import WirelessLAN, WirelessLANGroup
  22. from .common import InterfaceCommonForm, ModuleCommonForm
  23. __all__ = (
  24. 'CableForm',
  25. 'ConsolePortForm',
  26. 'ConsolePortTemplateForm',
  27. 'ConsoleServerPortForm',
  28. 'ConsoleServerPortTemplateForm',
  29. 'DeviceBayForm',
  30. 'DeviceBayTemplateForm',
  31. 'DeviceForm',
  32. 'DeviceRoleForm',
  33. 'DeviceTypeForm',
  34. 'DeviceVCMembershipForm',
  35. 'FrontPortForm',
  36. 'FrontPortTemplateForm',
  37. 'InterfaceForm',
  38. 'InterfaceTemplateForm',
  39. 'InventoryItemForm',
  40. 'InventoryItemRoleForm',
  41. 'InventoryItemTemplateForm',
  42. 'LocationForm',
  43. 'ManufacturerForm',
  44. 'ModuleForm',
  45. 'ModuleBayForm',
  46. 'ModuleBayTemplateForm',
  47. 'ModuleTypeForm',
  48. 'PlatformForm',
  49. 'PopulateDeviceBayForm',
  50. 'PowerFeedForm',
  51. 'PowerOutletForm',
  52. 'PowerOutletTemplateForm',
  53. 'PowerPanelForm',
  54. 'PowerPortForm',
  55. 'PowerPortTemplateForm',
  56. 'RackForm',
  57. 'RackReservationForm',
  58. 'RackRoleForm',
  59. 'RearPortForm',
  60. 'RearPortTemplateForm',
  61. 'RegionForm',
  62. 'SiteForm',
  63. 'SiteGroupForm',
  64. 'VCMemberSelectForm',
  65. 'VirtualChassisForm',
  66. 'VirtualDeviceContextForm'
  67. )
  68. class RegionForm(NetBoxModelForm):
  69. parent = DynamicModelChoiceField(
  70. label=_('Parent'),
  71. queryset=Region.objects.all(),
  72. required=False
  73. )
  74. slug = SlugField()
  75. fieldsets = (
  76. (_('Region'), (
  77. 'parent', 'name', 'slug', 'description', 'tags',
  78. )),
  79. )
  80. class Meta:
  81. model = Region
  82. fields = (
  83. 'parent', 'name', 'slug', 'description', 'tags',
  84. )
  85. class SiteGroupForm(NetBoxModelForm):
  86. parent = DynamicModelChoiceField(
  87. label=_('Parent'),
  88. queryset=SiteGroup.objects.all(),
  89. required=False
  90. )
  91. slug = SlugField()
  92. fieldsets = (
  93. (_('Site Group'), (
  94. 'parent', 'name', 'slug', 'description', 'tags',
  95. )),
  96. )
  97. class Meta:
  98. model = SiteGroup
  99. fields = (
  100. 'parent', 'name', 'slug', 'description', 'tags',
  101. )
  102. class SiteForm(TenancyForm, NetBoxModelForm):
  103. region = DynamicModelChoiceField(
  104. label=_('Region'),
  105. queryset=Region.objects.all(),
  106. required=False
  107. )
  108. group = DynamicModelChoiceField(
  109. label=_('Group'),
  110. queryset=SiteGroup.objects.all(),
  111. required=False
  112. )
  113. asns = DynamicModelMultipleChoiceField(
  114. queryset=ASN.objects.all(),
  115. label=_('ASNs'),
  116. required=False
  117. )
  118. slug = SlugField()
  119. time_zone = TimeZoneFormField(
  120. label=_('Time zone'),
  121. choices=add_blank_choice(TimeZoneFormField().choices),
  122. required=False
  123. )
  124. comments = CommentField()
  125. fieldsets = (
  126. (_('Site'), (
  127. 'name', 'slug', 'status', 'region', 'group', 'facility', 'asns', 'time_zone', 'description', 'tags',
  128. )),
  129. (_('Tenancy'), ('tenant_group', 'tenant')),
  130. (_('Contact Info'), ('physical_address', 'shipping_address', 'latitude', 'longitude')),
  131. )
  132. class Meta:
  133. model = Site
  134. fields = (
  135. 'name', 'slug', 'status', 'region', 'group', 'tenant_group', 'tenant', 'facility', 'asns', 'time_zone',
  136. 'description', 'physical_address', 'shipping_address', 'latitude', 'longitude', 'comments', 'tags',
  137. )
  138. widgets = {
  139. 'physical_address': forms.Textarea(
  140. attrs={
  141. 'rows': 3,
  142. }
  143. ),
  144. 'shipping_address': forms.Textarea(
  145. attrs={
  146. 'rows': 3,
  147. }
  148. ),
  149. }
  150. class LocationForm(TenancyForm, NetBoxModelForm):
  151. site = DynamicModelChoiceField(
  152. label=_('Site'),
  153. queryset=Site.objects.all(),
  154. selector=True
  155. )
  156. parent = DynamicModelChoiceField(
  157. label=_('Parent'),
  158. queryset=Location.objects.all(),
  159. required=False,
  160. query_params={
  161. 'site_id': '$site'
  162. }
  163. )
  164. slug = SlugField()
  165. fieldsets = (
  166. (_('Location'), ('site', 'parent', 'name', 'slug', 'status', 'facility', 'description', 'tags')),
  167. (_('Tenancy'), ('tenant_group', 'tenant')),
  168. )
  169. class Meta:
  170. model = Location
  171. fields = (
  172. 'site', 'parent', 'name', 'slug', 'status', 'description', 'tenant_group', 'tenant', 'facility', 'tags',
  173. )
  174. class RackRoleForm(NetBoxModelForm):
  175. slug = SlugField()
  176. fieldsets = (
  177. (_('Rack Role'), (
  178. 'name', 'slug', 'color', 'description', 'tags',
  179. )),
  180. )
  181. class Meta:
  182. model = RackRole
  183. fields = [
  184. 'name', 'slug', 'color', 'description', 'tags',
  185. ]
  186. class RackForm(TenancyForm, NetBoxModelForm):
  187. site = DynamicModelChoiceField(
  188. label=_('Site'),
  189. queryset=Site.objects.all(),
  190. selector=True
  191. )
  192. location = DynamicModelChoiceField(
  193. label=_('Location'),
  194. queryset=Location.objects.all(),
  195. required=False,
  196. query_params={
  197. 'site_id': '$site'
  198. }
  199. )
  200. role = DynamicModelChoiceField(
  201. label=_('Role'),
  202. queryset=RackRole.objects.all(),
  203. required=False
  204. )
  205. comments = CommentField()
  206. fieldsets = (
  207. (_('Rack'), ('site', 'location', 'name', 'status', 'role', 'description', 'tags')),
  208. (_('Inventory Control'), ('facility_id', 'serial', 'asset_tag')),
  209. (_('Tenancy'), ('tenant_group', 'tenant')),
  210. (_('Dimensions'), (
  211. 'type',
  212. 'width',
  213. 'starting_unit',
  214. 'u_height',
  215. InlineFields('outer_width', 'outer_depth', 'outer_unit', label=_('Outer Dimensions')),
  216. InlineFields('weight', 'max_weight', 'weight_unit', label=_('Weight')),
  217. 'mounting_depth',
  218. 'desc_units',
  219. )),
  220. )
  221. class Meta:
  222. model = Rack
  223. fields = [
  224. 'site', 'location', 'name', 'facility_id', 'tenant_group', 'tenant', 'status', 'role', 'serial',
  225. 'asset_tag', 'type', 'width', 'u_height', 'starting_unit', 'desc_units', 'outer_width', 'outer_depth',
  226. 'outer_unit', 'mounting_depth', 'weight', 'max_weight', 'weight_unit', 'description', 'comments', 'tags',
  227. ]
  228. class RackReservationForm(TenancyForm, NetBoxModelForm):
  229. rack = DynamicModelChoiceField(
  230. label=_('Rack'),
  231. queryset=Rack.objects.all(),
  232. selector=True
  233. )
  234. units = NumericArrayField(
  235. label=_('Units'),
  236. base_field=forms.IntegerField(),
  237. help_text=_("Comma-separated list of numeric unit IDs. A range may be specified using a hyphen.")
  238. )
  239. user = forms.ModelChoiceField(
  240. label=_('User'),
  241. queryset=get_user_model().objects.order_by(
  242. 'username'
  243. )
  244. )
  245. comments = CommentField()
  246. fieldsets = (
  247. (_('Reservation'), ('rack', 'units', 'user', 'description', 'tags')),
  248. (_('Tenancy'), ('tenant_group', 'tenant')),
  249. )
  250. class Meta:
  251. model = RackReservation
  252. fields = [
  253. 'rack', 'units', 'user', 'tenant_group', 'tenant', 'description', 'comments', 'tags',
  254. ]
  255. class ManufacturerForm(NetBoxModelForm):
  256. slug = SlugField()
  257. fieldsets = (
  258. (_('Manufacturer'), (
  259. 'name', 'slug', 'description', 'tags',
  260. )),
  261. )
  262. class Meta:
  263. model = Manufacturer
  264. fields = [
  265. 'name', 'slug', 'description', 'tags',
  266. ]
  267. class DeviceTypeForm(NetBoxModelForm):
  268. manufacturer = DynamicModelChoiceField(
  269. label=_('Manufacturer'),
  270. queryset=Manufacturer.objects.all()
  271. )
  272. default_platform = DynamicModelChoiceField(
  273. label=_('Default platform'),
  274. queryset=Platform.objects.all(),
  275. required=False,
  276. selector=True,
  277. query_params={
  278. 'manufacturer_id': ['$manufacturer', 'null'],
  279. }
  280. )
  281. slug = SlugField(
  282. label=_('Slug'),
  283. slug_source='model'
  284. )
  285. comments = CommentField()
  286. fieldsets = (
  287. (_('Device Type'), ('manufacturer', 'model', 'slug', 'default_platform', 'description', 'tags')),
  288. (_('Chassis'), (
  289. 'u_height', 'exclude_from_utilization', 'is_full_depth', 'part_number', 'subdevice_role', 'airflow',
  290. 'weight', 'weight_unit',
  291. )),
  292. (_('Images'), ('front_image', 'rear_image')),
  293. )
  294. class Meta:
  295. model = DeviceType
  296. fields = [
  297. 'manufacturer', 'model', 'slug', 'default_platform', 'part_number', 'u_height', 'exclude_from_utilization',
  298. 'is_full_depth', 'subdevice_role', 'airflow', 'weight', 'weight_unit', 'front_image', 'rear_image',
  299. 'description', 'comments', 'tags',
  300. ]
  301. widgets = {
  302. 'front_image': ClearableFileInput(attrs={
  303. 'accept': DEVICETYPE_IMAGE_FORMATS
  304. }),
  305. 'rear_image': ClearableFileInput(attrs={
  306. 'accept': DEVICETYPE_IMAGE_FORMATS
  307. }),
  308. }
  309. class ModuleTypeForm(NetBoxModelForm):
  310. manufacturer = DynamicModelChoiceField(
  311. label=_('Manufacturer'),
  312. queryset=Manufacturer.objects.all()
  313. )
  314. comments = CommentField()
  315. fieldsets = (
  316. (_('Module Type'), ('manufacturer', 'model', 'part_number', 'description', 'tags')),
  317. (_('Weight'), ('weight', 'weight_unit'))
  318. )
  319. class Meta:
  320. model = ModuleType
  321. fields = [
  322. 'manufacturer', 'model', 'part_number', 'weight', 'weight_unit', 'description', 'comments', 'tags',
  323. ]
  324. class DeviceRoleForm(NetBoxModelForm):
  325. config_template = DynamicModelChoiceField(
  326. label=_('Config template'),
  327. queryset=ConfigTemplate.objects.all(),
  328. required=False
  329. )
  330. slug = SlugField()
  331. fieldsets = (
  332. (_('Device Role'), (
  333. 'name', 'slug', 'color', 'vm_role', 'config_template', 'description', 'tags',
  334. )),
  335. )
  336. class Meta:
  337. model = DeviceRole
  338. fields = [
  339. 'name', 'slug', 'color', 'vm_role', 'config_template', 'description', 'tags',
  340. ]
  341. class PlatformForm(NetBoxModelForm):
  342. manufacturer = DynamicModelChoiceField(
  343. label=_('Manufacturer'),
  344. queryset=Manufacturer.objects.all(),
  345. required=False
  346. )
  347. config_template = DynamicModelChoiceField(
  348. label=_('Config template'),
  349. queryset=ConfigTemplate.objects.all(),
  350. required=False
  351. )
  352. slug = SlugField(
  353. label=_('Slug'),
  354. max_length=64
  355. )
  356. fieldsets = (
  357. (_('Platform'), ('name', 'slug', 'manufacturer', 'config_template', 'description', 'tags')),
  358. )
  359. class Meta:
  360. model = Platform
  361. fields = [
  362. 'name', 'slug', 'manufacturer', 'config_template', 'description', 'tags',
  363. ]
  364. class DeviceForm(TenancyForm, NetBoxModelForm):
  365. site = DynamicModelChoiceField(
  366. label=_('Site'),
  367. queryset=Site.objects.all(),
  368. selector=True
  369. )
  370. location = DynamicModelChoiceField(
  371. label=_('Location'),
  372. queryset=Location.objects.all(),
  373. required=False,
  374. query_params={
  375. 'site_id': '$site'
  376. },
  377. initial_params={
  378. 'racks': '$rack'
  379. }
  380. )
  381. rack = DynamicModelChoiceField(
  382. label=_('Rack'),
  383. queryset=Rack.objects.all(),
  384. required=False,
  385. query_params={
  386. 'site_id': '$site',
  387. 'location_id': '$location',
  388. }
  389. )
  390. position = forms.DecimalField(
  391. label=_('Position'),
  392. required=False,
  393. help_text=_("The lowest-numbered unit occupied by the device"),
  394. localize=True,
  395. widget=APISelect(
  396. api_url='/api/dcim/racks/{{rack}}/elevation/',
  397. attrs={
  398. 'ts-disabled-field': 'device',
  399. 'data-dynamic-params': '[{"fieldName":"face","queryParam":"face"}]'
  400. },
  401. )
  402. )
  403. device_type = DynamicModelChoiceField(
  404. label=_('Device type'),
  405. queryset=DeviceType.objects.all(),
  406. context={
  407. 'parent': 'manufacturer',
  408. },
  409. selector=True
  410. )
  411. role = DynamicModelChoiceField(
  412. label=_('Device role'),
  413. queryset=DeviceRole.objects.all()
  414. )
  415. platform = DynamicModelChoiceField(
  416. label=_('Platform'),
  417. queryset=Platform.objects.all(),
  418. required=False,
  419. selector=True,
  420. query_params={
  421. 'available_for_device_type': '$device_type',
  422. }
  423. )
  424. cluster = DynamicModelChoiceField(
  425. label=_('Cluster'),
  426. queryset=Cluster.objects.all(),
  427. required=False,
  428. selector=True
  429. )
  430. comments = CommentField()
  431. local_context_data = JSONField(
  432. required=False,
  433. label=''
  434. )
  435. virtual_chassis = DynamicModelChoiceField(
  436. label=_('Virtual chassis'),
  437. queryset=VirtualChassis.objects.all(),
  438. required=False,
  439. context={
  440. 'parent': 'master',
  441. },
  442. selector=True
  443. )
  444. vc_position = forms.IntegerField(
  445. required=False,
  446. label=_('Position'),
  447. help_text=_("The position in the virtual chassis this device is identified by")
  448. )
  449. vc_priority = forms.IntegerField(
  450. required=False,
  451. label=_('Priority'),
  452. help_text=_("The priority of the device in the virtual chassis")
  453. )
  454. config_template = DynamicModelChoiceField(
  455. label=_('Config template'),
  456. queryset=ConfigTemplate.objects.all(),
  457. required=False
  458. )
  459. class Meta:
  460. model = Device
  461. fields = [
  462. 'name', 'role', 'device_type', 'serial', 'asset_tag', 'site', 'rack', 'location', 'position', 'face',
  463. 'latitude', 'longitude', 'status', 'airflow', 'platform', 'primary_ip4', 'primary_ip6', 'oob_ip', 'cluster',
  464. 'tenant_group', 'tenant', 'virtual_chassis', 'vc_position', 'vc_priority', 'description', 'config_template',
  465. 'comments', 'tags', 'local_context_data',
  466. ]
  467. def __init__(self, *args, **kwargs):
  468. super().__init__(*args, **kwargs)
  469. if self.instance.pk:
  470. # Compile list of choices for primary IPv4 and IPv6 addresses
  471. oob_ip_choices = [(None, '---------')]
  472. for family in [4, 6]:
  473. ip_choices = [(None, '---------')]
  474. # Gather PKs of all interfaces belonging to this Device or a peer VirtualChassis member
  475. interface_ids = self.instance.vc_interfaces(if_master=False).values_list('pk', flat=True)
  476. # Collect interface IPs
  477. interface_ips = IPAddress.objects.filter(
  478. address__family=family,
  479. assigned_object_type=ContentType.objects.get_for_model(Interface),
  480. assigned_object_id__in=interface_ids
  481. ).prefetch_related('assigned_object')
  482. if interface_ips:
  483. ip_list = [(ip.id, f'{ip.address} ({ip.assigned_object})') for ip in interface_ips]
  484. ip_choices.append(('Interface IPs', ip_list))
  485. oob_ip_choices.extend(ip_list)
  486. # Collect NAT IPs
  487. nat_ips = IPAddress.objects.prefetch_related('nat_inside').filter(
  488. address__family=family,
  489. nat_inside__assigned_object_type=ContentType.objects.get_for_model(Interface),
  490. nat_inside__assigned_object_id__in=interface_ids
  491. ).prefetch_related('assigned_object')
  492. if nat_ips:
  493. ip_list = [(ip.id, f'{ip.address} (NAT)') for ip in nat_ips]
  494. ip_choices.append(('NAT IPs', ip_list))
  495. self.fields['primary_ip{}'.format(family)].choices = ip_choices
  496. self.fields['oob_ip'].choices = oob_ip_choices
  497. # If editing an existing device, exclude it from the list of occupied rack units. This ensures that a device
  498. # can be flipped from one face to another.
  499. self.fields['position'].widget.add_query_param('exclude', self.instance.pk)
  500. # Disable rack assignment if this is a child device installed in a parent device
  501. if self.instance.device_type.is_child_device and hasattr(self.instance, 'parent_bay'):
  502. self.fields['site'].disabled = True
  503. self.fields['rack'].disabled = True
  504. self.initial['site'] = self.instance.parent_bay.device.site_id
  505. self.initial['rack'] = self.instance.parent_bay.device.rack_id
  506. else:
  507. # An object that doesn't exist yet can't have any IPs assigned to it
  508. self.fields['primary_ip4'].choices = []
  509. self.fields['primary_ip4'].widget.attrs['readonly'] = True
  510. self.fields['primary_ip6'].choices = []
  511. self.fields['primary_ip6'].widget.attrs['readonly'] = True
  512. self.fields['oob_ip'].choices = []
  513. self.fields['oob_ip'].widget.attrs['readonly'] = True
  514. # Rack position
  515. position = self.data.get('position') or self.initial.get('position')
  516. if position:
  517. self.fields['position'].widget.choices = [(position, f'U{position}')]
  518. class ModuleForm(ModuleCommonForm, NetBoxModelForm):
  519. device = DynamicModelChoiceField(
  520. label=_('Device'),
  521. queryset=Device.objects.all(),
  522. initial_params={
  523. 'modulebays': '$module_bay'
  524. }
  525. )
  526. module_bay = DynamicModelChoiceField(
  527. label=_('Module bay'),
  528. queryset=ModuleBay.objects.all(),
  529. query_params={
  530. 'device_id': '$device'
  531. }
  532. )
  533. module_type = DynamicModelChoiceField(
  534. label=_('Module type'),
  535. queryset=ModuleType.objects.all(),
  536. context={
  537. 'parent': 'manufacturer',
  538. },
  539. selector=True
  540. )
  541. comments = CommentField()
  542. replicate_components = forms.BooleanField(
  543. label=_('Replicate components'),
  544. required=False,
  545. initial=True,
  546. help_text=_("Automatically populate components associated with this module type")
  547. )
  548. adopt_components = forms.BooleanField(
  549. label=_('Adopt components'),
  550. required=False,
  551. initial=False,
  552. help_text=_("Adopt already existing components")
  553. )
  554. fieldsets = (
  555. (_('Module'), ('device', 'module_bay', 'module_type', 'status', 'description', 'tags')),
  556. (_('Hardware'), (
  557. 'serial', 'asset_tag', 'replicate_components', 'adopt_components',
  558. )),
  559. )
  560. class Meta:
  561. model = Module
  562. fields = [
  563. 'device', 'module_bay', 'module_type', 'status', 'serial', 'asset_tag', 'tags', 'replicate_components',
  564. 'adopt_components', 'description', 'comments',
  565. ]
  566. def __init__(self, *args, **kwargs):
  567. super().__init__(*args, **kwargs)
  568. if self.instance.pk:
  569. self.fields['device'].disabled = True
  570. self.fields['replicate_components'].initial = False
  571. self.fields['replicate_components'].disabled = True
  572. self.fields['adopt_components'].initial = False
  573. self.fields['adopt_components'].disabled = True
  574. class CableForm(TenancyForm, NetBoxModelForm):
  575. comments = CommentField()
  576. class Meta:
  577. model = Cable
  578. fields = [
  579. 'type', 'status', 'tenant_group', 'tenant', 'label', 'color', 'length', 'length_unit', 'description',
  580. 'comments', 'tags',
  581. ]
  582. error_messages = {
  583. 'length': {
  584. 'max_value': _('Maximum length is 32767 (any unit)')
  585. }
  586. }
  587. class PowerPanelForm(NetBoxModelForm):
  588. site = DynamicModelChoiceField(
  589. label=_('Site'),
  590. queryset=Site.objects.all(),
  591. selector=True
  592. )
  593. location = DynamicModelChoiceField(
  594. label=_('Location'),
  595. queryset=Location.objects.all(),
  596. required=False,
  597. query_params={
  598. 'site_id': '$site'
  599. }
  600. )
  601. comments = CommentField()
  602. fieldsets = (
  603. ('Power Panel', ('site', 'location', 'name', 'description', 'tags')),
  604. )
  605. class Meta:
  606. model = PowerPanel
  607. fields = [
  608. 'site', 'location', 'name', 'description', 'comments', 'tags',
  609. ]
  610. class PowerFeedForm(TenancyForm, NetBoxModelForm):
  611. power_panel = DynamicModelChoiceField(
  612. label=_('Power panel'),
  613. queryset=PowerPanel.objects.all(),
  614. selector=True
  615. )
  616. rack = DynamicModelChoiceField(
  617. label=_('Rack'),
  618. queryset=Rack.objects.all(),
  619. required=False,
  620. selector=True
  621. )
  622. comments = CommentField()
  623. fieldsets = (
  624. (_('Power Feed'), ('power_panel', 'rack', 'name', 'status', 'type', 'description', 'mark_connected', 'tags')),
  625. (_('Characteristics'), ('supply', 'voltage', 'amperage', 'phase', 'max_utilization')),
  626. (_('Tenancy'), ('tenant_group', 'tenant')),
  627. )
  628. class Meta:
  629. model = PowerFeed
  630. fields = [
  631. 'power_panel', 'rack', 'name', 'status', 'type', 'mark_connected', 'supply', 'phase', 'voltage', 'amperage',
  632. 'max_utilization', 'tenant_group', 'tenant', 'description', 'comments', 'tags'
  633. ]
  634. #
  635. # Virtual chassis
  636. #
  637. class VirtualChassisForm(NetBoxModelForm):
  638. master = forms.ModelChoiceField(
  639. label=_('Master'),
  640. queryset=Device.objects.all(),
  641. required=False,
  642. )
  643. comments = CommentField()
  644. class Meta:
  645. model = VirtualChassis
  646. fields = [
  647. 'name', 'domain', 'master', 'description', 'comments', 'tags',
  648. ]
  649. widgets = {
  650. 'master': SelectWithPK(),
  651. }
  652. def __init__(self, *args, **kwargs):
  653. super().__init__(*args, **kwargs)
  654. self.fields['master'].queryset = Device.objects.filter(virtual_chassis=self.instance)
  655. class DeviceVCMembershipForm(forms.ModelForm):
  656. class Meta:
  657. model = Device
  658. fields = [
  659. 'vc_position', 'vc_priority',
  660. ]
  661. labels = {
  662. 'vc_position': 'Position',
  663. 'vc_priority': 'Priority',
  664. }
  665. def __init__(self, validate_vc_position=False, *args, **kwargs):
  666. super().__init__(*args, **kwargs)
  667. # Require VC position (only required when the Device is a VirtualChassis member)
  668. self.fields['vc_position'].required = True
  669. # Add bootstrap classes to form elements.
  670. self.fields['vc_position'].widget.attrs = {'class': 'form-control'}
  671. self.fields['vc_priority'].widget.attrs = {'class': 'form-control'}
  672. # Validation of vc_position is optional. This is only required when adding a new member to an existing
  673. # VirtualChassis. Otherwise, vc_position validation is handled by BaseVCMemberFormSet.
  674. self.validate_vc_position = validate_vc_position
  675. def clean_vc_position(self):
  676. vc_position = self.cleaned_data['vc_position']
  677. if self.validate_vc_position:
  678. conflicting_members = Device.objects.filter(
  679. virtual_chassis=self.instance.virtual_chassis,
  680. vc_position=vc_position
  681. )
  682. if conflicting_members.exists():
  683. raise forms.ValidationError(
  684. 'A virtual chassis member already exists in position {}.'.format(vc_position)
  685. )
  686. return vc_position
  687. class VCMemberSelectForm(forms.Form):
  688. device = DynamicModelChoiceField(
  689. label=_('Device'),
  690. queryset=Device.objects.all(),
  691. query_params={
  692. 'virtual_chassis_id': 'null',
  693. },
  694. selector=True
  695. )
  696. def clean_device(self):
  697. device = self.cleaned_data['device']
  698. if device.virtual_chassis is not None:
  699. raise forms.ValidationError(
  700. f"Device {device} is already assigned to a virtual chassis."
  701. )
  702. return device
  703. #
  704. # Device component templates
  705. #
  706. class ComponentTemplateForm(forms.ModelForm):
  707. device_type = DynamicModelChoiceField(
  708. label=_('Device type'),
  709. queryset=DeviceType.objects.all(),
  710. context={
  711. 'parent': 'manufacturer',
  712. }
  713. )
  714. def __init__(self, *args, **kwargs):
  715. super().__init__(*args, **kwargs)
  716. # Disable reassignment of DeviceType when editing an existing instance
  717. if self.instance.pk:
  718. self.fields['device_type'].disabled = True
  719. class ModularComponentTemplateForm(ComponentTemplateForm):
  720. device_type = DynamicModelChoiceField(
  721. label=_('Device type'),
  722. queryset=DeviceType.objects.all().all(),
  723. required=False,
  724. context={
  725. 'parent': 'manufacturer',
  726. }
  727. )
  728. module_type = DynamicModelChoiceField(
  729. label=_('Module type'),
  730. queryset=ModuleType.objects.all(),
  731. required=False,
  732. context={
  733. 'parent': 'manufacturer',
  734. }
  735. )
  736. def __init__(self, *args, **kwargs):
  737. super().__init__(*args, **kwargs)
  738. # Disable reassignment of ModuleType when editing an existing instance
  739. if self.instance.pk:
  740. self.fields['module_type'].disabled = True
  741. class ConsolePortTemplateForm(ModularComponentTemplateForm):
  742. fieldsets = (
  743. (None, ('device_type', 'module_type', 'name', 'label', 'type', 'description')),
  744. )
  745. class Meta:
  746. model = ConsolePortTemplate
  747. fields = [
  748. 'device_type', 'module_type', 'name', 'label', 'type', 'description',
  749. ]
  750. class ConsoleServerPortTemplateForm(ModularComponentTemplateForm):
  751. fieldsets = (
  752. (None, ('device_type', 'module_type', 'name', 'label', 'type', 'description')),
  753. )
  754. class Meta:
  755. model = ConsoleServerPortTemplate
  756. fields = [
  757. 'device_type', 'module_type', 'name', 'label', 'type', 'description',
  758. ]
  759. class PowerPortTemplateForm(ModularComponentTemplateForm):
  760. fieldsets = (
  761. (None, (
  762. 'device_type', 'module_type', 'name', 'label', 'type', 'maximum_draw', 'allocated_draw', 'description',
  763. )),
  764. )
  765. class Meta:
  766. model = PowerPortTemplate
  767. fields = [
  768. 'device_type', 'module_type', 'name', 'label', 'type', 'maximum_draw', 'allocated_draw', 'description',
  769. ]
  770. class PowerOutletTemplateForm(ModularComponentTemplateForm):
  771. power_port = DynamicModelChoiceField(
  772. label=_('Power port'),
  773. queryset=PowerPortTemplate.objects.all(),
  774. required=False,
  775. query_params={
  776. 'devicetype_id': '$device_type',
  777. }
  778. )
  779. fieldsets = (
  780. (None, ('device_type', 'module_type', 'name', 'label', 'type', 'power_port', 'feed_leg', 'description')),
  781. )
  782. class Meta:
  783. model = PowerOutletTemplate
  784. fields = [
  785. 'device_type', 'module_type', 'name', 'label', 'type', 'power_port', 'feed_leg', 'description',
  786. ]
  787. class InterfaceTemplateForm(ModularComponentTemplateForm):
  788. bridge = DynamicModelChoiceField(
  789. label=_('Bridge'),
  790. queryset=InterfaceTemplate.objects.all(),
  791. required=False,
  792. query_params={
  793. 'devicetype_id': '$device_type',
  794. 'moduletype_id': '$module_type',
  795. }
  796. )
  797. fieldsets = (
  798. (None, ('device_type', 'module_type', 'name', 'label', 'type', 'enabled', 'mgmt_only', 'description', 'bridge')),
  799. (_('PoE'), ('poe_mode', 'poe_type')),
  800. (_('Wireless'), ('rf_role',)),
  801. )
  802. class Meta:
  803. model = InterfaceTemplate
  804. fields = [
  805. 'device_type', 'module_type', 'name', 'label', 'type', 'mgmt_only', 'enabled', 'description', 'poe_mode', 'poe_type', 'bridge', 'rf_role',
  806. ]
  807. class FrontPortTemplateForm(ModularComponentTemplateForm):
  808. rear_port = DynamicModelChoiceField(
  809. label=_('Rear port'),
  810. queryset=RearPortTemplate.objects.all(),
  811. required=False,
  812. query_params={
  813. 'devicetype_id': '$device_type',
  814. 'moduletype_id': '$module_type',
  815. }
  816. )
  817. fieldsets = (
  818. (None, (
  819. 'device_type', 'module_type', 'name', 'label', 'type', 'color', 'rear_port', 'rear_port_position',
  820. 'description',
  821. )),
  822. )
  823. class Meta:
  824. model = FrontPortTemplate
  825. fields = [
  826. 'device_type', 'module_type', 'name', 'label', 'type', 'color', 'rear_port', 'rear_port_position',
  827. 'description',
  828. ]
  829. class RearPortTemplateForm(ModularComponentTemplateForm):
  830. fieldsets = (
  831. (None, ('device_type', 'module_type', 'name', 'label', 'type', 'color', 'positions', 'description')),
  832. )
  833. class Meta:
  834. model = RearPortTemplate
  835. fields = [
  836. 'device_type', 'module_type', 'name', 'label', 'type', 'color', 'positions', 'description',
  837. ]
  838. class ModuleBayTemplateForm(ComponentTemplateForm):
  839. fieldsets = (
  840. (None, ('device_type', 'name', 'label', 'position', 'description')),
  841. )
  842. class Meta:
  843. model = ModuleBayTemplate
  844. fields = [
  845. 'device_type', 'name', 'label', 'position', 'description',
  846. ]
  847. class DeviceBayTemplateForm(ComponentTemplateForm):
  848. fieldsets = (
  849. (None, ('device_type', 'name', 'label', 'description')),
  850. )
  851. class Meta:
  852. model = DeviceBayTemplate
  853. fields = [
  854. 'device_type', 'name', 'label', 'description',
  855. ]
  856. class InventoryItemTemplateForm(ComponentTemplateForm):
  857. parent = DynamicModelChoiceField(
  858. label=_('Parent'),
  859. queryset=InventoryItemTemplate.objects.all(),
  860. required=False,
  861. query_params={
  862. 'devicetype_id': '$device_type'
  863. }
  864. )
  865. role = DynamicModelChoiceField(
  866. label=_('Role'),
  867. queryset=InventoryItemRole.objects.all(),
  868. required=False
  869. )
  870. manufacturer = DynamicModelChoiceField(
  871. label=_('Manufacturer'),
  872. queryset=Manufacturer.objects.all(),
  873. required=False
  874. )
  875. component_type = ContentTypeChoiceField(
  876. queryset=ContentType.objects.all(),
  877. limit_choices_to=MODULAR_COMPONENT_TEMPLATE_MODELS,
  878. required=False,
  879. widget=forms.HiddenInput
  880. )
  881. component_id = forms.IntegerField(
  882. required=False,
  883. widget=forms.HiddenInput
  884. )
  885. fieldsets = (
  886. (None, (
  887. 'device_type', 'parent', 'name', 'label', 'role', 'manufacturer', 'part_id', 'description',
  888. 'component_type', 'component_id',
  889. )),
  890. )
  891. class Meta:
  892. model = InventoryItemTemplate
  893. fields = [
  894. 'device_type', 'parent', 'name', 'label', 'role', 'manufacturer', 'part_id', 'description',
  895. 'component_type', 'component_id',
  896. ]
  897. #
  898. # Device components
  899. #
  900. class DeviceComponentForm(NetBoxModelForm):
  901. device = DynamicModelChoiceField(
  902. label=_('Device'),
  903. queryset=Device.objects.all(),
  904. selector=True
  905. )
  906. def __init__(self, *args, **kwargs):
  907. super().__init__(*args, **kwargs)
  908. # Disable reassignment of Device when editing an existing instance
  909. if self.instance.pk:
  910. self.fields['device'].disabled = True
  911. class ModularDeviceComponentForm(DeviceComponentForm):
  912. module = DynamicModelChoiceField(
  913. label=_('Module'),
  914. queryset=Module.objects.all(),
  915. required=False,
  916. query_params={
  917. 'device_id': '$device',
  918. }
  919. )
  920. class ConsolePortForm(ModularDeviceComponentForm):
  921. fieldsets = (
  922. (None, (
  923. 'device', 'module', 'name', 'label', 'type', 'speed', 'mark_connected', 'description', 'tags',
  924. )),
  925. )
  926. class Meta:
  927. model = ConsolePort
  928. fields = [
  929. 'device', 'module', 'name', 'label', 'type', 'speed', 'mark_connected', 'description', 'tags',
  930. ]
  931. class ConsoleServerPortForm(ModularDeviceComponentForm):
  932. fieldsets = (
  933. (None, (
  934. 'device', 'module', 'name', 'label', 'type', 'speed', 'mark_connected', 'description', 'tags',
  935. )),
  936. )
  937. class Meta:
  938. model = ConsoleServerPort
  939. fields = [
  940. 'device', 'module', 'name', 'label', 'type', 'speed', 'mark_connected', 'description', 'tags',
  941. ]
  942. class PowerPortForm(ModularDeviceComponentForm):
  943. fieldsets = (
  944. (None, (
  945. 'device', 'module', 'name', 'label', 'type', 'maximum_draw', 'allocated_draw', 'mark_connected',
  946. 'description', 'tags',
  947. )),
  948. )
  949. class Meta:
  950. model = PowerPort
  951. fields = [
  952. 'device', 'module', 'name', 'label', 'type', 'maximum_draw', 'allocated_draw', 'mark_connected',
  953. 'description', 'tags',
  954. ]
  955. class PowerOutletForm(ModularDeviceComponentForm):
  956. power_port = DynamicModelChoiceField(
  957. label=_('Power port'),
  958. queryset=PowerPort.objects.all(),
  959. required=False,
  960. query_params={
  961. 'device_id': '$device',
  962. }
  963. )
  964. fieldsets = (
  965. (None, (
  966. 'device', 'module', 'name', 'label', 'type', 'power_port', 'feed_leg', 'mark_connected', 'description',
  967. 'tags',
  968. )),
  969. )
  970. class Meta:
  971. model = PowerOutlet
  972. fields = [
  973. 'device', 'module', 'name', 'label', 'type', 'power_port', 'feed_leg', 'mark_connected', 'description',
  974. 'tags',
  975. ]
  976. class InterfaceForm(InterfaceCommonForm, ModularDeviceComponentForm):
  977. vdcs = DynamicModelMultipleChoiceField(
  978. queryset=VirtualDeviceContext.objects.all(),
  979. required=False,
  980. label=_('Virtual device contexts'),
  981. initial_params={
  982. 'interfaces': '$parent',
  983. },
  984. query_params={
  985. 'device_id': '$device',
  986. }
  987. )
  988. parent = DynamicModelChoiceField(
  989. queryset=Interface.objects.all(),
  990. required=False,
  991. label=_('Parent interface'),
  992. query_params={
  993. 'virtual_chassis_member_id': '$device',
  994. }
  995. )
  996. bridge = DynamicModelChoiceField(
  997. queryset=Interface.objects.all(),
  998. required=False,
  999. label=_('Bridged interface'),
  1000. query_params={
  1001. 'virtual_chassis_member_id': '$device',
  1002. }
  1003. )
  1004. lag = DynamicModelChoiceField(
  1005. queryset=Interface.objects.all(),
  1006. required=False,
  1007. label=_('LAG interface'),
  1008. query_params={
  1009. 'virtual_chassis_member_id': '$device',
  1010. 'type': 'lag',
  1011. }
  1012. )
  1013. wireless_lan_group = DynamicModelChoiceField(
  1014. queryset=WirelessLANGroup.objects.all(),
  1015. required=False,
  1016. label=_('Wireless LAN group')
  1017. )
  1018. wireless_lans = DynamicModelMultipleChoiceField(
  1019. queryset=WirelessLAN.objects.all(),
  1020. required=False,
  1021. label=_('Wireless LANs'),
  1022. query_params={
  1023. 'group_id': '$wireless_lan_group',
  1024. }
  1025. )
  1026. vlan_group = DynamicModelChoiceField(
  1027. queryset=VLANGroup.objects.all(),
  1028. required=False,
  1029. label=_('VLAN group')
  1030. )
  1031. untagged_vlan = DynamicModelChoiceField(
  1032. queryset=VLAN.objects.all(),
  1033. required=False,
  1034. label=_('Untagged VLAN'),
  1035. query_params={
  1036. 'group_id': '$vlan_group',
  1037. 'available_on_device': '$device',
  1038. }
  1039. )
  1040. tagged_vlans = DynamicModelMultipleChoiceField(
  1041. queryset=VLAN.objects.all(),
  1042. required=False,
  1043. label=_('Tagged VLANs'),
  1044. query_params={
  1045. 'group_id': '$vlan_group',
  1046. 'available_on_device': '$device',
  1047. }
  1048. )
  1049. vrf = DynamicModelChoiceField(
  1050. queryset=VRF.objects.all(),
  1051. required=False,
  1052. label=_('VRF')
  1053. )
  1054. wwn = forms.CharField(
  1055. empty_value=None,
  1056. required=False,
  1057. label=_('WWN')
  1058. )
  1059. fieldsets = (
  1060. (_('Interface'), ('device', 'module', 'name', 'label', 'type', 'speed', 'duplex', 'description', 'tags')),
  1061. (_('Addressing'), ('vrf', 'mac_address', 'wwn')),
  1062. (_('Operation'), ('vdcs', 'mtu', 'tx_power', 'enabled', 'mgmt_only', 'mark_connected')),
  1063. (_('Related Interfaces'), ('parent', 'bridge', 'lag')),
  1064. (_('PoE'), ('poe_mode', 'poe_type')),
  1065. (_('802.1Q Switching'), ('mode', 'vlan_group', 'untagged_vlan', 'tagged_vlans')),
  1066. (_('Wireless'), (
  1067. 'rf_role', 'rf_channel', 'rf_channel_frequency', 'rf_channel_width', 'wireless_lan_group', 'wireless_lans',
  1068. )),
  1069. )
  1070. class Meta:
  1071. model = Interface
  1072. fields = [
  1073. 'device', 'module', 'vdcs', 'name', 'label', 'type', 'speed', 'duplex', 'enabled', 'parent', 'bridge', 'lag',
  1074. 'mac_address', 'wwn', 'mtu', 'mgmt_only', 'mark_connected', 'description', 'poe_mode', 'poe_type', 'mode',
  1075. 'rf_role', 'rf_channel', 'rf_channel_frequency', 'rf_channel_width', 'tx_power', 'wireless_lans',
  1076. 'untagged_vlan', 'tagged_vlans', 'vrf', 'tags',
  1077. ]
  1078. widgets = {
  1079. 'speed': NumberWithOptions(
  1080. options=InterfaceSpeedChoices
  1081. ),
  1082. 'mode': HTMXSelect(),
  1083. }
  1084. labels = {
  1085. 'mode': '802.1Q Mode',
  1086. }
  1087. class FrontPortForm(ModularDeviceComponentForm):
  1088. rear_port = DynamicModelChoiceField(
  1089. queryset=RearPort.objects.all(),
  1090. query_params={
  1091. 'device_id': '$device',
  1092. }
  1093. )
  1094. fieldsets = (
  1095. (None, (
  1096. 'device', 'module', 'name', 'label', 'type', 'color', 'rear_port', 'rear_port_position', 'mark_connected',
  1097. 'description', 'tags',
  1098. )),
  1099. )
  1100. class Meta:
  1101. model = FrontPort
  1102. fields = [
  1103. 'device', 'module', 'name', 'label', 'type', 'color', 'rear_port', 'rear_port_position', 'mark_connected',
  1104. 'description', 'tags',
  1105. ]
  1106. class RearPortForm(ModularDeviceComponentForm):
  1107. fieldsets = (
  1108. (None, (
  1109. 'device', 'module', 'name', 'label', 'type', 'color', 'positions', 'mark_connected', 'description', 'tags',
  1110. )),
  1111. )
  1112. class Meta:
  1113. model = RearPort
  1114. fields = [
  1115. 'device', 'module', 'name', 'label', 'type', 'color', 'positions', 'mark_connected', 'description', 'tags',
  1116. ]
  1117. class ModuleBayForm(DeviceComponentForm):
  1118. fieldsets = (
  1119. (None, ('device', 'name', 'label', 'position', 'description', 'tags',)),
  1120. )
  1121. class Meta:
  1122. model = ModuleBay
  1123. fields = [
  1124. 'device', 'name', 'label', 'position', 'description', 'tags',
  1125. ]
  1126. class DeviceBayForm(DeviceComponentForm):
  1127. fieldsets = (
  1128. (None, ('device', 'name', 'label', 'description', 'tags',)),
  1129. )
  1130. class Meta:
  1131. model = DeviceBay
  1132. fields = [
  1133. 'device', 'name', 'label', 'description', 'tags',
  1134. ]
  1135. class PopulateDeviceBayForm(forms.Form):
  1136. installed_device = forms.ModelChoiceField(
  1137. queryset=Device.objects.all(),
  1138. label=_('Child Device'),
  1139. help_text=_("Child devices must first be created and assigned to the site and rack of the parent device.")
  1140. )
  1141. def __init__(self, device_bay, *args, **kwargs):
  1142. super().__init__(*args, **kwargs)
  1143. self.fields['installed_device'].queryset = Device.objects.filter(
  1144. site=device_bay.device.site,
  1145. rack=device_bay.device.rack,
  1146. parent_bay__isnull=True,
  1147. device_type__u_height=0,
  1148. device_type__subdevice_role=SubdeviceRoleChoices.ROLE_CHILD
  1149. ).exclude(pk=device_bay.device.pk)
  1150. class InventoryItemForm(DeviceComponentForm):
  1151. parent = DynamicModelChoiceField(
  1152. label=_('Parent'),
  1153. queryset=InventoryItem.objects.all(),
  1154. required=False,
  1155. query_params={
  1156. 'device_id': '$device'
  1157. }
  1158. )
  1159. role = DynamicModelChoiceField(
  1160. label=_('Role'),
  1161. queryset=InventoryItemRole.objects.all(),
  1162. required=False
  1163. )
  1164. manufacturer = DynamicModelChoiceField(
  1165. label=_('Manufacturer'),
  1166. queryset=Manufacturer.objects.all(),
  1167. required=False
  1168. )
  1169. # Assigned component selectors
  1170. consoleport = DynamicModelChoiceField(
  1171. queryset=ConsolePort.objects.all(),
  1172. required=False,
  1173. query_params={
  1174. 'device_id': '$device'
  1175. },
  1176. label=_('Console port')
  1177. )
  1178. consoleserverport = DynamicModelChoiceField(
  1179. queryset=ConsoleServerPort.objects.all(),
  1180. required=False,
  1181. query_params={
  1182. 'device_id': '$device'
  1183. },
  1184. label=_('Console server port')
  1185. )
  1186. frontport = DynamicModelChoiceField(
  1187. queryset=FrontPort.objects.all(),
  1188. required=False,
  1189. query_params={
  1190. 'device_id': '$device'
  1191. },
  1192. label=_('Front port')
  1193. )
  1194. interface = DynamicModelChoiceField(
  1195. queryset=Interface.objects.all(),
  1196. required=False,
  1197. query_params={
  1198. 'device_id': '$device'
  1199. },
  1200. label=_('Interface')
  1201. )
  1202. poweroutlet = DynamicModelChoiceField(
  1203. queryset=PowerOutlet.objects.all(),
  1204. required=False,
  1205. query_params={
  1206. 'device_id': '$device'
  1207. },
  1208. label=_('Power outlet')
  1209. )
  1210. powerport = DynamicModelChoiceField(
  1211. queryset=PowerPort.objects.all(),
  1212. required=False,
  1213. query_params={
  1214. 'device_id': '$device'
  1215. },
  1216. label=_('Power port')
  1217. )
  1218. rearport = DynamicModelChoiceField(
  1219. queryset=RearPort.objects.all(),
  1220. required=False,
  1221. query_params={
  1222. 'device_id': '$device'
  1223. },
  1224. label=_('Rear port')
  1225. )
  1226. fieldsets = (
  1227. (_('Inventory Item'), ('device', 'parent', 'name', 'label', 'role', 'description', 'tags')),
  1228. (_('Hardware'), ('manufacturer', 'part_id', 'serial', 'asset_tag')),
  1229. )
  1230. class Meta:
  1231. model = InventoryItem
  1232. fields = [
  1233. 'device', 'parent', 'name', 'label', 'role', 'manufacturer', 'part_id', 'serial', 'asset_tag',
  1234. 'description', 'tags',
  1235. ]
  1236. def __init__(self, *args, **kwargs):
  1237. instance = kwargs.get('instance')
  1238. initial = kwargs.get('initial', {}).copy()
  1239. component_type = initial.get('component_type')
  1240. component_id = initial.get('component_id')
  1241. # Used for picking the default active tab for component selection
  1242. self.no_component = True
  1243. if instance:
  1244. # When editing set the initial value for component selectin
  1245. for component_model in ContentType.objects.filter(MODULAR_COMPONENT_MODELS):
  1246. if type(instance.component) is component_model.model_class():
  1247. initial[component_model.model] = instance.component
  1248. self.no_component = False
  1249. break
  1250. elif component_type and component_id:
  1251. # When adding the InventoryItem from a component page
  1252. if content_type := ContentType.objects.filter(MODULAR_COMPONENT_MODELS).filter(pk=component_type).first():
  1253. if component := content_type.model_class().objects.filter(pk=component_id).first():
  1254. initial[content_type.model] = component
  1255. self.no_component = False
  1256. kwargs['initial'] = initial
  1257. super().__init__(*args, **kwargs)
  1258. # Specifically allow editing the device of IntentoryItems
  1259. if self.instance.pk:
  1260. self.fields['device'].disabled = False
  1261. def clean(self):
  1262. super().clean()
  1263. # Handle object assignment
  1264. selected_objects = [
  1265. field for field in (
  1266. 'consoleport', 'consoleserverport', 'frontport', 'interface', 'poweroutlet', 'powerport', 'rearport'
  1267. ) if self.cleaned_data[field]
  1268. ]
  1269. if len(selected_objects) > 1:
  1270. raise forms.ValidationError(_("An InventoryItem can only be assigned to a single component."))
  1271. elif selected_objects:
  1272. self.instance.component = self.cleaned_data[selected_objects[0]]
  1273. else:
  1274. self.instance.component = None
  1275. # Device component roles
  1276. #
  1277. class InventoryItemRoleForm(NetBoxModelForm):
  1278. slug = SlugField()
  1279. fieldsets = (
  1280. (_('Inventory Item Role'), (
  1281. 'name', 'slug', 'color', 'description', 'tags',
  1282. )),
  1283. )
  1284. class Meta:
  1285. model = InventoryItemRole
  1286. fields = [
  1287. 'name', 'slug', 'color', 'description', 'tags',
  1288. ]
  1289. class VirtualDeviceContextForm(TenancyForm, NetBoxModelForm):
  1290. device = DynamicModelChoiceField(
  1291. label=_('Device'),
  1292. queryset=Device.objects.all(),
  1293. selector=True
  1294. )
  1295. primary_ip4 = DynamicModelChoiceField(
  1296. queryset=IPAddress.objects.all(),
  1297. label=_('Primary IPv4'),
  1298. required=False,
  1299. query_params={
  1300. 'device_id': '$device',
  1301. 'family': '4',
  1302. }
  1303. )
  1304. primary_ip6 = DynamicModelChoiceField(
  1305. queryset=IPAddress.objects.all(),
  1306. label=_('Primary IPv6'),
  1307. required=False,
  1308. query_params={
  1309. 'device_id': '$device',
  1310. 'family': '6',
  1311. }
  1312. )
  1313. fieldsets = (
  1314. (_('Virtual Device Context'), ('device', 'name', 'status', 'identifier', 'primary_ip4', 'primary_ip6', 'tags')),
  1315. (_('Tenancy'), ('tenant_group', 'tenant'))
  1316. )
  1317. class Meta:
  1318. model = VirtualDeviceContext
  1319. fields = [
  1320. 'device', 'name', 'status', 'identifier', 'primary_ip4', 'primary_ip6', 'tenant_group', 'tenant',
  1321. 'comments', 'tags'
  1322. ]