2
0

models.py 37 KB

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