forms.py 130 KB

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