bulk_import.py 50 KB

12345678910111213141516171819202122232425262728293031323334353637383940414243444546474849505152535455565758596061626364656667686970717273747576777879808182838485868788899091929394959697989910010110210310410510610710810911011111211311411511611711811912012112212312412512612712812913013113213313413513613713813914014114214314414514614714814915015115215315415515615715815916016116216316416516616716816917017117217317417517617717817918018118218318418518618718818919019119219319419519619719819920020120220320420520620720820921021121221321421521621721821922022122222322422522622722822923023123223323423523623723823924024124224324424524624724824925025125225325425525625725825926026126226326426526626726826927027127227327427527627727827928028128228328428528628728828929029129229329429529629729829930030130230330430530630730830931031131231331431531631731831932032132232332432532632732832933033133233333433533633733833934034134234334434534634734834935035135235335435535635735835936036136236336436536636736836937037137237337437537637737837938038138238338438538638738838939039139239339439539639739839940040140240340440540640740840941041141241341441541641741841942042142242342442542642742842943043143243343443543643743843944044144244344444544644744844945045145245345445545645745845946046146246346446546646746846947047147247347447547647747847948048148248348448548648748848949049149249349449549649749849950050150250350450550650750850951051151251351451551651751851952052152252352452552652752852953053153253353453553653753853954054154254354454554654754854955055155255355455555655755855956056156256356456556656756856957057157257357457557657757857958058158258358458558658758858959059159259359459559659759859960060160260360460560660760860961061161261361461561661761861962062162262362462562662762862963063163263363463563663763863964064164264364464564664764864965065165265365465565665765865966066166266366466566666766866967067167267367467567667767867968068168268368468568668768868969069169269369469569669769869970070170270370470570670770870971071171271371471571671771871972072172272372472572672772872973073173273373473573673773873974074174274374474574674774874975075175275375475575675775875976076176276376476576676776876977077177277377477577677777877978078178278378478578678778878979079179279379479579679779879980080180280380480580680780880981081181281381481581681781881982082182282382482582682782882983083183283383483583683783883984084184284384484584684784884985085185285385485585685785885986086186286386486586686786886987087187287387487587687787887988088188288388488588688788888989089189289389489589689789889990090190290390490590690790890991091191291391491591691791891992092192292392492592692792892993093193293393493593693793893994094194294394494594694794894995095195295395495595695795895996096196296396496596696796896997097197297397497597697797897998098198298398498598698798898999099199299399499599699799899910001001100210031004100510061007100810091010101110121013101410151016101710181019102010211022102310241025102610271028102910301031103210331034103510361037103810391040104110421043104410451046104710481049105010511052105310541055105610571058105910601061106210631064106510661067106810691070107110721073107410751076107710781079108010811082108310841085108610871088108910901091109210931094109510961097109810991100110111021103110411051106110711081109111011111112111311141115111611171118111911201121112211231124112511261127112811291130113111321133113411351136113711381139114011411142114311441145114611471148114911501151115211531154115511561157115811591160116111621163116411651166116711681169117011711172117311741175117611771178117911801181118211831184118511861187118811891190119111921193119411951196119711981199120012011202120312041205120612071208120912101211121212131214121512161217121812191220122112221223122412251226122712281229123012311232123312341235123612371238123912401241124212431244124512461247124812491250125112521253125412551256125712581259126012611262126312641265126612671268126912701271127212731274127512761277127812791280128112821283128412851286128712881289129012911292129312941295129612971298129913001301130213031304130513061307130813091310131113121313131413151316131713181319132013211322132313241325132613271328132913301331133213331334133513361337133813391340134113421343134413451346134713481349135013511352135313541355135613571358135913601361136213631364136513661367136813691370137113721373137413751376137713781379138013811382138313841385138613871388138913901391139213931394139513961397139813991400140114021403140414051406140714081409141014111412141314141415141614171418141914201421142214231424142514261427142814291430143114321433143414351436143714381439144014411442144314441445144614471448144914501451145214531454145514561457145814591460146114621463146414651466146714681469147014711472147314741475147614771478147914801481148214831484148514861487148814891490149114921493149414951496149714981499150015011502150315041505150615071508150915101511151215131514151515161517151815191520152115221523152415251526152715281529153015311532153315341535153615371538153915401541154215431544154515461547154815491550155115521553155415551556155715581559156015611562156315641565156615671568156915701571157215731574157515761577157815791580158115821583158415851586158715881589159015911592159315941595159615971598159916001601
  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. parent = CSVModelChoiceField(
  415. label=_('Parent'),
  416. queryset=DeviceRole.objects.all(),
  417. required=False,
  418. to_field_name='name',
  419. help_text=_('Parent Device Role'),
  420. error_messages={
  421. 'invalid_choice': _('Device role not found.'),
  422. }
  423. )
  424. config_template = CSVModelChoiceField(
  425. label=_('Config template'),
  426. queryset=ConfigTemplate.objects.all(),
  427. to_field_name='name',
  428. required=False,
  429. help_text=_('Config template')
  430. )
  431. slug = SlugField()
  432. class Meta:
  433. model = DeviceRole
  434. fields = (
  435. 'name', 'slug', 'parent', 'color', 'vm_role', 'config_template', 'description', 'comments', 'tags'
  436. )
  437. class PlatformImportForm(NetBoxModelImportForm):
  438. slug = SlugField()
  439. manufacturer = CSVModelChoiceField(
  440. label=_('Manufacturer'),
  441. queryset=Manufacturer.objects.all(),
  442. required=False,
  443. to_field_name='name',
  444. help_text=_('Limit platform assignments to this manufacturer')
  445. )
  446. config_template = CSVModelChoiceField(
  447. label=_('Config template'),
  448. queryset=ConfigTemplate.objects.all(),
  449. to_field_name='name',
  450. required=False,
  451. help_text=_('Config template')
  452. )
  453. class Meta:
  454. model = Platform
  455. fields = (
  456. 'name', 'slug', 'manufacturer', 'config_template', 'description', 'tags',
  457. )
  458. class BaseDeviceImportForm(NetBoxModelImportForm):
  459. role = CSVModelChoiceField(
  460. label=_('Device role'),
  461. queryset=DeviceRole.objects.all(),
  462. to_field_name='name',
  463. help_text=_('Assigned role')
  464. )
  465. tenant = CSVModelChoiceField(
  466. label=_('Tenant'),
  467. queryset=Tenant.objects.all(),
  468. required=False,
  469. to_field_name='name',
  470. help_text=_('Assigned tenant')
  471. )
  472. manufacturer = CSVModelChoiceField(
  473. label=_('Manufacturer'),
  474. queryset=Manufacturer.objects.all(),
  475. to_field_name='name',
  476. help_text=_('Device type manufacturer')
  477. )
  478. device_type = CSVModelChoiceField(
  479. label=_('Device type'),
  480. queryset=DeviceType.objects.all(),
  481. to_field_name='model',
  482. help_text=_('Device type model')
  483. )
  484. platform = CSVModelChoiceField(
  485. label=_('Platform'),
  486. queryset=Platform.objects.all(),
  487. required=False,
  488. to_field_name='name',
  489. help_text=_('Assigned platform')
  490. )
  491. status = CSVChoiceField(
  492. label=_('Status'),
  493. choices=DeviceStatusChoices,
  494. help_text=_('Operational status')
  495. )
  496. virtual_chassis = CSVModelChoiceField(
  497. label=_('Virtual chassis'),
  498. queryset=VirtualChassis.objects.all(),
  499. to_field_name='name',
  500. required=False,
  501. help_text=_('Virtual chassis')
  502. )
  503. cluster = CSVModelChoiceField(
  504. label=_('Cluster'),
  505. queryset=Cluster.objects.all(),
  506. to_field_name='name',
  507. required=False,
  508. help_text=_('Virtualization cluster')
  509. )
  510. class Meta:
  511. fields = []
  512. model = Device
  513. def __init__(self, data=None, *args, **kwargs):
  514. super().__init__(data, *args, **kwargs)
  515. if data:
  516. # Limit device type queryset by manufacturer
  517. params = {f"manufacturer__{self.fields['manufacturer'].to_field_name}": data.get('manufacturer')}
  518. self.fields['device_type'].queryset = self.fields['device_type'].queryset.filter(**params)
  519. class DeviceImportForm(BaseDeviceImportForm):
  520. site = CSVModelChoiceField(
  521. label=_('Site'),
  522. queryset=Site.objects.all(),
  523. to_field_name='name',
  524. help_text=_('Assigned site')
  525. )
  526. location = CSVModelChoiceField(
  527. label=_('Location'),
  528. queryset=Location.objects.all(),
  529. to_field_name='name',
  530. required=False,
  531. help_text=_("Assigned location (if any)")
  532. )
  533. rack = CSVModelChoiceField(
  534. label=_('Rack'),
  535. queryset=Rack.objects.all(),
  536. to_field_name='name',
  537. required=False,
  538. help_text=_("Assigned rack (if any)")
  539. )
  540. face = CSVChoiceField(
  541. label=_('Face'),
  542. choices=DeviceFaceChoices,
  543. required=False,
  544. help_text=_('Mounted rack face')
  545. )
  546. parent = CSVModelChoiceField(
  547. label=_('Parent'),
  548. queryset=Device.objects.all(),
  549. to_field_name='name',
  550. required=False,
  551. help_text=_('Parent device (for child devices)')
  552. )
  553. device_bay = CSVModelChoiceField(
  554. label=_('Device bay'),
  555. queryset=DeviceBay.objects.all(),
  556. to_field_name='name',
  557. required=False,
  558. help_text=_('Device bay in which this device is installed (for child devices)')
  559. )
  560. airflow = CSVChoiceField(
  561. label=_('Airflow'),
  562. choices=DeviceAirflowChoices,
  563. required=False,
  564. help_text=_('Airflow direction')
  565. )
  566. config_template = CSVModelChoiceField(
  567. label=_('Config template'),
  568. queryset=ConfigTemplate.objects.all(),
  569. to_field_name='name',
  570. required=False,
  571. help_text=_('Config template')
  572. )
  573. class Meta(BaseDeviceImportForm.Meta):
  574. fields = [
  575. 'name', 'role', 'tenant', 'manufacturer', 'device_type', 'platform', 'serial', 'asset_tag', 'status',
  576. 'site', 'location', 'rack', 'position', 'face', 'latitude', 'longitude', 'parent', 'device_bay', 'airflow',
  577. 'virtual_chassis', 'vc_position', 'vc_priority', 'cluster', 'description', 'config_template', 'comments',
  578. 'tags',
  579. ]
  580. def __init__(self, data=None, *args, **kwargs):
  581. super().__init__(data, *args, **kwargs)
  582. if data:
  583. # Limit location queryset by assigned site
  584. params = {f"site__{self.fields['site'].to_field_name}": data.get('site')}
  585. self.fields['location'].queryset = self.fields['location'].queryset.filter(**params)
  586. self.fields['parent'].queryset = self.fields['parent'].queryset.filter(**params)
  587. # Limit rack queryset by assigned site and location
  588. params = {
  589. f"site__{self.fields['site'].to_field_name}": data.get('site'),
  590. }
  591. if location := data.get('location'):
  592. params.update({
  593. f"location__{self.fields['location'].to_field_name}": location,
  594. })
  595. self.fields['rack'].queryset = self.fields['rack'].queryset.filter(**params)
  596. # Limit device bay queryset by parent device
  597. if parent := data.get('parent'):
  598. params = {f"device__{self.fields['parent'].to_field_name}": parent}
  599. self.fields['device_bay'].queryset = self.fields['device_bay'].queryset.filter(**params)
  600. def clean(self):
  601. super().clean()
  602. # Inherit site and rack from parent device
  603. if parent := self.cleaned_data.get('parent'):
  604. self.instance.site = parent.site
  605. self.instance.rack = parent.rack
  606. # Set parent_bay reverse relationship
  607. if device_bay := self.cleaned_data.get('device_bay'):
  608. self.instance.parent_bay = device_bay
  609. class ModuleImportForm(ModuleCommonForm, NetBoxModelImportForm):
  610. device = CSVModelChoiceField(
  611. label=_('Device'),
  612. queryset=Device.objects.all(),
  613. to_field_name='name',
  614. help_text=_('The device in which this module is installed')
  615. )
  616. module_bay = CSVModelChoiceField(
  617. label=_('Module bay'),
  618. queryset=ModuleBay.objects.all(),
  619. to_field_name='name',
  620. help_text=_('The module bay in which this module is installed')
  621. )
  622. module_type = CSVModelChoiceField(
  623. label=_('Module type'),
  624. queryset=ModuleType.objects.all(),
  625. to_field_name='model',
  626. help_text=_('The type of module')
  627. )
  628. status = CSVChoiceField(
  629. label=_('Status'),
  630. choices=ModuleStatusChoices,
  631. help_text=_('Operational status')
  632. )
  633. replicate_components = forms.BooleanField(
  634. label=_('Replicate components'),
  635. required=False,
  636. help_text=_('Automatically populate components associated with this module type (enabled by default)')
  637. )
  638. adopt_components = forms.BooleanField(
  639. label=_('Adopt components'),
  640. required=False,
  641. help_text=_('Adopt already existing components')
  642. )
  643. class Meta:
  644. model = Module
  645. fields = (
  646. 'device', 'module_bay', 'module_type', 'serial', 'asset_tag', 'status', 'description', 'comments',
  647. 'replicate_components', 'adopt_components', 'tags',
  648. )
  649. def __init__(self, data=None, *args, **kwargs):
  650. super().__init__(data, *args, **kwargs)
  651. if data:
  652. # Limit module_bay queryset by assigned device
  653. params = {f"device__{self.fields['device'].to_field_name}": data.get('device')}
  654. self.fields['module_bay'].queryset = self.fields['module_bay'].queryset.filter(**params)
  655. def clean_replicate_components(self):
  656. # Make sure replicate_components is True when it's not included in the uploaded data
  657. if 'replicate_components' not in self.data:
  658. return True
  659. else:
  660. return self.cleaned_data['replicate_components']
  661. #
  662. # Device components
  663. #
  664. class ConsolePortImportForm(NetBoxModelImportForm):
  665. device = CSVModelChoiceField(
  666. label=_('Device'),
  667. queryset=Device.objects.all(),
  668. to_field_name='name'
  669. )
  670. type = CSVChoiceField(
  671. label=_('Type'),
  672. choices=ConsolePortTypeChoices,
  673. required=False,
  674. help_text=_('Port type')
  675. )
  676. speed = CSVTypedChoiceField(
  677. label=_('Speed'),
  678. choices=ConsolePortSpeedChoices,
  679. coerce=int,
  680. empty_value=None,
  681. required=False,
  682. help_text=_('Port speed in bps')
  683. )
  684. class Meta:
  685. model = ConsolePort
  686. fields = ('device', 'name', 'label', 'type', 'speed', 'mark_connected', 'description', 'tags')
  687. class ConsoleServerPortImportForm(NetBoxModelImportForm):
  688. device = CSVModelChoiceField(
  689. label=_('Device'),
  690. queryset=Device.objects.all(),
  691. to_field_name='name'
  692. )
  693. type = CSVChoiceField(
  694. label=_('Type'),
  695. choices=ConsolePortTypeChoices,
  696. required=False,
  697. help_text=_('Port type')
  698. )
  699. speed = CSVTypedChoiceField(
  700. label=_('Speed'),
  701. choices=ConsolePortSpeedChoices,
  702. coerce=int,
  703. empty_value=None,
  704. required=False,
  705. help_text=_('Port speed in bps')
  706. )
  707. class Meta:
  708. model = ConsoleServerPort
  709. fields = ('device', 'name', 'label', 'type', 'speed', 'mark_connected', 'description', 'tags')
  710. class PowerPortImportForm(NetBoxModelImportForm):
  711. device = CSVModelChoiceField(
  712. label=_('Device'),
  713. queryset=Device.objects.all(),
  714. to_field_name='name'
  715. )
  716. type = CSVChoiceField(
  717. label=_('Type'),
  718. choices=PowerPortTypeChoices,
  719. required=False,
  720. help_text=_('Port type')
  721. )
  722. class Meta:
  723. model = PowerPort
  724. fields = (
  725. 'device', 'name', 'label', 'type', 'mark_connected', 'maximum_draw', 'allocated_draw', 'description', 'tags'
  726. )
  727. class PowerOutletImportForm(NetBoxModelImportForm):
  728. device = CSVModelChoiceField(
  729. label=_('Device'),
  730. queryset=Device.objects.all(),
  731. to_field_name='name'
  732. )
  733. type = CSVChoiceField(
  734. label=_('Type'),
  735. choices=PowerOutletTypeChoices,
  736. required=False,
  737. help_text=_('Outlet type')
  738. )
  739. power_port = CSVModelChoiceField(
  740. label=_('Power port'),
  741. queryset=PowerPort.objects.all(),
  742. required=False,
  743. to_field_name='name',
  744. help_text=_('Local power port which feeds this outlet')
  745. )
  746. feed_leg = CSVChoiceField(
  747. label=_('Feed leg'),
  748. choices=PowerOutletFeedLegChoices,
  749. required=False,
  750. help_text=_('Electrical phase (for three-phase circuits)')
  751. )
  752. class Meta:
  753. model = PowerOutlet
  754. fields = (
  755. 'device', 'name', 'label', 'type', 'color', 'mark_connected', 'power_port', 'feed_leg', 'description',
  756. 'tags',
  757. )
  758. def __init__(self, *args, **kwargs):
  759. super().__init__(*args, **kwargs)
  760. # Limit PowerPort choices to those belonging to this device (or VC master)
  761. if self.is_bound and 'device' in self.data:
  762. try:
  763. device = self.fields['device'].to_python(self.data['device'])
  764. except forms.ValidationError:
  765. device = None
  766. else:
  767. try:
  768. device = self.instance.device
  769. except Device.DoesNotExist:
  770. device = None
  771. if device:
  772. self.fields['power_port'].queryset = PowerPort.objects.filter(
  773. device__in=[device, device.get_vc_master()]
  774. )
  775. else:
  776. self.fields['power_port'].queryset = PowerPort.objects.none()
  777. class InterfaceImportForm(NetBoxModelImportForm):
  778. device = CSVModelChoiceField(
  779. label=_('Device'),
  780. queryset=Device.objects.all(),
  781. to_field_name='name'
  782. )
  783. parent = CSVModelChoiceField(
  784. label=_('Parent'),
  785. queryset=Interface.objects.all(),
  786. required=False,
  787. to_field_name='name',
  788. help_text=_('Parent interface')
  789. )
  790. bridge = CSVModelChoiceField(
  791. label=_('Bridge'),
  792. queryset=Interface.objects.all(),
  793. required=False,
  794. to_field_name='name',
  795. help_text=_('Bridged interface')
  796. )
  797. lag = CSVModelChoiceField(
  798. label=_('Lag'),
  799. queryset=Interface.objects.all(),
  800. required=False,
  801. to_field_name='name',
  802. help_text=_('Parent LAG interface')
  803. )
  804. vdcs = CSVModelMultipleChoiceField(
  805. label=_('Vdcs'),
  806. queryset=VirtualDeviceContext.objects.all(),
  807. required=False,
  808. to_field_name='name',
  809. help_text=mark_safe(
  810. _('VDC names separated by commas, encased with double quotes. Example:') + ' <code>vdc1,vdc2,vdc3</code>'
  811. )
  812. )
  813. type = CSVChoiceField(
  814. label=_('Type'),
  815. choices=InterfaceTypeChoices,
  816. help_text=_('Physical medium')
  817. )
  818. duplex = CSVChoiceField(
  819. label=_('Duplex'),
  820. choices=InterfaceDuplexChoices,
  821. required=False
  822. )
  823. poe_mode = CSVChoiceField(
  824. label=_('Poe mode'),
  825. choices=InterfacePoEModeChoices,
  826. required=False,
  827. help_text=_('PoE mode')
  828. )
  829. poe_type = CSVChoiceField(
  830. label=_('Poe type'),
  831. choices=InterfacePoETypeChoices,
  832. required=False,
  833. help_text=_('PoE type')
  834. )
  835. mode = CSVChoiceField(
  836. label=_('Mode'),
  837. choices=InterfaceModeChoices,
  838. required=False,
  839. help_text=_('IEEE 802.1Q operational mode (for L2 interfaces)')
  840. )
  841. vrf = CSVModelChoiceField(
  842. label=_('VRF'),
  843. queryset=VRF.objects.all(),
  844. required=False,
  845. to_field_name='rd',
  846. help_text=_('Assigned VRF')
  847. )
  848. rf_role = CSVChoiceField(
  849. label=_('Rf role'),
  850. choices=WirelessRoleChoices,
  851. required=False,
  852. help_text=_('Wireless role (AP/station)')
  853. )
  854. class Meta:
  855. model = Interface
  856. fields = (
  857. 'device', 'name', 'label', 'parent', 'bridge', 'lag', 'type', 'speed', 'duplex', 'enabled',
  858. 'mark_connected', 'wwn', 'vdcs', 'mtu', 'mgmt_only', 'description', 'poe_mode', 'poe_type', 'mode',
  859. 'vrf', 'rf_role', 'rf_channel', 'rf_channel_frequency', 'rf_channel_width', 'tx_power', 'tags'
  860. )
  861. def __init__(self, data=None, *args, **kwargs):
  862. super().__init__(data, *args, **kwargs)
  863. if data:
  864. # Limit choices for parent, bridge, and LAG interfaces to the assigned device
  865. if device := data.get('device'):
  866. params = {
  867. f"device__{self.fields['device'].to_field_name}": device
  868. }
  869. self.fields['parent'].queryset = self.fields['parent'].queryset.filter(**params)
  870. self.fields['bridge'].queryset = self.fields['bridge'].queryset.filter(**params)
  871. self.fields['lag'].queryset = self.fields['lag'].queryset.filter(**params)
  872. self.fields['vdcs'].queryset = self.fields['vdcs'].queryset.filter(**params)
  873. def clean_enabled(self):
  874. # Make sure enabled is True when it's not included in the uploaded data
  875. if 'enabled' not in self.data:
  876. return True
  877. else:
  878. return self.cleaned_data['enabled']
  879. def clean_vdcs(self):
  880. for vdc in self.cleaned_data['vdcs']:
  881. if vdc.device != self.cleaned_data['device']:
  882. raise forms.ValidationError(
  883. _("VDC {vdc} is not assigned to device {device}").format(
  884. vdc=vdc, device=self.cleaned_data['device']
  885. )
  886. )
  887. return self.cleaned_data['vdcs']
  888. class FrontPortImportForm(NetBoxModelImportForm):
  889. device = CSVModelChoiceField(
  890. label=_('Device'),
  891. queryset=Device.objects.all(),
  892. to_field_name='name'
  893. )
  894. rear_port = CSVModelChoiceField(
  895. label=_('Rear port'),
  896. queryset=RearPort.objects.all(),
  897. to_field_name='name',
  898. help_text=_('Corresponding rear port')
  899. )
  900. type = CSVChoiceField(
  901. label=_('Type'),
  902. choices=PortTypeChoices,
  903. help_text=_('Physical medium classification')
  904. )
  905. class Meta:
  906. model = FrontPort
  907. fields = (
  908. 'device', 'name', 'label', 'type', 'color', 'mark_connected', 'rear_port', 'rear_port_position',
  909. 'description', 'tags'
  910. )
  911. def __init__(self, *args, **kwargs):
  912. super().__init__(*args, **kwargs)
  913. # Limit RearPort choices to those belonging to this device (or VC master)
  914. if self.is_bound and 'device' in self.data:
  915. try:
  916. device = self.fields['device'].to_python(self.data['device'])
  917. except forms.ValidationError:
  918. device = None
  919. else:
  920. try:
  921. device = self.instance.device
  922. except Device.DoesNotExist:
  923. device = None
  924. if device:
  925. self.fields['rear_port'].queryset = RearPort.objects.filter(
  926. device__in=[device, device.get_vc_master()]
  927. )
  928. else:
  929. self.fields['rear_port'].queryset = RearPort.objects.none()
  930. class RearPortImportForm(NetBoxModelImportForm):
  931. device = CSVModelChoiceField(
  932. label=_('Device'),
  933. queryset=Device.objects.all(),
  934. to_field_name='name'
  935. )
  936. type = CSVChoiceField(
  937. label=_('Type'),
  938. help_text=_('Physical medium classification'),
  939. choices=PortTypeChoices,
  940. )
  941. class Meta:
  942. model = RearPort
  943. fields = ('device', 'name', 'label', 'type', 'color', 'mark_connected', 'positions', 'description', 'tags')
  944. class ModuleBayImportForm(NetBoxModelImportForm):
  945. device = CSVModelChoiceField(
  946. label=_('Device'),
  947. queryset=Device.objects.all(),
  948. to_field_name='name'
  949. )
  950. class Meta:
  951. model = ModuleBay
  952. fields = ('device', 'name', 'label', 'position', 'description', 'tags')
  953. class DeviceBayImportForm(NetBoxModelImportForm):
  954. device = CSVModelChoiceField(
  955. label=_('Device'),
  956. queryset=Device.objects.all(),
  957. to_field_name='name'
  958. )
  959. installed_device = CSVModelChoiceField(
  960. label=_('Installed device'),
  961. queryset=Device.objects.all(),
  962. required=False,
  963. to_field_name='name',
  964. help_text=_('Child device installed within this bay'),
  965. error_messages={
  966. 'invalid_choice': _('Child device not found.'),
  967. }
  968. )
  969. class Meta:
  970. model = DeviceBay
  971. fields = ('device', 'name', 'label', 'installed_device', 'description', 'tags')
  972. def __init__(self, *args, **kwargs):
  973. super().__init__(*args, **kwargs)
  974. # Limit installed device choices to devices of the correct type and location
  975. if self.is_bound and 'device' in self.data:
  976. try:
  977. device = self.fields['device'].to_python(self.data['device'])
  978. except forms.ValidationError:
  979. device = None
  980. else:
  981. try:
  982. device = self.instance.device
  983. except Device.DoesNotExist:
  984. device = None
  985. if device:
  986. self.fields['installed_device'].queryset = Device.objects.filter(
  987. site=device.site,
  988. rack=device.rack,
  989. parent_bay__isnull=True,
  990. device_type__u_height=0,
  991. device_type__subdevice_role=SubdeviceRoleChoices.ROLE_CHILD
  992. ).exclude(pk=device.pk)
  993. else:
  994. self.fields['installed_device'].queryset = Device.objects.none()
  995. class InventoryItemImportForm(NetBoxModelImportForm):
  996. device = CSVModelChoiceField(
  997. label=_('Device'),
  998. queryset=Device.objects.all(),
  999. to_field_name='name'
  1000. )
  1001. role = CSVModelChoiceField(
  1002. label=_('Role'),
  1003. queryset=InventoryItemRole.objects.all(),
  1004. to_field_name='name',
  1005. required=False
  1006. )
  1007. manufacturer = CSVModelChoiceField(
  1008. label=_('Manufacturer'),
  1009. queryset=Manufacturer.objects.all(),
  1010. to_field_name='name',
  1011. required=False
  1012. )
  1013. parent = CSVModelChoiceField(
  1014. label=_('Parent'),
  1015. queryset=Device.objects.all(),
  1016. to_field_name='name',
  1017. required=False,
  1018. help_text=_('Parent inventory item')
  1019. )
  1020. component_type = CSVContentTypeField(
  1021. label=_('Component type'),
  1022. queryset=ContentType.objects.all(),
  1023. limit_choices_to=MODULAR_COMPONENT_MODELS,
  1024. required=False,
  1025. help_text=_('Component Type')
  1026. )
  1027. component_name = forms.CharField(
  1028. label=_('Compnent name'),
  1029. required=False,
  1030. help_text=_('Component Name')
  1031. )
  1032. status = CSVChoiceField(
  1033. label=_('Status'),
  1034. choices=InventoryItemStatusChoices,
  1035. help_text=_('Operational status')
  1036. )
  1037. class Meta:
  1038. model = InventoryItem
  1039. fields = (
  1040. 'device', 'name', 'label', 'status', 'role', 'manufacturer', 'parent', 'part_id', 'serial', 'asset_tag',
  1041. 'discovered', 'description', 'tags', 'component_type', 'component_name',
  1042. )
  1043. def __init__(self, *args, **kwargs):
  1044. super().__init__(*args, **kwargs)
  1045. # Limit parent choices to inventory items belonging to this device
  1046. device = None
  1047. if self.is_bound and 'device' in self.data:
  1048. try:
  1049. device = self.fields['device'].to_python(self.data['device'])
  1050. except forms.ValidationError:
  1051. pass
  1052. if device:
  1053. self.fields['parent'].queryset = InventoryItem.objects.filter(device=device)
  1054. else:
  1055. self.fields['parent'].queryset = InventoryItem.objects.none()
  1056. def clean_component_name(self):
  1057. content_type = self.cleaned_data.get('component_type')
  1058. component_name = self.cleaned_data.get('component_name')
  1059. device = self.cleaned_data.get("device")
  1060. if not device and hasattr(self, 'instance') and hasattr(self.instance, 'device'):
  1061. device = self.instance.device
  1062. if not all([device, content_type, component_name]):
  1063. return None
  1064. model = content_type.model_class()
  1065. try:
  1066. component = model.objects.get(device=device, name=component_name)
  1067. self.instance.component = component
  1068. except ObjectDoesNotExist:
  1069. raise forms.ValidationError(
  1070. _("Component not found: {device} - {component_name}").format(
  1071. device=device, component_name=component_name
  1072. )
  1073. )
  1074. #
  1075. # Device component roles
  1076. #
  1077. class InventoryItemRoleImportForm(NetBoxModelImportForm):
  1078. slug = SlugField()
  1079. class Meta:
  1080. model = InventoryItemRole
  1081. fields = ('name', 'slug', 'color', 'description')
  1082. #
  1083. # Addressing
  1084. #
  1085. class MACAddressImportForm(NetBoxModelImportForm):
  1086. device = CSVModelChoiceField(
  1087. label=_('Device'),
  1088. queryset=Device.objects.all(),
  1089. required=False,
  1090. to_field_name='name',
  1091. help_text=_('Parent device of assigned interface (if any)')
  1092. )
  1093. virtual_machine = CSVModelChoiceField(
  1094. label=_('Virtual machine'),
  1095. queryset=VirtualMachine.objects.all(),
  1096. required=False,
  1097. to_field_name='name',
  1098. help_text=_('Parent VM of assigned interface (if any)')
  1099. )
  1100. interface = CSVModelChoiceField(
  1101. label=_('Interface'),
  1102. queryset=Interface.objects.none(), # Can also refer to VMInterface
  1103. required=False,
  1104. to_field_name='name',
  1105. help_text=_('Assigned interface')
  1106. )
  1107. is_primary = forms.BooleanField(
  1108. label=_('Is primary'),
  1109. help_text=_('Make this the primary MAC address for the assigned interface'),
  1110. required=False
  1111. )
  1112. class Meta:
  1113. model = MACAddress
  1114. fields = [
  1115. 'mac_address', 'device', 'virtual_machine', 'interface', 'is_primary', 'description', 'comments', 'tags',
  1116. ]
  1117. def __init__(self, data=None, *args, **kwargs):
  1118. super().__init__(data, *args, **kwargs)
  1119. if data:
  1120. # Limit interface queryset by assigned device
  1121. if data.get('device'):
  1122. self.fields['interface'].queryset = Interface.objects.filter(
  1123. **{f"device__{self.fields['device'].to_field_name}": data['device']}
  1124. )
  1125. # Limit interface queryset by assigned device
  1126. elif data.get('virtual_machine'):
  1127. self.fields['interface'].queryset = VMInterface.objects.filter(
  1128. **{f"virtual_machine__{self.fields['virtual_machine'].to_field_name}": data['virtual_machine']}
  1129. )
  1130. def clean(self):
  1131. super().clean()
  1132. device = self.cleaned_data.get('device')
  1133. virtual_machine = self.cleaned_data.get('virtual_machine')
  1134. interface = self.cleaned_data.get('interface')
  1135. # Validate interface assignment
  1136. if interface and not device and not virtual_machine:
  1137. raise forms.ValidationError({
  1138. "interface": _("Must specify the parent device or VM when assigning an interface")
  1139. })
  1140. def save(self, *args, **kwargs):
  1141. # Set interface assignment
  1142. if interface := self.cleaned_data.get('interface'):
  1143. self.instance.assigned_object = interface
  1144. instance = super().save(*args, **kwargs)
  1145. # Assign the MAC address as primary for its interface, if designated as such
  1146. if interface and self.cleaned_data['is_primary'] and self.instance.pk:
  1147. interface.primary_mac_address = self.instance
  1148. interface.save()
  1149. return instance
  1150. #
  1151. # Cables
  1152. #
  1153. class CableImportForm(NetBoxModelImportForm):
  1154. # Termination A
  1155. side_a_device = CSVModelChoiceField(
  1156. label=_('Side A device'),
  1157. queryset=Device.objects.all(),
  1158. to_field_name='name',
  1159. help_text=_('Device name')
  1160. )
  1161. side_a_type = CSVContentTypeField(
  1162. label=_('Side A type'),
  1163. queryset=ContentType.objects.all(),
  1164. limit_choices_to=CABLE_TERMINATION_MODELS,
  1165. help_text=_('Termination type')
  1166. )
  1167. side_a_name = forms.CharField(
  1168. label=_('Side A name'),
  1169. help_text=_('Termination name')
  1170. )
  1171. # Termination B
  1172. side_b_device = CSVModelChoiceField(
  1173. label=_('Side B device'),
  1174. queryset=Device.objects.all(),
  1175. to_field_name='name',
  1176. help_text=_('Device name')
  1177. )
  1178. side_b_type = CSVContentTypeField(
  1179. label=_('Side B type'),
  1180. queryset=ContentType.objects.all(),
  1181. limit_choices_to=CABLE_TERMINATION_MODELS,
  1182. help_text=_('Termination type')
  1183. )
  1184. side_b_name = forms.CharField(
  1185. label=_('Side B name'),
  1186. help_text=_('Termination name')
  1187. )
  1188. # Cable attributes
  1189. status = CSVChoiceField(
  1190. label=_('Status'),
  1191. choices=LinkStatusChoices,
  1192. required=False,
  1193. help_text=_('Connection status')
  1194. )
  1195. type = CSVChoiceField(
  1196. label=_('Type'),
  1197. choices=CableTypeChoices,
  1198. required=False,
  1199. help_text=_('Physical medium classification')
  1200. )
  1201. tenant = CSVModelChoiceField(
  1202. label=_('Tenant'),
  1203. queryset=Tenant.objects.all(),
  1204. required=False,
  1205. to_field_name='name',
  1206. help_text=_('Assigned tenant')
  1207. )
  1208. length_unit = CSVChoiceField(
  1209. label=_('Length unit'),
  1210. choices=CableLengthUnitChoices,
  1211. required=False,
  1212. help_text=_('Length unit')
  1213. )
  1214. class Meta:
  1215. model = Cable
  1216. fields = [
  1217. 'side_a_device', 'side_a_type', 'side_a_name', 'side_b_device', 'side_b_type', 'side_b_name', 'type',
  1218. 'status', 'tenant', 'label', 'color', 'length', 'length_unit', 'description', 'comments', 'tags',
  1219. ]
  1220. def _clean_side(self, side):
  1221. """
  1222. Derive a Cable's A/B termination objects.
  1223. :param side: 'a' or 'b'
  1224. """
  1225. assert side in 'ab', f"Invalid side designation: {side}"
  1226. device = self.cleaned_data.get(f'side_{side}_device')
  1227. content_type = self.cleaned_data.get(f'side_{side}_type')
  1228. name = self.cleaned_data.get(f'side_{side}_name')
  1229. if not device or not content_type or not name:
  1230. return None
  1231. model = content_type.model_class()
  1232. try:
  1233. if device.virtual_chassis and device.virtual_chassis.master == device and \
  1234. model.objects.filter(device=device, name=name).count() == 0:
  1235. termination_object = model.objects.get(device__in=device.virtual_chassis.members.all(), name=name)
  1236. else:
  1237. termination_object = model.objects.get(device=device, name=name)
  1238. if termination_object.cable is not None and termination_object.cable != self.instance:
  1239. raise forms.ValidationError(
  1240. _("Side {side_upper}: {device} {termination_object} is already connected").format(
  1241. side_upper=side.upper(), device=device, termination_object=termination_object
  1242. )
  1243. )
  1244. except ObjectDoesNotExist:
  1245. raise forms.ValidationError(
  1246. _("{side_upper} side termination not found: {device} {name}").format(
  1247. side_upper=side.upper(), device=device, name=name
  1248. )
  1249. )
  1250. setattr(self.instance, f'{side}_terminations', [termination_object])
  1251. return termination_object
  1252. def clean_side_a_name(self):
  1253. return self._clean_side('a')
  1254. def clean_side_b_name(self):
  1255. return self._clean_side('b')
  1256. def clean_length_unit(self):
  1257. # Avoid trying to save as NULL
  1258. length_unit = self.cleaned_data.get('length_unit', None)
  1259. return length_unit if length_unit is not None else ''
  1260. #
  1261. # Virtual chassis
  1262. #
  1263. class VirtualChassisImportForm(NetBoxModelImportForm):
  1264. master = CSVModelChoiceField(
  1265. label=_('Master'),
  1266. queryset=Device.objects.all(),
  1267. to_field_name='name',
  1268. required=False,
  1269. help_text=_('Master device')
  1270. )
  1271. class Meta:
  1272. model = VirtualChassis
  1273. fields = ('name', 'domain', 'master', 'description', 'comments', 'tags')
  1274. #
  1275. # Power
  1276. #
  1277. class PowerPanelImportForm(NetBoxModelImportForm):
  1278. site = CSVModelChoiceField(
  1279. label=_('Site'),
  1280. queryset=Site.objects.all(),
  1281. to_field_name='name',
  1282. help_text=_('Name of parent site')
  1283. )
  1284. location = CSVModelChoiceField(
  1285. label=_('Location'),
  1286. queryset=Location.objects.all(),
  1287. required=False,
  1288. to_field_name='name'
  1289. )
  1290. class Meta:
  1291. model = PowerPanel
  1292. fields = ('site', 'location', 'name', 'description', 'comments', 'tags')
  1293. def __init__(self, data=None, *args, **kwargs):
  1294. super().__init__(data, *args, **kwargs)
  1295. if data:
  1296. # Limit group queryset by assigned site
  1297. params = {f"site__{self.fields['site'].to_field_name}": data.get('site')}
  1298. self.fields['location'].queryset = self.fields['location'].queryset.filter(**params)
  1299. class PowerFeedImportForm(NetBoxModelImportForm):
  1300. site = CSVModelChoiceField(
  1301. label=_('Site'),
  1302. queryset=Site.objects.all(),
  1303. to_field_name='name',
  1304. help_text=_('Assigned site')
  1305. )
  1306. power_panel = CSVModelChoiceField(
  1307. label=_('Power panel'),
  1308. queryset=PowerPanel.objects.all(),
  1309. to_field_name='name',
  1310. help_text=_('Upstream power panel')
  1311. )
  1312. location = CSVModelChoiceField(
  1313. label=_('Location'),
  1314. queryset=Location.objects.all(),
  1315. to_field_name='name',
  1316. required=False,
  1317. help_text=_("Rack's location (if any)")
  1318. )
  1319. rack = CSVModelChoiceField(
  1320. label=_('Rack'),
  1321. queryset=Rack.objects.all(),
  1322. to_field_name='name',
  1323. required=False,
  1324. help_text=_('Rack')
  1325. )
  1326. tenant = CSVModelChoiceField(
  1327. queryset=Tenant.objects.all(),
  1328. to_field_name='name',
  1329. required=False,
  1330. help_text=_('Assigned tenant')
  1331. )
  1332. status = CSVChoiceField(
  1333. label=_('Status'),
  1334. choices=PowerFeedStatusChoices,
  1335. help_text=_('Operational status')
  1336. )
  1337. type = CSVChoiceField(
  1338. label=_('Type'),
  1339. choices=PowerFeedTypeChoices,
  1340. help_text=_('Primary or redundant')
  1341. )
  1342. supply = CSVChoiceField(
  1343. label=_('Supply'),
  1344. choices=PowerFeedSupplyChoices,
  1345. help_text=_('Supply type (AC/DC)')
  1346. )
  1347. phase = CSVChoiceField(
  1348. label=_('Phase'),
  1349. choices=PowerFeedPhaseChoices,
  1350. help_text=_('Single or three-phase')
  1351. )
  1352. class Meta:
  1353. model = PowerFeed
  1354. fields = (
  1355. 'site', 'power_panel', 'location', 'rack', 'name', 'status', 'type', 'mark_connected', 'supply', 'phase',
  1356. 'voltage', 'amperage', 'max_utilization', 'tenant', 'description', 'comments', 'tags',
  1357. )
  1358. def __init__(self, data=None, *args, **kwargs):
  1359. super().__init__(data, *args, **kwargs)
  1360. if data:
  1361. # Limit power_panel queryset by site
  1362. params = {f"site__{self.fields['site'].to_field_name}": data.get('site')}
  1363. self.fields['power_panel'].queryset = self.fields['power_panel'].queryset.filter(**params)
  1364. # Limit location queryset by site
  1365. params = {f"site__{self.fields['site'].to_field_name}": data.get('site')}
  1366. self.fields['location'].queryset = self.fields['location'].queryset.filter(**params)
  1367. # Limit rack queryset by site and group
  1368. params = {
  1369. f"site__{self.fields['site'].to_field_name}": data.get('site'),
  1370. f"location__{self.fields['location'].to_field_name}": data.get('location'),
  1371. }
  1372. self.fields['rack'].queryset = self.fields['rack'].queryset.filter(**params)
  1373. class VirtualDeviceContextImportForm(NetBoxModelImportForm):
  1374. device = CSVModelChoiceField(
  1375. label=_('Device'),
  1376. queryset=Device.objects.all(),
  1377. to_field_name='name',
  1378. help_text=_('Assigned role')
  1379. )
  1380. tenant = CSVModelChoiceField(
  1381. label=_('Tenant'),
  1382. queryset=Tenant.objects.all(),
  1383. required=False,
  1384. to_field_name='name',
  1385. help_text=_('Assigned tenant')
  1386. )
  1387. status = CSVChoiceField(
  1388. label=_('Status'),
  1389. choices=VirtualDeviceContextStatusChoices,
  1390. )
  1391. primary_ip4 = CSVModelChoiceField(
  1392. label=_('Primary IPv4'),
  1393. queryset=IPAddress.objects.all(),
  1394. required=False,
  1395. to_field_name='address',
  1396. help_text=_('IPv4 address with mask, e.g. 1.2.3.4/24')
  1397. )
  1398. primary_ip6 = CSVModelChoiceField(
  1399. label=_('Primary IPv6'),
  1400. queryset=IPAddress.objects.all(),
  1401. required=False,
  1402. to_field_name='address',
  1403. help_text=_('IPv6 address with prefix length, e.g. 2001:db8::1/64')
  1404. )
  1405. class Meta:
  1406. fields = [
  1407. 'name', 'device', 'status', 'tenant', 'identifier', 'comments', 'primary_ip4', 'primary_ip6',
  1408. ]
  1409. model = VirtualDeviceContext
  1410. def __init__(self, data=None, *args, **kwargs):
  1411. super().__init__(data, *args, **kwargs)
  1412. if data:
  1413. # Limit primary_ip4/ip6 querysets by assigned device
  1414. params = {f"interface__device__{self.fields['device'].to_field_name}": data.get('device')}
  1415. self.fields['primary_ip4'].queryset = self.fields['primary_ip4'].queryset.filter(**params)
  1416. self.fields['primary_ip6'].queryset = self.fields['primary_ip6'].queryset.filter(**params)