forms.py 110 KB

1234567891011121314151617181920212223242526272829303132333435363738394041424344454647484950515253545556575859606162636465666768697071727374757677787980818283848586878889909192939495969798991001011021031041051061071081091101111121131141151161171181191201211221231241251261271281291301311321331341351361371381391401411421431441451461471481491501511521531541551561571581591601611621631641651661671681691701711721731741751761771781791801811821831841851861871881891901911921931941951961971981992002012022032042052062072082092102112122132142152162172182192202212222232242252262272282292302312322332342352362372382392402412422432442452462472482492502512522532542552562572582592602612622632642652662672682692702712722732742752762772782792802812822832842852862872882892902912922932942952962972982993003013023033043053063073083093103113123133143153163173183193203213223233243253263273283293303313323333343353363373383393403413423433443453463473483493503513523533543553563573583593603613623633643653663673683693703713723733743753763773783793803813823833843853863873883893903913923933943953963973983994004014024034044054064074084094104114124134144154164174184194204214224234244254264274284294304314324334344354364374384394404414424434444454464474484494504514524534544554564574584594604614624634644654664674684694704714724734744754764774784794804814824834844854864874884894904914924934944954964974984995005015025035045055065075085095105115125135145155165175185195205215225235245255265275285295305315325335345355365375385395405415425435445455465475485495505515525535545555565575585595605615625635645655665675685695705715725735745755765775785795805815825835845855865875885895905915925935945955965975985996006016026036046056066076086096106116126136146156166176186196206216226236246256266276286296306316326336346356366376386396406416426436446456466476486496506516526536546556566576586596606616626636646656666676686696706716726736746756766776786796806816826836846856866876886896906916926936946956966976986997007017027037047057067077087097107117127137147157167177187197207217227237247257267277287297307317327337347357367377387397407417427437447457467477487497507517527537547557567577587597607617627637647657667677687697707717727737747757767777787797807817827837847857867877887897907917927937947957967977987998008018028038048058068078088098108118128138148158168178188198208218228238248258268278288298308318328338348358368378388398408418428438448458468478488498508518528538548558568578588598608618628638648658668678688698708718728738748758768778788798808818828838848858868878888898908918928938948958968978988999009019029039049059069079089099109119129139149159169179189199209219229239249259269279289299309319329339349359369379389399409419429439449459469479489499509519529539549559569579589599609619629639649659669679689699709719729739749759769779789799809819829839849859869879889899909919929939949959969979989991000100110021003100410051006100710081009101010111012101310141015101610171018101910201021102210231024102510261027102810291030103110321033103410351036103710381039104010411042104310441045104610471048104910501051105210531054105510561057105810591060106110621063106410651066106710681069107010711072107310741075107610771078107910801081108210831084108510861087108810891090109110921093109410951096109710981099110011011102110311041105110611071108110911101111111211131114111511161117111811191120112111221123112411251126112711281129113011311132113311341135113611371138113911401141114211431144114511461147114811491150115111521153115411551156115711581159116011611162116311641165116611671168116911701171117211731174117511761177117811791180118111821183118411851186118711881189119011911192119311941195119611971198119912001201120212031204120512061207120812091210121112121213121412151216121712181219122012211222122312241225122612271228122912301231123212331234123512361237123812391240124112421243124412451246124712481249125012511252125312541255125612571258125912601261126212631264126512661267126812691270127112721273127412751276127712781279128012811282128312841285128612871288128912901291129212931294129512961297129812991300130113021303130413051306130713081309131013111312131313141315131613171318131913201321132213231324132513261327132813291330133113321333133413351336133713381339134013411342134313441345134613471348134913501351135213531354135513561357135813591360136113621363136413651366136713681369137013711372137313741375137613771378137913801381138213831384138513861387138813891390139113921393139413951396139713981399140014011402140314041405140614071408140914101411141214131414141514161417141814191420142114221423142414251426142714281429143014311432143314341435143614371438143914401441144214431444144514461447144814491450145114521453145414551456145714581459146014611462146314641465146614671468146914701471147214731474147514761477147814791480148114821483148414851486148714881489149014911492149314941495149614971498149915001501150215031504150515061507150815091510151115121513151415151516151715181519152015211522152315241525152615271528152915301531153215331534153515361537153815391540154115421543154415451546154715481549155015511552155315541555155615571558155915601561156215631564156515661567156815691570157115721573157415751576157715781579158015811582158315841585158615871588158915901591159215931594159515961597159815991600160116021603160416051606160716081609161016111612161316141615161616171618161916201621162216231624162516261627162816291630163116321633163416351636163716381639164016411642164316441645164616471648164916501651165216531654165516561657165816591660166116621663166416651666166716681669167016711672167316741675167616771678167916801681168216831684168516861687168816891690169116921693169416951696169716981699170017011702170317041705170617071708170917101711171217131714171517161717171817191720172117221723172417251726172717281729173017311732173317341735173617371738173917401741174217431744174517461747174817491750175117521753175417551756175717581759176017611762176317641765176617671768176917701771177217731774177517761777177817791780178117821783178417851786178717881789179017911792179317941795179617971798179918001801180218031804180518061807180818091810181118121813181418151816181718181819182018211822182318241825182618271828182918301831183218331834183518361837183818391840184118421843184418451846184718481849185018511852185318541855185618571858185918601861186218631864186518661867186818691870187118721873187418751876187718781879188018811882188318841885188618871888188918901891189218931894189518961897189818991900190119021903190419051906190719081909191019111912191319141915191619171918191919201921192219231924192519261927192819291930193119321933193419351936193719381939194019411942194319441945194619471948194919501951195219531954195519561957195819591960196119621963196419651966196719681969197019711972197319741975197619771978197919801981198219831984198519861987198819891990199119921993199419951996199719981999200020012002200320042005200620072008200920102011201220132014201520162017201820192020202120222023202420252026202720282029203020312032203320342035203620372038203920402041204220432044204520462047204820492050205120522053205420552056205720582059206020612062206320642065206620672068206920702071207220732074207520762077207820792080208120822083208420852086208720882089209020912092209320942095209620972098209921002101210221032104210521062107210821092110211121122113211421152116211721182119212021212122212321242125212621272128212921302131213221332134213521362137213821392140214121422143214421452146214721482149215021512152215321542155215621572158215921602161216221632164216521662167216821692170217121722173217421752176217721782179218021812182218321842185218621872188218921902191219221932194219521962197219821992200220122022203220422052206220722082209221022112212221322142215221622172218221922202221222222232224222522262227222822292230223122322233223422352236223722382239224022412242224322442245224622472248224922502251225222532254225522562257225822592260226122622263226422652266226722682269227022712272227322742275227622772278227922802281228222832284228522862287228822892290229122922293229422952296229722982299230023012302230323042305230623072308230923102311231223132314231523162317231823192320232123222323232423252326232723282329233023312332233323342335233623372338233923402341234223432344234523462347234823492350235123522353235423552356235723582359236023612362236323642365236623672368236923702371237223732374237523762377237823792380238123822383238423852386238723882389239023912392239323942395239623972398239924002401240224032404240524062407240824092410241124122413241424152416241724182419242024212422242324242425242624272428242924302431243224332434243524362437243824392440244124422443244424452446244724482449245024512452245324542455245624572458245924602461246224632464246524662467246824692470247124722473247424752476247724782479248024812482248324842485248624872488248924902491249224932494249524962497249824992500250125022503250425052506250725082509251025112512251325142515251625172518251925202521252225232524252525262527252825292530253125322533253425352536253725382539254025412542254325442545254625472548254925502551255225532554255525562557255825592560256125622563256425652566256725682569257025712572257325742575257625772578257925802581258225832584258525862587258825892590259125922593259425952596259725982599260026012602260326042605260626072608260926102611261226132614261526162617261826192620262126222623262426252626262726282629263026312632263326342635263626372638263926402641264226432644264526462647264826492650265126522653265426552656265726582659266026612662266326642665266626672668266926702671267226732674267526762677267826792680268126822683268426852686268726882689269026912692269326942695269626972698269927002701270227032704270527062707270827092710271127122713271427152716271727182719272027212722272327242725272627272728272927302731273227332734273527362737273827392740274127422743274427452746274727482749275027512752275327542755275627572758275927602761276227632764276527662767276827692770277127722773277427752776277727782779278027812782278327842785278627872788278927902791279227932794279527962797279827992800280128022803280428052806280728082809281028112812281328142815281628172818281928202821282228232824282528262827282828292830283128322833283428352836283728382839284028412842284328442845284628472848284928502851285228532854285528562857285828592860286128622863286428652866286728682869287028712872287328742875287628772878287928802881288228832884288528862887288828892890289128922893289428952896289728982899290029012902290329042905290629072908290929102911291229132914291529162917291829192920292129222923292429252926292729282929293029312932293329342935293629372938293929402941294229432944294529462947294829492950295129522953295429552956295729582959296029612962296329642965296629672968296929702971297229732974297529762977297829792980298129822983298429852986298729882989299029912992299329942995299629972998299930003001300230033004300530063007300830093010301130123013301430153016301730183019302030213022302330243025302630273028302930303031303230333034303530363037303830393040304130423043304430453046304730483049305030513052305330543055305630573058305930603061306230633064306530663067306830693070307130723073307430753076307730783079308030813082308330843085308630873088308930903091309230933094309530963097309830993100310131023103310431053106310731083109311031113112311331143115311631173118311931203121312231233124312531263127312831293130313131323133313431353136313731383139314031413142314331443145314631473148314931503151315231533154315531563157315831593160316131623163316431653166316731683169317031713172317331743175317631773178317931803181318231833184318531863187318831893190319131923193319431953196319731983199320032013202320332043205320632073208320932103211321232133214321532163217321832193220322132223223322432253226322732283229323032313232323332343235323632373238323932403241324232433244324532463247324832493250325132523253325432553256325732583259326032613262326332643265326632673268326932703271327232733274327532763277327832793280328132823283328432853286328732883289329032913292329332943295329632973298329933003301330233033304330533063307330833093310331133123313331433153316331733183319332033213322332333243325332633273328332933303331333233333334333533363337333833393340334133423343334433453346334733483349335033513352335333543355335633573358335933603361336233633364336533663367336833693370337133723373337433753376337733783379338033813382338333843385338633873388338933903391339233933394339533963397339833993400340134023403340434053406340734083409341034113412341334143415341634173418341934203421342234233424342534263427342834293430343134323433343434353436343734383439344034413442344334443445344634473448344934503451345234533454345534563457345834593460346134623463346434653466346734683469347034713472347334743475347634773478347934803481348234833484348534863487348834893490349134923493349434953496349734983499350035013502350335043505350635073508350935103511351235133514351535163517351835193520352135223523352435253526352735283529353035313532353335343535353635373538353935403541354235433544354535463547354835493550355135523553355435553556355735583559356035613562356335643565356635673568356935703571357235733574357535763577357835793580358135823583358435853586358735883589359035913592359335943595359635973598359936003601360236033604360536063607360836093610361136123613361436153616361736183619362036213622362336243625362636273628362936303631363236333634363536363637363836393640364136423643364436453646364736483649365036513652365336543655365636573658365936603661366236633664366536663667366836693670367136723673367436753676367736783679368036813682368336843685368636873688368936903691369236933694369536963697369836993700370137023703370437053706370737083709371037113712371337143715371637173718371937203721372237233724372537263727372837293730373137323733373437353736373737383739374037413742374337443745374637473748374937503751375237533754375537563757375837593760376137623763376437653766376737683769377037713772377337743775377637773778377937803781378237833784378537863787378837893790379137923793379437953796379737983799380038013802380338043805380638073808380938103811381238133814381538163817381838193820382138223823382438253826382738283829383038313832383338343835383638373838383938403841384238433844384538463847384838493850385138523853385438553856385738583859386038613862386338643865386638673868386938703871387238733874387538763877387838793880388138823883388438853886388738883889389038913892389338943895389638973898389939003901390239033904390539063907390839093910391139123913391439153916391739183919392039213922392339243925392639273928392939303931393239333934393539363937
  1. import re
  2. from django import forms
  3. from django.contrib.auth.models import User
  4. from django.contrib.contenttypes.models import ContentType
  5. from django.contrib.postgres.forms.array import SimpleArrayField
  6. from django.core.exceptions import ObjectDoesNotExist
  7. from django.db.models import Q
  8. from mptt.forms import TreeNodeChoiceField
  9. from netaddr import EUI
  10. from netaddr.core import AddrFormatError
  11. from taggit.forms import TagField
  12. from timezone_field import TimeZoneFormField
  13. from circuits.models import Circuit, Provider
  14. from extras.forms import (
  15. AddRemoveTagsForm, CustomFieldForm, CustomFieldBulkEditForm, CustomFieldFilterForm, LocalConfigContextFilterForm
  16. )
  17. from ipam.models import IPAddress, VLAN, VLANGroup
  18. from tenancy.forms import TenancyFilterForm, TenancyForm
  19. from tenancy.models import Tenant, TenantGroup
  20. from utilities.forms import (
  21. APISelect, APISelectMultiple, add_blank_choice, ArrayFieldSelectMultiple, BootstrapMixin, BulkEditForm,
  22. BulkEditNullBooleanSelect, ChainedFieldsMixin, ChainedModelChoiceField, ColorSelect, CommentField, ComponentForm,
  23. ConfirmationForm, CSVChoiceField, ExpandableNameField, FilterChoiceField, FlexibleModelChoiceField, JSONField,
  24. SelectWithPK, SmallTextarea, SlugField, StaticSelect2, StaticSelect2Multiple, BOOLEAN_WITH_BLANK_CHOICES
  25. )
  26. from virtualization.models import Cluster, ClusterGroup
  27. from .constants import *
  28. from .models import (
  29. Cable, DeviceBay, DeviceBayTemplate, ConsolePort, ConsolePortTemplate, ConsoleServerPort, ConsoleServerPortTemplate,
  30. Device, DeviceRole, DeviceType, FrontPort, FrontPortTemplate, Interface, InterfaceTemplate, Manufacturer,
  31. InventoryItem, Platform, PowerFeed, PowerOutlet, PowerOutletTemplate, PowerPanel, PowerPort, PowerPortTemplate,
  32. Rack, RackGroup, RackReservation, RackRole, RearPort, RearPortTemplate, Region, Site, VirtualChassis,
  33. )
  34. DEVICE_BY_PK_RE = r'{\d+\}'
  35. INTERFACE_MODE_HELP_TEXT = """
  36. Access: One untagged VLAN<br />
  37. Tagged: One untagged VLAN and/or one or more tagged VLANs<br />
  38. Tagged All: Implies all VLANs are available (w/optional untagged VLAN)
  39. """
  40. def get_device_by_name_or_pk(name):
  41. """
  42. Attempt to retrieve a device by either its name or primary key ('{pk}').
  43. """
  44. if re.match(DEVICE_BY_PK_RE, name):
  45. pk = name.strip('{}')
  46. device = Device.objects.get(pk=pk)
  47. else:
  48. device = Device.objects.get(name=name)
  49. return device
  50. class InterfaceCommonForm:
  51. def clean(self):
  52. super().clean()
  53. # Validate VLAN assignments
  54. tagged_vlans = self.cleaned_data['tagged_vlans']
  55. # Untagged interfaces cannot be assigned tagged VLANs
  56. if self.cleaned_data['mode'] == IFACE_MODE_ACCESS and tagged_vlans:
  57. raise forms.ValidationError({
  58. 'mode': "An access interface cannot have tagged VLANs assigned."
  59. })
  60. # Remove all tagged VLAN assignments from "tagged all" interfaces
  61. elif self.cleaned_data['mode'] == IFACE_MODE_TAGGED_ALL:
  62. self.cleaned_data['tagged_vlans'] = []
  63. class BulkRenameForm(forms.Form):
  64. """
  65. An extendable form to be used for renaming device components in bulk.
  66. """
  67. find = forms.CharField()
  68. replace = forms.CharField()
  69. use_regex = forms.BooleanField(
  70. required=False,
  71. initial=True,
  72. label='Use regular expressions'
  73. )
  74. def clean(self):
  75. # Validate regular expression in "find" field
  76. if self.cleaned_data['use_regex']:
  77. try:
  78. re.compile(self.cleaned_data['find'])
  79. except re.error:
  80. raise forms.ValidationError({
  81. 'find': "Invalid regular expression"
  82. })
  83. #
  84. # Fields
  85. #
  86. class MACAddressField(forms.Field):
  87. widget = forms.CharField
  88. default_error_messages = {
  89. 'invalid': 'MAC address must be in EUI-48 format',
  90. }
  91. def to_python(self, value):
  92. value = super().to_python(value)
  93. # Validate MAC address format
  94. try:
  95. value = EUI(value.strip())
  96. except AddrFormatError:
  97. raise forms.ValidationError(self.error_messages['invalid'], code='invalid')
  98. return value
  99. #
  100. # Regions
  101. #
  102. class RegionForm(BootstrapMixin, forms.ModelForm):
  103. slug = SlugField()
  104. class Meta:
  105. model = Region
  106. fields = [
  107. 'parent', 'name', 'slug',
  108. ]
  109. widgets = {
  110. 'parent': APISelect(
  111. api_url="/api/dcim/regions/"
  112. )
  113. }
  114. class RegionCSVForm(forms.ModelForm):
  115. parent = forms.ModelChoiceField(
  116. queryset=Region.objects.all(),
  117. required=False,
  118. to_field_name='name',
  119. help_text='Name of parent region',
  120. error_messages={
  121. 'invalid_choice': 'Region not found.',
  122. }
  123. )
  124. class Meta:
  125. model = Region
  126. fields = Region.csv_headers
  127. help_texts = {
  128. 'name': 'Region name',
  129. 'slug': 'URL-friendly slug',
  130. }
  131. class RegionFilterForm(BootstrapMixin, forms.Form):
  132. model = Site
  133. q = forms.CharField(
  134. required=False,
  135. label='Search'
  136. )
  137. #
  138. # Sites
  139. #
  140. class SiteForm(BootstrapMixin, TenancyForm, CustomFieldForm):
  141. region = TreeNodeChoiceField(
  142. queryset=Region.objects.all(),
  143. required=False,
  144. widget=APISelect(
  145. api_url="/api/dcim/regions/"
  146. )
  147. )
  148. slug = SlugField()
  149. comments = CommentField()
  150. tags = TagField(
  151. required=False
  152. )
  153. class Meta:
  154. model = Site
  155. fields = [
  156. 'name', 'slug', 'status', 'region', 'tenant_group', 'tenant', 'facility', 'asn', 'time_zone', 'description',
  157. 'physical_address', 'shipping_address', 'latitude', 'longitude', 'contact_name', 'contact_phone',
  158. 'contact_email', 'comments', 'tags',
  159. ]
  160. widgets = {
  161. 'physical_address': SmallTextarea(
  162. attrs={
  163. 'rows': 3,
  164. }
  165. ),
  166. 'shipping_address': SmallTextarea(
  167. attrs={
  168. 'rows': 3,
  169. }
  170. ),
  171. 'status': StaticSelect2(),
  172. 'time_zone': StaticSelect2(),
  173. }
  174. help_texts = {
  175. 'name': "Full name of the site",
  176. 'facility': "Data center provider and facility (e.g. Equinix NY7)",
  177. 'asn': "BGP autonomous system number",
  178. 'time_zone': "Local time zone",
  179. 'description': "Short description (will appear in sites list)",
  180. 'physical_address': "Physical location of the building (e.g. for GPS)",
  181. 'shipping_address': "If different from the physical address",
  182. 'latitude': "Latitude in decimal format (xx.yyyyyy)",
  183. 'longitude': "Longitude in decimal format (xx.yyyyyy)"
  184. }
  185. class SiteCSVForm(forms.ModelForm):
  186. status = CSVChoiceField(
  187. choices=SITE_STATUS_CHOICES,
  188. required=False,
  189. help_text='Operational status'
  190. )
  191. region = forms.ModelChoiceField(
  192. queryset=Region.objects.all(),
  193. required=False,
  194. to_field_name='name',
  195. help_text='Name of assigned region',
  196. error_messages={
  197. 'invalid_choice': 'Region not found.',
  198. }
  199. )
  200. tenant = forms.ModelChoiceField(
  201. queryset=Tenant.objects.all(),
  202. required=False,
  203. to_field_name='name',
  204. help_text='Name of assigned tenant',
  205. error_messages={
  206. 'invalid_choice': 'Tenant not found.',
  207. }
  208. )
  209. class Meta:
  210. model = Site
  211. fields = Site.csv_headers
  212. help_texts = {
  213. 'name': 'Site name',
  214. 'slug': 'URL-friendly slug',
  215. 'asn': '32-bit autonomous system number',
  216. }
  217. class SiteBulkEditForm(BootstrapMixin, AddRemoveTagsForm, CustomFieldBulkEditForm):
  218. pk = forms.ModelMultipleChoiceField(
  219. queryset=Site.objects.all(),
  220. widget=forms.MultipleHiddenInput
  221. )
  222. status = forms.ChoiceField(
  223. choices=add_blank_choice(SITE_STATUS_CHOICES),
  224. required=False,
  225. initial='',
  226. widget=StaticSelect2()
  227. )
  228. region = TreeNodeChoiceField(
  229. queryset=Region.objects.all(),
  230. required=False,
  231. widget=APISelect(
  232. api_url="/api/dcim/regions/"
  233. )
  234. )
  235. tenant = forms.ModelChoiceField(
  236. queryset=Tenant.objects.all(),
  237. required=False,
  238. widget=APISelect(
  239. api_url="/api/tenancy/tenants",
  240. )
  241. )
  242. asn = forms.IntegerField(
  243. min_value=1,
  244. max_value=4294967295,
  245. required=False,
  246. label='ASN'
  247. )
  248. description = forms.CharField(
  249. max_length=100,
  250. required=False
  251. )
  252. time_zone = TimeZoneFormField(
  253. choices=add_blank_choice(TimeZoneFormField().choices),
  254. required=False,
  255. widget=StaticSelect2()
  256. )
  257. class Meta:
  258. nullable_fields = [
  259. 'region', 'tenant', 'asn', 'description', 'time_zone',
  260. ]
  261. class SiteFilterForm(BootstrapMixin, TenancyFilterForm, CustomFieldFilterForm):
  262. model = Site
  263. field_order = ['q', 'status', 'region', 'tenant_group', 'tenant']
  264. q = forms.CharField(
  265. required=False,
  266. label='Search'
  267. )
  268. status = forms.MultipleChoiceField(
  269. choices=SITE_STATUS_CHOICES,
  270. required=False,
  271. widget=StaticSelect2Multiple()
  272. )
  273. region = forms.ModelMultipleChoiceField(
  274. queryset=Region.objects.all(),
  275. to_field_name='slug',
  276. required=False,
  277. widget=APISelectMultiple(
  278. api_url="/api/dcim/regions/",
  279. value_field="slug",
  280. )
  281. )
  282. #
  283. # Rack groups
  284. #
  285. class RackGroupForm(BootstrapMixin, forms.ModelForm):
  286. slug = SlugField()
  287. class Meta:
  288. model = RackGroup
  289. fields = [
  290. 'site', 'name', 'slug',
  291. ]
  292. widgets = {
  293. 'site': APISelect(
  294. api_url="/api/dcim/sites/"
  295. )
  296. }
  297. class RackGroupCSVForm(forms.ModelForm):
  298. site = forms.ModelChoiceField(
  299. queryset=Site.objects.all(),
  300. to_field_name='name',
  301. help_text='Name of parent site',
  302. error_messages={
  303. 'invalid_choice': 'Site not found.',
  304. }
  305. )
  306. class Meta:
  307. model = RackGroup
  308. fields = RackGroup.csv_headers
  309. help_texts = {
  310. 'name': 'Name of rack group',
  311. 'slug': 'URL-friendly slug',
  312. }
  313. class RackGroupFilterForm(BootstrapMixin, forms.Form):
  314. site = FilterChoiceField(
  315. queryset=Site.objects.all(),
  316. to_field_name='slug',
  317. widget=APISelectMultiple(
  318. api_url="/api/dcim/sites/",
  319. value_field="slug",
  320. )
  321. )
  322. #
  323. # Rack roles
  324. #
  325. class RackRoleForm(BootstrapMixin, forms.ModelForm):
  326. slug = SlugField()
  327. class Meta:
  328. model = RackRole
  329. fields = [
  330. 'name', 'slug', 'color',
  331. ]
  332. class RackRoleCSVForm(forms.ModelForm):
  333. slug = SlugField()
  334. class Meta:
  335. model = RackRole
  336. fields = RackRole.csv_headers
  337. help_texts = {
  338. 'name': 'Name of rack role',
  339. 'color': 'RGB color in hexadecimal (e.g. 00ff00)'
  340. }
  341. #
  342. # Racks
  343. #
  344. class RackForm(BootstrapMixin, TenancyForm, CustomFieldForm):
  345. group = ChainedModelChoiceField(
  346. queryset=RackGroup.objects.all(),
  347. chains=(
  348. ('site', 'site'),
  349. ),
  350. required=False,
  351. widget=APISelect(
  352. api_url='/api/dcim/rack-groups/',
  353. )
  354. )
  355. comments = CommentField()
  356. tags = TagField(
  357. required=False
  358. )
  359. class Meta:
  360. model = Rack
  361. fields = [
  362. 'site', 'group', 'name', 'facility_id', 'tenant_group', 'tenant', 'status', 'role', 'serial', 'asset_tag',
  363. 'type', 'width', 'u_height', 'desc_units', 'outer_width', 'outer_depth', 'outer_unit', 'comments', 'tags',
  364. ]
  365. help_texts = {
  366. 'site': "The site at which the rack exists",
  367. 'name': "Organizational rack name",
  368. 'facility_id': "The unique rack ID assigned by the facility",
  369. 'u_height': "Height in rack units",
  370. }
  371. widgets = {
  372. 'site': APISelect(
  373. api_url="/api/dcim/sites/",
  374. filter_for={
  375. 'group': 'site_id',
  376. }
  377. ),
  378. 'status': StaticSelect2(),
  379. 'role': APISelect(
  380. api_url="/api/dcim/rack-roles/"
  381. ),
  382. 'type': StaticSelect2(),
  383. 'width': StaticSelect2(),
  384. 'outer_unit': StaticSelect2(),
  385. }
  386. class RackCSVForm(forms.ModelForm):
  387. site = forms.ModelChoiceField(
  388. queryset=Site.objects.all(),
  389. to_field_name='name',
  390. help_text='Name of parent site',
  391. error_messages={
  392. 'invalid_choice': 'Site not found.',
  393. }
  394. )
  395. group_name = forms.CharField(
  396. help_text='Name of rack group',
  397. required=False
  398. )
  399. tenant = forms.ModelChoiceField(
  400. queryset=Tenant.objects.all(),
  401. required=False,
  402. to_field_name='name',
  403. help_text='Name of assigned tenant',
  404. error_messages={
  405. 'invalid_choice': 'Tenant not found.',
  406. }
  407. )
  408. status = CSVChoiceField(
  409. choices=RACK_STATUS_CHOICES,
  410. required=False,
  411. help_text='Operational status'
  412. )
  413. role = forms.ModelChoiceField(
  414. queryset=RackRole.objects.all(),
  415. required=False,
  416. to_field_name='name',
  417. help_text='Name of assigned role',
  418. error_messages={
  419. 'invalid_choice': 'Role not found.',
  420. }
  421. )
  422. type = CSVChoiceField(
  423. choices=RACK_TYPE_CHOICES,
  424. required=False,
  425. help_text='Rack type'
  426. )
  427. width = forms.ChoiceField(
  428. choices=(
  429. (RACK_WIDTH_19IN, '19'),
  430. (RACK_WIDTH_23IN, '23'),
  431. ),
  432. help_text='Rail-to-rail width (in inches)'
  433. )
  434. outer_unit = CSVChoiceField(
  435. choices=RACK_DIMENSION_UNIT_CHOICES,
  436. required=False,
  437. help_text='Unit for outer dimensions'
  438. )
  439. class Meta:
  440. model = Rack
  441. fields = Rack.csv_headers
  442. help_texts = {
  443. 'name': 'Rack name',
  444. 'u_height': 'Height in rack units',
  445. }
  446. def clean(self):
  447. super().clean()
  448. site = self.cleaned_data.get('site')
  449. group_name = self.cleaned_data.get('group_name')
  450. name = self.cleaned_data.get('name')
  451. facility_id = self.cleaned_data.get('facility_id')
  452. # Validate rack group
  453. if group_name:
  454. try:
  455. self.instance.group = RackGroup.objects.get(site=site, name=group_name)
  456. except RackGroup.DoesNotExist:
  457. raise forms.ValidationError("Rack group {} not found for site {}".format(group_name, site))
  458. # Validate uniqueness of rack name within group
  459. if Rack.objects.filter(group=self.instance.group, name=name).exists():
  460. raise forms.ValidationError(
  461. "A rack named {} already exists within group {}".format(name, group_name)
  462. )
  463. # Validate uniqueness of facility ID within group
  464. if facility_id and Rack.objects.filter(group=self.instance.group, facility_id=facility_id).exists():
  465. raise forms.ValidationError(
  466. "A rack with the facility ID {} already exists within group {}".format(facility_id, group_name)
  467. )
  468. class RackBulkEditForm(BootstrapMixin, AddRemoveTagsForm, CustomFieldBulkEditForm):
  469. pk = forms.ModelMultipleChoiceField(
  470. queryset=Rack.objects.all(),
  471. widget=forms.MultipleHiddenInput
  472. )
  473. site = forms.ModelChoiceField(
  474. queryset=Site.objects.all(),
  475. required=False,
  476. widget=APISelect(
  477. api_url="/api/dcim/sites",
  478. filter_for={
  479. 'group': 'site_id',
  480. }
  481. )
  482. )
  483. group = forms.ModelChoiceField(
  484. queryset=RackGroup.objects.all(),
  485. required=False,
  486. widget=APISelect(
  487. api_url="/api/dcim/rack-groups",
  488. )
  489. )
  490. tenant = forms.ModelChoiceField(
  491. queryset=Tenant.objects.all(),
  492. required=False,
  493. widget=APISelect(
  494. api_url="/api/tenancy/tenants",
  495. )
  496. )
  497. status = forms.ChoiceField(
  498. choices=add_blank_choice(RACK_STATUS_CHOICES),
  499. required=False,
  500. initial='',
  501. widget=StaticSelect2()
  502. )
  503. role = forms.ModelChoiceField(
  504. queryset=RackRole.objects.all(),
  505. required=False,
  506. widget=APISelect(
  507. api_url="/api/dcim/rack-roles",
  508. )
  509. )
  510. serial = forms.CharField(
  511. max_length=50,
  512. required=False,
  513. label='Serial Number'
  514. )
  515. asset_tag = forms.CharField(
  516. max_length=50,
  517. required=False
  518. )
  519. type = forms.ChoiceField(
  520. choices=add_blank_choice(RACK_TYPE_CHOICES),
  521. required=False,
  522. widget=StaticSelect2()
  523. )
  524. width = forms.ChoiceField(
  525. choices=add_blank_choice(RACK_WIDTH_CHOICES),
  526. required=False,
  527. widget=StaticSelect2()
  528. )
  529. u_height = forms.IntegerField(
  530. required=False,
  531. label='Height (U)'
  532. )
  533. desc_units = forms.NullBooleanField(
  534. required=False,
  535. widget=BulkEditNullBooleanSelect,
  536. label='Descending units'
  537. )
  538. outer_width = forms.IntegerField(
  539. required=False,
  540. min_value=1
  541. )
  542. outer_depth = forms.IntegerField(
  543. required=False,
  544. min_value=1
  545. )
  546. outer_unit = forms.ChoiceField(
  547. choices=add_blank_choice(RACK_DIMENSION_UNIT_CHOICES),
  548. required=False,
  549. widget=StaticSelect2()
  550. )
  551. comments = CommentField(
  552. widget=SmallTextarea
  553. )
  554. class Meta:
  555. nullable_fields = [
  556. 'group', 'tenant', 'role', 'serial', 'asset_tag', 'outer_width', 'outer_depth', 'outer_unit', 'comments',
  557. ]
  558. class RackFilterForm(BootstrapMixin, TenancyFilterForm, CustomFieldFilterForm):
  559. model = Rack
  560. field_order = ['q', 'site', 'group_id', 'status', 'role', 'tenant_group', 'tenant']
  561. q = forms.CharField(
  562. required=False,
  563. label='Search'
  564. )
  565. site = FilterChoiceField(
  566. queryset=Site.objects.all(),
  567. to_field_name='slug',
  568. widget=APISelectMultiple(
  569. api_url="/api/dcim/sites/",
  570. value_field="slug",
  571. filter_for={
  572. 'group_id': 'site'
  573. }
  574. )
  575. )
  576. group_id = FilterChoiceField(
  577. queryset=RackGroup.objects.prefetch_related(
  578. 'site'
  579. ),
  580. label='Rack group',
  581. null_label='-- None --',
  582. widget=APISelectMultiple(
  583. api_url="/api/dcim/rack-groups/",
  584. null_option=True
  585. )
  586. )
  587. status = forms.MultipleChoiceField(
  588. choices=RACK_STATUS_CHOICES,
  589. required=False,
  590. widget=StaticSelect2Multiple()
  591. )
  592. role = FilterChoiceField(
  593. queryset=RackRole.objects.all(),
  594. to_field_name='slug',
  595. null_label='-- None --',
  596. widget=APISelectMultiple(
  597. api_url="/api/dcim/rack-roles/",
  598. value_field="slug",
  599. null_option=True,
  600. )
  601. )
  602. #
  603. # Rack reservations
  604. #
  605. class RackReservationForm(BootstrapMixin, TenancyForm, forms.ModelForm):
  606. units = SimpleArrayField(
  607. base_field=forms.IntegerField(),
  608. widget=ArrayFieldSelectMultiple(
  609. attrs={
  610. 'size': 10,
  611. }
  612. )
  613. )
  614. user = forms.ModelChoiceField(
  615. queryset=User.objects.order_by(
  616. 'username'
  617. ),
  618. widget=StaticSelect2()
  619. )
  620. class Meta:
  621. model = RackReservation
  622. fields = [
  623. 'units', 'user', 'tenant_group', 'tenant', 'description',
  624. ]
  625. def __init__(self, *args, **kwargs):
  626. super().__init__(*args, **kwargs)
  627. # Populate rack unit choices
  628. self.fields['units'].widget.choices = self._get_unit_choices()
  629. def _get_unit_choices(self):
  630. rack = self.instance.rack
  631. reserved_units = []
  632. for resv in rack.reservations.exclude(pk=self.instance.pk):
  633. for u in resv.units:
  634. reserved_units.append(u)
  635. unit_choices = [(u, {'label': str(u), 'disabled': u in reserved_units}) for u in rack.units]
  636. return unit_choices
  637. class RackReservationBulkEditForm(BootstrapMixin, BulkEditForm):
  638. pk = forms.ModelMultipleChoiceField(
  639. queryset=RackReservation.objects.all(),
  640. widget=forms.MultipleHiddenInput()
  641. )
  642. user = forms.ModelChoiceField(
  643. queryset=User.objects.order_by(
  644. 'username'
  645. ),
  646. required=False,
  647. widget=StaticSelect2()
  648. )
  649. tenant = forms.ModelChoiceField(
  650. queryset=Tenant.objects.all(),
  651. required=False,
  652. widget=APISelect(
  653. api_url="/api/tenancy/tenant",
  654. )
  655. )
  656. description = forms.CharField(
  657. max_length=100,
  658. required=False
  659. )
  660. class Meta:
  661. nullable_fields = []
  662. class RackReservationFilterForm(BootstrapMixin, TenancyFilterForm):
  663. field_order = ['q', 'site', 'group_id', 'tenant_group', 'tenant']
  664. q = forms.CharField(
  665. required=False,
  666. label='Search'
  667. )
  668. site = FilterChoiceField(
  669. queryset=Site.objects.all(),
  670. to_field_name='slug',
  671. widget=APISelectMultiple(
  672. api_url="/api/dcim/sites/",
  673. value_field="slug",
  674. )
  675. )
  676. group_id = FilterChoiceField(
  677. queryset=RackGroup.objects.prefetch_related('site'),
  678. label='Rack group',
  679. null_label='-- None --',
  680. widget=APISelectMultiple(
  681. api_url="/api/dcim/rack-groups/",
  682. null_option=True,
  683. )
  684. )
  685. #
  686. # Manufacturers
  687. #
  688. class ManufacturerForm(BootstrapMixin, forms.ModelForm):
  689. slug = SlugField()
  690. class Meta:
  691. model = Manufacturer
  692. fields = [
  693. 'name', 'slug',
  694. ]
  695. class ManufacturerCSVForm(forms.ModelForm):
  696. class Meta:
  697. model = Manufacturer
  698. fields = Manufacturer.csv_headers
  699. help_texts = {
  700. 'name': 'Manufacturer name',
  701. 'slug': 'URL-friendly slug',
  702. }
  703. #
  704. # Device types
  705. #
  706. class DeviceTypeForm(BootstrapMixin, CustomFieldForm):
  707. slug = SlugField(
  708. slug_source='model'
  709. )
  710. comments = CommentField()
  711. tags = TagField(
  712. required=False
  713. )
  714. class Meta:
  715. model = DeviceType
  716. fields = [
  717. 'manufacturer', 'model', 'slug', 'part_number', 'u_height', 'is_full_depth', 'subdevice_role', 'comments',
  718. 'tags',
  719. ]
  720. widgets = {
  721. 'manufacturer': APISelect(
  722. api_url="/api/dcim/manufacturers/"
  723. ),
  724. 'subdevice_role': StaticSelect2()
  725. }
  726. class DeviceTypeCSVForm(forms.ModelForm):
  727. manufacturer = forms.ModelChoiceField(
  728. queryset=Manufacturer.objects.all(),
  729. required=True,
  730. to_field_name='name',
  731. help_text='Manufacturer name',
  732. error_messages={
  733. 'invalid_choice': 'Manufacturer not found.',
  734. }
  735. )
  736. subdevice_role = CSVChoiceField(
  737. choices=SUBDEVICE_ROLE_CHOICES,
  738. required=False,
  739. help_text='Parent/child status'
  740. )
  741. class Meta:
  742. model = DeviceType
  743. fields = DeviceType.csv_headers
  744. help_texts = {
  745. 'model': 'Model name',
  746. 'slug': 'URL-friendly slug',
  747. }
  748. class DeviceTypeBulkEditForm(BootstrapMixin, AddRemoveTagsForm, CustomFieldBulkEditForm):
  749. pk = forms.ModelMultipleChoiceField(
  750. queryset=DeviceType.objects.all(),
  751. widget=forms.MultipleHiddenInput()
  752. )
  753. manufacturer = forms.ModelChoiceField(
  754. queryset=Manufacturer.objects.all(),
  755. required=False,
  756. widget=APISelect(
  757. api_url="/api/dcim/manufactureres"
  758. )
  759. )
  760. u_height = forms.IntegerField(
  761. min_value=1,
  762. required=False
  763. )
  764. is_full_depth = forms.NullBooleanField(
  765. required=False,
  766. widget=BulkEditNullBooleanSelect(),
  767. label='Is full depth'
  768. )
  769. class Meta:
  770. nullable_fields = []
  771. class DeviceTypeFilterForm(BootstrapMixin, CustomFieldFilterForm):
  772. model = DeviceType
  773. q = forms.CharField(
  774. required=False,
  775. label='Search'
  776. )
  777. manufacturer = FilterChoiceField(
  778. queryset=Manufacturer.objects.all(),
  779. to_field_name='slug',
  780. widget=APISelectMultiple(
  781. api_url="/api/dcim/manufacturers/",
  782. value_field="slug",
  783. )
  784. )
  785. subdevice_role = forms.NullBooleanField(
  786. required=False,
  787. label='Subdevice role',
  788. widget=StaticSelect2(
  789. choices=add_blank_choice(SUBDEVICE_ROLE_CHOICES)
  790. )
  791. )
  792. console_ports = forms.NullBooleanField(
  793. required=False,
  794. label='Has console ports',
  795. widget=StaticSelect2(
  796. choices=BOOLEAN_WITH_BLANK_CHOICES
  797. )
  798. )
  799. console_server_ports = forms.NullBooleanField(
  800. required=False,
  801. label='Has console server ports',
  802. widget=StaticSelect2(
  803. choices=BOOLEAN_WITH_BLANK_CHOICES
  804. )
  805. )
  806. power_ports = forms.NullBooleanField(
  807. required=False,
  808. label='Has power ports',
  809. widget=StaticSelect2(
  810. choices=BOOLEAN_WITH_BLANK_CHOICES
  811. )
  812. )
  813. power_outlets = forms.NullBooleanField(
  814. required=False,
  815. label='Has power outlets',
  816. widget=StaticSelect2(
  817. choices=BOOLEAN_WITH_BLANK_CHOICES
  818. )
  819. )
  820. interfaces = forms.NullBooleanField(
  821. required=False,
  822. label='Has interfaces',
  823. widget=StaticSelect2(
  824. choices=BOOLEAN_WITH_BLANK_CHOICES
  825. )
  826. )
  827. pass_through_ports = forms.NullBooleanField(
  828. required=False,
  829. label='Has pass-through ports',
  830. widget=StaticSelect2(
  831. choices=BOOLEAN_WITH_BLANK_CHOICES
  832. )
  833. )
  834. #
  835. # Device component templates
  836. #
  837. class ConsolePortTemplateForm(BootstrapMixin, forms.ModelForm):
  838. class Meta:
  839. model = ConsolePortTemplate
  840. fields = [
  841. 'device_type', 'name',
  842. ]
  843. widgets = {
  844. 'device_type': forms.HiddenInput(),
  845. }
  846. class ConsolePortTemplateCreateForm(ComponentForm):
  847. name_pattern = ExpandableNameField(
  848. label='Name'
  849. )
  850. class ConsoleServerPortTemplateForm(BootstrapMixin, forms.ModelForm):
  851. class Meta:
  852. model = ConsoleServerPortTemplate
  853. fields = [
  854. 'device_type', 'name',
  855. ]
  856. widgets = {
  857. 'device_type': forms.HiddenInput(),
  858. }
  859. class ConsoleServerPortTemplateCreateForm(ComponentForm):
  860. name_pattern = ExpandableNameField(
  861. label='Name'
  862. )
  863. class PowerPortTemplateForm(BootstrapMixin, forms.ModelForm):
  864. class Meta:
  865. model = PowerPortTemplate
  866. fields = [
  867. 'device_type', 'name', 'maximum_draw', 'allocated_draw',
  868. ]
  869. widgets = {
  870. 'device_type': forms.HiddenInput(),
  871. }
  872. class PowerPortTemplateCreateForm(ComponentForm):
  873. name_pattern = ExpandableNameField(
  874. label='Name'
  875. )
  876. maximum_draw = forms.IntegerField(
  877. min_value=1,
  878. required=False,
  879. help_text="Maximum current draw (watts)"
  880. )
  881. allocated_draw = forms.IntegerField(
  882. min_value=1,
  883. required=False,
  884. help_text="Allocated current draw (watts)"
  885. )
  886. class PowerOutletTemplateForm(BootstrapMixin, forms.ModelForm):
  887. class Meta:
  888. model = PowerOutletTemplate
  889. fields = [
  890. 'device_type', 'name', 'power_port', 'feed_leg',
  891. ]
  892. widgets = {
  893. 'device_type': forms.HiddenInput(),
  894. }
  895. def __init__(self, *args, **kwargs):
  896. super().__init__(*args, **kwargs)
  897. # Limit power_port choices to current DeviceType
  898. if hasattr(self.instance, 'device_type'):
  899. self.fields['power_port'].queryset = PowerPortTemplate.objects.filter(
  900. device_type=self.instance.device_type
  901. )
  902. class PowerOutletTemplateCreateForm(ComponentForm):
  903. name_pattern = ExpandableNameField(
  904. label='Name'
  905. )
  906. power_port = forms.ModelChoiceField(
  907. queryset=PowerPortTemplate.objects.all(),
  908. required=False
  909. )
  910. feed_leg = forms.ChoiceField(
  911. choices=add_blank_choice(POWERFEED_LEG_CHOICES),
  912. required=False,
  913. widget=StaticSelect2()
  914. )
  915. def __init__(self, *args, **kwargs):
  916. super().__init__(*args, **kwargs)
  917. # Limit power_port choices to current DeviceType
  918. self.fields['power_port'].queryset = PowerPortTemplate.objects.filter(
  919. device_type=self.parent
  920. )
  921. class InterfaceTemplateForm(BootstrapMixin, forms.ModelForm):
  922. class Meta:
  923. model = InterfaceTemplate
  924. fields = [
  925. 'device_type', 'name', 'type', 'mgmt_only',
  926. ]
  927. widgets = {
  928. 'device_type': forms.HiddenInput(),
  929. 'type': StaticSelect2(),
  930. }
  931. class InterfaceTemplateCreateForm(ComponentForm):
  932. name_pattern = ExpandableNameField(
  933. label='Name'
  934. )
  935. type = forms.ChoiceField(
  936. choices=IFACE_TYPE_CHOICES,
  937. widget=StaticSelect2()
  938. )
  939. mgmt_only = forms.BooleanField(
  940. required=False,
  941. label='Management only'
  942. )
  943. class InterfaceTemplateBulkEditForm(BootstrapMixin, BulkEditForm):
  944. pk = forms.ModelMultipleChoiceField(
  945. queryset=InterfaceTemplate.objects.all(),
  946. widget=forms.MultipleHiddenInput()
  947. )
  948. type = forms.ChoiceField(
  949. choices=add_blank_choice(IFACE_TYPE_CHOICES),
  950. required=False,
  951. widget=StaticSelect2()
  952. )
  953. mgmt_only = forms.NullBooleanField(
  954. required=False,
  955. widget=BulkEditNullBooleanSelect,
  956. label='Management only'
  957. )
  958. class Meta:
  959. nullable_fields = []
  960. class FrontPortTemplateForm(BootstrapMixin, forms.ModelForm):
  961. class Meta:
  962. model = FrontPortTemplate
  963. fields = [
  964. 'device_type', 'name', 'type', 'rear_port', 'rear_port_position',
  965. ]
  966. widgets = {
  967. 'device_type': forms.HiddenInput(),
  968. 'rear_port': StaticSelect2(),
  969. }
  970. def __init__(self, *args, **kwargs):
  971. super().__init__(*args, **kwargs)
  972. # Limit rear_port choices to current DeviceType
  973. if hasattr(self.instance, 'device_type'):
  974. self.fields['rear_port'].queryset = RearPortTemplate.objects.filter(
  975. device_type=self.instance.device_type
  976. )
  977. class FrontPortTemplateCreateForm(ComponentForm):
  978. name_pattern = ExpandableNameField(
  979. label='Name'
  980. )
  981. type = forms.ChoiceField(
  982. choices=PORT_TYPE_CHOICES,
  983. widget=StaticSelect2()
  984. )
  985. rear_port_set = forms.MultipleChoiceField(
  986. choices=[],
  987. label='Rear ports',
  988. help_text='Select one rear port assignment for each front port being created.',
  989. )
  990. def __init__(self, *args, **kwargs):
  991. super().__init__(*args, **kwargs)
  992. # Determine which rear port positions are occupied. These will be excluded from the list of available mappings.
  993. occupied_port_positions = [
  994. (front_port.rear_port_id, front_port.rear_port_position)
  995. for front_port in self.parent.frontport_templates.all()
  996. ]
  997. # Populate rear port choices
  998. choices = []
  999. rear_ports = RearPortTemplate.objects.filter(device_type=self.parent)
  1000. for rear_port in rear_ports:
  1001. for i in range(1, rear_port.positions + 1):
  1002. if (rear_port.pk, i) not in occupied_port_positions:
  1003. choices.append(
  1004. ('{}:{}'.format(rear_port.pk, i), '{}:{}'.format(rear_port.name, i))
  1005. )
  1006. self.fields['rear_port_set'].choices = choices
  1007. def clean(self):
  1008. # Validate that the number of ports being created equals the number of selected (rear port, position) tuples
  1009. front_port_count = len(self.cleaned_data['name_pattern'])
  1010. rear_port_count = len(self.cleaned_data['rear_port_set'])
  1011. if front_port_count != rear_port_count:
  1012. raise forms.ValidationError({
  1013. 'rear_port_set': 'The provided name pattern will create {} ports, however {} rear port assignments '
  1014. 'were selected. These counts must match.'.format(front_port_count, rear_port_count)
  1015. })
  1016. def get_iterative_data(self, iteration):
  1017. # Assign rear port and position from selected set
  1018. rear_port, position = self.cleaned_data['rear_port_set'][iteration].split(':')
  1019. return {
  1020. 'rear_port': int(rear_port),
  1021. 'rear_port_position': int(position),
  1022. }
  1023. class RearPortTemplateForm(BootstrapMixin, forms.ModelForm):
  1024. class Meta:
  1025. model = RearPortTemplate
  1026. fields = [
  1027. 'device_type', 'name', 'type', 'positions',
  1028. ]
  1029. widgets = {
  1030. 'device_type': forms.HiddenInput(),
  1031. 'type': StaticSelect2(),
  1032. }
  1033. class RearPortTemplateCreateForm(ComponentForm):
  1034. name_pattern = ExpandableNameField(
  1035. label='Name'
  1036. )
  1037. type = forms.ChoiceField(
  1038. choices=PORT_TYPE_CHOICES,
  1039. widget=StaticSelect2(),
  1040. )
  1041. positions = forms.IntegerField(
  1042. min_value=1,
  1043. max_value=64,
  1044. initial=1,
  1045. help_text='The number of front ports which may be mapped to each rear port'
  1046. )
  1047. class DeviceBayTemplateForm(BootstrapMixin, forms.ModelForm):
  1048. class Meta:
  1049. model = DeviceBayTemplate
  1050. fields = [
  1051. 'device_type', 'name',
  1052. ]
  1053. widgets = {
  1054. 'device_type': forms.HiddenInput(),
  1055. }
  1056. class DeviceBayTemplateCreateForm(ComponentForm):
  1057. name_pattern = ExpandableNameField(
  1058. label='Name'
  1059. )
  1060. #
  1061. # Device roles
  1062. #
  1063. class DeviceRoleForm(BootstrapMixin, forms.ModelForm):
  1064. slug = SlugField()
  1065. class Meta:
  1066. model = DeviceRole
  1067. fields = [
  1068. 'name', 'slug', 'color', 'vm_role',
  1069. ]
  1070. class DeviceRoleCSVForm(forms.ModelForm):
  1071. slug = SlugField()
  1072. class Meta:
  1073. model = DeviceRole
  1074. fields = DeviceRole.csv_headers
  1075. help_texts = {
  1076. 'name': 'Name of device role',
  1077. 'color': 'RGB color in hexadecimal (e.g. 00ff00)'
  1078. }
  1079. #
  1080. # Platforms
  1081. #
  1082. class PlatformForm(BootstrapMixin, forms.ModelForm):
  1083. slug = SlugField(
  1084. max_length=64
  1085. )
  1086. class Meta:
  1087. model = Platform
  1088. fields = [
  1089. 'name', 'slug', 'manufacturer', 'napalm_driver', 'napalm_args',
  1090. ]
  1091. widgets = {
  1092. 'manufacturer': APISelect(
  1093. api_url="/api/dcim/manufacturers/"
  1094. ),
  1095. 'napalm_args': SmallTextarea(),
  1096. }
  1097. class PlatformCSVForm(forms.ModelForm):
  1098. slug = SlugField()
  1099. manufacturer = forms.ModelChoiceField(
  1100. queryset=Manufacturer.objects.all(),
  1101. required=False,
  1102. to_field_name='name',
  1103. help_text='Manufacturer name',
  1104. error_messages={
  1105. 'invalid_choice': 'Manufacturer not found.',
  1106. }
  1107. )
  1108. class Meta:
  1109. model = Platform
  1110. fields = Platform.csv_headers
  1111. help_texts = {
  1112. 'name': 'Platform name',
  1113. }
  1114. #
  1115. # Devices
  1116. #
  1117. class DeviceForm(BootstrapMixin, TenancyForm, CustomFieldForm):
  1118. site = forms.ModelChoiceField(
  1119. queryset=Site.objects.all(),
  1120. widget=APISelect(
  1121. api_url="/api/dcim/sites/",
  1122. filter_for={
  1123. 'rack': 'site_id'
  1124. }
  1125. )
  1126. )
  1127. rack = ChainedModelChoiceField(
  1128. queryset=Rack.objects.all(),
  1129. chains=(
  1130. ('site', 'site'),
  1131. ),
  1132. required=False,
  1133. widget=APISelect(
  1134. api_url='/api/dcim/racks/',
  1135. display_field='display_name'
  1136. )
  1137. )
  1138. position = forms.TypedChoiceField(
  1139. required=False,
  1140. empty_value=None,
  1141. help_text="The lowest-numbered unit occupied by the device",
  1142. widget=APISelect(
  1143. api_url='/api/dcim/racks/{{rack}}/units/',
  1144. disabled_indicator='device'
  1145. )
  1146. )
  1147. manufacturer = forms.ModelChoiceField(
  1148. queryset=Manufacturer.objects.all(),
  1149. widget=APISelect(
  1150. api_url="/api/dcim/manufacturers/",
  1151. filter_for={
  1152. 'device_type': 'manufacturer_id',
  1153. 'platform': 'manufacturer_id'
  1154. }
  1155. )
  1156. )
  1157. device_type = ChainedModelChoiceField(
  1158. queryset=DeviceType.objects.all(),
  1159. chains=(
  1160. ('manufacturer', 'manufacturer'),
  1161. ),
  1162. label='Device type',
  1163. widget=APISelect(
  1164. api_url='/api/dcim/device-types/',
  1165. display_field='model'
  1166. )
  1167. )
  1168. cluster_group = forms.ModelChoiceField(
  1169. queryset=ClusterGroup.objects.all(),
  1170. required=False,
  1171. widget=APISelect(
  1172. api_url="/api/virtualization/cluster-groups/",
  1173. filter_for={
  1174. 'cluster': 'group_id'
  1175. },
  1176. attrs={
  1177. 'nullable': 'true'
  1178. }
  1179. )
  1180. )
  1181. cluster = ChainedModelChoiceField(
  1182. queryset=Cluster.objects.all(),
  1183. chains=(
  1184. ('group', 'cluster_group'),
  1185. ),
  1186. required=False,
  1187. widget=APISelect(
  1188. api_url='/api/virtualization/clusters/',
  1189. )
  1190. )
  1191. comments = CommentField()
  1192. tags = TagField(required=False)
  1193. local_context_data = JSONField(
  1194. required=False,
  1195. label=''
  1196. )
  1197. class Meta:
  1198. model = Device
  1199. fields = [
  1200. 'name', 'device_role', 'device_type', 'serial', 'asset_tag', 'site', 'rack', 'position', 'face',
  1201. 'status', 'platform', 'primary_ip4', 'primary_ip6', 'cluster_group', 'cluster', 'tenant_group', 'tenant',
  1202. 'comments', 'tags', 'local_context_data'
  1203. ]
  1204. help_texts = {
  1205. 'device_role': "The function this device serves",
  1206. 'serial': "Chassis serial number",
  1207. 'local_context_data': "Local config context data overwrites all source contexts in the final rendered "
  1208. "config context",
  1209. }
  1210. widgets = {
  1211. 'face': StaticSelect2(
  1212. filter_for={
  1213. 'position': 'face'
  1214. }
  1215. ),
  1216. 'device_role': APISelect(
  1217. api_url='/api/dcim/device-roles/'
  1218. ),
  1219. 'status': StaticSelect2(),
  1220. 'platform': APISelect(
  1221. api_url="/api/dcim/platforms/",
  1222. additional_query_params={
  1223. "manufacturer_id": "null"
  1224. }
  1225. ),
  1226. 'primary_ip4': StaticSelect2(),
  1227. 'primary_ip6': StaticSelect2(),
  1228. }
  1229. def __init__(self, *args, **kwargs):
  1230. # Initialize helper selectors
  1231. instance = kwargs.get('instance')
  1232. if 'initial' not in kwargs:
  1233. kwargs['initial'] = {}
  1234. # Using hasattr() instead of "is not None" to avoid RelatedObjectDoesNotExist on required field
  1235. if instance and hasattr(instance, 'device_type'):
  1236. kwargs['initial']['manufacturer'] = instance.device_type.manufacturer
  1237. if instance and instance.cluster is not None:
  1238. kwargs['initial']['cluster_group'] = instance.cluster.group
  1239. super().__init__(*args, **kwargs)
  1240. if self.instance.pk:
  1241. # Compile list of choices for primary IPv4 and IPv6 addresses
  1242. for family in [4, 6]:
  1243. ip_choices = [(None, '---------')]
  1244. # Gather PKs of all interfaces belonging to this Device or a peer VirtualChassis member
  1245. interface_ids = self.instance.vc_interfaces.values('pk')
  1246. # Collect interface IPs
  1247. interface_ips = IPAddress.objects.prefetch_related('interface').filter(
  1248. family=family, interface_id__in=interface_ids
  1249. )
  1250. if interface_ips:
  1251. ip_list = [(ip.id, '{} ({})'.format(ip.address, ip.interface)) for ip in interface_ips]
  1252. ip_choices.append(('Interface IPs', ip_list))
  1253. # Collect NAT IPs
  1254. nat_ips = IPAddress.objects.prefetch_related('nat_inside').filter(
  1255. family=family, nat_inside__interface__in=interface_ids
  1256. )
  1257. if nat_ips:
  1258. ip_list = [(ip.id, '{} ({})'.format(ip.address, ip.nat_inside.address)) for ip in nat_ips]
  1259. ip_choices.append(('NAT IPs', ip_list))
  1260. self.fields['primary_ip{}'.format(family)].choices = ip_choices
  1261. # If editing an existing device, exclude it from the list of occupied rack units. This ensures that a device
  1262. # can be flipped from one face to another.
  1263. self.fields['position'].widget.add_additional_query_param('exclude', self.instance.pk)
  1264. # Limit platform by manufacturer
  1265. self.fields['platform'].queryset = Platform.objects.filter(
  1266. Q(manufacturer__isnull=True) | Q(manufacturer=self.instance.device_type.manufacturer)
  1267. )
  1268. else:
  1269. # An object that doesn't exist yet can't have any IPs assigned to it
  1270. self.fields['primary_ip4'].choices = []
  1271. self.fields['primary_ip4'].widget.attrs['readonly'] = True
  1272. self.fields['primary_ip6'].choices = []
  1273. self.fields['primary_ip6'].widget.attrs['readonly'] = True
  1274. # Rack position
  1275. pk = self.instance.pk if self.instance.pk else None
  1276. try:
  1277. if self.is_bound and self.data.get('rack') and str(self.data.get('face')):
  1278. position_choices = Rack.objects.get(pk=self.data['rack']) \
  1279. .get_rack_units(face=self.data.get('face'), exclude=pk)
  1280. elif self.initial.get('rack') and str(self.initial.get('face')):
  1281. position_choices = Rack.objects.get(pk=self.initial['rack']) \
  1282. .get_rack_units(face=self.initial.get('face'), exclude=pk)
  1283. else:
  1284. position_choices = []
  1285. except Rack.DoesNotExist:
  1286. position_choices = []
  1287. self.fields['position'].choices = [('', '---------')] + [
  1288. (p['id'], {
  1289. 'label': p['name'],
  1290. 'disabled': bool(p['device'] and p['id'] != self.initial.get('position')),
  1291. }) for p in position_choices
  1292. ]
  1293. # Disable rack assignment if this is a child device installed in a parent device
  1294. if pk and self.instance.device_type.is_child_device and hasattr(self.instance, 'parent_bay'):
  1295. self.fields['site'].disabled = True
  1296. self.fields['rack'].disabled = True
  1297. self.initial['site'] = self.instance.parent_bay.device.site_id
  1298. self.initial['rack'] = self.instance.parent_bay.device.rack_id
  1299. class BaseDeviceCSVForm(forms.ModelForm):
  1300. device_role = forms.ModelChoiceField(
  1301. queryset=DeviceRole.objects.all(),
  1302. to_field_name='name',
  1303. help_text='Name of assigned role',
  1304. error_messages={
  1305. 'invalid_choice': 'Invalid device role.',
  1306. }
  1307. )
  1308. tenant = forms.ModelChoiceField(
  1309. queryset=Tenant.objects.all(),
  1310. required=False,
  1311. to_field_name='name',
  1312. help_text='Name of assigned tenant',
  1313. error_messages={
  1314. 'invalid_choice': 'Tenant not found.',
  1315. }
  1316. )
  1317. manufacturer = forms.ModelChoiceField(
  1318. queryset=Manufacturer.objects.all(),
  1319. to_field_name='name',
  1320. help_text='Device type manufacturer',
  1321. error_messages={
  1322. 'invalid_choice': 'Invalid manufacturer.',
  1323. }
  1324. )
  1325. model_name = forms.CharField(
  1326. help_text='Device type model name'
  1327. )
  1328. platform = forms.ModelChoiceField(
  1329. queryset=Platform.objects.all(),
  1330. required=False,
  1331. to_field_name='name',
  1332. help_text='Name of assigned platform',
  1333. error_messages={
  1334. 'invalid_choice': 'Invalid platform.',
  1335. }
  1336. )
  1337. status = CSVChoiceField(
  1338. choices=DEVICE_STATUS_CHOICES,
  1339. help_text='Operational status'
  1340. )
  1341. class Meta:
  1342. fields = []
  1343. model = Device
  1344. help_texts = {
  1345. 'name': 'Device name',
  1346. }
  1347. def clean(self):
  1348. super().clean()
  1349. manufacturer = self.cleaned_data.get('manufacturer')
  1350. model_name = self.cleaned_data.get('model_name')
  1351. # Validate device type
  1352. if manufacturer and model_name:
  1353. try:
  1354. self.instance.device_type = DeviceType.objects.get(manufacturer=manufacturer, model=model_name)
  1355. except DeviceType.DoesNotExist:
  1356. raise forms.ValidationError("Device type {} {} not found".format(manufacturer, model_name))
  1357. class DeviceCSVForm(BaseDeviceCSVForm):
  1358. site = forms.ModelChoiceField(
  1359. queryset=Site.objects.all(),
  1360. to_field_name='name',
  1361. help_text='Name of parent site',
  1362. error_messages={
  1363. 'invalid_choice': 'Invalid site name.',
  1364. }
  1365. )
  1366. rack_group = forms.CharField(
  1367. required=False,
  1368. help_text='Parent rack\'s group (if any)'
  1369. )
  1370. rack_name = forms.CharField(
  1371. required=False,
  1372. help_text='Name of parent rack'
  1373. )
  1374. face = CSVChoiceField(
  1375. choices=RACK_FACE_CHOICES,
  1376. required=False,
  1377. help_text='Mounted rack face'
  1378. )
  1379. cluster = forms.ModelChoiceField(
  1380. queryset=Cluster.objects.all(),
  1381. to_field_name='name',
  1382. required=False,
  1383. help_text='Virtualization cluster',
  1384. error_messages={
  1385. 'invalid_choice': 'Invalid cluster name.',
  1386. }
  1387. )
  1388. class Meta(BaseDeviceCSVForm.Meta):
  1389. fields = [
  1390. 'name', 'device_role', 'tenant', 'manufacturer', 'model_name', 'platform', 'serial', 'asset_tag', 'status',
  1391. 'site', 'rack_group', 'rack_name', 'position', 'face', 'cluster', 'comments',
  1392. ]
  1393. def clean(self):
  1394. super().clean()
  1395. site = self.cleaned_data.get('site')
  1396. rack_group = self.cleaned_data.get('rack_group')
  1397. rack_name = self.cleaned_data.get('rack_name')
  1398. # Validate rack
  1399. if site and rack_group and rack_name:
  1400. try:
  1401. self.instance.rack = Rack.objects.get(site=site, group__name=rack_group, name=rack_name)
  1402. except Rack.DoesNotExist:
  1403. raise forms.ValidationError("Rack {} not found in site {} group {}".format(rack_name, site, rack_group))
  1404. elif site and rack_name:
  1405. try:
  1406. self.instance.rack = Rack.objects.get(site=site, group__isnull=True, name=rack_name)
  1407. except Rack.DoesNotExist:
  1408. raise forms.ValidationError("Rack {} not found in site {} (no group)".format(rack_name, site))
  1409. class ChildDeviceCSVForm(BaseDeviceCSVForm):
  1410. parent = FlexibleModelChoiceField(
  1411. queryset=Device.objects.all(),
  1412. to_field_name='name',
  1413. help_text='Name or ID of parent device',
  1414. error_messages={
  1415. 'invalid_choice': 'Parent device not found.',
  1416. }
  1417. )
  1418. device_bay_name = forms.CharField(
  1419. help_text='Name of device bay',
  1420. )
  1421. cluster = forms.ModelChoiceField(
  1422. queryset=Cluster.objects.all(),
  1423. to_field_name='name',
  1424. required=False,
  1425. help_text='Virtualization cluster',
  1426. error_messages={
  1427. 'invalid_choice': 'Invalid cluster name.',
  1428. }
  1429. )
  1430. class Meta(BaseDeviceCSVForm.Meta):
  1431. fields = [
  1432. 'name', 'device_role', 'tenant', 'manufacturer', 'model_name', 'platform', 'serial', 'asset_tag', 'status',
  1433. 'parent', 'device_bay_name', 'cluster', 'comments',
  1434. ]
  1435. def clean(self):
  1436. super().clean()
  1437. parent = self.cleaned_data.get('parent')
  1438. device_bay_name = self.cleaned_data.get('device_bay_name')
  1439. # Validate device bay
  1440. if parent and device_bay_name:
  1441. try:
  1442. self.instance.parent_bay = DeviceBay.objects.get(device=parent, name=device_bay_name)
  1443. # Inherit site and rack from parent device
  1444. self.instance.site = parent.site
  1445. self.instance.rack = parent.rack
  1446. except DeviceBay.DoesNotExist:
  1447. raise forms.ValidationError("Parent device/bay ({} {}) not found".format(parent, device_bay_name))
  1448. class DeviceBulkEditForm(BootstrapMixin, AddRemoveTagsForm, CustomFieldBulkEditForm):
  1449. pk = forms.ModelMultipleChoiceField(
  1450. queryset=Device.objects.all(),
  1451. widget=forms.MultipleHiddenInput()
  1452. )
  1453. device_type = forms.ModelChoiceField(
  1454. queryset=DeviceType.objects.all(),
  1455. required=False,
  1456. label='Type',
  1457. widget=APISelect(
  1458. api_url="/api/dcim/device-types/",
  1459. display_field='display_name'
  1460. )
  1461. )
  1462. device_role = forms.ModelChoiceField(
  1463. queryset=DeviceRole.objects.all(),
  1464. required=False,
  1465. label='Role',
  1466. widget=APISelect(
  1467. api_url="/api/dcim/device-roles/"
  1468. )
  1469. )
  1470. tenant = forms.ModelChoiceField(
  1471. queryset=Tenant.objects.all(),
  1472. required=False,
  1473. widget=APISelect(
  1474. api_url="/api/tenancy/tenants/"
  1475. )
  1476. )
  1477. platform = forms.ModelChoiceField(
  1478. queryset=Platform.objects.all(),
  1479. required=False,
  1480. widget=APISelect(
  1481. api_url="/api/dcim/platforms/"
  1482. )
  1483. )
  1484. status = forms.ChoiceField(
  1485. choices=add_blank_choice(DEVICE_STATUS_CHOICES),
  1486. required=False,
  1487. initial='',
  1488. widget=StaticSelect2()
  1489. )
  1490. serial = forms.CharField(
  1491. max_length=50,
  1492. required=False,
  1493. label='Serial Number'
  1494. )
  1495. class Meta:
  1496. nullable_fields = [
  1497. 'tenant', 'platform', 'serial',
  1498. ]
  1499. class DeviceFilterForm(BootstrapMixin, LocalConfigContextFilterForm, TenancyFilterForm, CustomFieldFilterForm):
  1500. model = Device
  1501. field_order = [
  1502. 'q', 'region', 'site', 'group_id', 'rack_id', 'status', 'role', 'tenant_group', 'tenant',
  1503. 'manufacturer_id', 'device_type_id', 'mac_address', 'has_primary_ip',
  1504. ]
  1505. q = forms.CharField(
  1506. required=False,
  1507. label='Search'
  1508. )
  1509. region = FilterChoiceField(
  1510. queryset=Region.objects.all(),
  1511. to_field_name='slug',
  1512. required=False,
  1513. widget=APISelectMultiple(
  1514. api_url="/api/dcim/regions/",
  1515. value_field="slug",
  1516. filter_for={
  1517. 'site': 'region'
  1518. }
  1519. )
  1520. )
  1521. site = FilterChoiceField(
  1522. queryset=Site.objects.all(),
  1523. to_field_name='slug',
  1524. widget=APISelectMultiple(
  1525. api_url="/api/dcim/sites/",
  1526. value_field="slug",
  1527. filter_for={
  1528. 'group_id': 'site',
  1529. 'rack_id': 'site',
  1530. }
  1531. )
  1532. )
  1533. group_id = FilterChoiceField(
  1534. queryset=RackGroup.objects.prefetch_related(
  1535. 'site'
  1536. ),
  1537. label='Rack group',
  1538. widget=APISelectMultiple(
  1539. api_url="/api/dcim/rack-groups/",
  1540. filter_for={
  1541. 'rack_id': 'group_id',
  1542. }
  1543. )
  1544. )
  1545. rack_id = FilterChoiceField(
  1546. queryset=Rack.objects.all(),
  1547. label='Rack',
  1548. null_label='-- None --',
  1549. widget=APISelectMultiple(
  1550. api_url="/api/dcim/racks/",
  1551. null_option=True,
  1552. )
  1553. )
  1554. role = FilterChoiceField(
  1555. queryset=DeviceRole.objects.all(),
  1556. to_field_name='slug',
  1557. widget=APISelectMultiple(
  1558. api_url="/api/dcim/device-roles/",
  1559. value_field="slug",
  1560. )
  1561. )
  1562. manufacturer_id = FilterChoiceField(
  1563. queryset=Manufacturer.objects.all(),
  1564. label='Manufacturer',
  1565. widget=APISelectMultiple(
  1566. api_url="/api/dcim/manufacturers/",
  1567. filter_for={
  1568. 'device_type_id': 'manufacturer_id',
  1569. }
  1570. )
  1571. )
  1572. device_type_id = FilterChoiceField(
  1573. queryset=DeviceType.objects.prefetch_related(
  1574. 'manufacturer'
  1575. ),
  1576. label='Model',
  1577. widget=APISelectMultiple(
  1578. api_url="/api/dcim/device-types/",
  1579. display_field="model",
  1580. )
  1581. )
  1582. platform = FilterChoiceField(
  1583. queryset=Platform.objects.all(),
  1584. to_field_name='slug',
  1585. null_label='-- None --',
  1586. widget=APISelectMultiple(
  1587. api_url="/api/dcim/platforms/",
  1588. value_field="slug",
  1589. null_option=True,
  1590. )
  1591. )
  1592. status = forms.MultipleChoiceField(
  1593. choices=DEVICE_STATUS_CHOICES,
  1594. required=False,
  1595. widget=StaticSelect2Multiple()
  1596. )
  1597. mac_address = forms.CharField(
  1598. required=False,
  1599. label='MAC address'
  1600. )
  1601. has_primary_ip = forms.NullBooleanField(
  1602. required=False,
  1603. label='Has a primary IP',
  1604. widget=StaticSelect2(
  1605. choices=BOOLEAN_WITH_BLANK_CHOICES
  1606. )
  1607. )
  1608. virtual_chassis_member = forms.NullBooleanField(
  1609. required=False,
  1610. label='Virtual chassis member',
  1611. widget=StaticSelect2(
  1612. choices=BOOLEAN_WITH_BLANK_CHOICES
  1613. )
  1614. )
  1615. console_ports = forms.NullBooleanField(
  1616. required=False,
  1617. label='Has console ports',
  1618. widget=StaticSelect2(
  1619. choices=BOOLEAN_WITH_BLANK_CHOICES
  1620. )
  1621. )
  1622. console_server_ports = forms.NullBooleanField(
  1623. required=False,
  1624. label='Has console server ports',
  1625. widget=StaticSelect2(
  1626. choices=BOOLEAN_WITH_BLANK_CHOICES
  1627. )
  1628. )
  1629. power_ports = forms.NullBooleanField(
  1630. required=False,
  1631. label='Has power ports',
  1632. widget=StaticSelect2(
  1633. choices=BOOLEAN_WITH_BLANK_CHOICES
  1634. )
  1635. )
  1636. power_outlets = forms.NullBooleanField(
  1637. required=False,
  1638. label='Has power outlets',
  1639. widget=StaticSelect2(
  1640. choices=BOOLEAN_WITH_BLANK_CHOICES
  1641. )
  1642. )
  1643. interfaces = forms.NullBooleanField(
  1644. required=False,
  1645. label='Has interfaces',
  1646. widget=StaticSelect2(
  1647. choices=BOOLEAN_WITH_BLANK_CHOICES
  1648. )
  1649. )
  1650. pass_through_ports = forms.NullBooleanField(
  1651. required=False,
  1652. label='Has pass-through ports',
  1653. widget=StaticSelect2(
  1654. choices=BOOLEAN_WITH_BLANK_CHOICES
  1655. )
  1656. )
  1657. #
  1658. # Bulk device component creation
  1659. #
  1660. class DeviceBulkAddComponentForm(BootstrapMixin, forms.Form):
  1661. pk = forms.ModelMultipleChoiceField(
  1662. queryset=Device.objects.all(),
  1663. widget=forms.MultipleHiddenInput()
  1664. )
  1665. name_pattern = ExpandableNameField(
  1666. label='Name'
  1667. )
  1668. class DeviceBulkAddInterfaceForm(DeviceBulkAddComponentForm):
  1669. type = forms.ChoiceField(
  1670. choices=IFACE_TYPE_CHOICES,
  1671. widget=StaticSelect2()
  1672. )
  1673. enabled = forms.BooleanField(
  1674. required=False,
  1675. initial=True
  1676. )
  1677. mtu = forms.IntegerField(
  1678. required=False,
  1679. min_value=1,
  1680. max_value=32767,
  1681. label='MTU'
  1682. )
  1683. mgmt_only = forms.BooleanField(
  1684. required=False,
  1685. label='Management only'
  1686. )
  1687. description = forms.CharField(
  1688. max_length=100,
  1689. required=False
  1690. )
  1691. #
  1692. # Console ports
  1693. #
  1694. class ConsolePortForm(BootstrapMixin, forms.ModelForm):
  1695. tags = TagField(
  1696. required=False
  1697. )
  1698. class Meta:
  1699. model = ConsolePort
  1700. fields = [
  1701. 'device', 'name', 'description', 'tags',
  1702. ]
  1703. widgets = {
  1704. 'device': forms.HiddenInput(),
  1705. }
  1706. class ConsolePortCreateForm(ComponentForm):
  1707. name_pattern = ExpandableNameField(
  1708. label='Name'
  1709. )
  1710. description = forms.CharField(
  1711. max_length=100,
  1712. required=False
  1713. )
  1714. tags = TagField(
  1715. required=False
  1716. )
  1717. #
  1718. # Console server ports
  1719. #
  1720. class ConsoleServerPortForm(BootstrapMixin, forms.ModelForm):
  1721. tags = TagField(
  1722. required=False
  1723. )
  1724. class Meta:
  1725. model = ConsoleServerPort
  1726. fields = [
  1727. 'device', 'name', 'description', 'tags',
  1728. ]
  1729. widgets = {
  1730. 'device': forms.HiddenInput(),
  1731. }
  1732. class ConsoleServerPortCreateForm(ComponentForm):
  1733. name_pattern = ExpandableNameField(
  1734. label='Name'
  1735. )
  1736. description = forms.CharField(
  1737. max_length=100,
  1738. required=False
  1739. )
  1740. tags = TagField(
  1741. required=False
  1742. )
  1743. class ConsoleServerPortBulkEditForm(BootstrapMixin, AddRemoveTagsForm, BulkEditForm):
  1744. pk = forms.ModelMultipleChoiceField(
  1745. queryset=ConsoleServerPort.objects.all(),
  1746. widget=forms.MultipleHiddenInput()
  1747. )
  1748. description = forms.CharField(
  1749. max_length=100,
  1750. required=False
  1751. )
  1752. class Meta:
  1753. nullable_fields = [
  1754. 'description',
  1755. ]
  1756. class ConsoleServerPortBulkRenameForm(BulkRenameForm):
  1757. pk = forms.ModelMultipleChoiceField(
  1758. queryset=ConsoleServerPort.objects.all(),
  1759. widget=forms.MultipleHiddenInput()
  1760. )
  1761. class ConsoleServerPortBulkDisconnectForm(ConfirmationForm):
  1762. pk = forms.ModelMultipleChoiceField(
  1763. queryset=ConsoleServerPort.objects.all(),
  1764. widget=forms.MultipleHiddenInput()
  1765. )
  1766. #
  1767. # Power ports
  1768. #
  1769. class PowerPortForm(BootstrapMixin, forms.ModelForm):
  1770. tags = TagField(
  1771. required=False
  1772. )
  1773. class Meta:
  1774. model = PowerPort
  1775. fields = [
  1776. 'device', 'name', 'maximum_draw', 'allocated_draw', 'description', 'tags',
  1777. ]
  1778. widgets = {
  1779. 'device': forms.HiddenInput(),
  1780. }
  1781. class PowerPortCreateForm(ComponentForm):
  1782. name_pattern = ExpandableNameField(
  1783. label='Name'
  1784. )
  1785. maximum_draw = forms.IntegerField(
  1786. min_value=1,
  1787. required=False,
  1788. help_text="Maximum draw in watts"
  1789. )
  1790. allocated_draw = forms.IntegerField(
  1791. min_value=1,
  1792. required=False,
  1793. help_text="Allocated draw in watts"
  1794. )
  1795. description = forms.CharField(
  1796. max_length=100,
  1797. required=False
  1798. )
  1799. tags = TagField(
  1800. required=False
  1801. )
  1802. #
  1803. # Power outlets
  1804. #
  1805. class PowerOutletForm(BootstrapMixin, forms.ModelForm):
  1806. power_port = forms.ModelChoiceField(
  1807. queryset=PowerPort.objects.all(),
  1808. required=False
  1809. )
  1810. tags = TagField(
  1811. required=False
  1812. )
  1813. class Meta:
  1814. model = PowerOutlet
  1815. fields = [
  1816. 'device', 'name', 'power_port', 'feed_leg', 'description', 'tags',
  1817. ]
  1818. widgets = {
  1819. 'device': forms.HiddenInput(),
  1820. }
  1821. def __init__(self, *args, **kwargs):
  1822. super().__init__(*args, **kwargs)
  1823. # Limit power_port choices to the local device
  1824. if hasattr(self.instance, 'device'):
  1825. self.fields['power_port'].queryset = PowerPort.objects.filter(
  1826. device=self.instance.device
  1827. )
  1828. class PowerOutletCreateForm(ComponentForm):
  1829. name_pattern = ExpandableNameField(
  1830. label='Name'
  1831. )
  1832. power_port = forms.ModelChoiceField(
  1833. queryset=PowerPort.objects.all(),
  1834. required=False
  1835. )
  1836. feed_leg = forms.ChoiceField(
  1837. choices=add_blank_choice(POWERFEED_LEG_CHOICES),
  1838. required=False
  1839. )
  1840. description = forms.CharField(
  1841. max_length=100,
  1842. required=False
  1843. )
  1844. tags = TagField(
  1845. required=False
  1846. )
  1847. def __init__(self, *args, **kwargs):
  1848. super().__init__(*args, **kwargs)
  1849. # Limit power_port choices to those on the parent device
  1850. self.fields['power_port'].queryset = PowerPort.objects.filter(device=self.parent)
  1851. class PowerOutletBulkEditForm(BootstrapMixin, AddRemoveTagsForm, BulkEditForm):
  1852. pk = forms.ModelMultipleChoiceField(
  1853. queryset=PowerOutlet.objects.all(),
  1854. widget=forms.MultipleHiddenInput()
  1855. )
  1856. feed_leg = forms.ChoiceField(
  1857. choices=add_blank_choice(POWERFEED_LEG_CHOICES),
  1858. required=False,
  1859. )
  1860. power_port = forms.ModelChoiceField(
  1861. queryset=PowerPort.objects.all(),
  1862. required=False
  1863. )
  1864. description = forms.CharField(
  1865. max_length=100,
  1866. required=False
  1867. )
  1868. class Meta:
  1869. nullable_fields = [
  1870. 'feed_leg', 'power_port', 'description',
  1871. ]
  1872. def __init__(self, *args, **kwargs):
  1873. super().__init__(*args, **kwargs)
  1874. # Limit power_port queryset to PowerPorts which belong to the parent Device
  1875. self.fields['power_port'].queryset = PowerPort.objects.filter(device=self.parent_obj)
  1876. class PowerOutletBulkRenameForm(BulkRenameForm):
  1877. pk = forms.ModelMultipleChoiceField(
  1878. queryset=PowerOutlet.objects.all(),
  1879. widget=forms.MultipleHiddenInput
  1880. )
  1881. class PowerOutletBulkDisconnectForm(ConfirmationForm):
  1882. pk = forms.ModelMultipleChoiceField(
  1883. queryset=PowerOutlet.objects.all(),
  1884. widget=forms.MultipleHiddenInput
  1885. )
  1886. #
  1887. # Interfaces
  1888. #
  1889. class InterfaceForm(InterfaceCommonForm, BootstrapMixin, forms.ModelForm):
  1890. untagged_vlan = forms.ModelChoiceField(
  1891. queryset=VLAN.objects.all(),
  1892. required=False,
  1893. widget=APISelect(
  1894. api_url="/api/ipam/vlans/",
  1895. display_field='display_name',
  1896. full=True
  1897. )
  1898. )
  1899. tagged_vlans = forms.ModelMultipleChoiceField(
  1900. queryset=VLAN.objects.all(),
  1901. required=False,
  1902. widget=APISelectMultiple(
  1903. api_url="/api/ipam/vlans/",
  1904. display_field='display_name',
  1905. full=True
  1906. )
  1907. )
  1908. tags = TagField(
  1909. required=False
  1910. )
  1911. class Meta:
  1912. model = Interface
  1913. fields = [
  1914. 'device', 'name', 'type', 'enabled', 'lag', 'mac_address', 'mtu', 'mgmt_only', 'description',
  1915. 'mode', 'untagged_vlan', 'tagged_vlans', 'tags',
  1916. ]
  1917. widgets = {
  1918. 'device': forms.HiddenInput(),
  1919. 'type': StaticSelect2(),
  1920. 'lag': StaticSelect2(),
  1921. 'mode': StaticSelect2(),
  1922. }
  1923. labels = {
  1924. 'mode': '802.1Q Mode',
  1925. }
  1926. help_texts = {
  1927. 'mode': INTERFACE_MODE_HELP_TEXT,
  1928. }
  1929. def __init__(self, *args, **kwargs):
  1930. super().__init__(*args, **kwargs)
  1931. # Limit LAG choices to interfaces belonging to this device (or VC master)
  1932. if self.is_bound:
  1933. device = Device.objects.get(pk=self.data['device'])
  1934. self.fields['lag'].queryset = Interface.objects.filter(
  1935. device__in=[device, device.get_vc_master()], type=IFACE_TYPE_LAG
  1936. )
  1937. else:
  1938. device = self.instance.device
  1939. self.fields['lag'].queryset = Interface.objects.filter(
  1940. device__in=[self.instance.device, self.instance.device.get_vc_master()], type=IFACE_TYPE_LAG
  1941. )
  1942. # Limit VLan choices to those in: global vlans, global groups, the current site's group, the current site
  1943. vlan_choices = []
  1944. global_vlans = VLAN.objects.filter(site=None, group=None)
  1945. vlan_choices.append(
  1946. ('Global', [(vlan.pk, vlan) for vlan in global_vlans])
  1947. )
  1948. for group in VLANGroup.objects.filter(site=None):
  1949. global_group_vlans = VLAN.objects.filter(group=group)
  1950. vlan_choices.append(
  1951. (group.name, [(vlan.pk, vlan) for vlan in global_group_vlans])
  1952. )
  1953. site = getattr(self.instance.parent, 'site', None)
  1954. if site is not None:
  1955. # Add non-grouped site VLANs
  1956. site_vlans = VLAN.objects.filter(site=site, group=None)
  1957. vlan_choices.append((site.name, [(vlan.pk, vlan) for vlan in site_vlans]))
  1958. # Add grouped site VLANs
  1959. for group in VLANGroup.objects.filter(site=site):
  1960. site_group_vlans = VLAN.objects.filter(group=group)
  1961. vlan_choices.append((
  1962. '{} / {}'.format(group.site.name, group.name),
  1963. [(vlan.pk, vlan) for vlan in site_group_vlans]
  1964. ))
  1965. self.fields['untagged_vlan'].choices = [(None, '---------')] + vlan_choices
  1966. self.fields['tagged_vlans'].choices = vlan_choices
  1967. class InterfaceCreateForm(InterfaceCommonForm, ComponentForm, forms.Form):
  1968. name_pattern = ExpandableNameField(
  1969. label='Name'
  1970. )
  1971. type = forms.ChoiceField(
  1972. choices=IFACE_TYPE_CHOICES,
  1973. widget=StaticSelect2(),
  1974. )
  1975. enabled = forms.BooleanField(
  1976. required=False
  1977. )
  1978. lag = forms.ModelChoiceField(
  1979. queryset=Interface.objects.all(),
  1980. required=False,
  1981. label='Parent LAG',
  1982. widget=StaticSelect2(),
  1983. )
  1984. mtu = forms.IntegerField(
  1985. required=False,
  1986. min_value=1,
  1987. max_value=32767,
  1988. label='MTU'
  1989. )
  1990. mac_address = forms.CharField(
  1991. required=False,
  1992. label='MAC Address'
  1993. )
  1994. mgmt_only = forms.BooleanField(
  1995. required=False,
  1996. label='Management only',
  1997. help_text='This interface is used only for out-of-band management'
  1998. )
  1999. description = forms.CharField(
  2000. max_length=100,
  2001. required=False
  2002. )
  2003. mode = forms.ChoiceField(
  2004. choices=add_blank_choice(IFACE_MODE_CHOICES),
  2005. required=False,
  2006. widget=StaticSelect2(),
  2007. )
  2008. tags = TagField(
  2009. required=False
  2010. )
  2011. untagged_vlan = forms.ModelChoiceField(
  2012. queryset=VLAN.objects.all(),
  2013. required=False,
  2014. widget=APISelect(
  2015. api_url="/api/ipam/vlans/",
  2016. display_field='display_name',
  2017. full=True
  2018. )
  2019. )
  2020. tagged_vlans = forms.ModelMultipleChoiceField(
  2021. queryset=VLAN.objects.all(),
  2022. required=False,
  2023. widget=APISelectMultiple(
  2024. api_url="/api/ipam/vlans/",
  2025. display_field='display_name',
  2026. full=True
  2027. )
  2028. )
  2029. def __init__(self, *args, **kwargs):
  2030. # Set interfaces enabled by default
  2031. kwargs['initial'] = kwargs.get('initial', {}).copy()
  2032. kwargs['initial'].update({'enabled': True})
  2033. super().__init__(*args, **kwargs)
  2034. # Limit LAG choices to interfaces belonging to this device (or its VC master)
  2035. if self.parent is not None:
  2036. self.fields['lag'].queryset = Interface.objects.filter(
  2037. device__in=[self.parent, self.parent.get_vc_master()], type=IFACE_TYPE_LAG
  2038. )
  2039. else:
  2040. self.fields['lag'].queryset = Interface.objects.none()
  2041. # Limit VLan choices to those in: global vlans, global groups, the current site's group, the current site
  2042. vlan_choices = []
  2043. global_vlans = VLAN.objects.filter(site=None, group=None)
  2044. vlan_choices.append(
  2045. ('Global', [(vlan.pk, vlan) for vlan in global_vlans])
  2046. )
  2047. for group in VLANGroup.objects.filter(site=None):
  2048. global_group_vlans = VLAN.objects.filter(group=group)
  2049. vlan_choices.append(
  2050. (group.name, [(vlan.pk, vlan) for vlan in global_group_vlans])
  2051. )
  2052. site = getattr(self.parent, 'site', None)
  2053. if site is not None:
  2054. # Add non-grouped site VLANs
  2055. site_vlans = VLAN.objects.filter(site=site, group=None)
  2056. vlan_choices.append((site.name, [(vlan.pk, vlan) for vlan in site_vlans]))
  2057. # Add grouped site VLANs
  2058. for group in VLANGroup.objects.filter(site=site):
  2059. site_group_vlans = VLAN.objects.filter(group=group)
  2060. vlan_choices.append((
  2061. '{} / {}'.format(group.site.name, group.name),
  2062. [(vlan.pk, vlan) for vlan in site_group_vlans]
  2063. ))
  2064. self.fields['untagged_vlan'].choices = [(None, '---------')] + vlan_choices
  2065. self.fields['tagged_vlans'].choices = vlan_choices
  2066. class InterfaceBulkEditForm(InterfaceCommonForm, BootstrapMixin, AddRemoveTagsForm, BulkEditForm):
  2067. pk = forms.ModelMultipleChoiceField(
  2068. queryset=Interface.objects.all(),
  2069. widget=forms.MultipleHiddenInput()
  2070. )
  2071. type = forms.ChoiceField(
  2072. choices=add_blank_choice(IFACE_TYPE_CHOICES),
  2073. required=False,
  2074. widget=StaticSelect2()
  2075. )
  2076. enabled = forms.NullBooleanField(
  2077. required=False,
  2078. widget=BulkEditNullBooleanSelect()
  2079. )
  2080. lag = forms.ModelChoiceField(
  2081. queryset=Interface.objects.all(),
  2082. required=False,
  2083. label='Parent LAG',
  2084. widget=StaticSelect2()
  2085. )
  2086. mac_address = forms.CharField(
  2087. required=False,
  2088. label='MAC Address'
  2089. )
  2090. mtu = forms.IntegerField(
  2091. required=False,
  2092. min_value=1,
  2093. max_value=32767,
  2094. label='MTU'
  2095. )
  2096. mgmt_only = forms.NullBooleanField(
  2097. required=False,
  2098. widget=BulkEditNullBooleanSelect(),
  2099. label='Management only'
  2100. )
  2101. description = forms.CharField(
  2102. max_length=100,
  2103. required=False
  2104. )
  2105. mode = forms.ChoiceField(
  2106. choices=add_blank_choice(IFACE_MODE_CHOICES),
  2107. required=False,
  2108. widget=StaticSelect2()
  2109. )
  2110. untagged_vlan = forms.ModelChoiceField(
  2111. queryset=VLAN.objects.all(),
  2112. required=False,
  2113. widget=APISelect(
  2114. api_url="/api/ipam/vlans/",
  2115. display_field='display_name',
  2116. full=True
  2117. )
  2118. )
  2119. tagged_vlans = forms.ModelMultipleChoiceField(
  2120. queryset=VLAN.objects.all(),
  2121. required=False,
  2122. widget=APISelectMultiple(
  2123. api_url="/api/ipam/vlans/",
  2124. display_field='display_name',
  2125. full=True
  2126. )
  2127. )
  2128. class Meta:
  2129. nullable_fields = [
  2130. 'lag', 'mac_address', 'mtu', 'description', 'mode', 'untagged_vlan', 'tagged_vlans'
  2131. ]
  2132. def __init__(self, *args, **kwargs):
  2133. super().__init__(*args, **kwargs)
  2134. # Limit LAG choices to interfaces which belong to the parent device (or VC master)
  2135. device = self.parent_obj
  2136. if device is not None:
  2137. self.fields['lag'].queryset = Interface.objects.filter(
  2138. device__in=[device, device.get_vc_master()],
  2139. type=IFACE_TYPE_LAG
  2140. )
  2141. else:
  2142. self.fields['lag'].choices = []
  2143. # Limit VLan choices to those in: global vlans, global groups, the current site's group, the current site
  2144. vlan_choices = []
  2145. global_vlans = VLAN.objects.filter(site=None, group=None)
  2146. vlan_choices.append(
  2147. ('Global', [(vlan.pk, vlan) for vlan in global_vlans])
  2148. )
  2149. for group in VLANGroup.objects.filter(site=None):
  2150. global_group_vlans = VLAN.objects.filter(group=group)
  2151. vlan_choices.append(
  2152. (group.name, [(vlan.pk, vlan) for vlan in global_group_vlans])
  2153. )
  2154. if self.parent_obj is not None:
  2155. site = getattr(self.parent_obj, 'site', None)
  2156. if site is not None:
  2157. # Add non-grouped site VLANs
  2158. site_vlans = VLAN.objects.filter(site=site, group=None)
  2159. vlan_choices.append((site.name, [(vlan.pk, vlan) for vlan in site_vlans]))
  2160. # Add grouped site VLANs
  2161. for group in VLANGroup.objects.filter(site=site):
  2162. site_group_vlans = VLAN.objects.filter(group=group)
  2163. vlan_choices.append((
  2164. '{} / {}'.format(group.site.name, group.name),
  2165. [(vlan.pk, vlan) for vlan in site_group_vlans]
  2166. ))
  2167. self.fields['untagged_vlan'].choices = [(None, '---------')] + vlan_choices
  2168. self.fields['tagged_vlans'].choices = vlan_choices
  2169. class InterfaceBulkRenameForm(BulkRenameForm):
  2170. pk = forms.ModelMultipleChoiceField(
  2171. queryset=Interface.objects.all(),
  2172. widget=forms.MultipleHiddenInput()
  2173. )
  2174. class InterfaceBulkDisconnectForm(ConfirmationForm):
  2175. pk = forms.ModelMultipleChoiceField(
  2176. queryset=Interface.objects.all(),
  2177. widget=forms.MultipleHiddenInput()
  2178. )
  2179. #
  2180. # Front pass-through ports
  2181. #
  2182. class FrontPortForm(BootstrapMixin, forms.ModelForm):
  2183. tags = TagField(
  2184. required=False
  2185. )
  2186. class Meta:
  2187. model = FrontPort
  2188. fields = [
  2189. 'device', 'name', 'type', 'rear_port', 'rear_port_position', 'description', 'tags',
  2190. ]
  2191. widgets = {
  2192. 'device': forms.HiddenInput(),
  2193. 'type': StaticSelect2(),
  2194. 'rear_port': StaticSelect2(),
  2195. }
  2196. def __init__(self, *args, **kwargs):
  2197. super().__init__(*args, **kwargs)
  2198. # Limit RearPort choices to the local device
  2199. if hasattr(self.instance, 'device'):
  2200. self.fields['rear_port'].queryset = self.fields['rear_port'].queryset.filter(
  2201. device=self.instance.device
  2202. )
  2203. # TODO: Merge with FrontPortTemplateCreateForm to remove duplicate logic
  2204. class FrontPortCreateForm(ComponentForm):
  2205. name_pattern = ExpandableNameField(
  2206. label='Name'
  2207. )
  2208. type = forms.ChoiceField(
  2209. choices=PORT_TYPE_CHOICES,
  2210. widget=StaticSelect2(),
  2211. )
  2212. rear_port_set = forms.MultipleChoiceField(
  2213. choices=[],
  2214. label='Rear ports',
  2215. help_text='Select one rear port assignment for each front port being created.',
  2216. )
  2217. description = forms.CharField(
  2218. required=False
  2219. )
  2220. def __init__(self, *args, **kwargs):
  2221. super().__init__(*args, **kwargs)
  2222. # Determine which rear port positions are occupied. These will be excluded from the list of available mappings.
  2223. occupied_port_positions = [
  2224. (front_port.rear_port_id, front_port.rear_port_position)
  2225. for front_port in self.parent.frontports.all()
  2226. ]
  2227. # Populate rear port choices
  2228. choices = []
  2229. rear_ports = RearPort.objects.filter(device=self.parent)
  2230. for rear_port in rear_ports:
  2231. for i in range(1, rear_port.positions + 1):
  2232. if (rear_port.pk, i) not in occupied_port_positions:
  2233. choices.append(
  2234. ('{}:{}'.format(rear_port.pk, i), '{}:{}'.format(rear_port.name, i))
  2235. )
  2236. self.fields['rear_port_set'].choices = choices
  2237. def clean(self):
  2238. # Validate that the number of ports being created equals the number of selected (rear port, position) tuples
  2239. front_port_count = len(self.cleaned_data['name_pattern'])
  2240. rear_port_count = len(self.cleaned_data['rear_port_set'])
  2241. if front_port_count != rear_port_count:
  2242. raise forms.ValidationError({
  2243. 'rear_port_set': 'The provided name pattern will create {} ports, however {} rear port assignments '
  2244. 'were selected. These counts must match.'.format(front_port_count, rear_port_count)
  2245. })
  2246. def get_iterative_data(self, iteration):
  2247. # Assign rear port and position from selected set
  2248. rear_port, position = self.cleaned_data['rear_port_set'][iteration].split(':')
  2249. return {
  2250. 'rear_port': int(rear_port),
  2251. 'rear_port_position': int(position),
  2252. }
  2253. class FrontPortBulkEditForm(BootstrapMixin, AddRemoveTagsForm, BulkEditForm):
  2254. pk = forms.ModelMultipleChoiceField(
  2255. queryset=FrontPort.objects.all(),
  2256. widget=forms.MultipleHiddenInput()
  2257. )
  2258. type = forms.ChoiceField(
  2259. choices=add_blank_choice(PORT_TYPE_CHOICES),
  2260. required=False,
  2261. widget=StaticSelect2()
  2262. )
  2263. description = forms.CharField(
  2264. max_length=100,
  2265. required=False
  2266. )
  2267. class Meta:
  2268. nullable_fields = [
  2269. 'description',
  2270. ]
  2271. class FrontPortBulkRenameForm(BulkRenameForm):
  2272. pk = forms.ModelMultipleChoiceField(
  2273. queryset=FrontPort.objects.all(),
  2274. widget=forms.MultipleHiddenInput
  2275. )
  2276. class FrontPortBulkDisconnectForm(ConfirmationForm):
  2277. pk = forms.ModelMultipleChoiceField(
  2278. queryset=FrontPort.objects.all(),
  2279. widget=forms.MultipleHiddenInput
  2280. )
  2281. #
  2282. # Rear pass-through ports
  2283. #
  2284. class RearPortForm(BootstrapMixin, forms.ModelForm):
  2285. tags = TagField(
  2286. required=False
  2287. )
  2288. class Meta:
  2289. model = RearPort
  2290. fields = [
  2291. 'device', 'name', 'type', 'positions', 'description', 'tags',
  2292. ]
  2293. widgets = {
  2294. 'device': forms.HiddenInput(),
  2295. 'type': StaticSelect2(),
  2296. }
  2297. class RearPortCreateForm(ComponentForm):
  2298. name_pattern = ExpandableNameField(
  2299. label='Name'
  2300. )
  2301. type = forms.ChoiceField(
  2302. choices=PORT_TYPE_CHOICES,
  2303. widget=StaticSelect2(),
  2304. )
  2305. positions = forms.IntegerField(
  2306. min_value=1,
  2307. max_value=64,
  2308. initial=1,
  2309. help_text='The number of front ports which may be mapped to each rear port'
  2310. )
  2311. description = forms.CharField(
  2312. required=False
  2313. )
  2314. class RearPortBulkEditForm(BootstrapMixin, AddRemoveTagsForm, BulkEditForm):
  2315. pk = forms.ModelMultipleChoiceField(
  2316. queryset=RearPort.objects.all(),
  2317. widget=forms.MultipleHiddenInput()
  2318. )
  2319. type = forms.ChoiceField(
  2320. choices=add_blank_choice(PORT_TYPE_CHOICES),
  2321. required=False,
  2322. widget=StaticSelect2()
  2323. )
  2324. description = forms.CharField(
  2325. max_length=100,
  2326. required=False
  2327. )
  2328. class Meta:
  2329. nullable_fields = [
  2330. 'description',
  2331. ]
  2332. class RearPortBulkRenameForm(BulkRenameForm):
  2333. pk = forms.ModelMultipleChoiceField(
  2334. queryset=RearPort.objects.all(),
  2335. widget=forms.MultipleHiddenInput
  2336. )
  2337. class RearPortBulkDisconnectForm(ConfirmationForm):
  2338. pk = forms.ModelMultipleChoiceField(
  2339. queryset=RearPort.objects.all(),
  2340. widget=forms.MultipleHiddenInput
  2341. )
  2342. #
  2343. # Cables
  2344. #
  2345. class ConnectCableToDeviceForm(BootstrapMixin, ChainedFieldsMixin, forms.ModelForm):
  2346. """
  2347. Base form for connecting a Cable to a Device component
  2348. """
  2349. termination_b_site = forms.ModelChoiceField(
  2350. queryset=Site.objects.all(),
  2351. label='Site',
  2352. required=False,
  2353. widget=APISelect(
  2354. api_url='/api/dcim/sites/',
  2355. filter_for={
  2356. 'termination_b_rack': 'site_id',
  2357. 'termination_b_device': 'site_id',
  2358. }
  2359. )
  2360. )
  2361. termination_b_rack = ChainedModelChoiceField(
  2362. queryset=Rack.objects.all(),
  2363. chains=(
  2364. ('site', 'termination_b_site'),
  2365. ),
  2366. label='Rack',
  2367. required=False,
  2368. widget=APISelect(
  2369. api_url='/api/dcim/racks/',
  2370. filter_for={
  2371. 'termination_b_device': 'rack_id',
  2372. },
  2373. attrs={
  2374. 'nullable': 'true',
  2375. }
  2376. )
  2377. )
  2378. termination_b_device = ChainedModelChoiceField(
  2379. queryset=Device.objects.all(),
  2380. chains=(
  2381. ('site', 'termination_b_site'),
  2382. ('rack', 'termination_b_rack'),
  2383. ),
  2384. label='Device',
  2385. required=False,
  2386. widget=APISelect(
  2387. api_url='/api/dcim/devices/',
  2388. display_field='display_name',
  2389. filter_for={
  2390. 'termination_b_id': 'device_id',
  2391. }
  2392. )
  2393. )
  2394. class Meta:
  2395. model = Cable
  2396. fields = [
  2397. 'termination_b_site', 'termination_b_rack', 'termination_b_device', 'termination_b_id', 'type', 'status',
  2398. 'label', 'color', 'length', 'length_unit',
  2399. ]
  2400. class ConnectCableToConsolePortForm(ConnectCableToDeviceForm):
  2401. termination_b_id = forms.IntegerField(
  2402. label='Name',
  2403. widget=APISelect(
  2404. api_url='/api/dcim/console-ports/',
  2405. disabled_indicator='cable',
  2406. )
  2407. )
  2408. class ConnectCableToConsoleServerPortForm(ConnectCableToDeviceForm):
  2409. termination_b_id = forms.IntegerField(
  2410. label='Name',
  2411. widget=APISelect(
  2412. api_url='/api/dcim/console-server-ports/',
  2413. disabled_indicator='cable',
  2414. )
  2415. )
  2416. class ConnectCableToPowerPortForm(ConnectCableToDeviceForm):
  2417. termination_b_id = forms.IntegerField(
  2418. label='Name',
  2419. widget=APISelect(
  2420. api_url='/api/dcim/power-ports/',
  2421. disabled_indicator='cable',
  2422. )
  2423. )
  2424. class ConnectCableToPowerOutletForm(ConnectCableToDeviceForm):
  2425. termination_b_id = forms.IntegerField(
  2426. label='Name',
  2427. widget=APISelect(
  2428. api_url='/api/dcim/power-outlets/',
  2429. disabled_indicator='cable',
  2430. )
  2431. )
  2432. class ConnectCableToInterfaceForm(ConnectCableToDeviceForm):
  2433. termination_b_id = forms.IntegerField(
  2434. label='Name',
  2435. widget=APISelect(
  2436. api_url='/api/dcim/interfaces/',
  2437. disabled_indicator='cable',
  2438. additional_query_params={
  2439. 'kind': 'physical',
  2440. }
  2441. )
  2442. )
  2443. class ConnectCableToFrontPortForm(ConnectCableToDeviceForm):
  2444. termination_b_id = forms.IntegerField(
  2445. label='Name',
  2446. widget=APISelect(
  2447. api_url='/api/dcim/front-ports/',
  2448. disabled_indicator='cable',
  2449. )
  2450. )
  2451. class ConnectCableToRearPortForm(ConnectCableToDeviceForm):
  2452. termination_b_id = forms.IntegerField(
  2453. label='Name',
  2454. widget=APISelect(
  2455. api_url='/api/dcim/rear-ports/',
  2456. disabled_indicator='cable',
  2457. )
  2458. )
  2459. class ConnectCableToCircuitTerminationForm(BootstrapMixin, ChainedFieldsMixin, forms.ModelForm):
  2460. termination_b_provider = forms.ModelChoiceField(
  2461. queryset=Provider.objects.all(),
  2462. label='Provider',
  2463. widget=APISelect(
  2464. api_url='/api/circuits/providers/',
  2465. filter_for={
  2466. 'termination_b_circuit': 'provider_id',
  2467. }
  2468. )
  2469. )
  2470. termination_b_site = forms.ModelChoiceField(
  2471. queryset=Site.objects.all(),
  2472. label='Site',
  2473. required=False,
  2474. widget=APISelect(
  2475. api_url='/api/dcim/sites/',
  2476. filter_for={
  2477. 'termination_b_circuit': 'site_id',
  2478. }
  2479. )
  2480. )
  2481. termination_b_circuit = ChainedModelChoiceField(
  2482. queryset=Circuit.objects.all(),
  2483. chains=(
  2484. ('provider', 'termination_b_provider'),
  2485. ),
  2486. label='Circuit',
  2487. widget=APISelect(
  2488. api_url='/api/circuits/circuits/',
  2489. display_field='cid',
  2490. filter_for={
  2491. 'termination_b_id': 'circuit_id',
  2492. }
  2493. )
  2494. )
  2495. termination_b_id = forms.IntegerField(
  2496. label='Side',
  2497. widget=APISelect(
  2498. api_url='/api/circuits/circuit-terminations/',
  2499. disabled_indicator='cable',
  2500. display_field='term_side'
  2501. )
  2502. )
  2503. class Meta:
  2504. model = Cable
  2505. fields = [
  2506. 'termination_b_provider', 'termination_b_site', 'termination_b_circuit', 'termination_b_id', 'type',
  2507. 'status', 'label', 'color', 'length', 'length_unit',
  2508. ]
  2509. class ConnectCableToPowerFeedForm(BootstrapMixin, ChainedFieldsMixin, forms.ModelForm):
  2510. termination_b_site = forms.ModelChoiceField(
  2511. queryset=Site.objects.all(),
  2512. label='Site',
  2513. widget=APISelect(
  2514. api_url='/api/dcim/sites/',
  2515. display_field='cid',
  2516. filter_for={
  2517. 'termination_b_rackgroup': 'site_id',
  2518. 'termination_b_powerpanel': 'site_id',
  2519. }
  2520. )
  2521. )
  2522. termination_b_rackgroup = ChainedModelChoiceField(
  2523. queryset=RackGroup.objects.all(),
  2524. label='Rack Group',
  2525. chains=(
  2526. ('site', 'termination_b_site'),
  2527. ),
  2528. required=False,
  2529. widget=APISelect(
  2530. api_url='/api/dcim/rack-groups/',
  2531. display_field='cid',
  2532. filter_for={
  2533. 'termination_b_powerpanel': 'rackgroup_id',
  2534. }
  2535. )
  2536. )
  2537. termination_b_powerpanel = ChainedModelChoiceField(
  2538. queryset=PowerPanel.objects.all(),
  2539. chains=(
  2540. ('site', 'termination_b_site'),
  2541. ('rack_group', 'termination_b_rackgroup'),
  2542. ),
  2543. label='Power Panel',
  2544. widget=APISelect(
  2545. api_url='/api/dcim/power-panels/',
  2546. filter_for={
  2547. 'termination_b_id': 'power_panel_id',
  2548. }
  2549. )
  2550. )
  2551. termination_b_id = forms.IntegerField(
  2552. label='Name',
  2553. widget=APISelect(
  2554. api_url='/api/dcim/power-feeds/',
  2555. )
  2556. )
  2557. class Meta:
  2558. model = Cable
  2559. fields = [
  2560. 'termination_b_rackgroup', 'termination_b_powerpanel', 'termination_b_id', 'type', 'status', 'label',
  2561. 'color', 'length', 'length_unit',
  2562. ]
  2563. class CableForm(BootstrapMixin, forms.ModelForm):
  2564. class Meta:
  2565. model = Cable
  2566. fields = [
  2567. 'type', 'status', 'label', 'color', 'length', 'length_unit',
  2568. ]
  2569. class CableCSVForm(forms.ModelForm):
  2570. # Termination A
  2571. side_a_device = FlexibleModelChoiceField(
  2572. queryset=Device.objects.all(),
  2573. to_field_name='name',
  2574. help_text='Side A device name or ID',
  2575. error_messages={
  2576. 'invalid_choice': 'Side A device not found',
  2577. }
  2578. )
  2579. side_a_type = forms.ModelChoiceField(
  2580. queryset=ContentType.objects.all(),
  2581. limit_choices_to={
  2582. 'model__in': CABLE_TERMINATION_TYPES,
  2583. },
  2584. to_field_name='model',
  2585. help_text='Side A type'
  2586. )
  2587. side_a_name = forms.CharField(
  2588. help_text='Side A component'
  2589. )
  2590. # Termination B
  2591. side_b_device = FlexibleModelChoiceField(
  2592. queryset=Device.objects.all(),
  2593. to_field_name='name',
  2594. help_text='Side B device name or ID',
  2595. error_messages={
  2596. 'invalid_choice': 'Side B device not found',
  2597. }
  2598. )
  2599. side_b_type = forms.ModelChoiceField(
  2600. queryset=ContentType.objects.all(),
  2601. limit_choices_to={
  2602. 'model__in': CABLE_TERMINATION_TYPES,
  2603. },
  2604. to_field_name='model',
  2605. help_text='Side B type'
  2606. )
  2607. side_b_name = forms.CharField(
  2608. help_text='Side B component'
  2609. )
  2610. # Cable attributes
  2611. status = CSVChoiceField(
  2612. choices=CONNECTION_STATUS_CHOICES,
  2613. required=False,
  2614. help_text='Connection status'
  2615. )
  2616. type = CSVChoiceField(
  2617. choices=CABLE_TYPE_CHOICES,
  2618. required=False,
  2619. help_text='Cable type'
  2620. )
  2621. length_unit = CSVChoiceField(
  2622. choices=CABLE_LENGTH_UNIT_CHOICES,
  2623. required=False,
  2624. help_text='Length unit'
  2625. )
  2626. class Meta:
  2627. model = Cable
  2628. fields = [
  2629. 'side_a_device', 'side_a_type', 'side_a_name', 'side_b_device', 'side_b_type', 'side_b_name', 'type',
  2630. 'status', 'label', 'color', 'length', 'length_unit',
  2631. ]
  2632. help_texts = {
  2633. 'color': 'RGB color in hexadecimal (e.g. 00ff00)'
  2634. }
  2635. # TODO: Merge the clean() methods for either end
  2636. def clean_side_a_name(self):
  2637. device = self.cleaned_data.get('side_a_device')
  2638. content_type = self.cleaned_data.get('side_a_type')
  2639. name = self.cleaned_data.get('side_a_name')
  2640. if not device or not content_type or not name:
  2641. return None
  2642. model = content_type.model_class()
  2643. try:
  2644. termination_object = model.objects.get(
  2645. device=device,
  2646. name=name
  2647. )
  2648. if termination_object.cable is not None:
  2649. raise forms.ValidationError(
  2650. "Side A: {} {} is already connected".format(device, termination_object)
  2651. )
  2652. except ObjectDoesNotExist:
  2653. raise forms.ValidationError(
  2654. "A side termination not found: {} {}".format(device, name)
  2655. )
  2656. self.instance.termination_a = termination_object
  2657. return termination_object
  2658. def clean_side_b_name(self):
  2659. device = self.cleaned_data.get('side_b_device')
  2660. content_type = self.cleaned_data.get('side_b_type')
  2661. name = self.cleaned_data.get('side_b_name')
  2662. if not device or not content_type or not name:
  2663. return None
  2664. model = content_type.model_class()
  2665. try:
  2666. termination_object = model.objects.get(
  2667. device=device,
  2668. name=name
  2669. )
  2670. if termination_object.cable is not None:
  2671. raise forms.ValidationError(
  2672. "Side B: {} {} is already connected".format(device, termination_object)
  2673. )
  2674. except ObjectDoesNotExist:
  2675. raise forms.ValidationError(
  2676. "B side termination not found: {} {}".format(device, name)
  2677. )
  2678. self.instance.termination_b = termination_object
  2679. return termination_object
  2680. def clean_length_unit(self):
  2681. # Avoid trying to save as NULL
  2682. length_unit = self.cleaned_data.get('length_unit', None)
  2683. return length_unit if length_unit is not None else ''
  2684. class CableBulkEditForm(BootstrapMixin, BulkEditForm):
  2685. pk = forms.ModelMultipleChoiceField(
  2686. queryset=Cable.objects.all(),
  2687. widget=forms.MultipleHiddenInput
  2688. )
  2689. type = forms.ChoiceField(
  2690. choices=add_blank_choice(CABLE_TYPE_CHOICES),
  2691. required=False,
  2692. initial='',
  2693. widget=StaticSelect2()
  2694. )
  2695. status = forms.ChoiceField(
  2696. choices=add_blank_choice(CONNECTION_STATUS_CHOICES),
  2697. required=False,
  2698. widget=StaticSelect2(),
  2699. initial=''
  2700. )
  2701. label = forms.CharField(
  2702. max_length=100,
  2703. required=False
  2704. )
  2705. color = forms.CharField(
  2706. max_length=6,
  2707. required=False,
  2708. widget=ColorSelect()
  2709. )
  2710. length = forms.IntegerField(
  2711. min_value=1,
  2712. required=False
  2713. )
  2714. length_unit = forms.ChoiceField(
  2715. choices=add_blank_choice(CABLE_LENGTH_UNIT_CHOICES),
  2716. required=False,
  2717. initial='',
  2718. widget=StaticSelect2()
  2719. )
  2720. class Meta:
  2721. nullable_fields = [
  2722. 'type', 'status', 'label', 'color', 'length',
  2723. ]
  2724. def clean(self):
  2725. # Validate length/unit
  2726. length = self.cleaned_data.get('length')
  2727. length_unit = self.cleaned_data.get('length_unit')
  2728. if length and not length_unit:
  2729. raise forms.ValidationError({
  2730. 'length_unit': "Must specify a unit when setting length"
  2731. })
  2732. class CableFilterForm(BootstrapMixin, forms.Form):
  2733. model = Cable
  2734. q = forms.CharField(
  2735. required=False,
  2736. label='Search'
  2737. )
  2738. site = FilterChoiceField(
  2739. queryset=Site.objects.all(),
  2740. to_field_name='slug',
  2741. widget=APISelectMultiple(
  2742. api_url="/api/dcim/sites/",
  2743. value_field="slug",
  2744. filter_for={
  2745. 'rack_id': 'site',
  2746. }
  2747. )
  2748. )
  2749. rack_id = FilterChoiceField(
  2750. queryset=Rack.objects.all(),
  2751. label='Rack',
  2752. null_label='-- None --',
  2753. widget=APISelectMultiple(
  2754. api_url="/api/dcim/racks/",
  2755. null_option=True,
  2756. )
  2757. )
  2758. type = forms.MultipleChoiceField(
  2759. choices=add_blank_choice(CABLE_TYPE_CHOICES),
  2760. required=False,
  2761. widget=StaticSelect2()
  2762. )
  2763. status = forms.ChoiceField(
  2764. required=False,
  2765. choices=add_blank_choice(CONNECTION_STATUS_CHOICES),
  2766. widget=StaticSelect2()
  2767. )
  2768. color = forms.CharField(
  2769. max_length=6,
  2770. required=False,
  2771. widget=ColorSelect()
  2772. )
  2773. device_id = FilterChoiceField(
  2774. queryset=Device.objects.all(),
  2775. required=False,
  2776. label='Device',
  2777. widget=APISelectMultiple(
  2778. api_url='/api/dcim/devices/',
  2779. )
  2780. )
  2781. #
  2782. # Device bays
  2783. #
  2784. class DeviceBayForm(BootstrapMixin, forms.ModelForm):
  2785. tags = TagField(
  2786. required=False
  2787. )
  2788. class Meta:
  2789. model = DeviceBay
  2790. fields = [
  2791. 'device', 'name', 'description', 'tags',
  2792. ]
  2793. widgets = {
  2794. 'device': forms.HiddenInput(),
  2795. }
  2796. class DeviceBayCreateForm(ComponentForm):
  2797. name_pattern = ExpandableNameField(
  2798. label='Name'
  2799. )
  2800. tags = TagField(
  2801. required=False
  2802. )
  2803. class PopulateDeviceBayForm(BootstrapMixin, forms.Form):
  2804. installed_device = forms.ModelChoiceField(
  2805. queryset=Device.objects.all(),
  2806. label='Child Device',
  2807. help_text="Child devices must first be created and assigned to the site/rack of the parent device.",
  2808. widget=StaticSelect2(),
  2809. )
  2810. def __init__(self, device_bay, *args, **kwargs):
  2811. super().__init__(*args, **kwargs)
  2812. self.fields['installed_device'].queryset = Device.objects.filter(
  2813. site=device_bay.device.site,
  2814. rack=device_bay.device.rack,
  2815. parent_bay__isnull=True,
  2816. device_type__u_height=0,
  2817. device_type__subdevice_role=SUBDEVICE_ROLE_CHILD
  2818. ).exclude(pk=device_bay.device.pk)
  2819. class DeviceBayBulkRenameForm(BulkRenameForm):
  2820. pk = forms.ModelMultipleChoiceField(
  2821. queryset=DeviceBay.objects.all(),
  2822. widget=forms.MultipleHiddenInput()
  2823. )
  2824. #
  2825. # Connections
  2826. #
  2827. class ConsoleConnectionFilterForm(BootstrapMixin, forms.Form):
  2828. site = FilterChoiceField(
  2829. queryset=Site.objects.all(),
  2830. to_field_name='slug',
  2831. widget=APISelectMultiple(
  2832. api_url="/api/dcim/sites/",
  2833. value_field="slug",
  2834. )
  2835. )
  2836. device_id = FilterChoiceField(
  2837. queryset=Device.objects.all(),
  2838. required=False,
  2839. label='Device',
  2840. widget=APISelectMultiple(
  2841. api_url='/api/dcim/devices/',
  2842. )
  2843. )
  2844. class PowerConnectionFilterForm(BootstrapMixin, forms.Form):
  2845. site = FilterChoiceField(
  2846. queryset=Site.objects.all(),
  2847. to_field_name='slug',
  2848. widget=APISelectMultiple(
  2849. api_url="/api/dcim/sites/",
  2850. value_field="slug",
  2851. )
  2852. )
  2853. device_id = FilterChoiceField(
  2854. queryset=Device.objects.all(),
  2855. required=False,
  2856. label='Device',
  2857. widget=APISelectMultiple(
  2858. api_url='/api/dcim/devices/',
  2859. )
  2860. )
  2861. class InterfaceConnectionFilterForm(BootstrapMixin, forms.Form):
  2862. site = FilterChoiceField(
  2863. queryset=Site.objects.all(),
  2864. to_field_name='slug',
  2865. widget=APISelectMultiple(
  2866. api_url="/api/dcim/sites/",
  2867. value_field="slug",
  2868. )
  2869. )
  2870. device_id = FilterChoiceField(
  2871. queryset=Device.objects.all(),
  2872. required=False,
  2873. label='Device',
  2874. widget=APISelectMultiple(
  2875. api_url='/api/dcim/devices/',
  2876. )
  2877. )
  2878. #
  2879. # Inventory items
  2880. #
  2881. class InventoryItemForm(BootstrapMixin, forms.ModelForm):
  2882. tags = TagField(
  2883. required=False
  2884. )
  2885. class Meta:
  2886. model = InventoryItem
  2887. fields = [
  2888. 'name', 'device', 'manufacturer', 'part_id', 'serial', 'asset_tag', 'description', 'tags',
  2889. ]
  2890. widgets = {
  2891. 'device': APISelect(
  2892. api_url="/api/dcim/devices/"
  2893. ),
  2894. 'manufacturer': APISelect(
  2895. api_url="/api/dcim/manufacturers/"
  2896. )
  2897. }
  2898. class InventoryItemCSVForm(forms.ModelForm):
  2899. device = FlexibleModelChoiceField(
  2900. queryset=Device.objects.all(),
  2901. to_field_name='name',
  2902. help_text='Device name or ID',
  2903. error_messages={
  2904. 'invalid_choice': 'Device not found.',
  2905. }
  2906. )
  2907. manufacturer = forms.ModelChoiceField(
  2908. queryset=Manufacturer.objects.all(),
  2909. to_field_name='name',
  2910. required=False,
  2911. help_text='Manufacturer name',
  2912. error_messages={
  2913. 'invalid_choice': 'Invalid manufacturer.',
  2914. }
  2915. )
  2916. class Meta:
  2917. model = InventoryItem
  2918. fields = InventoryItem.csv_headers
  2919. class InventoryItemBulkEditForm(BootstrapMixin, BulkEditForm):
  2920. pk = forms.ModelMultipleChoiceField(
  2921. queryset=InventoryItem.objects.all(),
  2922. widget=forms.MultipleHiddenInput()
  2923. )
  2924. device = forms.ModelChoiceField(
  2925. queryset=Device.objects.all(),
  2926. required=False,
  2927. widget=APISelect(
  2928. api_url="/api/dcim/devices/"
  2929. )
  2930. )
  2931. manufacturer = forms.ModelChoiceField(
  2932. queryset=Manufacturer.objects.all(),
  2933. required=False,
  2934. widget=APISelect(
  2935. api_url="/api/dcim/manufacturers/"
  2936. )
  2937. )
  2938. part_id = forms.CharField(
  2939. max_length=50,
  2940. required=False,
  2941. label='Part ID'
  2942. )
  2943. description = forms.CharField(
  2944. max_length=100,
  2945. required=False
  2946. )
  2947. class Meta:
  2948. nullable_fields = [
  2949. 'manufacturer', 'part_id', 'description',
  2950. ]
  2951. class InventoryItemFilterForm(BootstrapMixin, forms.Form):
  2952. model = InventoryItem
  2953. q = forms.CharField(
  2954. required=False,
  2955. label='Search'
  2956. )
  2957. device_id = FilterChoiceField(
  2958. queryset=Device.objects.all(),
  2959. required=False,
  2960. label='Device',
  2961. widget=APISelect(
  2962. api_url='/api/dcim/devices/',
  2963. )
  2964. )
  2965. manufacturer = FilterChoiceField(
  2966. queryset=Manufacturer.objects.all(),
  2967. to_field_name='slug',
  2968. widget=APISelect(
  2969. api_url="/api/dcim/manufacturers/",
  2970. value_field="slug",
  2971. )
  2972. )
  2973. discovered = forms.NullBooleanField(
  2974. required=False,
  2975. widget=StaticSelect2(
  2976. choices=BOOLEAN_WITH_BLANK_CHOICES
  2977. )
  2978. )
  2979. #
  2980. # Virtual chassis
  2981. #
  2982. class DeviceSelectionForm(forms.Form):
  2983. pk = forms.ModelMultipleChoiceField(
  2984. queryset=Device.objects.all(),
  2985. widget=forms.MultipleHiddenInput()
  2986. )
  2987. class VirtualChassisForm(BootstrapMixin, forms.ModelForm):
  2988. tags = TagField(
  2989. required=False
  2990. )
  2991. class Meta:
  2992. model = VirtualChassis
  2993. fields = [
  2994. 'master', 'domain', 'tags',
  2995. ]
  2996. widgets = {
  2997. 'master': SelectWithPK(),
  2998. }
  2999. class BaseVCMemberFormSet(forms.BaseModelFormSet):
  3000. def clean(self):
  3001. super().clean()
  3002. # Check for duplicate VC position values
  3003. vc_position_list = []
  3004. for form in self.forms:
  3005. vc_position = form.cleaned_data.get('vc_position')
  3006. if vc_position:
  3007. if vc_position in vc_position_list:
  3008. error_msg = 'A virtual chassis member already exists in position {}.'.format(vc_position)
  3009. form.add_error('vc_position', error_msg)
  3010. vc_position_list.append(vc_position)
  3011. class DeviceVCMembershipForm(forms.ModelForm):
  3012. class Meta:
  3013. model = Device
  3014. fields = [
  3015. 'vc_position', 'vc_priority',
  3016. ]
  3017. labels = {
  3018. 'vc_position': 'Position',
  3019. 'vc_priority': 'Priority',
  3020. }
  3021. def __init__(self, validate_vc_position=False, *args, **kwargs):
  3022. super().__init__(*args, **kwargs)
  3023. # Require VC position (only required when the Device is a VirtualChassis member)
  3024. self.fields['vc_position'].required = True
  3025. # Validation of vc_position is optional. This is only required when adding a new member to an existing
  3026. # VirtualChassis. Otherwise, vc_position validation is handled by BaseVCMemberFormSet.
  3027. self.validate_vc_position = validate_vc_position
  3028. def clean_vc_position(self):
  3029. vc_position = self.cleaned_data['vc_position']
  3030. if self.validate_vc_position:
  3031. conflicting_members = Device.objects.filter(
  3032. virtual_chassis=self.instance.virtual_chassis,
  3033. vc_position=vc_position
  3034. )
  3035. if conflicting_members.exists():
  3036. raise forms.ValidationError(
  3037. 'A virtual chassis member already exists in position {}.'.format(vc_position)
  3038. )
  3039. return vc_position
  3040. class VCMemberSelectForm(BootstrapMixin, ChainedFieldsMixin, forms.Form):
  3041. site = forms.ModelChoiceField(
  3042. queryset=Site.objects.all(),
  3043. label='Site',
  3044. required=False,
  3045. widget=APISelect(
  3046. api_url="/api/dcim/sites/",
  3047. filter_for={
  3048. 'rack': 'site_id',
  3049. 'device': 'site_id',
  3050. }
  3051. )
  3052. )
  3053. rack = ChainedModelChoiceField(
  3054. queryset=Rack.objects.all(),
  3055. chains=(
  3056. ('site', 'site'),
  3057. ),
  3058. label='Rack',
  3059. required=False,
  3060. widget=APISelect(
  3061. api_url='/api/dcim/racks/',
  3062. filter_for={
  3063. 'device': 'rack_id'
  3064. },
  3065. attrs={
  3066. 'nullable': 'true',
  3067. }
  3068. )
  3069. )
  3070. device = ChainedModelChoiceField(
  3071. queryset=Device.objects.filter(
  3072. virtual_chassis__isnull=True
  3073. ),
  3074. chains=(
  3075. ('site', 'site'),
  3076. ('rack', 'rack'),
  3077. ),
  3078. label='Device',
  3079. widget=APISelect(
  3080. api_url='/api/dcim/devices/',
  3081. display_field='display_name',
  3082. disabled_indicator='virtual_chassis'
  3083. )
  3084. )
  3085. def clean_device(self):
  3086. device = self.cleaned_data['device']
  3087. if device.virtual_chassis is not None:
  3088. raise forms.ValidationError(
  3089. "Device {} is already assigned to a virtual chassis.".format(device)
  3090. )
  3091. return device
  3092. class VirtualChassisFilterForm(BootstrapMixin, CustomFieldFilterForm):
  3093. model = VirtualChassis
  3094. q = forms.CharField(
  3095. required=False,
  3096. label='Search'
  3097. )
  3098. site = FilterChoiceField(
  3099. queryset=Site.objects.all(),
  3100. to_field_name='slug',
  3101. widget=APISelectMultiple(
  3102. api_url="/api/dcim/sites/",
  3103. value_field="slug",
  3104. )
  3105. )
  3106. tenant_group = FilterChoiceField(
  3107. queryset=TenantGroup.objects.all(),
  3108. to_field_name='slug',
  3109. null_label='-- None --',
  3110. widget=APISelectMultiple(
  3111. api_url="/api/tenancy/tenant-groups/",
  3112. value_field="slug",
  3113. null_option=True,
  3114. filter_for={
  3115. 'tenant': 'group'
  3116. }
  3117. )
  3118. )
  3119. tenant = FilterChoiceField(
  3120. queryset=Tenant.objects.all(),
  3121. to_field_name='slug',
  3122. null_label='-- None --',
  3123. widget=APISelectMultiple(
  3124. api_url="/api/tenancy/tenants/",
  3125. value_field="slug",
  3126. null_option=True,
  3127. )
  3128. )
  3129. #
  3130. # Power panels
  3131. #
  3132. class PowerPanelForm(BootstrapMixin, forms.ModelForm):
  3133. rack_group = ChainedModelChoiceField(
  3134. queryset=RackGroup.objects.all(),
  3135. chains=(
  3136. ('site', 'site'),
  3137. ),
  3138. required=False,
  3139. widget=APISelect(
  3140. api_url='/api/dcim/rack-groups/',
  3141. )
  3142. )
  3143. class Meta:
  3144. model = PowerPanel
  3145. fields = [
  3146. 'site', 'rack_group', 'name',
  3147. ]
  3148. widgets = {
  3149. 'site': APISelect(
  3150. api_url="/api/dcim/sites/",
  3151. filter_for={
  3152. 'rack_group': 'site_id',
  3153. }
  3154. ),
  3155. }
  3156. class PowerPanelCSVForm(forms.ModelForm):
  3157. site = forms.ModelChoiceField(
  3158. queryset=Site.objects.all(),
  3159. to_field_name='name',
  3160. help_text='Name of parent site',
  3161. error_messages={
  3162. 'invalid_choice': 'Site not found.',
  3163. }
  3164. )
  3165. rack_group_name = forms.CharField(
  3166. required=False,
  3167. help_text="Rack group name (optional)"
  3168. )
  3169. class Meta:
  3170. model = PowerPanel
  3171. fields = PowerPanel.csv_headers
  3172. def clean(self):
  3173. super().clean()
  3174. site = self.cleaned_data.get('site')
  3175. rack_group_name = self.cleaned_data.get('rack_group_name')
  3176. # Validate rack group
  3177. if rack_group_name:
  3178. try:
  3179. self.instance.rack_group = RackGroup.objects.get(site=site, name=rack_group_name)
  3180. except RackGroup.DoesNotExist:
  3181. raise forms.ValidationError(
  3182. "Rack group {} not found in site {}".format(rack_group_name, site)
  3183. )
  3184. class PowerPanelFilterForm(BootstrapMixin, CustomFieldFilterForm):
  3185. model = PowerPanel
  3186. q = forms.CharField(
  3187. required=False,
  3188. label='Search'
  3189. )
  3190. site = FilterChoiceField(
  3191. queryset=Site.objects.all(),
  3192. to_field_name='slug',
  3193. widget=APISelectMultiple(
  3194. api_url="/api/dcim/sites/",
  3195. value_field="slug",
  3196. filter_for={
  3197. 'rack_group_id': 'site',
  3198. }
  3199. )
  3200. )
  3201. rack_group_id = FilterChoiceField(
  3202. queryset=RackGroup.objects.all(),
  3203. label='Rack group (ID)',
  3204. null_label='-- None --',
  3205. widget=APISelectMultiple(
  3206. api_url="/api/dcim/rack-groups/",
  3207. null_option=True,
  3208. )
  3209. )
  3210. #
  3211. # Power feeds
  3212. #
  3213. class PowerFeedForm(BootstrapMixin, CustomFieldForm):
  3214. site = ChainedModelChoiceField(
  3215. queryset=Site.objects.all(),
  3216. required=False,
  3217. widget=APISelect(
  3218. api_url='/api/dcim/sites/',
  3219. filter_for={
  3220. 'power_panel': 'site_id',
  3221. 'rack': 'site_id',
  3222. }
  3223. )
  3224. )
  3225. comments = CommentField()
  3226. tags = TagField(
  3227. required=False
  3228. )
  3229. class Meta:
  3230. model = PowerFeed
  3231. fields = [
  3232. 'site', 'power_panel', 'rack', 'name', 'status', 'type', 'supply', 'phase', 'voltage', 'amperage',
  3233. 'max_utilization', 'comments', 'tags',
  3234. ]
  3235. widgets = {
  3236. 'power_panel': APISelect(
  3237. api_url="/api/dcim/power-panels/"
  3238. ),
  3239. 'rack': APISelect(
  3240. api_url="/api/dcim/racks/"
  3241. ),
  3242. 'status': StaticSelect2(),
  3243. 'type': StaticSelect2(),
  3244. 'supply': StaticSelect2(),
  3245. 'phase': StaticSelect2(),
  3246. }
  3247. def __init__(self, *args, **kwargs):
  3248. super().__init__(*args, **kwargs)
  3249. # Initialize site field
  3250. if self.instance and hasattr(self.instance, 'power_panel'):
  3251. self.initial['site'] = self.instance.power_panel.site
  3252. class PowerFeedCSVForm(forms.ModelForm):
  3253. site = forms.ModelChoiceField(
  3254. queryset=Site.objects.all(),
  3255. to_field_name='name',
  3256. help_text='Name of parent site',
  3257. error_messages={
  3258. 'invalid_choice': 'Site not found.',
  3259. }
  3260. )
  3261. panel_name = forms.ModelChoiceField(
  3262. queryset=PowerPanel.objects.all(),
  3263. to_field_name='name',
  3264. help_text='Name of upstream power panel',
  3265. error_messages={
  3266. 'invalid_choice': 'Power panel not found.',
  3267. }
  3268. )
  3269. rack_group = forms.CharField(
  3270. required=False,
  3271. help_text="Rack group name (optional)"
  3272. )
  3273. rack_name = forms.CharField(
  3274. required=False,
  3275. help_text="Rack name (optional)"
  3276. )
  3277. status = CSVChoiceField(
  3278. choices=POWERFEED_STATUS_CHOICES,
  3279. required=False,
  3280. help_text='Operational status'
  3281. )
  3282. type = CSVChoiceField(
  3283. choices=POWERFEED_TYPE_CHOICES,
  3284. required=False,
  3285. help_text='Primary or redundant'
  3286. )
  3287. supply = CSVChoiceField(
  3288. choices=POWERFEED_SUPPLY_CHOICES,
  3289. required=False,
  3290. help_text='AC/DC'
  3291. )
  3292. phase = CSVChoiceField(
  3293. choices=POWERFEED_PHASE_CHOICES,
  3294. required=False,
  3295. help_text='Single or three-phase'
  3296. )
  3297. class Meta:
  3298. model = PowerFeed
  3299. fields = PowerFeed.csv_headers
  3300. def clean(self):
  3301. super().clean()
  3302. site = self.cleaned_data.get('site')
  3303. panel_name = self.cleaned_data.get('panel_name')
  3304. rack_group = self.cleaned_data.get('rack_group')
  3305. rack_name = self.cleaned_data.get('rack_name')
  3306. # Validate power panel
  3307. if panel_name:
  3308. try:
  3309. self.instance.power_panel = PowerPanel.objects.get(site=site, name=panel_name)
  3310. except Rack.DoesNotExist:
  3311. raise forms.ValidationError(
  3312. "Power panel {} not found in site {}".format(panel_name, site)
  3313. )
  3314. # Validate rack
  3315. if rack_name:
  3316. try:
  3317. self.instance.rack = Rack.objects.get(site=site, group__name=rack_group, name=rack_name)
  3318. except Rack.DoesNotExist:
  3319. raise forms.ValidationError(
  3320. "Rack {} not found in site {}, group {}".format(rack_name, site, rack_group)
  3321. )
  3322. class PowerFeedBulkEditForm(BootstrapMixin, AddRemoveTagsForm, CustomFieldBulkEditForm):
  3323. pk = forms.ModelMultipleChoiceField(
  3324. queryset=PowerFeed.objects.all(),
  3325. widget=forms.MultipleHiddenInput
  3326. )
  3327. powerpanel = forms.ModelChoiceField(
  3328. queryset=PowerPanel.objects.all(),
  3329. required=False,
  3330. widget=APISelect(
  3331. api_url="/api/dcim/power-panels/",
  3332. filter_for={
  3333. 'rackgroup': 'site_id',
  3334. }
  3335. )
  3336. )
  3337. rack = forms.ModelChoiceField(
  3338. queryset=Rack.objects.all(),
  3339. required=False,
  3340. widget=APISelect(
  3341. api_url="/api/dcim/racks",
  3342. )
  3343. )
  3344. status = forms.ChoiceField(
  3345. choices=add_blank_choice(POWERFEED_STATUS_CHOICES),
  3346. required=False,
  3347. initial='',
  3348. widget=StaticSelect2()
  3349. )
  3350. type = forms.ChoiceField(
  3351. choices=add_blank_choice(POWERFEED_TYPE_CHOICES),
  3352. required=False,
  3353. initial='',
  3354. widget=StaticSelect2()
  3355. )
  3356. supply = forms.ChoiceField(
  3357. choices=add_blank_choice(POWERFEED_SUPPLY_CHOICES),
  3358. required=False,
  3359. initial='',
  3360. widget=StaticSelect2()
  3361. )
  3362. phase = forms.ChoiceField(
  3363. choices=add_blank_choice(POWERFEED_PHASE_CHOICES),
  3364. required=False,
  3365. initial='',
  3366. widget=StaticSelect2()
  3367. )
  3368. voltage = forms.IntegerField(
  3369. required=False
  3370. )
  3371. amperage = forms.IntegerField(
  3372. required=False
  3373. )
  3374. max_utilization = forms.IntegerField(
  3375. required=False
  3376. )
  3377. comments = forms.CharField(
  3378. required=False
  3379. )
  3380. class Meta:
  3381. nullable_fields = [
  3382. 'rackgroup', 'comments',
  3383. ]
  3384. class PowerFeedFilterForm(BootstrapMixin, CustomFieldFilterForm):
  3385. model = PowerFeed
  3386. q = forms.CharField(
  3387. required=False,
  3388. label='Search'
  3389. )
  3390. site = FilterChoiceField(
  3391. queryset=Site.objects.all(),
  3392. to_field_name='slug',
  3393. widget=APISelectMultiple(
  3394. api_url="/api/dcim/sites/",
  3395. value_field="slug",
  3396. filter_for={
  3397. 'power_panel_id': 'site',
  3398. 'rack_id': 'site',
  3399. }
  3400. )
  3401. )
  3402. power_panel_id = FilterChoiceField(
  3403. queryset=PowerPanel.objects.all(),
  3404. label='Power panel',
  3405. null_label='-- None --',
  3406. widget=APISelectMultiple(
  3407. api_url="/api/dcim/power-panels/",
  3408. null_option=True,
  3409. )
  3410. )
  3411. rack_id = FilterChoiceField(
  3412. queryset=Rack.objects.all(),
  3413. label='Rack',
  3414. null_label='-- None --',
  3415. widget=APISelectMultiple(
  3416. api_url="/api/dcim/racks/",
  3417. null_option=True,
  3418. )
  3419. )
  3420. status = forms.MultipleChoiceField(
  3421. choices=POWERFEED_STATUS_CHOICES,
  3422. required=False,
  3423. widget=StaticSelect2Multiple()
  3424. )
  3425. type = forms.ChoiceField(
  3426. choices=add_blank_choice(POWERFEED_TYPE_CHOICES),
  3427. required=False,
  3428. widget=StaticSelect2()
  3429. )
  3430. supply = forms.ChoiceField(
  3431. choices=add_blank_choice(POWERFEED_SUPPLY_CHOICES),
  3432. required=False,
  3433. widget=StaticSelect2()
  3434. )
  3435. phase = forms.ChoiceField(
  3436. choices=add_blank_choice(POWERFEED_PHASE_CHOICES),
  3437. required=False,
  3438. widget=StaticSelect2()
  3439. )
  3440. voltage = forms.IntegerField(
  3441. required=False
  3442. )
  3443. amperage = forms.IntegerField(
  3444. required=False
  3445. )
  3446. max_utilization = forms.IntegerField(
  3447. required=False
  3448. )