2
0

forms.py 123 KB

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