feat(xo-server/xo-web/health): detect invalid vhd-parent VDIs (#6356)

This commit is contained in:
Mathieu 2022-08-31 11:35:35 +02:00 committed by GitHub
parent cb1223f72e
commit a9c1239149
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
8 changed files with 77 additions and 41 deletions

View File

@ -7,6 +7,8 @@
> Users must be able to say: “Nice enhancement, I'm eager to test it” > Users must be able to say: “Nice enhancement, I'm eager to test it”
- [Dashboard/Health] Detect broken VHD chains and display missing parent VDIs (PR [#6356](https://github.com/vatesfr/xen-orchestra/pull/6356))
### Bug fixes ### Bug fixes
> Users must be able to say: “I had this issue, happy to know it's fixed” > Users must be able to say: “I had this issue, happy to know it's fixed”
@ -27,4 +29,7 @@
<!--packages-start--> <!--packages-start-->
- xo-server minor
- xo-web minor
<!--packages-end--> <!--packages-end-->

View File

@ -47,7 +47,7 @@ const DEFAULT_BLOCKED_LIST = {
'session.getUser': true, 'session.getUser': true,
'session.signIn': true, 'session.signIn': true,
'sr.getAllUnhealthyVdiChainsLength': true, 'sr.getAllUnhealthyVdiChainsLength': true,
'sr.getUnhealthyVdiChainsLength': true, 'sr.getVdiChainsInfo': true,
'sr.stats': true, 'sr.stats': true,
'system.getMethodsInfo': true, 'system.getMethodsInfo': true,
'system.getServerTimezone': true, 'system.getServerTimezone': true,

View File

@ -872,7 +872,7 @@ probeNfsExists.resolve = {
export const getAllUnhealthyVdiChainsLength = debounceWithKey(function getAllUnhealthyVdiChainsLength() { export const getAllUnhealthyVdiChainsLength = debounceWithKey(function getAllUnhealthyVdiChainsLength() {
const unhealthyVdiChainsLengthBySr = {} const unhealthyVdiChainsLengthBySr = {}
filter(this.objects.all, obj => obj.type === 'SR' && obj.content_type !== 'iso' && obj.size > 0).forEach(sr => { filter(this.objects.all, obj => obj.type === 'SR' && obj.content_type !== 'iso' && obj.size > 0).forEach(sr => {
const unhealthyVdiChainsLengthByVdi = this.getXapi(sr).getUnhealthyVdiChainsLength(sr) const unhealthyVdiChainsLengthByVdi = this.getXapi(sr).getVdiChainsInfo(sr)
if (!isEmpty(unhealthyVdiChainsLengthByVdi)) { if (!isEmpty(unhealthyVdiChainsLengthByVdi)) {
unhealthyVdiChainsLengthBySr[sr.uuid] = unhealthyVdiChainsLengthByVdi unhealthyVdiChainsLengthBySr[sr.uuid] = unhealthyVdiChainsLengthByVdi
} }
@ -882,15 +882,15 @@ export const getAllUnhealthyVdiChainsLength = debounceWithKey(function getAllUnh
// ------------------------------------------------------------------- // -------------------------------------------------------------------
export function getUnhealthyVdiChainsLength({ sr }) { export function getVdiChainsInfo({ sr }) {
return this.getXapi(sr).getUnhealthyVdiChainsLength(sr) return this.getXapi(sr).getVdiChainsInfo(sr)
} }
getUnhealthyVdiChainsLength.params = { getVdiChainsInfo.params = {
id: { type: 'string' }, id: { type: 'string' },
} }
getUnhealthyVdiChainsLength.resolve = { getVdiChainsInfo.resolve = {
sr: ['id', 'SR', 'operate'], sr: ['id', 'SR', 'operate'],
} }

View File

@ -3,9 +3,6 @@ import forEach from 'lodash/forEach.js'
import groupBy from 'lodash/groupBy.js' import groupBy from 'lodash/groupBy.js'
import { decorateWith } from '@vates/decorate-with' import { decorateWith } from '@vates/decorate-with'
import { defer } from 'golike-defer' import { defer } from 'golike-defer'
import { createLogger } from '@xen-orchestra/log'
const log = createLogger('xo:storage')
export default { export default {
_connectAllSrPbds(sr) { _connectAllSrPbds(sr) {
@ -52,39 +49,53 @@ export default {
await this._unplugPbd(this.getObject(id)) await this._unplugPbd(this.getObject(id))
}, },
_getUnhealthyVdiChainLength(uuid, childrenMap, cache) { _getVdiChainsInfo(uuid, childrenMap, cache) {
let length = cache[uuid] let info = cache[uuid]
if (length === undefined) { if (info === undefined) {
const children = childrenMap[uuid] const children = childrenMap[uuid]
length = children !== undefined && children.length === 1 ? 1 : 0 const unhealthyLength = children !== undefined && children.length === 1 ? 1 : 0
try { const vdi = this.getObjectByUuid(uuid, undefined)
const parent = this.getObjectByUuid(uuid).sm_config['vhd-parent'] if (vdi === undefined) {
info = { unhealthyLength, missingParent: uuid }
} else {
const parent = vdi.sm_config['vhd-parent']
if (parent !== undefined) { if (parent !== undefined) {
length += this._getUnhealthyVdiChainLength(parent, childrenMap, cache) info = this._getVdiChainsInfo(parent, childrenMap, cache)
info.unhealthyLength += unhealthyLength
} else {
info = { unhealthyLength }
} }
} catch (error) {
log.warn(`Xapi#_getUnhealthyVdiChainLength(${uuid})`, { error })
} }
cache[uuid] = length cache[uuid] = info
} }
return length return info
}, },
getUnhealthyVdiChainsLength(sr) { getVdiChainsInfo(sr) {
const vdis = this.getObject(sr).$VDIs const vdis = this.getObject(sr).$VDIs
const unhealthyVdis = { __proto__: null } const unhealthyVdis = { __proto__: null }
const children = groupBy(vdis, 'sm_config.vhd-parent') const children = groupBy(vdis, 'sm_config.vhd-parent')
const vdisWithUnknownVhdParent = { __proto__: null }
const cache = { __proto__: null } const cache = { __proto__: null }
forEach(vdis, vdi => { forEach(vdis, vdi => {
if (vdi.managed && !vdi.is_a_snapshot) { if (vdi.managed && !vdi.is_a_snapshot) {
const { uuid } = vdi const { uuid } = vdi
const length = this._getUnhealthyVdiChainLength(uuid, children, cache) const { unhealthyLength, missingParent } = this._getVdiChainsInfo(uuid, children, cache)
if (length !== 0) {
unhealthyVdis[uuid] = length if (unhealthyLength !== 0) {
unhealthyVdis[uuid] = unhealthyLength
}
if (missingParent !== undefined) {
vdisWithUnknownVhdParent[uuid] = missingParent
} }
} }
}) })
return unhealthyVdis
return {
vdisWithUnknownVhdParent,
unhealthyVdis,
}
}, },
// This function helps to reattach a forgotten NFS/iSCSI/HBA SR // This function helps to reattach a forgotten NFS/iSCSI/HBA SR

View File

@ -1450,7 +1450,9 @@ const messages = {
alarmObject: 'Issue on', alarmObject: 'Issue on',
alarmPool: 'Pool', alarmPool: 'Pool',
spaceLeftTooltip: '{used}% used ({free} left)', spaceLeftTooltip: '{used}% used ({free} left)',
unhealthyVdis: 'Unhealthy VDIs',
vdisToCoalesce: 'VDIs to coalesce', vdisToCoalesce: 'VDIs to coalesce',
vdisWithInvalidVhdParent: 'VDIs with invalid parent VHD',
srVdisToCoalesceWarning: 'This SR has more than {limitVdis, number} VDIs to coalesce', srVdisToCoalesceWarning: 'This SR has more than {limitVdis, number} VDIs to coalesce',
// ----- New VM ----- // ----- New VM -----

View File

@ -544,7 +544,7 @@ export const createSrUnhealthyVdiChainsLengthSubscription = sr => {
sr = resolveId(sr) sr = resolveId(sr)
let subscription = unhealthyVdiChainsLengthSubscriptionsBySr[sr] let subscription = unhealthyVdiChainsLengthSubscriptionsBySr[sr]
if (subscription === undefined) { if (subscription === undefined) {
subscription = createSubscription(() => _call('sr.getUnhealthyVdiChainsLength', { sr })) subscription = createSubscription(() => _call('sr.getVdiChainsInfo', { sr }))
unhealthyVdiChainsLengthSubscriptionsBySr[sr] = subscription unhealthyVdiChainsLengthSubscriptionsBySr[sr] = subscription
} }
return subscription return subscription

View File

@ -9,16 +9,16 @@ import Tooltip from 'tooltip'
import { Card, CardHeader, CardBlock } from 'card' import { Card, CardHeader, CardBlock } from 'card'
import { Col, Row } from 'grid' import { Col, Row } from 'grid'
import { injectState, provideState } from 'reaclette' import { injectState, provideState } from 'reaclette'
import { map, size } from 'lodash' import { forEach, isEmpty, map, size } from 'lodash'
import { Sr, Vdi } from 'render-xo-item' import { Sr, Vdi } from 'render-xo-item'
import { subscribeSrsUnhealthyVdiChainsLength, VDIS_TO_COALESCE_LIMIT } from 'xo' import { subscribeSrsUnhealthyVdiChainsLength, VDIS_TO_COALESCE_LIMIT } from 'xo'
const COLUMNS = [ const COLUMNS = [
{ {
itemRenderer: (srId, { unhealthyVdiChainsLengthBySr }) => ( itemRenderer: (srId, { vdisHealthBySr }) => (
<div> <div>
<Sr id={srId} link />{' '} <Sr id={srId} link />{' '}
{size(unhealthyVdiChainsLengthBySr[srId]) >= VDIS_TO_COALESCE_LIMIT && ( {size(vdisHealthBySr[srId].unhealthyVdis) >= VDIS_TO_COALESCE_LIMIT && (
<Tooltip content={_('srVdisToCoalesceWarning', { limitVdis: VDIS_TO_COALESCE_LIMIT })}> <Tooltip content={_('srVdisToCoalesceWarning', { limitVdis: VDIS_TO_COALESCE_LIMIT })}>
<span className='text-warning'> <span className='text-warning'>
<Icon icon='alarm' /> <Icon icon='alarm' />
@ -31,15 +31,15 @@ const COLUMNS = [
sortCriteria: 'name_label', sortCriteria: 'name_label',
}, },
{ {
itemRenderer: (srId, { unhealthyVdiChainsLengthBySr }) => ( itemRenderer: (srId, { vdisHealthBySr }) => (
<div> <div>
{map(unhealthyVdiChainsLengthBySr[srId], (chainLength, vdiId) => ( {map(vdisHealthBySr[srId].unhealthyVdis, (unhealthyVdiLength, vdiId) => (
<SingleLineRow key={vdiId}> <SingleLineRow key={vdiId}>
<Col> <Col>
<Vdi id={vdiId} /> <Vdi id={vdiId} />
</Col> </Col>
<Col> <Col>
<span>{_('length', { length: chainLength })}</span> <span>{_('length', { length: unhealthyVdiLength })}</span>
</Col> </Col>
</SingleLineRow> </SingleLineRow>
))} ))}
@ -47,33 +47,51 @@ const COLUMNS = [
), ),
name: _('vdisToCoalesce'), name: _('vdisToCoalesce'),
}, },
{
itemRenderer: (srId, { vdisHealthBySr }) => (
<div>
{Object.keys(vdisHealthBySr[srId].vdisWithUnknownVhdParent).map(vdiId => (
<Vdi id={vdiId} key={vdiId} />
))}
</div>
),
name: _('vdisWithInvalidVhdParent'),
},
] ]
const UnhealthyVdis = decorate([ const UnhealthyVdis = decorate([
addSubscriptions({ addSubscriptions({
unhealthyVdiChainsLengthBySr: subscribeSrsUnhealthyVdiChainsLength, vdisHealthBySr: subscribeSrsUnhealthyVdiChainsLength,
}), }),
provideState({ provideState({
computed: { computed: {
srIds: (state, { unhealthyVdiChainsLengthBySr = {} }) => Object.keys(unhealthyVdiChainsLengthBySr), srIds: (_, { vdisHealthBySr = {} }) => {
const srIds = []
forEach(vdisHealthBySr, ({ unhealthyVdis, vdisWithUnknownVhdParent }, srId) => {
if (!isEmpty(unhealthyVdis) || vdisWithUnknownVhdParent.length > 0) {
srIds.push(srId)
}
})
return srIds
},
}, },
}), }),
injectState, injectState,
({ state: { srIds }, unhealthyVdiChainsLengthBySr }) => ( ({ state: { srIds }, vdisHealthBySr }) => (
<Row> <Row>
<Col> <Col>
<Card> <Card>
<CardHeader> <CardHeader>
<Icon icon='disk' /> {_('vdisToCoalesce')} <Icon icon='disk' /> {_('unhealthyVdis')}
</CardHeader> </CardHeader>
<CardBlock> <CardBlock>
<Row> <Row>
<Col> <Col>
<SortedTable <SortedTable
data-unhealthyVdiChainsLengthBySr={unhealthyVdiChainsLengthBySr} data-vdisHealthBySr={vdisHealthBySr}
collection={srIds} collection={srIds}
columns={COLUMNS} columns={COLUMNS}
stateUrlParam='s_vdis_to_coalesce' stateUrlParam='s_unhealthy_vdis'
/> />
</Col> </Col>
</Row> </Row>

View File

@ -44,12 +44,12 @@ const UnhealthyVdiChains = flowRight(
connectStore(() => ({ connectStore(() => ({
vdis: createGetObjectsOfType('VDI').pick(createSelector((_, props) => props.chains, keys)), vdis: createGetObjectsOfType('VDI').pick(createSelector((_, props) => props.chains, keys)),
})) }))
)(({ chains, vdis }) => )(({ chains: { unhealthyVdis } = {}, vdis }) =>
isEmpty(vdis) ? null : ( isEmpty(vdis) ? null : (
<div> <div>
<hr /> <hr />
<h3>{_('srUnhealthyVdiTitle', { total: sum(values(chains)) })}</h3> <h3>{_('srUnhealthyVdiTitle', { total: sum(values(unhealthyVdis)) })}</h3>
<SortedTable collection={vdis} columns={COLUMNS} stateUrlParam='s_unhealthy_vdis' userData={chains} /> <SortedTable collection={vdis} columns={COLUMNS} stateUrlParam='s_unhealthy_vdis' userData={unhealthyVdis} />
</div> </div>
) )
) )