]> git.proxmox.com Git - pve-manager.git/blob - www/manager6/ceph/Pool.js
ui: ceph pool edit: rework with controller and formulas
[pve-manager.git] / www / manager6 / ceph / Pool.js
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 });