Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat(xo-server): implement disk status check #7060

Merged
merged 3 commits into from
Sep 28, 2023
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 4 additions & 2 deletions CHANGELOG.unreleased.md
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,8 @@

> Users must be able to say: “Nice enhancement, I'm eager to test it”

- [Host/Advanced] Display system disks health based on the _smartctl_ plugin. [#4458](https://github.com/vatesfr/xen-orchestra/issues/4458) (PR [#7060](https://github.com/vatesfr/xen-orchestra/pull/7060))

### Bug fixes

> Users must be able to say: “I had this issue, happy to know it's fixed”
Expand Down Expand Up @@ -34,7 +36,7 @@
- @xen-orchestra/xapi minor
- vhd-lib patch
- xo-server-backup-reports patch
- xo-server patch
- xo-web patch
- xo-server minor
- xo-web minor
julien-f marked this conversation as resolved.
Show resolved Hide resolved

<!--packages-end-->
49 changes: 49 additions & 0 deletions packages/xo-server/src/api/host.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -456,3 +456,52 @@ setControlDomainMemory.params = {
setControlDomainMemory.resolve = {
host: ['id', 'host', 'administrate'],
}

// -------------------------------------------------------------------
/**
*
* @param {{host:HOST}} params
* @returns null if plugin is not installed or don't have the method
* an object device: status on success
*/
export function getSmartctlHealth({ host }) {
return this.getXapi(host).getSmartctlHealth(host._xapiId)
}

getSmartctlHealth.description = 'get smartctl health status'

getSmartctlHealth.params = {
id: { type: 'string' },
}

getSmartctlHealth.resolve = {
host: ['id', 'host', 'view'],
}

/**
*
* @param {{host:HOST}} params
* @returns null if plugin is not installed or don't have the method
* an object device: full device information on success
*/
export function getSmartctlInformation({ host, deviceNames }) {
return this.getXapi(host).getSmartctlInformation(host._xapiId, deviceNames)
}

getSmartctlInformation.description = 'get smartctl information'

getSmartctlInformation.params = {
id: { type: 'string' },

deviceNames: {
type: 'array',
items: {
type: 'string',
},
optional: true,
},
}

getSmartctlInformation.resolve = {
host: ['id', 'host', 'view'],
}
31 changes: 31 additions & 0 deletions packages/xo-server/src/xapi/index.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@ import mixin from '@xen-orchestra/mixin/legacy.js'
import ms from 'ms'
import noop from 'lodash/noop.js'
import once from 'lodash/once.js'
import pick from 'lodash/pick.js'
import tarStream from 'tar-stream'
import uniq from 'lodash/uniq.js'
import { asyncMap } from '@xen-orchestra/async-map'
Expand Down Expand Up @@ -1428,4 +1429,34 @@ export default class Xapi extends XapiBase {
}
}
}

async getSmartctlHealth(hostId) {
try {
return JSON.parse(await this.call('host.call_plugin', this.getObject(hostId).$ref, 'smartctl.py', 'health', {}))
} catch (error) {
if (error.code === 'XENAPI_MISSING_PLUGIN' || error.code === 'UNKNOWN_XENAPI_PLUGIN_FUNCTION') {
return null
} else {
throw error
}
}
}

async getSmartctlInformation(hostId, deviceNames) {
try {
const informations = JSON.parse(
await this.call('host.call_plugin', this.getObject(hostId).$ref, 'smartctl.py', 'information', {})
)
if (deviceNames === undefined) {
return informations
}
return pick(informations, deviceNames)
} catch (error) {
if (error.code === 'XENAPI_MISSING_PLUGIN' || error.code === 'UNKNOWN_XENAPI_PLUGIN_FUNCTION') {
return null
} else {
throw error
}
}
}
}
3 changes: 3 additions & 0 deletions packages/xo-web/src/common/intl/messages.js
Original file line number Diff line number Diff line change
Expand Up @@ -981,6 +981,7 @@ const messages = {
// ----- host stat tab -----
statLoad: 'Load average',
// ----- host advanced tab -----
disksSystemHealthy: 'All disks are healthy ✅',
editHostIscsiIqnTitle: 'Edit iSCSI IQN',
editHostIscsiIqnMessage:
'Are you sure you want to edit the iSCSI IQN? This may result in failures connecting to existing SRs if the host is attached to iSCSI SRs.',
Expand Down Expand Up @@ -1031,6 +1032,7 @@ const messages = {
hostRemoteSyslog: 'Remote syslog',
hostIommu: 'IOMMU',
hostNoCertificateInstalled: 'No certificates installed on this host',
smartctlPluginNotInstalled: 'Smartctl plugin not installed',
supplementalPacks: 'Installed supplemental packs',
supplementalPackNew: 'Install new supplemental pack',
supplementalPackPoolNew: 'Install supplemental pack on every host',
Expand All @@ -1041,6 +1043,7 @@ const messages = {
supplementalPackInstallErrorMessage: 'The installation of the supplemental pack failed.',
supplementalPackInstallSuccessTitle: 'Installation success',
supplementalPackInstallSuccessMessage: 'Supplemental pack successfully installed.',
systemDisksHealth: 'System disks health',
uniqueHostIscsiIqnInfo: 'The iSCSI IQN must be unique. ',
// ----- Host net tabs -----
networkCreateButton: 'Add a network',
Expand Down
5 changes: 5 additions & 0 deletions packages/xo-web/src/common/xo/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -1024,6 +1024,11 @@ export const isHyperThreadingEnabledHost = host =>
id: resolveId(host),
})

export const getSmartctlHealth = host => _call('host.getSmartctlHealth', { id: resolveId(host) })

export const getSmartctlInformation = (host, deviceNames) =>
_call('host.getSmartctlInformation', { id: resolveId(host), deviceNames })

export const installCertificateOnHost = (id, props) => _call('host.installCertificate', { id, ...props })

export const setControlDomainMemory = (id, memory) => _call('host.setControlDomainMemory', { id, memory })
Expand Down
50 changes: 47 additions & 3 deletions packages/xo-web/src/xo-app/host/tab-advanced.js
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
import _ from 'intl'
import ActionButton from 'action-button'
import BulkIcons from 'bulk-icons'
import Component from 'base-component'
import Copiable from 'copiable'
import decorate from 'apply-decorators'
Expand Down Expand Up @@ -33,6 +34,8 @@ import {
isHyperThreadingEnabledHost,
isNetDataInstalledOnHost,
getPlugin,
getSmartctlHealth,
getSmartctlInformation,
restartHost,
setControlDomainMemory,
setHostsMultipathing,
Expand Down Expand Up @@ -161,17 +164,36 @@ MultipathableSrs.propTypes = {
})
export default class extends Component {
async componentDidMount() {
const { host } = this.props
const plugin = await getPlugin('netdata')
const isNetDataPluginCorrectlySet = plugin !== undefined && plugin.loaded
this.setState({ isNetDataPluginCorrectlySet })
if (isNetDataPluginCorrectlySet) {
this.setState({
isNetDataPluginInstalledOnHost: await isNetDataInstalledOnHost(this.props.host),
isNetDataPluginInstalledOnHost: await isNetDataInstalledOnHost(host),
})
}

const smartctlHealth = await getSmartctlHealth(host)
const isSmartctlHealthEnabled = smartctlHealth !== null
const smartctlUnhealthyDevices = isSmartctlHealthEnabled
? Object.keys(smartctlHealth).filter(deviceName => smartctlHealth[deviceName] !== 'PASSED')
: undefined

let unhealthyDevicesAlerts
if (smartctlUnhealthyDevices?.length > 0) {
const unhealthyDeviceInformations = await getSmartctlInformation(host, smartctlUnhealthyDevices)
unhealthyDevicesAlerts = map(unhealthyDeviceInformations, (value, key) => ({
level: 'warning',
render: <pre>{_('keyValue', { key, value: JSON.stringify(value, null, 2) })}</pre>,
}))
}

this.setState({
isHtEnabled: await isHyperThreadingEnabledHost(this.props.host),
isHtEnabled: await isHyperThreadingEnabledHost(host).catch(() => null),
isSmartctlHealthEnabled,
smartctlUnhealthyDevices,
unhealthyDevicesAlerts,
})
}

Expand Down Expand Up @@ -233,7 +255,14 @@ export default class extends Component {

render() {
const { controlDomain, host, pcis, pgpus, schedGran } = this.props
const { isHtEnabled, isNetDataPluginInstalledOnHost, isNetDataPluginCorrectlySet } = this.state
const {
isHtEnabled,
isNetDataPluginInstalledOnHost,
isNetDataPluginCorrectlySet,
isSmartctlHealthEnabled,
unhealthyDevicesAlerts,
smartctlUnhealthyDevices,
} = this.state

const _isXcpNgHost = host.productBrand === 'XCP-ng'

Expand Down Expand Up @@ -512,6 +541,21 @@ export default class extends Component {
{host.bios_strings['bios-vendor']} ({host.bios_strings['bios-version']})
</td>
</tr>
<tr>
<th>{_('systemDisksHealth')}</th>
<td>
{isSmartctlHealthEnabled !== undefined &&
(isSmartctlHealthEnabled ? (
smartctlUnhealthyDevices?.length === 0 ? (
_('disksSystemHealthy')
) : (
<BulkIcons alerts={unhealthyDevicesAlerts ?? []} />
)
) : (
_('smartctlPluginNotInstalled')
))}
</td>
</tr>
</tbody>
</table>
<br />
Expand Down