models.py 41 KB

1234567891011121314151617181920212223242526272829303132333435363738394041424344454647484950515253545556575859606162636465666768697071727374757677787980818283848586878889909192939495969798991001011021031041051061071081091101111121131141151161171181191201211221231241251261271281291301311321331341351361371381391401411421431441451461471481491501511521531541551561571581591601611621631641651661671681691701711721731741751761771781791801811821831841851861871881891901911921931941951961971981992002012022032042052062072082092102112122132142152162172182192202212222232242252262272282292302312322332342352362372382392402412422432442452462472482492502512522532542552562572582592602612622632642652662672682692702712722732742752762772782792802812822832842852862872882892902912922932942952962972982993003013023033043053063073083093103113123133143153163173183193203213223233243253263273283293303313323333343353363373383393403413423433443453463473483493503513523533543553563573583593603613623633643653663673683693703713723733743753763773783793803813823833843853863873883893903913923933943953963973983994004014024034044054064074084094104114124134144154164174184194204214224234244254264274284294304314324334344354364374384394404414424434444454464474484494504514524534544554564574584594604614624634644654664674684694704714724734744754764774784794804814824834844854864874884894904914924934944954964974984995005015025035045055065075085095105115125135145155165175185195205215225235245255265275285295305315325335345355365375385395405415425435445455465475485495505515525535545555565575585595605615625635645655665675685695705715725735745755765775785795805815825835845855865875885895905915925935945955965975985996006016026036046056066076086096106116126136146156166176186196206216226236246256266276286296306316326336346356366376386396406416426436446456466476486496506516526536546556566576586596606616626636646656666676686696706716726736746756766776786796806816826836846856866876886896906916926936946956966976986997007017027037047057067077087097107117127137147157167177187197207217227237247257267277287297307317327337347357367377387397407417427437447457467477487497507517527537547557567577587597607617627637647657667677687697707717727737747757767777787797807817827837847857867877887897907917927937947957967977987998008018028038048058068078088098108118128138148158168178188198208218228238248258268278288298308318328338348358368378388398408418428438448458468478488498508518528538548558568578588598608618628638648658668678688698708718728738748758768778788798808818828838848858868878888898908918928938948958968978988999009019029039049059069079089099109119129139149159169179189199209219229239249259269279289299309319329339349359369379389399409419429439449459469479489499509519529539549559569579589599609619629639649659669679689699709719729739749759769779789799809819829839849859869879889899909919929939949959969979989991000100110021003100410051006100710081009101010111012101310141015101610171018101910201021102210231024102510261027102810291030103110321033103410351036103710381039104010411042104310441045104610471048104910501051105210531054105510561057105810591060106110621063106410651066106710681069107010711072107310741075107610771078107910801081108210831084108510861087108810891090109110921093109410951096109710981099110011011102110311041105110611071108110911101111111211131114111511161117111811191120112111221123112411251126112711281129113011311132113311341135113611371138113911401141114211431144114511461147114811491150115111521153115411551156115711581159116011611162116311641165116611671168116911701171117211731174117511761177117811791180118111821183118411851186118711881189119011911192119311941195119611971198119912001201120212031204120512061207120812091210121112121213121412151216121712181219122012211222122312241225122612271228122912301231123212331234123512361237123812391240124112421243124412451246124712481249125012511252125312541255125612571258125912601261126212631264126512661267126812691270127112721273127412751276127712781279128012811282128312841285128612871288128912901291129212931294129512961297129812991300130113021303130413051306130713081309131013111312131313141315131613171318131913201321132213231324132513261327132813291330133113321333133413351336133713381339134013411342134313441345134613471348134913501351135213531354135513561357135813591360136113621363136413651366136713681369137013711372137313741375137613771378137913801381138213831384
  1. from django import forms
  2. from django.utils.translation import gettext as _
  3. from django.contrib.auth.models import User
  4. from django.contrib.contenttypes.models import ContentType
  5. from timezone_field import TimeZoneFormField
  6. from dcim.choices import *
  7. from dcim.constants import *
  8. from dcim.models import *
  9. from extras.forms import CustomFieldModelForm
  10. from extras.models import Tag
  11. from ipam.models import IPAddress, VLAN, VLANGroup, ASN
  12. from tenancy.forms import TenancyForm
  13. from utilities.forms import (
  14. APISelect, add_blank_choice, BootstrapMixin, ClearableFileInput, CommentField, DynamicModelChoiceField,
  15. DynamicModelMultipleChoiceField, JSONField, NumericArrayField, SelectWithPK, SmallTextarea,
  16. SlugField, StaticSelect,
  17. )
  18. from virtualization.models import Cluster, ClusterGroup
  19. from wireless.models import WirelessLAN, WirelessLANGroup
  20. from .common import InterfaceCommonForm
  21. __all__ = (
  22. 'CableForm',
  23. 'ConsolePortForm',
  24. 'ConsolePortTemplateForm',
  25. 'ConsoleServerPortForm',
  26. 'ConsoleServerPortTemplateForm',
  27. 'DeviceBayForm',
  28. 'DeviceBayTemplateForm',
  29. 'DeviceForm',
  30. 'DeviceRoleForm',
  31. 'DeviceTypeForm',
  32. 'DeviceVCMembershipForm',
  33. 'FrontPortForm',
  34. 'FrontPortTemplateForm',
  35. 'InterfaceForm',
  36. 'InterfaceTemplateForm',
  37. 'InventoryItemForm',
  38. 'LocationForm',
  39. 'ManufacturerForm',
  40. 'ModuleForm',
  41. 'ModuleBayForm',
  42. 'ModuleBayTemplateForm',
  43. 'ModuleTypeForm',
  44. 'PlatformForm',
  45. 'PopulateDeviceBayForm',
  46. 'PowerFeedForm',
  47. 'PowerOutletForm',
  48. 'PowerOutletTemplateForm',
  49. 'PowerPanelForm',
  50. 'PowerPortForm',
  51. 'PowerPortTemplateForm',
  52. 'RackForm',
  53. 'RackReservationForm',
  54. 'RackRoleForm',
  55. 'RearPortForm',
  56. 'RearPortTemplateForm',
  57. 'RegionForm',
  58. 'SiteForm',
  59. 'SiteGroupForm',
  60. 'VCMemberSelectForm',
  61. 'VirtualChassisForm',
  62. )
  63. INTERFACE_MODE_HELP_TEXT = """
  64. Access: One untagged VLAN<br />
  65. Tagged: One untagged VLAN and/or one or more tagged VLANs<br />
  66. Tagged (All): Implies all VLANs are available (w/optional untagged VLAN)
  67. """
  68. class RegionForm(CustomFieldModelForm):
  69. parent = DynamicModelChoiceField(
  70. queryset=Region.objects.all(),
  71. required=False
  72. )
  73. slug = SlugField()
  74. tags = DynamicModelMultipleChoiceField(
  75. queryset=Tag.objects.all(),
  76. required=False
  77. )
  78. class Meta:
  79. model = Region
  80. fields = (
  81. 'parent', 'name', 'slug', 'description', 'tags',
  82. )
  83. class SiteGroupForm(CustomFieldModelForm):
  84. parent = DynamicModelChoiceField(
  85. queryset=SiteGroup.objects.all(),
  86. required=False
  87. )
  88. slug = SlugField()
  89. tags = DynamicModelMultipleChoiceField(
  90. queryset=Tag.objects.all(),
  91. required=False
  92. )
  93. class Meta:
  94. model = SiteGroup
  95. fields = (
  96. 'parent', 'name', 'slug', 'description', 'tags',
  97. )
  98. class SiteForm(TenancyForm, CustomFieldModelForm):
  99. region = DynamicModelChoiceField(
  100. queryset=Region.objects.all(),
  101. required=False
  102. )
  103. group = DynamicModelChoiceField(
  104. queryset=SiteGroup.objects.all(),
  105. required=False
  106. )
  107. asns = DynamicModelMultipleChoiceField(
  108. queryset=ASN.objects.all(),
  109. label=_('ASNs'),
  110. required=False
  111. )
  112. slug = SlugField()
  113. time_zone = TimeZoneFormField(
  114. choices=add_blank_choice(TimeZoneFormField().choices),
  115. required=False,
  116. widget=StaticSelect()
  117. )
  118. comments = CommentField()
  119. tags = DynamicModelMultipleChoiceField(
  120. queryset=Tag.objects.all(),
  121. required=False
  122. )
  123. class Meta:
  124. model = Site
  125. fields = (
  126. 'name', 'slug', 'status', 'region', 'group', 'tenant_group', 'tenant', 'facility', 'asns', 'time_zone',
  127. 'description', 'physical_address', 'shipping_address', 'latitude', 'longitude', 'comments', 'tags',
  128. )
  129. fieldsets = (
  130. ('Site', (
  131. 'name', 'slug', 'status', 'region', 'group', 'facility', 'asns', 'time_zone', 'description', 'tags',
  132. )),
  133. ('Tenancy', ('tenant_group', 'tenant')),
  134. ('Contact Info', ('physical_address', 'shipping_address', 'latitude', 'longitude')),
  135. )
  136. widgets = {
  137. 'physical_address': SmallTextarea(
  138. attrs={
  139. 'rows': 3,
  140. }
  141. ),
  142. 'shipping_address': SmallTextarea(
  143. attrs={
  144. 'rows': 3,
  145. }
  146. ),
  147. 'status': StaticSelect(),
  148. 'time_zone': StaticSelect(),
  149. }
  150. help_texts = {
  151. 'name': "Full name of the site",
  152. 'facility': "Data center provider and facility (e.g. Equinix NY7)",
  153. 'time_zone': "Local time zone",
  154. 'description': "Short description (will appear in sites list)",
  155. 'physical_address': "Physical location of the building (e.g. for GPS)",
  156. 'shipping_address': "If different from the physical address",
  157. 'latitude': "Latitude in decimal format (xx.yyyyyy)",
  158. 'longitude': "Longitude in decimal format (xx.yyyyyy)"
  159. }
  160. class LocationForm(TenancyForm, CustomFieldModelForm):
  161. region = DynamicModelChoiceField(
  162. queryset=Region.objects.all(),
  163. required=False,
  164. initial_params={
  165. 'sites': '$site'
  166. }
  167. )
  168. site_group = DynamicModelChoiceField(
  169. queryset=SiteGroup.objects.all(),
  170. required=False,
  171. initial_params={
  172. 'sites': '$site'
  173. }
  174. )
  175. site = DynamicModelChoiceField(
  176. queryset=Site.objects.all(),
  177. query_params={
  178. 'region_id': '$region',
  179. 'group_id': '$site_group',
  180. }
  181. )
  182. parent = DynamicModelChoiceField(
  183. queryset=Location.objects.all(),
  184. required=False,
  185. query_params={
  186. 'site_id': '$site'
  187. }
  188. )
  189. slug = SlugField()
  190. tags = DynamicModelMultipleChoiceField(
  191. queryset=Tag.objects.all(),
  192. required=False
  193. )
  194. class Meta:
  195. model = Location
  196. fields = (
  197. 'region', 'site_group', 'site', 'parent', 'name', 'slug', 'description', 'tenant_group', 'tenant', 'tags',
  198. )
  199. fieldsets = (
  200. ('Location', (
  201. 'region', 'site_group', 'site', 'parent', 'name', 'slug', 'description', 'tags',
  202. )),
  203. ('Tenancy', ('tenant_group', 'tenant')),
  204. )
  205. class RackRoleForm(CustomFieldModelForm):
  206. slug = SlugField()
  207. tags = DynamicModelMultipleChoiceField(
  208. queryset=Tag.objects.all(),
  209. required=False
  210. )
  211. class Meta:
  212. model = RackRole
  213. fields = [
  214. 'name', 'slug', 'color', 'description', 'tags',
  215. ]
  216. class RackForm(TenancyForm, CustomFieldModelForm):
  217. region = DynamicModelChoiceField(
  218. queryset=Region.objects.all(),
  219. required=False,
  220. initial_params={
  221. 'sites': '$site'
  222. }
  223. )
  224. site_group = DynamicModelChoiceField(
  225. queryset=SiteGroup.objects.all(),
  226. required=False,
  227. initial_params={
  228. 'sites': '$site'
  229. }
  230. )
  231. site = DynamicModelChoiceField(
  232. queryset=Site.objects.all(),
  233. query_params={
  234. 'region_id': '$region',
  235. 'group_id': '$site_group',
  236. }
  237. )
  238. location = DynamicModelChoiceField(
  239. queryset=Location.objects.all(),
  240. required=False,
  241. query_params={
  242. 'site_id': '$site'
  243. }
  244. )
  245. role = DynamicModelChoiceField(
  246. queryset=RackRole.objects.all(),
  247. required=False
  248. )
  249. comments = CommentField()
  250. tags = DynamicModelMultipleChoiceField(
  251. queryset=Tag.objects.all(),
  252. required=False
  253. )
  254. class Meta:
  255. model = Rack
  256. fields = [
  257. 'region', 'site_group', 'site', 'location', 'name', 'facility_id', 'tenant_group', 'tenant', 'status',
  258. 'role', 'serial', 'asset_tag', 'type', 'width', 'u_height', 'desc_units', 'outer_width', 'outer_depth',
  259. 'outer_unit', 'comments', 'tags',
  260. ]
  261. help_texts = {
  262. 'site': "The site at which the rack exists",
  263. 'name': "Organizational rack name",
  264. 'facility_id': "The unique rack ID assigned by the facility",
  265. 'u_height': "Height in rack units",
  266. }
  267. widgets = {
  268. 'status': StaticSelect(),
  269. 'type': StaticSelect(),
  270. 'width': StaticSelect(),
  271. 'outer_unit': StaticSelect(),
  272. }
  273. class RackReservationForm(TenancyForm, CustomFieldModelForm):
  274. region = DynamicModelChoiceField(
  275. queryset=Region.objects.all(),
  276. required=False,
  277. initial_params={
  278. 'sites': '$site'
  279. },
  280. fetch_trigger='open'
  281. )
  282. site_group = DynamicModelChoiceField(
  283. queryset=SiteGroup.objects.all(),
  284. required=False,
  285. initial_params={
  286. 'sites': '$site'
  287. },
  288. fetch_trigger='open'
  289. )
  290. site = DynamicModelChoiceField(
  291. queryset=Site.objects.all(),
  292. required=False,
  293. query_params={
  294. 'region_id': '$region',
  295. 'group_id': '$site_group',
  296. },
  297. fetch_trigger='open'
  298. )
  299. location = DynamicModelChoiceField(
  300. queryset=Location.objects.all(),
  301. required=False,
  302. query_params={
  303. 'site_id': '$site'
  304. },
  305. fetch_trigger='open'
  306. )
  307. rack = DynamicModelChoiceField(
  308. queryset=Rack.objects.all(),
  309. query_params={
  310. 'site_id': '$site',
  311. 'location_id': '$location',
  312. },
  313. fetch_trigger='open'
  314. )
  315. units = NumericArrayField(
  316. base_field=forms.IntegerField(),
  317. help_text="Comma-separated list of numeric unit IDs. A range may be specified using a hyphen."
  318. )
  319. user = forms.ModelChoiceField(
  320. queryset=User.objects.order_by(
  321. 'username'
  322. ),
  323. widget=StaticSelect()
  324. )
  325. tags = DynamicModelMultipleChoiceField(
  326. queryset=Tag.objects.all(),
  327. required=False,
  328. fetch_trigger='open'
  329. )
  330. class Meta:
  331. model = RackReservation
  332. fields = [
  333. 'region', 'site_group', 'site', 'location', 'rack', 'units', 'user', 'tenant_group', 'tenant',
  334. 'description', 'tags',
  335. ]
  336. fieldsets = (
  337. ('Reservation', ('region', 'site', 'location', 'rack', 'units', 'user', 'description', 'tags')),
  338. ('Tenancy', ('tenant_group', 'tenant')),
  339. )
  340. class ManufacturerForm(CustomFieldModelForm):
  341. slug = SlugField()
  342. tags = DynamicModelMultipleChoiceField(
  343. queryset=Tag.objects.all(),
  344. required=False
  345. )
  346. class Meta:
  347. model = Manufacturer
  348. fields = [
  349. 'name', 'slug', 'description', 'tags',
  350. ]
  351. class DeviceTypeForm(CustomFieldModelForm):
  352. manufacturer = DynamicModelChoiceField(
  353. queryset=Manufacturer.objects.all()
  354. )
  355. slug = SlugField(
  356. slug_source='model'
  357. )
  358. comments = CommentField()
  359. tags = DynamicModelMultipleChoiceField(
  360. queryset=Tag.objects.all(),
  361. required=False
  362. )
  363. class Meta:
  364. model = DeviceType
  365. fields = [
  366. 'manufacturer', 'model', 'slug', 'part_number', 'u_height', 'is_full_depth', 'subdevice_role', 'airflow',
  367. 'front_image', 'rear_image', 'comments', 'tags',
  368. ]
  369. fieldsets = (
  370. ('Device Type', (
  371. 'manufacturer', 'model', 'slug', 'part_number', 'tags',
  372. )),
  373. ('Chassis', (
  374. 'u_height', 'is_full_depth', 'subdevice_role', 'airflow',
  375. )),
  376. ('Images', ('front_image', 'rear_image')),
  377. )
  378. widgets = {
  379. 'subdevice_role': StaticSelect(),
  380. 'front_image': ClearableFileInput(attrs={
  381. 'accept': DEVICETYPE_IMAGE_FORMATS
  382. }),
  383. 'rear_image': ClearableFileInput(attrs={
  384. 'accept': DEVICETYPE_IMAGE_FORMATS
  385. })
  386. }
  387. class ModuleTypeForm(CustomFieldModelForm):
  388. manufacturer = DynamicModelChoiceField(
  389. queryset=Manufacturer.objects.all()
  390. )
  391. comments = CommentField()
  392. tags = DynamicModelMultipleChoiceField(
  393. queryset=Tag.objects.all(),
  394. required=False
  395. )
  396. class Meta:
  397. model = ModuleType
  398. fields = [
  399. 'manufacturer', 'model', 'part_number', 'comments', 'tags',
  400. ]
  401. class DeviceRoleForm(CustomFieldModelForm):
  402. slug = SlugField()
  403. tags = DynamicModelMultipleChoiceField(
  404. queryset=Tag.objects.all(),
  405. required=False
  406. )
  407. class Meta:
  408. model = DeviceRole
  409. fields = [
  410. 'name', 'slug', 'color', 'vm_role', 'description', 'tags',
  411. ]
  412. class PlatformForm(CustomFieldModelForm):
  413. manufacturer = DynamicModelChoiceField(
  414. queryset=Manufacturer.objects.all(),
  415. required=False
  416. )
  417. slug = SlugField(
  418. max_length=64
  419. )
  420. tags = DynamicModelMultipleChoiceField(
  421. queryset=Tag.objects.all(),
  422. required=False
  423. )
  424. class Meta:
  425. model = Platform
  426. fields = [
  427. 'name', 'slug', 'manufacturer', 'napalm_driver', 'napalm_args', 'description', 'tags',
  428. ]
  429. widgets = {
  430. 'napalm_args': SmallTextarea(),
  431. }
  432. class DeviceForm(TenancyForm, CustomFieldModelForm):
  433. region = DynamicModelChoiceField(
  434. queryset=Region.objects.all(),
  435. required=False,
  436. initial_params={
  437. 'sites': '$site'
  438. }
  439. )
  440. site_group = DynamicModelChoiceField(
  441. queryset=SiteGroup.objects.all(),
  442. required=False,
  443. initial_params={
  444. 'sites': '$site'
  445. }
  446. )
  447. site = DynamicModelChoiceField(
  448. queryset=Site.objects.all(),
  449. query_params={
  450. 'region_id': '$region',
  451. 'group_id': '$site_group',
  452. }
  453. )
  454. location = DynamicModelChoiceField(
  455. queryset=Location.objects.all(),
  456. required=False,
  457. query_params={
  458. 'site_id': '$site'
  459. },
  460. initial_params={
  461. 'racks': '$rack'
  462. }
  463. )
  464. rack = DynamicModelChoiceField(
  465. queryset=Rack.objects.all(),
  466. required=False,
  467. query_params={
  468. 'site_id': '$site',
  469. 'location_id': '$location',
  470. }
  471. )
  472. position = forms.IntegerField(
  473. required=False,
  474. help_text="The lowest-numbered unit occupied by the device",
  475. widget=APISelect(
  476. api_url='/api/dcim/racks/{{rack}}/elevation/',
  477. attrs={
  478. 'disabled-indicator': 'device',
  479. 'data-dynamic-params': '[{"fieldName":"face","queryParam":"face"}]'
  480. }
  481. )
  482. )
  483. manufacturer = DynamicModelChoiceField(
  484. queryset=Manufacturer.objects.all(),
  485. required=False,
  486. initial_params={
  487. 'device_types': '$device_type'
  488. }
  489. )
  490. device_type = DynamicModelChoiceField(
  491. queryset=DeviceType.objects.all(),
  492. query_params={
  493. 'manufacturer_id': '$manufacturer'
  494. }
  495. )
  496. device_role = DynamicModelChoiceField(
  497. queryset=DeviceRole.objects.all()
  498. )
  499. platform = DynamicModelChoiceField(
  500. queryset=Platform.objects.all(),
  501. required=False,
  502. query_params={
  503. 'manufacturer_id': ['$manufacturer', 'null']
  504. }
  505. )
  506. cluster_group = DynamicModelChoiceField(
  507. queryset=ClusterGroup.objects.all(),
  508. required=False,
  509. null_option='None',
  510. initial_params={
  511. 'clusters': '$cluster'
  512. }
  513. )
  514. cluster = DynamicModelChoiceField(
  515. queryset=Cluster.objects.all(),
  516. required=False,
  517. query_params={
  518. 'group_id': '$cluster_group'
  519. }
  520. )
  521. comments = CommentField()
  522. local_context_data = JSONField(
  523. required=False,
  524. label=''
  525. )
  526. tags = DynamicModelMultipleChoiceField(
  527. queryset=Tag.objects.all(),
  528. required=False
  529. )
  530. class Meta:
  531. model = Device
  532. fields = [
  533. 'name', 'device_role', 'device_type', 'serial', 'asset_tag', 'region', 'site_group', 'site', 'rack',
  534. 'location', 'position', 'face', 'status', 'airflow', 'platform', 'primary_ip4', 'primary_ip6',
  535. 'cluster_group', 'cluster', 'tenant_group', 'tenant', 'comments', 'tags', 'local_context_data'
  536. ]
  537. help_texts = {
  538. 'device_role': "The function this device serves",
  539. 'serial': "Chassis serial number",
  540. 'local_context_data': "Local config context data overwrites all source contexts in the final rendered "
  541. "config context",
  542. }
  543. widgets = {
  544. 'face': StaticSelect(),
  545. 'status': StaticSelect(),
  546. 'airflow': StaticSelect(),
  547. 'primary_ip4': StaticSelect(),
  548. 'primary_ip6': StaticSelect(),
  549. }
  550. def __init__(self, *args, **kwargs):
  551. super().__init__(*args, **kwargs)
  552. if self.instance.pk:
  553. # Compile list of choices for primary IPv4 and IPv6 addresses
  554. for family in [4, 6]:
  555. ip_choices = [(None, '---------')]
  556. # Gather PKs of all interfaces belonging to this Device or a peer VirtualChassis member
  557. interface_ids = self.instance.vc_interfaces(if_master=False).values_list('pk', flat=True)
  558. # Collect interface IPs
  559. interface_ips = IPAddress.objects.filter(
  560. address__family=family,
  561. assigned_object_type=ContentType.objects.get_for_model(Interface),
  562. assigned_object_id__in=interface_ids
  563. ).prefetch_related('assigned_object')
  564. if interface_ips:
  565. ip_list = [(ip.id, f'{ip.address} ({ip.assigned_object})') for ip in interface_ips]
  566. ip_choices.append(('Interface IPs', ip_list))
  567. # Collect NAT IPs
  568. nat_ips = IPAddress.objects.prefetch_related('nat_inside').filter(
  569. address__family=family,
  570. nat_inside__assigned_object_type=ContentType.objects.get_for_model(Interface),
  571. nat_inside__assigned_object_id__in=interface_ids
  572. ).prefetch_related('assigned_object')
  573. if nat_ips:
  574. ip_list = [(ip.id, f'{ip.address} (NAT)') for ip in nat_ips]
  575. ip_choices.append(('NAT IPs', ip_list))
  576. self.fields['primary_ip{}'.format(family)].choices = ip_choices
  577. # If editing an existing device, exclude it from the list of occupied rack units. This ensures that a device
  578. # can be flipped from one face to another.
  579. self.fields['position'].widget.add_query_param('exclude', self.instance.pk)
  580. # Limit platform by manufacturer
  581. self.fields['platform'].queryset = Platform.objects.filter(
  582. Q(manufacturer__isnull=True) | Q(manufacturer=self.instance.device_type.manufacturer)
  583. )
  584. # Disable rack assignment if this is a child device installed in a parent device
  585. if self.instance.device_type.is_child_device and hasattr(self.instance, 'parent_bay'):
  586. self.fields['site'].disabled = True
  587. self.fields['rack'].disabled = True
  588. self.initial['site'] = self.instance.parent_bay.device.site_id
  589. self.initial['rack'] = self.instance.parent_bay.device.rack_id
  590. else:
  591. # An object that doesn't exist yet can't have any IPs assigned to it
  592. self.fields['primary_ip4'].choices = []
  593. self.fields['primary_ip4'].widget.attrs['readonly'] = True
  594. self.fields['primary_ip6'].choices = []
  595. self.fields['primary_ip6'].widget.attrs['readonly'] = True
  596. # Rack position
  597. position = self.data.get('position') or self.initial.get('position')
  598. if position:
  599. self.fields['position'].widget.choices = [(position, f'U{position}')]
  600. class ModuleForm(CustomFieldModelForm):
  601. device = DynamicModelChoiceField(
  602. queryset=Device.objects.all(),
  603. required=False,
  604. initial_params={
  605. 'modulebays': '$module_bay'
  606. }
  607. )
  608. module_bay = DynamicModelChoiceField(
  609. queryset=ModuleBay.objects.all(),
  610. query_params={
  611. 'device_id': '$device'
  612. }
  613. )
  614. manufacturer = DynamicModelChoiceField(
  615. queryset=Manufacturer.objects.all(),
  616. required=False,
  617. initial_params={
  618. 'device_types': '$device_type'
  619. }
  620. )
  621. module_type = DynamicModelChoiceField(
  622. queryset=ModuleType.objects.all(),
  623. query_params={
  624. 'manufacturer_id': '$manufacturer'
  625. }
  626. )
  627. comments = CommentField()
  628. tags = DynamicModelMultipleChoiceField(
  629. queryset=Tag.objects.all(),
  630. required=False
  631. )
  632. class Meta:
  633. model = Module
  634. fields = [
  635. 'device', 'module_bay', 'manufacturer', 'module_type', 'serial', 'asset_tag', 'tags', 'comments',
  636. ]
  637. class CableForm(TenancyForm, CustomFieldModelForm):
  638. tags = DynamicModelMultipleChoiceField(
  639. queryset=Tag.objects.all(),
  640. required=False
  641. )
  642. class Meta:
  643. model = Cable
  644. fields = [
  645. 'type', 'status', 'tenant_group', 'tenant', 'label', 'color', 'length', 'length_unit', 'tags',
  646. ]
  647. widgets = {
  648. 'status': StaticSelect,
  649. 'type': StaticSelect,
  650. 'length_unit': StaticSelect,
  651. }
  652. error_messages = {
  653. 'length': {
  654. 'max_value': 'Maximum length is 32767 (any unit)'
  655. }
  656. }
  657. class PowerPanelForm(CustomFieldModelForm):
  658. region = DynamicModelChoiceField(
  659. queryset=Region.objects.all(),
  660. required=False,
  661. initial_params={
  662. 'sites': '$site'
  663. }
  664. )
  665. site_group = DynamicModelChoiceField(
  666. queryset=SiteGroup.objects.all(),
  667. required=False,
  668. initial_params={
  669. 'sites': '$site'
  670. }
  671. )
  672. site = DynamicModelChoiceField(
  673. queryset=Site.objects.all(),
  674. query_params={
  675. 'region_id': '$region',
  676. 'group_id': '$site_group',
  677. }
  678. )
  679. location = DynamicModelChoiceField(
  680. queryset=Location.objects.all(),
  681. required=False,
  682. query_params={
  683. 'site_id': '$site'
  684. }
  685. )
  686. tags = DynamicModelMultipleChoiceField(
  687. queryset=Tag.objects.all(),
  688. required=False
  689. )
  690. class Meta:
  691. model = PowerPanel
  692. fields = [
  693. 'region', 'site_group', 'site', 'location', 'name', 'tags',
  694. ]
  695. fieldsets = (
  696. ('Power Panel', ('region', 'site_group', 'site', 'location', 'name', 'tags')),
  697. )
  698. class PowerFeedForm(CustomFieldModelForm):
  699. region = DynamicModelChoiceField(
  700. queryset=Region.objects.all(),
  701. required=False,
  702. initial_params={
  703. 'sites__powerpanel': '$power_panel'
  704. }
  705. )
  706. site_group = DynamicModelChoiceField(
  707. queryset=SiteGroup.objects.all(),
  708. required=False,
  709. initial_params={
  710. 'sites': '$site'
  711. }
  712. )
  713. site = DynamicModelChoiceField(
  714. queryset=Site.objects.all(),
  715. required=False,
  716. initial_params={
  717. 'powerpanel': '$power_panel'
  718. },
  719. query_params={
  720. 'region_id': '$region',
  721. 'group_id': '$site_group',
  722. }
  723. )
  724. power_panel = DynamicModelChoiceField(
  725. queryset=PowerPanel.objects.all(),
  726. query_params={
  727. 'site_id': '$site'
  728. }
  729. )
  730. rack = DynamicModelChoiceField(
  731. queryset=Rack.objects.all(),
  732. required=False,
  733. query_params={
  734. 'site_id': '$site'
  735. }
  736. )
  737. comments = CommentField()
  738. tags = DynamicModelMultipleChoiceField(
  739. queryset=Tag.objects.all(),
  740. required=False
  741. )
  742. class Meta:
  743. model = PowerFeed
  744. fields = [
  745. 'region', 'site_group', 'site', 'power_panel', 'rack', 'name', 'status', 'type', 'mark_connected', 'supply',
  746. 'phase', 'voltage', 'amperage', 'max_utilization', 'comments', 'tags',
  747. ]
  748. fieldsets = (
  749. ('Power Panel', ('region', 'site', 'power_panel')),
  750. ('Power Feed', ('rack', 'name', 'status', 'type', 'mark_connected', 'tags')),
  751. ('Characteristics', ('supply', 'voltage', 'amperage', 'phase', 'max_utilization')),
  752. )
  753. widgets = {
  754. 'status': StaticSelect(),
  755. 'type': StaticSelect(),
  756. 'supply': StaticSelect(),
  757. 'phase': StaticSelect(),
  758. }
  759. #
  760. # Virtual chassis
  761. #
  762. class VirtualChassisForm(CustomFieldModelForm):
  763. master = forms.ModelChoiceField(
  764. queryset=Device.objects.all(),
  765. required=False,
  766. )
  767. tags = DynamicModelMultipleChoiceField(
  768. queryset=Tag.objects.all(),
  769. required=False
  770. )
  771. class Meta:
  772. model = VirtualChassis
  773. fields = [
  774. 'name', 'domain', 'master', 'tags',
  775. ]
  776. widgets = {
  777. 'master': SelectWithPK(),
  778. }
  779. def __init__(self, *args, **kwargs):
  780. super().__init__(*args, **kwargs)
  781. self.fields['master'].queryset = Device.objects.filter(virtual_chassis=self.instance)
  782. class DeviceVCMembershipForm(forms.ModelForm):
  783. class Meta:
  784. model = Device
  785. fields = [
  786. 'vc_position', 'vc_priority',
  787. ]
  788. labels = {
  789. 'vc_position': 'Position',
  790. 'vc_priority': 'Priority',
  791. }
  792. def __init__(self, validate_vc_position=False, *args, **kwargs):
  793. super().__init__(*args, **kwargs)
  794. # Require VC position (only required when the Device is a VirtualChassis member)
  795. self.fields['vc_position'].required = True
  796. # Add bootstrap classes to form elements.
  797. self.fields['vc_position'].widget.attrs = {'class': 'form-control'}
  798. self.fields['vc_priority'].widget.attrs = {'class': 'form-control'}
  799. # Validation of vc_position is optional. This is only required when adding a new member to an existing
  800. # VirtualChassis. Otherwise, vc_position validation is handled by BaseVCMemberFormSet.
  801. self.validate_vc_position = validate_vc_position
  802. def clean_vc_position(self):
  803. vc_position = self.cleaned_data['vc_position']
  804. if self.validate_vc_position:
  805. conflicting_members = Device.objects.filter(
  806. virtual_chassis=self.instance.virtual_chassis,
  807. vc_position=vc_position
  808. )
  809. if conflicting_members.exists():
  810. raise forms.ValidationError(
  811. 'A virtual chassis member already exists in position {}.'.format(vc_position)
  812. )
  813. return vc_position
  814. class VCMemberSelectForm(BootstrapMixin, forms.Form):
  815. region = DynamicModelChoiceField(
  816. queryset=Region.objects.all(),
  817. required=False,
  818. initial_params={
  819. 'sites': '$site'
  820. }
  821. )
  822. site_group = DynamicModelChoiceField(
  823. queryset=SiteGroup.objects.all(),
  824. required=False,
  825. initial_params={
  826. 'sites': '$site'
  827. }
  828. )
  829. site = DynamicModelChoiceField(
  830. queryset=Site.objects.all(),
  831. required=False,
  832. query_params={
  833. 'region_id': '$region',
  834. 'group_id': '$site_group',
  835. }
  836. )
  837. rack = DynamicModelChoiceField(
  838. queryset=Rack.objects.all(),
  839. required=False,
  840. null_option='None',
  841. query_params={
  842. 'site_id': '$site'
  843. }
  844. )
  845. device = DynamicModelChoiceField(
  846. queryset=Device.objects.all(),
  847. query_params={
  848. 'site_id': '$site',
  849. 'rack_id': '$rack',
  850. 'virtual_chassis_id': 'null',
  851. }
  852. )
  853. def clean_device(self):
  854. device = self.cleaned_data['device']
  855. if device.virtual_chassis is not None:
  856. raise forms.ValidationError(
  857. f"Device {device} is already assigned to a virtual chassis."
  858. )
  859. return device
  860. #
  861. # Device component templates
  862. #
  863. class ConsolePortTemplateForm(BootstrapMixin, forms.ModelForm):
  864. class Meta:
  865. model = ConsolePortTemplate
  866. fields = [
  867. 'device_type', 'module_type', 'name', 'label', 'type', 'description',
  868. ]
  869. widgets = {
  870. 'device_type': forms.HiddenInput(),
  871. 'module_type': forms.HiddenInput(),
  872. }
  873. class ConsoleServerPortTemplateForm(BootstrapMixin, forms.ModelForm):
  874. class Meta:
  875. model = ConsoleServerPortTemplate
  876. fields = [
  877. 'device_type', 'module_type', 'name', 'label', 'type', 'description',
  878. ]
  879. widgets = {
  880. 'device_type': forms.HiddenInput(),
  881. 'module_type': forms.HiddenInput(),
  882. }
  883. class PowerPortTemplateForm(BootstrapMixin, forms.ModelForm):
  884. class Meta:
  885. model = PowerPortTemplate
  886. fields = [
  887. 'device_type', 'module_type', 'name', 'label', 'type', 'maximum_draw', 'allocated_draw', 'description',
  888. ]
  889. widgets = {
  890. 'device_type': forms.HiddenInput(),
  891. 'module_type': forms.HiddenInput(),
  892. }
  893. class PowerOutletTemplateForm(BootstrapMixin, forms.ModelForm):
  894. class Meta:
  895. model = PowerOutletTemplate
  896. fields = [
  897. 'device_type', 'module_type', 'name', 'label', 'type', 'power_port', 'feed_leg', 'description',
  898. ]
  899. widgets = {
  900. 'device_type': forms.HiddenInput(),
  901. 'module_type': forms.HiddenInput(),
  902. }
  903. def __init__(self, *args, **kwargs):
  904. super().__init__(*args, **kwargs)
  905. # Limit power_port choices to current DeviceType/ModuleType
  906. if self.instance.pk:
  907. self.fields['power_port'].queryset = PowerPortTemplate.objects.filter(
  908. device_type=self.instance.device_type,
  909. module_type=self.instance.module_type
  910. )
  911. class InterfaceTemplateForm(BootstrapMixin, forms.ModelForm):
  912. class Meta:
  913. model = InterfaceTemplate
  914. fields = [
  915. 'device_type', 'module_type', 'name', 'label', 'type', 'mgmt_only', 'description',
  916. ]
  917. widgets = {
  918. 'device_type': forms.HiddenInput(),
  919. 'module_type': forms.HiddenInput(),
  920. 'type': StaticSelect(),
  921. }
  922. class FrontPortTemplateForm(BootstrapMixin, forms.ModelForm):
  923. class Meta:
  924. model = FrontPortTemplate
  925. fields = [
  926. 'device_type', 'module_type', 'name', 'label', 'type', 'color', 'rear_port', 'rear_port_position',
  927. 'description',
  928. ]
  929. widgets = {
  930. 'device_type': forms.HiddenInput(),
  931. 'module_type': forms.HiddenInput(),
  932. 'rear_port': StaticSelect(),
  933. }
  934. def __init__(self, *args, **kwargs):
  935. super().__init__(*args, **kwargs)
  936. # Limit rear_port choices to current DeviceType/ModuleType
  937. if self.instance.pk:
  938. self.fields['rear_port'].queryset = RearPortTemplate.objects.filter(
  939. device_type=self.instance.device_type,
  940. module_type=self.instance.module_type
  941. )
  942. class RearPortTemplateForm(BootstrapMixin, forms.ModelForm):
  943. class Meta:
  944. model = RearPortTemplate
  945. fields = [
  946. 'device_type', 'module_type', 'name', 'label', 'type', 'color', 'positions', 'description',
  947. ]
  948. widgets = {
  949. 'device_type': forms.HiddenInput(),
  950. 'module_type': forms.HiddenInput(),
  951. 'type': StaticSelect(),
  952. }
  953. class ModuleBayTemplateForm(BootstrapMixin, forms.ModelForm):
  954. class Meta:
  955. model = ModuleBayTemplate
  956. fields = [
  957. 'device_type', 'name', 'label', 'position', 'description',
  958. ]
  959. widgets = {
  960. 'device_type': forms.HiddenInput(),
  961. }
  962. class DeviceBayTemplateForm(BootstrapMixin, forms.ModelForm):
  963. class Meta:
  964. model = DeviceBayTemplate
  965. fields = [
  966. 'device_type', 'name', 'label', 'description',
  967. ]
  968. widgets = {
  969. 'device_type': forms.HiddenInput(),
  970. }
  971. #
  972. # Device components
  973. #
  974. class ConsolePortForm(CustomFieldModelForm):
  975. tags = DynamicModelMultipleChoiceField(
  976. queryset=Tag.objects.all(),
  977. required=False
  978. )
  979. class Meta:
  980. model = ConsolePort
  981. fields = [
  982. 'device', 'name', 'label', 'type', 'speed', 'mark_connected', 'description', 'tags',
  983. ]
  984. widgets = {
  985. 'device': forms.HiddenInput(),
  986. }
  987. class ConsoleServerPortForm(CustomFieldModelForm):
  988. tags = DynamicModelMultipleChoiceField(
  989. queryset=Tag.objects.all(),
  990. required=False
  991. )
  992. class Meta:
  993. model = ConsoleServerPort
  994. fields = [
  995. 'device', 'name', 'label', 'type', 'speed', 'mark_connected', 'description', 'tags',
  996. ]
  997. widgets = {
  998. 'device': forms.HiddenInput(),
  999. }
  1000. class PowerPortForm(CustomFieldModelForm):
  1001. tags = DynamicModelMultipleChoiceField(
  1002. queryset=Tag.objects.all(),
  1003. required=False
  1004. )
  1005. class Meta:
  1006. model = PowerPort
  1007. fields = [
  1008. 'device', 'name', 'label', 'type', 'maximum_draw', 'allocated_draw', 'mark_connected', 'description',
  1009. 'tags',
  1010. ]
  1011. widgets = {
  1012. 'device': forms.HiddenInput(),
  1013. }
  1014. class PowerOutletForm(CustomFieldModelForm):
  1015. power_port = forms.ModelChoiceField(
  1016. queryset=PowerPort.objects.all(),
  1017. required=False
  1018. )
  1019. tags = DynamicModelMultipleChoiceField(
  1020. queryset=Tag.objects.all(),
  1021. required=False
  1022. )
  1023. class Meta:
  1024. model = PowerOutlet
  1025. fields = [
  1026. 'device', 'name', 'label', 'type', 'power_port', 'feed_leg', 'mark_connected', 'description', 'tags',
  1027. ]
  1028. widgets = {
  1029. 'device': forms.HiddenInput(),
  1030. }
  1031. def __init__(self, *args, **kwargs):
  1032. super().__init__(*args, **kwargs)
  1033. # Limit power_port choices to the local device
  1034. if hasattr(self.instance, 'device'):
  1035. self.fields['power_port'].queryset = PowerPort.objects.filter(
  1036. device=self.instance.device
  1037. )
  1038. class InterfaceForm(InterfaceCommonForm, CustomFieldModelForm):
  1039. parent = DynamicModelChoiceField(
  1040. queryset=Interface.objects.all(),
  1041. required=False,
  1042. label='Parent interface'
  1043. )
  1044. bridge = DynamicModelChoiceField(
  1045. queryset=Interface.objects.all(),
  1046. required=False,
  1047. label='Bridged interface'
  1048. )
  1049. lag = DynamicModelChoiceField(
  1050. queryset=Interface.objects.all(),
  1051. required=False,
  1052. label='LAG interface',
  1053. query_params={
  1054. 'type': 'lag',
  1055. }
  1056. )
  1057. wireless_lan_group = DynamicModelChoiceField(
  1058. queryset=WirelessLANGroup.objects.all(),
  1059. required=False,
  1060. label='Wireless LAN group'
  1061. )
  1062. wireless_lans = DynamicModelMultipleChoiceField(
  1063. queryset=WirelessLAN.objects.all(),
  1064. required=False,
  1065. label='Wireless LANs',
  1066. query_params={
  1067. 'group_id': '$wireless_lan_group',
  1068. }
  1069. )
  1070. vlan_group = DynamicModelChoiceField(
  1071. queryset=VLANGroup.objects.all(),
  1072. required=False,
  1073. label='VLAN group'
  1074. )
  1075. untagged_vlan = DynamicModelChoiceField(
  1076. queryset=VLAN.objects.all(),
  1077. required=False,
  1078. label='Untagged VLAN',
  1079. query_params={
  1080. 'group_id': '$vlan_group',
  1081. }
  1082. )
  1083. tagged_vlans = DynamicModelMultipleChoiceField(
  1084. queryset=VLAN.objects.all(),
  1085. required=False,
  1086. label='Tagged VLANs',
  1087. query_params={
  1088. 'group_id': '$vlan_group',
  1089. }
  1090. )
  1091. tags = DynamicModelMultipleChoiceField(
  1092. queryset=Tag.objects.all(),
  1093. required=False
  1094. )
  1095. class Meta:
  1096. model = Interface
  1097. fields = [
  1098. 'device', 'name', 'label', 'type', 'enabled', 'parent', 'bridge', 'lag', 'mac_address', 'wwn', 'mtu',
  1099. 'mgmt_only', 'mark_connected', 'description', 'mode', 'rf_role', 'rf_channel', 'rf_channel_frequency',
  1100. 'rf_channel_width', 'tx_power', 'wireless_lans', 'untagged_vlan', 'tagged_vlans', 'tags',
  1101. ]
  1102. widgets = {
  1103. 'device': forms.HiddenInput(),
  1104. 'type': StaticSelect(),
  1105. 'mode': StaticSelect(),
  1106. 'rf_role': StaticSelect(),
  1107. 'rf_channel': StaticSelect(),
  1108. }
  1109. labels = {
  1110. 'mode': '802.1Q Mode',
  1111. }
  1112. help_texts = {
  1113. 'mode': INTERFACE_MODE_HELP_TEXT,
  1114. 'rf_channel_frequency': "Populated by selected channel (if set)",
  1115. 'rf_channel_width': "Populated by selected channel (if set)",
  1116. }
  1117. def __init__(self, *args, **kwargs):
  1118. super().__init__(*args, **kwargs)
  1119. device = Device.objects.get(pk=self.data['device']) if self.is_bound else self.instance.device
  1120. # Restrict parent/bridge/LAG interface assignment by device/VC
  1121. self.fields['parent'].widget.add_query_param('device_id', device.pk)
  1122. self.fields['bridge'].widget.add_query_param('device_id', device.pk)
  1123. self.fields['lag'].widget.add_query_param('device_id', device.pk)
  1124. if device.virtual_chassis and device.virtual_chassis.master:
  1125. self.fields['parent'].widget.add_query_param('device_id', device.virtual_chassis.master.pk)
  1126. self.fields['bridge'].widget.add_query_param('device_id', device.virtual_chassis.master.pk)
  1127. self.fields['lag'].widget.add_query_param('device_id', device.virtual_chassis.master.pk)
  1128. # Limit VLAN choices by device
  1129. self.fields['untagged_vlan'].widget.add_query_param('available_on_device', device.pk)
  1130. self.fields['tagged_vlans'].widget.add_query_param('available_on_device', device.pk)
  1131. class FrontPortForm(CustomFieldModelForm):
  1132. tags = DynamicModelMultipleChoiceField(
  1133. queryset=Tag.objects.all(),
  1134. required=False
  1135. )
  1136. class Meta:
  1137. model = FrontPort
  1138. fields = [
  1139. 'device', 'name', 'label', 'type', 'color', 'rear_port', 'rear_port_position', 'mark_connected',
  1140. 'description', 'tags',
  1141. ]
  1142. widgets = {
  1143. 'device': forms.HiddenInput(),
  1144. 'type': StaticSelect(),
  1145. 'rear_port': StaticSelect(),
  1146. }
  1147. def __init__(self, *args, **kwargs):
  1148. super().__init__(*args, **kwargs)
  1149. # Limit RearPort choices to the local device
  1150. if hasattr(self.instance, 'device'):
  1151. self.fields['rear_port'].queryset = self.fields['rear_port'].queryset.filter(
  1152. device=self.instance.device
  1153. )
  1154. class RearPortForm(CustomFieldModelForm):
  1155. tags = DynamicModelMultipleChoiceField(
  1156. queryset=Tag.objects.all(),
  1157. required=False
  1158. )
  1159. class Meta:
  1160. model = RearPort
  1161. fields = [
  1162. 'device', 'name', 'label', 'type', 'color', 'positions', 'mark_connected', 'description', 'tags',
  1163. ]
  1164. widgets = {
  1165. 'device': forms.HiddenInput(),
  1166. 'type': StaticSelect(),
  1167. }
  1168. class ModuleBayForm(CustomFieldModelForm):
  1169. tags = DynamicModelMultipleChoiceField(
  1170. queryset=Tag.objects.all(),
  1171. required=False
  1172. )
  1173. class Meta:
  1174. model = ModuleBay
  1175. fields = [
  1176. 'device', 'name', 'label', 'position', 'description', 'tags',
  1177. ]
  1178. widgets = {
  1179. 'device': forms.HiddenInput(),
  1180. }
  1181. class DeviceBayForm(CustomFieldModelForm):
  1182. tags = DynamicModelMultipleChoiceField(
  1183. queryset=Tag.objects.all(),
  1184. required=False
  1185. )
  1186. class Meta:
  1187. model = DeviceBay
  1188. fields = [
  1189. 'device', 'name', 'label', 'description', 'tags',
  1190. ]
  1191. widgets = {
  1192. 'device': forms.HiddenInput(),
  1193. }
  1194. class PopulateDeviceBayForm(BootstrapMixin, forms.Form):
  1195. installed_device = forms.ModelChoiceField(
  1196. queryset=Device.objects.all(),
  1197. label='Child Device',
  1198. help_text="Child devices must first be created and assigned to the site/rack of the parent device.",
  1199. widget=StaticSelect(),
  1200. )
  1201. def __init__(self, device_bay, *args, **kwargs):
  1202. super().__init__(*args, **kwargs)
  1203. self.fields['installed_device'].queryset = Device.objects.filter(
  1204. site=device_bay.device.site,
  1205. rack=device_bay.device.rack,
  1206. parent_bay__isnull=True,
  1207. device_type__u_height=0,
  1208. device_type__subdevice_role=SubdeviceRoleChoices.ROLE_CHILD
  1209. ).exclude(pk=device_bay.device.pk)
  1210. class InventoryItemForm(CustomFieldModelForm):
  1211. device = DynamicModelChoiceField(
  1212. queryset=Device.objects.all()
  1213. )
  1214. parent = DynamicModelChoiceField(
  1215. queryset=InventoryItem.objects.all(),
  1216. required=False,
  1217. query_params={
  1218. 'device_id': '$device'
  1219. }
  1220. )
  1221. manufacturer = DynamicModelChoiceField(
  1222. queryset=Manufacturer.objects.all(),
  1223. required=False
  1224. )
  1225. tags = DynamicModelMultipleChoiceField(
  1226. queryset=Tag.objects.all(),
  1227. required=False
  1228. )
  1229. class Meta:
  1230. model = InventoryItem
  1231. fields = [
  1232. 'device', 'parent', 'name', 'label', 'manufacturer', 'part_id', 'serial', 'asset_tag', 'description',
  1233. 'tags',
  1234. ]