-// 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;
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();
+ },
+
+});