model_forms.py 50 KB

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