bulk_import.py 32 KB

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