forms.py 130 KB

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