forms.py 129 KB

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