bulk_import.py 37 KB

12345678910111213141516171819202122232425262728293031323334353637383940414243444546474849505152535455565758596061626364656667686970717273747576777879808182838485868788899091929394959697989910010110210310410510610710810911011111211311411511611711811912012112212312412512612712812913013113213313413513613713813914014114214314414514614714814915015115215315415515615715815916016116216316416516616716816917017117217317417517617717817918018118218318418518618718818919019119219319419519619719819920020120220320420520620720820921021121221321421521621721821922022122222322422522622722822923023123223323423523623723823924024124224324424524624724824925025125225325425525625725825926026126226326426526626726826927027127227327427527627727827928028128228328428528628728828929029129229329429529629729829930030130230330430530630730830931031131231331431531631731831932032132232332432532632732832933033133233333433533633733833934034134234334434534634734834935035135235335435535635735835936036136236336436536636736836937037137237337437537637737837938038138238338438538638738838939039139239339439539639739839940040140240340440540640740840941041141241341441541641741841942042142242342442542642742842943043143243343443543643743843944044144244344444544644744844945045145245345445545645745845946046146246346446546646746846947047147247347447547647747847948048148248348448548648748848949049149249349449549649749849950050150250350450550650750850951051151251351451551651751851952052152252352452552652752852953053153253353453553653753853954054154254354454554654754854955055155255355455555655755855956056156256356456556656756856957057157257357457557657757857958058158258358458558658758858959059159259359459559659759859960060160260360460560660760860961061161261361461561661761861962062162262362462562662762862963063163263363463563663763863964064164264364464564664764864965065165265365465565665765865966066166266366466566666766866967067167267367467567667767867968068168268368468568668768868969069169269369469569669769869970070170270370470570670770870971071171271371471571671771871972072172272372472572672772872973073173273373473573673773873974074174274374474574674774874975075175275375475575675775875976076176276376476576676776876977077177277377477577677777877978078178278378478578678778878979079179279379479579679779879980080180280380480580680780880981081181281381481581681781881982082182282382482582682782882983083183283383483583683783883984084184284384484584684784884985085185285385485585685785885986086186286386486586686786886987087187287387487587687787887988088188288388488588688788888989089189289389489589689789889990090190290390490590690790890991091191291391491591691791891992092192292392492592692792892993093193293393493593693793893994094194294394494594694794894995095195295395495595695795895996096196296396496596696796896997097197297397497597697797897998098198298398498598698798898999099199299399499599699799899910001001100210031004100510061007100810091010101110121013101410151016101710181019102010211022102310241025102610271028102910301031103210331034103510361037103810391040104110421043104410451046104710481049105010511052105310541055105610571058105910601061106210631064106510661067106810691070107110721073107410751076107710781079108010811082108310841085108610871088108910901091109210931094109510961097109810991100110111021103110411051106110711081109111011111112111311141115111611171118111911201121112211231124112511261127112811291130113111321133113411351136113711381139114011411142114311441145114611471148114911501151115211531154115511561157115811591160116111621163116411651166116711681169117011711172117311741175117611771178117911801181118211831184118511861187118811891190119111921193119411951196
  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 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
  12. from netbox.forms import NetBoxModelImportForm
  13. from tenancy.models import Tenant
  14. from utilities.forms import CSVChoiceField, CSVContentTypeField, CSVModelChoiceField, CSVTypedChoiceField, SlugField
  15. from virtualization.models import Cluster
  16. from wireless.choices import WirelessRoleChoices
  17. from .common import ModuleCommonForm
  18. __all__ = (
  19. 'CableImportForm',
  20. 'ConsolePortImportForm',
  21. 'ConsoleServerPortImportForm',
  22. 'DeviceBayImportForm',
  23. 'DeviceImportForm',
  24. 'DeviceRoleImportForm',
  25. 'DeviceTypeImportForm',
  26. 'FrontPortImportForm',
  27. 'InterfaceImportForm',
  28. 'InventoryItemImportForm',
  29. 'InventoryItemRoleImportForm',
  30. 'LocationImportForm',
  31. 'ManufacturerImportForm',
  32. 'ModuleImportForm',
  33. 'ModuleBayImportForm',
  34. 'ModuleTypeImportForm',
  35. 'PlatformImportForm',
  36. 'PowerFeedImportForm',
  37. 'PowerOutletImportForm',
  38. 'PowerPanelImportForm',
  39. 'PowerPortImportForm',
  40. 'RackImportForm',
  41. 'RackReservationImportForm',
  42. 'RackRoleImportForm',
  43. 'RearPortImportForm',
  44. 'RegionImportForm',
  45. 'SiteImportForm',
  46. 'SiteGroupImportForm',
  47. 'VirtualChassisImportForm',
  48. 'VirtualDeviceContextImportForm'
  49. )
  50. class RegionImportForm(NetBoxModelImportForm):
  51. parent = CSVModelChoiceField(
  52. queryset=Region.objects.all(),
  53. required=False,
  54. to_field_name='name',
  55. help_text=_('Name of parent region')
  56. )
  57. class Meta:
  58. model = Region
  59. fields = ('name', 'slug', 'parent', 'description', 'tags')
  60. class SiteGroupImportForm(NetBoxModelImportForm):
  61. parent = CSVModelChoiceField(
  62. queryset=SiteGroup.objects.all(),
  63. required=False,
  64. to_field_name='name',
  65. help_text=_('Name of parent site group')
  66. )
  67. class Meta:
  68. model = SiteGroup
  69. fields = ('name', 'slug', 'parent', 'description')
  70. class SiteImportForm(NetBoxModelImportForm):
  71. status = CSVChoiceField(
  72. choices=SiteStatusChoices,
  73. help_text=_('Operational status')
  74. )
  75. region = CSVModelChoiceField(
  76. queryset=Region.objects.all(),
  77. required=False,
  78. to_field_name='name',
  79. help_text=_('Assigned region')
  80. )
  81. group = CSVModelChoiceField(
  82. queryset=SiteGroup.objects.all(),
  83. required=False,
  84. to_field_name='name',
  85. help_text=_('Assigned group')
  86. )
  87. tenant = CSVModelChoiceField(
  88. queryset=Tenant.objects.all(),
  89. required=False,
  90. to_field_name='name',
  91. help_text=_('Assigned tenant')
  92. )
  93. class Meta:
  94. model = Site
  95. fields = (
  96. 'name', 'slug', 'status', 'region', 'group', 'tenant', 'facility', 'time_zone', 'description',
  97. 'physical_address', 'shipping_address', 'latitude', 'longitude', 'comments', 'tags'
  98. )
  99. help_texts = {
  100. 'time_zone': mark_safe(
  101. _('Time zone (<a href="https://en.wikipedia.org/wiki/List_of_tz_database_time_zones">available options</a>)')
  102. )
  103. }
  104. class LocationImportForm(NetBoxModelImportForm):
  105. site = CSVModelChoiceField(
  106. queryset=Site.objects.all(),
  107. to_field_name='name',
  108. help_text=_('Assigned site')
  109. )
  110. parent = CSVModelChoiceField(
  111. queryset=Location.objects.all(),
  112. required=False,
  113. to_field_name='name',
  114. help_text=_('Parent location'),
  115. error_messages={
  116. 'invalid_choice': _('Location not found.'),
  117. }
  118. )
  119. status = CSVChoiceField(
  120. choices=LocationStatusChoices,
  121. help_text=_('Operational status')
  122. )
  123. tenant = CSVModelChoiceField(
  124. queryset=Tenant.objects.all(),
  125. required=False,
  126. to_field_name='name',
  127. help_text=_('Assigned tenant')
  128. )
  129. class Meta:
  130. model = Location
  131. fields = ('site', 'parent', 'name', 'slug', 'status', 'tenant', 'description', 'tags')
  132. class RackRoleImportForm(NetBoxModelImportForm):
  133. slug = SlugField()
  134. class Meta:
  135. model = RackRole
  136. fields = ('name', 'slug', 'color', 'description', 'tags')
  137. help_texts = {
  138. 'color': mark_safe(_('RGB color in hexadecimal (e.g. <code>00ff00</code>)')),
  139. }
  140. class RackImportForm(NetBoxModelImportForm):
  141. site = CSVModelChoiceField(
  142. queryset=Site.objects.all(),
  143. to_field_name='name'
  144. )
  145. location = CSVModelChoiceField(
  146. queryset=Location.objects.all(),
  147. required=False,
  148. to_field_name='name'
  149. )
  150. tenant = CSVModelChoiceField(
  151. queryset=Tenant.objects.all(),
  152. required=False,
  153. to_field_name='name',
  154. help_text=_('Name of assigned tenant')
  155. )
  156. status = CSVChoiceField(
  157. choices=RackStatusChoices,
  158. help_text=_('Operational status')
  159. )
  160. role = CSVModelChoiceField(
  161. queryset=RackRole.objects.all(),
  162. required=False,
  163. to_field_name='name',
  164. help_text=_('Name of assigned role')
  165. )
  166. type = CSVChoiceField(
  167. choices=RackTypeChoices,
  168. required=False,
  169. help_text=_('Rack type')
  170. )
  171. width = forms.ChoiceField(
  172. choices=RackWidthChoices,
  173. help_text=_('Rail-to-rail width (in inches)')
  174. )
  175. outer_unit = CSVChoiceField(
  176. choices=RackDimensionUnitChoices,
  177. required=False,
  178. help_text=_('Unit for outer dimensions')
  179. )
  180. weight_unit = CSVChoiceField(
  181. choices=WeightUnitChoices,
  182. required=False,
  183. help_text=_('Unit for rack weights')
  184. )
  185. class Meta:
  186. model = Rack
  187. fields = (
  188. 'site', 'location', 'name', 'facility_id', 'tenant', 'status', 'role', 'type', 'serial', 'asset_tag',
  189. 'width', 'u_height', 'desc_units', 'outer_width', 'outer_depth', 'outer_unit', 'mounting_depth', 'weight',
  190. 'max_weight', 'weight_unit', 'description', 'comments', 'tags',
  191. )
  192. def __init__(self, data=None, *args, **kwargs):
  193. super().__init__(data, *args, **kwargs)
  194. if data:
  195. # Limit location queryset by assigned site
  196. params = {f"site__{self.fields['site'].to_field_name}": data.get('site')}
  197. self.fields['location'].queryset = self.fields['location'].queryset.filter(**params)
  198. class RackReservationImportForm(NetBoxModelImportForm):
  199. site = CSVModelChoiceField(
  200. queryset=Site.objects.all(),
  201. to_field_name='name',
  202. help_text=_('Parent site')
  203. )
  204. location = CSVModelChoiceField(
  205. queryset=Location.objects.all(),
  206. to_field_name='name',
  207. required=False,
  208. help_text=_("Rack's location (if any)")
  209. )
  210. rack = CSVModelChoiceField(
  211. queryset=Rack.objects.all(),
  212. to_field_name='name',
  213. help_text=_('Rack')
  214. )
  215. units = SimpleArrayField(
  216. base_field=forms.IntegerField(),
  217. required=True,
  218. help_text=_('Comma-separated list of individual unit numbers')
  219. )
  220. tenant = CSVModelChoiceField(
  221. queryset=Tenant.objects.all(),
  222. required=False,
  223. to_field_name='name',
  224. help_text=_('Assigned tenant')
  225. )
  226. class Meta:
  227. model = RackReservation
  228. fields = ('site', 'location', 'rack', 'units', 'tenant', 'description', 'comments', 'tags')
  229. def __init__(self, data=None, *args, **kwargs):
  230. super().__init__(data, *args, **kwargs)
  231. if data:
  232. # Limit location queryset by assigned site
  233. params = {f"site__{self.fields['site'].to_field_name}": data.get('site')}
  234. self.fields['location'].queryset = self.fields['location'].queryset.filter(**params)
  235. # Limit rack queryset by assigned site and group
  236. params = {
  237. f"site__{self.fields['site'].to_field_name}": data.get('site'),
  238. f"location__{self.fields['location'].to_field_name}": data.get('location'),
  239. }
  240. self.fields['rack'].queryset = self.fields['rack'].queryset.filter(**params)
  241. class ManufacturerImportForm(NetBoxModelImportForm):
  242. class Meta:
  243. model = Manufacturer
  244. fields = ('name', 'slug', 'description', 'tags')
  245. class DeviceTypeImportForm(NetBoxModelImportForm):
  246. manufacturer = forms.ModelChoiceField(
  247. queryset=Manufacturer.objects.all(),
  248. to_field_name='name'
  249. )
  250. default_platform = forms.ModelChoiceField(
  251. queryset=Platform.objects.all(),
  252. to_field_name='name',
  253. required=False,
  254. )
  255. class Meta:
  256. model = DeviceType
  257. fields = [
  258. 'manufacturer', 'default_platform', 'model', 'slug', 'part_number', 'u_height', 'is_full_depth',
  259. 'subdevice_role', 'airflow', 'description', 'comments',
  260. ]
  261. class ModuleTypeImportForm(NetBoxModelImportForm):
  262. manufacturer = forms.ModelChoiceField(
  263. queryset=Manufacturer.objects.all(),
  264. to_field_name='name'
  265. )
  266. class Meta:
  267. model = ModuleType
  268. fields = ['manufacturer', 'model', 'part_number', 'description', 'comments']
  269. class DeviceRoleImportForm(NetBoxModelImportForm):
  270. config_template = CSVModelChoiceField(
  271. queryset=ConfigTemplate.objects.all(),
  272. to_field_name='name',
  273. required=False,
  274. help_text=_('Config template')
  275. )
  276. slug = SlugField()
  277. class Meta:
  278. model = DeviceRole
  279. fields = ('name', 'slug', 'color', 'vm_role', 'config_template', 'description', 'tags')
  280. help_texts = {
  281. 'color': mark_safe(_('RGB color in hexadecimal (e.g. <code>00ff00</code>)')),
  282. }
  283. class PlatformImportForm(NetBoxModelImportForm):
  284. slug = SlugField()
  285. manufacturer = CSVModelChoiceField(
  286. queryset=Manufacturer.objects.all(),
  287. required=False,
  288. to_field_name='name',
  289. help_text=_('Limit platform assignments to this manufacturer')
  290. )
  291. config_template = CSVModelChoiceField(
  292. queryset=ConfigTemplate.objects.all(),
  293. to_field_name='name',
  294. required=False,
  295. help_text=_('Config template')
  296. )
  297. class Meta:
  298. model = Platform
  299. fields = (
  300. 'name', 'slug', 'manufacturer', 'config_template', 'description', 'tags',
  301. )
  302. class BaseDeviceImportForm(NetBoxModelImportForm):
  303. device_role = CSVModelChoiceField(
  304. queryset=DeviceRole.objects.all(),
  305. to_field_name='name',
  306. help_text=_('Assigned role')
  307. )
  308. tenant = CSVModelChoiceField(
  309. queryset=Tenant.objects.all(),
  310. required=False,
  311. to_field_name='name',
  312. help_text=_('Assigned tenant')
  313. )
  314. manufacturer = CSVModelChoiceField(
  315. queryset=Manufacturer.objects.all(),
  316. to_field_name='name',
  317. help_text=_('Device type manufacturer')
  318. )
  319. device_type = CSVModelChoiceField(
  320. queryset=DeviceType.objects.all(),
  321. to_field_name='model',
  322. help_text=_('Device type model')
  323. )
  324. platform = CSVModelChoiceField(
  325. queryset=Platform.objects.all(),
  326. required=False,
  327. to_field_name='name',
  328. help_text=_('Assigned platform')
  329. )
  330. status = CSVChoiceField(
  331. choices=DeviceStatusChoices,
  332. help_text=_('Operational status')
  333. )
  334. virtual_chassis = CSVModelChoiceField(
  335. queryset=VirtualChassis.objects.all(),
  336. to_field_name='name',
  337. required=False,
  338. help_text=_('Virtual chassis')
  339. )
  340. cluster = CSVModelChoiceField(
  341. queryset=Cluster.objects.all(),
  342. to_field_name='name',
  343. required=False,
  344. help_text=_('Virtualization cluster')
  345. )
  346. class Meta:
  347. fields = []
  348. model = Device
  349. def __init__(self, data=None, *args, **kwargs):
  350. super().__init__(data, *args, **kwargs)
  351. if data:
  352. # Limit device type queryset by manufacturer
  353. params = {f"manufacturer__{self.fields['manufacturer'].to_field_name}": data.get('manufacturer')}
  354. self.fields['device_type'].queryset = self.fields['device_type'].queryset.filter(**params)
  355. class DeviceImportForm(BaseDeviceImportForm):
  356. site = CSVModelChoiceField(
  357. queryset=Site.objects.all(),
  358. to_field_name='name',
  359. help_text=_('Assigned site')
  360. )
  361. location = CSVModelChoiceField(
  362. queryset=Location.objects.all(),
  363. to_field_name='name',
  364. required=False,
  365. help_text=_("Assigned location (if any)")
  366. )
  367. rack = CSVModelChoiceField(
  368. queryset=Rack.objects.all(),
  369. to_field_name='name',
  370. required=False,
  371. help_text=_("Assigned rack (if any)")
  372. )
  373. face = CSVChoiceField(
  374. choices=DeviceFaceChoices,
  375. required=False,
  376. help_text=_('Mounted rack face')
  377. )
  378. parent = CSVModelChoiceField(
  379. queryset=Device.objects.all(),
  380. to_field_name='name',
  381. required=False,
  382. help_text=_('Parent device (for child devices)')
  383. )
  384. device_bay = CSVModelChoiceField(
  385. queryset=DeviceBay.objects.all(),
  386. to_field_name='name',
  387. required=False,
  388. help_text=_('Device bay in which this device is installed (for child devices)')
  389. )
  390. airflow = CSVChoiceField(
  391. choices=DeviceAirflowChoices,
  392. required=False,
  393. help_text=_('Airflow direction')
  394. )
  395. config_template = CSVModelChoiceField(
  396. queryset=ConfigTemplate.objects.all(),
  397. to_field_name='name',
  398. required=False,
  399. help_text=_('Config template')
  400. )
  401. class Meta(BaseDeviceImportForm.Meta):
  402. fields = [
  403. 'name', 'device_role', 'tenant', 'manufacturer', 'device_type', 'platform', 'serial', 'asset_tag', 'status',
  404. 'site', 'location', 'rack', 'position', 'face', 'parent', 'device_bay', 'airflow', 'virtual_chassis',
  405. 'vc_position', 'vc_priority', 'cluster', 'description', 'config_template', 'comments', 'tags',
  406. ]
  407. def __init__(self, data=None, *args, **kwargs):
  408. super().__init__(data, *args, **kwargs)
  409. if data:
  410. # Limit location queryset by assigned site
  411. params = {f"site__{self.fields['site'].to_field_name}": data.get('site')}
  412. self.fields['location'].queryset = self.fields['location'].queryset.filter(**params)
  413. self.fields['parent'].queryset = self.fields['parent'].queryset.filter(**params)
  414. # Limit rack queryset by assigned site and group
  415. params = {
  416. f"site__{self.fields['site'].to_field_name}": data.get('site'),
  417. f"location__{self.fields['location'].to_field_name}": data.get('location'),
  418. }
  419. self.fields['rack'].queryset = self.fields['rack'].queryset.filter(**params)
  420. # Limit device bay queryset by parent device
  421. if parent := data.get('parent'):
  422. params = {f"device__{self.fields['parent'].to_field_name}": parent}
  423. self.fields['device_bay'].queryset = self.fields['device_bay'].queryset.filter(**params)
  424. def clean(self):
  425. super().clean()
  426. # Inherit site and rack from parent device
  427. if parent := self.cleaned_data.get('parent'):
  428. self.instance.site = parent.site
  429. self.instance.rack = parent.rack
  430. # Set parent_bay reverse relationship
  431. if device_bay := self.cleaned_data.get('device_bay'):
  432. self.instance.parent_bay = device_bay
  433. class ModuleImportForm(ModuleCommonForm, NetBoxModelImportForm):
  434. device = CSVModelChoiceField(
  435. queryset=Device.objects.all(),
  436. to_field_name='name',
  437. help_text=_('The device in which this module is installed')
  438. )
  439. module_bay = CSVModelChoiceField(
  440. queryset=ModuleBay.objects.all(),
  441. to_field_name='name',
  442. help_text=_('The module bay in which this module is installed')
  443. )
  444. module_type = CSVModelChoiceField(
  445. queryset=ModuleType.objects.all(),
  446. to_field_name='model',
  447. help_text=_('The type of module')
  448. )
  449. status = CSVChoiceField(
  450. choices=ModuleStatusChoices,
  451. help_text=_('Operational status')
  452. )
  453. replicate_components = forms.BooleanField(
  454. required=False,
  455. help_text=_('Automatically populate components associated with this module type (enabled by default)')
  456. )
  457. adopt_components = forms.BooleanField(
  458. required=False,
  459. help_text=_('Adopt already existing components')
  460. )
  461. class Meta:
  462. model = Module
  463. fields = (
  464. 'device', 'module_bay', 'module_type', 'serial', 'asset_tag', 'status', 'description', 'comments',
  465. 'replicate_components', 'adopt_components', 'tags',
  466. )
  467. def __init__(self, data=None, *args, **kwargs):
  468. super().__init__(data, *args, **kwargs)
  469. if data:
  470. # Limit module_bay queryset by assigned device
  471. params = {f"device__{self.fields['device'].to_field_name}": data.get('device')}
  472. self.fields['module_bay'].queryset = self.fields['module_bay'].queryset.filter(**params)
  473. def clean_replicate_components(self):
  474. # Make sure replicate_components is True when it's not included in the uploaded data
  475. if 'replicate_components' not in self.data:
  476. return True
  477. else:
  478. return self.cleaned_data['replicate_components']
  479. #
  480. # Device components
  481. #
  482. class ConsolePortImportForm(NetBoxModelImportForm):
  483. device = CSVModelChoiceField(
  484. queryset=Device.objects.all(),
  485. to_field_name='name'
  486. )
  487. type = CSVChoiceField(
  488. choices=ConsolePortTypeChoices,
  489. required=False,
  490. help_text=_('Port type')
  491. )
  492. speed = CSVTypedChoiceField(
  493. choices=ConsolePortSpeedChoices,
  494. coerce=int,
  495. empty_value=None,
  496. required=False,
  497. help_text=_('Port speed in bps')
  498. )
  499. class Meta:
  500. model = ConsolePort
  501. fields = ('device', 'name', 'label', 'type', 'speed', 'mark_connected', 'description', 'tags')
  502. class ConsoleServerPortImportForm(NetBoxModelImportForm):
  503. device = CSVModelChoiceField(
  504. queryset=Device.objects.all(),
  505. to_field_name='name'
  506. )
  507. type = CSVChoiceField(
  508. choices=ConsolePortTypeChoices,
  509. required=False,
  510. help_text=_('Port type')
  511. )
  512. speed = CSVTypedChoiceField(
  513. choices=ConsolePortSpeedChoices,
  514. coerce=int,
  515. empty_value=None,
  516. required=False,
  517. help_text=_('Port speed in bps')
  518. )
  519. class Meta:
  520. model = ConsoleServerPort
  521. fields = ('device', 'name', 'label', 'type', 'speed', 'mark_connected', 'description', 'tags')
  522. class PowerPortImportForm(NetBoxModelImportForm):
  523. device = CSVModelChoiceField(
  524. queryset=Device.objects.all(),
  525. to_field_name='name'
  526. )
  527. type = CSVChoiceField(
  528. choices=PowerPortTypeChoices,
  529. required=False,
  530. help_text=_('Port type')
  531. )
  532. class Meta:
  533. model = PowerPort
  534. fields = (
  535. 'device', 'name', 'label', 'type', 'mark_connected', 'maximum_draw', 'allocated_draw', 'description', 'tags'
  536. )
  537. class PowerOutletImportForm(NetBoxModelImportForm):
  538. device = CSVModelChoiceField(
  539. queryset=Device.objects.all(),
  540. to_field_name='name'
  541. )
  542. type = CSVChoiceField(
  543. choices=PowerOutletTypeChoices,
  544. required=False,
  545. help_text=_('Outlet type')
  546. )
  547. power_port = CSVModelChoiceField(
  548. queryset=PowerPort.objects.all(),
  549. required=False,
  550. to_field_name='name',
  551. help_text=_('Local power port which feeds this outlet')
  552. )
  553. feed_leg = CSVChoiceField(
  554. choices=PowerOutletFeedLegChoices,
  555. required=False,
  556. help_text=_('Electrical phase (for three-phase circuits)')
  557. )
  558. class Meta:
  559. model = PowerOutlet
  560. fields = ('device', 'name', 'label', 'type', 'mark_connected', 'power_port', 'feed_leg', 'description', 'tags')
  561. def __init__(self, *args, **kwargs):
  562. super().__init__(*args, **kwargs)
  563. # Limit PowerPort choices to those belonging to this device (or VC master)
  564. if self.is_bound and 'device' in self.data:
  565. try:
  566. device = self.fields['device'].to_python(self.data['device'])
  567. except forms.ValidationError:
  568. device = None
  569. else:
  570. try:
  571. device = self.instance.device
  572. except Device.DoesNotExist:
  573. device = None
  574. if device:
  575. self.fields['power_port'].queryset = PowerPort.objects.filter(
  576. device__in=[device, device.get_vc_master()]
  577. )
  578. else:
  579. self.fields['power_port'].queryset = PowerPort.objects.none()
  580. class InterfaceImportForm(NetBoxModelImportForm):
  581. device = CSVModelChoiceField(
  582. queryset=Device.objects.all(),
  583. to_field_name='name'
  584. )
  585. parent = CSVModelChoiceField(
  586. queryset=Interface.objects.all(),
  587. required=False,
  588. to_field_name='name',
  589. help_text=_('Parent interface')
  590. )
  591. bridge = CSVModelChoiceField(
  592. queryset=Interface.objects.all(),
  593. required=False,
  594. to_field_name='name',
  595. help_text=_('Bridged interface')
  596. )
  597. lag = CSVModelChoiceField(
  598. queryset=Interface.objects.all(),
  599. required=False,
  600. to_field_name='name',
  601. help_text=_('Parent LAG interface')
  602. )
  603. type = CSVChoiceField(
  604. choices=InterfaceTypeChoices,
  605. help_text=_('Physical medium')
  606. )
  607. duplex = CSVChoiceField(
  608. choices=InterfaceDuplexChoices,
  609. required=False
  610. )
  611. poe_mode = CSVChoiceField(
  612. choices=InterfacePoEModeChoices,
  613. required=False,
  614. help_text=_('PoE mode')
  615. )
  616. poe_type = CSVChoiceField(
  617. choices=InterfacePoETypeChoices,
  618. required=False,
  619. help_text=_('PoE type')
  620. )
  621. mode = CSVChoiceField(
  622. choices=InterfaceModeChoices,
  623. required=False,
  624. help_text=_('IEEE 802.1Q operational mode (for L2 interfaces)')
  625. )
  626. vrf = CSVModelChoiceField(
  627. queryset=VRF.objects.all(),
  628. required=False,
  629. to_field_name='rd',
  630. help_text=_('Assigned VRF')
  631. )
  632. rf_role = CSVChoiceField(
  633. choices=WirelessRoleChoices,
  634. required=False,
  635. help_text=_('Wireless role (AP/station)')
  636. )
  637. class Meta:
  638. model = Interface
  639. fields = (
  640. 'device', 'name', 'label', 'parent', 'bridge', 'lag', 'type', 'speed', 'duplex', 'enabled',
  641. 'mark_connected', 'mac_address', 'wwn', 'mtu', 'mgmt_only', 'description', 'poe_mode', 'poe_type', 'mode',
  642. 'vrf', 'rf_role', 'rf_channel', 'rf_channel_frequency', 'rf_channel_width', 'tx_power', 'tags'
  643. )
  644. def __init__(self, data=None, *args, **kwargs):
  645. super().__init__(data, *args, **kwargs)
  646. if data:
  647. # Limit choices for parent, bridge, and LAG interfaces to the assigned device
  648. if device := data.get('device'):
  649. params = {
  650. f"device__{self.fields['device'].to_field_name}": device
  651. }
  652. self.fields['parent'].queryset = self.fields['parent'].queryset.filter(**params)
  653. self.fields['bridge'].queryset = self.fields['bridge'].queryset.filter(**params)
  654. self.fields['lag'].queryset = self.fields['lag'].queryset.filter(**params)
  655. def clean_enabled(self):
  656. # Make sure enabled is True when it's not included in the uploaded data
  657. if 'enabled' not in self.data:
  658. return True
  659. else:
  660. return self.cleaned_data['enabled']
  661. class FrontPortImportForm(NetBoxModelImportForm):
  662. device = CSVModelChoiceField(
  663. queryset=Device.objects.all(),
  664. to_field_name='name'
  665. )
  666. rear_port = CSVModelChoiceField(
  667. queryset=RearPort.objects.all(),
  668. to_field_name='name',
  669. help_text=_('Corresponding rear port')
  670. )
  671. type = CSVChoiceField(
  672. choices=PortTypeChoices,
  673. help_text=_('Physical medium classification')
  674. )
  675. class Meta:
  676. model = FrontPort
  677. fields = (
  678. 'device', 'name', 'label', 'type', 'color', 'mark_connected', 'rear_port', 'rear_port_position',
  679. 'description', 'tags'
  680. )
  681. def __init__(self, *args, **kwargs):
  682. super().__init__(*args, **kwargs)
  683. # Limit RearPort choices to those belonging to this device (or VC master)
  684. if self.is_bound and 'device' in self.data:
  685. try:
  686. device = self.fields['device'].to_python(self.data['device'])
  687. except forms.ValidationError:
  688. device = None
  689. else:
  690. try:
  691. device = self.instance.device
  692. except Device.DoesNotExist:
  693. device = None
  694. if device:
  695. self.fields['rear_port'].queryset = RearPort.objects.filter(
  696. device__in=[device, device.get_vc_master()]
  697. )
  698. else:
  699. self.fields['rear_port'].queryset = RearPort.objects.none()
  700. class RearPortImportForm(NetBoxModelImportForm):
  701. device = CSVModelChoiceField(
  702. queryset=Device.objects.all(),
  703. to_field_name='name'
  704. )
  705. type = CSVChoiceField(
  706. help_text=_('Physical medium classification'),
  707. choices=PortTypeChoices,
  708. )
  709. class Meta:
  710. model = RearPort
  711. fields = ('device', 'name', 'label', 'type', 'color', 'mark_connected', 'positions', 'description', 'tags')
  712. class ModuleBayImportForm(NetBoxModelImportForm):
  713. device = CSVModelChoiceField(
  714. queryset=Device.objects.all(),
  715. to_field_name='name'
  716. )
  717. class Meta:
  718. model = ModuleBay
  719. fields = ('device', 'name', 'label', 'position', 'description', 'tags')
  720. class DeviceBayImportForm(NetBoxModelImportForm):
  721. device = CSVModelChoiceField(
  722. queryset=Device.objects.all(),
  723. to_field_name='name'
  724. )
  725. installed_device = CSVModelChoiceField(
  726. queryset=Device.objects.all(),
  727. required=False,
  728. to_field_name='name',
  729. help_text=_('Child device installed within this bay'),
  730. error_messages={
  731. 'invalid_choice': _('Child device not found.'),
  732. }
  733. )
  734. class Meta:
  735. model = DeviceBay
  736. fields = ('device', 'name', 'label', 'installed_device', 'description', 'tags')
  737. def __init__(self, *args, **kwargs):
  738. super().__init__(*args, **kwargs)
  739. # Limit installed device choices to devices of the correct type and location
  740. if self.is_bound and 'device' in self.data:
  741. try:
  742. device = self.fields['device'].to_python(self.data['device'])
  743. except forms.ValidationError:
  744. device = None
  745. else:
  746. try:
  747. device = self.instance.device
  748. except Device.DoesNotExist:
  749. device = None
  750. if device:
  751. self.fields['installed_device'].queryset = Device.objects.filter(
  752. site=device.site,
  753. rack=device.rack,
  754. parent_bay__isnull=True,
  755. device_type__u_height=0,
  756. device_type__subdevice_role=SubdeviceRoleChoices.ROLE_CHILD
  757. ).exclude(pk=device.pk)
  758. else:
  759. self.fields['installed_device'].queryset = Interface.objects.none()
  760. class InventoryItemImportForm(NetBoxModelImportForm):
  761. device = CSVModelChoiceField(
  762. queryset=Device.objects.all(),
  763. to_field_name='name'
  764. )
  765. role = CSVModelChoiceField(
  766. queryset=InventoryItemRole.objects.all(),
  767. to_field_name='name',
  768. required=False
  769. )
  770. manufacturer = CSVModelChoiceField(
  771. queryset=Manufacturer.objects.all(),
  772. to_field_name='name',
  773. required=False
  774. )
  775. parent = CSVModelChoiceField(
  776. queryset=Device.objects.all(),
  777. to_field_name='name',
  778. required=False,
  779. help_text=_('Parent inventory item')
  780. )
  781. component_type = CSVContentTypeField(
  782. queryset=ContentType.objects.all(),
  783. limit_choices_to=MODULAR_COMPONENT_MODELS,
  784. required=False,
  785. help_text=_('Component Type')
  786. )
  787. component_name = forms.CharField(
  788. required=False,
  789. help_text=_('Component Name')
  790. )
  791. class Meta:
  792. model = InventoryItem
  793. fields = (
  794. 'device', 'name', 'label', 'role', 'manufacturer', 'part_id', 'serial', 'asset_tag', 'discovered',
  795. 'description', 'tags', 'component_type', 'component_name',
  796. )
  797. def __init__(self, *args, **kwargs):
  798. super().__init__(*args, **kwargs)
  799. # Limit parent choices to inventory items belonging to this device
  800. device = None
  801. if self.is_bound and 'device' in self.data:
  802. try:
  803. device = self.fields['device'].to_python(self.data['device'])
  804. except forms.ValidationError:
  805. pass
  806. if device:
  807. self.fields['parent'].queryset = InventoryItem.objects.filter(device=device)
  808. else:
  809. self.fields['parent'].queryset = InventoryItem.objects.none()
  810. def clean_component_name(self):
  811. content_type = self.cleaned_data.get('component_type')
  812. component_name = self.cleaned_data.get('component_name')
  813. device = self.cleaned_data.get("device")
  814. if not device and hasattr(self, 'instance'):
  815. device = self.instance.device
  816. if not all([device, content_type, component_name]):
  817. return None
  818. model = content_type.model_class()
  819. try:
  820. component = model.objects.get(device=device, name=component_name)
  821. self.instance.component = component
  822. except ObjectDoesNotExist:
  823. raise forms.ValidationError(f"Component not found: {device} - {component_name}")
  824. #
  825. # Device component roles
  826. #
  827. class InventoryItemRoleImportForm(NetBoxModelImportForm):
  828. slug = SlugField()
  829. class Meta:
  830. model = InventoryItemRole
  831. fields = ('name', 'slug', 'color', 'description')
  832. help_texts = {
  833. 'color': mark_safe(_('RGB color in hexadecimal (e.g. <code>00ff00</code>)')),
  834. }
  835. #
  836. # Cables
  837. #
  838. class CableImportForm(NetBoxModelImportForm):
  839. # Termination A
  840. side_a_device = CSVModelChoiceField(
  841. queryset=Device.objects.all(),
  842. to_field_name='name',
  843. help_text=_('Side A device')
  844. )
  845. side_a_type = CSVContentTypeField(
  846. queryset=ContentType.objects.all(),
  847. limit_choices_to=CABLE_TERMINATION_MODELS,
  848. help_text=_('Side A type')
  849. )
  850. side_a_name = forms.CharField(
  851. help_text=_('Side A component name')
  852. )
  853. # Termination B
  854. side_b_device = CSVModelChoiceField(
  855. queryset=Device.objects.all(),
  856. to_field_name='name',
  857. help_text=_('Side B device')
  858. )
  859. side_b_type = CSVContentTypeField(
  860. queryset=ContentType.objects.all(),
  861. limit_choices_to=CABLE_TERMINATION_MODELS,
  862. help_text=_('Side B type')
  863. )
  864. side_b_name = forms.CharField(
  865. help_text=_('Side B component name')
  866. )
  867. # Cable attributes
  868. status = CSVChoiceField(
  869. choices=LinkStatusChoices,
  870. required=False,
  871. help_text=_('Connection status')
  872. )
  873. type = CSVChoiceField(
  874. choices=CableTypeChoices,
  875. required=False,
  876. help_text=_('Physical medium classification')
  877. )
  878. tenant = CSVModelChoiceField(
  879. queryset=Tenant.objects.all(),
  880. required=False,
  881. to_field_name='name',
  882. help_text=_('Assigned tenant')
  883. )
  884. length_unit = CSVChoiceField(
  885. choices=CableLengthUnitChoices,
  886. required=False,
  887. help_text=_('Length unit')
  888. )
  889. class Meta:
  890. model = Cable
  891. fields = [
  892. 'side_a_device', 'side_a_type', 'side_a_name', 'side_b_device', 'side_b_type', 'side_b_name', 'type',
  893. 'status', 'tenant', 'label', 'color', 'length', 'length_unit', 'description', 'comments', 'tags',
  894. ]
  895. help_texts = {
  896. 'color': mark_safe(_('RGB color in hexadecimal (e.g. <code>00ff00</code>)')),
  897. }
  898. def _clean_side(self, side):
  899. """
  900. Derive a Cable's A/B termination objects.
  901. :param side: 'a' or 'b'
  902. """
  903. assert side in 'ab', f"Invalid side designation: {side}"
  904. device = self.cleaned_data.get(f'side_{side}_device')
  905. content_type = self.cleaned_data.get(f'side_{side}_type')
  906. name = self.cleaned_data.get(f'side_{side}_name')
  907. if not device or not content_type or not name:
  908. return None
  909. model = content_type.model_class()
  910. try:
  911. termination_object = model.objects.get(device=device, name=name)
  912. if termination_object.cable is not None:
  913. raise forms.ValidationError(f"Side {side.upper()}: {device} {termination_object} is already connected")
  914. except ObjectDoesNotExist:
  915. raise forms.ValidationError(f"{side.upper()} side termination not found: {device} {name}")
  916. setattr(self.instance, f'{side}_terminations', [termination_object])
  917. return termination_object
  918. def clean_side_a_name(self):
  919. return self._clean_side('a')
  920. def clean_side_b_name(self):
  921. return self._clean_side('b')
  922. def clean_length_unit(self):
  923. # Avoid trying to save as NULL
  924. length_unit = self.cleaned_data.get('length_unit', None)
  925. return length_unit if length_unit is not None else ''
  926. #
  927. # Virtual chassis
  928. #
  929. class VirtualChassisImportForm(NetBoxModelImportForm):
  930. master = CSVModelChoiceField(
  931. queryset=Device.objects.all(),
  932. to_field_name='name',
  933. required=False,
  934. help_text=_('Master device')
  935. )
  936. class Meta:
  937. model = VirtualChassis
  938. fields = ('name', 'domain', 'master', 'description', 'comments', 'tags')
  939. #
  940. # Power
  941. #
  942. class PowerPanelImportForm(NetBoxModelImportForm):
  943. site = CSVModelChoiceField(
  944. queryset=Site.objects.all(),
  945. to_field_name='name',
  946. help_text=_('Name of parent site')
  947. )
  948. location = CSVModelChoiceField(
  949. queryset=Location.objects.all(),
  950. required=False,
  951. to_field_name='name'
  952. )
  953. class Meta:
  954. model = PowerPanel
  955. fields = ('site', 'location', 'name', 'description', 'comments', 'tags')
  956. def __init__(self, data=None, *args, **kwargs):
  957. super().__init__(data, *args, **kwargs)
  958. if data:
  959. # Limit group queryset by assigned site
  960. params = {f"site__{self.fields['site'].to_field_name}": data.get('site')}
  961. self.fields['location'].queryset = self.fields['location'].queryset.filter(**params)
  962. class PowerFeedImportForm(NetBoxModelImportForm):
  963. site = CSVModelChoiceField(
  964. queryset=Site.objects.all(),
  965. to_field_name='name',
  966. help_text=_('Assigned site')
  967. )
  968. power_panel = CSVModelChoiceField(
  969. queryset=PowerPanel.objects.all(),
  970. to_field_name='name',
  971. help_text=_('Upstream power panel')
  972. )
  973. location = CSVModelChoiceField(
  974. queryset=Location.objects.all(),
  975. to_field_name='name',
  976. required=False,
  977. help_text=_("Rack's location (if any)")
  978. )
  979. rack = CSVModelChoiceField(
  980. queryset=Rack.objects.all(),
  981. to_field_name='name',
  982. required=False,
  983. help_text=_('Rack')
  984. )
  985. status = CSVChoiceField(
  986. choices=PowerFeedStatusChoices,
  987. help_text=_('Operational status')
  988. )
  989. type = CSVChoiceField(
  990. choices=PowerFeedTypeChoices,
  991. help_text=_('Primary or redundant')
  992. )
  993. supply = CSVChoiceField(
  994. choices=PowerFeedSupplyChoices,
  995. help_text=_('Supply type (AC/DC)')
  996. )
  997. phase = CSVChoiceField(
  998. choices=PowerFeedPhaseChoices,
  999. help_text=_('Single or three-phase')
  1000. )
  1001. class Meta:
  1002. model = PowerFeed
  1003. fields = (
  1004. 'site', 'power_panel', 'location', 'rack', 'name', 'status', 'type', 'mark_connected', 'supply', 'phase',
  1005. 'voltage', 'amperage', 'max_utilization', 'description', 'comments', 'tags',
  1006. )
  1007. def __init__(self, data=None, *args, **kwargs):
  1008. super().__init__(data, *args, **kwargs)
  1009. if data:
  1010. # Limit power_panel queryset by site
  1011. params = {f"site__{self.fields['site'].to_field_name}": data.get('site')}
  1012. self.fields['power_panel'].queryset = self.fields['power_panel'].queryset.filter(**params)
  1013. # Limit location queryset by site
  1014. params = {f"site__{self.fields['site'].to_field_name}": data.get('site')}
  1015. self.fields['location'].queryset = self.fields['location'].queryset.filter(**params)
  1016. # Limit rack queryset by site and group
  1017. params = {
  1018. f"site__{self.fields['site'].to_field_name}": data.get('site'),
  1019. f"location__{self.fields['location'].to_field_name}": data.get('location'),
  1020. }
  1021. self.fields['rack'].queryset = self.fields['rack'].queryset.filter(**params)
  1022. class VirtualDeviceContextImportForm(NetBoxModelImportForm):
  1023. device = CSVModelChoiceField(
  1024. queryset=Device.objects.all(),
  1025. to_field_name='name',
  1026. help_text='Assigned role'
  1027. )
  1028. tenant = CSVModelChoiceField(
  1029. queryset=Tenant.objects.all(),
  1030. required=False,
  1031. to_field_name='name',
  1032. help_text='Assigned tenant'
  1033. )
  1034. class Meta:
  1035. fields = [
  1036. 'name', 'device', 'status', 'tenant', 'identifier', 'comments',
  1037. ]
  1038. model = VirtualDeviceContext