models.py 40 KB

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