Ext.define('PVE.node.CephStatus', {
- extend: 'PVE.grid.ObjectGrid',
- alias: ['widget.pveNodeCephStatus'],
+ extend: 'Ext.panel.Panel',
+ alias: 'widget.pveNodeCephStatus',
- initComponent: function() {
- /*jslint confusion: true */
- var me = this;
+ onlineHelp: 'chapter_pveceph',
- var nodename = me.pveSelNode.data.node;
- if (!nodename) {
- throw "no node name specified";
+ scrollable: true,
+ bodyPadding: 5,
+ layout: {
+ type: 'column',
+ },
+
+ defaults: {
+ padding: 5,
+ },
+
+ items: [
+ {
+ xtype: 'panel',
+ title: gettext('Health'),
+ bodyPadding: 10,
+ plugins: 'responsive',
+ responsiveConfig: {
+ 'width < 1600': {
+ minHeight: 230,
+ columnWidth: 1,
+ },
+ 'width >= 1600': {
+ minHeight: 500,
+ columnWidth: 0.5,
+ },
+ },
+ layout: {
+ type: 'hbox',
+ align: 'stretch',
+ },
+ items: [
+ {
+ xtype: 'container',
+ layout: {
+ type: 'vbox',
+ align: 'stretch',
+ },
+ flex: 1,
+ items: [
+ {
+ flex: 1,
+ itemId: 'overallhealth',
+ xtype: 'pveHealthWidget',
+ title: gettext('Status'),
+ },
+ {
+ itemId: 'versioninfo',
+ xtype: 'displayfield',
+ fieldLabel: gettext('Ceph Version'),
+ value: "",
+ autoEl: {
+ tag: 'div',
+ 'data-qtip': gettext('The newest version installed in the Cluster.'),
+ },
+ padding: '10 0 0 0',
+ style: {
+ 'text-align': 'center',
+ },
+ },
+ ],
+ },
+ {
+ flex: 2,
+ itemId: 'warnings',
+ stateful: true,
+ stateId: 'ceph-status-warnings',
+ xtype: 'grid',
+ // we load the store manually, to show an emptyText specify an empty intermediate store
+ store: {
+ trackRemoved: false,
+ data: [],
+ },
+ emptyText: gettext('No Warnings/Errors'),
+ columns: [
+ {
+ dataIndex: 'severity',
+ header: gettext('Severity'),
+ align: 'center',
+ width: 70,
+ renderer: function(value) {
+ let health = PVE.Utils.map_ceph_health[value];
+ let icon = PVE.Utils.get_health_icon(health);
+ return `<i class="fa fa-fw ${icon}"></i>`;
+ },
+ sorter: {
+ sorterFn: function(a, b) {
+ let health = ['HEALTH_ERR', 'HEALTH_WARN', 'HEALTH_OK'];
+ return health.indexOf(b.data.severity) - health.indexOf(a.data.severity);
+ },
+ },
+ },
+ {
+ dataIndex: 'summary',
+ header: gettext('Summary'),
+ flex: 1,
+ },
+ {
+ xtype: 'actioncolumn',
+ width: 40,
+ align: 'center',
+ tooltip: gettext('Detail'),
+ items: [
+ {
+ iconCls: 'x-fa fa-info-circle',
+ handler: function(grid, rowindex, colindex, item, e, record) {
+ var win = Ext.create('Ext.window.Window', {
+ title: gettext('Detail'),
+ resizable: true,
+ modal: true,
+ width: 650,
+ height: 400,
+ layout: {
+ type: 'fit',
+ },
+ items: [{
+ scrollable: true,
+ padding: 10,
+ xtype: 'box',
+ html: [
+ '<span>' + Ext.htmlEncode(record.data.summary) + '</span>',
+ '<pre>' + Ext.htmlEncode(record.data.detail) + '</pre>',
+ ],
+ }],
+ });
+ win.show();
+ },
+ },
+ ],
+ },
+ ],
+ },
+ ],
+ },
+ {
+ xtype: 'pveCephStatusDetail',
+ itemId: 'statusdetail',
+ plugins: 'responsive',
+ responsiveConfig: {
+ 'width < 1600': {
+ columnWidth: 1,
+ minHeight: 250,
+ },
+ 'width >= 1600': {
+ columnWidth: 0.5,
+ minHeight: 300,
+ },
+ },
+ title: gettext('Status'),
+ },
+ {
+ title: gettext('Services'),
+ xtype: 'pveCephServices',
+ itemId: 'services',
+ plugins: 'responsive',
+ layout: {
+ type: 'hbox',
+ align: 'stretch',
+ },
+ responsiveConfig: {
+ 'width < 1600': {
+ columnWidth: 1,
+ minHeight: 200,
+ },
+ 'width >= 1600': {
+ columnWidth: 0.5,
+ minHeight: 200,
+ },
+ },
+ },
+ {
+ xtype: 'panel',
+ title: gettext('Performance'),
+ columnWidth: 1,
+ bodyPadding: 5,
+ layout: {
+ type: 'hbox',
+ align: 'center',
+ },
+ items: [
+ {
+ flex: 1,
+ xtype: 'container',
+ items: [
+ {
+ xtype: 'proxmoxGauge',
+ itemId: 'space',
+ title: gettext('Usage'),
+ },
+ {
+ flex: 1,
+ border: false,
+ },
+ {
+ xtype: 'container',
+ itemId: 'recovery',
+ hidden: true,
+ padding: 25,
+ items: [
+ {
+ itemId: 'recoverychart',
+ xtype: 'pveRunningChart',
+ title: gettext('Recovery') +'/ '+ gettext('Rebalance'),
+ renderer: PVE.Utils.render_bandwidth,
+ height: 100,
+ },
+ {
+ xtype: 'progressbar',
+ itemId: 'recoveryprogress',
+ },
+ ],
+ },
+ ],
+ },
+ {
+ flex: 2,
+ xtype: 'container',
+ defaults: {
+ padding: 0,
+ height: 100,
+ },
+ items: [
+ {
+ itemId: 'reads',
+ xtype: 'pveRunningChart',
+ title: gettext('Reads'),
+ renderer: PVE.Utils.render_bandwidth,
+ },
+ {
+ itemId: 'writes',
+ xtype: 'pveRunningChart',
+ title: gettext('Writes'),
+ renderer: PVE.Utils.render_bandwidth,
+ },
+ {
+ itemId: 'readiops',
+ xtype: 'pveRunningChart',
+ title: 'IOPS: ' + gettext('Reads'),
+ renderer: Ext.util.Format.numberRenderer('0,000'),
+ },
+ {
+ itemId: 'writeiops',
+ xtype: 'pveRunningChart',
+ title: 'IOPS: ' + gettext('Writes'),
+ renderer: Ext.util.Format.numberRenderer('0,000'),
+ },
+ ],
+ },
+ ],
+ },
+ ],
+
+ generateCheckData: function(health) {
+ var result = [];
+ let checks = health.checks || {};
+
+ Object.keys(checks).sort().forEach(key => {
+ let check = checks[key];
+ result.push({
+ id: key,
+ summary: check.summary.message,
+ detail: check.detail.reduce((acc, v) => `${acc}\n${v.message}`, ''),
+ severity: check.severity,
+ });
+ });
+ return result;
+ },
+
+ updateAll: function(store, records, success) {
+ if (!success || records.length === 0) {
+ return;
}
- var renderquorum = function(value) {
- if (!value || value.length < 0) {
- return 'No';
- }
+ var me = this;
+ var rec = records[0];
+ me.status = rec.data;
- return 'Yes {' + value.join(' ') + '}';
- };
+ // add health panel
+ me.down('#overallhealth').updateHealth(PVE.Utils.render_ceph_health(rec.data.health || {}));
+ // add errors to gridstore
+ me.down('#warnings').getStore().loadRawData(me.generateCheckData(rec.data.health || {}), false);
- var rendermonmap = function(d) {
- if (!d) {
- return '';
- }
+ // update services
+ me.getComponent('services').updateAll(me.metadata || {}, rec.data);
- var txt = 'e' + d.epoch + ': ' + d.mons.length + " mons at ";
+ // update detailstatus panel
+ me.getComponent('statusdetail').updateAll(me.metadata || {}, rec.data);
- Ext.Array.each(d.mons, function(d) {
- txt += d.name + '=' + d.addr + ',';
- });
+ // add performance data
+ let pgmap = rec.data.pgmap;
+ let used = pgmap.bytes_used;
+ let total = pgmap.bytes_total;
+
+ var text = Ext.String.format(gettext('{0} of {1}'),
+ Proxmox.Utils.render_size(used),
+ Proxmox.Utils.render_size(total),
+ );
+
+ // update the usage widget
+ me.down('#space').updateValue(used/total, text);
- return txt;
- };
+ let readiops = pgmap.read_op_per_sec;
+ let writeiops = pgmap.write_op_per_sec;
+ let reads = pgmap.read_bytes_sec || 0;
+ let writes = pgmap.write_bytes_sec || 0;
- var renderosdmap = function(value) {
- if (!value || !value.osdmap) {
- return '';
+ // update the graphs
+ me.reads.addDataPoint(reads);
+ me.writes.addDataPoint(writes);
+ me.readiops.addDataPoint(readiops);
+ me.writeiops.addDataPoint(writeiops);
+
+ let degraded = pgmap.degraded_objects || 0;
+ let misplaced = pgmap.misplaced_objects || 0;
+ let unfound = pgmap.unfound_objects || 0;
+ let unhealthy = degraded + unfound + misplaced;
+ // update recovery
+ if (pgmap.recovering_objects_per_sec !== undefined || unhealthy > 0) {
+ let toRecoverObjects = pgmap.misplaced_total || pgmap.unfound_total || pgmap.degraded_total || 0;
+ if (toRecoverObjects === 0) {
+ return; // FIXME: unexpected return and leaves things possible visible when it shouldn't?
}
+ let recovered = toRecoverObjects - unhealthy || 0;
+ let speed = pgmap.recovering_bytes_per_sec || 0;
- var d = value.osdmap;
+ let recoveryRatio = recovered / toRecoverObjects;
+ let txt = `${(recoveryRatio * 100).toFixed(2)}%`;
+ if (speed > 0) {
+ let obj_per_sec = speed / (4 * 1024 * 1024); // 4 MiB per Object
+ let duration = Proxmox.Utils.format_duration_human(unhealthy/obj_per_sec);
+ let speedTxt = PVE.Utils.render_bandwidth(speed);
+ txt += ` (${speedTxt} - ${duration} left)`;
+ }
- var txt = 'e' + d.epoch + ': ';
+ me.down('#recovery').setVisible(true);
+ me.down('#recoveryprogress').updateValue(recoveryRatio);
+ me.down('#recoveryprogress').updateText(txt);
+ me.down('#recoverychart').addDataPoint(speed);
+ } else {
+ me.down('#recovery').setVisible(false);
+ me.down('#recoverychart').addDataPoint(0);
+ }
+ },
- txt += d.num_osds + ' osds: ' + d.num_up_osds + ' up, ' +
- d.num_in_osds + " in";
+ initComponent: function() {
+ var me = this;
- return txt;
- };
+ var nodename = me.pveSelNode.data.node;
- var renderhealth = function(value) {
- if (!value || !value.overall_status) {
- return '';
- }
+ me.callParent();
+ var baseurl = '/api2/json' + (nodename ? '/nodes/' + nodename : '/cluster') + '/ceph';
+ me.store = Ext.create('Proxmox.data.UpdateStore', {
+ storeid: 'ceph-status-' + (nodename || 'cluster'),
+ interval: 5000,
+ proxy: {
+ type: 'proxmox',
+ url: baseurl + '/status',
+ },
+ });
- var txt = value.overall_status;
+ me.metadatastore = Ext.create('Proxmox.data.UpdateStore', {
+ storeid: 'ceph-metadata-' + (nodename || 'cluster'),
+ interval: 15*1000,
+ proxy: {
+ type: 'proxmox',
+ url: '/api2/json/cluster/ceph/metadata',
+ },
+ });
- Ext.Array.each(value.summary, function(d) {
- txt += " " + d.summary + ';';
- });
+ // save references for the updatefunction
+ me.iops = me.down('#iops');
+ me.readiops = me.down('#readiops');
+ me.writeiops = me.down('#writeiops');
+ me.reads = me.down('#reads');
+ me.writes = me.down('#writes');
- return txt;
- };
+ // manages the "install ceph?" overlay
+ PVE.Utils.monitor_ceph_installed(me, me.store, nodename);
- var renderpgmap = function(d) {
- if (!d) {
- return '';
+ me.mon(me.store, 'load', me.updateAll, me);
+ me.mon(me.metadatastore, 'load', function(store, records, success) {
+ if (!success || records.length < 1) {
+ return;
}
+ me.metadata = records[0].data;
- var txt = 'v' + d.version + ': ';
+ // update services
+ me.getComponent('services').updateAll(me.metadata, me.status || {});
- txt += d.num_pgs + " pgs:";
+ // update detailstatus panel
+ me.getComponent('statusdetail').updateAll(me.metadata, me.status || {});
- Ext.Array.each(d.pgs_by_state, function(s) {
- txt += " " + s.count + " " + s.state_name;
- });
- txt += '; ';
-
- txt += PVE.Utils.format_size(d.data_bytes) + " data, ";
- txt += PVE.Utils.format_size(d.bytes_used) + " used, ";
- txt += PVE.Utils.format_size(d.bytes_avail) + " avail";
-
- return txt;
- };
-
- Ext.applyIf(me, {
- url: "/api2/json/nodes/" + nodename + "/ceph/status",
- cwidth1: 150,
- interval: 3000,
- rows: {
- health: {
- header: 'health',
- renderer: renderhealth,
- required: true
- },
- quorum_names: {
- header: 'quorum',
- renderer: renderquorum,
- required: true
- },
- fsid: {
- header: 'cluster',
- required: true
- },
- monmap: {
- header: 'monmap',
- renderer: rendermonmap,
- required: true
- },
- osdmap: {
- header: 'osdmap',
- renderer: renderosdmap,
- required: true
- },
- pgmap: {
- header: 'pgmap',
- renderer: renderpgmap,
- required: true
+ let maxversion = [];
+ let maxversiontext = "";
+ for (const [_nodename, data] of Object.entries(me.metadata.node)) {
+ let version = data.version.parts;
+ if (PVE.Utils.compare_ceph_versions(version, maxversion) > 0) {
+ maxversion = version;
+ maxversiontext = data.version.str;
}
}
- });
+ me.down('#versioninfo').setValue(maxversiontext);
+ }, me);
- me.callParent();
+ me.on('destroy', me.store.stopUpdate);
+ me.on('destroy', me.metadatastore.stopUpdate);
+ me.store.startUpdate();
+ me.metadatastore.startUpdate();
+ },
- me.on('show', me.rstore.startUpdate);
- me.on('hide', me.rstore.stopUpdate);
- me.on('destroy', me.rstore.stopUpdate);
- }
});