bulk_import.py 30 KB

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