forms.py 107 KB

1234567891011121314151617181920212223242526272829303132333435363738394041424344454647484950515253545556575859606162636465666768697071727374757677787980818283848586878889909192939495969798991001011021031041051061071081091101111121131141151161171181191201211221231241251261271281291301311321331341351361371381391401411421431441451461471481491501511521531541551561571581591601611621631641651661671681691701711721731741751761771781791801811821831841851861871881891901911921931941951961971981992002012022032042052062072082092102112122132142152162172182192202212222232242252262272282292302312322332342352362372382392402412422432442452462472482492502512522532542552562572582592602612622632642652662672682692702712722732742752762772782792802812822832842852862872882892902912922932942952962972982993003013023033043053063073083093103113123133143153163173183193203213223233243253263273283293303313323333343353363373383393403413423433443453463473483493503513523533543553563573583593603613623633643653663673683693703713723733743753763773783793803813823833843853863873883893903913923933943953963973983994004014024034044054064074084094104114124134144154164174184194204214224234244254264274284294304314324334344354364374384394404414424434444454464474484494504514524534544554564574584594604614624634644654664674684694704714724734744754764774784794804814824834844854864874884894904914924934944954964974984995005015025035045055065075085095105115125135145155165175185195205215225235245255265275285295305315325335345355365375385395405415425435445455465475485495505515525535545555565575585595605615625635645655665675685695705715725735745755765775785795805815825835845855865875885895905915925935945955965975985996006016026036046056066076086096106116126136146156166176186196206216226236246256266276286296306316326336346356366376386396406416426436446456466476486496506516526536546556566576586596606616626636646656666676686696706716726736746756766776786796806816826836846856866876886896906916926936946956966976986997007017027037047057067077087097107117127137147157167177187197207217227237247257267277287297307317327337347357367377387397407417427437447457467477487497507517527537547557567577587597607617627637647657667677687697707717727737747757767777787797807817827837847857867877887897907917927937947957967977987998008018028038048058068078088098108118128138148158168178188198208218228238248258268278288298308318328338348358368378388398408418428438448458468478488498508518528538548558568578588598608618628638648658668678688698708718728738748758768778788798808818828838848858868878888898908918928938948958968978988999009019029039049059069079089099109119129139149159169179189199209219229239249259269279289299309319329339349359369379389399409419429439449459469479489499509519529539549559569579589599609619629639649659669679689699709719729739749759769779789799809819829839849859869879889899909919929939949959969979989991000100110021003100410051006100710081009101010111012101310141015101610171018101910201021102210231024102510261027102810291030103110321033103410351036103710381039104010411042104310441045104610471048104910501051105210531054105510561057105810591060106110621063106410651066106710681069107010711072107310741075107610771078107910801081108210831084108510861087108810891090109110921093109410951096109710981099110011011102110311041105110611071108110911101111111211131114111511161117111811191120112111221123112411251126112711281129113011311132113311341135113611371138113911401141114211431144114511461147114811491150115111521153115411551156115711581159116011611162116311641165116611671168116911701171117211731174117511761177117811791180118111821183118411851186118711881189119011911192119311941195119611971198119912001201120212031204120512061207120812091210121112121213121412151216121712181219122012211222122312241225122612271228122912301231123212331234123512361237123812391240124112421243124412451246124712481249125012511252125312541255125612571258125912601261126212631264126512661267126812691270127112721273127412751276127712781279128012811282128312841285128612871288128912901291129212931294129512961297129812991300130113021303130413051306130713081309131013111312131313141315131613171318131913201321132213231324132513261327132813291330133113321333133413351336133713381339134013411342134313441345134613471348134913501351135213531354135513561357135813591360136113621363136413651366136713681369137013711372137313741375137613771378137913801381138213831384138513861387138813891390139113921393139413951396139713981399140014011402140314041405140614071408140914101411141214131414141514161417141814191420142114221423142414251426142714281429143014311432143314341435143614371438143914401441144214431444144514461447144814491450145114521453145414551456145714581459146014611462146314641465146614671468146914701471147214731474147514761477147814791480148114821483148414851486148714881489149014911492149314941495149614971498149915001501150215031504150515061507150815091510151115121513151415151516151715181519152015211522152315241525152615271528152915301531153215331534153515361537153815391540154115421543154415451546154715481549155015511552155315541555155615571558155915601561156215631564156515661567156815691570157115721573157415751576157715781579158015811582158315841585158615871588158915901591159215931594159515961597159815991600160116021603160416051606160716081609161016111612161316141615161616171618161916201621162216231624162516261627162816291630163116321633163416351636163716381639164016411642164316441645164616471648164916501651165216531654165516561657165816591660166116621663166416651666166716681669167016711672167316741675167616771678167916801681168216831684168516861687168816891690169116921693169416951696169716981699170017011702170317041705170617071708170917101711171217131714171517161717171817191720172117221723172417251726172717281729173017311732173317341735173617371738173917401741174217431744174517461747174817491750175117521753175417551756175717581759176017611762176317641765176617671768176917701771177217731774177517761777177817791780178117821783178417851786178717881789179017911792179317941795179617971798179918001801180218031804180518061807180818091810181118121813181418151816181718181819182018211822182318241825182618271828182918301831183218331834183518361837183818391840184118421843184418451846184718481849185018511852185318541855185618571858185918601861186218631864186518661867186818691870187118721873187418751876187718781879188018811882188318841885188618871888188918901891189218931894189518961897189818991900190119021903190419051906190719081909191019111912191319141915191619171918191919201921192219231924192519261927192819291930193119321933193419351936193719381939194019411942194319441945194619471948194919501951195219531954195519561957195819591960196119621963196419651966196719681969197019711972197319741975197619771978197919801981198219831984198519861987198819891990199119921993199419951996199719981999200020012002200320042005200620072008200920102011201220132014201520162017201820192020202120222023202420252026202720282029203020312032203320342035203620372038203920402041204220432044204520462047204820492050205120522053205420552056205720582059206020612062206320642065206620672068206920702071207220732074207520762077207820792080208120822083208420852086208720882089209020912092209320942095209620972098209921002101210221032104210521062107210821092110211121122113211421152116211721182119212021212122212321242125212621272128212921302131213221332134213521362137213821392140214121422143214421452146214721482149215021512152215321542155215621572158215921602161216221632164216521662167216821692170217121722173217421752176217721782179218021812182218321842185218621872188218921902191219221932194219521962197219821992200220122022203220422052206220722082209221022112212221322142215221622172218221922202221222222232224222522262227222822292230223122322233223422352236223722382239224022412242224322442245224622472248224922502251225222532254225522562257225822592260226122622263226422652266226722682269227022712272227322742275227622772278227922802281228222832284228522862287228822892290229122922293229422952296229722982299230023012302230323042305230623072308230923102311231223132314231523162317231823192320232123222323232423252326232723282329233023312332233323342335233623372338233923402341234223432344234523462347234823492350235123522353235423552356235723582359236023612362236323642365236623672368236923702371237223732374237523762377237823792380238123822383238423852386238723882389239023912392239323942395239623972398239924002401240224032404240524062407240824092410241124122413241424152416241724182419242024212422242324242425242624272428242924302431243224332434243524362437243824392440244124422443244424452446244724482449245024512452245324542455245624572458245924602461246224632464246524662467246824692470247124722473247424752476247724782479248024812482248324842485248624872488248924902491249224932494249524962497249824992500250125022503250425052506250725082509251025112512251325142515251625172518251925202521252225232524252525262527252825292530253125322533253425352536253725382539254025412542254325442545254625472548254925502551255225532554255525562557255825592560256125622563256425652566256725682569257025712572257325742575257625772578257925802581258225832584258525862587258825892590259125922593259425952596259725982599260026012602260326042605260626072608260926102611261226132614261526162617261826192620262126222623262426252626262726282629263026312632263326342635263626372638263926402641264226432644264526462647264826492650265126522653265426552656265726582659266026612662266326642665266626672668266926702671267226732674267526762677267826792680268126822683268426852686268726882689269026912692269326942695269626972698269927002701270227032704270527062707270827092710271127122713271427152716271727182719272027212722272327242725272627272728272927302731273227332734273527362737273827392740274127422743274427452746274727482749275027512752275327542755275627572758275927602761276227632764276527662767276827692770277127722773277427752776277727782779278027812782278327842785278627872788278927902791279227932794279527962797279827992800280128022803280428052806280728082809281028112812281328142815281628172818281928202821282228232824282528262827282828292830283128322833283428352836283728382839284028412842284328442845284628472848284928502851285228532854285528562857285828592860286128622863286428652866286728682869287028712872287328742875287628772878287928802881288228832884288528862887288828892890289128922893289428952896289728982899290029012902290329042905290629072908290929102911291229132914291529162917291829192920292129222923292429252926292729282929293029312932293329342935293629372938293929402941294229432944294529462947294829492950295129522953295429552956295729582959296029612962296329642965296629672968296929702971297229732974297529762977297829792980298129822983298429852986298729882989299029912992299329942995299629972998299930003001300230033004300530063007300830093010301130123013301430153016301730183019302030213022302330243025302630273028302930303031303230333034303530363037303830393040304130423043304430453046304730483049305030513052305330543055305630573058305930603061306230633064306530663067306830693070307130723073307430753076307730783079308030813082308330843085308630873088308930903091309230933094309530963097309830993100310131023103310431053106310731083109311031113112311331143115311631173118311931203121312231233124312531263127312831293130313131323133313431353136313731383139314031413142314331443145314631473148314931503151315231533154315531563157315831593160316131623163316431653166316731683169317031713172317331743175317631773178317931803181318231833184318531863187318831893190319131923193319431953196319731983199320032013202320332043205320632073208320932103211321232133214321532163217321832193220322132223223322432253226322732283229323032313232323332343235323632373238323932403241324232433244324532463247324832493250325132523253325432553256325732583259326032613262326332643265326632673268326932703271327232733274327532763277327832793280328132823283328432853286328732883289329032913292329332943295329632973298329933003301330233033304330533063307330833093310331133123313331433153316331733183319332033213322332333243325332633273328332933303331333233333334333533363337333833393340334133423343334433453346334733483349335033513352335333543355335633573358335933603361336233633364336533663367336833693370337133723373337433753376337733783379338033813382338333843385338633873388338933903391339233933394339533963397339833993400340134023403340434053406340734083409341034113412341334143415341634173418341934203421342234233424342534263427342834293430343134323433343434353436343734383439344034413442344334443445344634473448344934503451345234533454345534563457345834593460346134623463346434653466346734683469347034713472347334743475347634773478347934803481348234833484348534863487348834893490349134923493349434953496349734983499350035013502350335043505350635073508350935103511351235133514351535163517351835193520352135223523352435253526352735283529353035313532353335343535353635373538353935403541354235433544354535463547354835493550355135523553355435553556355735583559356035613562356335643565356635673568356935703571357235733574357535763577357835793580358135823583358435853586358735883589359035913592359335943595359635973598359936003601360236033604360536063607360836093610361136123613361436153616361736183619362036213622362336243625362636273628362936303631363236333634363536363637363836393640364136423643364436453646364736483649365036513652365336543655365636573658365936603661366236633664366536663667366836693670367136723673367436753676367736783679368036813682368336843685368636873688368936903691369236933694369536963697369836993700370137023703370437053706370737083709371037113712371337143715371637173718371937203721372237233724372537263727372837293730373137323733373437353736373737383739374037413742374337443745374637473748374937503751375237533754375537563757375837593760376137623763376437653766376737683769377037713772377337743775377637773778377937803781378237833784378537863787378837893790379137923793379437953796379737983799380038013802380338043805380638073808380938103811381238133814381538163817381838193820382138223823382438253826382738283829383038313832383338343835383638373838383938403841384238433844
  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.db.models import Q
  8. from mptt.forms import TreeNodeChoiceField
  9. from netaddr import EUI
  10. from netaddr.core import AddrFormatError
  11. from taggit.forms import TagField
  12. from timezone_field import TimeZoneFormField
  13. from circuits.models import Circuit, Provider
  14. from extras.forms import (
  15. AddRemoveTagsForm, CustomFieldForm, CustomFieldBulkEditForm, CustomFieldFilterForm, LocalConfigContextFilterForm
  16. )
  17. from ipam.models import IPAddress, VLAN, VLANGroup
  18. from tenancy.forms import TenancyFilterForm, TenancyForm
  19. from tenancy.models import Tenant, TenantGroup
  20. from utilities.forms import (
  21. APISelect, APISelectMultiple, add_blank_choice, ArrayFieldSelectMultiple, BootstrapMixin, BulkEditForm,
  22. BulkEditNullBooleanSelect, ChainedFieldsMixin, ChainedModelChoiceField, ColorSelect, CommentField, ComponentForm,
  23. ConfirmationForm, CSVChoiceField, ExpandableNameField, FilterChoiceField, FlexibleModelChoiceField, JSONField,
  24. SelectWithPK, SmallTextarea, SlugField, StaticSelect2, StaticSelect2Multiple, BOOLEAN_WITH_BLANK_CHOICES
  25. )
  26. from virtualization.models import Cluster, ClusterGroup
  27. from .constants import *
  28. from .models import (
  29. Cable, DeviceBay, DeviceBayTemplate, ConsolePort, ConsolePortTemplate, ConsoleServerPort, ConsoleServerPortTemplate,
  30. Device, DeviceRole, DeviceType, FrontPort, FrontPortTemplate, Interface, InterfaceTemplate, Manufacturer,
  31. InventoryItem, Platform, PowerFeed, PowerOutlet, PowerOutletTemplate, PowerPanel, PowerPort, PowerPortTemplate,
  32. Rack, RackGroup, RackReservation, RackRole, RearPort, RearPortTemplate, Region, Site, VirtualChassis,
  33. )
  34. DEVICE_BY_PK_RE = r'{\d+\}'
  35. INTERFACE_MODE_HELP_TEXT = """
  36. Access: One untagged VLAN<br />
  37. Tagged: One untagged VLAN and/or one or more tagged VLANs<br />
  38. Tagged All: Implies all VLANs are available (w/optional untagged VLAN)
  39. """
  40. def get_device_by_name_or_pk(name):
  41. """
  42. Attempt to retrieve a device by either its name or primary key ('{pk}').
  43. """
  44. if re.match(DEVICE_BY_PK_RE, name):
  45. pk = name.strip('{}')
  46. device = Device.objects.get(pk=pk)
  47. else:
  48. device = Device.objects.get(name=name)
  49. return device
  50. class InterfaceCommonForm:
  51. def clean(self):
  52. super().clean()
  53. # Validate VLAN assignments
  54. tagged_vlans = self.cleaned_data['tagged_vlans']
  55. # Untagged interfaces cannot be assigned tagged VLANs
  56. if self.cleaned_data['mode'] == IFACE_MODE_ACCESS and tagged_vlans:
  57. raise forms.ValidationError({
  58. 'mode': "An access interface cannot have tagged VLANs assigned."
  59. })
  60. # Remove all tagged VLAN assignments from "tagged all" interfaces
  61. elif self.cleaned_data['mode'] == IFACE_MODE_TAGGED_ALL:
  62. self.cleaned_data['tagged_vlans'] = []
  63. # Validate tagged VLANs; must be a global VLAN or in the same site
  64. else:
  65. valid_sites = [None, self.cleaned_data['device'].site]
  66. invalid_vlans = [str(v) for v in tagged_vlans if v.site not in valid_sites]
  67. if invalid_vlans:
  68. raise forms.ValidationError({
  69. 'tagged_vlans': "The tagged VLANs ({}) must belong to the same site as the interface's parent "
  70. "device/VM, or they must be global".format(', '.join(invalid_vlans))
  71. })
  72. class BulkRenameForm(forms.Form):
  73. """
  74. An extendable form to be used for renaming device components in bulk.
  75. """
  76. find = forms.CharField()
  77. replace = forms.CharField()
  78. use_regex = forms.BooleanField(
  79. required=False,
  80. initial=True,
  81. label='Use regular expressions'
  82. )
  83. def clean(self):
  84. # Validate regular expression in "find" field
  85. if self.cleaned_data['use_regex']:
  86. try:
  87. re.compile(self.cleaned_data['find'])
  88. except re.error:
  89. raise forms.ValidationError({
  90. 'find': "Invalid regular expression"
  91. })
  92. #
  93. # Fields
  94. #
  95. class MACAddressField(forms.Field):
  96. widget = forms.CharField
  97. default_error_messages = {
  98. 'invalid': 'MAC address must be in EUI-48 format',
  99. }
  100. def to_python(self, value):
  101. value = super().to_python(value)
  102. # Validate MAC address format
  103. try:
  104. value = EUI(value.strip())
  105. except AddrFormatError:
  106. raise forms.ValidationError(self.error_messages['invalid'], code='invalid')
  107. return value
  108. #
  109. # Regions
  110. #
  111. class RegionForm(BootstrapMixin, forms.ModelForm):
  112. slug = SlugField()
  113. class Meta:
  114. model = Region
  115. fields = [
  116. 'parent', 'name', 'slug',
  117. ]
  118. widgets = {
  119. 'parent': APISelect(
  120. api_url="/api/dcim/regions/"
  121. )
  122. }
  123. class RegionCSVForm(forms.ModelForm):
  124. parent = forms.ModelChoiceField(
  125. queryset=Region.objects.all(),
  126. required=False,
  127. to_field_name='name',
  128. help_text='Name of parent region',
  129. error_messages={
  130. 'invalid_choice': 'Region not found.',
  131. }
  132. )
  133. class Meta:
  134. model = Region
  135. fields = Region.csv_headers
  136. help_texts = {
  137. 'name': 'Region name',
  138. 'slug': 'URL-friendly slug',
  139. }
  140. class RegionFilterForm(BootstrapMixin, forms.Form):
  141. model = Site
  142. q = forms.CharField(
  143. required=False,
  144. label='Search'
  145. )
  146. #
  147. # Sites
  148. #
  149. class SiteForm(BootstrapMixin, TenancyForm, CustomFieldForm):
  150. region = TreeNodeChoiceField(
  151. queryset=Region.objects.all(),
  152. required=False,
  153. widget=APISelect(
  154. api_url="/api/dcim/regions/"
  155. )
  156. )
  157. slug = SlugField()
  158. comments = CommentField()
  159. tags = TagField(
  160. required=False
  161. )
  162. class Meta:
  163. model = Site
  164. fields = [
  165. 'name', 'slug', 'status', 'region', 'tenant_group', 'tenant', 'facility', 'asn', 'time_zone', 'description',
  166. 'physical_address', 'shipping_address', 'latitude', 'longitude', 'contact_name', 'contact_phone',
  167. 'contact_email', 'comments', 'tags',
  168. ]
  169. widgets = {
  170. 'physical_address': SmallTextarea(
  171. attrs={
  172. 'rows': 3,
  173. }
  174. ),
  175. 'shipping_address': SmallTextarea(
  176. attrs={
  177. 'rows': 3,
  178. }
  179. ),
  180. 'status': StaticSelect2(),
  181. 'time_zone': StaticSelect2(),
  182. }
  183. help_texts = {
  184. 'name': "Full name of the site",
  185. 'facility': "Data center provider and facility (e.g. Equinix NY7)",
  186. 'asn': "BGP autonomous system number",
  187. 'time_zone': "Local time zone",
  188. 'description': "Short description (will appear in sites list)",
  189. 'physical_address': "Physical location of the building (e.g. for GPS)",
  190. 'shipping_address': "If different from the physical address",
  191. 'latitude': "Latitude in decimal format (xx.yyyyyy)",
  192. 'longitude': "Longitude in decimal format (xx.yyyyyy)"
  193. }
  194. class SiteCSVForm(forms.ModelForm):
  195. status = CSVChoiceField(
  196. choices=SITE_STATUS_CHOICES,
  197. required=False,
  198. help_text='Operational status'
  199. )
  200. region = forms.ModelChoiceField(
  201. queryset=Region.objects.all(),
  202. required=False,
  203. to_field_name='name',
  204. help_text='Name of assigned region',
  205. error_messages={
  206. 'invalid_choice': 'Region not found.',
  207. }
  208. )
  209. tenant = forms.ModelChoiceField(
  210. queryset=Tenant.objects.all(),
  211. required=False,
  212. to_field_name='name',
  213. help_text='Name of assigned tenant',
  214. error_messages={
  215. 'invalid_choice': 'Tenant not found.',
  216. }
  217. )
  218. class Meta:
  219. model = Site
  220. fields = Site.csv_headers
  221. help_texts = {
  222. 'name': 'Site name',
  223. 'slug': 'URL-friendly slug',
  224. 'asn': '32-bit autonomous system number',
  225. }
  226. class SiteBulkEditForm(BootstrapMixin, AddRemoveTagsForm, CustomFieldBulkEditForm):
  227. pk = forms.ModelMultipleChoiceField(
  228. queryset=Site.objects.all(),
  229. widget=forms.MultipleHiddenInput
  230. )
  231. status = forms.ChoiceField(
  232. choices=add_blank_choice(SITE_STATUS_CHOICES),
  233. required=False,
  234. initial='',
  235. widget=StaticSelect2()
  236. )
  237. region = TreeNodeChoiceField(
  238. queryset=Region.objects.all(),
  239. required=False,
  240. widget=APISelect(
  241. api_url="/api/dcim/regions/"
  242. )
  243. )
  244. tenant = forms.ModelChoiceField(
  245. queryset=Tenant.objects.all(),
  246. required=False,
  247. widget=APISelect(
  248. api_url="/api/tenancy/tenants",
  249. )
  250. )
  251. asn = forms.IntegerField(
  252. min_value=1,
  253. max_value=4294967295,
  254. required=False,
  255. label='ASN'
  256. )
  257. description = forms.CharField(
  258. max_length=100,
  259. required=False
  260. )
  261. time_zone = TimeZoneFormField(
  262. choices=add_blank_choice(TimeZoneFormField().choices),
  263. required=False,
  264. widget=StaticSelect2()
  265. )
  266. class Meta:
  267. nullable_fields = [
  268. 'region', 'tenant', 'asn', 'description', 'time_zone',
  269. ]
  270. class SiteFilterForm(BootstrapMixin, TenancyFilterForm, CustomFieldFilterForm):
  271. model = Site
  272. field_order = ['q', 'status', 'region', 'tenant_group', 'tenant']
  273. q = forms.CharField(
  274. required=False,
  275. label='Search'
  276. )
  277. status = forms.MultipleChoiceField(
  278. choices=SITE_STATUS_CHOICES,
  279. required=False,
  280. widget=StaticSelect2Multiple()
  281. )
  282. region = forms.ModelMultipleChoiceField(
  283. queryset=Region.objects.all(),
  284. to_field_name='slug',
  285. required=False,
  286. widget=APISelectMultiple(
  287. api_url="/api/dcim/regions/",
  288. value_field="slug",
  289. )
  290. )
  291. #
  292. # Rack groups
  293. #
  294. class RackGroupForm(BootstrapMixin, forms.ModelForm):
  295. slug = SlugField()
  296. class Meta:
  297. model = RackGroup
  298. fields = [
  299. 'site', 'name', 'slug',
  300. ]
  301. widgets = {
  302. 'site': APISelect(
  303. api_url="/api/dcim/sites/"
  304. )
  305. }
  306. class RackGroupCSVForm(forms.ModelForm):
  307. site = forms.ModelChoiceField(
  308. queryset=Site.objects.all(),
  309. to_field_name='name',
  310. help_text='Name of parent site',
  311. error_messages={
  312. 'invalid_choice': 'Site not found.',
  313. }
  314. )
  315. class Meta:
  316. model = RackGroup
  317. fields = RackGroup.csv_headers
  318. help_texts = {
  319. 'name': 'Name of rack group',
  320. 'slug': 'URL-friendly slug',
  321. }
  322. class RackGroupFilterForm(BootstrapMixin, forms.Form):
  323. site = FilterChoiceField(
  324. queryset=Site.objects.all(),
  325. to_field_name='slug',
  326. widget=APISelectMultiple(
  327. api_url="/api/dcim/sites/",
  328. value_field="slug",
  329. )
  330. )
  331. #
  332. # Rack roles
  333. #
  334. class RackRoleForm(BootstrapMixin, forms.ModelForm):
  335. slug = SlugField()
  336. class Meta:
  337. model = RackRole
  338. fields = [
  339. 'name', 'slug', 'color',
  340. ]
  341. class RackRoleCSVForm(forms.ModelForm):
  342. slug = SlugField()
  343. class Meta:
  344. model = RackRole
  345. fields = RackRole.csv_headers
  346. help_texts = {
  347. 'name': 'Name of rack role',
  348. 'color': 'RGB color in hexadecimal (e.g. 00ff00)'
  349. }
  350. #
  351. # Racks
  352. #
  353. class RackForm(BootstrapMixin, TenancyForm, CustomFieldForm):
  354. group = ChainedModelChoiceField(
  355. queryset=RackGroup.objects.all(),
  356. chains=(
  357. ('site', 'site'),
  358. ),
  359. required=False,
  360. widget=APISelect(
  361. api_url='/api/dcim/rack-groups/',
  362. )
  363. )
  364. comments = CommentField()
  365. tags = TagField(
  366. required=False
  367. )
  368. class Meta:
  369. model = Rack
  370. fields = [
  371. 'site', 'group', 'name', 'facility_id', 'tenant_group', 'tenant', 'status', 'role', 'serial', 'asset_tag',
  372. 'type', 'width', 'u_height', 'desc_units', 'outer_width', 'outer_depth', 'outer_unit', 'comments', 'tags',
  373. ]
  374. help_texts = {
  375. 'site': "The site at which the rack exists",
  376. 'name': "Organizational rack name",
  377. 'facility_id': "The unique rack ID assigned by the facility",
  378. 'u_height': "Height in rack units",
  379. }
  380. widgets = {
  381. 'site': APISelect(
  382. api_url="/api/dcim/sites/",
  383. filter_for={
  384. 'group': 'site_id',
  385. }
  386. ),
  387. 'status': StaticSelect2(),
  388. 'role': APISelect(
  389. api_url="/api/dcim/rack-roles/"
  390. ),
  391. 'type': StaticSelect2(),
  392. 'width': StaticSelect2(),
  393. 'outer_unit': StaticSelect2(),
  394. }
  395. class RackCSVForm(forms.ModelForm):
  396. site = forms.ModelChoiceField(
  397. queryset=Site.objects.all(),
  398. to_field_name='name',
  399. help_text='Name of parent site',
  400. error_messages={
  401. 'invalid_choice': 'Site not found.',
  402. }
  403. )
  404. group_name = forms.CharField(
  405. help_text='Name of rack group',
  406. required=False
  407. )
  408. tenant = forms.ModelChoiceField(
  409. queryset=Tenant.objects.all(),
  410. required=False,
  411. to_field_name='name',
  412. help_text='Name of assigned tenant',
  413. error_messages={
  414. 'invalid_choice': 'Tenant not found.',
  415. }
  416. )
  417. status = CSVChoiceField(
  418. choices=RACK_STATUS_CHOICES,
  419. required=False,
  420. help_text='Operational status'
  421. )
  422. role = forms.ModelChoiceField(
  423. queryset=RackRole.objects.all(),
  424. required=False,
  425. to_field_name='name',
  426. help_text='Name of assigned role',
  427. error_messages={
  428. 'invalid_choice': 'Role not found.',
  429. }
  430. )
  431. type = CSVChoiceField(
  432. choices=RACK_TYPE_CHOICES,
  433. required=False,
  434. help_text='Rack type'
  435. )
  436. width = forms.ChoiceField(
  437. choices=(
  438. (RACK_WIDTH_19IN, '19'),
  439. (RACK_WIDTH_23IN, '23'),
  440. ),
  441. help_text='Rail-to-rail width (in inches)'
  442. )
  443. outer_unit = CSVChoiceField(
  444. choices=RACK_DIMENSION_UNIT_CHOICES,
  445. required=False,
  446. help_text='Unit for outer dimensions'
  447. )
  448. class Meta:
  449. model = Rack
  450. fields = Rack.csv_headers
  451. help_texts = {
  452. 'name': 'Rack name',
  453. 'u_height': 'Height in rack units',
  454. }
  455. def clean(self):
  456. super().clean()
  457. site = self.cleaned_data.get('site')
  458. group_name = self.cleaned_data.get('group_name')
  459. name = self.cleaned_data.get('name')
  460. facility_id = self.cleaned_data.get('facility_id')
  461. # Validate rack group
  462. if group_name:
  463. try:
  464. self.instance.group = RackGroup.objects.get(site=site, name=group_name)
  465. except RackGroup.DoesNotExist:
  466. raise forms.ValidationError("Rack group {} not found for site {}".format(group_name, site))
  467. # Validate uniqueness of rack name within group
  468. if Rack.objects.filter(group=self.instance.group, name=name).exists():
  469. raise forms.ValidationError(
  470. "A rack named {} already exists within group {}".format(name, group_name)
  471. )
  472. # Validate uniqueness of facility ID within group
  473. if facility_id and Rack.objects.filter(group=self.instance.group, facility_id=facility_id).exists():
  474. raise forms.ValidationError(
  475. "A rack with the facility ID {} already exists within group {}".format(facility_id, group_name)
  476. )
  477. class RackBulkEditForm(BootstrapMixin, AddRemoveTagsForm, CustomFieldBulkEditForm):
  478. pk = forms.ModelMultipleChoiceField(
  479. queryset=Rack.objects.all(),
  480. widget=forms.MultipleHiddenInput
  481. )
  482. site = forms.ModelChoiceField(
  483. queryset=Site.objects.all(),
  484. required=False,
  485. widget=APISelect(
  486. api_url="/api/dcim/sites",
  487. filter_for={
  488. 'group': 'site_id',
  489. }
  490. )
  491. )
  492. group = forms.ModelChoiceField(
  493. queryset=RackGroup.objects.all(),
  494. required=False,
  495. widget=APISelect(
  496. api_url="/api/dcim/rack-groups",
  497. )
  498. )
  499. tenant = forms.ModelChoiceField(
  500. queryset=Tenant.objects.all(),
  501. required=False,
  502. widget=APISelect(
  503. api_url="/api/tenancy/tenants",
  504. )
  505. )
  506. status = forms.ChoiceField(
  507. choices=add_blank_choice(RACK_STATUS_CHOICES),
  508. required=False,
  509. initial='',
  510. widget=StaticSelect2()
  511. )
  512. role = forms.ModelChoiceField(
  513. queryset=RackRole.objects.all(),
  514. required=False,
  515. widget=APISelect(
  516. api_url="/api/dcim/rack-roles",
  517. )
  518. )
  519. serial = forms.CharField(
  520. max_length=50,
  521. required=False,
  522. label='Serial Number'
  523. )
  524. asset_tag = forms.CharField(
  525. max_length=50,
  526. required=False
  527. )
  528. type = forms.ChoiceField(
  529. choices=add_blank_choice(RACK_TYPE_CHOICES),
  530. required=False,
  531. widget=StaticSelect2()
  532. )
  533. width = forms.ChoiceField(
  534. choices=add_blank_choice(RACK_WIDTH_CHOICES),
  535. required=False,
  536. widget=StaticSelect2()
  537. )
  538. u_height = forms.IntegerField(
  539. required=False,
  540. label='Height (U)'
  541. )
  542. desc_units = forms.NullBooleanField(
  543. required=False,
  544. widget=BulkEditNullBooleanSelect,
  545. label='Descending units'
  546. )
  547. outer_width = forms.IntegerField(
  548. required=False,
  549. min_value=1
  550. )
  551. outer_depth = forms.IntegerField(
  552. required=False,
  553. min_value=1
  554. )
  555. outer_unit = forms.ChoiceField(
  556. choices=add_blank_choice(RACK_DIMENSION_UNIT_CHOICES),
  557. required=False,
  558. widget=StaticSelect2()
  559. )
  560. comments = CommentField(
  561. widget=SmallTextarea
  562. )
  563. class Meta:
  564. nullable_fields = [
  565. 'group', 'tenant', 'role', 'serial', 'asset_tag', 'outer_width', 'outer_depth', 'outer_unit', 'comments',
  566. ]
  567. class RackFilterForm(BootstrapMixin, TenancyFilterForm, CustomFieldFilterForm):
  568. model = Rack
  569. field_order = ['q', 'site', 'group_id', 'status', 'role', 'tenant_group', 'tenant']
  570. q = forms.CharField(
  571. required=False,
  572. label='Search'
  573. )
  574. site = FilterChoiceField(
  575. queryset=Site.objects.all(),
  576. to_field_name='slug',
  577. widget=APISelectMultiple(
  578. api_url="/api/dcim/sites/",
  579. value_field="slug",
  580. filter_for={
  581. 'group_id': 'site'
  582. }
  583. )
  584. )
  585. group_id = ChainedModelChoiceField(
  586. label='Rack group',
  587. queryset=RackGroup.objects.prefetch_related('site'),
  588. chains=(
  589. ('site', 'site'),
  590. ),
  591. required=False,
  592. widget=APISelectMultiple(
  593. api_url="/api/dcim/rack-groups/",
  594. null_option=True,
  595. )
  596. )
  597. status = forms.MultipleChoiceField(
  598. choices=RACK_STATUS_CHOICES,
  599. required=False,
  600. widget=StaticSelect2Multiple()
  601. )
  602. role = FilterChoiceField(
  603. queryset=RackRole.objects.all(),
  604. to_field_name='slug',
  605. null_label='-- None --',
  606. widget=APISelectMultiple(
  607. api_url="/api/dcim/rack-roles/",
  608. value_field="slug",
  609. null_option=True,
  610. )
  611. )
  612. #
  613. # Rack reservations
  614. #
  615. class RackReservationForm(BootstrapMixin, TenancyForm, forms.ModelForm):
  616. units = SimpleArrayField(
  617. base_field=forms.IntegerField(),
  618. widget=ArrayFieldSelectMultiple(
  619. attrs={
  620. 'size': 10,
  621. }
  622. )
  623. )
  624. user = forms.ModelChoiceField(
  625. queryset=User.objects.order_by(
  626. 'username'
  627. ),
  628. widget=StaticSelect2()
  629. )
  630. class Meta:
  631. model = RackReservation
  632. fields = [
  633. 'units', 'user', 'tenant_group', 'tenant', 'description',
  634. ]
  635. def __init__(self, *args, **kwargs):
  636. super().__init__(*args, **kwargs)
  637. # Populate rack unit choices
  638. self.fields['units'].widget.choices = self._get_unit_choices()
  639. def _get_unit_choices(self):
  640. rack = self.instance.rack
  641. reserved_units = []
  642. for resv in rack.reservations.exclude(pk=self.instance.pk):
  643. for u in resv.units:
  644. reserved_units.append(u)
  645. unit_choices = [(u, {'label': str(u), 'disabled': u in reserved_units}) for u in rack.units]
  646. return unit_choices
  647. class RackReservationBulkEditForm(BootstrapMixin, BulkEditForm):
  648. pk = forms.ModelMultipleChoiceField(
  649. queryset=RackReservation.objects.all(),
  650. widget=forms.MultipleHiddenInput()
  651. )
  652. user = forms.ModelChoiceField(
  653. queryset=User.objects.order_by(
  654. 'username'
  655. ),
  656. required=False,
  657. widget=StaticSelect2()
  658. )
  659. tenant = forms.ModelChoiceField(
  660. queryset=Tenant.objects.all(),
  661. required=False,
  662. widget=APISelect(
  663. api_url="/api/tenancy/tenant",
  664. )
  665. )
  666. description = forms.CharField(
  667. max_length=100,
  668. required=False
  669. )
  670. class Meta:
  671. nullable_fields = []
  672. class RackReservationFilterForm(BootstrapMixin, TenancyFilterForm):
  673. field_order = ['q', 'site', 'group_id', 'tenant_group', 'tenant']
  674. q = forms.CharField(
  675. required=False,
  676. label='Search'
  677. )
  678. site = FilterChoiceField(
  679. queryset=Site.objects.all(),
  680. to_field_name='slug',
  681. widget=APISelectMultiple(
  682. api_url="/api/dcim/sites/",
  683. value_field="slug",
  684. )
  685. )
  686. group_id = FilterChoiceField(
  687. queryset=RackGroup.objects.prefetch_related('site'),
  688. label='Rack group',
  689. null_label='-- None --',
  690. widget=APISelectMultiple(
  691. api_url="/api/dcim/rack-groups/",
  692. null_option=True,
  693. )
  694. )
  695. #
  696. # Manufacturers
  697. #
  698. class ManufacturerForm(BootstrapMixin, forms.ModelForm):
  699. slug = SlugField()
  700. class Meta:
  701. model = Manufacturer
  702. fields = [
  703. 'name', 'slug',
  704. ]
  705. class ManufacturerCSVForm(forms.ModelForm):
  706. class Meta:
  707. model = Manufacturer
  708. fields = Manufacturer.csv_headers
  709. help_texts = {
  710. 'name': 'Manufacturer name',
  711. 'slug': 'URL-friendly slug',
  712. }
  713. #
  714. # Device types
  715. #
  716. class DeviceTypeForm(BootstrapMixin, CustomFieldForm):
  717. slug = SlugField(
  718. slug_source='model'
  719. )
  720. comments = CommentField()
  721. tags = TagField(
  722. required=False
  723. )
  724. class Meta:
  725. model = DeviceType
  726. fields = [
  727. 'manufacturer', 'model', 'slug', 'part_number', 'u_height', 'is_full_depth', 'subdevice_role', 'comments',
  728. 'tags',
  729. ]
  730. widgets = {
  731. 'manufacturer': APISelect(
  732. api_url="/api/dcim/manufacturers/"
  733. ),
  734. 'subdevice_role': StaticSelect2()
  735. }
  736. class DeviceTypeCSVForm(forms.ModelForm):
  737. manufacturer = forms.ModelChoiceField(
  738. queryset=Manufacturer.objects.all(),
  739. required=True,
  740. to_field_name='name',
  741. help_text='Manufacturer name',
  742. error_messages={
  743. 'invalid_choice': 'Manufacturer not found.',
  744. }
  745. )
  746. subdevice_role = CSVChoiceField(
  747. choices=SUBDEVICE_ROLE_CHOICES,
  748. required=False,
  749. help_text='Parent/child status'
  750. )
  751. class Meta:
  752. model = DeviceType
  753. fields = DeviceType.csv_headers
  754. help_texts = {
  755. 'model': 'Model name',
  756. 'slug': 'URL-friendly slug',
  757. }
  758. class DeviceTypeBulkEditForm(BootstrapMixin, AddRemoveTagsForm, CustomFieldBulkEditForm):
  759. pk = forms.ModelMultipleChoiceField(
  760. queryset=DeviceType.objects.all(),
  761. widget=forms.MultipleHiddenInput()
  762. )
  763. manufacturer = forms.ModelChoiceField(
  764. queryset=Manufacturer.objects.all(),
  765. required=False,
  766. widget=APISelect(
  767. api_url="/api/dcim/manufactureres"
  768. )
  769. )
  770. u_height = forms.IntegerField(
  771. min_value=1,
  772. required=False
  773. )
  774. is_full_depth = forms.NullBooleanField(
  775. required=False,
  776. widget=BulkEditNullBooleanSelect(),
  777. label='Is full depth'
  778. )
  779. class Meta:
  780. nullable_fields = []
  781. class DeviceTypeFilterForm(BootstrapMixin, CustomFieldFilterForm):
  782. model = DeviceType
  783. q = forms.CharField(
  784. required=False,
  785. label='Search'
  786. )
  787. manufacturer = FilterChoiceField(
  788. queryset=Manufacturer.objects.all(),
  789. to_field_name='slug',
  790. widget=APISelectMultiple(
  791. api_url="/api/dcim/manufacturers/",
  792. value_field="slug",
  793. )
  794. )
  795. subdevice_role = forms.NullBooleanField(
  796. required=False,
  797. label='Subdevice role',
  798. widget=StaticSelect2(
  799. choices=add_blank_choice(SUBDEVICE_ROLE_CHOICES)
  800. )
  801. )
  802. console_ports = forms.NullBooleanField(
  803. required=False,
  804. label='Has console ports',
  805. widget=StaticSelect2(
  806. choices=BOOLEAN_WITH_BLANK_CHOICES
  807. )
  808. )
  809. console_server_ports = forms.NullBooleanField(
  810. required=False,
  811. label='Has console server ports',
  812. widget=StaticSelect2(
  813. choices=BOOLEAN_WITH_BLANK_CHOICES
  814. )
  815. )
  816. power_ports = forms.NullBooleanField(
  817. required=False,
  818. label='Has power ports',
  819. widget=StaticSelect2(
  820. choices=BOOLEAN_WITH_BLANK_CHOICES
  821. )
  822. )
  823. power_outlets = forms.NullBooleanField(
  824. required=False,
  825. label='Has power outlets',
  826. widget=StaticSelect2(
  827. choices=BOOLEAN_WITH_BLANK_CHOICES
  828. )
  829. )
  830. interfaces = forms.NullBooleanField(
  831. required=False,
  832. label='Has interfaces',
  833. widget=StaticSelect2(
  834. choices=BOOLEAN_WITH_BLANK_CHOICES
  835. )
  836. )
  837. pass_through_ports = forms.NullBooleanField(
  838. required=False,
  839. label='Has pass-through ports',
  840. widget=StaticSelect2(
  841. choices=BOOLEAN_WITH_BLANK_CHOICES
  842. )
  843. )
  844. #
  845. # Device component templates
  846. #
  847. class ConsolePortTemplateForm(BootstrapMixin, forms.ModelForm):
  848. class Meta:
  849. model = ConsolePortTemplate
  850. fields = [
  851. 'device_type', 'name',
  852. ]
  853. widgets = {
  854. 'device_type': forms.HiddenInput(),
  855. }
  856. class ConsolePortTemplateCreateForm(ComponentForm):
  857. name_pattern = ExpandableNameField(
  858. label='Name'
  859. )
  860. class ConsoleServerPortTemplateForm(BootstrapMixin, forms.ModelForm):
  861. class Meta:
  862. model = ConsoleServerPortTemplate
  863. fields = [
  864. 'device_type', 'name',
  865. ]
  866. widgets = {
  867. 'device_type': forms.HiddenInput(),
  868. }
  869. class ConsoleServerPortTemplateCreateForm(ComponentForm):
  870. name_pattern = ExpandableNameField(
  871. label='Name'
  872. )
  873. class PowerPortTemplateForm(BootstrapMixin, forms.ModelForm):
  874. class Meta:
  875. model = PowerPortTemplate
  876. fields = [
  877. 'device_type', 'name', 'maximum_draw', 'allocated_draw',
  878. ]
  879. widgets = {
  880. 'device_type': forms.HiddenInput(),
  881. }
  882. class PowerPortTemplateCreateForm(ComponentForm):
  883. name_pattern = ExpandableNameField(
  884. label='Name'
  885. )
  886. maximum_draw = forms.IntegerField(
  887. min_value=1,
  888. required=False,
  889. help_text="Maximum current draw (watts)"
  890. )
  891. allocated_draw = forms.IntegerField(
  892. min_value=1,
  893. required=False,
  894. help_text="Allocated current draw (watts)"
  895. )
  896. class PowerOutletTemplateForm(BootstrapMixin, forms.ModelForm):
  897. class Meta:
  898. model = PowerOutletTemplate
  899. fields = [
  900. 'device_type', 'name', 'power_port', 'feed_leg',
  901. ]
  902. widgets = {
  903. 'device_type': forms.HiddenInput(),
  904. }
  905. def __init__(self, *args, **kwargs):
  906. super().__init__(*args, **kwargs)
  907. # Limit power_port choices to current DeviceType
  908. if hasattr(self.instance, 'device_type'):
  909. self.fields['power_port'].queryset = PowerPortTemplate.objects.filter(
  910. device_type=self.instance.device_type
  911. )
  912. class PowerOutletTemplateCreateForm(ComponentForm):
  913. name_pattern = ExpandableNameField(
  914. label='Name'
  915. )
  916. power_port = forms.ModelChoiceField(
  917. queryset=PowerPortTemplate.objects.all(),
  918. required=False
  919. )
  920. feed_leg = forms.ChoiceField(
  921. choices=add_blank_choice(POWERFEED_LEG_CHOICES),
  922. required=False,
  923. widget=StaticSelect2()
  924. )
  925. def __init__(self, *args, **kwargs):
  926. super().__init__(*args, **kwargs)
  927. # Limit power_port choices to current DeviceType
  928. self.fields['power_port'].queryset = PowerPortTemplate.objects.filter(
  929. device_type=self.parent
  930. )
  931. class InterfaceTemplateForm(BootstrapMixin, forms.ModelForm):
  932. class Meta:
  933. model = InterfaceTemplate
  934. fields = [
  935. 'device_type', 'name', 'type', 'mgmt_only',
  936. ]
  937. widgets = {
  938. 'device_type': forms.HiddenInput(),
  939. 'type': StaticSelect2(),
  940. }
  941. class InterfaceTemplateCreateForm(ComponentForm):
  942. name_pattern = ExpandableNameField(
  943. label='Name'
  944. )
  945. type = forms.ChoiceField(
  946. choices=IFACE_TYPE_CHOICES,
  947. widget=StaticSelect2()
  948. )
  949. mgmt_only = forms.BooleanField(
  950. required=False,
  951. label='Management only'
  952. )
  953. class InterfaceTemplateBulkEditForm(BootstrapMixin, BulkEditForm):
  954. pk = forms.ModelMultipleChoiceField(
  955. queryset=InterfaceTemplate.objects.all(),
  956. widget=forms.MultipleHiddenInput()
  957. )
  958. type = forms.ChoiceField(
  959. choices=add_blank_choice(IFACE_TYPE_CHOICES),
  960. required=False,
  961. widget=StaticSelect2()
  962. )
  963. mgmt_only = forms.NullBooleanField(
  964. required=False,
  965. widget=BulkEditNullBooleanSelect,
  966. label='Management only'
  967. )
  968. class Meta:
  969. nullable_fields = []
  970. class FrontPortTemplateForm(BootstrapMixin, forms.ModelForm):
  971. class Meta:
  972. model = FrontPortTemplate
  973. fields = [
  974. 'device_type', 'name', 'type', 'rear_port', 'rear_port_position',
  975. ]
  976. widgets = {
  977. 'device_type': forms.HiddenInput(),
  978. 'rear_port': StaticSelect2(),
  979. }
  980. def __init__(self, *args, **kwargs):
  981. super().__init__(*args, **kwargs)
  982. # Limit rear_port choices to current DeviceType
  983. if hasattr(self.instance, 'device_type'):
  984. self.fields['rear_port'].queryset = RearPortTemplate.objects.filter(
  985. device_type=self.instance.device_type
  986. )
  987. class FrontPortTemplateCreateForm(ComponentForm):
  988. name_pattern = ExpandableNameField(
  989. label='Name'
  990. )
  991. type = forms.ChoiceField(
  992. choices=PORT_TYPE_CHOICES,
  993. widget=StaticSelect2()
  994. )
  995. rear_port_set = forms.MultipleChoiceField(
  996. choices=[],
  997. label='Rear ports',
  998. help_text='Select one rear port assignment for each front port being created.',
  999. )
  1000. def __init__(self, *args, **kwargs):
  1001. super().__init__(*args, **kwargs)
  1002. # Determine which rear port positions are occupied. These will be excluded from the list of available mappings.
  1003. occupied_port_positions = [
  1004. (front_port.rear_port_id, front_port.rear_port_position)
  1005. for front_port in self.parent.frontport_templates.all()
  1006. ]
  1007. # Populate rear port choices
  1008. choices = []
  1009. rear_ports = RearPortTemplate.objects.filter(device_type=self.parent)
  1010. for rear_port in rear_ports:
  1011. for i in range(1, rear_port.positions + 1):
  1012. if (rear_port.pk, i) not in occupied_port_positions:
  1013. choices.append(
  1014. ('{}:{}'.format(rear_port.pk, i), '{}:{}'.format(rear_port.name, i))
  1015. )
  1016. self.fields['rear_port_set'].choices = choices
  1017. def clean(self):
  1018. # Validate that the number of ports being created equals the number of selected (rear port, position) tuples
  1019. front_port_count = len(self.cleaned_data['name_pattern'])
  1020. rear_port_count = len(self.cleaned_data['rear_port_set'])
  1021. if front_port_count != rear_port_count:
  1022. raise forms.ValidationError({
  1023. 'rear_port_set': 'The provided name pattern will create {} ports, however {} rear port assignments '
  1024. 'were selected. These counts must match.'.format(front_port_count, rear_port_count)
  1025. })
  1026. def get_iterative_data(self, iteration):
  1027. # Assign rear port and position from selected set
  1028. rear_port, position = self.cleaned_data['rear_port_set'][iteration].split(':')
  1029. return {
  1030. 'rear_port': int(rear_port),
  1031. 'rear_port_position': int(position),
  1032. }
  1033. class RearPortTemplateForm(BootstrapMixin, forms.ModelForm):
  1034. class Meta:
  1035. model = RearPortTemplate
  1036. fields = [
  1037. 'device_type', 'name', 'type', 'positions',
  1038. ]
  1039. widgets = {
  1040. 'device_type': forms.HiddenInput(),
  1041. 'type': StaticSelect2(),
  1042. }
  1043. class RearPortTemplateCreateForm(ComponentForm):
  1044. name_pattern = ExpandableNameField(
  1045. label='Name'
  1046. )
  1047. type = forms.ChoiceField(
  1048. choices=PORT_TYPE_CHOICES,
  1049. widget=StaticSelect2(),
  1050. )
  1051. positions = forms.IntegerField(
  1052. min_value=1,
  1053. max_value=64,
  1054. initial=1,
  1055. help_text='The number of front ports which may be mapped to each rear port'
  1056. )
  1057. class DeviceBayTemplateForm(BootstrapMixin, forms.ModelForm):
  1058. class Meta:
  1059. model = DeviceBayTemplate
  1060. fields = [
  1061. 'device_type', 'name',
  1062. ]
  1063. widgets = {
  1064. 'device_type': forms.HiddenInput(),
  1065. }
  1066. class DeviceBayTemplateCreateForm(ComponentForm):
  1067. name_pattern = ExpandableNameField(
  1068. label='Name'
  1069. )
  1070. #
  1071. # Device roles
  1072. #
  1073. class DeviceRoleForm(BootstrapMixin, forms.ModelForm):
  1074. slug = SlugField()
  1075. class Meta:
  1076. model = DeviceRole
  1077. fields = [
  1078. 'name', 'slug', 'color', 'vm_role',
  1079. ]
  1080. class DeviceRoleCSVForm(forms.ModelForm):
  1081. slug = SlugField()
  1082. class Meta:
  1083. model = DeviceRole
  1084. fields = DeviceRole.csv_headers
  1085. help_texts = {
  1086. 'name': 'Name of device role',
  1087. 'color': 'RGB color in hexadecimal (e.g. 00ff00)'
  1088. }
  1089. #
  1090. # Platforms
  1091. #
  1092. class PlatformForm(BootstrapMixin, forms.ModelForm):
  1093. slug = SlugField(
  1094. max_length=64
  1095. )
  1096. class Meta:
  1097. model = Platform
  1098. fields = [
  1099. 'name', 'slug', 'manufacturer', 'napalm_driver', 'napalm_args',
  1100. ]
  1101. widgets = {
  1102. 'manufacturer': APISelect(
  1103. api_url="/api/dcim/manufacturers/"
  1104. ),
  1105. 'napalm_args': SmallTextarea(),
  1106. }
  1107. class PlatformCSVForm(forms.ModelForm):
  1108. slug = SlugField()
  1109. manufacturer = forms.ModelChoiceField(
  1110. queryset=Manufacturer.objects.all(),
  1111. required=False,
  1112. to_field_name='name',
  1113. help_text='Manufacturer name',
  1114. error_messages={
  1115. 'invalid_choice': 'Manufacturer not found.',
  1116. }
  1117. )
  1118. class Meta:
  1119. model = Platform
  1120. fields = Platform.csv_headers
  1121. help_texts = {
  1122. 'name': 'Platform name',
  1123. }
  1124. #
  1125. # Devices
  1126. #
  1127. class DeviceForm(BootstrapMixin, TenancyForm, CustomFieldForm):
  1128. site = forms.ModelChoiceField(
  1129. queryset=Site.objects.all(),
  1130. widget=APISelect(
  1131. api_url="/api/dcim/sites/",
  1132. filter_for={
  1133. 'rack': 'site_id'
  1134. }
  1135. )
  1136. )
  1137. rack = ChainedModelChoiceField(
  1138. queryset=Rack.objects.all(),
  1139. chains=(
  1140. ('site', 'site'),
  1141. ),
  1142. required=False,
  1143. widget=APISelect(
  1144. api_url='/api/dcim/racks/',
  1145. display_field='display_name'
  1146. )
  1147. )
  1148. position = forms.TypedChoiceField(
  1149. required=False,
  1150. empty_value=None,
  1151. help_text="The lowest-numbered unit occupied by the device",
  1152. widget=APISelect(
  1153. api_url='/api/dcim/racks/{{rack}}/units/',
  1154. disabled_indicator='device'
  1155. )
  1156. )
  1157. manufacturer = forms.ModelChoiceField(
  1158. queryset=Manufacturer.objects.all(),
  1159. widget=APISelect(
  1160. api_url="/api/dcim/manufacturers/",
  1161. filter_for={
  1162. 'device_type': 'manufacturer_id',
  1163. 'platform': 'manufacturer_id'
  1164. }
  1165. )
  1166. )
  1167. device_type = ChainedModelChoiceField(
  1168. queryset=DeviceType.objects.all(),
  1169. chains=(
  1170. ('manufacturer', 'manufacturer'),
  1171. ),
  1172. label='Device type',
  1173. widget=APISelect(
  1174. api_url='/api/dcim/device-types/',
  1175. display_field='model'
  1176. )
  1177. )
  1178. cluster_group = forms.ModelChoiceField(
  1179. queryset=ClusterGroup.objects.all(),
  1180. required=False,
  1181. widget=APISelect(
  1182. api_url="/api/virtualization/cluster-groups/",
  1183. filter_for={
  1184. 'cluster': 'group_id'
  1185. },
  1186. attrs={
  1187. 'nullable': 'true'
  1188. }
  1189. )
  1190. )
  1191. cluster = ChainedModelChoiceField(
  1192. queryset=Cluster.objects.all(),
  1193. chains=(
  1194. ('group', 'cluster_group'),
  1195. ),
  1196. required=False,
  1197. widget=APISelect(
  1198. api_url='/api/virtualization/clusters/',
  1199. )
  1200. )
  1201. comments = CommentField()
  1202. tags = TagField(required=False)
  1203. local_context_data = JSONField(
  1204. required=False,
  1205. label=''
  1206. )
  1207. class Meta:
  1208. model = Device
  1209. fields = [
  1210. 'name', 'device_role', 'device_type', 'serial', 'asset_tag', 'site', 'rack', 'position', 'face',
  1211. 'status', 'platform', 'primary_ip4', 'primary_ip6', 'cluster_group', 'cluster', 'tenant_group', 'tenant',
  1212. 'comments', 'tags', 'local_context_data'
  1213. ]
  1214. help_texts = {
  1215. 'device_role': "The function this device serves",
  1216. 'serial': "Chassis serial number",
  1217. 'local_context_data': "Local config context data overwrites all source contexts in the final rendered "
  1218. "config context",
  1219. }
  1220. widgets = {
  1221. 'face': StaticSelect2(
  1222. filter_for={
  1223. 'position': 'face'
  1224. }
  1225. ),
  1226. 'device_role': APISelect(
  1227. api_url='/api/dcim/device-roles/'
  1228. ),
  1229. 'status': StaticSelect2(),
  1230. 'platform': APISelect(
  1231. api_url="/api/dcim/platforms/",
  1232. additional_query_params={
  1233. "manufacturer_id": "null"
  1234. }
  1235. ),
  1236. 'primary_ip4': StaticSelect2(),
  1237. 'primary_ip6': StaticSelect2(),
  1238. }
  1239. def __init__(self, *args, **kwargs):
  1240. # Initialize helper selectors
  1241. instance = kwargs.get('instance')
  1242. if 'initial' not in kwargs:
  1243. kwargs['initial'] = {}
  1244. # Using hasattr() instead of "is not None" to avoid RelatedObjectDoesNotExist on required field
  1245. if instance and hasattr(instance, 'device_type'):
  1246. kwargs['initial']['manufacturer'] = instance.device_type.manufacturer
  1247. if instance and instance.cluster is not None:
  1248. kwargs['initial']['cluster_group'] = instance.cluster.group
  1249. super().__init__(*args, **kwargs)
  1250. if self.instance.pk:
  1251. # Compile list of choices for primary IPv4 and IPv6 addresses
  1252. for family in [4, 6]:
  1253. ip_choices = [(None, '---------')]
  1254. # Gather PKs of all interfaces belonging to this Device or a peer VirtualChassis member
  1255. interface_ids = self.instance.vc_interfaces.values('pk')
  1256. # Collect interface IPs
  1257. interface_ips = IPAddress.objects.prefetch_related('interface').filter(
  1258. family=family, interface_id__in=interface_ids
  1259. )
  1260. if interface_ips:
  1261. ip_list = [(ip.id, '{} ({})'.format(ip.address, ip.interface)) for ip in interface_ips]
  1262. ip_choices.append(('Interface IPs', ip_list))
  1263. # Collect NAT IPs
  1264. nat_ips = IPAddress.objects.prefetch_related('nat_inside').filter(
  1265. family=family, nat_inside__interface__in=interface_ids
  1266. )
  1267. if nat_ips:
  1268. ip_list = [(ip.id, '{} ({})'.format(ip.address, ip.nat_inside.address)) for ip in nat_ips]
  1269. ip_choices.append(('NAT IPs', ip_list))
  1270. self.fields['primary_ip{}'.format(family)].choices = ip_choices
  1271. # If editing an existing device, exclude it from the list of occupied rack units. This ensures that a device
  1272. # can be flipped from one face to another.
  1273. self.fields['position'].widget.add_additional_query_param('exclude', self.instance.pk)
  1274. # Limit platform by manufacturer
  1275. self.fields['platform'].queryset = Platform.objects.filter(
  1276. Q(manufacturer__isnull=True) | Q(manufacturer=self.instance.device_type.manufacturer)
  1277. )
  1278. else:
  1279. # An object that doesn't exist yet can't have any IPs assigned to it
  1280. self.fields['primary_ip4'].choices = []
  1281. self.fields['primary_ip4'].widget.attrs['readonly'] = True
  1282. self.fields['primary_ip6'].choices = []
  1283. self.fields['primary_ip6'].widget.attrs['readonly'] = True
  1284. # Rack position
  1285. pk = self.instance.pk if self.instance.pk else None
  1286. try:
  1287. if self.is_bound and self.data.get('rack') and str(self.data.get('face')):
  1288. position_choices = Rack.objects.get(pk=self.data['rack']) \
  1289. .get_rack_units(face=self.data.get('face'), exclude=pk)
  1290. elif self.initial.get('rack') and str(self.initial.get('face')):
  1291. position_choices = Rack.objects.get(pk=self.initial['rack']) \
  1292. .get_rack_units(face=self.initial.get('face'), exclude=pk)
  1293. else:
  1294. position_choices = []
  1295. except Rack.DoesNotExist:
  1296. position_choices = []
  1297. self.fields['position'].choices = [('', '---------')] + [
  1298. (p['id'], {
  1299. 'label': p['name'],
  1300. 'disabled': bool(p['device'] and p['id'] != self.initial.get('position')),
  1301. }) for p in position_choices
  1302. ]
  1303. # Disable rack assignment if this is a child device installed in a parent device
  1304. if pk and self.instance.device_type.is_child_device and hasattr(self.instance, 'parent_bay'):
  1305. self.fields['site'].disabled = True
  1306. self.fields['rack'].disabled = True
  1307. self.initial['site'] = self.instance.parent_bay.device.site_id
  1308. self.initial['rack'] = self.instance.parent_bay.device.rack_id
  1309. class BaseDeviceCSVForm(forms.ModelForm):
  1310. device_role = forms.ModelChoiceField(
  1311. queryset=DeviceRole.objects.all(),
  1312. to_field_name='name',
  1313. help_text='Name of assigned role',
  1314. error_messages={
  1315. 'invalid_choice': 'Invalid device role.',
  1316. }
  1317. )
  1318. tenant = forms.ModelChoiceField(
  1319. queryset=Tenant.objects.all(),
  1320. required=False,
  1321. to_field_name='name',
  1322. help_text='Name of assigned tenant',
  1323. error_messages={
  1324. 'invalid_choice': 'Tenant not found.',
  1325. }
  1326. )
  1327. manufacturer = forms.ModelChoiceField(
  1328. queryset=Manufacturer.objects.all(),
  1329. to_field_name='name',
  1330. help_text='Device type manufacturer',
  1331. error_messages={
  1332. 'invalid_choice': 'Invalid manufacturer.',
  1333. }
  1334. )
  1335. model_name = forms.CharField(
  1336. help_text='Device type model name'
  1337. )
  1338. platform = forms.ModelChoiceField(
  1339. queryset=Platform.objects.all(),
  1340. required=False,
  1341. to_field_name='name',
  1342. help_text='Name of assigned platform',
  1343. error_messages={
  1344. 'invalid_choice': 'Invalid platform.',
  1345. }
  1346. )
  1347. status = CSVChoiceField(
  1348. choices=DEVICE_STATUS_CHOICES,
  1349. help_text='Operational status'
  1350. )
  1351. class Meta:
  1352. fields = []
  1353. model = Device
  1354. help_texts = {
  1355. 'name': 'Device name',
  1356. }
  1357. def clean(self):
  1358. super().clean()
  1359. manufacturer = self.cleaned_data.get('manufacturer')
  1360. model_name = self.cleaned_data.get('model_name')
  1361. # Validate device type
  1362. if manufacturer and model_name:
  1363. try:
  1364. self.instance.device_type = DeviceType.objects.get(manufacturer=manufacturer, model=model_name)
  1365. except DeviceType.DoesNotExist:
  1366. raise forms.ValidationError("Device type {} {} not found".format(manufacturer, model_name))
  1367. class DeviceCSVForm(BaseDeviceCSVForm):
  1368. site = forms.ModelChoiceField(
  1369. queryset=Site.objects.all(),
  1370. to_field_name='name',
  1371. help_text='Name of parent site',
  1372. error_messages={
  1373. 'invalid_choice': 'Invalid site name.',
  1374. }
  1375. )
  1376. rack_group = forms.CharField(
  1377. required=False,
  1378. help_text='Parent rack\'s group (if any)'
  1379. )
  1380. rack_name = forms.CharField(
  1381. required=False,
  1382. help_text='Name of parent rack'
  1383. )
  1384. face = CSVChoiceField(
  1385. choices=RACK_FACE_CHOICES,
  1386. required=False,
  1387. help_text='Mounted rack face'
  1388. )
  1389. cluster = forms.ModelChoiceField(
  1390. queryset=Cluster.objects.all(),
  1391. to_field_name='name',
  1392. required=False,
  1393. help_text='Virtualization cluster',
  1394. error_messages={
  1395. 'invalid_choice': 'Invalid cluster name.',
  1396. }
  1397. )
  1398. class Meta(BaseDeviceCSVForm.Meta):
  1399. fields = [
  1400. 'name', 'device_role', 'tenant', 'manufacturer', 'model_name', 'platform', 'serial', 'asset_tag', 'status',
  1401. 'site', 'rack_group', 'rack_name', 'position', 'face', 'cluster', 'comments',
  1402. ]
  1403. def clean(self):
  1404. super().clean()
  1405. site = self.cleaned_data.get('site')
  1406. rack_group = self.cleaned_data.get('rack_group')
  1407. rack_name = self.cleaned_data.get('rack_name')
  1408. # Validate rack
  1409. if site and rack_group and rack_name:
  1410. try:
  1411. self.instance.rack = Rack.objects.get(site=site, group__name=rack_group, name=rack_name)
  1412. except Rack.DoesNotExist:
  1413. raise forms.ValidationError("Rack {} not found in site {} group {}".format(rack_name, site, rack_group))
  1414. elif site and rack_name:
  1415. try:
  1416. self.instance.rack = Rack.objects.get(site=site, group__isnull=True, name=rack_name)
  1417. except Rack.DoesNotExist:
  1418. raise forms.ValidationError("Rack {} not found in site {} (no group)".format(rack_name, site))
  1419. class ChildDeviceCSVForm(BaseDeviceCSVForm):
  1420. parent = FlexibleModelChoiceField(
  1421. queryset=Device.objects.all(),
  1422. to_field_name='name',
  1423. help_text='Name or ID of parent device',
  1424. error_messages={
  1425. 'invalid_choice': 'Parent device not found.',
  1426. }
  1427. )
  1428. device_bay_name = forms.CharField(
  1429. help_text='Name of device bay',
  1430. )
  1431. cluster = forms.ModelChoiceField(
  1432. queryset=Cluster.objects.all(),
  1433. to_field_name='name',
  1434. required=False,
  1435. help_text='Virtualization cluster',
  1436. error_messages={
  1437. 'invalid_choice': 'Invalid cluster name.',
  1438. }
  1439. )
  1440. class Meta(BaseDeviceCSVForm.Meta):
  1441. fields = [
  1442. 'name', 'device_role', 'tenant', 'manufacturer', 'model_name', 'platform', 'serial', 'asset_tag', 'status',
  1443. 'parent', 'device_bay_name', 'cluster', 'comments',
  1444. ]
  1445. def clean(self):
  1446. super().clean()
  1447. parent = self.cleaned_data.get('parent')
  1448. device_bay_name = self.cleaned_data.get('device_bay_name')
  1449. # Validate device bay
  1450. if parent and device_bay_name:
  1451. try:
  1452. self.instance.parent_bay = DeviceBay.objects.get(device=parent, name=device_bay_name)
  1453. # Inherit site and rack from parent device
  1454. self.instance.site = parent.site
  1455. self.instance.rack = parent.rack
  1456. except DeviceBay.DoesNotExist:
  1457. raise forms.ValidationError("Parent device/bay ({} {}) not found".format(parent, device_bay_name))
  1458. class DeviceBulkEditForm(BootstrapMixin, AddRemoveTagsForm, CustomFieldBulkEditForm):
  1459. pk = forms.ModelMultipleChoiceField(
  1460. queryset=Device.objects.all(),
  1461. widget=forms.MultipleHiddenInput()
  1462. )
  1463. device_type = forms.ModelChoiceField(
  1464. queryset=DeviceType.objects.all(),
  1465. required=False,
  1466. label='Type',
  1467. widget=APISelect(
  1468. api_url="/api/dcim/device-types/",
  1469. display_field='display_name'
  1470. )
  1471. )
  1472. device_role = forms.ModelChoiceField(
  1473. queryset=DeviceRole.objects.all(),
  1474. required=False,
  1475. label='Role',
  1476. widget=APISelect(
  1477. api_url="/api/dcim/device-roles/"
  1478. )
  1479. )
  1480. tenant = forms.ModelChoiceField(
  1481. queryset=Tenant.objects.all(),
  1482. required=False,
  1483. widget=APISelect(
  1484. api_url="/api/tenancy/tenants/"
  1485. )
  1486. )
  1487. platform = forms.ModelChoiceField(
  1488. queryset=Platform.objects.all(),
  1489. required=False,
  1490. widget=APISelect(
  1491. api_url="/api/dcim/platforms/"
  1492. )
  1493. )
  1494. status = forms.ChoiceField(
  1495. choices=add_blank_choice(DEVICE_STATUS_CHOICES),
  1496. required=False,
  1497. initial='',
  1498. widget=StaticSelect2()
  1499. )
  1500. serial = forms.CharField(
  1501. max_length=50,
  1502. required=False,
  1503. label='Serial Number'
  1504. )
  1505. class Meta:
  1506. nullable_fields = [
  1507. 'tenant', 'platform', 'serial',
  1508. ]
  1509. class DeviceFilterForm(BootstrapMixin, LocalConfigContextFilterForm, TenancyFilterForm, CustomFieldFilterForm):
  1510. model = Device
  1511. field_order = [
  1512. 'q', 'region', 'site', 'group_id', 'rack_id', 'status', 'role', 'tenant_group', 'tenant',
  1513. 'manufacturer_id', 'device_type_id', 'mac_address', 'has_primary_ip',
  1514. ]
  1515. q = forms.CharField(
  1516. required=False,
  1517. label='Search'
  1518. )
  1519. region = FilterChoiceField(
  1520. queryset=Region.objects.all(),
  1521. to_field_name='slug',
  1522. required=False,
  1523. widget=APISelectMultiple(
  1524. api_url="/api/dcim/regions/",
  1525. value_field="slug",
  1526. filter_for={
  1527. 'site': 'region'
  1528. }
  1529. )
  1530. )
  1531. site = FilterChoiceField(
  1532. queryset=Site.objects.all(),
  1533. to_field_name='slug',
  1534. widget=APISelectMultiple(
  1535. api_url="/api/dcim/sites/",
  1536. value_field="slug",
  1537. filter_for={
  1538. 'group_id': 'site',
  1539. 'rack_id': 'site',
  1540. }
  1541. )
  1542. )
  1543. group_id = FilterChoiceField(
  1544. queryset=RackGroup.objects.prefetch_related(
  1545. 'site'
  1546. ),
  1547. label='Rack group',
  1548. widget=APISelectMultiple(
  1549. api_url="/api/dcim/rack-groups/",
  1550. filter_for={
  1551. 'rack_id': 'group_id',
  1552. }
  1553. )
  1554. )
  1555. rack_id = FilterChoiceField(
  1556. queryset=Rack.objects.all(),
  1557. label='Rack',
  1558. null_label='-- None --',
  1559. widget=APISelectMultiple(
  1560. api_url="/api/dcim/racks/",
  1561. null_option=True,
  1562. )
  1563. )
  1564. role = FilterChoiceField(
  1565. queryset=DeviceRole.objects.all(),
  1566. to_field_name='slug',
  1567. widget=APISelectMultiple(
  1568. api_url="/api/dcim/device-roles/",
  1569. value_field="slug",
  1570. )
  1571. )
  1572. manufacturer_id = FilterChoiceField(
  1573. queryset=Manufacturer.objects.all(),
  1574. label='Manufacturer',
  1575. widget=APISelectMultiple(
  1576. api_url="/api/dcim/manufacturers/",
  1577. filter_for={
  1578. 'device_type_id': 'manufacturer_id',
  1579. }
  1580. )
  1581. )
  1582. device_type_id = FilterChoiceField(
  1583. queryset=DeviceType.objects.prefetch_related(
  1584. 'manufacturer'
  1585. ),
  1586. label='Model',
  1587. widget=APISelectMultiple(
  1588. api_url="/api/dcim/device-types/",
  1589. display_field="model",
  1590. )
  1591. )
  1592. platform = FilterChoiceField(
  1593. queryset=Platform.objects.all(),
  1594. to_field_name='slug',
  1595. null_label='-- None --',
  1596. widget=APISelectMultiple(
  1597. api_url="/api/dcim/platforms/",
  1598. value_field="slug",
  1599. null_option=True,
  1600. )
  1601. )
  1602. status = forms.MultipleChoiceField(
  1603. choices=DEVICE_STATUS_CHOICES,
  1604. required=False,
  1605. widget=StaticSelect2Multiple()
  1606. )
  1607. mac_address = forms.CharField(
  1608. required=False,
  1609. label='MAC address'
  1610. )
  1611. has_primary_ip = forms.NullBooleanField(
  1612. required=False,
  1613. label='Has a primary IP',
  1614. widget=StaticSelect2(
  1615. choices=BOOLEAN_WITH_BLANK_CHOICES
  1616. )
  1617. )
  1618. virtual_chassis_member = forms.NullBooleanField(
  1619. required=False,
  1620. label='Virtual chassis member',
  1621. widget=StaticSelect2(
  1622. choices=BOOLEAN_WITH_BLANK_CHOICES
  1623. )
  1624. )
  1625. console_ports = forms.NullBooleanField(
  1626. required=False,
  1627. label='Has console ports',
  1628. widget=StaticSelect2(
  1629. choices=BOOLEAN_WITH_BLANK_CHOICES
  1630. )
  1631. )
  1632. console_server_ports = forms.NullBooleanField(
  1633. required=False,
  1634. label='Has console server ports',
  1635. widget=StaticSelect2(
  1636. choices=BOOLEAN_WITH_BLANK_CHOICES
  1637. )
  1638. )
  1639. power_ports = forms.NullBooleanField(
  1640. required=False,
  1641. label='Has power ports',
  1642. widget=StaticSelect2(
  1643. choices=BOOLEAN_WITH_BLANK_CHOICES
  1644. )
  1645. )
  1646. power_outlets = forms.NullBooleanField(
  1647. required=False,
  1648. label='Has power outlets',
  1649. widget=StaticSelect2(
  1650. choices=BOOLEAN_WITH_BLANK_CHOICES
  1651. )
  1652. )
  1653. interfaces = forms.NullBooleanField(
  1654. required=False,
  1655. label='Has interfaces',
  1656. widget=StaticSelect2(
  1657. choices=BOOLEAN_WITH_BLANK_CHOICES
  1658. )
  1659. )
  1660. pass_through_ports = forms.NullBooleanField(
  1661. required=False,
  1662. label='Has pass-through ports',
  1663. widget=StaticSelect2(
  1664. choices=BOOLEAN_WITH_BLANK_CHOICES
  1665. )
  1666. )
  1667. #
  1668. # Bulk device component creation
  1669. #
  1670. class DeviceBulkAddComponentForm(BootstrapMixin, forms.Form):
  1671. pk = forms.ModelMultipleChoiceField(
  1672. queryset=Device.objects.all(),
  1673. widget=forms.MultipleHiddenInput()
  1674. )
  1675. name_pattern = ExpandableNameField(
  1676. label='Name'
  1677. )
  1678. class DeviceBulkAddInterfaceForm(DeviceBulkAddComponentForm):
  1679. type = forms.ChoiceField(
  1680. choices=IFACE_TYPE_CHOICES,
  1681. widget=StaticSelect2()
  1682. )
  1683. enabled = forms.BooleanField(
  1684. required=False,
  1685. initial=True
  1686. )
  1687. mtu = forms.IntegerField(
  1688. required=False,
  1689. min_value=1,
  1690. max_value=32767,
  1691. label='MTU'
  1692. )
  1693. mgmt_only = forms.BooleanField(
  1694. required=False,
  1695. label='Management only'
  1696. )
  1697. description = forms.CharField(
  1698. max_length=100,
  1699. required=False
  1700. )
  1701. #
  1702. # Console ports
  1703. #
  1704. class ConsolePortForm(BootstrapMixin, forms.ModelForm):
  1705. tags = TagField(
  1706. required=False
  1707. )
  1708. class Meta:
  1709. model = ConsolePort
  1710. fields = [
  1711. 'device', 'name', 'description', 'tags',
  1712. ]
  1713. widgets = {
  1714. 'device': forms.HiddenInput(),
  1715. }
  1716. class ConsolePortCreateForm(ComponentForm):
  1717. name_pattern = ExpandableNameField(
  1718. label='Name'
  1719. )
  1720. description = forms.CharField(
  1721. max_length=100,
  1722. required=False
  1723. )
  1724. tags = TagField(
  1725. required=False
  1726. )
  1727. #
  1728. # Console server ports
  1729. #
  1730. class ConsoleServerPortForm(BootstrapMixin, forms.ModelForm):
  1731. tags = TagField(
  1732. required=False
  1733. )
  1734. class Meta:
  1735. model = ConsoleServerPort
  1736. fields = [
  1737. 'device', 'name', 'description', 'tags',
  1738. ]
  1739. widgets = {
  1740. 'device': forms.HiddenInput(),
  1741. }
  1742. class ConsoleServerPortCreateForm(ComponentForm):
  1743. name_pattern = ExpandableNameField(
  1744. label='Name'
  1745. )
  1746. description = forms.CharField(
  1747. max_length=100,
  1748. required=False
  1749. )
  1750. tags = TagField(
  1751. required=False
  1752. )
  1753. class ConsoleServerPortBulkEditForm(BootstrapMixin, AddRemoveTagsForm, BulkEditForm):
  1754. pk = forms.ModelMultipleChoiceField(
  1755. queryset=ConsoleServerPort.objects.all(),
  1756. widget=forms.MultipleHiddenInput()
  1757. )
  1758. description = forms.CharField(
  1759. max_length=100,
  1760. required=False
  1761. )
  1762. class Meta:
  1763. nullable_fields = [
  1764. 'description',
  1765. ]
  1766. class ConsoleServerPortBulkRenameForm(BulkRenameForm):
  1767. pk = forms.ModelMultipleChoiceField(
  1768. queryset=ConsoleServerPort.objects.all(),
  1769. widget=forms.MultipleHiddenInput()
  1770. )
  1771. class ConsoleServerPortBulkDisconnectForm(ConfirmationForm):
  1772. pk = forms.ModelMultipleChoiceField(
  1773. queryset=ConsoleServerPort.objects.all(),
  1774. widget=forms.MultipleHiddenInput()
  1775. )
  1776. #
  1777. # Power ports
  1778. #
  1779. class PowerPortForm(BootstrapMixin, forms.ModelForm):
  1780. tags = TagField(
  1781. required=False
  1782. )
  1783. class Meta:
  1784. model = PowerPort
  1785. fields = [
  1786. 'device', 'name', 'maximum_draw', 'allocated_draw', 'description', 'tags',
  1787. ]
  1788. widgets = {
  1789. 'device': forms.HiddenInput(),
  1790. }
  1791. class PowerPortCreateForm(ComponentForm):
  1792. name_pattern = ExpandableNameField(
  1793. label='Name'
  1794. )
  1795. maximum_draw = forms.IntegerField(
  1796. min_value=1,
  1797. required=False,
  1798. help_text="Maximum draw in watts"
  1799. )
  1800. allocated_draw = forms.IntegerField(
  1801. min_value=1,
  1802. required=False,
  1803. help_text="Allocated draw in watts"
  1804. )
  1805. description = forms.CharField(
  1806. max_length=100,
  1807. required=False
  1808. )
  1809. tags = TagField(
  1810. required=False
  1811. )
  1812. #
  1813. # Power outlets
  1814. #
  1815. class PowerOutletForm(BootstrapMixin, forms.ModelForm):
  1816. power_port = forms.ModelChoiceField(
  1817. queryset=PowerPort.objects.all(),
  1818. required=False
  1819. )
  1820. tags = TagField(
  1821. required=False
  1822. )
  1823. class Meta:
  1824. model = PowerOutlet
  1825. fields = [
  1826. 'device', 'name', 'power_port', 'feed_leg', 'description', 'tags',
  1827. ]
  1828. widgets = {
  1829. 'device': forms.HiddenInput(),
  1830. }
  1831. def __init__(self, *args, **kwargs):
  1832. super().__init__(*args, **kwargs)
  1833. # Limit power_port choices to the local device
  1834. if hasattr(self.instance, 'device'):
  1835. self.fields['power_port'].queryset = PowerPort.objects.filter(
  1836. device=self.instance.device
  1837. )
  1838. class PowerOutletCreateForm(ComponentForm):
  1839. name_pattern = ExpandableNameField(
  1840. label='Name'
  1841. )
  1842. power_port = forms.ModelChoiceField(
  1843. queryset=PowerPort.objects.all(),
  1844. required=False
  1845. )
  1846. feed_leg = forms.ChoiceField(
  1847. choices=add_blank_choice(POWERFEED_LEG_CHOICES),
  1848. required=False
  1849. )
  1850. description = forms.CharField(
  1851. max_length=100,
  1852. required=False
  1853. )
  1854. tags = TagField(
  1855. required=False
  1856. )
  1857. def __init__(self, *args, **kwargs):
  1858. super().__init__(*args, **kwargs)
  1859. # Limit power_port choices to those on the parent device
  1860. self.fields['power_port'].queryset = PowerPort.objects.filter(device=self.parent)
  1861. class PowerOutletBulkEditForm(BootstrapMixin, AddRemoveTagsForm, BulkEditForm):
  1862. pk = forms.ModelMultipleChoiceField(
  1863. queryset=PowerOutlet.objects.all(),
  1864. widget=forms.MultipleHiddenInput()
  1865. )
  1866. feed_leg = forms.ChoiceField(
  1867. choices=add_blank_choice(POWERFEED_LEG_CHOICES),
  1868. required=False,
  1869. )
  1870. power_port = forms.ModelChoiceField(
  1871. queryset=PowerPort.objects.all(),
  1872. required=False
  1873. )
  1874. description = forms.CharField(
  1875. max_length=100,
  1876. required=False
  1877. )
  1878. class Meta:
  1879. nullable_fields = [
  1880. 'feed_leg', 'power_port', 'description',
  1881. ]
  1882. def __init__(self, *args, **kwargs):
  1883. super().__init__(*args, **kwargs)
  1884. # Limit power_port queryset to PowerPorts which belong to the parent Device
  1885. self.fields['power_port'].queryset = PowerPort.objects.filter(device=self.parent_obj)
  1886. class PowerOutletBulkRenameForm(BulkRenameForm):
  1887. pk = forms.ModelMultipleChoiceField(
  1888. queryset=PowerOutlet.objects.all(),
  1889. widget=forms.MultipleHiddenInput
  1890. )
  1891. class PowerOutletBulkDisconnectForm(ConfirmationForm):
  1892. pk = forms.ModelMultipleChoiceField(
  1893. queryset=PowerOutlet.objects.all(),
  1894. widget=forms.MultipleHiddenInput
  1895. )
  1896. #
  1897. # Interfaces
  1898. #
  1899. class InterfaceForm(InterfaceCommonForm, BootstrapMixin, forms.ModelForm):
  1900. untagged_vlan = forms.ModelChoiceField(
  1901. queryset=VLAN.objects.all(),
  1902. required=False,
  1903. widget=APISelect(
  1904. api_url="/api/ipam/vlans/",
  1905. display_field='display_name',
  1906. additional_query_params={
  1907. 'site_id': 'null'
  1908. },
  1909. full=True
  1910. )
  1911. )
  1912. tagged_vlans = forms.ModelMultipleChoiceField(
  1913. queryset=VLAN.objects.all(),
  1914. required=False,
  1915. widget=APISelectMultiple(
  1916. api_url="/api/ipam/vlans/",
  1917. display_field='display_name',
  1918. additional_query_params={
  1919. 'site_id': 'null'
  1920. },
  1921. full=True
  1922. )
  1923. )
  1924. tags = TagField(
  1925. required=False
  1926. )
  1927. class Meta:
  1928. model = Interface
  1929. fields = [
  1930. 'device', 'name', 'type', 'enabled', 'lag', 'mac_address', 'mtu', 'mgmt_only', 'description',
  1931. 'mode', 'untagged_vlan', 'tagged_vlans', 'tags',
  1932. ]
  1933. widgets = {
  1934. 'device': forms.HiddenInput(),
  1935. 'type': StaticSelect2(),
  1936. 'lag': StaticSelect2(),
  1937. 'mode': StaticSelect2(),
  1938. }
  1939. labels = {
  1940. 'mode': '802.1Q Mode',
  1941. }
  1942. help_texts = {
  1943. 'mode': INTERFACE_MODE_HELP_TEXT,
  1944. }
  1945. def __init__(self, *args, **kwargs):
  1946. super().__init__(*args, **kwargs)
  1947. # Limit LAG choices to interfaces belonging to this device (or VC master)
  1948. if self.is_bound:
  1949. device = Device.objects.get(pk=self.data['device'])
  1950. self.fields['lag'].queryset = Interface.objects.filter(
  1951. device__in=[device, device.get_vc_master()], type=IFACE_TYPE_LAG
  1952. )
  1953. else:
  1954. device = self.instance.device
  1955. self.fields['lag'].queryset = Interface.objects.filter(
  1956. device__in=[self.instance.device, self.instance.device.get_vc_master()], type=IFACE_TYPE_LAG
  1957. )
  1958. # Add the current site to the list of filtered VLANs
  1959. self.fields['untagged_vlan'].widget.attrs['1-data-additional-query-param-site_id'] = self.instance.device.site.pk
  1960. self.fields['tagged_vlans'].widget.attrs['1-data-additional-query-param-site_id'] = self.instance.device.site.pk
  1961. class InterfaceCreateForm(InterfaceCommonForm, ComponentForm, forms.Form):
  1962. name_pattern = ExpandableNameField(
  1963. label='Name'
  1964. )
  1965. type = forms.ChoiceField(
  1966. choices=IFACE_TYPE_CHOICES,
  1967. widget=StaticSelect2(),
  1968. )
  1969. enabled = forms.BooleanField(
  1970. required=False
  1971. )
  1972. lag = forms.ModelChoiceField(
  1973. queryset=Interface.objects.all(),
  1974. required=False,
  1975. label='Parent LAG',
  1976. widget=StaticSelect2(),
  1977. )
  1978. mtu = forms.IntegerField(
  1979. required=False,
  1980. min_value=1,
  1981. max_value=32767,
  1982. label='MTU'
  1983. )
  1984. mac_address = forms.CharField(
  1985. required=False,
  1986. label='MAC Address'
  1987. )
  1988. mgmt_only = forms.BooleanField(
  1989. required=False,
  1990. label='Management only',
  1991. help_text='This interface is used only for out-of-band management'
  1992. )
  1993. description = forms.CharField(
  1994. max_length=100,
  1995. required=False
  1996. )
  1997. mode = forms.ChoiceField(
  1998. choices=add_blank_choice(IFACE_MODE_CHOICES),
  1999. required=False,
  2000. widget=StaticSelect2(),
  2001. )
  2002. tags = TagField(
  2003. required=False
  2004. )
  2005. untagged_vlan = forms.ModelChoiceField(
  2006. queryset=VLAN.objects.all(),
  2007. required=False,
  2008. widget=APISelect(
  2009. api_url="/api/ipam/vlans/",
  2010. display_field='display_name',
  2011. additional_query_params={
  2012. 'site_id': 'null'
  2013. },
  2014. full=True
  2015. )
  2016. )
  2017. tagged_vlans = forms.ModelMultipleChoiceField(
  2018. queryset=VLAN.objects.all(),
  2019. required=False,
  2020. widget=APISelectMultiple(
  2021. api_url="/api/ipam/vlans/",
  2022. display_field='display_name',
  2023. additional_query_params={
  2024. 'site_id': 'null'
  2025. },
  2026. full=True
  2027. )
  2028. )
  2029. def __init__(self, *args, **kwargs):
  2030. # Set interfaces enabled by default
  2031. kwargs['initial'] = kwargs.get('initial', {}).copy()
  2032. kwargs['initial'].update({'enabled': True})
  2033. super().__init__(*args, **kwargs)
  2034. # Limit LAG choices to interfaces belonging to this device (or its VC master)
  2035. if self.parent is not None:
  2036. self.fields['lag'].queryset = Interface.objects.filter(
  2037. device__in=[self.parent, self.parent.get_vc_master()], type=IFACE_TYPE_LAG
  2038. )
  2039. else:
  2040. self.fields['lag'].queryset = Interface.objects.none()
  2041. # Add the current site to the list of filtered VLANs
  2042. self.fields['untagged_vlan'].widget.attrs['1-data-additional-query-param-site_id'] = self.parent.site.pk
  2043. self.fields['tagged_vlans'].widget.attrs['1-data-additional-query-param-site_id'] = self.parent.site.pk
  2044. class InterfaceBulkEditForm(InterfaceCommonForm, BootstrapMixin, AddRemoveTagsForm, BulkEditForm):
  2045. pk = forms.ModelMultipleChoiceField(
  2046. queryset=Interface.objects.all(),
  2047. widget=forms.MultipleHiddenInput()
  2048. )
  2049. type = forms.ChoiceField(
  2050. choices=add_blank_choice(IFACE_TYPE_CHOICES),
  2051. required=False,
  2052. widget=StaticSelect2()
  2053. )
  2054. enabled = forms.NullBooleanField(
  2055. required=False,
  2056. widget=BulkEditNullBooleanSelect()
  2057. )
  2058. lag = forms.ModelChoiceField(
  2059. queryset=Interface.objects.all(),
  2060. required=False,
  2061. label='Parent LAG',
  2062. widget=StaticSelect2()
  2063. )
  2064. mac_address = forms.CharField(
  2065. required=False,
  2066. label='MAC Address'
  2067. )
  2068. mtu = forms.IntegerField(
  2069. required=False,
  2070. min_value=1,
  2071. max_value=32767,
  2072. label='MTU'
  2073. )
  2074. mgmt_only = forms.NullBooleanField(
  2075. required=False,
  2076. widget=BulkEditNullBooleanSelect(),
  2077. label='Management only'
  2078. )
  2079. description = forms.CharField(
  2080. max_length=100,
  2081. required=False
  2082. )
  2083. mode = forms.ChoiceField(
  2084. choices=add_blank_choice(IFACE_MODE_CHOICES),
  2085. required=False,
  2086. widget=StaticSelect2()
  2087. )
  2088. untagged_vlan = forms.ModelChoiceField(
  2089. queryset=VLAN.objects.all(),
  2090. required=False,
  2091. widget=APISelect(
  2092. api_url="/api/ipam/vlans/",
  2093. display_field='display_name',
  2094. additional_query_params={
  2095. 'site_id': 'null'
  2096. },
  2097. full=True
  2098. )
  2099. )
  2100. tagged_vlans = forms.ModelMultipleChoiceField(
  2101. queryset=VLAN.objects.all(),
  2102. required=False,
  2103. widget=APISelectMultiple(
  2104. api_url="/api/ipam/vlans/",
  2105. display_field='display_name',
  2106. additional_query_params={
  2107. 'site_id': 'null'
  2108. },
  2109. full=True
  2110. )
  2111. )
  2112. class Meta:
  2113. nullable_fields = [
  2114. 'lag', 'mac_address', 'mtu', 'description', 'mode', 'untagged_vlan', 'tagged_vlans'
  2115. ]
  2116. def __init__(self, *args, **kwargs):
  2117. super().__init__(*args, **kwargs)
  2118. # Limit LAG choices to interfaces which belong to the parent device (or VC master)
  2119. device = self.parent_obj
  2120. if device is not None:
  2121. self.fields['lag'].queryset = Interface.objects.filter(
  2122. device__in=[device, device.get_vc_master()],
  2123. type=IFACE_TYPE_LAG
  2124. )
  2125. else:
  2126. self.fields['lag'].choices = []
  2127. # Add the current site to the list of filtered VLANs
  2128. self.fields['untagged_vlan'].widget.attrs['1-data-additional-query-param-site_id'] = self.parent_obj.site.pk
  2129. self.fields['tagged_vlans'].widget.attrs['1-data-additional-query-param-site_id'] = self.parent_obj.site.pk
  2130. class InterfaceBulkRenameForm(BulkRenameForm):
  2131. pk = forms.ModelMultipleChoiceField(
  2132. queryset=Interface.objects.all(),
  2133. widget=forms.MultipleHiddenInput()
  2134. )
  2135. class InterfaceBulkDisconnectForm(ConfirmationForm):
  2136. pk = forms.ModelMultipleChoiceField(
  2137. queryset=Interface.objects.all(),
  2138. widget=forms.MultipleHiddenInput()
  2139. )
  2140. #
  2141. # Front pass-through ports
  2142. #
  2143. class FrontPortForm(BootstrapMixin, forms.ModelForm):
  2144. tags = TagField(
  2145. required=False
  2146. )
  2147. class Meta:
  2148. model = FrontPort
  2149. fields = [
  2150. 'device', 'name', 'type', 'rear_port', 'rear_port_position', 'description', 'tags',
  2151. ]
  2152. widgets = {
  2153. 'device': forms.HiddenInput(),
  2154. 'type': StaticSelect2(),
  2155. 'rear_port': StaticSelect2(),
  2156. }
  2157. def __init__(self, *args, **kwargs):
  2158. super().__init__(*args, **kwargs)
  2159. # Limit RearPort choices to the local device
  2160. if hasattr(self.instance, 'device'):
  2161. self.fields['rear_port'].queryset = self.fields['rear_port'].queryset.filter(
  2162. device=self.instance.device
  2163. )
  2164. # TODO: Merge with FrontPortTemplateCreateForm to remove duplicate logic
  2165. class FrontPortCreateForm(ComponentForm):
  2166. name_pattern = ExpandableNameField(
  2167. label='Name'
  2168. )
  2169. type = forms.ChoiceField(
  2170. choices=PORT_TYPE_CHOICES,
  2171. widget=StaticSelect2(),
  2172. )
  2173. rear_port_set = forms.MultipleChoiceField(
  2174. choices=[],
  2175. label='Rear ports',
  2176. help_text='Select one rear port assignment for each front port being created.',
  2177. )
  2178. description = forms.CharField(
  2179. required=False
  2180. )
  2181. def __init__(self, *args, **kwargs):
  2182. super().__init__(*args, **kwargs)
  2183. # Determine which rear port positions are occupied. These will be excluded from the list of available mappings.
  2184. occupied_port_positions = [
  2185. (front_port.rear_port_id, front_port.rear_port_position)
  2186. for front_port in self.parent.frontports.all()
  2187. ]
  2188. # Populate rear port choices
  2189. choices = []
  2190. rear_ports = RearPort.objects.filter(device=self.parent)
  2191. for rear_port in rear_ports:
  2192. for i in range(1, rear_port.positions + 1):
  2193. if (rear_port.pk, i) not in occupied_port_positions:
  2194. choices.append(
  2195. ('{}:{}'.format(rear_port.pk, i), '{}:{}'.format(rear_port.name, i))
  2196. )
  2197. self.fields['rear_port_set'].choices = choices
  2198. def clean(self):
  2199. # Validate that the number of ports being created equals the number of selected (rear port, position) tuples
  2200. front_port_count = len(self.cleaned_data['name_pattern'])
  2201. rear_port_count = len(self.cleaned_data['rear_port_set'])
  2202. if front_port_count != rear_port_count:
  2203. raise forms.ValidationError({
  2204. 'rear_port_set': 'The provided name pattern will create {} ports, however {} rear port assignments '
  2205. 'were selected. These counts must match.'.format(front_port_count, rear_port_count)
  2206. })
  2207. def get_iterative_data(self, iteration):
  2208. # Assign rear port and position from selected set
  2209. rear_port, position = self.cleaned_data['rear_port_set'][iteration].split(':')
  2210. return {
  2211. 'rear_port': int(rear_port),
  2212. 'rear_port_position': int(position),
  2213. }
  2214. class FrontPortBulkEditForm(BootstrapMixin, AddRemoveTagsForm, BulkEditForm):
  2215. pk = forms.ModelMultipleChoiceField(
  2216. queryset=FrontPort.objects.all(),
  2217. widget=forms.MultipleHiddenInput()
  2218. )
  2219. type = forms.ChoiceField(
  2220. choices=add_blank_choice(PORT_TYPE_CHOICES),
  2221. required=False,
  2222. widget=StaticSelect2()
  2223. )
  2224. description = forms.CharField(
  2225. max_length=100,
  2226. required=False
  2227. )
  2228. class Meta:
  2229. nullable_fields = [
  2230. 'description',
  2231. ]
  2232. class FrontPortBulkRenameForm(BulkRenameForm):
  2233. pk = forms.ModelMultipleChoiceField(
  2234. queryset=FrontPort.objects.all(),
  2235. widget=forms.MultipleHiddenInput
  2236. )
  2237. class FrontPortBulkDisconnectForm(ConfirmationForm):
  2238. pk = forms.ModelMultipleChoiceField(
  2239. queryset=FrontPort.objects.all(),
  2240. widget=forms.MultipleHiddenInput
  2241. )
  2242. #
  2243. # Rear pass-through ports
  2244. #
  2245. class RearPortForm(BootstrapMixin, forms.ModelForm):
  2246. tags = TagField(
  2247. required=False
  2248. )
  2249. class Meta:
  2250. model = RearPort
  2251. fields = [
  2252. 'device', 'name', 'type', 'positions', 'description', 'tags',
  2253. ]
  2254. widgets = {
  2255. 'device': forms.HiddenInput(),
  2256. 'type': StaticSelect2(),
  2257. }
  2258. class RearPortCreateForm(ComponentForm):
  2259. name_pattern = ExpandableNameField(
  2260. label='Name'
  2261. )
  2262. type = forms.ChoiceField(
  2263. choices=PORT_TYPE_CHOICES,
  2264. widget=StaticSelect2(),
  2265. )
  2266. positions = forms.IntegerField(
  2267. min_value=1,
  2268. max_value=64,
  2269. initial=1,
  2270. help_text='The number of front ports which may be mapped to each rear port'
  2271. )
  2272. description = forms.CharField(
  2273. required=False
  2274. )
  2275. class RearPortBulkEditForm(BootstrapMixin, AddRemoveTagsForm, BulkEditForm):
  2276. pk = forms.ModelMultipleChoiceField(
  2277. queryset=RearPort.objects.all(),
  2278. widget=forms.MultipleHiddenInput()
  2279. )
  2280. type = forms.ChoiceField(
  2281. choices=add_blank_choice(PORT_TYPE_CHOICES),
  2282. required=False,
  2283. widget=StaticSelect2()
  2284. )
  2285. description = forms.CharField(
  2286. max_length=100,
  2287. required=False
  2288. )
  2289. class Meta:
  2290. nullable_fields = [
  2291. 'description',
  2292. ]
  2293. class RearPortBulkRenameForm(BulkRenameForm):
  2294. pk = forms.ModelMultipleChoiceField(
  2295. queryset=RearPort.objects.all(),
  2296. widget=forms.MultipleHiddenInput
  2297. )
  2298. class RearPortBulkDisconnectForm(ConfirmationForm):
  2299. pk = forms.ModelMultipleChoiceField(
  2300. queryset=RearPort.objects.all(),
  2301. widget=forms.MultipleHiddenInput
  2302. )
  2303. #
  2304. # Cables
  2305. #
  2306. class ConnectCableToDeviceForm(BootstrapMixin, ChainedFieldsMixin, forms.ModelForm):
  2307. """
  2308. Base form for connecting a Cable to a Device component
  2309. """
  2310. termination_b_site = forms.ModelChoiceField(
  2311. queryset=Site.objects.all(),
  2312. label='Site',
  2313. required=False,
  2314. widget=APISelect(
  2315. api_url='/api/dcim/sites/',
  2316. filter_for={
  2317. 'termination_b_rack': 'site_id',
  2318. 'termination_b_device': 'site_id',
  2319. }
  2320. )
  2321. )
  2322. termination_b_rack = ChainedModelChoiceField(
  2323. queryset=Rack.objects.all(),
  2324. chains=(
  2325. ('site', 'termination_b_site'),
  2326. ),
  2327. label='Rack',
  2328. required=False,
  2329. widget=APISelect(
  2330. api_url='/api/dcim/racks/',
  2331. filter_for={
  2332. 'termination_b_device': 'rack_id',
  2333. },
  2334. attrs={
  2335. 'nullable': 'true',
  2336. }
  2337. )
  2338. )
  2339. termination_b_device = ChainedModelChoiceField(
  2340. queryset=Device.objects.all(),
  2341. chains=(
  2342. ('site', 'termination_b_site'),
  2343. ('rack', 'termination_b_rack'),
  2344. ),
  2345. label='Device',
  2346. required=False,
  2347. widget=APISelect(
  2348. api_url='/api/dcim/devices/',
  2349. display_field='display_name',
  2350. filter_for={
  2351. 'termination_b_id': 'device_id',
  2352. }
  2353. )
  2354. )
  2355. class Meta:
  2356. model = Cable
  2357. fields = [
  2358. 'termination_b_site', 'termination_b_rack', 'termination_b_device', 'termination_b_id', 'type', 'status',
  2359. 'label', 'color', 'length', 'length_unit',
  2360. ]
  2361. class ConnectCableToConsolePortForm(ConnectCableToDeviceForm):
  2362. termination_b_id = forms.IntegerField(
  2363. label='Name',
  2364. widget=APISelect(
  2365. api_url='/api/dcim/console-ports/',
  2366. disabled_indicator='cable',
  2367. )
  2368. )
  2369. class ConnectCableToConsoleServerPortForm(ConnectCableToDeviceForm):
  2370. termination_b_id = forms.IntegerField(
  2371. label='Name',
  2372. widget=APISelect(
  2373. api_url='/api/dcim/console-server-ports/',
  2374. disabled_indicator='cable',
  2375. )
  2376. )
  2377. class ConnectCableToPowerPortForm(ConnectCableToDeviceForm):
  2378. termination_b_id = forms.IntegerField(
  2379. label='Name',
  2380. widget=APISelect(
  2381. api_url='/api/dcim/power-ports/',
  2382. disabled_indicator='cable',
  2383. )
  2384. )
  2385. class ConnectCableToPowerOutletForm(ConnectCableToDeviceForm):
  2386. termination_b_id = forms.IntegerField(
  2387. label='Name',
  2388. widget=APISelect(
  2389. api_url='/api/dcim/power-outlets/',
  2390. disabled_indicator='cable',
  2391. )
  2392. )
  2393. class ConnectCableToInterfaceForm(ConnectCableToDeviceForm):
  2394. termination_b_id = forms.IntegerField(
  2395. label='Name',
  2396. widget=APISelect(
  2397. api_url='/api/dcim/interfaces/',
  2398. disabled_indicator='cable',
  2399. additional_query_params={
  2400. 'kind': 'physical',
  2401. }
  2402. )
  2403. )
  2404. class ConnectCableToFrontPortForm(ConnectCableToDeviceForm):
  2405. termination_b_id = forms.IntegerField(
  2406. label='Name',
  2407. widget=APISelect(
  2408. api_url='/api/dcim/front-ports/',
  2409. disabled_indicator='cable',
  2410. )
  2411. )
  2412. class ConnectCableToRearPortForm(ConnectCableToDeviceForm):
  2413. termination_b_id = forms.IntegerField(
  2414. label='Name',
  2415. widget=APISelect(
  2416. api_url='/api/dcim/rear-ports/',
  2417. disabled_indicator='cable',
  2418. )
  2419. )
  2420. class ConnectCableToCircuitTerminationForm(BootstrapMixin, ChainedFieldsMixin, forms.ModelForm):
  2421. termination_b_provider = forms.ModelChoiceField(
  2422. queryset=Provider.objects.all(),
  2423. label='Provider',
  2424. widget=APISelect(
  2425. api_url='/api/circuits/providers/',
  2426. filter_for={
  2427. 'termination_b_circuit': 'provider_id',
  2428. }
  2429. )
  2430. )
  2431. termination_b_site = forms.ModelChoiceField(
  2432. queryset=Site.objects.all(),
  2433. label='Site',
  2434. required=False,
  2435. widget=APISelect(
  2436. api_url='/api/dcim/sites/',
  2437. filter_for={
  2438. 'termination_b_circuit': 'site_id',
  2439. }
  2440. )
  2441. )
  2442. termination_b_circuit = ChainedModelChoiceField(
  2443. queryset=Circuit.objects.all(),
  2444. chains=(
  2445. ('provider', 'termination_b_provider'),
  2446. ),
  2447. label='Circuit',
  2448. widget=APISelect(
  2449. api_url='/api/circuits/circuits/',
  2450. display_field='cid',
  2451. filter_for={
  2452. 'termination_b_id': 'circuit_id',
  2453. }
  2454. )
  2455. )
  2456. termination_b_id = forms.IntegerField(
  2457. label='Side',
  2458. widget=APISelect(
  2459. api_url='/api/circuits/circuit-terminations/',
  2460. disabled_indicator='cable',
  2461. display_field='term_side'
  2462. )
  2463. )
  2464. class Meta:
  2465. model = Cable
  2466. fields = [
  2467. 'termination_b_provider', 'termination_b_site', 'termination_b_circuit', 'termination_b_id', 'type',
  2468. 'status', 'label', 'color', 'length', 'length_unit',
  2469. ]
  2470. class ConnectCableToPowerFeedForm(BootstrapMixin, ChainedFieldsMixin, forms.ModelForm):
  2471. termination_b_site = forms.ModelChoiceField(
  2472. queryset=Site.objects.all(),
  2473. label='Site',
  2474. widget=APISelect(
  2475. api_url='/api/dcim/sites/',
  2476. display_field='cid',
  2477. filter_for={
  2478. 'termination_b_rackgroup': 'site_id',
  2479. 'termination_b_powerpanel': 'site_id',
  2480. }
  2481. )
  2482. )
  2483. termination_b_rackgroup = ChainedModelChoiceField(
  2484. queryset=RackGroup.objects.all(),
  2485. label='Rack Group',
  2486. chains=(
  2487. ('site', 'termination_b_site'),
  2488. ),
  2489. required=False,
  2490. widget=APISelect(
  2491. api_url='/api/dcim/rack-groups/',
  2492. display_field='cid',
  2493. filter_for={
  2494. 'termination_b_powerpanel': 'rackgroup_id',
  2495. }
  2496. )
  2497. )
  2498. termination_b_powerpanel = ChainedModelChoiceField(
  2499. queryset=PowerPanel.objects.all(),
  2500. chains=(
  2501. ('site', 'termination_b_site'),
  2502. ('rack_group', 'termination_b_rackgroup'),
  2503. ),
  2504. label='Power Panel',
  2505. widget=APISelect(
  2506. api_url='/api/dcim/power-panels/',
  2507. filter_for={
  2508. 'termination_b_id': 'power_panel_id',
  2509. }
  2510. )
  2511. )
  2512. termination_b_id = forms.IntegerField(
  2513. label='Name',
  2514. widget=APISelect(
  2515. api_url='/api/dcim/power-feeds/',
  2516. )
  2517. )
  2518. class Meta:
  2519. model = Cable
  2520. fields = [
  2521. 'termination_b_rackgroup', 'termination_b_powerpanel', 'termination_b_id', 'type', 'status', 'label',
  2522. 'color', 'length', 'length_unit',
  2523. ]
  2524. class CableForm(BootstrapMixin, forms.ModelForm):
  2525. class Meta:
  2526. model = Cable
  2527. fields = [
  2528. 'type', 'status', 'label', 'color', 'length', 'length_unit',
  2529. ]
  2530. class CableCSVForm(forms.ModelForm):
  2531. # Termination A
  2532. side_a_device = FlexibleModelChoiceField(
  2533. queryset=Device.objects.all(),
  2534. to_field_name='name',
  2535. help_text='Side A device name or ID',
  2536. error_messages={
  2537. 'invalid_choice': 'Side A device not found',
  2538. }
  2539. )
  2540. side_a_type = forms.ModelChoiceField(
  2541. queryset=ContentType.objects.all(),
  2542. limit_choices_to={
  2543. 'model__in': CABLE_TERMINATION_TYPES,
  2544. },
  2545. to_field_name='model',
  2546. help_text='Side A type'
  2547. )
  2548. side_a_name = forms.CharField(
  2549. help_text='Side A component'
  2550. )
  2551. # Termination B
  2552. side_b_device = FlexibleModelChoiceField(
  2553. queryset=Device.objects.all(),
  2554. to_field_name='name',
  2555. help_text='Side B device name or ID',
  2556. error_messages={
  2557. 'invalid_choice': 'Side B device not found',
  2558. }
  2559. )
  2560. side_b_type = forms.ModelChoiceField(
  2561. queryset=ContentType.objects.all(),
  2562. limit_choices_to={
  2563. 'model__in': CABLE_TERMINATION_TYPES,
  2564. },
  2565. to_field_name='model',
  2566. help_text='Side B type'
  2567. )
  2568. side_b_name = forms.CharField(
  2569. help_text='Side B component'
  2570. )
  2571. # Cable attributes
  2572. status = CSVChoiceField(
  2573. choices=CONNECTION_STATUS_CHOICES,
  2574. required=False,
  2575. help_text='Connection status'
  2576. )
  2577. type = CSVChoiceField(
  2578. choices=CABLE_TYPE_CHOICES,
  2579. required=False,
  2580. help_text='Cable type'
  2581. )
  2582. length_unit = CSVChoiceField(
  2583. choices=CABLE_LENGTH_UNIT_CHOICES,
  2584. required=False,
  2585. help_text='Length unit'
  2586. )
  2587. class Meta:
  2588. model = Cable
  2589. fields = [
  2590. 'side_a_device', 'side_a_type', 'side_a_name', 'side_b_device', 'side_b_type', 'side_b_name', 'type',
  2591. 'status', 'label', 'color', 'length', 'length_unit',
  2592. ]
  2593. help_texts = {
  2594. 'color': 'RGB color in hexadecimal (e.g. 00ff00)'
  2595. }
  2596. # TODO: Merge the clean() methods for either end
  2597. def clean_side_a_name(self):
  2598. device = self.cleaned_data.get('side_a_device')
  2599. content_type = self.cleaned_data.get('side_a_type')
  2600. name = self.cleaned_data.get('side_a_name')
  2601. if not device or not content_type or not name:
  2602. return None
  2603. model = content_type.model_class()
  2604. try:
  2605. termination_object = model.objects.get(
  2606. device=device,
  2607. name=name
  2608. )
  2609. if termination_object.cable is not None:
  2610. raise forms.ValidationError(
  2611. "Side A: {} {} is already connected".format(device, termination_object)
  2612. )
  2613. except ObjectDoesNotExist:
  2614. raise forms.ValidationError(
  2615. "A side termination not found: {} {}".format(device, name)
  2616. )
  2617. self.instance.termination_a = termination_object
  2618. return termination_object
  2619. def clean_side_b_name(self):
  2620. device = self.cleaned_data.get('side_b_device')
  2621. content_type = self.cleaned_data.get('side_b_type')
  2622. name = self.cleaned_data.get('side_b_name')
  2623. if not device or not content_type or not name:
  2624. return None
  2625. model = content_type.model_class()
  2626. try:
  2627. termination_object = model.objects.get(
  2628. device=device,
  2629. name=name
  2630. )
  2631. if termination_object.cable is not None:
  2632. raise forms.ValidationError(
  2633. "Side B: {} {} is already connected".format(device, termination_object)
  2634. )
  2635. except ObjectDoesNotExist:
  2636. raise forms.ValidationError(
  2637. "B side termination not found: {} {}".format(device, name)
  2638. )
  2639. self.instance.termination_b = termination_object
  2640. return termination_object
  2641. def clean_length_unit(self):
  2642. # Avoid trying to save as NULL
  2643. length_unit = self.cleaned_data.get('length_unit', None)
  2644. return length_unit if length_unit is not None else ''
  2645. class CableBulkEditForm(BootstrapMixin, BulkEditForm):
  2646. pk = forms.ModelMultipleChoiceField(
  2647. queryset=Cable.objects.all(),
  2648. widget=forms.MultipleHiddenInput
  2649. )
  2650. type = forms.ChoiceField(
  2651. choices=add_blank_choice(CABLE_TYPE_CHOICES),
  2652. required=False,
  2653. initial='',
  2654. widget=StaticSelect2()
  2655. )
  2656. status = forms.ChoiceField(
  2657. choices=add_blank_choice(CONNECTION_STATUS_CHOICES),
  2658. required=False,
  2659. widget=StaticSelect2(),
  2660. initial=''
  2661. )
  2662. label = forms.CharField(
  2663. max_length=100,
  2664. required=False
  2665. )
  2666. color = forms.CharField(
  2667. max_length=6,
  2668. required=False,
  2669. widget=ColorSelect()
  2670. )
  2671. length = forms.IntegerField(
  2672. min_value=1,
  2673. required=False
  2674. )
  2675. length_unit = forms.ChoiceField(
  2676. choices=add_blank_choice(CABLE_LENGTH_UNIT_CHOICES),
  2677. required=False,
  2678. initial='',
  2679. widget=StaticSelect2()
  2680. )
  2681. class Meta:
  2682. nullable_fields = [
  2683. 'type', 'status', 'label', 'color', 'length',
  2684. ]
  2685. def clean(self):
  2686. # Validate length/unit
  2687. length = self.cleaned_data.get('length')
  2688. length_unit = self.cleaned_data.get('length_unit')
  2689. if length and not length_unit:
  2690. raise forms.ValidationError({
  2691. 'length_unit': "Must specify a unit when setting length"
  2692. })
  2693. class CableFilterForm(BootstrapMixin, forms.Form):
  2694. model = Cable
  2695. q = forms.CharField(
  2696. required=False,
  2697. label='Search'
  2698. )
  2699. site = FilterChoiceField(
  2700. queryset=Site.objects.all(),
  2701. to_field_name='slug',
  2702. widget=APISelectMultiple(
  2703. api_url="/api/dcim/sites/",
  2704. value_field="slug",
  2705. filter_for={
  2706. 'rack_id': 'site',
  2707. }
  2708. )
  2709. )
  2710. rack_id = FilterChoiceField(
  2711. queryset=Rack.objects.all(),
  2712. label='Rack',
  2713. null_label='-- None --',
  2714. widget=APISelectMultiple(
  2715. api_url="/api/dcim/racks/",
  2716. null_option=True,
  2717. )
  2718. )
  2719. type = forms.MultipleChoiceField(
  2720. choices=add_blank_choice(CABLE_TYPE_CHOICES),
  2721. required=False,
  2722. widget=StaticSelect2()
  2723. )
  2724. status = forms.ChoiceField(
  2725. required=False,
  2726. choices=add_blank_choice(CONNECTION_STATUS_CHOICES),
  2727. widget=StaticSelect2()
  2728. )
  2729. color = forms.CharField(
  2730. max_length=6,
  2731. required=False,
  2732. widget=ColorSelect()
  2733. )
  2734. device = forms.CharField(
  2735. required=False,
  2736. label='Device name'
  2737. )
  2738. #
  2739. # Device bays
  2740. #
  2741. class DeviceBayForm(BootstrapMixin, forms.ModelForm):
  2742. tags = TagField(
  2743. required=False
  2744. )
  2745. class Meta:
  2746. model = DeviceBay
  2747. fields = [
  2748. 'device', 'name', 'description', 'tags',
  2749. ]
  2750. widgets = {
  2751. 'device': forms.HiddenInput(),
  2752. }
  2753. class DeviceBayCreateForm(ComponentForm):
  2754. name_pattern = ExpandableNameField(
  2755. label='Name'
  2756. )
  2757. tags = TagField(
  2758. required=False
  2759. )
  2760. class PopulateDeviceBayForm(BootstrapMixin, forms.Form):
  2761. installed_device = forms.ModelChoiceField(
  2762. queryset=Device.objects.all(),
  2763. label='Child Device',
  2764. help_text="Child devices must first be created and assigned to the site/rack of the parent device.",
  2765. widget=StaticSelect2(),
  2766. )
  2767. def __init__(self, device_bay, *args, **kwargs):
  2768. super().__init__(*args, **kwargs)
  2769. self.fields['installed_device'].queryset = Device.objects.filter(
  2770. site=device_bay.device.site,
  2771. rack=device_bay.device.rack,
  2772. parent_bay__isnull=True,
  2773. device_type__u_height=0,
  2774. device_type__subdevice_role=SUBDEVICE_ROLE_CHILD
  2775. ).exclude(pk=device_bay.device.pk)
  2776. class DeviceBayBulkRenameForm(BulkRenameForm):
  2777. pk = forms.ModelMultipleChoiceField(
  2778. queryset=DeviceBay.objects.all(),
  2779. widget=forms.MultipleHiddenInput()
  2780. )
  2781. #
  2782. # Connections
  2783. #
  2784. class ConsoleConnectionFilterForm(BootstrapMixin, forms.Form):
  2785. site = forms.ModelChoiceField(
  2786. queryset=Site.objects.all(),
  2787. required=False,
  2788. to_field_name='slug'
  2789. )
  2790. device = forms.CharField(
  2791. required=False,
  2792. label='Device name'
  2793. )
  2794. class PowerConnectionFilterForm(BootstrapMixin, forms.Form):
  2795. site = forms.ModelChoiceField(
  2796. queryset=Site.objects.all(),
  2797. required=False,
  2798. to_field_name='slug'
  2799. )
  2800. device = forms.CharField(
  2801. required=False,
  2802. label='Device name'
  2803. )
  2804. class InterfaceConnectionFilterForm(BootstrapMixin, forms.Form):
  2805. site = forms.ModelChoiceField(
  2806. queryset=Site.objects.all(),
  2807. required=False,
  2808. to_field_name='slug'
  2809. )
  2810. device = forms.CharField(
  2811. required=False,
  2812. label='Device name'
  2813. )
  2814. #
  2815. # Inventory items
  2816. #
  2817. class InventoryItemForm(BootstrapMixin, forms.ModelForm):
  2818. tags = TagField(
  2819. required=False
  2820. )
  2821. class Meta:
  2822. model = InventoryItem
  2823. fields = [
  2824. 'name', 'manufacturer', 'part_id', 'serial', 'asset_tag', 'description', 'tags',
  2825. ]
  2826. widgets = {
  2827. 'manufacturer': APISelect(
  2828. api_url="/api/dcim/manufacturers/"
  2829. )
  2830. }
  2831. class InventoryItemCSVForm(forms.ModelForm):
  2832. device = FlexibleModelChoiceField(
  2833. queryset=Device.objects.all(),
  2834. to_field_name='name',
  2835. help_text='Device name or ID',
  2836. error_messages={
  2837. 'invalid_choice': 'Device not found.',
  2838. }
  2839. )
  2840. manufacturer = forms.ModelChoiceField(
  2841. queryset=Manufacturer.objects.all(),
  2842. to_field_name='name',
  2843. required=False,
  2844. help_text='Manufacturer name',
  2845. error_messages={
  2846. 'invalid_choice': 'Invalid manufacturer.',
  2847. }
  2848. )
  2849. class Meta:
  2850. model = InventoryItem
  2851. fields = InventoryItem.csv_headers
  2852. class InventoryItemBulkEditForm(BootstrapMixin, BulkEditForm):
  2853. pk = forms.ModelMultipleChoiceField(
  2854. queryset=InventoryItem.objects.all(),
  2855. widget=forms.MultipleHiddenInput()
  2856. )
  2857. manufacturer = forms.ModelChoiceField(
  2858. queryset=Manufacturer.objects.all(),
  2859. required=False
  2860. )
  2861. part_id = forms.CharField(
  2862. max_length=50,
  2863. required=False,
  2864. label='Part ID'
  2865. )
  2866. description = forms.CharField(
  2867. max_length=100,
  2868. required=False
  2869. )
  2870. class Meta:
  2871. nullable_fields = [
  2872. 'manufacturer', 'part_id', 'description',
  2873. ]
  2874. class InventoryItemFilterForm(BootstrapMixin, forms.Form):
  2875. model = InventoryItem
  2876. q = forms.CharField(
  2877. required=False,
  2878. label='Search'
  2879. )
  2880. device = forms.CharField(
  2881. required=False,
  2882. label='Device name'
  2883. )
  2884. manufacturer = FilterChoiceField(
  2885. queryset=Manufacturer.objects.all(),
  2886. to_field_name='slug',
  2887. null_label='-- None --'
  2888. )
  2889. discovered = forms.NullBooleanField(
  2890. required=False,
  2891. widget=forms.Select(
  2892. choices=BOOLEAN_WITH_BLANK_CHOICES
  2893. )
  2894. )
  2895. #
  2896. # Virtual chassis
  2897. #
  2898. class DeviceSelectionForm(forms.Form):
  2899. pk = forms.ModelMultipleChoiceField(
  2900. queryset=Device.objects.all(),
  2901. widget=forms.MultipleHiddenInput()
  2902. )
  2903. class VirtualChassisForm(BootstrapMixin, forms.ModelForm):
  2904. tags = TagField(
  2905. required=False
  2906. )
  2907. class Meta:
  2908. model = VirtualChassis
  2909. fields = [
  2910. 'master', 'domain', 'tags',
  2911. ]
  2912. widgets = {
  2913. 'master': SelectWithPK(),
  2914. }
  2915. class BaseVCMemberFormSet(forms.BaseModelFormSet):
  2916. def clean(self):
  2917. super().clean()
  2918. # Check for duplicate VC position values
  2919. vc_position_list = []
  2920. for form in self.forms:
  2921. vc_position = form.cleaned_data.get('vc_position')
  2922. if vc_position:
  2923. if vc_position in vc_position_list:
  2924. error_msg = 'A virtual chassis member already exists in position {}.'.format(vc_position)
  2925. form.add_error('vc_position', error_msg)
  2926. vc_position_list.append(vc_position)
  2927. class DeviceVCMembershipForm(forms.ModelForm):
  2928. class Meta:
  2929. model = Device
  2930. fields = [
  2931. 'vc_position', 'vc_priority',
  2932. ]
  2933. labels = {
  2934. 'vc_position': 'Position',
  2935. 'vc_priority': 'Priority',
  2936. }
  2937. def __init__(self, validate_vc_position=False, *args, **kwargs):
  2938. super().__init__(*args, **kwargs)
  2939. # Require VC position (only required when the Device is a VirtualChassis member)
  2940. self.fields['vc_position'].required = True
  2941. # Validation of vc_position is optional. This is only required when adding a new member to an existing
  2942. # VirtualChassis. Otherwise, vc_position validation is handled by BaseVCMemberFormSet.
  2943. self.validate_vc_position = validate_vc_position
  2944. def clean_vc_position(self):
  2945. vc_position = self.cleaned_data['vc_position']
  2946. if self.validate_vc_position:
  2947. conflicting_members = Device.objects.filter(
  2948. virtual_chassis=self.instance.virtual_chassis,
  2949. vc_position=vc_position
  2950. )
  2951. if conflicting_members.exists():
  2952. raise forms.ValidationError(
  2953. 'A virtual chassis member already exists in position {}.'.format(vc_position)
  2954. )
  2955. return vc_position
  2956. class VCMemberSelectForm(BootstrapMixin, ChainedFieldsMixin, forms.Form):
  2957. site = forms.ModelChoiceField(
  2958. queryset=Site.objects.all(),
  2959. label='Site',
  2960. required=False,
  2961. widget=APISelect(
  2962. api_url="/api/dcim/sites/",
  2963. filter_for={
  2964. 'rack': 'site_id',
  2965. 'device': 'site_id',
  2966. }
  2967. )
  2968. )
  2969. rack = ChainedModelChoiceField(
  2970. queryset=Rack.objects.all(),
  2971. chains=(
  2972. ('site', 'site'),
  2973. ),
  2974. label='Rack',
  2975. required=False,
  2976. widget=APISelect(
  2977. api_url='/api/dcim/racks/',
  2978. filter_for={
  2979. 'device': 'rack_id'
  2980. },
  2981. attrs={
  2982. 'nullable': 'true',
  2983. }
  2984. )
  2985. )
  2986. device = ChainedModelChoiceField(
  2987. queryset=Device.objects.filter(
  2988. virtual_chassis__isnull=True
  2989. ),
  2990. chains=(
  2991. ('site', 'site'),
  2992. ('rack', 'rack'),
  2993. ),
  2994. label='Device',
  2995. widget=APISelect(
  2996. api_url='/api/dcim/devices/',
  2997. display_field='display_name',
  2998. disabled_indicator='virtual_chassis'
  2999. )
  3000. )
  3001. def clean_device(self):
  3002. device = self.cleaned_data['device']
  3003. if device.virtual_chassis is not None:
  3004. raise forms.ValidationError(
  3005. "Device {} is already assigned to a virtual chassis.".format(device)
  3006. )
  3007. return device
  3008. class VirtualChassisFilterForm(BootstrapMixin, CustomFieldFilterForm):
  3009. model = VirtualChassis
  3010. q = forms.CharField(
  3011. required=False,
  3012. label='Search'
  3013. )
  3014. site = FilterChoiceField(
  3015. queryset=Site.objects.all(),
  3016. to_field_name='slug',
  3017. widget=APISelectMultiple(
  3018. api_url="/api/dcim/sites/",
  3019. value_field="slug",
  3020. )
  3021. )
  3022. tenant_group = FilterChoiceField(
  3023. queryset=TenantGroup.objects.all(),
  3024. to_field_name='slug',
  3025. null_label='-- None --',
  3026. widget=APISelectMultiple(
  3027. api_url="/api/tenancy/tenant-groups/",
  3028. value_field="slug",
  3029. null_option=True,
  3030. filter_for={
  3031. 'tenant': 'group'
  3032. }
  3033. )
  3034. )
  3035. tenant = FilterChoiceField(
  3036. queryset=Tenant.objects.all(),
  3037. to_field_name='slug',
  3038. null_label='-- None --',
  3039. widget=APISelectMultiple(
  3040. api_url="/api/tenancy/tenants/",
  3041. value_field="slug",
  3042. null_option=True,
  3043. )
  3044. )
  3045. #
  3046. # Power panels
  3047. #
  3048. class PowerPanelForm(BootstrapMixin, forms.ModelForm):
  3049. rack_group = ChainedModelChoiceField(
  3050. queryset=RackGroup.objects.all(),
  3051. chains=(
  3052. ('site', 'site'),
  3053. ),
  3054. required=False,
  3055. widget=APISelect(
  3056. api_url='/api/dcim/rack-groups/',
  3057. )
  3058. )
  3059. class Meta:
  3060. model = PowerPanel
  3061. fields = [
  3062. 'site', 'rack_group', 'name',
  3063. ]
  3064. widgets = {
  3065. 'site': APISelect(
  3066. api_url="/api/dcim/sites/",
  3067. filter_for={
  3068. 'rack_group': 'site_id',
  3069. }
  3070. ),
  3071. }
  3072. class PowerPanelCSVForm(forms.ModelForm):
  3073. site = forms.ModelChoiceField(
  3074. queryset=Site.objects.all(),
  3075. to_field_name='name',
  3076. help_text='Name of parent site',
  3077. error_messages={
  3078. 'invalid_choice': 'Site not found.',
  3079. }
  3080. )
  3081. rack_group_name = forms.CharField(
  3082. required=False,
  3083. help_text="Rack group name (optional)"
  3084. )
  3085. class Meta:
  3086. model = PowerPanel
  3087. fields = PowerPanel.csv_headers
  3088. def clean(self):
  3089. super().clean()
  3090. site = self.cleaned_data.get('site')
  3091. rack_group_name = self.cleaned_data.get('rack_group_name')
  3092. # Validate rack group
  3093. if rack_group_name:
  3094. try:
  3095. self.instance.rack_group = RackGroup.objects.get(site=site, name=rack_group_name)
  3096. except RackGroup.DoesNotExist:
  3097. raise forms.ValidationError(
  3098. "Rack group {} not found in site {}".format(rack_group_name, site)
  3099. )
  3100. class PowerPanelFilterForm(BootstrapMixin, CustomFieldFilterForm):
  3101. model = PowerPanel
  3102. q = forms.CharField(
  3103. required=False,
  3104. label='Search'
  3105. )
  3106. site = FilterChoiceField(
  3107. queryset=Site.objects.all(),
  3108. to_field_name='slug',
  3109. widget=APISelectMultiple(
  3110. api_url="/api/dcim/sites/",
  3111. value_field="slug",
  3112. filter_for={
  3113. 'rack_group_id': 'site',
  3114. }
  3115. )
  3116. )
  3117. rack_group_id = FilterChoiceField(
  3118. queryset=RackGroup.objects.all(),
  3119. label='Rack group (ID)',
  3120. null_label='-- None --',
  3121. widget=APISelectMultiple(
  3122. api_url="/api/dcim/rack-groups/",
  3123. null_option=True,
  3124. )
  3125. )
  3126. #
  3127. # Power feeds
  3128. #
  3129. class PowerFeedForm(BootstrapMixin, CustomFieldForm):
  3130. site = ChainedModelChoiceField(
  3131. queryset=Site.objects.all(),
  3132. required=False,
  3133. widget=APISelect(
  3134. api_url='/api/dcim/sites/',
  3135. filter_for={
  3136. 'power_panel': 'site_id',
  3137. 'rack': 'site_id',
  3138. }
  3139. )
  3140. )
  3141. comments = CommentField()
  3142. tags = TagField(
  3143. required=False
  3144. )
  3145. class Meta:
  3146. model = PowerFeed
  3147. fields = [
  3148. 'site', 'power_panel', 'rack', 'name', 'status', 'type', 'supply', 'phase', 'voltage', 'amperage',
  3149. 'max_utilization', 'comments', 'tags',
  3150. ]
  3151. widgets = {
  3152. 'power_panel': APISelect(
  3153. api_url="/api/dcim/power-panels/"
  3154. ),
  3155. 'rack': APISelect(
  3156. api_url="/api/dcim/racks/"
  3157. ),
  3158. 'status': StaticSelect2(),
  3159. 'type': StaticSelect2(),
  3160. 'supply': StaticSelect2(),
  3161. 'phase': StaticSelect2(),
  3162. }
  3163. def __init__(self, *args, **kwargs):
  3164. super().__init__(*args, **kwargs)
  3165. # Initialize site field
  3166. if self.instance and hasattr(self.instance, 'power_panel'):
  3167. self.initial['site'] = self.instance.power_panel.site
  3168. class PowerFeedCSVForm(forms.ModelForm):
  3169. site = forms.ModelChoiceField(
  3170. queryset=Site.objects.all(),
  3171. to_field_name='name',
  3172. help_text='Name of parent site',
  3173. error_messages={
  3174. 'invalid_choice': 'Site not found.',
  3175. }
  3176. )
  3177. panel_name = forms.ModelChoiceField(
  3178. queryset=PowerPanel.objects.all(),
  3179. to_field_name='name',
  3180. help_text='Name of upstream power panel',
  3181. error_messages={
  3182. 'invalid_choice': 'Power panel not found.',
  3183. }
  3184. )
  3185. rack_group = forms.CharField(
  3186. required=False,
  3187. help_text="Rack group name (optional)"
  3188. )
  3189. rack_name = forms.CharField(
  3190. required=False,
  3191. help_text="Rack name (optional)"
  3192. )
  3193. status = CSVChoiceField(
  3194. choices=POWERFEED_STATUS_CHOICES,
  3195. required=False,
  3196. help_text='Operational status'
  3197. )
  3198. type = CSVChoiceField(
  3199. choices=POWERFEED_TYPE_CHOICES,
  3200. required=False,
  3201. help_text='Primary or redundant'
  3202. )
  3203. supply = CSVChoiceField(
  3204. choices=POWERFEED_SUPPLY_CHOICES,
  3205. required=False,
  3206. help_text='AC/DC'
  3207. )
  3208. phase = CSVChoiceField(
  3209. choices=POWERFEED_PHASE_CHOICES,
  3210. required=False,
  3211. help_text='Single or three-phase'
  3212. )
  3213. class Meta:
  3214. model = PowerFeed
  3215. fields = PowerFeed.csv_headers
  3216. def clean(self):
  3217. super().clean()
  3218. site = self.cleaned_data.get('site')
  3219. panel_name = self.cleaned_data.get('panel_name')
  3220. rack_group = self.cleaned_data.get('rack_group')
  3221. rack_name = self.cleaned_data.get('rack_name')
  3222. # Validate power panel
  3223. if panel_name:
  3224. try:
  3225. self.instance.power_panel = PowerPanel.objects.get(site=site, name=panel_name)
  3226. except Rack.DoesNotExist:
  3227. raise forms.ValidationError(
  3228. "Power panel {} not found in site {}".format(panel_name, site)
  3229. )
  3230. # Validate rack
  3231. if rack_name:
  3232. try:
  3233. self.instance.rack = Rack.objects.get(site=site, group__name=rack_group, name=rack_name)
  3234. except Rack.DoesNotExist:
  3235. raise forms.ValidationError(
  3236. "Rack {} not found in site {}, group {}".format(rack_name, site, rack_group)
  3237. )
  3238. class PowerFeedBulkEditForm(BootstrapMixin, AddRemoveTagsForm, CustomFieldBulkEditForm):
  3239. pk = forms.ModelMultipleChoiceField(
  3240. queryset=PowerFeed.objects.all(),
  3241. widget=forms.MultipleHiddenInput
  3242. )
  3243. powerpanel = forms.ModelChoiceField(
  3244. queryset=PowerPanel.objects.all(),
  3245. required=False,
  3246. widget=APISelect(
  3247. api_url="/api/dcim/power-panels/",
  3248. filter_for={
  3249. 'rackgroup': 'site_id',
  3250. }
  3251. )
  3252. )
  3253. rack = forms.ModelChoiceField(
  3254. queryset=Rack.objects.all(),
  3255. required=False,
  3256. widget=APISelect(
  3257. api_url="/api/dcim/racks",
  3258. )
  3259. )
  3260. status = forms.ChoiceField(
  3261. choices=add_blank_choice(POWERFEED_STATUS_CHOICES),
  3262. required=False,
  3263. initial='',
  3264. widget=StaticSelect2()
  3265. )
  3266. type = forms.ChoiceField(
  3267. choices=add_blank_choice(POWERFEED_TYPE_CHOICES),
  3268. required=False,
  3269. initial='',
  3270. widget=StaticSelect2()
  3271. )
  3272. supply = forms.ChoiceField(
  3273. choices=add_blank_choice(POWERFEED_SUPPLY_CHOICES),
  3274. required=False,
  3275. initial='',
  3276. widget=StaticSelect2()
  3277. )
  3278. phase = forms.ChoiceField(
  3279. choices=add_blank_choice(POWERFEED_PHASE_CHOICES),
  3280. required=False,
  3281. initial='',
  3282. widget=StaticSelect2()
  3283. )
  3284. voltage = forms.IntegerField(
  3285. required=False
  3286. )
  3287. amperage = forms.IntegerField(
  3288. required=False
  3289. )
  3290. max_utilization = forms.IntegerField(
  3291. required=False
  3292. )
  3293. comments = forms.CharField(
  3294. required=False
  3295. )
  3296. class Meta:
  3297. nullable_fields = [
  3298. 'rackgroup', 'comments',
  3299. ]
  3300. class PowerFeedFilterForm(BootstrapMixin, CustomFieldFilterForm):
  3301. model = PowerFeed
  3302. q = forms.CharField(
  3303. required=False,
  3304. label='Search'
  3305. )
  3306. site = FilterChoiceField(
  3307. queryset=Site.objects.all(),
  3308. to_field_name='slug',
  3309. widget=APISelectMultiple(
  3310. api_url="/api/dcim/sites/",
  3311. value_field="slug",
  3312. filter_for={
  3313. 'power_panel_id': 'site',
  3314. 'rack_id': 'site',
  3315. }
  3316. )
  3317. )
  3318. power_panel_id = FilterChoiceField(
  3319. queryset=PowerPanel.objects.all(),
  3320. label='Power panel',
  3321. null_label='-- None --',
  3322. widget=APISelectMultiple(
  3323. api_url="/api/dcim/power-panels/",
  3324. null_option=True,
  3325. )
  3326. )
  3327. rack_id = FilterChoiceField(
  3328. queryset=Rack.objects.all(),
  3329. label='Rack',
  3330. null_label='-- None --',
  3331. widget=APISelectMultiple(
  3332. api_url="/api/dcim/racks/",
  3333. null_option=True,
  3334. )
  3335. )
  3336. status = forms.MultipleChoiceField(
  3337. choices=POWERFEED_STATUS_CHOICES,
  3338. required=False,
  3339. widget=StaticSelect2Multiple()
  3340. )
  3341. type = forms.ChoiceField(
  3342. choices=add_blank_choice(POWERFEED_TYPE_CHOICES),
  3343. required=False,
  3344. widget=StaticSelect2()
  3345. )
  3346. supply = forms.ChoiceField(
  3347. choices=add_blank_choice(POWERFEED_SUPPLY_CHOICES),
  3348. required=False,
  3349. widget=StaticSelect2()
  3350. )
  3351. phase = forms.ChoiceField(
  3352. choices=add_blank_choice(POWERFEED_PHASE_CHOICES),
  3353. required=False,
  3354. widget=StaticSelect2()
  3355. )
  3356. voltage = forms.IntegerField(
  3357. required=False
  3358. )
  3359. amperage = forms.IntegerField(
  3360. required=False
  3361. )
  3362. max_utilization = forms.IntegerField(
  3363. required=False
  3364. )