forms.py 124 KB

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