forms.py 147 KB

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