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