forms.py 75 KB

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