feat(xo-web/pool/host): add warning if hosts don't have the same version (#7280)

Fixes #7059
This commit is contained in:
Mathieu
2024-01-18 17:13:57 +01:00
committed by GitHub
parent b7a66e9f73
commit c1c122d92c
7 changed files with 310 additions and 217 deletions

View File

@@ -11,6 +11,7 @@
- [REST API] New pool action: `emergency_shutdown`, it suspends all the VMs and then shuts down all the host [#7277](https://github.com/vatesfr/xen-orchestra/issues/7277) (PR [#7279](https://github.com/vatesfr/xen-orchestra/pull/7279))
- [Tasks] Hide `/rrd_updates` tasks by default
- [Sign in] Support _Remember me_ feature with external providers (PR [#7298](https://github.com/vatesfr/xen-orchestra/pull/7298))
- [Pool/Host] Add a warning if hosts do not have the same version within a pool [#7059](https://github.com/vatesfr/xen-orchestra/issues/7059) (PR [#7280](https://github.com/vatesfr/xen-orchestra/pull/7280))
### Bug fixes

View File

@@ -328,6 +328,7 @@ const messages = {
powerState: 'Power state',
srSharedType: 'Shared {type}',
warningHostTimeTooltip: 'Host time and XOA time are not consistent with each other',
notAllHostsHaveTheSameVersion: 'Not all hosts within {pool} have the same version',
selectExistingTags: 'Select from existing tags',
sortByDisksUsage: 'Disks usage',

View File

@@ -30,6 +30,7 @@ import {
createSelector,
} from 'selectors'
import { injectState } from 'reaclette'
import { Host, Pool } from 'render-xo-item'
import MiniStats from './mini-stats'
import styles from './index.css'
@@ -126,13 +127,16 @@ export default class HostItem extends Component {
message,
}
}
_getAreHostsVersionsEqual = () => this.props.state.areHostsVersionsEqualByPool[this.props.item.$pool]
_getAlerts = createSelector(
() => this.props.needsRestart,
() => this.props.item,
this._isMaintained,
() => this.state.isHostTimeConsistentWithXoaTime,
(needsRestart, host, isMaintained, isHostTimeConsistentWithXoaTime) => {
this._getAreHostsVersionsEqual,
() => this.props.state.hostsByPoolId[this.props.item.$pool],
(needsRestart, host, isMaintained, isHostTimeConsistentWithXoaTime, areHostsVersionsEqual, poolHosts) => {
const alerts = []
if (needsRestart) {
@@ -201,6 +205,25 @@ export default class HostItem extends Component {
),
})
}
if (!areHostsVersionsEqual) {
alerts.push({
level: 'danger',
render: (
<div>
<p>
<Icon icon='alarm' /> {_('notAllHostsHaveTheSameVersion', { pool: <Pool id={host.$pool} link /> })}
</p>
<ul>
{map(poolHosts, host => (
<li>{_('keyValue', { key: <Host id={host.id} />, value: host.version })}</li>
))}
</ul>
</div>
),
})
}
return alerts
}
)

View File

@@ -13,6 +13,7 @@ import { addTag, editPool, getHostMissingPatches, removeTag } from 'xo'
import { connectStore, formatSizeShort } from 'utils'
import { compact, flatten, map, size, uniq } from 'lodash'
import { createGetObjectsOfType, createGetHostMetrics, createSelector } from 'selectors'
import { Host, Pool } from 'render-xo-item'
import { injectState } from 'reaclette'
import styles from './index.css'
@@ -101,10 +102,15 @@ export default class PoolItem extends Component {
_getPoolLicenseInfo = () => this.props.state.poolLicenseInfoByPoolId[this.props.item.id]
_getAreHostsVersionsEqual = () => this.props.state.areHostsVersionsEqualByPool[this.props.item.id]
_getAlerts = createSelector(
() => this.props.isAdmin,
this._getPoolLicenseInfo,
(isAdmin, poolLicenseInfo) => {
this._getAreHostsVersionsEqual,
() => this.props.poolHosts,
() => this.props.item.id,
(isAdmin, poolLicenseInfo, areHostsVersionsEqual, hosts, poolId) => {
const alerts = []
if (isAdmin && this._isXcpngPool()) {
@@ -120,6 +126,25 @@ export default class PoolItem extends Component {
})
}
}
if (!areHostsVersionsEqual) {
alerts.push({
level: 'danger',
render: (
<div>
<p>
<Icon icon='alarm' /> {_('notAllHostsHaveTheSameVersion', { pool: <Pool id={poolId} link /> })}
</p>
<ul>
{map(hosts, host => (
<li>{_('keyValue', { key: <Host id={host.id} />, value: host.version })}</li>
))}
</ul>
</div>
),
})
}
return alerts
}
)

View File

@@ -1,6 +1,7 @@
import * as CM from 'complex-matcher'
import _ from 'intl'
import Copiable from 'copiable'
import decorate from 'apply-decorators'
import Icon from 'icon'
import map from 'lodash/map'
import React from 'react'
@@ -11,151 +12,169 @@ import { BlockLink } from 'link'
import { Container, Row, Col } from 'grid'
import { FormattedRelative } from 'react-intl'
import { formatSize, formatSizeShort, hasLicenseRestrictions } from 'utils'
import { injectState, provideState } from 'reaclette'
import Usage, { UsageElement } from 'usage'
import { getObject } from 'selectors'
import { CpuSparkLines, MemorySparkLines, NetworkSparkLines, LoadSparkLines } from 'xo-sparklines'
import { Pool } from 'render-xo-item'
import LicenseWarning from './license-warning'
export default ({ statsOverview, host, nVms, vmController, vms }) => {
const pool = getObject(store.getState(), host.$pool)
const vmsFilter = encodeURIComponent(new CM.Property('$container', new CM.String(host.id)).toString())
return (
<Container>
<br />
<Row className='text-xs-center'>
<Col mediumSize={3}>
<h2>
{host.CPUs.cpu_count}x <Icon icon='cpu' size='lg' />
</h2>
<BlockLink to={`/hosts/${host.id}/stats`}>
{statsOverview && <CpuSparkLines data={statsOverview} />}
</BlockLink>
</Col>
<Col mediumSize={3}>
<h2>
{formatSize(host.memory.size)} <Icon icon='memory' size='lg' />
</h2>
<BlockLink to={`/hosts/${host.id}/stats`}>
{statsOverview && <MemorySparkLines data={statsOverview} />}
</BlockLink>
</Col>
<Col mediumSize={3}>
<BlockLink to={`/hosts/${host.id}/network`}>
export default decorate([
provideState({
computed: {
areHostsVersionsEqual: ({ areHostsVersionsEqualByPool }, { host }) => areHostsVersionsEqualByPool[host.$pool],
},
}),
injectState,
({ statsOverview, host, nVms, vmController, vms, state: { areHostsVersionsEqual } }) => {
const pool = getObject(store.getState(), host.$pool)
const vmsFilter = encodeURIComponent(new CM.Property('$container', new CM.String(host.id)).toString())
return (
<Container>
<br />
<Row className='text-xs-center'>
<Col mediumSize={3}>
<h2>
{host.$PIFs.length}x <Icon icon='network' size='lg' />
{host.CPUs.cpu_count}x <Icon icon='cpu' size='lg' />
</h2>
</BlockLink>
<BlockLink to={`/hosts/${host.id}/stats`}>
{statsOverview && <NetworkSparkLines data={statsOverview} />}
</BlockLink>
</Col>
<Col mediumSize={3}>
<BlockLink to={`/hosts/${host.id}/storage`}>
<BlockLink to={`/hosts/${host.id}/stats`}>
{statsOverview && <CpuSparkLines data={statsOverview} />}
</BlockLink>
</Col>
<Col mediumSize={3}>
<h2>
{host.$PBDs.length}x <Icon icon='disk' size='lg' />
{formatSize(host.memory.size)} <Icon icon='memory' size='lg' />
</h2>
</BlockLink>
<BlockLink to={`/hosts/${host.id}/stats`}>
{statsOverview && <LoadSparkLines data={statsOverview} />}
</BlockLink>
</Col>
</Row>
<br />
<Row className='text-xs-center'>
<Col mediumSize={3}>
<p className='text-xs-center'>
{_('started', {
ago: <FormattedRelative value={host.startTime * 1000} />,
})}
</p>
</Col>
<Col mediumSize={3}>
<p>
{host.productBrand} {host.version} (
{host.productBrand !== 'XCP-ng' ? host.license_params.sku_type : 'GPLv2'}){' '}
{hasLicenseRestrictions(host) && <LicenseWarning iconSize='lg' />}
</p>
</Col>
<Col mediumSize={3}>
<Copiable tagName='p'>{host.address}</Copiable>
</Col>
<Col mediumSize={3}>
<p>
{host.bios_strings['system-manufacturer']} {host.bios_strings['system-product-name']}
</p>
</Col>
</Row>
<br />
<Row>
<Col className='text-xs-center'>
<BlockLink to={`/home?t=VM&s=${vmsFilter}`}>
<h2>
{nVms}x <Icon icon='vm' size='lg' />
</h2>
</BlockLink>
</Col>
</Row>
<br />
<Row>
<Col className='text-xs-center'>
<h5>{_('hostTitleRamUsage')}</h5>
</Col>
</Row>
<Row>
<Col smallOffset={1} mediumSize={10}>
<Usage total={host.memory.size}>
<UsageElement
highlight
tooltip={`${host.productBrand} (${formatSize(vmController.memory.size)})`}
value={vmController.memory.size}
/>
{map(vms, vm => (
<BlockLink to={`/hosts/${host.id}/stats`}>
{statsOverview && <MemorySparkLines data={statsOverview} />}
</BlockLink>
</Col>
<Col mediumSize={3}>
<BlockLink to={`/hosts/${host.id}/network`}>
<h2>
{host.$PIFs.length}x <Icon icon='network' size='lg' />
</h2>
</BlockLink>
<BlockLink to={`/hosts/${host.id}/stats`}>
{statsOverview && <NetworkSparkLines data={statsOverview} />}
</BlockLink>
</Col>
<Col mediumSize={3}>
<BlockLink to={`/hosts/${host.id}/storage`}>
<h2>
{host.$PBDs.length}x <Icon icon='disk' size='lg' />
</h2>
</BlockLink>
<BlockLink to={`/hosts/${host.id}/stats`}>
{statsOverview && <LoadSparkLines data={statsOverview} />}
</BlockLink>
</Col>
</Row>
<br />
<Row className='text-xs-center'>
<Col mediumSize={3}>
<p className='text-xs-center'>
{_('started', {
ago: <FormattedRelative value={host.startTime * 1000} />,
})}
</p>
</Col>
<Col mediumSize={3}>
<p>
{host.productBrand} {host.version} (
{host.productBrand !== 'XCP-ng' ? host.license_params.sku_type : 'GPLv2'}){' '}
{hasLicenseRestrictions(host) && <LicenseWarning iconSize='lg' />}
</p>
</Col>
<Col mediumSize={3}>
<Copiable tagName='p'>{host.address}</Copiable>
</Col>
<Col mediumSize={3}>
<p>
{host.bios_strings['system-manufacturer']} {host.bios_strings['system-product-name']}
</p>
</Col>
</Row>
<br />
<Row>
<Col className='text-xs-center'>
<BlockLink to={`/home?t=VM&s=${vmsFilter}`}>
<h2>
{nVms}x <Icon icon='vm' size='lg' />
</h2>
</BlockLink>
</Col>
</Row>
<br />
<Row>
<Col className='text-xs-center'>
<h5>{_('hostTitleRamUsage')}</h5>
</Col>
</Row>
<Row>
<Col smallOffset={1} mediumSize={10}>
<Usage total={host.memory.size}>
<UsageElement
tooltip={`${vm.name_label} (${formatSize(vm.memory.size)})`}
key={vm.id}
value={vm.memory.size}
href={`#/vms/${vm.id}`}
highlight
tooltip={`${host.productBrand} (${formatSize(vmController.memory.size)})`}
value={vmController.memory.size}
/>
))}
</Usage>
</Col>
</Row>
<Row>
<Col className='text-xs-center'>
<h5>
{_('memoryHostState', {
memoryUsed: formatSizeShort(host.memory.usage),
memoryTotal: formatSizeShort(host.memory.size),
memoryFree: formatSizeShort(host.memory.size - host.memory.usage),
})}
</h5>
</Col>
</Row>
<Row>
{pool && host.id === pool.master && (
<Row className='text-xs-center'>
{map(vms, vm => (
<UsageElement
tooltip={`${vm.name_label} (${formatSize(vm.memory.size)})`}
key={vm.id}
value={vm.memory.size}
href={`#/vms/${vm.id}`}
/>
))}
</Usage>
</Col>
</Row>
<Row>
<Col className='text-xs-center'>
<h5>
{_('memoryHostState', {
memoryUsed: formatSizeShort(host.memory.usage),
memoryTotal: formatSizeShort(host.memory.size),
memoryFree: formatSizeShort(host.memory.size - host.memory.usage),
})}
</h5>
</Col>
</Row>
<Row>
{pool && host.id === pool.master && (
<Row className='text-xs-center'>
<Col>
<h3>
<span className='tag tag-pill tag-info'>{_('pillMaster')}</span>
</h3>
</Col>
</Row>
)}
</Row>
<Row>
<Col>
<h2 className='text-xs-center'>
<HomeTags
type='host'
labels={host.tags}
onDelete={tag => removeTag(host.id, tag)}
onAdd={tag => addTag(host.id, tag)}
/>
</h2>
</Col>
</Row>
{!areHostsVersionsEqual && (
<Row className='text-xs-center text-danger'>
<Col>
<h3>
<span className='tag tag-pill tag-info'>{_('pillMaster')}</span>
</h3>
<p>
<Icon icon='alarm' /> {_('notAllHostsHaveTheSameVersion', { pool: <Pool id={host.$pool} link /> })}
</p>
</Col>
</Row>
)}
</Row>
<Row>
<Col>
<h2 className='text-xs-center'>
<HomeTags
type='host'
labels={host.tags}
onDelete={tag => removeTag(host.id, tag)}
onAdd={tag => addTag(host.id, tag)}
/>
</h2>
</Col>
</Row>
</Container>
)
}
</Container>
)
},
])

View File

@@ -1,9 +1,11 @@
import Component from 'base-component'
import cookies from 'js-cookie'
import DocumentTitle from 'react-document-title'
import every from 'lodash/every'
import Icon from 'icon'
import Link from 'link'
import map from 'lodash/map'
import mapValues from 'lodash/mapValues'
import PropTypes from 'prop-types'
import React from 'react'
import Shortcuts from 'shortcuts'
@@ -172,7 +174,7 @@ export const ICON_POOL_LICENSE = {
xcpngLicenseById: (_, { xcpLicenses }) => keyBy(xcpLicenses, 'id'),
hostsByPoolId: createCollectionWrapper((_, { hosts }) =>
groupBy(
map(hosts, host => pick(host, ['$poolId', 'id'])),
map(hosts, host => pick(host, ['$poolId', 'id', 'version'])),
'$poolId'
)
),
@@ -230,6 +232,8 @@ export const ICON_POOL_LICENSE = {
placeholder: '',
},
isXoaStatusOk: ({ xoaStatus }) => !xoaStatus.includes('✖'),
areHostsVersionsEqualByPool: ({ hostsByPoolId }) =>
mapValues(hostsByPoolId, hosts => every(hosts, host => host.version === hosts[0].version)),
},
})
export default class XoApp extends Component {

View File

@@ -1,4 +1,5 @@
import _ from 'intl'
import decorate from 'apply-decorators'
import find from 'lodash/find'
import Icon from 'icon'
import map from 'lodash/map'
@@ -11,87 +12,106 @@ import { Container, Row, Col } from 'grid'
import Usage, { UsageElement } from 'usage'
import { formatSizeShort } from 'utils'
import Tooltip from 'tooltip'
import { injectState, provideState } from 'reaclette'
import { Pool } from 'render-xo-item'
export default ({ hosts, nVms, pool, srs }) => (
<Container>
<br />
<Row className='text-xs-center'>
<Col mediumSize={4}>
<Tooltip content={_('displayAllHosts')}>
<BlockLink to={`/home?s=$pool:${pool.id}&t=host`}>
<h2>
{hosts.length}x <Icon icon='host' size='lg' />
</h2>
</BlockLink>
</Tooltip>
</Col>
<Col mediumSize={4}>
<Tooltip content={_('displayAllStorages')}>
<BlockLink to={`/home?s=$pool:${pool.id}&t=SR`}>
<h2>
{srs.length}x <Icon icon='sr' size='lg' />
</h2>
</BlockLink>
</Tooltip>
</Col>
<Col mediumSize={4}>
<Tooltip content={_('displayAllVMs')}>
<BlockLink to={`/home?s=$pool:${pool.id}`}>
<h2>
{nVms}x <Icon icon='vm' size='lg' />
</h2>
</BlockLink>
</Tooltip>
</Col>
</Row>
<br />
<Row>
<Col className='text-xs-center'>
<h5>{_('poolTitleRamUsage')}</h5>
</Col>
</Row>
<Row>
<Col smallOffset={1} mediumSize={10}>
<Usage total={sumBy(hosts, 'memory.size')}>
{map(hosts, host => (
<UsageElement
tooltip={`${host.name_label} (${formatSizeShort(host.memory.usage)})`}
key={host.id}
value={host.memory.usage}
href={`#/hosts/${host.id}`}
export default decorate([
provideState({
computed: {
areHostsVersionsEqual: ({ areHostsVersionsEqualByPool }, { pool }) => areHostsVersionsEqualByPool[pool.id],
},
}),
injectState,
({ hosts, nVms, pool, srs, state: { areHostsVersionsEqual } }) => (
<Container>
<br />
<Row className='text-xs-center'>
<Col mediumSize={4}>
<Tooltip content={_('displayAllHosts')}>
<BlockLink to={`/home?s=$pool:${pool.id}&t=host`}>
<h2>
{hosts.length}x <Icon icon='host' size='lg' />
</h2>
</BlockLink>
</Tooltip>
</Col>
<Col mediumSize={4}>
<Tooltip content={_('displayAllStorages')}>
<BlockLink to={`/home?s=$pool:${pool.id}&t=SR`}>
<h2>
{srs.length}x <Icon icon='sr' size='lg' />
</h2>
</BlockLink>
</Tooltip>
</Col>
<Col mediumSize={4}>
<Tooltip content={_('displayAllVMs')}>
<BlockLink to={`/home?s=$pool:${pool.id}`}>
<h2>
{nVms}x <Icon icon='vm' size='lg' />
</h2>
</BlockLink>
</Tooltip>
</Col>
</Row>
<br />
<Row>
<Col className='text-xs-center'>
<h5>{_('poolTitleRamUsage')}</h5>
</Col>
</Row>
<Row>
<Col smallOffset={1} mediumSize={10}>
<Usage total={sumBy(hosts, 'memory.size')}>
{map(hosts, host => (
<UsageElement
tooltip={`${host.name_label} (${formatSizeShort(host.memory.usage)})`}
key={host.id}
value={host.memory.usage}
href={`#/hosts/${host.id}`}
/>
))}
</Usage>
</Col>
</Row>
<Row>
<Col className='text-xs-center'>
<h5>
{_('poolRamUsage', {
used: formatSizeShort(sumBy(hosts, 'memory.usage')),
total: formatSizeShort(sumBy(hosts, 'memory.size')),
free: formatSizeShort(sumBy(hosts, host => host.memory.size - host.memory.usage)),
})}
</h5>
</Col>
</Row>
<Row className='text-xs-center'>
<Col>
{_('poolMaster')}{' '}
<Link to={`/hosts/${pool.master}`}>{find(hosts, host => host.id === pool.master).name_label}</Link>
</Col>
</Row>
<Row className='text-xs-center'>
<Col>
<h2>
<HomeTags
type='pool'
labels={pool.tags}
onDelete={tag => removeTag(pool.id, tag)}
onAdd={tag => addTag(pool.id, tag)}
/>
))}
</Usage>
</Col>
</Row>
<Row>
<Col className='text-xs-center'>
<h5>
{_('poolRamUsage', {
used: formatSizeShort(sumBy(hosts, 'memory.usage')),
total: formatSizeShort(sumBy(hosts, 'memory.size')),
free: formatSizeShort(sumBy(hosts, host => host.memory.size - host.memory.usage)),
})}
</h5>
</Col>
</Row>
<Row className='text-xs-center'>
<Col>
{_('poolMaster')}{' '}
<Link to={`/hosts/${pool.master}`}>{find(hosts, host => host.id === pool.master).name_label}</Link>
</Col>
</Row>
<Row className='text-xs-center'>
<Col>
<h2>
<HomeTags
type='pool'
labels={pool.tags}
onDelete={tag => removeTag(pool.id, tag)}
onAdd={tag => addTag(pool.id, tag)}
/>
</h2>
</Col>
</Row>
</Container>
)
</h2>
</Col>
</Row>
{!areHostsVersionsEqual && (
<Row className='text-xs-center text-danger'>
<Col>
<p>
<Icon icon='alarm' /> {_('notAllHostsHaveTheSameVersion', { pool: <Pool id={pool.id} link /> })}
</p>
</Col>
</Row>
)}
</Container>
),
])