forms.py 133 KB

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