forms.py 129 KB

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