forms.py 124 KB

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