bulk_import.py 30 KB

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