forms.py 105 KB

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