model_forms.py 48 KB

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