forms.py 148 KB

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