forms.py 152 KB

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