model_forms.py 53 KB

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