2
0

models.py 37 KB

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