forms.py 160 KB

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