bulk_import.py 51 KB

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