mirror of
https://git.proxmox.com/git/pve-manager
synced 2025-07-24 05:44:07 +00:00

we are only allowed to set autoselect the first record after load on creation, else we may change the value by mistake which, if the admin does not notices when changing some other setting, can be quite fatal as it can trigger a huge rebalance, where the cause may then not even be obvious and thus an admin be quite baffled. Signed-off-by: Thomas Lamprecht <t.lamprecht@proxmox.com>
500 lines
11 KiB
JavaScript
500 lines
11 KiB
JavaScript
Ext.define('PVE.CephPoolInputPanel', {
|
|
extend: 'Proxmox.panel.InputPanel',
|
|
xtype: 'pveCephPoolInputPanel',
|
|
mixins: ['Proxmox.Mixin.CBind'],
|
|
|
|
showProgress: true,
|
|
onlineHelp: 'pve_ceph_pools',
|
|
|
|
subject: 'Ceph Pool',
|
|
column1: [
|
|
{
|
|
xtype: 'pmxDisplayEditField',
|
|
fieldLabel: gettext('Name'),
|
|
cbind: {
|
|
editable: '{isCreate}',
|
|
value: '{pool_name}',
|
|
},
|
|
name: 'name',
|
|
allowBlank: false,
|
|
},
|
|
{
|
|
xtype: 'proxmoxintegerfield',
|
|
fieldLabel: gettext('Size'),
|
|
name: 'size',
|
|
value: 3,
|
|
minValue: 2,
|
|
maxValue: 7,
|
|
allowBlank: false,
|
|
listeners: {
|
|
change: function(field, val) {
|
|
let size = Math.round((val + 1) / 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: 'warn',
|
|
allowBlank: false,
|
|
autoSelect: false,
|
|
labelWidth: 140,
|
|
},
|
|
{
|
|
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: 2,
|
|
cbind: {
|
|
minValue: (get) => 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: '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: 'pveCephRuleSelector',
|
|
fieldLabel: 'Crush Rule', // do not localize
|
|
name: 'crush_rule',
|
|
cbind: {
|
|
nodename: '{nodename}',
|
|
isCreate: '{isCreate}',
|
|
},
|
|
allowBlank: false,
|
|
},
|
|
{
|
|
xtype: 'proxmoxintegerfield',
|
|
fieldLabel: '# of PGs',
|
|
name: 'pg_num',
|
|
value: 128,
|
|
minValue: 1,
|
|
maxValue: 32768,
|
|
allowBlank: false,
|
|
emptyText: 128,
|
|
},
|
|
],
|
|
advancedColumn2: [
|
|
{
|
|
xtype: 'numberfield',
|
|
fieldLabel: gettext('Target Ratio'),
|
|
name: 'target_size_ratio',
|
|
minValue: 0,
|
|
decimalPrecision: 3,
|
|
allowBlank: true,
|
|
allowZero: 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',
|
|
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',
|
|
},
|
|
],
|
|
|
|
onGetValues: function(values) {
|
|
Object.keys(values || {}).forEach(function(name) {
|
|
if (values[name] === '') {
|
|
delete values[name];
|
|
}
|
|
});
|
|
|
|
return values;
|
|
},
|
|
});
|
|
|
|
Ext.define('PVE.CephPoolEdit', {
|
|
extend: 'Proxmox.window.Edit',
|
|
alias: 'widget.pveCephPoolEdit',
|
|
xtype: 'pveCephPoolEdit',
|
|
mixins: ['Proxmox.Mixin.CBind'],
|
|
|
|
cbindData: {
|
|
pool_name: '',
|
|
isCreate: (cfg) => !cfg.pool_name,
|
|
},
|
|
|
|
cbind: {
|
|
autoLoad: get => !get('isCreate'),
|
|
url: get => get('isCreate')
|
|
? `/nodes/${get('nodename')}/ceph/pools`
|
|
: `/nodes/${get('nodename')}/ceph/pools/${get('pool_name')}`,
|
|
method: get => get('isCreate') ? 'POST' : 'PUT',
|
|
},
|
|
|
|
showProgress: true,
|
|
|
|
subject: gettext('Ceph Pool'),
|
|
|
|
items: [{
|
|
xtype: 'pveCephPoolInputPanel',
|
|
cbind: {
|
|
nodename: '{nodename}',
|
|
pool_name: '{pool_name}',
|
|
isCreate: '{isCreate}',
|
|
},
|
|
}],
|
|
});
|
|
|
|
Ext.define('PVE.node.CephPoolList', {
|
|
extend: 'Ext.grid.GridPanel',
|
|
alias: 'widget.pveNodeCephPoolList',
|
|
|
|
onlineHelp: 'chapter_pveceph',
|
|
|
|
stateful: true,
|
|
stateId: 'grid-ceph-pools',
|
|
bufferedRenderer: false,
|
|
|
|
features: [{ ftype: 'summary' }],
|
|
|
|
columns: [
|
|
{
|
|
text: gettext('Name'),
|
|
minWidth: 120,
|
|
flex: 2,
|
|
sortable: true,
|
|
dataIndex: 'pool_name',
|
|
},
|
|
{
|
|
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;
|
|
},
|
|
},
|
|
{
|
|
text: gettext('Min. # of PGs'),
|
|
flex: 1,
|
|
minWidth: 100,
|
|
align: 'right',
|
|
dataIndex: 'pg_num_min',
|
|
hidden: true,
|
|
},
|
|
{
|
|
text: gettext('Target Ratio'),
|
|
flex: 1,
|
|
minWidth: 100,
|
|
align: 'right',
|
|
dataIndex: 'target_size_ratio',
|
|
renderer: Ext.util.Format.numberRenderer('0.0000'),
|
|
hidden: true,
|
|
},
|
|
{
|
|
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 nodename = me.pveSelNode.data.node;
|
|
if (!nodename) {
|
|
throw "no node name specified";
|
|
}
|
|
|
|
var sm = Ext.create('Ext.selection.RowModel', {});
|
|
|
|
var rstore = Ext.create('Proxmox.data.UpdateStore', {
|
|
interval: 3000,
|
|
storeid: 'ceph-pool-list' + nodename,
|
|
model: 'ceph-pool-list',
|
|
proxy: {
|
|
type: 'proxmox',
|
|
url: "/api2/json/nodes/" + nodename + "/ceph/pools",
|
|
},
|
|
});
|
|
let store = Ext.create('Proxmox.data.DiffStore', { rstore: rstore });
|
|
|
|
PVE.Utils.handleStoreErrorOrMask(
|
|
me,
|
|
rstore,
|
|
/not (installed|initialized)/i,
|
|
(_, error) => {
|
|
rstore.stopUpdate();
|
|
PVE.Utils.showCephInstallOrMask(me, error.statusText, nodename, win => {
|
|
me.mon(win, 'cephInstallWindowClosed', () => rstore.startUpdate());
|
|
});
|
|
},
|
|
);
|
|
|
|
var run_editor = function() {
|
|
let rec = sm.getSelection()[0];
|
|
if (!rec || !rec.data.pool_name) {
|
|
return;
|
|
}
|
|
Ext.create('PVE.CephPoolEdit', {
|
|
title: gettext('Edit') + ': Ceph Pool',
|
|
nodename: nodename,
|
|
pool_name: rec.data.pool_name,
|
|
autoShow: true,
|
|
listeners: {
|
|
destroy: () => rstore.load(),
|
|
},
|
|
});
|
|
};
|
|
|
|
Ext.apply(me, {
|
|
store: store,
|
|
selModel: sm,
|
|
tbar: [
|
|
{
|
|
text: gettext('Create'),
|
|
handler: function() {
|
|
Ext.create('PVE.CephPoolEdit', {
|
|
title: gettext('Create') + ': Ceph Pool',
|
|
isCreate: true,
|
|
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/pools/${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(),
|
|
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: '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',
|
|
});
|
|
});
|
|
|
|
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";
|
|
}
|
|
|
|
let store = Ext.create('Ext.data.Store', {
|
|
fields: ['name'],
|
|
sorters: 'name',
|
|
proxy: {
|
|
type: 'proxmox',
|
|
url: `/api2/json/nodes/${me.nodename}/ceph/rules`,
|
|
},
|
|
autoLoad: me.isCreate ? {
|
|
callback: (records, op, success) => {
|
|
if (success && records.length > 0) {
|
|
me.select(records[0]);
|
|
}
|
|
},
|
|
} : true,
|
|
});
|
|
Ext.apply(me, {
|
|
store: store,
|
|
});
|
|
|
|
me.callParent();
|
|
},
|
|
|
|
});
|