forms.py 131 KB

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