forms.py 113 KB

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