models.py 38 KB

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