bulk_import.py 33 KB

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