forms.py 142 KB

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