]> git.proxmox.com Git - pve-manager.git/blobdiff - www/manager6/ceph/Pool.js
fix #2515: ui: ceph pool create: use configured defaults for size and min_size
[pve-manager.git] / www / manager6 / ceph / Pool.js
index 240cc2c370c7a0dd36e166a9be6b873299602eb2..0ad59baf44ae10d5035e0f165ce12bde772cbce8 100644 (file)
-// Ext.create is a function, but
-// we defined create a bool in PVE.window.Edit
-/*jslint confusion: true*/
-Ext.define('PVE.CephCreatePool', {
-    extend: 'PVE.window.Edit',
-    alias: ['widget.pveCephCreatePool'],
+Ext.define('PVE.CephPoolInputPanel', {
+    extend: 'Proxmox.panel.InputPanel',
+    xtype: 'pveCephPoolInputPanel',
+    mixins: ['Proxmox.Mixin.CBind'],
+
+    showProgress: true,
+    onlineHelp: 'pve_ceph_pools',
 
     subject: 'Ceph Pool',
-    create: true,
-    method: 'POST',
-    items: [
+
+    defaultSize: undefined,
+    defaultMinSize: undefined,
+
+    column1: [
        {
-           xtype: 'textfield',
+           xtype: 'pmxDisplayEditField',
            fieldLabel: gettext('Name'),
+           cbind: {
+               editable: '{isCreate}',
+               value: '{pool_name}',
+           },
            name: 'name',
-           allowBlank: false
+           allowBlank: false,
        },
        {
-           xtype: 'pveIntegerField',
+           xtype: 'pmxDisplayEditField',
+           cbind: {
+               editable: '{!isErasure}',
+           },
            fieldLabel: gettext('Size'),
            name: 'size',
-           value: 2,
-           minValue: 1,
-           maxValue: 3,
-           allowBlank: false
+           editConfig: {
+               xtype: 'proxmoxintegerfield',
+               cbind: {
+                   value: (get) => get('defaultSize'),
+               },
+               minValue: 2,
+               maxValue: 7,
+               allowBlank: false,
+               listeners: {
+                   change: function(field, val) {
+                       let size = Math.round(val / 2);
+                       if (size > 1) {
+                           field.up('inputpanel').down('field[name=min_size]').setValue(size);
+                       }
+                   },
+               },
+           },
+       },
+    ],
+    column2: [
+       {
+           xtype: 'proxmoxKVComboBox',
+           fieldLabel: 'PG Autoscale Mode',
+           name: 'pg_autoscale_mode',
+           comboItems: [
+               ['warn', 'warn'],
+               ['on', 'on'],
+               ['off', 'off'],
+           ],
+           value: 'on', // FIXME: check ceph version and only default to on on octopus and newer
+           allowBlank: false,
+           autoSelect: false,
+           labelWidth: 140,
        },
        {
-           xtype: 'pveIntegerField',
+           xtype: 'proxmoxcheckbox',
+           fieldLabel: gettext('Add as Storage'),
+           cbind: {
+               value: '{isCreate}',
+               hidden: '{!isCreate}',
+           },
+           name: 'add_storages',
+           labelWidth: 140,
+           autoEl: {
+               tag: 'div',
+               'data-qtip': gettext('Add the new pool to the cluster storage configuration.'),
+           },
+       },
+    ],
+    advancedColumn1: [
+       {
+           xtype: 'proxmoxintegerfield',
            fieldLabel: gettext('Min. Size'),
            name: 'min_size',
-           value: 1,
-           minValue: 1,
-           maxValue: 3,
-           allowBlank: false
+           cbind: {
+               value: (get) => get('defaultMinSize'),
+               minValue: (get) => {
+                   if (Number(get('defaultMinSize')) === 1) {
+                       return 1;
+                   } else {
+                       return get('isCreate') ? 2 : 1;
+                   }
+               },
+           },
+           maxValue: 7,
+           allowBlank: false,
+           listeners: {
+               change: function(field, minSize) {
+                   let panel = field.up('inputpanel');
+                   let size = panel.down('field[name=size]').getValue();
+
+                   let showWarning = minSize < (size / 2) && minSize !== size;
+
+                   let fieldLabel = gettext('Min. Size');
+                   if (showWarning) {
+                       fieldLabel = gettext('Min. Size') + ' <i class="fa fa-exclamation-triangle warning"></i>';
+                   }
+                   panel.down('field[name=min_size-warning]').setHidden(!showWarning);
+                   field.setFieldLabel(fieldLabel);
+               },
+           },
        },
        {
-           xtype: 'pveIntegerField',
-           fieldLabel: 'Crush RuleSet', // do not localize
-           name: 'crush_ruleset',
-           value: 0,
-           minValue: 0,
-           maxValue: 32768,
-           allowBlank: false
+           xtype: 'displayfield',
+           name: 'min_size-warning',
+           userCls: 'pmx-hint',
+           value: gettext('min_size < size/2 can lead to data loss, incomplete PGs or unfound objects.'),
+           hidden: true,
+       },
+       {
+           xtype: 'pmxDisplayEditField',
+           cbind: {
+               editable: '{!isErasure}',
+               nodename: '{nodename}',
+               isCreate: '{isCreate}',
+           },
+           fieldLabel: 'Crush Rule', // do not localize
+           name: 'crush_rule',
+           editConfig: {
+               xtype: 'pveCephRuleSelector',
+               allowBlank: false,
+           },
        },
        {
-           xtype: 'pveIntegerField',
-           fieldLabel: 'pg_num',
+           xtype: 'proxmoxintegerfield',
+           fieldLabel: '# of PGs',
            name: 'pg_num',
-           value: 64,
-           minValue: 8,
+           value: 128,
+           minValue: 1,
            maxValue: 32768,
-           allowBlank: false
-       }
+           allowBlank: false,
+           emptyText: 128,
+       },
+    ],
+    advancedColumn2: [
+       {
+           xtype: 'numberfield',
+           fieldLabel: gettext('Target Ratio'),
+           name: 'target_size_ratio',
+           minValue: 0,
+           decimalPrecision: 3,
+           allowBlank: true,
+           emptyText: '0.0',
+           autoEl: {
+               tag: 'div',
+               'data-qtip': gettext('The ratio of storage amount this pool will consume compared to other pools with ratios. Used for auto-scaling.'),
+           },
+       },
+       {
+           xtype: 'pveSizeField',
+           name: 'target_size',
+           fieldLabel: gettext('Target Size'),
+           unit: 'GiB',
+           minValue: 0,
+           allowBlank: true,
+           allowZero: true,
+           emptyText: '0',
+           emptyValue: 0,
+           autoEl: {
+               tag: 'div',
+               'data-qtip': gettext('The amount of data eventually stored in this pool. Used for auto-scaling.'),
+           },
+       },
+       {
+           xtype: 'displayfield',
+           userCls: 'pmx-hint',
+           value: Ext.String.format(gettext('{0} takes precedence.'), gettext('Target Ratio')), // FIXME: tooltip?
+       },
+       {
+           xtype: 'proxmoxintegerfield',
+           fieldLabel: 'Min. # of PGs',
+           name: 'pg_num_min',
+           minValue: 0,
+           allowBlank: true,
+           emptyText: '0',
+       },
     ],
-    initComponent : function() {
-        /*jslint confusion: true */
-        var me = this;
 
-       if (!me.nodename) {
-           throw "no node name specified";
-       }
+    onGetValues: function(values) {
+       Object.keys(values || {}).forEach(function(name) {
+           if (values[name] === '') {
+               delete values[name];
+           }
+       });
+
+       return values;
+    },
+});
+
+Ext.define('PVE.Ceph.PoolEdit', {
+    extend: 'Proxmox.window.Edit',
+    alias: 'widget.pveCephPoolEdit',
+    mixins: ['Proxmox.Mixin.CBind'],
+
+    cbindData: {
+       pool_name: '',
+       isCreate: (cfg) => !cfg.pool_name,
+       defaultSize: undefined,
+       defaultMinSize: undefined,
+    },
+
+    cbind: {
+       autoLoad: get => !get('isCreate'),
+       url: get => get('isCreate')
+           ? `/nodes/${get('nodename')}/ceph/pool`
+           : `/nodes/${get('nodename')}/ceph/pool/${get('pool_name')}`,
+       loadUrl: get => `/nodes/${get('nodename')}/ceph/pool/${get('pool_name')}/status`,
+       method: get => get('isCreate') ? 'POST' : 'PUT',
+    },
 
-        Ext.applyIf(me, {
-           url: "/nodes/" + me.nodename + "/ceph/pools"
-        });
+    showProgress: true,
 
-        me.callParent();
-    }
+    subject: gettext('Ceph Pool'),
+
+    items: [{
+       xtype: 'pveCephPoolInputPanel',
+       cbind: {
+           nodename: '{nodename}',
+           pool_name: '{pool_name}',
+           isErasure: '{isErasure}',
+           isCreate: '{isCreate}',
+           defaultSize: '{defaultSize}',
+           defaultMinSize: '{defaultMinSize}',
+       },
+    }],
 });
 
-Ext.define('PVE.node.CephPoolList', {
+Ext.define('PVE.node.Ceph.PoolList', {
     extend: 'Ext.grid.GridPanel',
-    alias: ['widget.pveNodeCephPoolList'],
+    alias: 'widget.pveNodeCephPoolList',
 
     onlineHelp: 'chapter_pveceph',
+
     stateful: true,
     stateId: 'grid-ceph-pools',
     bufferedRenderer: false,
-    features: [ { ftype: 'summary'} ],
+
+    features: [{ ftype: 'summary' }],
+
     columns: [
        {
-           header: gettext('Name'),
-           width: 100,
+           text: gettext('Pool #'),
+           minWidth: 70,
+           flex: 1,
+           align: 'right',
            sortable: true,
-           dataIndex: 'pool_name'
+           dataIndex: 'pool',
        },
        {
-           header: gettext('Size') + '/min',
-           width: 80,
-           sortable: false,
-           renderer: function(v, meta, rec) {
-               return v + '/' + rec.data.min_size;
+           text: gettext('Name'),
+           minWidth: 120,
+           flex: 2,
+           sortable: true,
+           dataIndex: 'pool_name',
+       },
+       {
+           text: gettext('Type'),
+           minWidth: 100,
+           flex: 1,
+           dataIndex: 'type',
+           hidden: true,
+       },
+       {
+           text: gettext('Size') + '/min',
+           minWidth: 100,
+           flex: 1,
+           align: 'right',
+           renderer: (v, meta, rec) => `${v}/${rec.data.min_size}`,
+           dataIndex: 'size',
+       },
+       {
+           text: '# of Placement Groups',
+           flex: 1,
+           minWidth: 100,
+           align: 'right',
+           dataIndex: 'pg_num',
+       },
+       {
+           text: gettext('Optimal # of PGs'),
+           flex: 1,
+           minWidth: 100,
+           align: 'right',
+           dataIndex: 'pg_num_final',
+           renderer: function(value, metaData) {
+               if (!value) {
+                   value = '<i class="fa fa-info-circle faded"></i> n/a';
+                   metaData.tdAttr = 'data-qtip="Needs pg_autoscaler module enabled."';
+               }
+               return value;
            },
-           dataIndex: 'size'
        },
        {
-           header: 'pg_num',
-           width: 100,
-           sortable: false,
-           dataIndex: 'pg_num'
+           text: gettext('Min. # of PGs'),
+           flex: 1,
+           minWidth: 100,
+           align: 'right',
+           dataIndex: 'pg_num_min',
+           hidden: true,
        },
        {
-           header: 'ruleset',
-           width: 50,
-           sortable: false,
-           dataIndex: 'crush_ruleset'
+           text: gettext('Target Ratio'),
+           flex: 1,
+           minWidth: 100,
+           align: 'right',
+           dataIndex: 'target_size_ratio',
+           renderer: Ext.util.Format.numberRenderer('0.0000'),
+           hidden: true,
        },
        {
-           header: gettext('Used'),
-           columns: [
-               {
-                   header: '%',
-                   width: 80,
-                   sortable: true,
-                   align: 'right',
-                   renderer: Ext.util.Format.numberRenderer('0.00'),
-                   dataIndex: 'percent_used',
-                   summaryType: 'sum',
-                   summaryRenderer: Ext.util.Format.numberRenderer('0.00')
-               },
-               {
-                   header: gettext('Total'),
-                   width: 100,
-                   sortable: true,
-                   renderer: PVE.Utils.render_size,
-                   align: 'right',
-                   dataIndex: 'bytes_used',
-                   summaryType: 'sum',
-                   summaryRenderer: PVE.Utils.render_size
+           text: gettext('Target Size'),
+           flex: 1,
+           minWidth: 100,
+           align: 'right',
+           dataIndex: 'target_size',
+           hidden: true,
+           renderer: function(v, metaData, rec) {
+               let value = Proxmox.Utils.render_size(v);
+               if (rec.data.target_size_ratio > 0) {
+                   value = '<i class="fa fa-info-circle faded"></i> ' + value;
+                   metaData.tdAttr = 'data-qtip="Target Size Ratio takes precedence over Target Size."';
                }
-           ]
-       }
+               return value;
+           },
+       },
+       {
+           text: gettext('Autoscale Mode'),
+           flex: 1,
+           minWidth: 100,
+           align: 'right',
+           dataIndex: 'pg_autoscale_mode',
+       },
+       {
+           text: 'CRUSH Rule (ID)',
+           flex: 1,
+           align: 'right',
+           minWidth: 150,
+           renderer: (v, meta, rec) => `${v} (${rec.data.crush_rule})`,
+           dataIndex: 'crush_rule_name',
+       },
+       {
+           text: gettext('Used') + ' (%)',
+           flex: 1,
+           minWidth: 150,
+           sortable: true,
+           align: 'right',
+           dataIndex: 'bytes_used',
+           summaryType: 'sum',
+           summaryRenderer: Proxmox.Utils.render_size,
+           renderer: function(v, meta, rec) {
+               let percentage = Ext.util.Format.percent(rec.data.percent_used, '0.00');
+               let used = Proxmox.Utils.render_size(v);
+               return `${used} (${percentage})`;
+           },
+       },
     ],
     initComponent: function() {
         var me = this;
@@ -141,84 +374,190 @@ Ext.define('PVE.node.CephPoolList', {
 
        var sm = Ext.create('Ext.selection.RowModel', {});
 
-       var rstore = Ext.create('PVE.data.UpdateStore', {
+       var rstore = Ext.create('Proxmox.data.UpdateStore', {
            interval: 3000,
            storeid: 'ceph-pool-list' + nodename,
            model: 'ceph-pool-list',
            proxy: {
-                type: 'pve',
-                url: "/api2/json/nodes/" + nodename + "/ceph/pools"
-           }
-       });
-
-       var store = Ext.create('PVE.data.DiffStore', { rstore: rstore });
-
-       PVE.Utils.monStoreErrors(me, rstore);
-
-       var create_btn = new Ext.Button({
-           text: gettext('Create'),
-           handler: function() {
-               var win = Ext.create('PVE.CephCreatePool', {
-                    nodename: nodename
-               });
-               win.show();
-           }
-       });
-
-       var remove_btn = new PVE.button.Button({
-           text: gettext('Remove'),
-           selModel: sm,
-           disabled: true,
-           confirmMsg: function(rec) {
-               var msg = Ext.String.format(gettext('Are you sure you want to remove entry {0}'),
-                                           "'" + rec.data.pool_name + "'");
-               msg += " " + gettext('This will permanently erase all data.');
-
-               return msg;
+               type: 'proxmox',
+               url: `/api2/json/nodes/${nodename}/ceph/pool`,
            },
-           handler: function() {
-               var rec = sm.getSelection()[0];
+       });
+       let store = Ext.create('Proxmox.data.DiffStore', { rstore: rstore });
 
-               if (!rec.data.pool_name) {
-                   return;
-               }
+       // manages the "install ceph?" overlay
+       PVE.Utils.monitor_ceph_installed(me, rstore, nodename);
 
-               PVE.Utils.API2Request({
-                   url: "/nodes/" + nodename + "/ceph/pools/" +
-                       rec.data.pool_name,
-                   method: 'DELETE',
-                   failure: function(response, opts) {
-                       Ext.Msg.alert(gettext('Error'), response.htmlStatus);
-                   }
-               });
+       var run_editor = function() {
+           let rec = sm.getSelection()[0];
+           if (!rec || !rec.data.pool_name) {
+               return;
            }
-       });
+           Ext.create('PVE.Ceph.PoolEdit', {
+               title: gettext('Edit') + ': Ceph Pool',
+               nodename: nodename,
+               pool_name: rec.data.pool_name,
+               isErasure: rec.data.type === 'erasure',
+               autoShow: true,
+               listeners: {
+                   destroy: () => rstore.load(),
+               },
+           });
+       };
 
        Ext.apply(me, {
            store: store,
            selModel: sm,
-           tbar: [ create_btn, remove_btn ],
+           tbar: [
+               {
+                   text: gettext('Create'),
+                   handler: function() {
+                       let keys = [
+                           'global:osd-pool-default-min-size',
+                           'global:osd-pool-default-size',
+                       ];
+                       let params = {
+                           'config-keys': keys.join(';'),
+                       };
+
+                       Proxmox.Utils.API2Request({
+                           url: '/nodes/localhost/ceph/cfg/value',
+                           method: 'GET',
+                           params,
+                           waitMsgTarget: me.getView(),
+                           failure: response => Ext.Msg.alert(gettext('Error'), response.htmlStatus),
+                           success: function({ result: { data } }) {
+                               let global = data.global;
+                               let defaultSize = global?.['osd-pool-default-size'] ?? 3;
+                               let defaultMinSize = global?.['osd-pool-default-min-size'] ?? 2;
+
+                               Ext.create('PVE.Ceph.PoolEdit', {
+                                   title: gettext('Create') + ': Ceph Pool',
+                                   isCreate: true,
+                                   isErasure: false,
+                                   defaultSize,
+                                   defaultMinSize,
+                                   nodename: nodename,
+                                   autoShow: true,
+                                   listeners: {
+                                       destroy: () => rstore.load(),
+                                   },
+                               });
+                           },
+                       });
+                   },
+               },
+               {
+                   xtype: 'proxmoxButton',
+                   text: gettext('Edit'),
+                   selModel: sm,
+                   disabled: true,
+                   handler: run_editor,
+               },
+               {
+                   xtype: 'proxmoxButton',
+                   text: gettext('Destroy'),
+                   selModel: sm,
+                   disabled: true,
+                   handler: function() {
+                       let rec = sm.getSelection()[0];
+                       if (!rec || !rec.data.pool_name) {
+                           return;
+                       }
+                       let poolName = rec.data.pool_name;
+                       Ext.create('Proxmox.window.SafeDestroy', {
+                           showProgress: true,
+                           url: `/nodes/${nodename}/ceph/pool/${poolName}`,
+                           params: {
+                               remove_storages: 1,
+                           },
+                           item: {
+                               type: 'CephPool',
+                               id: poolName,
+                           },
+                           taskName: 'cephdestroypool',
+                           autoShow: true,
+                           listeners: {
+                               destroy: () => rstore.load(),
+                           },
+                       });
+                   },
+               },
+           ],
            listeners: {
-               activate: rstore.startUpdate,
-               destroy: rstore.stopUpdate
-           }
+               activate: () => rstore.startUpdate(),
+               destroy: () => rstore.stopUpdate(),
+               itemdblclick: run_editor,
+           },
        });
 
        me.callParent();
-    }
+    },
 }, function() {
-
     Ext.define('ceph-pool-list', {
        extend: 'Ext.data.Model',
-       fields: [ 'pool_name',
-                 { name: 'pool', type: 'integer'},
-                 { name: 'size', type: 'integer'},
-                 { name: 'min_size', type: 'integer'},
-                 { name: 'pg_num', type: 'integer'},
-                 { name: 'bytes_used', type: 'integer'},
-                 { name: 'percent_used', type: 'number'},
-                 { name: 'crush_ruleset', type: 'integer'}
+       fields: ['pool_name',
+                 { name: 'pool', type: 'integer' },
+                 { name: 'size', type: 'integer' },
+                 { name: 'min_size', type: 'integer' },
+                 { name: 'pg_num', type: 'integer' },
+                 { name: 'pg_num_min', type: 'integer' },
+                 { name: 'bytes_used', type: 'integer' },
+                 { name: 'percent_used', type: 'number' },
+                 { name: 'crush_rule', type: 'integer' },
+                 { name: 'crush_rule_name', type: 'string' },
+                 { name: 'pg_autoscale_mode', type: 'string' },
+                 { name: 'pg_num_final', type: 'integer' },
+                 { name: 'target_size_ratio', type: 'number' },
+                 { name: 'target_size', type: 'integer' },
                ],
-       idProperty: 'pool_name'
+       idProperty: 'pool_name',
     });
 });
+
+Ext.define('PVE.form.CephRuleSelector', {
+    extend: 'Ext.form.field.ComboBox',
+    alias: 'widget.pveCephRuleSelector',
+
+    allowBlank: false,
+    valueField: 'name',
+    displayField: 'name',
+    editable: false,
+    queryMode: 'local',
+
+    initComponent: function() {
+       let me = this;
+
+       if (!me.nodename) {
+           throw "no nodename given";
+       }
+
+       me.originalAllowBlank = me.allowBlank;
+       me.allowBlank = true;
+
+       Ext.apply(me, {
+           store: {
+               fields: ['name'],
+               sorters: 'name',
+               proxy: {
+                   type: 'proxmox',
+                   url: `/api2/json/nodes/${me.nodename}/ceph/rules`,
+               },
+               autoLoad: {
+                   callback: (records, op, success) => {
+                       if (me.isCreate && success && records.length > 0) {
+                           me.select(records[0]);
+                       }
+
+                       me.allowBlank = me.originalAllowBlank;
+                       delete me.originalAllowBlank;
+                       me.validate();
+                   },
+               },
+           },
+       });
+
+       me.callParent();
+    },
+
+});