feat(xo-server/xo-web/health): detect invalid vhd-parent VDIs (#6356)
This commit is contained in:
parent
cb1223f72e
commit
a9c1239149
@ -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-->
|
||||||
|
@ -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,
|
||||||
|
@ -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'],
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -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
|
||||||
|
@ -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 -----
|
||||||
|
@ -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
|
||||||
|
@ -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>
|
||||||
|
@ -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>
|
||||||
)
|
)
|
||||||
)
|
)
|
||||||
|
Loading…
Reference in New Issue
Block a user