forms.py 78 KB

1234567891011121314151617181920212223242526272829303132333435363738394041424344454647484950515253545556575859606162636465666768697071727374757677787980818283848586878889909192939495969798991001011021031041051061071081091101111121131141151161171181191201211221231241251261271281291301311321331341351361371381391401411421431441451461471481491501511521531541551561571581591601611621631641651661671681691701711721731741751761771781791801811821831841851861871881891901911921931941951961971981992002012022032042052062072082092102112122132142152162172182192202212222232242252262272282292302312322332342352362372382392402412422432442452462472482492502512522532542552562572582592602612622632642652662672682692702712722732742752762772782792802812822832842852862872882892902912922932942952962972982993003013023033043053063073083093103113123133143153163173183193203213223233243253263273283293303313323333343353363373383393403413423433443453463473483493503513523533543553563573583593603613623633643653663673683693703713723733743753763773783793803813823833843853863873883893903913923933943953963973983994004014024034044054064074084094104114124134144154164174184194204214224234244254264274284294304314324334344354364374384394404414424434444454464474484494504514524534544554564574584594604614624634644654664674684694704714724734744754764774784794804814824834844854864874884894904914924934944954964974984995005015025035045055065075085095105115125135145155165175185195205215225235245255265275285295305315325335345355365375385395405415425435445455465475485495505515525535545555565575585595605615625635645655665675685695705715725735745755765775785795805815825835845855865875885895905915925935945955965975985996006016026036046056066076086096106116126136146156166176186196206216226236246256266276286296306316326336346356366376386396406416426436446456466476486496506516526536546556566576586596606616626636646656666676686696706716726736746756766776786796806816826836846856866876886896906916926936946956966976986997007017027037047057067077087097107117127137147157167177187197207217227237247257267277287297307317327337347357367377387397407417427437447457467477487497507517527537547557567577587597607617627637647657667677687697707717727737747757767777787797807817827837847857867877887897907917927937947957967977987998008018028038048058068078088098108118128138148158168178188198208218228238248258268278288298308318328338348358368378388398408418428438448458468478488498508518528538548558568578588598608618628638648658668678688698708718728738748758768778788798808818828838848858868878888898908918928938948958968978988999009019029039049059069079089099109119129139149159169179189199209219229239249259269279289299309319329339349359369379389399409419429439449459469479489499509519529539549559569579589599609619629639649659669679689699709719729739749759769779789799809819829839849859869879889899909919929939949959969979989991000100110021003100410051006100710081009101010111012101310141015101610171018101910201021102210231024102510261027102810291030103110321033103410351036103710381039104010411042104310441045104610471048104910501051105210531054105510561057105810591060106110621063106410651066106710681069107010711072107310741075107610771078107910801081108210831084108510861087108810891090109110921093109410951096109710981099110011011102110311041105110611071108110911101111111211131114111511161117111811191120112111221123112411251126112711281129113011311132113311341135113611371138113911401141114211431144114511461147114811491150115111521153115411551156115711581159116011611162116311641165116611671168116911701171117211731174117511761177117811791180118111821183118411851186118711881189119011911192119311941195119611971198119912001201120212031204120512061207120812091210121112121213121412151216121712181219122012211222122312241225122612271228122912301231123212331234123512361237123812391240124112421243124412451246124712481249125012511252125312541255125612571258125912601261126212631264126512661267126812691270127112721273127412751276127712781279128012811282128312841285128612871288128912901291129212931294129512961297129812991300130113021303130413051306130713081309131013111312131313141315131613171318131913201321132213231324132513261327132813291330133113321333133413351336133713381339134013411342134313441345134613471348134913501351135213531354135513561357135813591360136113621363136413651366136713681369137013711372137313741375137613771378137913801381138213831384138513861387138813891390139113921393139413951396139713981399140014011402140314041405140614071408140914101411141214131414141514161417141814191420142114221423142414251426142714281429143014311432143314341435143614371438143914401441144214431444144514461447144814491450145114521453145414551456145714581459146014611462146314641465146614671468146914701471147214731474147514761477147814791480148114821483148414851486148714881489149014911492149314941495149614971498149915001501150215031504150515061507150815091510151115121513151415151516151715181519152015211522152315241525152615271528152915301531153215331534153515361537153815391540154115421543154415451546154715481549155015511552155315541555155615571558155915601561156215631564156515661567156815691570157115721573157415751576157715781579158015811582158315841585158615871588158915901591159215931594159515961597159815991600160116021603160416051606160716081609161016111612161316141615161616171618161916201621162216231624162516261627162816291630163116321633163416351636163716381639164016411642164316441645164616471648164916501651165216531654165516561657165816591660166116621663166416651666166716681669167016711672167316741675167616771678167916801681168216831684168516861687168816891690169116921693169416951696169716981699170017011702170317041705170617071708170917101711171217131714171517161717171817191720172117221723172417251726172717281729173017311732173317341735173617371738173917401741174217431744174517461747174817491750175117521753175417551756175717581759176017611762176317641765176617671768176917701771177217731774177517761777177817791780178117821783178417851786178717881789179017911792179317941795179617971798179918001801180218031804180518061807180818091810181118121813181418151816181718181819182018211822182318241825182618271828182918301831183218331834183518361837183818391840184118421843184418451846184718481849185018511852185318541855185618571858185918601861186218631864186518661867186818691870187118721873187418751876187718781879188018811882188318841885188618871888188918901891189218931894189518961897189818991900190119021903190419051906190719081909191019111912191319141915191619171918191919201921192219231924192519261927192819291930193119321933193419351936193719381939194019411942194319441945194619471948194919501951195219531954195519561957195819591960196119621963196419651966196719681969197019711972197319741975197619771978197919801981198219831984198519861987198819891990199119921993199419951996199719981999200020012002200320042005200620072008200920102011201220132014201520162017201820192020202120222023202420252026202720282029203020312032203320342035203620372038203920402041204220432044204520462047204820492050205120522053205420552056205720582059206020612062206320642065206620672068206920702071207220732074207520762077207820792080208120822083208420852086208720882089209020912092209320942095209620972098209921002101210221032104210521062107210821092110211121122113211421152116211721182119212021212122212321242125212621272128212921302131213221332134213521362137213821392140214121422143214421452146214721482149215021512152215321542155215621572158215921602161216221632164216521662167216821692170217121722173217421752176217721782179218021812182218321842185218621872188218921902191219221932194219521962197219821992200220122022203220422052206220722082209221022112212221322142215221622172218221922202221222222232224222522262227222822292230223122322233223422352236223722382239224022412242224322442245224622472248224922502251225222532254225522562257225822592260226122622263226422652266226722682269227022712272227322742275227622772278227922802281228222832284228522862287228822892290229122922293229422952296229722982299230023012302230323042305230623072308230923102311231223132314231523162317231823192320232123222323232423252326232723282329233023312332233323342335233623372338233923402341234223432344234523462347234823492350235123522353235423552356235723582359236023612362236323642365236623672368236923702371237223732374237523762377237823792380238123822383238423852386238723882389239023912392239323942395239623972398239924002401
  1. from __future__ import unicode_literals
  2. import re
  3. from django import forms
  4. from django.contrib.auth.models import User
  5. from django.contrib.postgres.forms.array import SimpleArrayField
  6. from django.db.models import Count, Q
  7. from mptt.forms import TreeNodeChoiceField
  8. from timezone_field import TimeZoneFormField
  9. from extras.forms import CustomFieldForm, CustomFieldBulkEditForm, CustomFieldFilterForm
  10. from ipam.models import IPAddress, VLAN, VLANGroup
  11. from tenancy.forms import TenancyForm
  12. from tenancy.models import Tenant
  13. from utilities.forms import (
  14. APISelect, APISelectMultiple, add_blank_choice, ArrayFieldSelectMultiple, BootstrapMixin, BulkEditForm,
  15. BulkEditNullBooleanSelect, ChainedFieldsMixin, ChainedModelChoiceField, ChainedModelMultipleChoiceField,
  16. CommentField, ComponentForm, ConfirmationForm, CSVChoiceField, ExpandableNameField, FilterChoiceField,
  17. FilterTreeNodeMultipleChoiceField, FlexibleModelChoiceField, Livesearch, SelectWithDisabled, SelectWithPK,
  18. SmallTextarea, SlugField,
  19. )
  20. from virtualization.models import Cluster
  21. from .constants import (
  22. CONNECTION_STATUS_CHOICES, CONNECTION_STATUS_CONNECTED, DEVICE_STATUS_CHOICES, IFACE_FF_CHOICES, IFACE_FF_LAG,
  23. IFACE_MODE_ACCESS, IFACE_MODE_CHOICES, IFACE_MODE_TAGGED_ALL, IFACE_ORDERING_CHOICES, RACK_FACE_CHOICES,
  24. RACK_TYPE_CHOICES, RACK_WIDTH_CHOICES, RACK_WIDTH_19IN, RACK_WIDTH_23IN, SITE_STATUS_CHOICES, SUBDEVICE_ROLE_CHILD,
  25. SUBDEVICE_ROLE_PARENT, SUBDEVICE_ROLE_CHOICES,
  26. )
  27. from .formfields import MACAddressFormField
  28. from .models import (
  29. DeviceBay, DeviceBayTemplate, ConsolePort, ConsolePortTemplate, ConsoleServerPort, ConsoleServerPortTemplate,
  30. Device, DeviceRole, DeviceType, Interface, InterfaceConnection, InterfaceTemplate, Manufacturer, InventoryItem,
  31. Platform, PowerOutlet, PowerOutletTemplate, PowerPort, PowerPortTemplate, Rack, RackGroup, RackReservation,
  32. RackRole, Region, Site, VirtualChassis
  33. )
  34. DEVICE_BY_PK_RE = '{\d+\}'
  35. def get_device_by_name_or_pk(name):
  36. """
  37. Attempt to retrieve a device by either its name or primary key ('{pk}').
  38. """
  39. if re.match(DEVICE_BY_PK_RE, name):
  40. pk = name.strip('{}')
  41. device = Device.objects.get(pk=pk)
  42. else:
  43. device = Device.objects.get(name=name)
  44. return device
  45. class BulkRenameForm(forms.Form):
  46. """
  47. An extendable form to be used for renaming device components in bulk.
  48. """
  49. find = forms.CharField()
  50. replace = forms.CharField()
  51. #
  52. # Regions
  53. #
  54. class RegionForm(BootstrapMixin, forms.ModelForm):
  55. slug = SlugField()
  56. class Meta:
  57. model = Region
  58. fields = ['parent', 'name', 'slug']
  59. class RegionCSVForm(forms.ModelForm):
  60. parent = forms.ModelChoiceField(
  61. queryset=Region.objects.all(),
  62. required=False,
  63. to_field_name='name',
  64. help_text='Name of parent region',
  65. error_messages={
  66. 'invalid_choice': 'Region not found.',
  67. }
  68. )
  69. class Meta:
  70. model = Region
  71. fields = Region.csv_headers
  72. help_texts = {
  73. 'name': 'Region name',
  74. 'slug': 'URL-friendly slug',
  75. }
  76. class RegionFilterForm(BootstrapMixin, forms.Form):
  77. model = Site
  78. q = forms.CharField(required=False, label='Search')
  79. #
  80. # Sites
  81. #
  82. class SiteForm(BootstrapMixin, TenancyForm, CustomFieldForm):
  83. region = TreeNodeChoiceField(queryset=Region.objects.all(), required=False)
  84. slug = SlugField()
  85. comments = CommentField()
  86. class Meta:
  87. model = Site
  88. fields = [
  89. 'name', 'slug', 'status', 'region', 'tenant_group', 'tenant', 'facility', 'asn', 'description',
  90. 'physical_address', 'shipping_address', 'contact_name', 'contact_phone', 'contact_email', 'time_zone',
  91. 'comments',
  92. ]
  93. widgets = {
  94. 'physical_address': SmallTextarea(attrs={'rows': 3}),
  95. 'shipping_address': SmallTextarea(attrs={'rows': 3}),
  96. }
  97. help_texts = {
  98. 'name': "Full name of the site",
  99. 'facility': "Data center provider and facility (e.g. Equinix NY7)",
  100. 'asn': "BGP autonomous system number",
  101. 'physical_address': "Physical location of the building (e.g. for GPS)",
  102. 'shipping_address': "If different from the physical address"
  103. }
  104. class SiteCSVForm(forms.ModelForm):
  105. status = CSVChoiceField(
  106. choices=DEVICE_STATUS_CHOICES,
  107. required=False,
  108. help_text='Operational status'
  109. )
  110. region = forms.ModelChoiceField(
  111. queryset=Region.objects.all(),
  112. required=False,
  113. to_field_name='name',
  114. help_text='Name of assigned region',
  115. error_messages={
  116. 'invalid_choice': 'Region not found.',
  117. }
  118. )
  119. tenant = forms.ModelChoiceField(
  120. queryset=Tenant.objects.all(),
  121. required=False,
  122. to_field_name='name',
  123. help_text='Name of assigned tenant',
  124. error_messages={
  125. 'invalid_choice': 'Tenant not found.',
  126. }
  127. )
  128. class Meta:
  129. model = Site
  130. fields = Site.csv_headers
  131. help_texts = {
  132. 'name': 'Site name',
  133. 'slug': 'URL-friendly slug',
  134. 'asn': '32-bit autonomous system number',
  135. }
  136. class SiteBulkEditForm(BootstrapMixin, CustomFieldBulkEditForm):
  137. pk = forms.ModelMultipleChoiceField(queryset=Site.objects.all(), widget=forms.MultipleHiddenInput)
  138. status = forms.ChoiceField(choices=add_blank_choice(SITE_STATUS_CHOICES), required=False, initial='')
  139. region = TreeNodeChoiceField(queryset=Region.objects.all(), required=False)
  140. tenant = forms.ModelChoiceField(queryset=Tenant.objects.all(), required=False)
  141. asn = forms.IntegerField(min_value=1, max_value=4294967295, required=False, label='ASN')
  142. description = forms.CharField(max_length=100, required=False)
  143. time_zone = TimeZoneFormField(required=False)
  144. class Meta:
  145. nullable_fields = ['region', 'tenant', 'asn', 'description', 'time_zone']
  146. def site_status_choices():
  147. status_counts = {}
  148. for status in Site.objects.values('status').annotate(count=Count('status')).order_by('status'):
  149. status_counts[status['status']] = status['count']
  150. return [(s[0], '{} ({})'.format(s[1], status_counts.get(s[0], 0))) for s in SITE_STATUS_CHOICES]
  151. class SiteFilterForm(BootstrapMixin, CustomFieldFilterForm):
  152. model = Site
  153. q = forms.CharField(required=False, label='Search')
  154. status = forms.MultipleChoiceField(choices=site_status_choices, required=False)
  155. region = FilterTreeNodeMultipleChoiceField(
  156. queryset=Region.objects.annotate(filter_count=Count('sites')),
  157. to_field_name='slug',
  158. required=False,
  159. )
  160. tenant = FilterChoiceField(
  161. queryset=Tenant.objects.annotate(filter_count=Count('sites')),
  162. to_field_name='slug',
  163. null_label='-- None --'
  164. )
  165. #
  166. # Rack groups
  167. #
  168. class RackGroupForm(BootstrapMixin, forms.ModelForm):
  169. slug = SlugField()
  170. class Meta:
  171. model = RackGroup
  172. fields = ['site', 'name', 'slug']
  173. class RackGroupCSVForm(forms.ModelForm):
  174. site = forms.ModelChoiceField(
  175. queryset=Site.objects.all(),
  176. to_field_name='name',
  177. help_text='Name of parent site',
  178. error_messages={
  179. 'invalid_choice': 'Site not found.',
  180. }
  181. )
  182. class Meta:
  183. model = RackGroup
  184. fields = RackGroup.csv_headers
  185. help_texts = {
  186. 'name': 'Name of rack group',
  187. 'slug': 'URL-friendly slug',
  188. }
  189. class RackGroupFilterForm(BootstrapMixin, forms.Form):
  190. site = FilterChoiceField(queryset=Site.objects.annotate(filter_count=Count('rack_groups')), to_field_name='slug')
  191. #
  192. # Rack roles
  193. #
  194. class RackRoleForm(BootstrapMixin, forms.ModelForm):
  195. slug = SlugField()
  196. class Meta:
  197. model = RackRole
  198. fields = ['name', 'slug', 'color']
  199. class RackRoleCSVForm(forms.ModelForm):
  200. slug = SlugField()
  201. class Meta:
  202. model = RackRole
  203. fields = RackRole.csv_headers
  204. help_texts = {
  205. 'name': 'Name of rack role',
  206. 'color': 'RGB color in hexadecimal (e.g. 00ff00)'
  207. }
  208. #
  209. # Racks
  210. #
  211. class RackForm(BootstrapMixin, TenancyForm, CustomFieldForm):
  212. group = ChainedModelChoiceField(
  213. queryset=RackGroup.objects.all(),
  214. chains=(
  215. ('site', 'site'),
  216. ),
  217. required=False,
  218. widget=APISelect(
  219. api_url='/api/dcim/rack-groups/?site_id={{site}}',
  220. )
  221. )
  222. comments = CommentField()
  223. class Meta:
  224. model = Rack
  225. fields = [
  226. 'site', 'group', 'name', 'facility_id', 'tenant_group', 'tenant', 'role', 'serial', 'type', 'width',
  227. 'u_height', 'desc_units', 'comments',
  228. ]
  229. help_texts = {
  230. 'site': "The site at which the rack exists",
  231. 'name': "Organizational rack name",
  232. 'facility_id': "The unique rack ID assigned by the facility",
  233. 'u_height': "Height in rack units",
  234. }
  235. widgets = {
  236. 'site': forms.Select(attrs={'filter-for': 'group'}),
  237. }
  238. class RackCSVForm(forms.ModelForm):
  239. site = forms.ModelChoiceField(
  240. queryset=Site.objects.all(),
  241. to_field_name='name',
  242. help_text='Name of parent site',
  243. error_messages={
  244. 'invalid_choice': 'Site not found.',
  245. }
  246. )
  247. group_name = forms.CharField(
  248. help_text='Name of rack group',
  249. required=False
  250. )
  251. tenant = forms.ModelChoiceField(
  252. queryset=Tenant.objects.all(),
  253. required=False,
  254. to_field_name='name',
  255. help_text='Name of assigned tenant',
  256. error_messages={
  257. 'invalid_choice': 'Tenant not found.',
  258. }
  259. )
  260. role = forms.ModelChoiceField(
  261. queryset=RackRole.objects.all(),
  262. required=False,
  263. to_field_name='name',
  264. help_text='Name of assigned role',
  265. error_messages={
  266. 'invalid_choice': 'Role not found.',
  267. }
  268. )
  269. type = CSVChoiceField(
  270. choices=RACK_TYPE_CHOICES,
  271. required=False,
  272. help_text='Rack type'
  273. )
  274. width = forms.ChoiceField(
  275. choices=(
  276. (RACK_WIDTH_19IN, '19'),
  277. (RACK_WIDTH_23IN, '23'),
  278. ),
  279. help_text='Rail-to-rail width (in inches)'
  280. )
  281. class Meta:
  282. model = Rack
  283. fields = Rack.csv_headers
  284. help_texts = {
  285. 'name': 'Rack name',
  286. 'u_height': 'Height in rack units',
  287. }
  288. def clean(self):
  289. super(RackCSVForm, self).clean()
  290. site = self.cleaned_data.get('site')
  291. group_name = self.cleaned_data.get('group_name')
  292. # Validate rack group
  293. if group_name:
  294. try:
  295. self.instance.group = RackGroup.objects.get(site=site, name=group_name)
  296. except RackGroup.DoesNotExist:
  297. raise forms.ValidationError("Rack group {} not found for site {}".format(group_name, site))
  298. class RackBulkEditForm(BootstrapMixin, CustomFieldBulkEditForm):
  299. pk = forms.ModelMultipleChoiceField(queryset=Rack.objects.all(), widget=forms.MultipleHiddenInput)
  300. site = forms.ModelChoiceField(queryset=Site.objects.all(), required=False, label='Site')
  301. group = forms.ModelChoiceField(queryset=RackGroup.objects.all(), required=False, label='Group')
  302. tenant = forms.ModelChoiceField(queryset=Tenant.objects.all(), required=False)
  303. role = forms.ModelChoiceField(queryset=RackRole.objects.all(), required=False)
  304. serial = forms.CharField(max_length=50, required=False, label='Serial Number')
  305. type = forms.ChoiceField(choices=add_blank_choice(RACK_TYPE_CHOICES), required=False, label='Type')
  306. width = forms.ChoiceField(choices=add_blank_choice(RACK_WIDTH_CHOICES), required=False, label='Width')
  307. u_height = forms.IntegerField(required=False, label='Height (U)')
  308. desc_units = forms.NullBooleanField(required=False, widget=BulkEditNullBooleanSelect, label='Descending units')
  309. comments = CommentField(widget=SmallTextarea)
  310. class Meta:
  311. nullable_fields = ['group', 'tenant', 'role', 'serial', 'comments']
  312. class RackFilterForm(BootstrapMixin, CustomFieldFilterForm):
  313. model = Rack
  314. q = forms.CharField(required=False, label='Search')
  315. site = FilterChoiceField(
  316. queryset=Site.objects.annotate(filter_count=Count('racks')),
  317. to_field_name='slug'
  318. )
  319. group_id = FilterChoiceField(
  320. queryset=RackGroup.objects.select_related('site').annotate(filter_count=Count('racks')),
  321. label='Rack group',
  322. null_label='-- None --'
  323. )
  324. tenant = FilterChoiceField(
  325. queryset=Tenant.objects.annotate(filter_count=Count('racks')),
  326. to_field_name='slug',
  327. null_label='-- None --'
  328. )
  329. role = FilterChoiceField(
  330. queryset=RackRole.objects.annotate(filter_count=Count('racks')),
  331. to_field_name='slug',
  332. null_label='-- None --'
  333. )
  334. #
  335. # Rack reservations
  336. #
  337. class RackReservationForm(BootstrapMixin, TenancyForm, forms.ModelForm):
  338. units = SimpleArrayField(forms.IntegerField(), widget=ArrayFieldSelectMultiple(attrs={'size': 10}))
  339. user = forms.ModelChoiceField(queryset=User.objects.order_by('username'))
  340. class Meta:
  341. model = RackReservation
  342. fields = ['units', 'user', 'tenant_group', 'tenant', 'description']
  343. def __init__(self, *args, **kwargs):
  344. super(RackReservationForm, self).__init__(*args, **kwargs)
  345. # Populate rack unit choices
  346. self.fields['units'].widget.choices = self._get_unit_choices()
  347. def _get_unit_choices(self):
  348. rack = self.instance.rack
  349. reserved_units = []
  350. for resv in rack.reservations.exclude(pk=self.instance.pk):
  351. for u in resv.units:
  352. reserved_units.append(u)
  353. unit_choices = [(u, {'label': str(u), 'disabled': u in reserved_units}) for u in rack.units]
  354. return unit_choices
  355. class RackReservationFilterForm(BootstrapMixin, forms.Form):
  356. q = forms.CharField(required=False, label='Search')
  357. site = FilterChoiceField(
  358. queryset=Site.objects.annotate(filter_count=Count('racks__reservations')),
  359. to_field_name='slug'
  360. )
  361. group_id = FilterChoiceField(
  362. queryset=RackGroup.objects.select_related('site').annotate(filter_count=Count('racks__reservations')),
  363. label='Rack group',
  364. null_label='-- None --'
  365. )
  366. tenant = FilterChoiceField(
  367. queryset=Tenant.objects.annotate(filter_count=Count('rackreservations')),
  368. to_field_name='slug',
  369. null_label='-- None --'
  370. )
  371. class RackReservationBulkEditForm(BootstrapMixin, BulkEditForm):
  372. pk = forms.ModelMultipleChoiceField(queryset=RackReservation.objects.all(), widget=forms.MultipleHiddenInput)
  373. user = forms.ModelChoiceField(queryset=User.objects.order_by('username'), required=False)
  374. tenant = forms.ModelChoiceField(queryset=Tenant.objects.all(), required=False)
  375. description = forms.CharField(max_length=100, required=False)
  376. class Meta:
  377. nullable_fields = []
  378. #
  379. # Manufacturers
  380. #
  381. class ManufacturerForm(BootstrapMixin, forms.ModelForm):
  382. slug = SlugField()
  383. class Meta:
  384. model = Manufacturer
  385. fields = ['name', 'slug']
  386. class ManufacturerCSVForm(forms.ModelForm):
  387. class Meta:
  388. model = Manufacturer
  389. fields = Manufacturer.csv_headers
  390. help_texts = {
  391. 'name': 'Manufacturer name',
  392. 'slug': 'URL-friendly slug',
  393. }
  394. #
  395. # Device types
  396. #
  397. class DeviceTypeForm(BootstrapMixin, CustomFieldForm):
  398. slug = SlugField(slug_source='model')
  399. class Meta:
  400. model = DeviceType
  401. fields = ['manufacturer', 'model', 'slug', 'part_number', 'u_height', 'is_full_depth', 'is_console_server',
  402. 'is_pdu', 'is_network_device', 'subdevice_role', 'interface_ordering', 'comments']
  403. labels = {
  404. 'interface_ordering': 'Order interfaces by',
  405. }
  406. class DeviceTypeCSVForm(forms.ModelForm):
  407. manufacturer = forms.ModelChoiceField(
  408. queryset=Manufacturer.objects.all(),
  409. required=True,
  410. to_field_name='name',
  411. help_text='Manufacturer name',
  412. error_messages={
  413. 'invalid_choice': 'Manufacturer not found.',
  414. }
  415. )
  416. subdevice_role = CSVChoiceField(
  417. choices=SUBDEVICE_ROLE_CHOICES,
  418. required=False,
  419. help_text='Parent/child status'
  420. )
  421. interface_ordering = CSVChoiceField(
  422. choices=IFACE_ORDERING_CHOICES,
  423. required=False,
  424. help_text='Interface ordering'
  425. )
  426. class Meta:
  427. model = DeviceType
  428. fields = DeviceType.csv_headers
  429. help_texts = {
  430. 'model': 'Model name',
  431. 'slug': 'URL-friendly slug',
  432. }
  433. class DeviceTypeBulkEditForm(BootstrapMixin, CustomFieldBulkEditForm):
  434. pk = forms.ModelMultipleChoiceField(queryset=DeviceType.objects.all(), widget=forms.MultipleHiddenInput)
  435. manufacturer = forms.ModelChoiceField(queryset=Manufacturer.objects.all(), required=False)
  436. u_height = forms.IntegerField(min_value=1, required=False)
  437. is_full_depth = forms.NullBooleanField(required=False, widget=BulkEditNullBooleanSelect, label='Is full depth')
  438. interface_ordering = forms.ChoiceField(choices=add_blank_choice(IFACE_ORDERING_CHOICES), required=False)
  439. is_console_server = forms.NullBooleanField(
  440. required=False, widget=BulkEditNullBooleanSelect, label='Is a console server'
  441. )
  442. is_pdu = forms.NullBooleanField(required=False, widget=BulkEditNullBooleanSelect, label='Is a PDU')
  443. is_network_device = forms.NullBooleanField(
  444. required=False, widget=BulkEditNullBooleanSelect, label='Is a network device'
  445. )
  446. class Meta:
  447. nullable_fields = []
  448. class DeviceTypeFilterForm(BootstrapMixin, CustomFieldFilterForm):
  449. model = DeviceType
  450. q = forms.CharField(required=False, label='Search')
  451. manufacturer = FilterChoiceField(
  452. queryset=Manufacturer.objects.annotate(filter_count=Count('device_types')),
  453. to_field_name='slug'
  454. )
  455. is_console_server = forms.BooleanField(
  456. required=False, label='Is a console server', widget=forms.CheckboxInput(attrs={'value': 'True'}))
  457. is_pdu = forms.BooleanField(
  458. required=False, label='Is a PDU', widget=forms.CheckboxInput(attrs={'value': 'True'})
  459. )
  460. is_network_device = forms.BooleanField(
  461. required=False, label='Is a network device', widget=forms.CheckboxInput(attrs={'value': 'True'})
  462. )
  463. subdevice_role = forms.NullBooleanField(
  464. required=False, label='Subdevice role', widget=forms.Select(choices=(
  465. ('', '---------'),
  466. (SUBDEVICE_ROLE_PARENT, 'Parent'),
  467. (SUBDEVICE_ROLE_CHILD, 'Child'),
  468. ))
  469. )
  470. #
  471. # Device component templates
  472. #
  473. class ConsolePortTemplateForm(BootstrapMixin, forms.ModelForm):
  474. class Meta:
  475. model = ConsolePortTemplate
  476. fields = ['device_type', 'name']
  477. widgets = {
  478. 'device_type': forms.HiddenInput(),
  479. }
  480. class ConsolePortTemplateCreateForm(ComponentForm):
  481. name_pattern = ExpandableNameField(label='Name')
  482. class ConsoleServerPortTemplateForm(BootstrapMixin, forms.ModelForm):
  483. class Meta:
  484. model = ConsoleServerPortTemplate
  485. fields = ['device_type', 'name']
  486. widgets = {
  487. 'device_type': forms.HiddenInput(),
  488. }
  489. class ConsoleServerPortTemplateCreateForm(ComponentForm):
  490. name_pattern = ExpandableNameField(label='Name')
  491. class PowerPortTemplateForm(BootstrapMixin, forms.ModelForm):
  492. class Meta:
  493. model = PowerPortTemplate
  494. fields = ['device_type', 'name']
  495. widgets = {
  496. 'device_type': forms.HiddenInput(),
  497. }
  498. class PowerPortTemplateCreateForm(ComponentForm):
  499. name_pattern = ExpandableNameField(label='Name')
  500. class PowerOutletTemplateForm(BootstrapMixin, forms.ModelForm):
  501. class Meta:
  502. model = PowerOutletTemplate
  503. fields = ['device_type', 'name']
  504. widgets = {
  505. 'device_type': forms.HiddenInput(),
  506. }
  507. class PowerOutletTemplateCreateForm(ComponentForm):
  508. name_pattern = ExpandableNameField(label='Name')
  509. class InterfaceTemplateForm(BootstrapMixin, forms.ModelForm):
  510. class Meta:
  511. model = InterfaceTemplate
  512. fields = ['device_type', 'name', 'form_factor', 'mgmt_only']
  513. widgets = {
  514. 'device_type': forms.HiddenInput(),
  515. }
  516. class InterfaceTemplateCreateForm(ComponentForm):
  517. name_pattern = ExpandableNameField(label='Name')
  518. form_factor = forms.ChoiceField(choices=IFACE_FF_CHOICES)
  519. mgmt_only = forms.BooleanField(required=False, label='OOB Management')
  520. class InterfaceTemplateBulkEditForm(BootstrapMixin, BulkEditForm):
  521. pk = forms.ModelMultipleChoiceField(queryset=InterfaceTemplate.objects.all(), widget=forms.MultipleHiddenInput)
  522. form_factor = forms.ChoiceField(choices=add_blank_choice(IFACE_FF_CHOICES), required=False)
  523. mgmt_only = forms.NullBooleanField(required=False, widget=BulkEditNullBooleanSelect, label='Management only')
  524. class Meta:
  525. nullable_fields = []
  526. class DeviceBayTemplateForm(BootstrapMixin, forms.ModelForm):
  527. class Meta:
  528. model = DeviceBayTemplate
  529. fields = ['device_type', 'name']
  530. widgets = {
  531. 'device_type': forms.HiddenInput(),
  532. }
  533. class DeviceBayTemplateCreateForm(ComponentForm):
  534. name_pattern = ExpandableNameField(label='Name')
  535. #
  536. # Device roles
  537. #
  538. class DeviceRoleForm(BootstrapMixin, forms.ModelForm):
  539. slug = SlugField()
  540. class Meta:
  541. model = DeviceRole
  542. fields = ['name', 'slug', 'color', 'vm_role']
  543. class DeviceRoleCSVForm(forms.ModelForm):
  544. slug = SlugField()
  545. class Meta:
  546. model = DeviceRole
  547. fields = DeviceRole.csv_headers
  548. help_texts = {
  549. 'name': 'Name of device role',
  550. 'color': 'RGB color in hexadecimal (e.g. 00ff00)'
  551. }
  552. #
  553. # Platforms
  554. #
  555. class PlatformForm(BootstrapMixin, forms.ModelForm):
  556. slug = SlugField()
  557. class Meta:
  558. model = Platform
  559. fields = ['name', 'slug', 'manufacturer', 'napalm_driver', 'rpc_client']
  560. class PlatformCSVForm(forms.ModelForm):
  561. slug = SlugField()
  562. class Meta:
  563. model = Platform
  564. fields = Platform.csv_headers
  565. help_texts = {
  566. 'name': 'Platform name',
  567. 'manufacturer': 'Manufacturer name',
  568. }
  569. #
  570. # Devices
  571. #
  572. class DeviceForm(BootstrapMixin, TenancyForm, CustomFieldForm):
  573. site = forms.ModelChoiceField(
  574. queryset=Site.objects.all(),
  575. widget=forms.Select(
  576. attrs={'filter-for': 'rack'}
  577. )
  578. )
  579. rack = ChainedModelChoiceField(
  580. queryset=Rack.objects.all(),
  581. chains=(
  582. ('site', 'site'),
  583. ),
  584. required=False,
  585. widget=APISelect(
  586. api_url='/api/dcim/racks/?site_id={{site}}',
  587. display_field='display_name',
  588. attrs={'filter-for': 'position'}
  589. )
  590. )
  591. position = forms.TypedChoiceField(
  592. required=False,
  593. empty_value=None,
  594. help_text="The lowest-numbered unit occupied by the device",
  595. widget=APISelect(
  596. api_url='/api/dcim/racks/{{rack}}/units/?face={{face}}',
  597. disabled_indicator='device'
  598. )
  599. )
  600. manufacturer = forms.ModelChoiceField(
  601. queryset=Manufacturer.objects.all(),
  602. widget=forms.Select(
  603. attrs={'filter-for': 'device_type'}
  604. )
  605. )
  606. device_type = ChainedModelChoiceField(
  607. queryset=DeviceType.objects.all(),
  608. chains=(
  609. ('manufacturer', 'manufacturer'),
  610. ),
  611. label='Device type',
  612. widget=APISelect(
  613. api_url='/api/dcim/device-types/?manufacturer_id={{manufacturer}}',
  614. display_field='model'
  615. )
  616. )
  617. comments = CommentField()
  618. class Meta:
  619. model = Device
  620. fields = [
  621. 'name', 'device_role', 'device_type', 'serial', 'asset_tag', 'site', 'rack', 'position', 'face', 'status',
  622. 'platform', 'primary_ip4', 'primary_ip6', 'tenant_group', 'tenant', 'comments',
  623. ]
  624. help_texts = {
  625. 'device_role': "The function this device serves",
  626. 'serial': "Chassis serial number",
  627. }
  628. widgets = {
  629. 'face': forms.Select(attrs={'filter-for': 'position'}),
  630. }
  631. def __init__(self, *args, **kwargs):
  632. # Initialize helper selectors
  633. instance = kwargs.get('instance')
  634. # Using hasattr() instead of "is not None" to avoid RelatedObjectDoesNotExist on required field
  635. if instance and hasattr(instance, 'device_type'):
  636. initial = kwargs.get('initial', {}).copy()
  637. initial['manufacturer'] = instance.device_type.manufacturer
  638. kwargs['initial'] = initial
  639. super(DeviceForm, self).__init__(*args, **kwargs)
  640. if self.instance.pk:
  641. # Compile list of choices for primary IPv4 and IPv6 addresses
  642. for family in [4, 6]:
  643. ip_choices = [(None, '---------')]
  644. # Gather PKs of all interfaces belonging to this Device or a peer VirtualChassis member
  645. interface_ids = self.instance.vc_interfaces.values('pk')
  646. # Collect interface IPs
  647. interface_ips = IPAddress.objects.select_related('interface').filter(
  648. family=family, interface_id__in=interface_ids
  649. )
  650. if interface_ips:
  651. ip_list = [(ip.id, '{} ({})'.format(ip.address, ip.interface)) for ip in interface_ips]
  652. ip_choices.append(('Interface IPs', ip_list))
  653. # Collect NAT IPs
  654. nat_ips = IPAddress.objects.select_related('nat_inside').filter(
  655. family=family, nat_inside__interface__in=interface_ids
  656. )
  657. if nat_ips:
  658. ip_list = [(ip.id, '{} ({})'.format(ip.address, ip.nat_inside.address)) for ip in nat_ips]
  659. ip_choices.append(('NAT IPs', ip_list))
  660. self.fields['primary_ip{}'.format(family)].choices = ip_choices
  661. # If editing an existing device, exclude it from the list of occupied rack units. This ensures that a device
  662. # can be flipped from one face to another.
  663. self.fields['position'].widget.attrs['api-url'] += '&exclude={}'.format(self.instance.pk)
  664. # Limit platform by manufacturer
  665. self.fields['platform'].queryset = Platform.objects.filter(
  666. Q(manufacturer__isnull=True) | Q(manufacturer=self.instance.device_type.manufacturer)
  667. )
  668. else:
  669. # An object that doesn't exist yet can't have any IPs assigned to it
  670. self.fields['primary_ip4'].choices = []
  671. self.fields['primary_ip4'].widget.attrs['readonly'] = True
  672. self.fields['primary_ip6'].choices = []
  673. self.fields['primary_ip6'].widget.attrs['readonly'] = True
  674. # Rack position
  675. pk = self.instance.pk if self.instance.pk else None
  676. try:
  677. if self.is_bound and self.data.get('rack') and str(self.data.get('face')):
  678. position_choices = Rack.objects.get(pk=self.data['rack']) \
  679. .get_rack_units(face=self.data.get('face'), exclude=pk)
  680. elif self.initial.get('rack') and str(self.initial.get('face')):
  681. position_choices = Rack.objects.get(pk=self.initial['rack']) \
  682. .get_rack_units(face=self.initial.get('face'), exclude=pk)
  683. else:
  684. position_choices = []
  685. except Rack.DoesNotExist:
  686. position_choices = []
  687. self.fields['position'].choices = [('', '---------')] + [
  688. (p['id'], {
  689. 'label': p['name'],
  690. 'disabled': bool(p['device'] and p['id'] != self.initial.get('position')),
  691. }) for p in position_choices
  692. ]
  693. # Disable rack assignment if this is a child device installed in a parent device
  694. if pk and self.instance.device_type.is_child_device and hasattr(self.instance, 'parent_bay'):
  695. self.fields['site'].disabled = True
  696. self.fields['rack'].disabled = True
  697. self.initial['site'] = self.instance.parent_bay.device.site_id
  698. self.initial['rack'] = self.instance.parent_bay.device.rack_id
  699. class BaseDeviceCSVForm(forms.ModelForm):
  700. device_role = forms.ModelChoiceField(
  701. queryset=DeviceRole.objects.all(),
  702. to_field_name='name',
  703. help_text='Name of assigned role',
  704. error_messages={
  705. 'invalid_choice': 'Invalid device role.',
  706. }
  707. )
  708. tenant = forms.ModelChoiceField(
  709. queryset=Tenant.objects.all(),
  710. required=False,
  711. to_field_name='name',
  712. help_text='Name of assigned tenant',
  713. error_messages={
  714. 'invalid_choice': 'Tenant not found.',
  715. }
  716. )
  717. manufacturer = forms.ModelChoiceField(
  718. queryset=Manufacturer.objects.all(),
  719. to_field_name='name',
  720. help_text='Device type manufacturer',
  721. error_messages={
  722. 'invalid_choice': 'Invalid manufacturer.',
  723. }
  724. )
  725. model_name = forms.CharField(
  726. help_text='Device type model name'
  727. )
  728. platform = forms.ModelChoiceField(
  729. queryset=Platform.objects.all(),
  730. required=False,
  731. to_field_name='name',
  732. help_text='Name of assigned platform',
  733. error_messages={
  734. 'invalid_choice': 'Invalid platform.',
  735. }
  736. )
  737. status = CSVChoiceField(
  738. choices=DEVICE_STATUS_CHOICES,
  739. help_text='Operational status'
  740. )
  741. class Meta:
  742. fields = []
  743. model = Device
  744. help_texts = {
  745. 'name': 'Device name',
  746. }
  747. def clean(self):
  748. super(BaseDeviceCSVForm, self).clean()
  749. manufacturer = self.cleaned_data.get('manufacturer')
  750. model_name = self.cleaned_data.get('model_name')
  751. # Validate device type
  752. if manufacturer and model_name:
  753. try:
  754. self.instance.device_type = DeviceType.objects.get(manufacturer=manufacturer, model=model_name)
  755. except DeviceType.DoesNotExist:
  756. raise forms.ValidationError("Device type {} {} not found".format(manufacturer, model_name))
  757. class DeviceCSVForm(BaseDeviceCSVForm):
  758. site = forms.ModelChoiceField(
  759. queryset=Site.objects.all(),
  760. to_field_name='name',
  761. help_text='Name of parent site',
  762. error_messages={
  763. 'invalid_choice': 'Invalid site name.',
  764. }
  765. )
  766. rack_group = forms.CharField(
  767. required=False,
  768. help_text='Parent rack\'s group (if any)'
  769. )
  770. rack_name = forms.CharField(
  771. required=False,
  772. help_text='Name of parent rack'
  773. )
  774. face = CSVChoiceField(
  775. choices=RACK_FACE_CHOICES,
  776. required=False,
  777. help_text='Mounted rack face'
  778. )
  779. cluster = forms.ModelChoiceField(
  780. queryset=Cluster.objects.all(),
  781. to_field_name='name',
  782. required=False,
  783. help_text='Virtualization cluster',
  784. error_messages={
  785. 'invalid_choice': 'Invalid cluster name.',
  786. }
  787. )
  788. class Meta(BaseDeviceCSVForm.Meta):
  789. fields = [
  790. 'name', 'device_role', 'tenant', 'manufacturer', 'model_name', 'platform', 'serial', 'asset_tag', 'status',
  791. 'site', 'rack_group', 'rack_name', 'position', 'face', 'cluster', 'comments',
  792. ]
  793. def clean(self):
  794. super(DeviceCSVForm, self).clean()
  795. site = self.cleaned_data.get('site')
  796. rack_group = self.cleaned_data.get('rack_group')
  797. rack_name = self.cleaned_data.get('rack_name')
  798. # Validate rack
  799. if site and rack_group and rack_name:
  800. try:
  801. self.instance.rack = Rack.objects.get(site=site, group__name=rack_group, name=rack_name)
  802. except Rack.DoesNotExist:
  803. raise forms.ValidationError("Rack {} not found in site {} group {}".format(rack_name, site, rack_group))
  804. elif site and rack_name:
  805. try:
  806. self.instance.rack = Rack.objects.get(site=site, group__isnull=True, name=rack_name)
  807. except Rack.DoesNotExist:
  808. raise forms.ValidationError("Rack {} not found in site {} (no group)".format(rack_name, site))
  809. class ChildDeviceCSVForm(BaseDeviceCSVForm):
  810. parent = FlexibleModelChoiceField(
  811. queryset=Device.objects.all(),
  812. to_field_name='name',
  813. help_text='Name or ID of parent device',
  814. error_messages={
  815. 'invalid_choice': 'Parent device not found.',
  816. }
  817. )
  818. device_bay_name = forms.CharField(
  819. help_text='Name of device bay',
  820. )
  821. cluster = forms.ModelChoiceField(
  822. queryset=Cluster.objects.all(),
  823. to_field_name='name',
  824. required=False,
  825. help_text='Virtualization cluster',
  826. error_messages={
  827. 'invalid_choice': 'Invalid cluster name.',
  828. }
  829. )
  830. class Meta(BaseDeviceCSVForm.Meta):
  831. fields = [
  832. 'name', 'device_role', 'tenant', 'manufacturer', 'model_name', 'platform', 'serial', 'asset_tag', 'status',
  833. 'parent', 'device_bay_name', 'cluster', 'comments',
  834. ]
  835. def clean(self):
  836. super(ChildDeviceCSVForm, self).clean()
  837. parent = self.cleaned_data.get('parent')
  838. device_bay_name = self.cleaned_data.get('device_bay_name')
  839. # Validate device bay
  840. if parent and device_bay_name:
  841. try:
  842. self.instance.parent_bay = DeviceBay.objects.get(device=parent, name=device_bay_name)
  843. # Inherit site and rack from parent device
  844. self.instance.site = parent.site
  845. self.instance.rack = parent.rack
  846. except DeviceBay.DoesNotExist:
  847. raise forms.ValidationError("Parent device/bay ({} {}) not found".format(parent, device_bay_name))
  848. class DeviceBulkEditForm(BootstrapMixin, CustomFieldBulkEditForm):
  849. pk = forms.ModelMultipleChoiceField(queryset=Device.objects.all(), widget=forms.MultipleHiddenInput)
  850. device_type = forms.ModelChoiceField(queryset=DeviceType.objects.all(), required=False, label='Type')
  851. device_role = forms.ModelChoiceField(queryset=DeviceRole.objects.all(), required=False, label='Role')
  852. tenant = forms.ModelChoiceField(queryset=Tenant.objects.all(), required=False)
  853. platform = forms.ModelChoiceField(queryset=Platform.objects.all(), required=False)
  854. status = forms.ChoiceField(choices=add_blank_choice(DEVICE_STATUS_CHOICES), required=False, initial='')
  855. serial = forms.CharField(max_length=50, required=False, label='Serial Number')
  856. class Meta:
  857. nullable_fields = ['tenant', 'platform', 'serial']
  858. def device_status_choices():
  859. status_counts = {}
  860. for status in Device.objects.values('status').annotate(count=Count('status')).order_by('status'):
  861. status_counts[status['status']] = status['count']
  862. return [(s[0], '{} ({})'.format(s[1], status_counts.get(s[0], 0))) for s in DEVICE_STATUS_CHOICES]
  863. class DeviceFilterForm(BootstrapMixin, CustomFieldFilterForm):
  864. model = Device
  865. q = forms.CharField(required=False, label='Search')
  866. site = FilterChoiceField(
  867. queryset=Site.objects.annotate(filter_count=Count('devices')),
  868. to_field_name='slug',
  869. )
  870. rack_group_id = FilterChoiceField(
  871. queryset=RackGroup.objects.select_related('site').annotate(filter_count=Count('racks__devices')),
  872. label='Rack group',
  873. )
  874. rack_id = FilterChoiceField(
  875. queryset=Rack.objects.annotate(filter_count=Count('devices')),
  876. label='Rack',
  877. null_label='-- None --',
  878. )
  879. role = FilterChoiceField(
  880. queryset=DeviceRole.objects.annotate(filter_count=Count('devices')),
  881. to_field_name='slug',
  882. )
  883. tenant = FilterChoiceField(
  884. queryset=Tenant.objects.annotate(filter_count=Count('devices')),
  885. to_field_name='slug',
  886. null_label='-- None --',
  887. )
  888. manufacturer_id = FilterChoiceField(queryset=Manufacturer.objects.all(), label='Manufacturer')
  889. device_type_id = FilterChoiceField(
  890. queryset=DeviceType.objects.select_related('manufacturer').order_by('model').annotate(
  891. filter_count=Count('instances'),
  892. ),
  893. label='Model',
  894. )
  895. platform = FilterChoiceField(
  896. queryset=Platform.objects.annotate(filter_count=Count('devices')),
  897. to_field_name='slug',
  898. null_label='-- None --',
  899. )
  900. status = forms.MultipleChoiceField(choices=device_status_choices, required=False)
  901. mac_address = forms.CharField(required=False, label='MAC address')
  902. has_primary_ip = forms.NullBooleanField(
  903. required=False,
  904. label='Has a primary IP',
  905. widget=forms.Select(choices=[
  906. ('', '---------'),
  907. ('True', 'Yes'),
  908. ('False', 'No'),
  909. ])
  910. )
  911. #
  912. # Bulk device component creation
  913. #
  914. class DeviceBulkAddComponentForm(BootstrapMixin, forms.Form):
  915. pk = forms.ModelMultipleChoiceField(queryset=Device.objects.all(), widget=forms.MultipleHiddenInput)
  916. name_pattern = ExpandableNameField(label='Name')
  917. class DeviceBulkAddInterfaceForm(DeviceBulkAddComponentForm):
  918. form_factor = forms.ChoiceField(choices=IFACE_FF_CHOICES)
  919. enabled = forms.BooleanField(required=False, initial=True)
  920. mtu = forms.IntegerField(required=False, min_value=1, max_value=32767, label='MTU')
  921. mgmt_only = forms.BooleanField(required=False, label='OOB Management')
  922. description = forms.CharField(max_length=100, required=False)
  923. #
  924. # Console ports
  925. #
  926. class ConsolePortForm(BootstrapMixin, forms.ModelForm):
  927. class Meta:
  928. model = ConsolePort
  929. fields = ['device', 'name']
  930. widgets = {
  931. 'device': forms.HiddenInput(),
  932. }
  933. class ConsolePortCreateForm(ComponentForm):
  934. name_pattern = ExpandableNameField(label='Name')
  935. class ConsoleConnectionCSVForm(forms.ModelForm):
  936. console_server = FlexibleModelChoiceField(
  937. queryset=Device.objects.filter(device_type__is_console_server=True),
  938. to_field_name='name',
  939. help_text='Console server name or ID',
  940. error_messages={
  941. 'invalid_choice': 'Console server not found',
  942. }
  943. )
  944. cs_port = forms.CharField(
  945. help_text='Console server port name'
  946. )
  947. device = FlexibleModelChoiceField(
  948. queryset=Device.objects.all(),
  949. to_field_name='name',
  950. help_text='Device name or ID',
  951. error_messages={
  952. 'invalid_choice': 'Device not found',
  953. }
  954. )
  955. console_port = forms.CharField(
  956. help_text='Console port name'
  957. )
  958. connection_status = CSVChoiceField(
  959. choices=CONNECTION_STATUS_CHOICES,
  960. help_text='Connection status'
  961. )
  962. class Meta:
  963. model = ConsolePort
  964. fields = ['console_server', 'cs_port', 'device', 'console_port', 'connection_status']
  965. def clean_console_port(self):
  966. console_port_name = self.cleaned_data.get('console_port')
  967. if not self.cleaned_data.get('device') or not console_port_name:
  968. return None
  969. try:
  970. # Retrieve console port by name
  971. consoleport = ConsolePort.objects.get(
  972. device=self.cleaned_data['device'], name=console_port_name
  973. )
  974. # Check if the console port is already connected
  975. if consoleport.cs_port is not None:
  976. raise forms.ValidationError("{} {} is already connected".format(
  977. self.cleaned_data['device'], console_port_name
  978. ))
  979. except ConsolePort.DoesNotExist:
  980. raise forms.ValidationError("Invalid console port ({} {})".format(
  981. self.cleaned_data['device'], console_port_name
  982. ))
  983. self.instance = consoleport
  984. return consoleport
  985. def clean_cs_port(self):
  986. cs_port_name = self.cleaned_data.get('cs_port')
  987. if not self.cleaned_data.get('console_server') or not cs_port_name:
  988. return None
  989. try:
  990. # Retrieve console server port by name
  991. cs_port = ConsoleServerPort.objects.get(
  992. device=self.cleaned_data['console_server'], name=cs_port_name
  993. )
  994. # Check if the console server port is already connected
  995. if ConsolePort.objects.filter(cs_port=cs_port).count():
  996. raise forms.ValidationError("{} {} is already connected".format(
  997. self.cleaned_data['console_server'], cs_port_name
  998. ))
  999. except ConsoleServerPort.DoesNotExist:
  1000. raise forms.ValidationError("Invalid console server port ({} {})".format(
  1001. self.cleaned_data['console_server'], cs_port_name
  1002. ))
  1003. return cs_port
  1004. class ConsolePortConnectionForm(BootstrapMixin, ChainedFieldsMixin, forms.ModelForm):
  1005. site = forms.ModelChoiceField(
  1006. queryset=Site.objects.all(),
  1007. required=False,
  1008. widget=forms.Select(
  1009. attrs={'filter-for': 'rack'}
  1010. )
  1011. )
  1012. rack = ChainedModelChoiceField(
  1013. queryset=Rack.objects.all(),
  1014. chains=(
  1015. ('site', 'site'),
  1016. ),
  1017. label='Rack',
  1018. required=False,
  1019. widget=APISelect(
  1020. api_url='/api/dcim/racks/?site_id={{site}}',
  1021. attrs={'filter-for': 'console_server', 'nullable': 'true'}
  1022. )
  1023. )
  1024. console_server = ChainedModelChoiceField(
  1025. queryset=Device.objects.filter(device_type__is_console_server=True),
  1026. chains=(
  1027. ('site', 'site'),
  1028. ('rack', 'rack'),
  1029. ),
  1030. label='Console Server',
  1031. required=False,
  1032. widget=APISelect(
  1033. api_url='/api/dcim/devices/?site_id={{site}}&rack_id={{rack}}&is_console_server=True',
  1034. display_field='display_name',
  1035. attrs={'filter-for': 'cs_port'}
  1036. )
  1037. )
  1038. livesearch = forms.CharField(
  1039. required=False,
  1040. label='Console Server',
  1041. widget=Livesearch(
  1042. query_key='q',
  1043. query_url='dcim-api:device-list',
  1044. field_to_update='console_server',
  1045. )
  1046. )
  1047. cs_port = ChainedModelChoiceField(
  1048. queryset=ConsoleServerPort.objects.all(),
  1049. chains=(
  1050. ('device', 'console_server'),
  1051. ),
  1052. label='Port',
  1053. widget=APISelect(
  1054. api_url='/api/dcim/console-server-ports/?device_id={{console_server}}',
  1055. disabled_indicator='connected_console',
  1056. )
  1057. )
  1058. class Meta:
  1059. model = ConsolePort
  1060. fields = ['site', 'rack', 'console_server', 'livesearch', 'cs_port', 'connection_status']
  1061. labels = {
  1062. 'cs_port': 'Port',
  1063. 'connection_status': 'Status',
  1064. }
  1065. def __init__(self, *args, **kwargs):
  1066. super(ConsolePortConnectionForm, self).__init__(*args, **kwargs)
  1067. if not self.instance.pk:
  1068. raise RuntimeError("ConsolePortConnectionForm must be initialized with an existing ConsolePort instance.")
  1069. #
  1070. # Console server ports
  1071. #
  1072. class ConsoleServerPortForm(BootstrapMixin, forms.ModelForm):
  1073. class Meta:
  1074. model = ConsoleServerPort
  1075. fields = ['device', 'name']
  1076. widgets = {
  1077. 'device': forms.HiddenInput(),
  1078. }
  1079. class ConsoleServerPortCreateForm(ComponentForm):
  1080. name_pattern = ExpandableNameField(label='Name')
  1081. class ConsoleServerPortConnectionForm(BootstrapMixin, ChainedFieldsMixin, forms.Form):
  1082. site = forms.ModelChoiceField(
  1083. queryset=Site.objects.all(),
  1084. required=False,
  1085. widget=forms.Select(
  1086. attrs={'filter-for': 'rack'}
  1087. )
  1088. )
  1089. rack = ChainedModelChoiceField(
  1090. queryset=Rack.objects.all(),
  1091. chains=(
  1092. ('site', 'site'),
  1093. ),
  1094. label='Rack',
  1095. required=False,
  1096. widget=APISelect(
  1097. api_url='/api/dcim/racks/?site_id={{site}}',
  1098. attrs={'filter-for': 'device', 'nullable': 'true'}
  1099. )
  1100. )
  1101. device = ChainedModelChoiceField(
  1102. queryset=Device.objects.all(),
  1103. chains=(
  1104. ('site', 'site'),
  1105. ('rack', 'rack'),
  1106. ),
  1107. label='Device',
  1108. required=False,
  1109. widget=APISelect(
  1110. api_url='/api/dcim/devices/?site_id={{site}}&rack_id={{rack}}',
  1111. display_field='display_name',
  1112. attrs={'filter-for': 'port'}
  1113. )
  1114. )
  1115. livesearch = forms.CharField(
  1116. required=False,
  1117. label='Device',
  1118. widget=Livesearch(
  1119. query_key='q',
  1120. query_url='dcim-api:device-list',
  1121. field_to_update='device'
  1122. )
  1123. )
  1124. port = ChainedModelChoiceField(
  1125. queryset=ConsolePort.objects.all(),
  1126. chains=(
  1127. ('device', 'device'),
  1128. ),
  1129. label='Port',
  1130. widget=APISelect(
  1131. api_url='/api/dcim/console-ports/?device_id={{device}}',
  1132. disabled_indicator='cs_port'
  1133. )
  1134. )
  1135. connection_status = forms.BooleanField(
  1136. required=False,
  1137. initial=CONNECTION_STATUS_CONNECTED,
  1138. label='Status',
  1139. widget=forms.Select(
  1140. choices=CONNECTION_STATUS_CHOICES
  1141. )
  1142. )
  1143. class Meta:
  1144. fields = ['site', 'rack', 'device', 'livesearch', 'port', 'connection_status']
  1145. labels = {
  1146. 'connection_status': 'Status',
  1147. }
  1148. class ConsoleServerPortBulkRenameForm(BulkRenameForm):
  1149. pk = forms.ModelMultipleChoiceField(queryset=ConsoleServerPort.objects.all(), widget=forms.MultipleHiddenInput)
  1150. class ConsoleServerPortBulkDisconnectForm(ConfirmationForm):
  1151. pk = forms.ModelMultipleChoiceField(queryset=ConsoleServerPort.objects.all(), widget=forms.MultipleHiddenInput)
  1152. #
  1153. # Power ports
  1154. #
  1155. class PowerPortForm(BootstrapMixin, forms.ModelForm):
  1156. class Meta:
  1157. model = PowerPort
  1158. fields = ['device', 'name']
  1159. widgets = {
  1160. 'device': forms.HiddenInput(),
  1161. }
  1162. class PowerPortCreateForm(ComponentForm):
  1163. name_pattern = ExpandableNameField(label='Name')
  1164. class PowerConnectionCSVForm(forms.ModelForm):
  1165. pdu = FlexibleModelChoiceField(
  1166. queryset=Device.objects.filter(device_type__is_pdu=True),
  1167. to_field_name='name',
  1168. help_text='PDU name or ID',
  1169. error_messages={
  1170. 'invalid_choice': 'PDU not found.',
  1171. }
  1172. )
  1173. power_outlet = forms.CharField(
  1174. help_text='Power outlet name'
  1175. )
  1176. device = FlexibleModelChoiceField(
  1177. queryset=Device.objects.all(),
  1178. to_field_name='name',
  1179. help_text='Device name or ID',
  1180. error_messages={
  1181. 'invalid_choice': 'Device not found',
  1182. }
  1183. )
  1184. power_port = forms.CharField(
  1185. help_text='Power port name'
  1186. )
  1187. connection_status = CSVChoiceField(
  1188. choices=CONNECTION_STATUS_CHOICES,
  1189. help_text='Connection status'
  1190. )
  1191. class Meta:
  1192. model = PowerPort
  1193. fields = ['pdu', 'power_outlet', 'device', 'power_port', 'connection_status']
  1194. def clean_power_port(self):
  1195. power_port_name = self.cleaned_data.get('power_port')
  1196. if not self.cleaned_data.get('device') or not power_port_name:
  1197. return None
  1198. try:
  1199. # Retrieve power port by name
  1200. powerport = PowerPort.objects.get(
  1201. device=self.cleaned_data['device'], name=power_port_name
  1202. )
  1203. # Check if the power port is already connected
  1204. if powerport.power_outlet is not None:
  1205. raise forms.ValidationError("{} {} is already connected".format(
  1206. self.cleaned_data['device'], power_port_name
  1207. ))
  1208. except PowerPort.DoesNotExist:
  1209. raise forms.ValidationError("Invalid power port ({} {})".format(
  1210. self.cleaned_data['device'], power_port_name
  1211. ))
  1212. self.instance = powerport
  1213. return powerport
  1214. def clean_power_outlet(self):
  1215. power_outlet_name = self.cleaned_data.get('power_outlet')
  1216. if not self.cleaned_data.get('pdu') or not power_outlet_name:
  1217. return None
  1218. try:
  1219. # Retrieve power outlet by name
  1220. power_outlet = PowerOutlet.objects.get(
  1221. device=self.cleaned_data['pdu'], name=power_outlet_name
  1222. )
  1223. # Check if the power outlet is already connected
  1224. if PowerPort.objects.filter(power_outlet=power_outlet).count():
  1225. raise forms.ValidationError("{} {} is already connected".format(
  1226. self.cleaned_data['pdu'], power_outlet_name
  1227. ))
  1228. except PowerOutlet.DoesNotExist:
  1229. raise forms.ValidationError("Invalid power outlet ({} {})".format(
  1230. self.cleaned_data['pdu'], power_outlet_name
  1231. ))
  1232. return power_outlet
  1233. class PowerPortConnectionForm(BootstrapMixin, ChainedFieldsMixin, forms.ModelForm):
  1234. site = forms.ModelChoiceField(
  1235. queryset=Site.objects.all(),
  1236. required=False,
  1237. widget=forms.Select(
  1238. attrs={'filter-for': 'rack'}
  1239. )
  1240. )
  1241. rack = ChainedModelChoiceField(
  1242. queryset=Rack.objects.all(),
  1243. chains=(
  1244. ('site', 'site'),
  1245. ),
  1246. label='Rack',
  1247. required=False,
  1248. widget=APISelect(
  1249. api_url='/api/dcim/racks/?site_id={{site}}',
  1250. attrs={'filter-for': 'pdu', 'nullable': 'true'}
  1251. )
  1252. )
  1253. pdu = ChainedModelChoiceField(
  1254. queryset=Device.objects.all(),
  1255. chains=(
  1256. ('site', 'site'),
  1257. ('rack', 'rack'),
  1258. ),
  1259. label='PDU',
  1260. required=False,
  1261. widget=APISelect(
  1262. api_url='/api/dcim/devices/?site_id={{site}}&rack_id={{rack}}&is_pdu=True',
  1263. display_field='display_name',
  1264. attrs={'filter-for': 'power_outlet'}
  1265. )
  1266. )
  1267. livesearch = forms.CharField(
  1268. required=False,
  1269. label='PDU',
  1270. widget=Livesearch(
  1271. query_key='q',
  1272. query_url='dcim-api:device-list',
  1273. field_to_update='pdu'
  1274. )
  1275. )
  1276. power_outlet = ChainedModelChoiceField(
  1277. queryset=PowerOutlet.objects.all(),
  1278. chains=(
  1279. ('device', 'pdu'),
  1280. ),
  1281. label='Outlet',
  1282. widget=APISelect(
  1283. api_url='/api/dcim/power-outlets/?device_id={{pdu}}',
  1284. disabled_indicator='connected_port'
  1285. )
  1286. )
  1287. class Meta:
  1288. model = PowerPort
  1289. fields = ['site', 'rack', 'pdu', 'livesearch', 'power_outlet', 'connection_status']
  1290. labels = {
  1291. 'power_outlet': 'Outlet',
  1292. 'connection_status': 'Status',
  1293. }
  1294. def __init__(self, *args, **kwargs):
  1295. super(PowerPortConnectionForm, self).__init__(*args, **kwargs)
  1296. if not self.instance.pk:
  1297. raise RuntimeError("PowerPortConnectionForm must be initialized with an existing PowerPort instance.")
  1298. #
  1299. # Power outlets
  1300. #
  1301. class PowerOutletForm(BootstrapMixin, forms.ModelForm):
  1302. class Meta:
  1303. model = PowerOutlet
  1304. fields = ['device', 'name']
  1305. widgets = {
  1306. 'device': forms.HiddenInput(),
  1307. }
  1308. class PowerOutletCreateForm(ComponentForm):
  1309. name_pattern = ExpandableNameField(label='Name')
  1310. class PowerOutletConnectionForm(BootstrapMixin, ChainedFieldsMixin, forms.Form):
  1311. site = forms.ModelChoiceField(
  1312. queryset=Site.objects.all(),
  1313. required=False,
  1314. widget=forms.Select(
  1315. attrs={'filter-for': 'rack'}
  1316. )
  1317. )
  1318. rack = ChainedModelChoiceField(
  1319. queryset=Rack.objects.all(),
  1320. chains=(
  1321. ('site', 'site'),
  1322. ),
  1323. label='Rack',
  1324. required=False,
  1325. widget=APISelect(
  1326. api_url='/api/dcim/racks/?site_id={{site}}',
  1327. attrs={'filter-for': 'device', 'nullable': 'true'}
  1328. )
  1329. )
  1330. device = ChainedModelChoiceField(
  1331. queryset=Device.objects.all(),
  1332. chains=(
  1333. ('site', 'site'),
  1334. ('rack', 'rack'),
  1335. ),
  1336. label='Device',
  1337. required=False,
  1338. widget=APISelect(
  1339. api_url='/api/dcim/devices/?site_id={{site}}&rack_id={{rack}}',
  1340. display_field='display_name',
  1341. attrs={'filter-for': 'port'}
  1342. )
  1343. )
  1344. livesearch = forms.CharField(
  1345. required=False,
  1346. label='Device',
  1347. widget=Livesearch(
  1348. query_key='q',
  1349. query_url='dcim-api:device-list',
  1350. field_to_update='device'
  1351. )
  1352. )
  1353. port = ChainedModelChoiceField(
  1354. queryset=PowerPort.objects.all(),
  1355. chains=(
  1356. ('device', 'device'),
  1357. ),
  1358. label='Port',
  1359. widget=APISelect(
  1360. api_url='/api/dcim/power-ports/?device_id={{device}}',
  1361. disabled_indicator='power_outlet'
  1362. )
  1363. )
  1364. connection_status = forms.BooleanField(
  1365. required=False,
  1366. initial=CONNECTION_STATUS_CONNECTED,
  1367. label='Status',
  1368. widget=forms.Select(
  1369. choices=CONNECTION_STATUS_CHOICES
  1370. )
  1371. )
  1372. class Meta:
  1373. fields = ['site', 'rack', 'device', 'livesearch', 'port', 'connection_status']
  1374. labels = {
  1375. 'connection_status': 'Status',
  1376. }
  1377. class PowerOutletBulkRenameForm(BulkRenameForm):
  1378. pk = forms.ModelMultipleChoiceField(queryset=PowerOutlet.objects.all(), widget=forms.MultipleHiddenInput)
  1379. class PowerOutletBulkDisconnectForm(ConfirmationForm):
  1380. pk = forms.ModelMultipleChoiceField(queryset=PowerOutlet.objects.all(), widget=forms.MultipleHiddenInput)
  1381. #
  1382. # Interfaces
  1383. #
  1384. class InterfaceForm(BootstrapMixin, forms.ModelForm, ChainedFieldsMixin):
  1385. site = forms.ModelChoiceField(
  1386. queryset=Site.objects.all(),
  1387. required=False,
  1388. label='VLAN site',
  1389. widget=forms.Select(
  1390. attrs={'filter-for': 'vlan_group', 'nullable': 'true'},
  1391. )
  1392. )
  1393. vlan_group = ChainedModelChoiceField(
  1394. queryset=VLANGroup.objects.all(),
  1395. chains=(
  1396. ('site', 'site'),
  1397. ),
  1398. required=False,
  1399. label='VLAN group',
  1400. widget=APISelect(
  1401. attrs={'filter-for': 'untagged_vlan tagged_vlans', 'nullable': 'true'},
  1402. api_url='/api/ipam/vlan-groups/?site_id={{site}}',
  1403. )
  1404. )
  1405. untagged_vlan = ChainedModelChoiceField(
  1406. queryset=VLAN.objects.all(),
  1407. chains=(
  1408. ('site', 'site'),
  1409. ('group', 'vlan_group'),
  1410. ),
  1411. required=False,
  1412. label='Untagged VLAN',
  1413. widget=APISelect(
  1414. api_url='/api/ipam/vlans/?site_id={{site}}&group_id={{vlan_group}}',
  1415. display_field='display_name'
  1416. )
  1417. )
  1418. tagged_vlans = ChainedModelMultipleChoiceField(
  1419. queryset=VLAN.objects.all(),
  1420. chains=(
  1421. ('site', 'site'),
  1422. ('group', 'vlan_group'),
  1423. ),
  1424. required=False,
  1425. label='Tagged VLANs',
  1426. widget=APISelectMultiple(
  1427. api_url='/api/ipam/vlans/?site_id={{site}}&group_id={{vlan_group}}',
  1428. display_field='display_name'
  1429. )
  1430. )
  1431. class Meta:
  1432. model = Interface
  1433. fields = [
  1434. 'device', 'name', 'form_factor', 'enabled', 'lag', 'mtu', 'mac_address', 'mgmt_only', 'description',
  1435. 'mode', 'site', 'vlan_group', 'untagged_vlan', 'tagged_vlans',
  1436. ]
  1437. widgets = {
  1438. 'device': forms.HiddenInput(),
  1439. }
  1440. def __init__(self, *args, **kwargs):
  1441. super(InterfaceForm, self).__init__(*args, **kwargs)
  1442. # Limit LAG choices to interfaces belonging to this device (or VC master)
  1443. if self.is_bound:
  1444. device = Device.objects.get(pk=self.data['device'])
  1445. self.fields['lag'].queryset = Interface.objects.order_naturally().filter(
  1446. device__in=[device, device.get_vc_master()], form_factor=IFACE_FF_LAG
  1447. )
  1448. else:
  1449. device = self.instance.device
  1450. self.fields['lag'].queryset = Interface.objects.order_naturally().filter(
  1451. device__in=[self.instance.device, self.instance.device.get_vc_master()], form_factor=IFACE_FF_LAG
  1452. )
  1453. # Limit the queryset for the site to only include the interface's device's site
  1454. if device and device.site:
  1455. self.fields['site'].queryset = Site.objects.filter(pk=device.site.id)
  1456. self.fields['site'].initial = None
  1457. else:
  1458. self.fields['site'].queryset = Site.objects.none()
  1459. self.fields['site'].initial = None
  1460. # Limit the initial vlan choices
  1461. if self.is_bound:
  1462. filter_dict = {
  1463. 'group_id': self.data.get('vlan_group') or None,
  1464. 'site_id': self.data.get('site') or None,
  1465. }
  1466. elif self.initial.get('untagged_vlan'):
  1467. filter_dict = {
  1468. 'group_id': self.instance.untagged_vlan.group,
  1469. 'site_id': self.instance.untagged_vlan.site,
  1470. }
  1471. elif self.initial.get('tagged_vlans'):
  1472. filter_dict = {
  1473. 'group_id': self.instance.tagged_vlans.first().group,
  1474. 'site_id': self.instance.tagged_vlans.first().site,
  1475. }
  1476. else:
  1477. filter_dict = {
  1478. 'group_id': None,
  1479. 'site_id': None,
  1480. }
  1481. self.fields['untagged_vlan'].queryset = VLAN.objects.filter(**filter_dict)
  1482. self.fields['tagged_vlans'].queryset = VLAN.objects.filter(**filter_dict)
  1483. def clean_tagged_vlans(self):
  1484. """
  1485. Because tagged_vlans is a many-to-many relationship, validation must be done in the form
  1486. """
  1487. if self.cleaned_data['mode'] == IFACE_MODE_ACCESS and self.cleaned_data['tagged_vlans']:
  1488. raise forms.ValidationError(
  1489. "An Access interface cannot have tagged VLANs."
  1490. )
  1491. if self.cleaned_data['mode'] == IFACE_MODE_TAGGED_ALL and self.cleaned_data['tagged_vlans']:
  1492. raise forms.ValidationError(
  1493. "Interface mode Tagged All implies all VLANs are tagged. "
  1494. "Do not select any tagged VLANs."
  1495. )
  1496. return self.cleaned_data['tagged_vlans']
  1497. class InterfaceCreateForm(ComponentForm, ChainedFieldsMixin):
  1498. name_pattern = ExpandableNameField(label='Name')
  1499. form_factor = forms.ChoiceField(choices=IFACE_FF_CHOICES)
  1500. enabled = forms.BooleanField(required=False)
  1501. lag = forms.ModelChoiceField(queryset=Interface.objects.all(), required=False, label='Parent LAG')
  1502. mtu = forms.IntegerField(required=False, min_value=1, max_value=32767, label='MTU')
  1503. mac_address = MACAddressFormField(required=False, label='MAC Address')
  1504. mgmt_only = forms.BooleanField(
  1505. required=False,
  1506. label='OOB Management',
  1507. help_text='This interface is used only for out-of-band management'
  1508. )
  1509. description = forms.CharField(max_length=100, required=False)
  1510. mode = forms.ChoiceField(choices=add_blank_choice(IFACE_MODE_CHOICES), required=False)
  1511. site = forms.ModelChoiceField(
  1512. queryset=Site.objects.all(),
  1513. required=False,
  1514. label='VLAN Site',
  1515. widget=forms.Select(
  1516. attrs={'filter-for': 'vlan_group', 'nullable': 'true'},
  1517. )
  1518. )
  1519. vlan_group = ChainedModelChoiceField(
  1520. queryset=VLANGroup.objects.all(),
  1521. chains=(
  1522. ('site', 'site'),
  1523. ),
  1524. required=False,
  1525. label='VLAN group',
  1526. widget=APISelect(
  1527. attrs={'filter-for': 'untagged_vlan tagged_vlans', 'nullable': 'true'},
  1528. api_url='/api/ipam/vlan-groups/?site_id={{site}}',
  1529. )
  1530. )
  1531. untagged_vlan = ChainedModelChoiceField(
  1532. queryset=VLAN.objects.all(),
  1533. chains=(
  1534. ('site', 'site'),
  1535. ('group', 'vlan_group'),
  1536. ),
  1537. required=False,
  1538. label='Untagged VLAN',
  1539. widget=APISelect(
  1540. api_url='/api/ipam/vlans/?site_id={{site}}&group_id={{vlan_group}}',
  1541. )
  1542. )
  1543. tagged_vlans = ChainedModelMultipleChoiceField(
  1544. queryset=VLAN.objects.all(),
  1545. chains=(
  1546. ('site', 'site'),
  1547. ('group', 'vlan_group'),
  1548. ),
  1549. required=False,
  1550. label='Tagged VLANs',
  1551. widget=APISelectMultiple(
  1552. api_url='/api/ipam/vlans/?site_id={{site}}&group_id={{vlan_group}}',
  1553. )
  1554. )
  1555. def __init__(self, *args, **kwargs):
  1556. # Set interfaces enabled by default
  1557. kwargs['initial'] = kwargs.get('initial', {}).copy()
  1558. kwargs['initial'].update({'enabled': True})
  1559. super(InterfaceCreateForm, self).__init__(*args, **kwargs)
  1560. # Limit LAG choices to interfaces belonging to this device (or its VC master)
  1561. if self.parent is not None:
  1562. self.fields['lag'].queryset = Interface.objects.order_naturally().filter(
  1563. device__in=[self.parent, self.parent.get_vc_master()], form_factor=IFACE_FF_LAG
  1564. )
  1565. else:
  1566. self.fields['lag'].queryset = Interface.objects.none()
  1567. # Limit the queryset for the site to only include the interface's device's site
  1568. if self.parent is not None and self.parent.site:
  1569. self.fields['site'].queryset = Site.objects.filter(pk=self.parent.site.id)
  1570. self.fields['site'].initial = None
  1571. else:
  1572. self.fields['site'].queryset = Site.objects.none()
  1573. self.fields['site'].initial = None
  1574. # Limit the initial vlan choices
  1575. if self.is_bound:
  1576. filter_dict = {
  1577. 'group_id': self.data.get('vlan_group') or None,
  1578. 'site_id': self.data.get('site') or None,
  1579. }
  1580. elif self.initial.get('untagged_vlan'):
  1581. filter_dict = {
  1582. 'group_id': self.untagged_vlan.group,
  1583. 'site_id': self.untagged_vlan.site,
  1584. }
  1585. elif self.initial.get('tagged_vlans'):
  1586. filter_dict = {
  1587. 'group_id': self.tagged_vlans.first().group,
  1588. 'site_id': self.tagged_vlans.first().site,
  1589. }
  1590. else:
  1591. filter_dict = {
  1592. 'group_id': None,
  1593. 'site_id': None,
  1594. }
  1595. self.fields['untagged_vlan'].queryset = VLAN.objects.filter(**filter_dict)
  1596. self.fields['tagged_vlans'].queryset = VLAN.objects.filter(**filter_dict)
  1597. class InterfaceBulkEditForm(BootstrapMixin, BulkEditForm, ChainedFieldsMixin):
  1598. pk = forms.ModelMultipleChoiceField(queryset=Interface.objects.all(), widget=forms.MultipleHiddenInput)
  1599. device = forms.ModelChoiceField(queryset=Device.objects.all(), widget=forms.HiddenInput)
  1600. form_factor = forms.ChoiceField(choices=add_blank_choice(IFACE_FF_CHOICES), required=False)
  1601. enabled = forms.NullBooleanField(required=False, widget=BulkEditNullBooleanSelect)
  1602. lag = forms.ModelChoiceField(queryset=Interface.objects.all(), required=False, label='Parent LAG')
  1603. mtu = forms.IntegerField(required=False, min_value=1, max_value=32767, label='MTU')
  1604. mgmt_only = forms.NullBooleanField(required=False, widget=BulkEditNullBooleanSelect, label='Management only')
  1605. description = forms.CharField(max_length=100, required=False)
  1606. mode = forms.ChoiceField(choices=add_blank_choice(IFACE_MODE_CHOICES), required=False)
  1607. site = forms.ModelChoiceField(
  1608. queryset=Site.objects.all(),
  1609. required=False,
  1610. label='VLAN Site',
  1611. widget=forms.Select(
  1612. attrs={'filter-for': 'vlan_group', 'nullable': 'true'},
  1613. )
  1614. )
  1615. vlan_group = ChainedModelChoiceField(
  1616. queryset=VLANGroup.objects.all(),
  1617. chains=(
  1618. ('site', 'site'),
  1619. ),
  1620. required=False,
  1621. label='VLAN group',
  1622. widget=APISelect(
  1623. attrs={'filter-for': 'untagged_vlan tagged_vlans', 'nullable': 'true'},
  1624. api_url='/api/ipam/vlan-groups/?site_id={{site}}',
  1625. )
  1626. )
  1627. untagged_vlan = ChainedModelChoiceField(
  1628. queryset=VLAN.objects.all(),
  1629. chains=(
  1630. ('site', 'site'),
  1631. ('group', 'vlan_group'),
  1632. ),
  1633. required=False,
  1634. label='Untagged VLAN',
  1635. widget=APISelect(
  1636. api_url='/api/ipam/vlans/?site_id={{site}}&group_id={{vlan_group}}',
  1637. )
  1638. )
  1639. tagged_vlans = ChainedModelMultipleChoiceField(
  1640. queryset=VLAN.objects.all(),
  1641. chains=(
  1642. ('site', 'site'),
  1643. ('group', 'vlan_group'),
  1644. ),
  1645. required=False,
  1646. label='Tagged VLANs',
  1647. widget=APISelectMultiple(
  1648. api_url='/api/ipam/vlans/?site_id={{site}}&group_id={{vlan_group}}',
  1649. )
  1650. )
  1651. class Meta:
  1652. nullable_fields = ['lag', 'mtu', 'description', 'untagged_vlan', 'tagged_vlans']
  1653. def __init__(self, *args, **kwargs):
  1654. super(InterfaceBulkEditForm, self).__init__(*args, **kwargs)
  1655. # Limit LAG choices to interfaces which belong to the parent device (or VC master)
  1656. device = None
  1657. if self.initial.get('device'):
  1658. try:
  1659. device = Device.objects.get(pk=self.initial.get('device'))
  1660. except Device.DoesNotExist:
  1661. pass
  1662. else:
  1663. try:
  1664. device = Device.objects.get(pk=self.data.get('device'))
  1665. except Device.DoesNotExist:
  1666. pass
  1667. if device is not None:
  1668. interface_ordering = device.device_type.interface_ordering
  1669. self.fields['lag'].queryset = Interface.objects.order_naturally(method=interface_ordering).filter(
  1670. device__in=[device, device.get_vc_master()], form_factor=IFACE_FF_LAG
  1671. )
  1672. else:
  1673. self.fields['lag'].choices = []
  1674. # Limit the queryset for the site to only include the interface's device's site
  1675. if device and device.site:
  1676. self.fields['site'].queryset = Site.objects.filter(pk=device.site.id)
  1677. self.fields['site'].initial = None
  1678. else:
  1679. self.fields['site'].queryset = Site.objects.none()
  1680. self.fields['site'].initial = None
  1681. if self.is_bound:
  1682. filter_dict = {
  1683. 'group_id': self.data.get('vlan_group') or None,
  1684. 'site_id': self.data.get('site') or None,
  1685. }
  1686. else:
  1687. filter_dict = {
  1688. 'group_id': None,
  1689. 'site_id': None,
  1690. }
  1691. self.fields['untagged_vlan'].queryset = VLAN.objects.filter(**filter_dict)
  1692. self.fields['tagged_vlans'].queryset = VLAN.objects.filter(**filter_dict)
  1693. class InterfaceBulkRenameForm(BulkRenameForm):
  1694. pk = forms.ModelMultipleChoiceField(queryset=Interface.objects.all(), widget=forms.MultipleHiddenInput)
  1695. class InterfaceBulkDisconnectForm(ConfirmationForm):
  1696. pk = forms.ModelMultipleChoiceField(queryset=Interface.objects.all(), widget=forms.MultipleHiddenInput)
  1697. #
  1698. # Interface connections
  1699. #
  1700. class InterfaceConnectionForm(BootstrapMixin, ChainedFieldsMixin, forms.ModelForm):
  1701. interface_a = forms.ChoiceField(
  1702. choices=[],
  1703. widget=SelectWithDisabled,
  1704. label='Interface'
  1705. )
  1706. site_b = forms.ModelChoiceField(
  1707. queryset=Site.objects.all(),
  1708. label='Site',
  1709. required=False,
  1710. widget=forms.Select(
  1711. attrs={'filter-for': 'rack_b'}
  1712. )
  1713. )
  1714. rack_b = ChainedModelChoiceField(
  1715. queryset=Rack.objects.all(),
  1716. chains=(
  1717. ('site', 'site_b'),
  1718. ),
  1719. label='Rack',
  1720. required=False,
  1721. widget=APISelect(
  1722. api_url='/api/dcim/racks/?site_id={{site_b}}',
  1723. attrs={'filter-for': 'device_b', 'nullable': 'true'}
  1724. )
  1725. )
  1726. device_b = ChainedModelChoiceField(
  1727. queryset=Device.objects.all(),
  1728. chains=(
  1729. ('site', 'site_b'),
  1730. ('rack', 'rack_b'),
  1731. ),
  1732. label='Device',
  1733. required=False,
  1734. widget=APISelect(
  1735. api_url='/api/dcim/devices/?site_id={{site_b}}&rack_id={{rack_b}}',
  1736. display_field='display_name',
  1737. attrs={'filter-for': 'interface_b'}
  1738. )
  1739. )
  1740. livesearch = forms.CharField(
  1741. required=False,
  1742. label='Device',
  1743. widget=Livesearch(
  1744. query_key='q',
  1745. query_url='dcim-api:device-list',
  1746. field_to_update='device_b'
  1747. )
  1748. )
  1749. interface_b = ChainedModelChoiceField(
  1750. queryset=Interface.objects.connectable().select_related(
  1751. 'circuit_termination', 'connected_as_a', 'connected_as_b'
  1752. ),
  1753. chains=(
  1754. ('device', 'device_b'),
  1755. ),
  1756. label='Interface',
  1757. widget=APISelect(
  1758. api_url='/api/dcim/interfaces/?device_id={{device_b}}&type=physical',
  1759. disabled_indicator='is_connected'
  1760. )
  1761. )
  1762. class Meta:
  1763. model = InterfaceConnection
  1764. fields = ['interface_a', 'site_b', 'rack_b', 'device_b', 'interface_b', 'livesearch', 'connection_status']
  1765. def __init__(self, device_a, *args, **kwargs):
  1766. super(InterfaceConnectionForm, self).__init__(*args, **kwargs)
  1767. # Initialize interface A choices
  1768. device_a_interfaces = device_a.vc_interfaces.connectable().order_naturally().select_related(
  1769. 'circuit_termination', 'connected_as_a', 'connected_as_b'
  1770. )
  1771. self.fields['interface_a'].choices = [
  1772. (iface.id, {'label': iface.name, 'disabled': iface.is_connected}) for iface in device_a_interfaces
  1773. ]
  1774. # Mark connected interfaces as disabled
  1775. if self.data.get('device_b'):
  1776. self.fields['interface_b'].choices = []
  1777. for iface in self.fields['interface_b'].queryset:
  1778. self.fields['interface_b'].choices.append(
  1779. (iface.id, {'label': iface.name, 'disabled': iface.is_connected})
  1780. )
  1781. class InterfaceConnectionCSVForm(forms.ModelForm):
  1782. device_a = FlexibleModelChoiceField(
  1783. queryset=Device.objects.all(),
  1784. to_field_name='name',
  1785. help_text='Name or ID of device A',
  1786. error_messages={'invalid_choice': 'Device A not found.'}
  1787. )
  1788. interface_a = forms.CharField(
  1789. help_text='Name of interface A'
  1790. )
  1791. device_b = FlexibleModelChoiceField(
  1792. queryset=Device.objects.all(),
  1793. to_field_name='name',
  1794. help_text='Name or ID of device B',
  1795. error_messages={'invalid_choice': 'Device B not found.'}
  1796. )
  1797. interface_b = forms.CharField(
  1798. help_text='Name of interface B'
  1799. )
  1800. connection_status = CSVChoiceField(
  1801. choices=CONNECTION_STATUS_CHOICES,
  1802. help_text='Connection status'
  1803. )
  1804. class Meta:
  1805. model = InterfaceConnection
  1806. fields = InterfaceConnection.csv_headers
  1807. def clean_interface_a(self):
  1808. interface_name = self.cleaned_data.get('interface_a')
  1809. if not interface_name:
  1810. return None
  1811. try:
  1812. # Retrieve interface by name
  1813. interface = Interface.objects.get(
  1814. device=self.cleaned_data['device_a'], name=interface_name
  1815. )
  1816. # Check for an existing connection to this interface
  1817. if InterfaceConnection.objects.filter(Q(interface_a=interface) | Q(interface_b=interface)).count():
  1818. raise forms.ValidationError("{} {} is already connected".format(
  1819. self.cleaned_data['device_a'], interface_name
  1820. ))
  1821. except Interface.DoesNotExist:
  1822. raise forms.ValidationError("Invalid interface ({} {})".format(
  1823. self.cleaned_data['device_a'], interface_name
  1824. ))
  1825. return interface
  1826. def clean_interface_b(self):
  1827. interface_name = self.cleaned_data.get('interface_b')
  1828. if not interface_name:
  1829. return None
  1830. try:
  1831. # Retrieve interface by name
  1832. interface = Interface.objects.get(
  1833. device=self.cleaned_data['device_b'], name=interface_name
  1834. )
  1835. # Check for an existing connection to this interface
  1836. if InterfaceConnection.objects.filter(Q(interface_a=interface) | Q(interface_b=interface)).count():
  1837. raise forms.ValidationError("{} {} is already connected".format(
  1838. self.cleaned_data['device_b'], interface_name
  1839. ))
  1840. except Interface.DoesNotExist:
  1841. raise forms.ValidationError("Invalid interface ({} {})".format(
  1842. self.cleaned_data['device_b'], interface_name
  1843. ))
  1844. return interface
  1845. #
  1846. # Device bays
  1847. #
  1848. class DeviceBayForm(BootstrapMixin, forms.ModelForm):
  1849. class Meta:
  1850. model = DeviceBay
  1851. fields = ['device', 'name']
  1852. widgets = {
  1853. 'device': forms.HiddenInput(),
  1854. }
  1855. class DeviceBayCreateForm(ComponentForm):
  1856. name_pattern = ExpandableNameField(label='Name')
  1857. class PopulateDeviceBayForm(BootstrapMixin, forms.Form):
  1858. installed_device = forms.ModelChoiceField(
  1859. queryset=Device.objects.all(),
  1860. label='Child Device',
  1861. help_text="Child devices must first be created and assigned to the site/rack of the parent device."
  1862. )
  1863. def __init__(self, device_bay, *args, **kwargs):
  1864. super(PopulateDeviceBayForm, self).__init__(*args, **kwargs)
  1865. self.fields['installed_device'].queryset = Device.objects.filter(
  1866. site=device_bay.device.site,
  1867. rack=device_bay.device.rack,
  1868. parent_bay__isnull=True,
  1869. device_type__u_height=0,
  1870. device_type__subdevice_role=SUBDEVICE_ROLE_CHILD
  1871. ).exclude(pk=device_bay.device.pk)
  1872. class DeviceBayBulkRenameForm(BulkRenameForm):
  1873. pk = forms.ModelMultipleChoiceField(queryset=DeviceBay.objects.all(), widget=forms.MultipleHiddenInput)
  1874. #
  1875. # Connections
  1876. #
  1877. class ConsoleConnectionFilterForm(BootstrapMixin, forms.Form):
  1878. site = forms.ModelChoiceField(required=False, queryset=Site.objects.all(), to_field_name='slug')
  1879. device = forms.CharField(required=False, label='Device name')
  1880. class PowerConnectionFilterForm(BootstrapMixin, forms.Form):
  1881. site = forms.ModelChoiceField(required=False, queryset=Site.objects.all(), to_field_name='slug')
  1882. device = forms.CharField(required=False, label='Device name')
  1883. class InterfaceConnectionFilterForm(BootstrapMixin, forms.Form):
  1884. site = forms.ModelChoiceField(required=False, queryset=Site.objects.all(), to_field_name='slug')
  1885. device = forms.CharField(required=False, label='Device name')
  1886. #
  1887. # Inventory items
  1888. #
  1889. class InventoryItemForm(BootstrapMixin, forms.ModelForm):
  1890. class Meta:
  1891. model = InventoryItem
  1892. fields = ['name', 'manufacturer', 'part_id', 'serial', 'asset_tag', 'description']
  1893. class InventoryItemCSVForm(forms.ModelForm):
  1894. device = FlexibleModelChoiceField(
  1895. queryset=Device.objects.all(),
  1896. to_field_name='name',
  1897. help_text='Device name or ID',
  1898. error_messages={
  1899. 'invalid_choice': 'Device not found.',
  1900. }
  1901. )
  1902. manufacturer = forms.ModelChoiceField(
  1903. queryset=Manufacturer.objects.all(),
  1904. to_field_name='name',
  1905. required=False,
  1906. help_text='Manufacturer name',
  1907. error_messages={
  1908. 'invalid_choice': 'Invalid manufacturer.',
  1909. }
  1910. )
  1911. class Meta:
  1912. model = InventoryItem
  1913. fields = InventoryItem.csv_headers
  1914. class InventoryItemBulkEditForm(BootstrapMixin, BulkEditForm):
  1915. pk = forms.ModelMultipleChoiceField(queryset=InventoryItem.objects.all(), widget=forms.MultipleHiddenInput)
  1916. manufacturer = forms.ModelChoiceField(queryset=Manufacturer.objects.all(), required=False)
  1917. part_id = forms.CharField(max_length=50, required=False, label='Part ID')
  1918. description = forms.CharField(max_length=100, required=False)
  1919. class Meta:
  1920. nullable_fields = ['manufacturer', 'part_id', 'description']
  1921. class InventoryItemFilterForm(BootstrapMixin, forms.Form):
  1922. model = InventoryItem
  1923. q = forms.CharField(required=False, label='Search')
  1924. manufacturer = FilterChoiceField(
  1925. queryset=Manufacturer.objects.annotate(filter_count=Count('inventory_items')),
  1926. to_field_name='slug',
  1927. null_label='-- None --'
  1928. )
  1929. #
  1930. # Virtual chassis
  1931. #
  1932. class DeviceSelectionForm(forms.Form):
  1933. pk = forms.ModelMultipleChoiceField(queryset=Device.objects.all(), widget=forms.MultipleHiddenInput)
  1934. class VirtualChassisForm(BootstrapMixin, forms.ModelForm):
  1935. class Meta:
  1936. model = VirtualChassis
  1937. fields = ['master', 'domain']
  1938. widgets = {
  1939. 'master': SelectWithPK,
  1940. }
  1941. class BaseVCMemberFormSet(forms.BaseModelFormSet):
  1942. def clean(self):
  1943. super(BaseVCMemberFormSet, self).clean()
  1944. # Check for duplicate VC position values
  1945. vc_position_list = []
  1946. for form in self.forms:
  1947. vc_position = form.cleaned_data['vc_position']
  1948. if vc_position in vc_position_list:
  1949. error_msg = 'A virtual chassis member already exists in position {}.'.format(vc_position)
  1950. form.add_error('vc_position', error_msg)
  1951. vc_position_list.append(vc_position)
  1952. class DeviceVCMembershipForm(forms.ModelForm):
  1953. class Meta:
  1954. model = Device
  1955. fields = ['vc_position', 'vc_priority']
  1956. labels = {
  1957. 'vc_position': 'Position',
  1958. 'vc_priority': 'Priority',
  1959. }
  1960. def __init__(self, validate_vc_position=False, *args, **kwargs):
  1961. super(DeviceVCMembershipForm, self).__init__(*args, **kwargs)
  1962. # Require VC position (only required when the Device is a VirtualChassis member)
  1963. self.fields['vc_position'].required = True
  1964. # Validation of vc_position is optional. This is only required when adding a new member to an existing
  1965. # VirtualChassis. Otherwise, vc_position validation is handled by BaseVCMemberFormSet.
  1966. self.validate_vc_position = validate_vc_position
  1967. def clean_vc_position(self):
  1968. vc_position = self.cleaned_data['vc_position']
  1969. if self.validate_vc_position:
  1970. conflicting_members = Device.objects.filter(
  1971. virtual_chassis=self.instance.virtual_chassis,
  1972. vc_position=vc_position
  1973. )
  1974. if conflicting_members.exists():
  1975. raise forms.ValidationError(
  1976. 'A virtual chassis member already exists in position {}.'.format(vc_position)
  1977. )
  1978. return vc_position
  1979. class VCMemberSelectForm(BootstrapMixin, ChainedFieldsMixin, forms.Form):
  1980. site = forms.ModelChoiceField(
  1981. queryset=Site.objects.all(),
  1982. label='Site',
  1983. required=False,
  1984. widget=forms.Select(
  1985. attrs={'filter-for': 'rack'}
  1986. )
  1987. )
  1988. rack = ChainedModelChoiceField(
  1989. queryset=Rack.objects.all(),
  1990. chains=(
  1991. ('site', 'site'),
  1992. ),
  1993. label='Rack',
  1994. required=False,
  1995. widget=APISelect(
  1996. api_url='/api/dcim/racks/?site_id={{site}}',
  1997. attrs={'filter-for': 'device', 'nullable': 'true'}
  1998. )
  1999. )
  2000. device = ChainedModelChoiceField(
  2001. queryset=Device.objects.filter(virtual_chassis__isnull=True),
  2002. chains=(
  2003. ('site', 'site'),
  2004. ('rack', 'rack'),
  2005. ),
  2006. label='Device',
  2007. widget=APISelect(
  2008. api_url='/api/dcim/devices/?site_id={{site}}&rack_id={{rack}}',
  2009. display_field='display_name',
  2010. disabled_indicator='virtual_chassis'
  2011. )
  2012. )
  2013. def clean_device(self):
  2014. device = self.cleaned_data['device']
  2015. if device.virtual_chassis is not None:
  2016. raise forms.ValidationError("Device {} is already assigned to a virtual chassis.".format(device))
  2017. return device
  2018. class VirtualChassisFilterForm(BootstrapMixin, CustomFieldFilterForm):
  2019. model = VirtualChassis
  2020. q = forms.CharField(required=False, label='Search')
  2021. site = FilterChoiceField(
  2022. queryset=Site.objects.all(),
  2023. to_field_name='slug',
  2024. )
  2025. tenant = FilterChoiceField(
  2026. queryset=Tenant.objects.all(),
  2027. to_field_name='slug',
  2028. null_label='-- None --',
  2029. )