forms.py 134 KB

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