forms.py 135 KB

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