Skip to content

Commit

Permalink
feat(xo-server/xo-web/health): detect invalid vhd-parent VDIs (#6356)
Browse files Browse the repository at this point in the history
  • Loading branch information
MathieuRA committed Aug 31, 2022
1 parent cb1223f commit a9c1239
Show file tree
Hide file tree
Showing 8 changed files with 77 additions and 41 deletions.
5 changes: 5 additions & 0 deletions CHANGELOG.unreleased.md
Expand Up @@ -7,6 +7,8 @@

> 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

> Users must be able to say: “I had this issue, happy to know it's fixed”
Expand All @@ -27,4 +29,7 @@
<!--packages-start-->

- xo-server minor
- xo-web minor

<!--packages-end-->
2 changes: 1 addition & 1 deletion packages/xo-server-audit/src/index.js
Expand Up @@ -47,7 +47,7 @@ const DEFAULT_BLOCKED_LIST = {
'session.getUser': true,
'session.signIn': true,
'sr.getAllUnhealthyVdiChainsLength': true,
'sr.getUnhealthyVdiChainsLength': true,
'sr.getVdiChainsInfo': true,
'sr.stats': true,
'system.getMethodsInfo': true,
'system.getServerTimezone': true,
Expand Down
10 changes: 5 additions & 5 deletions packages/xo-server/src/api/sr.mjs
Expand Up @@ -872,7 +872,7 @@ probeNfsExists.resolve = {
export const getAllUnhealthyVdiChainsLength = debounceWithKey(function getAllUnhealthyVdiChainsLength() {
const unhealthyVdiChainsLengthBySr = {}
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)) {
unhealthyVdiChainsLengthBySr[sr.uuid] = unhealthyVdiChainsLengthByVdi
}
Expand All @@ -882,15 +882,15 @@ export const getAllUnhealthyVdiChainsLength = debounceWithKey(function getAllUnh

// -------------------------------------------------------------------

export function getUnhealthyVdiChainsLength({ sr }) {
return this.getXapi(sr).getUnhealthyVdiChainsLength(sr)
export function getVdiChainsInfo({ sr }) {
return this.getXapi(sr).getVdiChainsInfo(sr)
}

getUnhealthyVdiChainsLength.params = {
getVdiChainsInfo.params = {
id: { type: 'string' },
}

getUnhealthyVdiChainsLength.resolve = {
getVdiChainsInfo.resolve = {
sr: ['id', 'SR', 'operate'],
}

Expand Down
49 changes: 30 additions & 19 deletions packages/xo-server/src/xapi/mixins/storage.mjs
Expand Up @@ -3,9 +3,6 @@ import forEach from 'lodash/forEach.js'
import groupBy from 'lodash/groupBy.js'
import { decorateWith } from '@vates/decorate-with'
import { defer } from 'golike-defer'
import { createLogger } from '@xen-orchestra/log'

const log = createLogger('xo:storage')

export default {
_connectAllSrPbds(sr) {
Expand Down Expand Up @@ -52,39 +49,53 @@ export default {
await this._unplugPbd(this.getObject(id))
},

_getUnhealthyVdiChainLength(uuid, childrenMap, cache) {
let length = cache[uuid]
if (length === undefined) {
_getVdiChainsInfo(uuid, childrenMap, cache) {
let info = cache[uuid]
if (info === undefined) {
const children = childrenMap[uuid]
length = children !== undefined && children.length === 1 ? 1 : 0
try {
const parent = this.getObjectByUuid(uuid).sm_config['vhd-parent']
const unhealthyLength = children !== undefined && children.length === 1 ? 1 : 0
const vdi = this.getObjectByUuid(uuid, undefined)
if (vdi === undefined) {
info = { unhealthyLength, missingParent: uuid }
} else {
const parent = vdi.sm_config['vhd-parent']
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 unhealthyVdis = { __proto__: null }
const children = groupBy(vdis, 'sm_config.vhd-parent')
const vdisWithUnknownVhdParent = { __proto__: null }

const cache = { __proto__: null }
forEach(vdis, vdi => {
if (vdi.managed && !vdi.is_a_snapshot) {
const { uuid } = vdi
const length = this._getUnhealthyVdiChainLength(uuid, children, cache)
if (length !== 0) {
unhealthyVdis[uuid] = length
const { unhealthyLength, missingParent } = this._getVdiChainsInfo(uuid, children, cache)

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
Expand Down
2 changes: 2 additions & 0 deletions packages/xo-web/src/common/intl/messages.js
Expand Up @@ -1450,7 +1450,9 @@ const messages = {
alarmObject: 'Issue on',
alarmPool: 'Pool',
spaceLeftTooltip: '{used}% used ({free} left)',
unhealthyVdis: 'Unhealthy VDIs',
vdisToCoalesce: 'VDIs to coalesce',
vdisWithInvalidVhdParent: 'VDIs with invalid parent VHD',
srVdisToCoalesceWarning: 'This SR has more than {limitVdis, number} VDIs to coalesce',

// ----- New VM -----
Expand Down
2 changes: 1 addition & 1 deletion packages/xo-web/src/common/xo/index.js
Expand Up @@ -544,7 +544,7 @@ export const createSrUnhealthyVdiChainsLengthSubscription = sr => {
sr = resolveId(sr)
let subscription = unhealthyVdiChainsLengthSubscriptionsBySr[sr]
if (subscription === undefined) {
subscription = createSubscription(() => _call('sr.getUnhealthyVdiChainsLength', { sr }))
subscription = createSubscription(() => _call('sr.getVdiChainsInfo', { sr }))
unhealthyVdiChainsLengthSubscriptionsBySr[sr] = subscription
}
return subscription
Expand Down
42 changes: 30 additions & 12 deletions packages/xo-web/src/xo-app/dashboard/health/unhealthyVdis.js
Expand Up @@ -9,16 +9,16 @@ import Tooltip from 'tooltip'
import { Card, CardHeader, CardBlock } from 'card'
import { Col, Row } from 'grid'
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 { subscribeSrsUnhealthyVdiChainsLength, VDIS_TO_COALESCE_LIMIT } from 'xo'

const COLUMNS = [
{
itemRenderer: (srId, { unhealthyVdiChainsLengthBySr }) => (
itemRenderer: (srId, { vdisHealthBySr }) => (
<div>
<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 })}>
<span className='text-warning'>
<Icon icon='alarm' />
Expand All @@ -31,49 +31,67 @@ const COLUMNS = [
sortCriteria: 'name_label',
},
{
itemRenderer: (srId, { unhealthyVdiChainsLengthBySr }) => (
itemRenderer: (srId, { vdisHealthBySr }) => (
<div>
{map(unhealthyVdiChainsLengthBySr[srId], (chainLength, vdiId) => (
{map(vdisHealthBySr[srId].unhealthyVdis, (unhealthyVdiLength, vdiId) => (
<SingleLineRow key={vdiId}>
<Col>
<Vdi id={vdiId} />
</Col>
<Col>
<span>{_('length', { length: chainLength })}</span>
<span>{_('length', { length: unhealthyVdiLength })}</span>
</Col>
</SingleLineRow>
))}
</div>
),
name: _('vdisToCoalesce'),
},
{
itemRenderer: (srId, { vdisHealthBySr }) => (
<div>
{Object.keys(vdisHealthBySr[srId].vdisWithUnknownVhdParent).map(vdiId => (
<Vdi id={vdiId} key={vdiId} />
))}
</div>
),
name: _('vdisWithInvalidVhdParent'),
},
]

const UnhealthyVdis = decorate([
addSubscriptions({
unhealthyVdiChainsLengthBySr: subscribeSrsUnhealthyVdiChainsLength,
vdisHealthBySr: subscribeSrsUnhealthyVdiChainsLength,
}),
provideState({
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,
({ state: { srIds }, unhealthyVdiChainsLengthBySr }) => (
({ state: { srIds }, vdisHealthBySr }) => (
<Row>
<Col>
<Card>
<CardHeader>
<Icon icon='disk' /> {_('vdisToCoalesce')}
<Icon icon='disk' /> {_('unhealthyVdis')}
</CardHeader>
<CardBlock>
<Row>
<Col>
<SortedTable
data-unhealthyVdiChainsLengthBySr={unhealthyVdiChainsLengthBySr}
data-vdisHealthBySr={vdisHealthBySr}
collection={srIds}
columns={COLUMNS}
stateUrlParam='s_vdis_to_coalesce'
stateUrlParam='s_unhealthy_vdis'
/>
</Col>
</Row>
Expand Down
6 changes: 3 additions & 3 deletions packages/xo-web/src/xo-app/sr/tab-advanced.js
Expand Up @@ -44,12 +44,12 @@ const UnhealthyVdiChains = flowRight(
connectStore(() => ({
vdis: createGetObjectsOfType('VDI').pick(createSelector((_, props) => props.chains, keys)),
}))
)(({ chains, vdis }) =>
)(({ chains: { unhealthyVdis } = {}, vdis }) =>
isEmpty(vdis) ? null : (
<div>
<hr />
<h3>{_('srUnhealthyVdiTitle', { total: sum(values(chains)) })}</h3>
<SortedTable collection={vdis} columns={COLUMNS} stateUrlParam='s_unhealthy_vdis' userData={chains} />
<h3>{_('srUnhealthyVdiTitle', { total: sum(values(unhealthyVdis)) })}</h3>
<SortedTable collection={vdis} columns={COLUMNS} stateUrlParam='s_unhealthy_vdis' userData={unhealthyVdis} />
</div>
)
)
Expand Down

0 comments on commit a9c1239

Please sign in to comment.