Ext.define('PVE.ceph.Services', { extend: 'Ext.panel.Panel', alias: 'widget.pveCephServices', layout: { type: 'hbox', align: 'stretch', }, bodyPadding: '0 5 20', defaults: { xtype: 'box', style: { 'text-align': 'center', }, }, items: [ { flex: 1, xtype: 'pveCephServiceList', itemId: 'mons', title: gettext('Monitors'), }, { flex: 1, xtype: 'pveCephServiceList', itemId: 'mgrs', title: gettext('Managers'), }, { flex: 1, xtype: 'pveCephServiceList', itemId: 'mdss', title: gettext('Meta Data Servers'), }, ], updateAll: function(metadata, status) { var me = this; const healthstates = { 'HEALTH_UNKNOWN': 0, 'HEALTH_ERR': 1, 'HEALTH_WARN': 2, 'HEALTH_UPGRADE': 3, 'HEALTH_OLD': 4, 'HEALTH_OK': 5, }; // order guarantee since es2020, but browsers did so before. Note, integers would break it. const healthmap = Object.keys(healthstates); let maxversion = "00.0.00"; Object.values(metadata.version || {}).forEach(function(version) { if (PVE.Utils.compare_ceph_versions(version, maxversion) > 0) { maxversion = version; } }); var quorummap = status && status.quorum_names ? status.quorum_names : []; let monmessages = {}, mgrmessages = {}, mdsmessages = {}; if (status) { if (status.health) { Ext.Object.each(status.health.checks, function(key, value, _obj) { if (!Ext.String.startsWith(key, "MON_")) { return; } for (let i = 0; i < value.detail.length; i++) { let match = value.detail[i].message.match(/mon.([a-zA-Z0-9\-.]+)/); if (!match) { continue; } let monid = match[1]; if (!monmessages[monid]) { monmessages[monid] = { worstSeverity: healthstates.HEALTH_OK, messages: [], }; } let severityIcon = PVE.Utils.get_ceph_icon_html(value.severity, true); let details = value.detail.reduce((acc, v) => `${acc}\n${v.message}`, ''); monmessages[monid].messages.push(severityIcon + details); if (healthstates[value.severity] < monmessages[monid].worstSeverity) { monmessages[monid].worstSeverity = healthstates[value.severity]; } } }); } if (status.mgrmap) { mgrmessages[status.mgrmap.active_name] = "active"; status.mgrmap.standbys.forEach(function(mgr) { mgrmessages[mgr.name] = "standby"; }); } if (status.fsmap) { status.fsmap.by_rank.forEach(function(mds) { mdsmessages[mds.name] = 'rank: ' + mds.rank + "; " + mds.status; }); } } let checks = { mon: function(mon) { if (quorummap.indexOf(mon.name) !== -1) { mon.health = healthstates.HEALTH_OK; } else { mon.health = healthstates.HEALTH_ERR; } if (monmessages[mon.name]) { if (monmessages[mon.name].worstSeverity < mon.health) { mon.health = monmessages[mon.name].worstSeverity; } Array.prototype.push.apply(mon.messages, monmessages[mon.name].messages); } return mon; }, mgr: function(mgr) { if (mgrmessages[mgr.name] === 'active') { mgr.title = '' + mgr.title + ''; mgr.statuses.push(gettext('Status') + ': active'); } else if (mgrmessages[mgr.name] === 'standby') { mgr.statuses.push(gettext('Status') + ': standby'); } else if (mgr.health > healthstates.HEALTH_WARN) { mgr.health = healthstates.HEALTH_WARN; } return mgr; }, mds: function(mds) { if (mdsmessages[mds.name]) { mds.title = '' + mds.title + ''; mds.statuses.push(gettext('Status') + ': ' + mdsmessages[mds.name]+""); } else if (mds.addr !== Proxmox.Utils.unknownText) { mds.statuses.push(gettext('Status') + ': standby'); } return mds; }, }; for (let type of ['mon', 'mgr', 'mds']) { var ids = Object.keys(metadata[type] || {}); me[type] = {}; for (let id of ids) { const [name, host] = id.split('@'); let result = { id: id, health: healthstates.HEALTH_OK, statuses: [], messages: [], name: name, title: metadata[type][id].name || name, host: host, version: PVE.Utils.parse_ceph_version(metadata[type][id]), service: metadata[type][id].service, addr: metadata[type][id].addr || metadata[type][id].addrs || Proxmox.Utils.unknownText, }; result.statuses = [ gettext('Host') + ": " + host, gettext('Address') + ": " + result.addr, ]; if (checks[type]) { result = checks[type](result); } if (result.service && !result.version) { result.messages.push( PVE.Utils.get_ceph_icon_html('HEALTH_UNKNOWN', true) + gettext('Stopped'), ); result.health = healthstates.HEALTH_UNKNOWN; } if (!result.version && result.addr === Proxmox.Utils.unknownText) { result.health = healthstates.HEALTH_UNKNOWN; } if (result.version) { result.statuses.push(gettext('Version') + ": " + result.version); if (result.version !== maxversion) { if (metadata.version[host] === maxversion) { if (result.health > healthstates.HEALTH_OLD) { result.health = healthstates.HEALTH_OLD; } result.messages.push( PVE.Utils.get_ceph_icon_html('HEALTH_OLD', true) + gettext('A newer version was installed but old version still running, please restart'), ); } else { if (result.health > healthstates.HEALTH_UPGRADE) { result.health = healthstates.HEALTH_UPGRADE; } result.messages.push( PVE.Utils.get_ceph_icon_html('HEALTH_UPGRADE', true) + gettext('Other cluster members use a newer version of this service, please upgrade and restart'), ); } } } result.statuses.push(''); // empty line result.text = result.statuses.concat(result.messages).join('
'); result.health = healthmap[result.health]; me[type][id] = result; } } me.getComponent('mons').updateAll(Object.values(me.mon)); me.getComponent('mgrs').updateAll(Object.values(me.mgr)); me.getComponent('mdss').updateAll(Object.values(me.mds)); }, }); Ext.define('PVE.ceph.ServiceList', { extend: 'Ext.container.Container', xtype: 'pveCephServiceList', style: { 'text-align': 'center', }, defaults: { xtype: 'box', style: { 'text-align': 'center', }, }, items: [ { itemId: 'title', data: { title: '', }, tpl: '

{title}

', }, ], updateAll: function(list) { var me = this; me.suspendLayout = true; list.sort((a, b) => a.id > b.id ? 1 : a.id < b.id ? -1 : 0); if (!me.ids) { me.ids = []; } let pendingRemoval = {}; me.ids.forEach(id => { pendingRemoval[id] = true; }); // mark all as to-remove first here for (let i = 0; i < list.length; i++) { let service = me.getComponent(list[i].id); if (!service) { // services and list are sorted, so just insert at i + 1 (first el. is the title) service = me.insert(i + 1, { xtype: 'pveCephServiceWidget', itemId: list[i].id, }); me.ids.push(list[i].id); } else { delete pendingRemoval[list[i].id]; // drop exisiting from for-removal } service.updateService(list[i].title, list[i].text, list[i].health); } Object.keys(pendingRemoval).forEach(id => me.remove(id)); // GC me.suspendLayout = false; me.updateLayout(); }, initComponent: function() { var me = this; me.callParent(); me.getComponent('title').update({ title: me.title, }); }, }); Ext.define('PVE.ceph.ServiceWidget', { extend: 'Ext.Component', alias: 'widget.pveCephServiceWidget', userCls: 'monitor inline-block', data: { title: '0', health: 'HEALTH_ERR', text: '', iconCls: PVE.Utils.get_health_icon(), }, tpl: [ '{title}: ', '', ], updateService: function(title, text, health) { var me = this; me.update(Ext.apply(me.data, { health: health, text: text, title: title, iconCls: PVE.Utils.get_health_icon(PVE.Utils.map_ceph_health[health]), })); if (me.tooltip) { me.tooltip.setHtml(text); } }, listeners: { destroy: function() { let me = this; if (me.tooltip) { me.tooltip.destroy(); delete me.tooltip; } }, mouseenter: { element: 'el', fn: function(events, element) { let view = this.component; if (!view) { return; } if (!view.tooltip || view.data.text !== view.tooltip.html) { view.tooltip = Ext.create('Ext.tip.ToolTip', { target: view.el, trackMouse: true, dismissDelay: 0, renderTo: Ext.getBody(), html: view.data.text, }); } view.tooltip.show(); }, }, mouseleave: { element: 'el', fn: function(events, element) { let view = this.component; if (view.tooltip) { view.tooltip.destroy(); delete view.tooltip; } }, }, }, });