forms.py 87 KB

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