bulk_import.py 60 KB

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