forms.py 73 KB

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