bulk_import.py 50 KB

12345678910111213141516171819202122232425262728293031323334353637383940414243444546474849505152535455565758596061626364656667686970717273747576777879808182838485868788899091929394959697989910010110210310410510610710810911011111211311411511611711811912012112212312412512612712812913013113213313413513613713813914014114214314414514614714814915015115215315415515615715815916016116216316416516616716816917017117217317417517617717817918018118218318418518618718818919019119219319419519619719819920020120220320420520620720820921021121221321421521621721821922022122222322422522622722822923023123223323423523623723823924024124224324424524624724824925025125225325425525625725825926026126226326426526626726826927027127227327427527627727827928028128228328428528628728828929029129229329429529629729829930030130230330430530630730830931031131231331431531631731831932032132232332432532632732832933033133233333433533633733833934034134234334434534634734834935035135235335435535635735835936036136236336436536636736836937037137237337437537637737837938038138238338438538638738838939039139239339439539639739839940040140240340440540640740840941041141241341441541641741841942042142242342442542642742842943043143243343443543643743843944044144244344444544644744844945045145245345445545645745845946046146246346446546646746846947047147247347447547647747847948048148248348448548648748848949049149249349449549649749849950050150250350450550650750850951051151251351451551651751851952052152252352452552652752852953053153253353453553653753853954054154254354454554654754854955055155255355455555655755855956056156256356456556656756856957057157257357457557657757857958058158258358458558658758858959059159259359459559659759859960060160260360460560660760860961061161261361461561661761861962062162262362462562662762862963063163263363463563663763863964064164264364464564664764864965065165265365465565665765865966066166266366466566666766866967067167267367467567667767867968068168268368468568668768868969069169269369469569669769869970070170270370470570670770870971071171271371471571671771871972072172272372472572672772872973073173273373473573673773873974074174274374474574674774874975075175275375475575675775875976076176276376476576676776876977077177277377477577677777877978078178278378478578678778878979079179279379479579679779879980080180280380480580680780880981081181281381481581681781881982082182282382482582682782882983083183283383483583683783883984084184284384484584684784884985085185285385485585685785885986086186286386486586686786886987087187287387487587687787887988088188288388488588688788888989089189289389489589689789889990090190290390490590690790890991091191291391491591691791891992092192292392492592692792892993093193293393493593693793893994094194294394494594694794894995095195295395495595695795895996096196296396496596696796896997097197297397497597697797897998098198298398498598698798898999099199299399499599699799899910001001100210031004100510061007100810091010101110121013101410151016101710181019102010211022102310241025102610271028102910301031103210331034103510361037103810391040104110421043104410451046104710481049105010511052105310541055105610571058105910601061106210631064106510661067106810691070107110721073107410751076107710781079108010811082108310841085108610871088108910901091109210931094109510961097109810991100110111021103110411051106110711081109111011111112111311141115111611171118111911201121112211231124112511261127112811291130113111321133113411351136113711381139114011411142114311441145114611471148114911501151115211531154115511561157115811591160116111621163116411651166116711681169117011711172117311741175117611771178117911801181118211831184118511861187118811891190119111921193119411951196119711981199120012011202120312041205120612071208120912101211121212131214121512161217121812191220122112221223122412251226122712281229123012311232123312341235123612371238123912401241124212431244124512461247124812491250125112521253125412551256125712581259126012611262126312641265126612671268126912701271127212731274127512761277127812791280128112821283128412851286128712881289129012911292129312941295129612971298129913001301130213031304130513061307130813091310131113121313131413151316131713181319132013211322132313241325132613271328132913301331133213331334133513361337133813391340134113421343134413451346134713481349135013511352135313541355135613571358135913601361136213631364136513661367136813691370137113721373137413751376137713781379138013811382138313841385138613871388138913901391139213931394139513961397139813991400140114021403140414051406140714081409141014111412141314141415141614171418141914201421142214231424142514261427142814291430143114321433143414351436143714381439144014411442144314441445144614471448144914501451145214531454145514561457145814591460146114621463146414651466146714681469147014711472147314741475147614771478147914801481148214831484148514861487148814891490149114921493149414951496149714981499150015011502150315041505150615071508150915101511151215131514151515161517151815191520152115221523152415251526152715281529153015311532153315341535153615371538153915401541154215431544154515461547154815491550155115521553155415551556155715581559156015611562156315641565156615671568156915701571157215731574157515761577157815791580158115821583158415851586158715881589
  1. from django import forms
  2. from django.contrib.contenttypes.models import ContentType
  3. from django.contrib.postgres.forms.array import SimpleArrayField
  4. from django.core.exceptions import ObjectDoesNotExist
  5. from django.utils.safestring import mark_safe
  6. from django.utils.translation import gettext_lazy as _
  7. from dcim.choices import *
  8. from dcim.constants import *
  9. from dcim.models import *
  10. from extras.models import ConfigTemplate
  11. from ipam.models import VRF, IPAddress
  12. from netbox.choices import *
  13. from netbox.forms import NetBoxModelImportForm
  14. from tenancy.models import Tenant
  15. from utilities.forms.fields import (
  16. CSVChoiceField, CSVContentTypeField, CSVModelChoiceField, CSVModelMultipleChoiceField, CSVTypedChoiceField,
  17. SlugField,
  18. )
  19. from virtualization.models import Cluster, VMInterface, VirtualMachine
  20. from wireless.choices import WirelessRoleChoices
  21. from .common import ModuleCommonForm
  22. __all__ = (
  23. 'CableImportForm',
  24. 'ConsolePortImportForm',
  25. 'ConsoleServerPortImportForm',
  26. 'DeviceBayImportForm',
  27. 'DeviceImportForm',
  28. 'DeviceRoleImportForm',
  29. 'DeviceTypeImportForm',
  30. 'FrontPortImportForm',
  31. 'InterfaceImportForm',
  32. 'InventoryItemImportForm',
  33. 'InventoryItemRoleImportForm',
  34. 'LocationImportForm',
  35. 'MACAddressImportForm',
  36. 'ManufacturerImportForm',
  37. 'ModuleImportForm',
  38. 'ModuleBayImportForm',
  39. 'ModuleTypeImportForm',
  40. 'PlatformImportForm',
  41. 'PowerFeedImportForm',
  42. 'PowerOutletImportForm',
  43. 'PowerPanelImportForm',
  44. 'PowerPortImportForm',
  45. 'RackImportForm',
  46. 'RackReservationImportForm',
  47. 'RackRoleImportForm',
  48. 'RackTypeImportForm',
  49. 'RearPortImportForm',
  50. 'RegionImportForm',
  51. 'SiteImportForm',
  52. 'SiteGroupImportForm',
  53. 'VirtualChassisImportForm',
  54. 'VirtualDeviceContextImportForm'
  55. )
  56. class RegionImportForm(NetBoxModelImportForm):
  57. parent = CSVModelChoiceField(
  58. label=_('Parent'),
  59. queryset=Region.objects.all(),
  60. required=False,
  61. to_field_name='name',
  62. help_text=_('Name of parent region')
  63. )
  64. class Meta:
  65. model = Region
  66. fields = ('name', 'slug', 'parent', 'description', 'tags', 'comments')
  67. class SiteGroupImportForm(NetBoxModelImportForm):
  68. parent = CSVModelChoiceField(
  69. label=_('Parent'),
  70. queryset=SiteGroup.objects.all(),
  71. required=False,
  72. to_field_name='name',
  73. help_text=_('Name of parent site group')
  74. )
  75. class Meta:
  76. model = SiteGroup
  77. fields = ('name', 'slug', 'parent', 'description', 'comments', 'tags')
  78. class SiteImportForm(NetBoxModelImportForm):
  79. status = CSVChoiceField(
  80. label=_('Status'),
  81. choices=SiteStatusChoices,
  82. help_text=_('Operational status')
  83. )
  84. region = CSVModelChoiceField(
  85. label=_('Region'),
  86. queryset=Region.objects.all(),
  87. required=False,
  88. to_field_name='name',
  89. help_text=_('Assigned region')
  90. )
  91. group = CSVModelChoiceField(
  92. label=_('Group'),
  93. queryset=SiteGroup.objects.all(),
  94. required=False,
  95. to_field_name='name',
  96. help_text=_('Assigned group')
  97. )
  98. tenant = CSVModelChoiceField(
  99. label=_('Tenant'),
  100. queryset=Tenant.objects.all(),
  101. required=False,
  102. to_field_name='name',
  103. help_text=_('Assigned tenant')
  104. )
  105. class Meta:
  106. model = Site
  107. fields = (
  108. 'name', 'slug', 'status', 'region', 'group', 'tenant', 'facility', 'time_zone', 'description',
  109. 'physical_address', 'shipping_address', 'latitude', 'longitude', 'comments', 'tags'
  110. )
  111. help_texts = {
  112. 'time_zone': mark_safe(
  113. '{} (<a href="https://en.wikipedia.org/wiki/List_of_tz_database_time_zones">{}</a>)'.format(
  114. _('Time zone'), _('available options')
  115. )
  116. )
  117. }
  118. class LocationImportForm(NetBoxModelImportForm):
  119. site = CSVModelChoiceField(
  120. label=_('Site'),
  121. queryset=Site.objects.all(),
  122. to_field_name='name',
  123. help_text=_('Assigned site')
  124. )
  125. parent = CSVModelChoiceField(
  126. label=_('Parent'),
  127. queryset=Location.objects.all(),
  128. required=False,
  129. to_field_name='name',
  130. help_text=_('Parent location'),
  131. error_messages={
  132. 'invalid_choice': _('Location not found.'),
  133. }
  134. )
  135. status = CSVChoiceField(
  136. label=_('Status'),
  137. choices=LocationStatusChoices,
  138. help_text=_('Operational status')
  139. )
  140. tenant = CSVModelChoiceField(
  141. label=_('Tenant'),
  142. queryset=Tenant.objects.all(),
  143. required=False,
  144. to_field_name='name',
  145. help_text=_('Assigned tenant')
  146. )
  147. class Meta:
  148. model = Location
  149. fields = (
  150. 'site', 'parent', 'name', 'slug', 'status', 'tenant', 'facility', 'description',
  151. 'tags', 'comments',
  152. )
  153. def __init__(self, data=None, *args, **kwargs):
  154. super().__init__(data, *args, **kwargs)
  155. if data:
  156. # Limit location queryset by assigned site
  157. params = {f"site__{self.fields['site'].to_field_name}": data.get('site')}
  158. self.fields['parent'].queryset = self.fields['parent'].queryset.filter(**params)
  159. class RackRoleImportForm(NetBoxModelImportForm):
  160. slug = SlugField()
  161. class Meta:
  162. model = RackRole
  163. fields = ('name', 'slug', 'color', 'description', 'tags')
  164. class RackTypeImportForm(NetBoxModelImportForm):
  165. manufacturer = forms.ModelChoiceField(
  166. label=_('Manufacturer'),
  167. queryset=Manufacturer.objects.all(),
  168. to_field_name='name',
  169. help_text=_('The manufacturer of this rack type')
  170. )
  171. form_factor = CSVChoiceField(
  172. label=_('Type'),
  173. choices=RackFormFactorChoices,
  174. required=False,
  175. help_text=_('Form factor')
  176. )
  177. starting_unit = forms.IntegerField(
  178. required=False,
  179. min_value=1,
  180. help_text=_('The lowest-numbered position in the rack')
  181. )
  182. width = forms.ChoiceField(
  183. label=_('Width'),
  184. choices=RackWidthChoices,
  185. help_text=_('Rail-to-rail width (in inches)')
  186. )
  187. outer_unit = CSVChoiceField(
  188. label=_('Outer unit'),
  189. choices=RackDimensionUnitChoices,
  190. required=False,
  191. help_text=_('Unit for outer dimensions')
  192. )
  193. weight_unit = CSVChoiceField(
  194. label=_('Weight unit'),
  195. choices=WeightUnitChoices,
  196. required=False,
  197. help_text=_('Unit for rack weights')
  198. )
  199. class Meta:
  200. model = RackType
  201. fields = (
  202. 'manufacturer', 'model', 'slug', 'form_factor', 'width', 'u_height', 'starting_unit', 'desc_units',
  203. 'outer_width', 'outer_height', 'outer_depth', 'outer_unit', 'mounting_depth', 'weight', 'max_weight',
  204. 'weight_unit', 'description', 'comments', 'tags',
  205. )
  206. def __init__(self, data=None, *args, **kwargs):
  207. super().__init__(data, *args, **kwargs)
  208. class RackImportForm(NetBoxModelImportForm):
  209. site = CSVModelChoiceField(
  210. label=_('Site'),
  211. queryset=Site.objects.all(),
  212. to_field_name='name'
  213. )
  214. location = CSVModelChoiceField(
  215. label=_('Location'),
  216. queryset=Location.objects.all(),
  217. required=False,
  218. to_field_name='name'
  219. )
  220. tenant = CSVModelChoiceField(
  221. label=_('Tenant'),
  222. queryset=Tenant.objects.all(),
  223. required=False,
  224. to_field_name='name',
  225. help_text=_('Name of assigned tenant')
  226. )
  227. status = CSVChoiceField(
  228. label=_('Status'),
  229. choices=RackStatusChoices,
  230. help_text=_('Operational status')
  231. )
  232. role = CSVModelChoiceField(
  233. label=_('Role'),
  234. queryset=RackRole.objects.all(),
  235. required=False,
  236. to_field_name='name',
  237. help_text=_('Name of assigned role')
  238. )
  239. rack_type = CSVModelChoiceField(
  240. label=_('Rack type'),
  241. queryset=RackType.objects.all(),
  242. to_field_name='model',
  243. required=False,
  244. help_text=_('Rack type model')
  245. )
  246. form_factor = CSVChoiceField(
  247. label=_('Type'),
  248. choices=RackFormFactorChoices,
  249. required=False,
  250. help_text=_('Form factor')
  251. )
  252. width = forms.ChoiceField(
  253. label=_('Width'),
  254. choices=RackWidthChoices,
  255. required=False,
  256. help_text=_('Rail-to-rail width (in inches)')
  257. )
  258. u_height = forms.IntegerField(
  259. required=False,
  260. label=_('Height (U)')
  261. )
  262. outer_unit = CSVChoiceField(
  263. label=_('Outer unit'),
  264. choices=RackDimensionUnitChoices,
  265. required=False,
  266. help_text=_('Unit for outer dimensions')
  267. )
  268. airflow = CSVChoiceField(
  269. label=_('Airflow'),
  270. choices=RackAirflowChoices,
  271. required=False,
  272. help_text=_('Airflow direction')
  273. )
  274. weight_unit = CSVChoiceField(
  275. label=_('Weight unit'),
  276. choices=WeightUnitChoices,
  277. required=False,
  278. help_text=_('Unit for rack weights')
  279. )
  280. class Meta:
  281. model = Rack
  282. fields = (
  283. 'site', 'location', 'name', 'facility_id', 'tenant', 'status', 'role', 'rack_type', 'form_factor', 'serial',
  284. 'asset_tag', 'width', 'u_height', 'desc_units', 'outer_width', 'outer_height', 'outer_depth', 'outer_unit',
  285. 'mounting_depth', 'airflow', 'weight', 'max_weight', 'weight_unit', 'description', 'comments', 'tags',
  286. )
  287. def __init__(self, data=None, *args, **kwargs):
  288. super().__init__(data, *args, **kwargs)
  289. if data:
  290. # Limit location queryset by assigned site
  291. params = {f"site__{self.fields['site'].to_field_name}": data.get('site')}
  292. self.fields['location'].queryset = self.fields['location'].queryset.filter(**params)
  293. def clean(self):
  294. super().clean()
  295. # width & u_height must be set if not specifying a rack type on import
  296. if not self.instance.pk:
  297. if not self.cleaned_data.get('rack_type') and not self.cleaned_data.get('width'):
  298. raise forms.ValidationError(_("Width must be set if not specifying a rack type."))
  299. if not self.cleaned_data.get('rack_type') and not self.cleaned_data.get('u_height'):
  300. raise forms.ValidationError(_("U height must be set if not specifying a rack type."))
  301. class RackReservationImportForm(NetBoxModelImportForm):
  302. site = CSVModelChoiceField(
  303. label=_('Site'),
  304. queryset=Site.objects.all(),
  305. to_field_name='name',
  306. help_text=_('Parent site')
  307. )
  308. location = CSVModelChoiceField(
  309. label=_('Location'),
  310. queryset=Location.objects.all(),
  311. to_field_name='name',
  312. required=False,
  313. help_text=_("Rack's location (if any)")
  314. )
  315. rack = CSVModelChoiceField(
  316. label=_('Rack'),
  317. queryset=Rack.objects.all(),
  318. to_field_name='name',
  319. help_text=_('Rack')
  320. )
  321. units = SimpleArrayField(
  322. label=_('Units'),
  323. base_field=forms.IntegerField(),
  324. required=True,
  325. help_text=_('Comma-separated list of individual unit numbers')
  326. )
  327. tenant = CSVModelChoiceField(
  328. label=_('Tenant'),
  329. queryset=Tenant.objects.all(),
  330. required=False,
  331. to_field_name='name',
  332. help_text=_('Assigned tenant')
  333. )
  334. class Meta:
  335. model = RackReservation
  336. fields = ('site', 'location', 'rack', 'units', 'tenant', 'description', 'comments', 'tags')
  337. def __init__(self, data=None, *args, **kwargs):
  338. super().__init__(data, *args, **kwargs)
  339. if data:
  340. # Limit location queryset by assigned site
  341. params = {f"site__{self.fields['site'].to_field_name}": data.get('site')}
  342. self.fields['location'].queryset = self.fields['location'].queryset.filter(**params)
  343. # Limit rack queryset by assigned site and group
  344. params = {
  345. f"site__{self.fields['site'].to_field_name}": data.get('site'),
  346. f"location__{self.fields['location'].to_field_name}": data.get('location'),
  347. }
  348. self.fields['rack'].queryset = self.fields['rack'].queryset.filter(**params)
  349. class ManufacturerImportForm(NetBoxModelImportForm):
  350. class Meta:
  351. model = Manufacturer
  352. fields = ('name', 'slug', 'description', 'tags')
  353. class DeviceTypeImportForm(NetBoxModelImportForm):
  354. manufacturer = CSVModelChoiceField(
  355. label=_('Manufacturer'),
  356. queryset=Manufacturer.objects.all(),
  357. to_field_name='name',
  358. help_text=_('The manufacturer which produces this device type')
  359. )
  360. default_platform = CSVModelChoiceField(
  361. label=_('Default platform'),
  362. queryset=Platform.objects.all(),
  363. to_field_name='name',
  364. required=False,
  365. help_text=_('The default platform for devices of this type (optional)')
  366. )
  367. weight = forms.DecimalField(
  368. label=_('Weight'),
  369. required=False,
  370. help_text=_('Device weight'),
  371. )
  372. weight_unit = CSVChoiceField(
  373. label=_('Weight unit'),
  374. choices=WeightUnitChoices,
  375. required=False,
  376. help_text=_('Unit for device weight')
  377. )
  378. class Meta:
  379. model = DeviceType
  380. fields = [
  381. 'manufacturer', 'default_platform', 'model', 'slug', 'part_number', 'u_height', 'exclude_from_utilization',
  382. 'is_full_depth', 'subdevice_role', 'airflow', 'description', 'weight', 'weight_unit', 'comments', 'tags',
  383. ]
  384. class ModuleTypeImportForm(NetBoxModelImportForm):
  385. manufacturer = forms.ModelChoiceField(
  386. label=_('Manufacturer'),
  387. queryset=Manufacturer.objects.all(),
  388. to_field_name='name'
  389. )
  390. airflow = CSVChoiceField(
  391. label=_('Airflow'),
  392. choices=ModuleAirflowChoices,
  393. required=False,
  394. help_text=_('Airflow direction')
  395. )
  396. weight = forms.DecimalField(
  397. label=_('Weight'),
  398. required=False,
  399. help_text=_('Module weight'),
  400. )
  401. weight_unit = CSVChoiceField(
  402. label=_('Weight unit'),
  403. choices=WeightUnitChoices,
  404. required=False,
  405. help_text=_('Unit for module weight')
  406. )
  407. class Meta:
  408. model = ModuleType
  409. fields = [
  410. 'manufacturer', 'model', 'part_number', 'description', 'airflow', 'weight', 'weight_unit', 'comments',
  411. 'tags',
  412. ]
  413. class DeviceRoleImportForm(NetBoxModelImportForm):
  414. config_template = CSVModelChoiceField(
  415. label=_('Config template'),
  416. queryset=ConfigTemplate.objects.all(),
  417. to_field_name='name',
  418. required=False,
  419. help_text=_('Config template')
  420. )
  421. slug = SlugField()
  422. class Meta:
  423. model = DeviceRole
  424. fields = ('name', 'slug', 'color', 'vm_role', 'config_template', 'description', 'tags')
  425. class PlatformImportForm(NetBoxModelImportForm):
  426. slug = SlugField()
  427. manufacturer = CSVModelChoiceField(
  428. label=_('Manufacturer'),
  429. queryset=Manufacturer.objects.all(),
  430. required=False,
  431. to_field_name='name',
  432. help_text=_('Limit platform assignments to this manufacturer')
  433. )
  434. config_template = CSVModelChoiceField(
  435. label=_('Config template'),
  436. queryset=ConfigTemplate.objects.all(),
  437. to_field_name='name',
  438. required=False,
  439. help_text=_('Config template')
  440. )
  441. class Meta:
  442. model = Platform
  443. fields = (
  444. 'name', 'slug', 'manufacturer', 'config_template', 'description', 'tags',
  445. )
  446. class BaseDeviceImportForm(NetBoxModelImportForm):
  447. role = CSVModelChoiceField(
  448. label=_('Device role'),
  449. queryset=DeviceRole.objects.all(),
  450. to_field_name='name',
  451. help_text=_('Assigned role')
  452. )
  453. tenant = CSVModelChoiceField(
  454. label=_('Tenant'),
  455. queryset=Tenant.objects.all(),
  456. required=False,
  457. to_field_name='name',
  458. help_text=_('Assigned tenant')
  459. )
  460. manufacturer = CSVModelChoiceField(
  461. label=_('Manufacturer'),
  462. queryset=Manufacturer.objects.all(),
  463. to_field_name='name',
  464. help_text=_('Device type manufacturer')
  465. )
  466. device_type = CSVModelChoiceField(
  467. label=_('Device type'),
  468. queryset=DeviceType.objects.all(),
  469. to_field_name='model',
  470. help_text=_('Device type model')
  471. )
  472. platform = CSVModelChoiceField(
  473. label=_('Platform'),
  474. queryset=Platform.objects.all(),
  475. required=False,
  476. to_field_name='name',
  477. help_text=_('Assigned platform')
  478. )
  479. status = CSVChoiceField(
  480. label=_('Status'),
  481. choices=DeviceStatusChoices,
  482. help_text=_('Operational status')
  483. )
  484. virtual_chassis = CSVModelChoiceField(
  485. label=_('Virtual chassis'),
  486. queryset=VirtualChassis.objects.all(),
  487. to_field_name='name',
  488. required=False,
  489. help_text=_('Virtual chassis')
  490. )
  491. cluster = CSVModelChoiceField(
  492. label=_('Cluster'),
  493. queryset=Cluster.objects.all(),
  494. to_field_name='name',
  495. required=False,
  496. help_text=_('Virtualization cluster')
  497. )
  498. class Meta:
  499. fields = []
  500. model = Device
  501. def __init__(self, data=None, *args, **kwargs):
  502. super().__init__(data, *args, **kwargs)
  503. if data:
  504. # Limit device type queryset by manufacturer
  505. params = {f"manufacturer__{self.fields['manufacturer'].to_field_name}": data.get('manufacturer')}
  506. self.fields['device_type'].queryset = self.fields['device_type'].queryset.filter(**params)
  507. class DeviceImportForm(BaseDeviceImportForm):
  508. site = CSVModelChoiceField(
  509. label=_('Site'),
  510. queryset=Site.objects.all(),
  511. to_field_name='name',
  512. help_text=_('Assigned site')
  513. )
  514. location = CSVModelChoiceField(
  515. label=_('Location'),
  516. queryset=Location.objects.all(),
  517. to_field_name='name',
  518. required=False,
  519. help_text=_("Assigned location (if any)")
  520. )
  521. rack = CSVModelChoiceField(
  522. label=_('Rack'),
  523. queryset=Rack.objects.all(),
  524. to_field_name='name',
  525. required=False,
  526. help_text=_("Assigned rack (if any)")
  527. )
  528. face = CSVChoiceField(
  529. label=_('Face'),
  530. choices=DeviceFaceChoices,
  531. required=False,
  532. help_text=_('Mounted rack face')
  533. )
  534. parent = CSVModelChoiceField(
  535. label=_('Parent'),
  536. queryset=Device.objects.all(),
  537. to_field_name='name',
  538. required=False,
  539. help_text=_('Parent device (for child devices)')
  540. )
  541. device_bay = CSVModelChoiceField(
  542. label=_('Device bay'),
  543. queryset=DeviceBay.objects.all(),
  544. to_field_name='name',
  545. required=False,
  546. help_text=_('Device bay in which this device is installed (for child devices)')
  547. )
  548. airflow = CSVChoiceField(
  549. label=_('Airflow'),
  550. choices=DeviceAirflowChoices,
  551. required=False,
  552. help_text=_('Airflow direction')
  553. )
  554. config_template = CSVModelChoiceField(
  555. label=_('Config template'),
  556. queryset=ConfigTemplate.objects.all(),
  557. to_field_name='name',
  558. required=False,
  559. help_text=_('Config template')
  560. )
  561. class Meta(BaseDeviceImportForm.Meta):
  562. fields = [
  563. 'name', 'role', 'tenant', 'manufacturer', 'device_type', 'platform', 'serial', 'asset_tag', 'status',
  564. 'site', 'location', 'rack', 'position', 'face', 'latitude', 'longitude', 'parent', 'device_bay', 'airflow',
  565. 'virtual_chassis', 'vc_position', 'vc_priority', 'cluster', 'description', 'config_template', 'comments',
  566. 'tags',
  567. ]
  568. def __init__(self, data=None, *args, **kwargs):
  569. super().__init__(data, *args, **kwargs)
  570. if data:
  571. # Limit location queryset by assigned site
  572. params = {f"site__{self.fields['site'].to_field_name}": data.get('site')}
  573. self.fields['location'].queryset = self.fields['location'].queryset.filter(**params)
  574. self.fields['parent'].queryset = self.fields['parent'].queryset.filter(**params)
  575. # Limit rack queryset by assigned site and location
  576. params = {
  577. f"site__{self.fields['site'].to_field_name}": data.get('site'),
  578. }
  579. if location := data.get('location'):
  580. params.update({
  581. f"location__{self.fields['location'].to_field_name}": location,
  582. })
  583. self.fields['rack'].queryset = self.fields['rack'].queryset.filter(**params)
  584. # Limit device bay queryset by parent device
  585. if parent := data.get('parent'):
  586. params = {f"device__{self.fields['parent'].to_field_name}": parent}
  587. self.fields['device_bay'].queryset = self.fields['device_bay'].queryset.filter(**params)
  588. def clean(self):
  589. super().clean()
  590. # Inherit site and rack from parent device
  591. if parent := self.cleaned_data.get('parent'):
  592. self.instance.site = parent.site
  593. self.instance.rack = parent.rack
  594. # Set parent_bay reverse relationship
  595. if device_bay := self.cleaned_data.get('device_bay'):
  596. self.instance.parent_bay = device_bay
  597. class ModuleImportForm(ModuleCommonForm, NetBoxModelImportForm):
  598. device = CSVModelChoiceField(
  599. label=_('Device'),
  600. queryset=Device.objects.all(),
  601. to_field_name='name',
  602. help_text=_('The device in which this module is installed')
  603. )
  604. module_bay = CSVModelChoiceField(
  605. label=_('Module bay'),
  606. queryset=ModuleBay.objects.all(),
  607. to_field_name='name',
  608. help_text=_('The module bay in which this module is installed')
  609. )
  610. module_type = CSVModelChoiceField(
  611. label=_('Module type'),
  612. queryset=ModuleType.objects.all(),
  613. to_field_name='model',
  614. help_text=_('The type of module')
  615. )
  616. status = CSVChoiceField(
  617. label=_('Status'),
  618. choices=ModuleStatusChoices,
  619. help_text=_('Operational status')
  620. )
  621. replicate_components = forms.BooleanField(
  622. label=_('Replicate components'),
  623. required=False,
  624. help_text=_('Automatically populate components associated with this module type (enabled by default)')
  625. )
  626. adopt_components = forms.BooleanField(
  627. label=_('Adopt components'),
  628. required=False,
  629. help_text=_('Adopt already existing components')
  630. )
  631. class Meta:
  632. model = Module
  633. fields = (
  634. 'device', 'module_bay', 'module_type', 'serial', 'asset_tag', 'status', 'description', 'comments',
  635. 'replicate_components', 'adopt_components', 'tags',
  636. )
  637. def __init__(self, data=None, *args, **kwargs):
  638. super().__init__(data, *args, **kwargs)
  639. if data:
  640. # Limit module_bay queryset by assigned device
  641. params = {f"device__{self.fields['device'].to_field_name}": data.get('device')}
  642. self.fields['module_bay'].queryset = self.fields['module_bay'].queryset.filter(**params)
  643. def clean_replicate_components(self):
  644. # Make sure replicate_components is True when it's not included in the uploaded data
  645. if 'replicate_components' not in self.data:
  646. return True
  647. else:
  648. return self.cleaned_data['replicate_components']
  649. #
  650. # Device components
  651. #
  652. class ConsolePortImportForm(NetBoxModelImportForm):
  653. device = CSVModelChoiceField(
  654. label=_('Device'),
  655. queryset=Device.objects.all(),
  656. to_field_name='name'
  657. )
  658. type = CSVChoiceField(
  659. label=_('Type'),
  660. choices=ConsolePortTypeChoices,
  661. required=False,
  662. help_text=_('Port type')
  663. )
  664. speed = CSVTypedChoiceField(
  665. label=_('Speed'),
  666. choices=ConsolePortSpeedChoices,
  667. coerce=int,
  668. empty_value=None,
  669. required=False,
  670. help_text=_('Port speed in bps')
  671. )
  672. class Meta:
  673. model = ConsolePort
  674. fields = ('device', 'name', 'label', 'type', 'speed', 'mark_connected', 'description', 'tags')
  675. class ConsoleServerPortImportForm(NetBoxModelImportForm):
  676. device = CSVModelChoiceField(
  677. label=_('Device'),
  678. queryset=Device.objects.all(),
  679. to_field_name='name'
  680. )
  681. type = CSVChoiceField(
  682. label=_('Type'),
  683. choices=ConsolePortTypeChoices,
  684. required=False,
  685. help_text=_('Port type')
  686. )
  687. speed = CSVTypedChoiceField(
  688. label=_('Speed'),
  689. choices=ConsolePortSpeedChoices,
  690. coerce=int,
  691. empty_value=None,
  692. required=False,
  693. help_text=_('Port speed in bps')
  694. )
  695. class Meta:
  696. model = ConsoleServerPort
  697. fields = ('device', 'name', 'label', 'type', 'speed', 'mark_connected', 'description', 'tags')
  698. class PowerPortImportForm(NetBoxModelImportForm):
  699. device = CSVModelChoiceField(
  700. label=_('Device'),
  701. queryset=Device.objects.all(),
  702. to_field_name='name'
  703. )
  704. type = CSVChoiceField(
  705. label=_('Type'),
  706. choices=PowerPortTypeChoices,
  707. required=False,
  708. help_text=_('Port type')
  709. )
  710. class Meta:
  711. model = PowerPort
  712. fields = (
  713. 'device', 'name', 'label', 'type', 'mark_connected', 'maximum_draw', 'allocated_draw', 'description', 'tags'
  714. )
  715. class PowerOutletImportForm(NetBoxModelImportForm):
  716. device = CSVModelChoiceField(
  717. label=_('Device'),
  718. queryset=Device.objects.all(),
  719. to_field_name='name'
  720. )
  721. type = CSVChoiceField(
  722. label=_('Type'),
  723. choices=PowerOutletTypeChoices,
  724. required=False,
  725. help_text=_('Outlet type')
  726. )
  727. power_port = CSVModelChoiceField(
  728. label=_('Power port'),
  729. queryset=PowerPort.objects.all(),
  730. required=False,
  731. to_field_name='name',
  732. help_text=_('Local power port which feeds this outlet')
  733. )
  734. feed_leg = CSVChoiceField(
  735. label=_('Feed leg'),
  736. choices=PowerOutletFeedLegChoices,
  737. required=False,
  738. help_text=_('Electrical phase (for three-phase circuits)')
  739. )
  740. class Meta:
  741. model = PowerOutlet
  742. fields = (
  743. 'device', 'name', 'label', 'type', 'color', 'mark_connected', 'power_port', 'feed_leg', 'description',
  744. 'tags',
  745. )
  746. def __init__(self, *args, **kwargs):
  747. super().__init__(*args, **kwargs)
  748. # Limit PowerPort choices to those belonging to this device (or VC master)
  749. if self.is_bound and 'device' in self.data:
  750. try:
  751. device = self.fields['device'].to_python(self.data['device'])
  752. except forms.ValidationError:
  753. device = None
  754. else:
  755. try:
  756. device = self.instance.device
  757. except Device.DoesNotExist:
  758. device = None
  759. if device:
  760. self.fields['power_port'].queryset = PowerPort.objects.filter(
  761. device__in=[device, device.get_vc_master()]
  762. )
  763. else:
  764. self.fields['power_port'].queryset = PowerPort.objects.none()
  765. class InterfaceImportForm(NetBoxModelImportForm):
  766. device = CSVModelChoiceField(
  767. label=_('Device'),
  768. queryset=Device.objects.all(),
  769. to_field_name='name'
  770. )
  771. parent = CSVModelChoiceField(
  772. label=_('Parent'),
  773. queryset=Interface.objects.all(),
  774. required=False,
  775. to_field_name='name',
  776. help_text=_('Parent interface')
  777. )
  778. bridge = CSVModelChoiceField(
  779. label=_('Bridge'),
  780. queryset=Interface.objects.all(),
  781. required=False,
  782. to_field_name='name',
  783. help_text=_('Bridged interface')
  784. )
  785. lag = CSVModelChoiceField(
  786. label=_('Lag'),
  787. queryset=Interface.objects.all(),
  788. required=False,
  789. to_field_name='name',
  790. help_text=_('Parent LAG interface')
  791. )
  792. vdcs = CSVModelMultipleChoiceField(
  793. label=_('Vdcs'),
  794. queryset=VirtualDeviceContext.objects.all(),
  795. required=False,
  796. to_field_name='name',
  797. help_text=mark_safe(
  798. _('VDC names separated by commas, encased with double quotes. Example:') + ' <code>vdc1,vdc2,vdc3</code>'
  799. )
  800. )
  801. type = CSVChoiceField(
  802. label=_('Type'),
  803. choices=InterfaceTypeChoices,
  804. help_text=_('Physical medium')
  805. )
  806. duplex = CSVChoiceField(
  807. label=_('Duplex'),
  808. choices=InterfaceDuplexChoices,
  809. required=False
  810. )
  811. poe_mode = CSVChoiceField(
  812. label=_('Poe mode'),
  813. choices=InterfacePoEModeChoices,
  814. required=False,
  815. help_text=_('PoE mode')
  816. )
  817. poe_type = CSVChoiceField(
  818. label=_('Poe type'),
  819. choices=InterfacePoETypeChoices,
  820. required=False,
  821. help_text=_('PoE type')
  822. )
  823. mode = CSVChoiceField(
  824. label=_('Mode'),
  825. choices=InterfaceModeChoices,
  826. required=False,
  827. help_text=_('IEEE 802.1Q operational mode (for L2 interfaces)')
  828. )
  829. vrf = CSVModelChoiceField(
  830. label=_('VRF'),
  831. queryset=VRF.objects.all(),
  832. required=False,
  833. to_field_name='rd',
  834. help_text=_('Assigned VRF')
  835. )
  836. rf_role = CSVChoiceField(
  837. label=_('Rf role'),
  838. choices=WirelessRoleChoices,
  839. required=False,
  840. help_text=_('Wireless role (AP/station)')
  841. )
  842. class Meta:
  843. model = Interface
  844. fields = (
  845. 'device', 'name', 'label', 'parent', 'bridge', 'lag', 'type', 'speed', 'duplex', 'enabled',
  846. 'mark_connected', 'wwn', 'vdcs', 'mtu', 'mgmt_only', 'description', 'poe_mode', 'poe_type', 'mode',
  847. 'vrf', 'rf_role', 'rf_channel', 'rf_channel_frequency', 'rf_channel_width', 'tx_power', 'tags'
  848. )
  849. def __init__(self, data=None, *args, **kwargs):
  850. super().__init__(data, *args, **kwargs)
  851. if data:
  852. # Limit choices for parent, bridge, and LAG interfaces to the assigned device
  853. if device := data.get('device'):
  854. params = {
  855. f"device__{self.fields['device'].to_field_name}": device
  856. }
  857. self.fields['parent'].queryset = self.fields['parent'].queryset.filter(**params)
  858. self.fields['bridge'].queryset = self.fields['bridge'].queryset.filter(**params)
  859. self.fields['lag'].queryset = self.fields['lag'].queryset.filter(**params)
  860. self.fields['vdcs'].queryset = self.fields['vdcs'].queryset.filter(**params)
  861. def clean_enabled(self):
  862. # Make sure enabled is True when it's not included in the uploaded data
  863. if 'enabled' not in self.data:
  864. return True
  865. else:
  866. return self.cleaned_data['enabled']
  867. def clean_vdcs(self):
  868. for vdc in self.cleaned_data['vdcs']:
  869. if vdc.device != self.cleaned_data['device']:
  870. raise forms.ValidationError(
  871. _("VDC {vdc} is not assigned to device {device}").format(
  872. vdc=vdc, device=self.cleaned_data['device']
  873. )
  874. )
  875. return self.cleaned_data['vdcs']
  876. class FrontPortImportForm(NetBoxModelImportForm):
  877. device = CSVModelChoiceField(
  878. label=_('Device'),
  879. queryset=Device.objects.all(),
  880. to_field_name='name'
  881. )
  882. rear_port = CSVModelChoiceField(
  883. label=_('Rear port'),
  884. queryset=RearPort.objects.all(),
  885. to_field_name='name',
  886. help_text=_('Corresponding rear port')
  887. )
  888. type = CSVChoiceField(
  889. label=_('Type'),
  890. choices=PortTypeChoices,
  891. help_text=_('Physical medium classification')
  892. )
  893. class Meta:
  894. model = FrontPort
  895. fields = (
  896. 'device', 'name', 'label', 'type', 'color', 'mark_connected', 'rear_port', 'rear_port_position',
  897. 'description', 'tags'
  898. )
  899. def __init__(self, *args, **kwargs):
  900. super().__init__(*args, **kwargs)
  901. # Limit RearPort choices to those belonging to this device (or VC master)
  902. if self.is_bound and 'device' in self.data:
  903. try:
  904. device = self.fields['device'].to_python(self.data['device'])
  905. except forms.ValidationError:
  906. device = None
  907. else:
  908. try:
  909. device = self.instance.device
  910. except Device.DoesNotExist:
  911. device = None
  912. if device:
  913. self.fields['rear_port'].queryset = RearPort.objects.filter(
  914. device__in=[device, device.get_vc_master()]
  915. )
  916. else:
  917. self.fields['rear_port'].queryset = RearPort.objects.none()
  918. class RearPortImportForm(NetBoxModelImportForm):
  919. device = CSVModelChoiceField(
  920. label=_('Device'),
  921. queryset=Device.objects.all(),
  922. to_field_name='name'
  923. )
  924. type = CSVChoiceField(
  925. label=_('Type'),
  926. help_text=_('Physical medium classification'),
  927. choices=PortTypeChoices,
  928. )
  929. class Meta:
  930. model = RearPort
  931. fields = ('device', 'name', 'label', 'type', 'color', 'mark_connected', 'positions', 'description', 'tags')
  932. class ModuleBayImportForm(NetBoxModelImportForm):
  933. device = CSVModelChoiceField(
  934. label=_('Device'),
  935. queryset=Device.objects.all(),
  936. to_field_name='name'
  937. )
  938. class Meta:
  939. model = ModuleBay
  940. fields = ('device', 'name', 'label', 'position', 'description', 'tags')
  941. class DeviceBayImportForm(NetBoxModelImportForm):
  942. device = CSVModelChoiceField(
  943. label=_('Device'),
  944. queryset=Device.objects.all(),
  945. to_field_name='name'
  946. )
  947. installed_device = CSVModelChoiceField(
  948. label=_('Installed device'),
  949. queryset=Device.objects.all(),
  950. required=False,
  951. to_field_name='name',
  952. help_text=_('Child device installed within this bay'),
  953. error_messages={
  954. 'invalid_choice': _('Child device not found.'),
  955. }
  956. )
  957. class Meta:
  958. model = DeviceBay
  959. fields = ('device', 'name', 'label', 'installed_device', 'description', 'tags')
  960. def __init__(self, *args, **kwargs):
  961. super().__init__(*args, **kwargs)
  962. # Limit installed device choices to devices of the correct type and location
  963. if self.is_bound and 'device' in self.data:
  964. try:
  965. device = self.fields['device'].to_python(self.data['device'])
  966. except forms.ValidationError:
  967. device = None
  968. else:
  969. try:
  970. device = self.instance.device
  971. except Device.DoesNotExist:
  972. device = None
  973. if device:
  974. self.fields['installed_device'].queryset = Device.objects.filter(
  975. site=device.site,
  976. rack=device.rack,
  977. parent_bay__isnull=True,
  978. device_type__u_height=0,
  979. device_type__subdevice_role=SubdeviceRoleChoices.ROLE_CHILD
  980. ).exclude(pk=device.pk)
  981. else:
  982. self.fields['installed_device'].queryset = Device.objects.none()
  983. class InventoryItemImportForm(NetBoxModelImportForm):
  984. device = CSVModelChoiceField(
  985. label=_('Device'),
  986. queryset=Device.objects.all(),
  987. to_field_name='name'
  988. )
  989. role = CSVModelChoiceField(
  990. label=_('Role'),
  991. queryset=InventoryItemRole.objects.all(),
  992. to_field_name='name',
  993. required=False
  994. )
  995. manufacturer = CSVModelChoiceField(
  996. label=_('Manufacturer'),
  997. queryset=Manufacturer.objects.all(),
  998. to_field_name='name',
  999. required=False
  1000. )
  1001. parent = CSVModelChoiceField(
  1002. label=_('Parent'),
  1003. queryset=Device.objects.all(),
  1004. to_field_name='name',
  1005. required=False,
  1006. help_text=_('Parent inventory item')
  1007. )
  1008. component_type = CSVContentTypeField(
  1009. label=_('Component type'),
  1010. queryset=ContentType.objects.all(),
  1011. limit_choices_to=MODULAR_COMPONENT_MODELS,
  1012. required=False,
  1013. help_text=_('Component Type')
  1014. )
  1015. component_name = forms.CharField(
  1016. label=_('Compnent name'),
  1017. required=False,
  1018. help_text=_('Component Name')
  1019. )
  1020. status = CSVChoiceField(
  1021. label=_('Status'),
  1022. choices=InventoryItemStatusChoices,
  1023. help_text=_('Operational status')
  1024. )
  1025. class Meta:
  1026. model = InventoryItem
  1027. fields = (
  1028. 'device', 'name', 'label', 'status', 'role', 'manufacturer', 'parent', 'part_id', 'serial', 'asset_tag',
  1029. 'discovered', 'description', 'tags', 'component_type', 'component_name',
  1030. )
  1031. def __init__(self, *args, **kwargs):
  1032. super().__init__(*args, **kwargs)
  1033. # Limit parent choices to inventory items belonging to this device
  1034. device = None
  1035. if self.is_bound and 'device' in self.data:
  1036. try:
  1037. device = self.fields['device'].to_python(self.data['device'])
  1038. except forms.ValidationError:
  1039. pass
  1040. if device:
  1041. self.fields['parent'].queryset = InventoryItem.objects.filter(device=device)
  1042. else:
  1043. self.fields['parent'].queryset = InventoryItem.objects.none()
  1044. def clean_component_name(self):
  1045. content_type = self.cleaned_data.get('component_type')
  1046. component_name = self.cleaned_data.get('component_name')
  1047. device = self.cleaned_data.get("device")
  1048. if not device and hasattr(self, 'instance') and hasattr(self.instance, 'device'):
  1049. device = self.instance.device
  1050. if not all([device, content_type, component_name]):
  1051. return None
  1052. model = content_type.model_class()
  1053. try:
  1054. component = model.objects.get(device=device, name=component_name)
  1055. self.instance.component = component
  1056. except ObjectDoesNotExist:
  1057. raise forms.ValidationError(
  1058. _("Component not found: {device} - {component_name}").format(
  1059. device=device, component_name=component_name
  1060. )
  1061. )
  1062. #
  1063. # Device component roles
  1064. #
  1065. class InventoryItemRoleImportForm(NetBoxModelImportForm):
  1066. slug = SlugField()
  1067. class Meta:
  1068. model = InventoryItemRole
  1069. fields = ('name', 'slug', 'color', 'description')
  1070. #
  1071. # Addressing
  1072. #
  1073. class MACAddressImportForm(NetBoxModelImportForm):
  1074. device = CSVModelChoiceField(
  1075. label=_('Device'),
  1076. queryset=Device.objects.all(),
  1077. required=False,
  1078. to_field_name='name',
  1079. help_text=_('Parent device of assigned interface (if any)')
  1080. )
  1081. virtual_machine = CSVModelChoiceField(
  1082. label=_('Virtual machine'),
  1083. queryset=VirtualMachine.objects.all(),
  1084. required=False,
  1085. to_field_name='name',
  1086. help_text=_('Parent VM of assigned interface (if any)')
  1087. )
  1088. interface = CSVModelChoiceField(
  1089. label=_('Interface'),
  1090. queryset=Interface.objects.none(), # Can also refer to VMInterface
  1091. required=False,
  1092. to_field_name='name',
  1093. help_text=_('Assigned interface')
  1094. )
  1095. is_primary = forms.BooleanField(
  1096. label=_('Is primary'),
  1097. help_text=_('Make this the primary MAC address for the assigned interface'),
  1098. required=False
  1099. )
  1100. class Meta:
  1101. model = MACAddress
  1102. fields = [
  1103. 'mac_address', 'device', 'virtual_machine', 'interface', 'is_primary', 'description', 'comments', 'tags',
  1104. ]
  1105. def __init__(self, data=None, *args, **kwargs):
  1106. super().__init__(data, *args, **kwargs)
  1107. if data:
  1108. # Limit interface queryset by assigned device
  1109. if data.get('device'):
  1110. self.fields['interface'].queryset = Interface.objects.filter(
  1111. **{f"device__{self.fields['device'].to_field_name}": data['device']}
  1112. )
  1113. # Limit interface queryset by assigned device
  1114. elif data.get('virtual_machine'):
  1115. self.fields['interface'].queryset = VMInterface.objects.filter(
  1116. **{f"virtual_machine__{self.fields['virtual_machine'].to_field_name}": data['virtual_machine']}
  1117. )
  1118. def clean(self):
  1119. super().clean()
  1120. device = self.cleaned_data.get('device')
  1121. virtual_machine = self.cleaned_data.get('virtual_machine')
  1122. interface = self.cleaned_data.get('interface')
  1123. # Validate interface assignment
  1124. if interface and not device and not virtual_machine:
  1125. raise forms.ValidationError({
  1126. "interface": _("Must specify the parent device or VM when assigning an interface")
  1127. })
  1128. def save(self, *args, **kwargs):
  1129. # Set interface assignment
  1130. if interface := self.cleaned_data.get('interface'):
  1131. self.instance.assigned_object = interface
  1132. instance = super().save(*args, **kwargs)
  1133. # Assign the MAC address as primary for its interface, if designated as such
  1134. if interface and self.cleaned_data['is_primary'] and self.instance.pk:
  1135. interface.primary_mac_address = self.instance
  1136. interface.save()
  1137. return instance
  1138. #
  1139. # Cables
  1140. #
  1141. class CableImportForm(NetBoxModelImportForm):
  1142. # Termination A
  1143. side_a_device = CSVModelChoiceField(
  1144. label=_('Side A device'),
  1145. queryset=Device.objects.all(),
  1146. to_field_name='name',
  1147. help_text=_('Device name')
  1148. )
  1149. side_a_type = CSVContentTypeField(
  1150. label=_('Side A type'),
  1151. queryset=ContentType.objects.all(),
  1152. limit_choices_to=CABLE_TERMINATION_MODELS,
  1153. help_text=_('Termination type')
  1154. )
  1155. side_a_name = forms.CharField(
  1156. label=_('Side A name'),
  1157. help_text=_('Termination name')
  1158. )
  1159. # Termination B
  1160. side_b_device = CSVModelChoiceField(
  1161. label=_('Side B device'),
  1162. queryset=Device.objects.all(),
  1163. to_field_name='name',
  1164. help_text=_('Device name')
  1165. )
  1166. side_b_type = CSVContentTypeField(
  1167. label=_('Side B type'),
  1168. queryset=ContentType.objects.all(),
  1169. limit_choices_to=CABLE_TERMINATION_MODELS,
  1170. help_text=_('Termination type')
  1171. )
  1172. side_b_name = forms.CharField(
  1173. label=_('Side B name'),
  1174. help_text=_('Termination name')
  1175. )
  1176. # Cable attributes
  1177. status = CSVChoiceField(
  1178. label=_('Status'),
  1179. choices=LinkStatusChoices,
  1180. required=False,
  1181. help_text=_('Connection status')
  1182. )
  1183. type = CSVChoiceField(
  1184. label=_('Type'),
  1185. choices=CableTypeChoices,
  1186. required=False,
  1187. help_text=_('Physical medium classification')
  1188. )
  1189. tenant = CSVModelChoiceField(
  1190. label=_('Tenant'),
  1191. queryset=Tenant.objects.all(),
  1192. required=False,
  1193. to_field_name='name',
  1194. help_text=_('Assigned tenant')
  1195. )
  1196. length_unit = CSVChoiceField(
  1197. label=_('Length unit'),
  1198. choices=CableLengthUnitChoices,
  1199. required=False,
  1200. help_text=_('Length unit')
  1201. )
  1202. class Meta:
  1203. model = Cable
  1204. fields = [
  1205. 'side_a_device', 'side_a_type', 'side_a_name', 'side_b_device', 'side_b_type', 'side_b_name', 'type',
  1206. 'status', 'tenant', 'label', 'color', 'length', 'length_unit', 'description', 'comments', 'tags',
  1207. ]
  1208. def _clean_side(self, side):
  1209. """
  1210. Derive a Cable's A/B termination objects.
  1211. :param side: 'a' or 'b'
  1212. """
  1213. assert side in 'ab', f"Invalid side designation: {side}"
  1214. device = self.cleaned_data.get(f'side_{side}_device')
  1215. content_type = self.cleaned_data.get(f'side_{side}_type')
  1216. name = self.cleaned_data.get(f'side_{side}_name')
  1217. if not device or not content_type or not name:
  1218. return None
  1219. model = content_type.model_class()
  1220. try:
  1221. if device.virtual_chassis and device.virtual_chassis.master == device and \
  1222. model.objects.filter(device=device, name=name).count() == 0:
  1223. termination_object = model.objects.get(device__in=device.virtual_chassis.members.all(), name=name)
  1224. else:
  1225. termination_object = model.objects.get(device=device, name=name)
  1226. if termination_object.cable is not None and termination_object.cable != self.instance:
  1227. raise forms.ValidationError(
  1228. _("Side {side_upper}: {device} {termination_object} is already connected").format(
  1229. side_upper=side.upper(), device=device, termination_object=termination_object
  1230. )
  1231. )
  1232. except ObjectDoesNotExist:
  1233. raise forms.ValidationError(
  1234. _("{side_upper} side termination not found: {device} {name}").format(
  1235. side_upper=side.upper(), device=device, name=name
  1236. )
  1237. )
  1238. setattr(self.instance, f'{side}_terminations', [termination_object])
  1239. return termination_object
  1240. def clean_side_a_name(self):
  1241. return self._clean_side('a')
  1242. def clean_side_b_name(self):
  1243. return self._clean_side('b')
  1244. def clean_length_unit(self):
  1245. # Avoid trying to save as NULL
  1246. length_unit = self.cleaned_data.get('length_unit', None)
  1247. return length_unit if length_unit is not None else ''
  1248. #
  1249. # Virtual chassis
  1250. #
  1251. class VirtualChassisImportForm(NetBoxModelImportForm):
  1252. master = CSVModelChoiceField(
  1253. label=_('Master'),
  1254. queryset=Device.objects.all(),
  1255. to_field_name='name',
  1256. required=False,
  1257. help_text=_('Master device')
  1258. )
  1259. class Meta:
  1260. model = VirtualChassis
  1261. fields = ('name', 'domain', 'master', 'description', 'comments', 'tags')
  1262. #
  1263. # Power
  1264. #
  1265. class PowerPanelImportForm(NetBoxModelImportForm):
  1266. site = CSVModelChoiceField(
  1267. label=_('Site'),
  1268. queryset=Site.objects.all(),
  1269. to_field_name='name',
  1270. help_text=_('Name of parent site')
  1271. )
  1272. location = CSVModelChoiceField(
  1273. label=_('Location'),
  1274. queryset=Location.objects.all(),
  1275. required=False,
  1276. to_field_name='name'
  1277. )
  1278. class Meta:
  1279. model = PowerPanel
  1280. fields = ('site', 'location', 'name', 'description', 'comments', 'tags')
  1281. def __init__(self, data=None, *args, **kwargs):
  1282. super().__init__(data, *args, **kwargs)
  1283. if data:
  1284. # Limit group queryset by assigned site
  1285. params = {f"site__{self.fields['site'].to_field_name}": data.get('site')}
  1286. self.fields['location'].queryset = self.fields['location'].queryset.filter(**params)
  1287. class PowerFeedImportForm(NetBoxModelImportForm):
  1288. site = CSVModelChoiceField(
  1289. label=_('Site'),
  1290. queryset=Site.objects.all(),
  1291. to_field_name='name',
  1292. help_text=_('Assigned site')
  1293. )
  1294. power_panel = CSVModelChoiceField(
  1295. label=_('Power panel'),
  1296. queryset=PowerPanel.objects.all(),
  1297. to_field_name='name',
  1298. help_text=_('Upstream power panel')
  1299. )
  1300. location = CSVModelChoiceField(
  1301. label=_('Location'),
  1302. queryset=Location.objects.all(),
  1303. to_field_name='name',
  1304. required=False,
  1305. help_text=_("Rack's location (if any)")
  1306. )
  1307. rack = CSVModelChoiceField(
  1308. label=_('Rack'),
  1309. queryset=Rack.objects.all(),
  1310. to_field_name='name',
  1311. required=False,
  1312. help_text=_('Rack')
  1313. )
  1314. tenant = CSVModelChoiceField(
  1315. queryset=Tenant.objects.all(),
  1316. to_field_name='name',
  1317. required=False,
  1318. help_text=_('Assigned tenant')
  1319. )
  1320. status = CSVChoiceField(
  1321. label=_('Status'),
  1322. choices=PowerFeedStatusChoices,
  1323. help_text=_('Operational status')
  1324. )
  1325. type = CSVChoiceField(
  1326. label=_('Type'),
  1327. choices=PowerFeedTypeChoices,
  1328. help_text=_('Primary or redundant')
  1329. )
  1330. supply = CSVChoiceField(
  1331. label=_('Supply'),
  1332. choices=PowerFeedSupplyChoices,
  1333. help_text=_('Supply type (AC/DC)')
  1334. )
  1335. phase = CSVChoiceField(
  1336. label=_('Phase'),
  1337. choices=PowerFeedPhaseChoices,
  1338. help_text=_('Single or three-phase')
  1339. )
  1340. class Meta:
  1341. model = PowerFeed
  1342. fields = (
  1343. 'site', 'power_panel', 'location', 'rack', 'name', 'status', 'type', 'mark_connected', 'supply', 'phase',
  1344. 'voltage', 'amperage', 'max_utilization', 'tenant', 'description', 'comments', 'tags',
  1345. )
  1346. def __init__(self, data=None, *args, **kwargs):
  1347. super().__init__(data, *args, **kwargs)
  1348. if data:
  1349. # Limit power_panel queryset by site
  1350. params = {f"site__{self.fields['site'].to_field_name}": data.get('site')}
  1351. self.fields['power_panel'].queryset = self.fields['power_panel'].queryset.filter(**params)
  1352. # Limit location queryset by site
  1353. params = {f"site__{self.fields['site'].to_field_name}": data.get('site')}
  1354. self.fields['location'].queryset = self.fields['location'].queryset.filter(**params)
  1355. # Limit rack queryset by site and group
  1356. params = {
  1357. f"site__{self.fields['site'].to_field_name}": data.get('site'),
  1358. f"location__{self.fields['location'].to_field_name}": data.get('location'),
  1359. }
  1360. self.fields['rack'].queryset = self.fields['rack'].queryset.filter(**params)
  1361. class VirtualDeviceContextImportForm(NetBoxModelImportForm):
  1362. device = CSVModelChoiceField(
  1363. label=_('Device'),
  1364. queryset=Device.objects.all(),
  1365. to_field_name='name',
  1366. help_text=_('Assigned role')
  1367. )
  1368. tenant = CSVModelChoiceField(
  1369. label=_('Tenant'),
  1370. queryset=Tenant.objects.all(),
  1371. required=False,
  1372. to_field_name='name',
  1373. help_text=_('Assigned tenant')
  1374. )
  1375. status = CSVChoiceField(
  1376. label=_('Status'),
  1377. choices=VirtualDeviceContextStatusChoices,
  1378. )
  1379. primary_ip4 = CSVModelChoiceField(
  1380. label=_('Primary IPv4'),
  1381. queryset=IPAddress.objects.all(),
  1382. required=False,
  1383. to_field_name='address',
  1384. help_text=_('IPv4 address with mask, e.g. 1.2.3.4/24')
  1385. )
  1386. primary_ip6 = CSVModelChoiceField(
  1387. label=_('Primary IPv6'),
  1388. queryset=IPAddress.objects.all(),
  1389. required=False,
  1390. to_field_name='address',
  1391. help_text=_('IPv6 address with prefix length, e.g. 2001:db8::1/64')
  1392. )
  1393. class Meta:
  1394. fields = [
  1395. 'name', 'device', 'status', 'tenant', 'identifier', 'comments', 'primary_ip4', 'primary_ip6',
  1396. ]
  1397. model = VirtualDeviceContext
  1398. def __init__(self, data=None, *args, **kwargs):
  1399. super().__init__(data, *args, **kwargs)
  1400. if data:
  1401. # Limit primary_ip4/ip6 querysets by assigned device
  1402. params = {f"interface__device__{self.fields['device'].to_field_name}": data.get('device')}
  1403. self.fields['primary_ip4'].queryset = self.fields['primary_ip4'].queryset.filter(**params)
  1404. self.fields['primary_ip6'].queryset = self.fields['primary_ip6'].queryset.filter(**params)