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