forms.py 136 KB

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