]> git.proxmox.com Git - pve-manager.git/blob - www/manager6/ceph/Pool.js
301a3f912c7dd2cdadfe5c5e1e8f03fc331f8d33
[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/pool`
204 : `/nodes/${get('nodename')}/ceph/pool/${get('pool_name')}`,
205 loadUrl: get => `/nodes/${get('nodename')}/ceph/pool/${get('pool_name')}/status`,
206 method: get => get('isCreate') ? 'POST' : 'PUT',
207 },
208
209 showProgress: true,
210
211 subject: gettext('Ceph Pool'),
212
213 items: [{
214 xtype: 'pveCephPoolInputPanel',
215 cbind: {
216 nodename: '{nodename}',
217 pool_name: '{pool_name}',
218 isErasure: '{isErasure}',
219 isCreate: '{isCreate}',
220 },
221 }],
222 });
223
224 Ext.define('PVE.node.Ceph.PoolList', {
225 extend: 'Ext.grid.GridPanel',
226 alias: 'widget.pveNodeCephPoolList',
227
228 onlineHelp: 'chapter_pveceph',
229
230 stateful: true,
231 stateId: 'grid-ceph-pools',
232 bufferedRenderer: false,
233
234 features: [{ ftype: 'summary' }],
235
236 columns: [
237 {
238 text: gettext('Name'),
239 minWidth: 120,
240 flex: 2,
241 sortable: true,
242 dataIndex: 'pool_name',
243 },
244 {
245 text: gettext('Type'),
246 minWidth: 100,
247 flex: 1,
248 dataIndex: 'type',
249 hidden: true,
250 },
251 {
252 text: gettext('Size') + '/min',
253 minWidth: 100,
254 flex: 1,
255 align: 'right',
256 renderer: (v, meta, rec) => `${v}/${rec.data.min_size}`,
257 dataIndex: 'size',
258 },
259 {
260 text: '# of Placement Groups',
261 flex: 1,
262 minWidth: 100,
263 align: 'right',
264 dataIndex: 'pg_num',
265 },
266 {
267 text: gettext('Optimal # of PGs'),
268 flex: 1,
269 minWidth: 100,
270 align: 'right',
271 dataIndex: 'pg_num_final',
272 renderer: function(value, metaData) {
273 if (!value) {
274 value = '<i class="fa fa-info-circle faded"></i> n/a';
275 metaData.tdAttr = 'data-qtip="Needs pg_autoscaler module enabled."';
276 }
277 return value;
278 },
279 },
280 {
281 text: gettext('Min. # of PGs'),
282 flex: 1,
283 minWidth: 100,
284 align: 'right',
285 dataIndex: 'pg_num_min',
286 hidden: true,
287 },
288 {
289 text: gettext('Target Ratio'),
290 flex: 1,
291 minWidth: 100,
292 align: 'right',
293 dataIndex: 'target_size_ratio',
294 renderer: Ext.util.Format.numberRenderer('0.0000'),
295 hidden: true,
296 },
297 {
298 text: gettext('Target Size'),
299 flex: 1,
300 minWidth: 100,
301 align: 'right',
302 dataIndex: 'target_size',
303 hidden: true,
304 renderer: function(v, metaData, rec) {
305 let value = Proxmox.Utils.render_size(v);
306 if (rec.data.target_size_ratio > 0) {
307 value = '<i class="fa fa-info-circle faded"></i> ' + value;
308 metaData.tdAttr = 'data-qtip="Target Size Ratio takes precedence over Target Size."';
309 }
310 return value;
311 },
312 },
313 {
314 text: gettext('Autoscale Mode'),
315 flex: 1,
316 minWidth: 100,
317 align: 'right',
318 dataIndex: 'pg_autoscale_mode',
319 },
320 {
321 text: 'CRUSH Rule (ID)',
322 flex: 1,
323 align: 'right',
324 minWidth: 150,
325 renderer: (v, meta, rec) => `${v} (${rec.data.crush_rule})`,
326 dataIndex: 'crush_rule_name',
327 },
328 {
329 text: gettext('Used') + ' (%)',
330 flex: 1,
331 minWidth: 150,
332 sortable: true,
333 align: 'right',
334 dataIndex: 'bytes_used',
335 summaryType: 'sum',
336 summaryRenderer: Proxmox.Utils.render_size,
337 renderer: function(v, meta, rec) {
338 let percentage = Ext.util.Format.percent(rec.data.percent_used, '0.00');
339 let used = Proxmox.Utils.render_size(v);
340 return `${used} (${percentage})`;
341 },
342 },
343 ],
344 initComponent: function() {
345 var me = this;
346
347 var nodename = me.pveSelNode.data.node;
348 if (!nodename) {
349 throw "no node name specified";
350 }
351
352 var sm = Ext.create('Ext.selection.RowModel', {});
353
354 var rstore = Ext.create('Proxmox.data.UpdateStore', {
355 interval: 3000,
356 storeid: 'ceph-pool-list' + nodename,
357 model: 'ceph-pool-list',
358 proxy: {
359 type: 'proxmox',
360 url: `/api2/json/nodes/${nodename}/ceph/pool`,
361 },
362 });
363 let store = Ext.create('Proxmox.data.DiffStore', { rstore: rstore });
364
365 // manages the "install ceph?" overlay
366 PVE.Utils.monitor_ceph_installed(me, rstore, nodename);
367
368 var run_editor = function() {
369 let rec = sm.getSelection()[0];
370 if (!rec || !rec.data.pool_name) {
371 return;
372 }
373 Ext.create('PVE.Ceph.PoolEdit', {
374 title: gettext('Edit') + ': Ceph Pool',
375 nodename: nodename,
376 pool_name: rec.data.pool_name,
377 isErasure: rec.data.type === 'erasure',
378 autoShow: true,
379 listeners: {
380 destroy: () => rstore.load(),
381 },
382 });
383 };
384
385 Ext.apply(me, {
386 store: store,
387 selModel: sm,
388 tbar: [
389 {
390 text: gettext('Create'),
391 handler: function() {
392 Ext.create('PVE.Ceph.PoolEdit', {
393 title: gettext('Create') + ': Ceph Pool',
394 isCreate: true,
395 isErasure: false,
396 nodename: nodename,
397 autoShow: true,
398 listeners: {
399 destroy: () => rstore.load(),
400 },
401 });
402 },
403 },
404 {
405 xtype: 'proxmoxButton',
406 text: gettext('Edit'),
407 selModel: sm,
408 disabled: true,
409 handler: run_editor,
410 },
411 {
412 xtype: 'proxmoxButton',
413 text: gettext('Destroy'),
414 selModel: sm,
415 disabled: true,
416 handler: function() {
417 let rec = sm.getSelection()[0];
418 if (!rec || !rec.data.pool_name) {
419 return;
420 }
421 let poolName = rec.data.pool_name;
422 Ext.create('Proxmox.window.SafeDestroy', {
423 showProgress: true,
424 url: `/nodes/${nodename}/ceph/pool/${poolName}`,
425 params: {
426 remove_storages: 1,
427 },
428 item: {
429 type: 'CephPool',
430 id: poolName,
431 },
432 taskName: 'cephdestroypool',
433 autoShow: true,
434 listeners: {
435 destroy: () => rstore.load(),
436 },
437 });
438 },
439 },
440 ],
441 listeners: {
442 activate: () => rstore.startUpdate(),
443 destroy: () => rstore.stopUpdate(),
444 itemdblclick: run_editor,
445 },
446 });
447
448 me.callParent();
449 },
450 }, function() {
451 Ext.define('ceph-pool-list', {
452 extend: 'Ext.data.Model',
453 fields: ['pool_name',
454 { name: 'pool', type: 'integer' },
455 { name: 'size', type: 'integer' },
456 { name: 'min_size', type: 'integer' },
457 { name: 'pg_num', type: 'integer' },
458 { name: 'pg_num_min', type: 'integer' },
459 { name: 'bytes_used', type: 'integer' },
460 { name: 'percent_used', type: 'number' },
461 { name: 'crush_rule', type: 'integer' },
462 { name: 'crush_rule_name', type: 'string' },
463 { name: 'pg_autoscale_mode', type: 'string' },
464 { name: 'pg_num_final', type: 'integer' },
465 { name: 'target_size_ratio', type: 'number' },
466 { name: 'target_size', type: 'integer' },
467 ],
468 idProperty: 'pool_name',
469 });
470 });
471
472 Ext.define('PVE.form.CephRuleSelector', {
473 extend: 'Ext.form.field.ComboBox',
474 alias: 'widget.pveCephRuleSelector',
475
476 allowBlank: false,
477 valueField: 'name',
478 displayField: 'name',
479 editable: false,
480 queryMode: 'local',
481
482 initComponent: function() {
483 let me = this;
484
485 if (!me.nodename) {
486 throw "no nodename given";
487 }
488
489 me.originalAllowBlank = me.allowBlank;
490 me.allowBlank = true;
491
492 Ext.apply(me, {
493 store: {
494 fields: ['name'],
495 sorters: 'name',
496 proxy: {
497 type: 'proxmox',
498 url: `/api2/json/nodes/${me.nodename}/ceph/rules`,
499 },
500 autoLoad: {
501 callback: (records, op, success) => {
502 if (me.isCreate && success && records.length > 0) {
503 me.select(records[0]);
504 }
505
506 me.allowBlank = me.originalAllowBlank;
507 delete me.originalAllowBlank;
508 me.validate();
509 },
510 },
511 },
512 });
513
514 me.callParent();
515 },
516
517 });