]>
Commit | Line | Data |
---|---|---|
1 | Ext.define('PVE.CephPoolInputPanel', { | |
2 | extend: 'Proxmox.panel.InputPanel', | |
3 | xtype: 'pveCephPoolInputPanel', | |
4 | mixins: ['Proxmox.Mixin.CBind'], | |
5 | ||
6 | showProgress: true, | |
7 | onlineHelp: 'pve_ceph_pools', | |
8 | ||
9 | subject: 'Ceph Pool', | |
10 | ||
11 | defaultSize: undefined, | |
12 | defaultMinSize: undefined, | |
13 | ||
14 | controller: { | |
15 | xclass: 'Ext.app.ViewController', | |
16 | ||
17 | init: function(view) { | |
18 | let vm = this.getViewModel(); | |
19 | vm.set('size', Number(view.defaultSize)); | |
20 | vm.set('minSize', Number(view.defaultMinSize)); | |
21 | }, | |
22 | sizeChange: function(field, val) { | |
23 | let vm = this.getViewModel(); | |
24 | let minSize = Math.round(val / 2); | |
25 | if (minSize > 1) { | |
26 | vm.set('minSize', minSize); | |
27 | } | |
28 | vm.set('size', val); // bind does not work in a pmxDisplayEditField, update manually | |
29 | }, | |
30 | }, | |
31 | ||
32 | viewModel: { | |
33 | data: { | |
34 | minSize: null, | |
35 | size: null, | |
36 | }, | |
37 | formulas: { | |
38 | minSizeLabel: (get) => { | |
39 | if (get('showMinSizeOneWarning') || get('showMinSizeHalfWarning')) { | |
40 | return `${gettext('Min. Size')} <i class="fa fa-exclamation-triangle warning"></i>`; | |
41 | } | |
42 | return gettext('Min. Size'); | |
43 | }, | |
44 | showMinSizeOneWarning: (get) => get('minSize') === 1, | |
45 | showMinSizeHalfWarning: (get) => { | |
46 | let minSize = get('minSize'); | |
47 | let size = get('size'); | |
48 | if (minSize === 1) { | |
49 | return false; | |
50 | } | |
51 | return minSize < (size / 2) && minSize !== size; | |
52 | }, | |
53 | }, | |
54 | }, | |
55 | ||
56 | column1: [ | |
57 | { | |
58 | xtype: 'pmxDisplayEditField', | |
59 | fieldLabel: gettext('Name'), | |
60 | cbind: { | |
61 | editable: '{isCreate}', | |
62 | value: '{pool_name}', | |
63 | }, | |
64 | name: 'name', | |
65 | allowBlank: false, | |
66 | }, | |
67 | { | |
68 | xtype: 'pmxDisplayEditField', | |
69 | cbind: { | |
70 | editable: '{!isErasure}', | |
71 | }, | |
72 | fieldLabel: gettext('Size'), | |
73 | name: 'size', | |
74 | editConfig: { | |
75 | xtype: 'proxmoxintegerfield', | |
76 | cbind: { | |
77 | value: (get) => get('defaultSize'), | |
78 | }, | |
79 | minValue: 2, | |
80 | maxValue: 7, | |
81 | allowBlank: false, | |
82 | listeners: { | |
83 | change: 'sizeChange', | |
84 | }, | |
85 | }, | |
86 | }, | |
87 | ], | |
88 | column2: [ | |
89 | { | |
90 | xtype: 'proxmoxKVComboBox', | |
91 | fieldLabel: 'PG Autoscale Mode', | |
92 | name: 'pg_autoscale_mode', | |
93 | comboItems: [ | |
94 | ['warn', 'warn'], | |
95 | ['on', 'on'], | |
96 | ['off', 'off'], | |
97 | ], | |
98 | value: 'on', // FIXME: check ceph version and only default to on on octopus and newer | |
99 | allowBlank: false, | |
100 | autoSelect: false, | |
101 | labelWidth: 140, | |
102 | }, | |
103 | { | |
104 | xtype: 'proxmoxcheckbox', | |
105 | fieldLabel: gettext('Add as Storage'), | |
106 | cbind: { | |
107 | value: '{isCreate}', | |
108 | hidden: '{!isCreate}', | |
109 | }, | |
110 | name: 'add_storages', | |
111 | labelWidth: 140, | |
112 | autoEl: { | |
113 | tag: 'div', | |
114 | 'data-qtip': gettext('Add the new pool to the cluster storage configuration.'), | |
115 | }, | |
116 | }, | |
117 | ], | |
118 | advancedColumn1: [ | |
119 | { | |
120 | xtype: 'proxmoxintegerfield', | |
121 | bind: { | |
122 | fieldLabel: '{minSizeLabel}', | |
123 | value: '{minSize}', | |
124 | }, | |
125 | name: 'min_size', | |
126 | cbind: { | |
127 | value: (get) => get('defaultMinSize'), | |
128 | minValue: (get) => { | |
129 | if (Number(get('defaultMinSize')) === 1) { | |
130 | return 1; | |
131 | } else { | |
132 | return get('isCreate') ? 2 : 1; | |
133 | } | |
134 | }, | |
135 | }, | |
136 | maxValue: 7, | |
137 | allowBlank: false, | |
138 | }, | |
139 | { | |
140 | xtype: 'displayfield', | |
141 | bind: { | |
142 | hidden: '{!showMinSizeHalfWarning}', | |
143 | }, | |
144 | hidden: true, | |
145 | userCls: 'pmx-hint', | |
146 | value: gettext('min_size < size/2 can lead to data loss, incomplete PGs or unfound objects.'), | |
147 | }, | |
148 | { | |
149 | xtype: 'displayfield', | |
150 | bind: { | |
151 | hidden: '{!showMinSizeOneWarning}', | |
152 | }, | |
153 | hidden: true, | |
154 | userCls: 'pmx-hint', | |
155 | value: gettext('a min_size of 1 is not recommended and can lead to data loss'), | |
156 | }, | |
157 | { | |
158 | xtype: 'pmxDisplayEditField', | |
159 | cbind: { | |
160 | editable: '{!isErasure}', | |
161 | nodename: '{nodename}', | |
162 | isCreate: '{isCreate}', | |
163 | }, | |
164 | fieldLabel: 'Crush Rule', // do not localize | |
165 | name: 'crush_rule', | |
166 | editConfig: { | |
167 | xtype: 'pveCephRuleSelector', | |
168 | allowBlank: false, | |
169 | }, | |
170 | }, | |
171 | { | |
172 | xtype: 'proxmoxintegerfield', | |
173 | fieldLabel: '# of PGs', | |
174 | name: 'pg_num', | |
175 | value: 128, | |
176 | minValue: 1, | |
177 | maxValue: 32768, | |
178 | allowBlank: false, | |
179 | emptyText: 128, | |
180 | }, | |
181 | ], | |
182 | advancedColumn2: [ | |
183 | { | |
184 | xtype: 'numberfield', | |
185 | fieldLabel: gettext('Target Ratio'), | |
186 | name: 'target_size_ratio', | |
187 | minValue: 0, | |
188 | decimalPrecision: 3, | |
189 | allowBlank: true, | |
190 | emptyText: '0.0', | |
191 | autoEl: { | |
192 | tag: 'div', | |
193 | 'data-qtip': gettext('The ratio of storage amount this pool will consume compared to other pools with ratios. Used for auto-scaling.'), | |
194 | }, | |
195 | }, | |
196 | { | |
197 | xtype: 'pveSizeField', | |
198 | name: 'target_size', | |
199 | fieldLabel: gettext('Target Size'), | |
200 | unit: 'GiB', | |
201 | minValue: 0, | |
202 | allowBlank: true, | |
203 | allowZero: true, | |
204 | emptyText: '0', | |
205 | emptyValue: 0, | |
206 | autoEl: { | |
207 | tag: 'div', | |
208 | 'data-qtip': gettext('The amount of data eventually stored in this pool. Used for auto-scaling.'), | |
209 | }, | |
210 | }, | |
211 | { | |
212 | xtype: 'displayfield', | |
213 | userCls: 'pmx-hint', | |
214 | value: Ext.String.format(gettext('{0} takes precedence.'), gettext('Target Ratio')), // FIXME: tooltip? | |
215 | }, | |
216 | { | |
217 | xtype: 'proxmoxintegerfield', | |
218 | fieldLabel: 'Min. # of PGs', | |
219 | name: 'pg_num_min', | |
220 | minValue: 0, | |
221 | allowBlank: true, | |
222 | emptyText: '0', | |
223 | }, | |
224 | ], | |
225 | ||
226 | onGetValues: function(values) { | |
227 | Object.keys(values || {}).forEach(function(name) { | |
228 | if (values[name] === '') { | |
229 | delete values[name]; | |
230 | } | |
231 | }); | |
232 | ||
233 | return values; | |
234 | }, | |
235 | }); | |
236 | ||
237 | Ext.define('PVE.Ceph.PoolEdit', { | |
238 | extend: 'Proxmox.window.Edit', | |
239 | alias: 'widget.pveCephPoolEdit', | |
240 | mixins: ['Proxmox.Mixin.CBind'], | |
241 | ||
242 | cbindData: { | |
243 | pool_name: '', | |
244 | isCreate: (cfg) => !cfg.pool_name, | |
245 | defaultSize: undefined, | |
246 | defaultMinSize: undefined, | |
247 | }, | |
248 | ||
249 | cbind: { | |
250 | autoLoad: get => !get('isCreate'), | |
251 | url: get => get('isCreate') | |
252 | ? `/nodes/${get('nodename')}/ceph/pool` | |
253 | : `/nodes/${get('nodename')}/ceph/pool/${get('pool_name')}`, | |
254 | loadUrl: get => `/nodes/${get('nodename')}/ceph/pool/${get('pool_name')}/status`, | |
255 | method: get => get('isCreate') ? 'POST' : 'PUT', | |
256 | }, | |
257 | ||
258 | showProgress: true, | |
259 | ||
260 | subject: gettext('Ceph Pool'), | |
261 | ||
262 | items: [{ | |
263 | xtype: 'pveCephPoolInputPanel', | |
264 | cbind: { | |
265 | nodename: '{nodename}', | |
266 | pool_name: '{pool_name}', | |
267 | isErasure: '{isErasure}', | |
268 | isCreate: '{isCreate}', | |
269 | defaultSize: '{defaultSize}', | |
270 | defaultMinSize: '{defaultMinSize}', | |
271 | }, | |
272 | }], | |
273 | }); | |
274 | ||
275 | Ext.define('PVE.node.Ceph.PoolList', { | |
276 | extend: 'Ext.grid.GridPanel', | |
277 | alias: 'widget.pveNodeCephPoolList', | |
278 | ||
279 | onlineHelp: 'chapter_pveceph', | |
280 | ||
281 | stateful: true, | |
282 | stateId: 'grid-ceph-pools', | |
283 | bufferedRenderer: false, | |
284 | ||
285 | features: [{ ftype: 'summary' }], | |
286 | ||
287 | columns: [ | |
288 | { | |
289 | text: gettext('Pool #'), | |
290 | minWidth: 70, | |
291 | flex: 1, | |
292 | align: 'right', | |
293 | sortable: true, | |
294 | dataIndex: 'pool', | |
295 | }, | |
296 | { | |
297 | text: gettext('Name'), | |
298 | minWidth: 120, | |
299 | flex: 2, | |
300 | sortable: true, | |
301 | dataIndex: 'pool_name', | |
302 | }, | |
303 | { | |
304 | text: gettext('Type'), | |
305 | minWidth: 100, | |
306 | flex: 1, | |
307 | dataIndex: 'type', | |
308 | hidden: true, | |
309 | }, | |
310 | { | |
311 | text: gettext('Size') + '/min', | |
312 | minWidth: 100, | |
313 | flex: 1, | |
314 | align: 'right', | |
315 | renderer: (v, meta, rec) => `${v}/${rec.data.min_size}`, | |
316 | dataIndex: 'size', | |
317 | }, | |
318 | { | |
319 | text: '# of Placement Groups', | |
320 | flex: 1, | |
321 | minWidth: 100, | |
322 | align: 'right', | |
323 | dataIndex: 'pg_num', | |
324 | }, | |
325 | { | |
326 | text: gettext('Optimal # of PGs'), | |
327 | flex: 1, | |
328 | minWidth: 100, | |
329 | align: 'right', | |
330 | dataIndex: 'pg_num_final', | |
331 | renderer: function(value, metaData) { | |
332 | if (!value) { | |
333 | value = '<i class="fa fa-info-circle faded"></i> n/a'; | |
334 | metaData.tdAttr = 'data-qtip="Needs pg_autoscaler module enabled."'; | |
335 | } | |
336 | return value; | |
337 | }, | |
338 | }, | |
339 | { | |
340 | text: gettext('Min. # of PGs'), | |
341 | flex: 1, | |
342 | minWidth: 100, | |
343 | align: 'right', | |
344 | dataIndex: 'pg_num_min', | |
345 | hidden: true, | |
346 | }, | |
347 | { | |
348 | text: gettext('Target Ratio'), | |
349 | flex: 1, | |
350 | minWidth: 100, | |
351 | align: 'right', | |
352 | dataIndex: 'target_size_ratio', | |
353 | renderer: Ext.util.Format.numberRenderer('0.0000'), | |
354 | hidden: true, | |
355 | }, | |
356 | { | |
357 | text: gettext('Target Size'), | |
358 | flex: 1, | |
359 | minWidth: 100, | |
360 | align: 'right', | |
361 | dataIndex: 'target_size', | |
362 | hidden: true, | |
363 | renderer: function(v, metaData, rec) { | |
364 | let value = Proxmox.Utils.render_size(v); | |
365 | if (rec.data.target_size_ratio > 0) { | |
366 | value = '<i class="fa fa-info-circle faded"></i> ' + value; | |
367 | metaData.tdAttr = 'data-qtip="Target Size Ratio takes precedence over Target Size."'; | |
368 | } | |
369 | return value; | |
370 | }, | |
371 | }, | |
372 | { | |
373 | text: gettext('Autoscale Mode'), | |
374 | flex: 1, | |
375 | minWidth: 100, | |
376 | align: 'right', | |
377 | dataIndex: 'pg_autoscale_mode', | |
378 | }, | |
379 | { | |
380 | text: 'CRUSH Rule (ID)', | |
381 | flex: 1, | |
382 | align: 'right', | |
383 | minWidth: 150, | |
384 | renderer: (v, meta, rec) => `${v} (${rec.data.crush_rule})`, | |
385 | dataIndex: 'crush_rule_name', | |
386 | }, | |
387 | { | |
388 | text: gettext('Used') + ' (%)', | |
389 | flex: 1, | |
390 | minWidth: 150, | |
391 | sortable: true, | |
392 | align: 'right', | |
393 | dataIndex: 'bytes_used', | |
394 | summaryType: 'sum', | |
395 | summaryRenderer: Proxmox.Utils.render_size, | |
396 | renderer: function(v, meta, rec) { | |
397 | let percentage = Ext.util.Format.percent(rec.data.percent_used, '0.00'); | |
398 | let used = Proxmox.Utils.render_size(v); | |
399 | return `${used} (${percentage})`; | |
400 | }, | |
401 | }, | |
402 | ], | |
403 | initComponent: function() { | |
404 | var me = this; | |
405 | ||
406 | var nodename = me.pveSelNode.data.node; | |
407 | if (!nodename) { | |
408 | throw "no node name specified"; | |
409 | } | |
410 | ||
411 | var sm = Ext.create('Ext.selection.RowModel', {}); | |
412 | ||
413 | var rstore = Ext.create('Proxmox.data.UpdateStore', { | |
414 | interval: 3000, | |
415 | storeid: 'ceph-pool-list' + nodename, | |
416 | model: 'ceph-pool-list', | |
417 | proxy: { | |
418 | type: 'proxmox', | |
419 | url: `/api2/json/nodes/${nodename}/ceph/pool`, | |
420 | }, | |
421 | }); | |
422 | let store = Ext.create('Proxmox.data.DiffStore', { rstore: rstore }); | |
423 | ||
424 | // manages the "install ceph?" overlay | |
425 | PVE.Utils.monitor_ceph_installed(me, rstore, nodename); | |
426 | ||
427 | var run_editor = function() { | |
428 | let rec = sm.getSelection()[0]; | |
429 | if (!rec || !rec.data.pool_name) { | |
430 | return; | |
431 | } | |
432 | Ext.create('PVE.Ceph.PoolEdit', { | |
433 | title: gettext('Edit') + ': Ceph Pool', | |
434 | nodename: nodename, | |
435 | pool_name: rec.data.pool_name, | |
436 | isErasure: rec.data.type === 'erasure', | |
437 | autoShow: true, | |
438 | listeners: { | |
439 | destroy: () => rstore.load(), | |
440 | }, | |
441 | }); | |
442 | }; | |
443 | ||
444 | Ext.apply(me, { | |
445 | store: store, | |
446 | selModel: sm, | |
447 | tbar: [ | |
448 | { | |
449 | text: gettext('Create'), | |
450 | handler: function() { | |
451 | let keys = [ | |
452 | 'global:osd-pool-default-min-size', | |
453 | 'global:osd-pool-default-size', | |
454 | ]; | |
455 | let params = { | |
456 | 'config-keys': keys.join(';'), | |
457 | }; | |
458 | ||
459 | Proxmox.Utils.API2Request({ | |
460 | url: '/nodes/localhost/ceph/cfg/value', | |
461 | method: 'GET', | |
462 | params, | |
463 | waitMsgTarget: me.getView(), | |
464 | failure: response => Ext.Msg.alert(gettext('Error'), response.htmlStatus), | |
465 | success: function({ result: { data } }) { | |
466 | let global = data.global; | |
467 | let defaultSize = global?.['osd-pool-default-size'] ?? 3; | |
468 | let defaultMinSize = global?.['osd-pool-default-min-size'] ?? 2; | |
469 | ||
470 | Ext.create('PVE.Ceph.PoolEdit', { | |
471 | title: gettext('Create') + ': Ceph Pool', | |
472 | isCreate: true, | |
473 | isErasure: false, | |
474 | defaultSize, | |
475 | defaultMinSize, | |
476 | nodename: nodename, | |
477 | autoShow: true, | |
478 | listeners: { | |
479 | destroy: () => rstore.load(), | |
480 | }, | |
481 | }); | |
482 | }, | |
483 | }); | |
484 | }, | |
485 | }, | |
486 | { | |
487 | xtype: 'proxmoxButton', | |
488 | text: gettext('Edit'), | |
489 | selModel: sm, | |
490 | disabled: true, | |
491 | handler: run_editor, | |
492 | }, | |
493 | { | |
494 | xtype: 'proxmoxButton', | |
495 | text: gettext('Destroy'), | |
496 | selModel: sm, | |
497 | disabled: true, | |
498 | handler: function() { | |
499 | let rec = sm.getSelection()[0]; | |
500 | if (!rec || !rec.data.pool_name) { | |
501 | return; | |
502 | } | |
503 | let poolName = rec.data.pool_name; | |
504 | Ext.create('Proxmox.window.SafeDestroy', { | |
505 | showProgress: true, | |
506 | url: `/nodes/${nodename}/ceph/pool/${poolName}`, | |
507 | params: { | |
508 | remove_storages: 1, | |
509 | }, | |
510 | item: { | |
511 | type: 'CephPool', | |
512 | id: poolName, | |
513 | }, | |
514 | taskName: 'cephdestroypool', | |
515 | autoShow: true, | |
516 | listeners: { | |
517 | destroy: () => rstore.load(), | |
518 | }, | |
519 | }); | |
520 | }, | |
521 | }, | |
522 | ], | |
523 | listeners: { | |
524 | activate: () => rstore.startUpdate(), | |
525 | destroy: () => rstore.stopUpdate(), | |
526 | itemdblclick: run_editor, | |
527 | }, | |
528 | }); | |
529 | ||
530 | me.callParent(); | |
531 | }, | |
532 | }, function() { | |
533 | Ext.define('ceph-pool-list', { | |
534 | extend: 'Ext.data.Model', | |
535 | fields: ['pool_name', | |
536 | { name: 'pool', type: 'integer' }, | |
537 | { name: 'size', type: 'integer' }, | |
538 | { name: 'min_size', type: 'integer' }, | |
539 | { name: 'pg_num', type: 'integer' }, | |
540 | { name: 'pg_num_min', type: 'integer' }, | |
541 | { name: 'bytes_used', type: 'integer' }, | |
542 | { name: 'percent_used', type: 'number' }, | |
543 | { name: 'crush_rule', type: 'integer' }, | |
544 | { name: 'crush_rule_name', type: 'string' }, | |
545 | { name: 'pg_autoscale_mode', type: 'string' }, | |
546 | { name: 'pg_num_final', type: 'integer' }, | |
547 | { name: 'target_size_ratio', type: 'number' }, | |
548 | { name: 'target_size', type: 'integer' }, | |
549 | ], | |
550 | idProperty: 'pool_name', | |
551 | }); | |
552 | }); | |
553 | ||
554 | Ext.define('PVE.form.CephRuleSelector', { | |
555 | extend: 'Ext.form.field.ComboBox', | |
556 | alias: 'widget.pveCephRuleSelector', | |
557 | ||
558 | allowBlank: false, | |
559 | valueField: 'name', | |
560 | displayField: 'name', | |
561 | editable: false, | |
562 | queryMode: 'local', | |
563 | ||
564 | initComponent: function() { | |
565 | let me = this; | |
566 | ||
567 | if (!me.nodename) { | |
568 | throw "no nodename given"; | |
569 | } | |
570 | ||
571 | me.originalAllowBlank = me.allowBlank; | |
572 | me.allowBlank = true; | |
573 | ||
574 | Ext.apply(me, { | |
575 | store: { | |
576 | fields: ['name'], | |
577 | sorters: 'name', | |
578 | proxy: { | |
579 | type: 'proxmox', | |
580 | url: `/api2/json/nodes/${me.nodename}/ceph/rules`, | |
581 | }, | |
582 | autoLoad: { | |
583 | callback: (records, op, success) => { | |
584 | if (me.isCreate && success && records.length > 0) { | |
585 | me.select(records[0]); | |
586 | } | |
587 | ||
588 | me.allowBlank = me.originalAllowBlank; | |
589 | delete me.originalAllowBlank; | |
590 | me.validate(); | |
591 | }, | |
592 | }, | |
593 | }, | |
594 | }); | |
595 | ||
596 | me.callParent(); | |
597 | }, | |
598 | ||
599 | }); |