forms.py 131 KB

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