diff --git a/@xen-orchestra/xapi/host.mjs b/@xen-orchestra/xapi/host.mjs index acadf8d2f2f..2f2344adddc 100644 --- a/@xen-orchestra/xapi/host.mjs +++ b/@xen-orchestra/xapi/host.mjs @@ -165,6 +165,21 @@ class Host { return ipmiSensorsByDataType } + + async getMdadmHealth(ref) { + try { + const result = await this.callAsync('host.call_plugin', ref, 'raid.py', 'check_raid_pool', {}) + const parsedResult = JSON.parse(result) + + return parsedResult + } catch (error) { + if (error.code === 'XENAPI_MISSING_PLUGIN' || error.code === 'UNKNOWN_XENAPI_PLUGIN_FUNCTION') { + return null + } else { + throw error + } + } + } } export default Host diff --git a/CHANGELOG.unreleased.md b/CHANGELOG.unreleased.md index ea3273d66d4..e00e5f5e410 100644 --- a/CHANGELOG.unreleased.md +++ b/CHANGELOG.unreleased.md @@ -13,6 +13,7 @@ - [Dashboard/Health] Add `BOND_STATUS_CHANGED` and `MULTIPATH_PERIODIC_ALERT` in alarms list (PR [#8199](https://github.com/vatesfr/xen-orchestra/pull/8199)) - [Signin] Start transitioning towards XO 6 Design System (PR [#8209](https://github.com/vatesfr/xen-orchestra/pull/8209)) +- [Host/Advanced] In host's advanced tab, display MDADM health information (PR [#8190](https://github.com/vatesfr/xen-orchestra/pull/8190)) ### Bug fixes diff --git a/packages/xo-server/src/api/host.mjs b/packages/xo-server/src/api/host.mjs index 017afad0c7d..a2cfc387d49 100644 --- a/packages/xo-server/src/api/host.mjs +++ b/packages/xo-server/src/api/host.mjs @@ -9,6 +9,8 @@ import { X509Certificate } from 'node:crypto' import backupGuard from './_backupGuard.mjs' import { asyncEach } from '@vates/async-each' +import { debounceWithKey } from '../_pDebounceWithKey.mjs' + const CERT_PUBKEY_MIN_SIZE = 2048 const IPMI_CACHE_TTL = 6e4 const IPMI_CACHE = new TTLCache({ @@ -624,6 +626,21 @@ getSmartctlInformation.resolve = { host: ['id', 'host', 'view'], } +function _getMdadmHealth({ host }) { + return this.getXapi(host).host_getMdadmHealth(host._xapiRef) +} +export const getMdadmHealth = debounceWithKey(_getMdadmHealth, 6e5, ({ host }) => host.id) + +getMdadmHealth.description = 'retrieve the mdadm RAID health information' + +getMdadmHealth.params = { + id: { type: 'string' }, +} + +getMdadmHealth.resolve = { + host: ['id', 'host', 'view'], +} + export async function getBlockdevices({ host }) { const xapi = this.getXapi(host) if (host.productBrand !== 'XCP-ng') { diff --git a/packages/xo-web/src/common/intl/messages.js b/packages/xo-web/src/common/intl/messages.js index 711f888d6bf..ae542d1ea12 100644 --- a/packages/xo-web/src/common/intl/messages.js +++ b/packages/xo-web/src/common/intl/messages.js @@ -1130,6 +1130,8 @@ const messages = { hostIommu: 'IOMMU', hostNoCertificateInstalled: 'No certificates installed on this host', 'onlyAvailableXcp8.3OrHigher': 'Only available for XCP-ng 8.3.0 or higher', + installRaidPlugin: 'To display RAID info, install raid.py plugin', + noRaidInformationAvailable: 'No RAID information available', pciDevices: 'PCI Devices', pciId: 'PCI ID', pcisEnable: 'PCI{nPcis, plural, one {} other {s}} enable', @@ -1148,6 +1150,9 @@ const messages = { supplementalPackInstallSuccessTitle: 'Installation success', supplementalPackInstallSuccessMessage: 'Supplemental pack successfully installed.', systemDisksHealth: 'System disks health', + raidHealthy: 'All mdadm RAID are healthy ✅', + raidStateWarning: 'RAID state needs your attention: {state}', + raidStatus: 'RAID Status', uniqueHostIscsiIqnInfo: 'The iSCSI IQN must be unique. ', vendorId: 'Vendor ID', // ----- Host net tabs ----- diff --git a/packages/xo-web/src/common/xo/index.js b/packages/xo-web/src/common/xo/index.js index f221a4d0bc5..a09340979c9 100644 --- a/packages/xo-web/src/common/xo/index.js +++ b/packages/xo-web/src/common/xo/index.js @@ -755,6 +755,17 @@ export const subscribeIpmiSensors = host => { return subscribeHostsIpmiSensors[hostId] } +const subscribeHostsMdadmHealth = {} +export const subscribeMdadmHealth = host => { + const hostId = resolveId(host) + + if (subscribeHostsMdadmHealth[hostId] === undefined) { + subscribeHostsMdadmHealth[hostId] = createSubscription(() => _call('host.getMdadmHealth', { id: hostId })) + } + + return subscribeHostsMdadmHealth[hostId] +} + export const getHostBiosInfo = host => _call('host.getBiosInfo', { id: resolveId(host) }) const subscribeVmSecurebootReadiness = {} diff --git a/packages/xo-web/src/xo-app/home/host-item.js b/packages/xo-web/src/xo-app/home/host-item.js index c4c0f8f2889..df7ed5eded1 100644 --- a/packages/xo-web/src/xo-app/home/host-item.js +++ b/packages/xo-web/src/xo-app/home/host-item.js @@ -21,6 +21,7 @@ import { startHost, stopHost, subscribeHvSupportedVersions, + subscribeMdadmHealth, } from 'xo' import { addSubscriptions, connectStore, formatSizeShort, hasLicenseRestrictions, osFamily } from 'utils' import { @@ -40,9 +41,10 @@ import BulkIcons from '../../common/bulk-icons' import { LICENSE_WARNING_BODY } from '../host/license-warning' import { getXoaPlan, SOURCES } from '../../common/xoa-plans' -@addSubscriptions({ +@addSubscriptions(props => ({ hvSupportedVersions: subscribeHvSupportedVersions, -}) + mdadmHealth: subscribeMdadmHealth(props.item), +})) @connectStore(() => ({ container: createGetObject((_, props) => props.item.$pool), isPubKeyTooShort: createSelector( @@ -144,6 +146,7 @@ export default class HostItem extends Component { this._getAreHostsVersionsEqual, () => this.props.state.hostsByPoolId[this.props.item.$pool], () => this.state.isPubKeyTooShort, + () => this.props.mdadmHealth, ( needsRestart, host, @@ -151,7 +154,8 @@ export default class HostItem extends Component { isHostTimeConsistentWithXoaTime, areHostsVersionsEqual, poolHosts, - isPubKeyTooShort + isPubKeyTooShort, + mdadmHealth ) => { const alerts = [] @@ -262,6 +266,17 @@ export default class HostItem extends Component { }) } + if (mdadmHealth?.raid?.State !== undefined && !['clean', 'active'].includes(mdadmHealth.raid.State)) { + alerts.push({ + level: 'danger', + render: ( + + {_('raidStateWarning', { state: mdadmHealth.raid.State })} + + ), + }) + } + return alerts } ) diff --git a/packages/xo-web/src/xo-app/host/tab-advanced.js b/packages/xo-web/src/xo-app/host/tab-advanced.js index ad73bdb62af..dee687fdd6f 100644 --- a/packages/xo-web/src/xo-app/host/tab-advanced.js +++ b/packages/xo-web/src/xo-app/host/tab-advanced.js @@ -45,6 +45,7 @@ import { setHostsMultipathing, setRemoteSyslogHost, setSchedulerGranularity, + subscribeMdadmHealth, subscribeSchedulerGranularity, toggleMaintenanceMode, } from 'xo' @@ -248,6 +249,7 @@ MultipathableSrs.propTypes = { @addSubscriptions(props => ({ schedGran: cb => subscribeSchedulerGranularity(props.host.id, cb), + mdadmHealth: subscribeMdadmHealth(props.host), })) @connectStore(() => { const getControlDomain = createGetObject((_, { host }) => host.controlDomain) @@ -347,6 +349,22 @@ export default class extends Component { } ) + displayMdadmStatus = createSelector( + () => this.props.mdadmHealth, + mdadmHealth => { + if (mdadmHealth == null) { + return _('installRaidPlugin') + } + + const raidState = mdadmHealth.raid?.State + if (raidState === undefined) { + return _('noRaidInformationAvailable') + } + + return ['clean', 'active'].includes(raidState) ? _('raidHealthy') : _('raidStateWarning', { state: raidState }) + } + ) + _setSchedulerGranularity = value => setSchedulerGranularity(this.props.host.id, value) _setHostIscsiIqn = iscsiIqn => @@ -688,6 +706,10 @@ export default class extends Component { ))} + + {_('raidStatus')} + {this.displayMdadmStatus()} +