forms.py 132 KB

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