feat(XOSAN): allow user to update packs (#2782)

This commit is contained in:
Pierre Donias 2018-05-15 16:11:04 +02:00 committed by Julien Fontanet
parent ebab7c0867
commit 114501ebc7
9 changed files with 693 additions and 622 deletions

View File

@ -3857,7 +3857,8 @@ export default {
xosanUsedSpace: 'Espace utilisé',
// Original text: "XOSAN pack needs to be installed on each host of the pool."
xosanNeedPack: 'La pack XOSAN doit être installé sur tous les hôtes du pool.',
xosanNeedPack:
'Le pack XOSAN doit être installé et à jour sur tous les hôtes du pool.',
// Original text: "Install it now!"
xosanInstallIt: 'Installer maintenant !',

View File

@ -1766,7 +1766,8 @@ const messages = {
xosanUsedSpace: 'Used space',
xosanLicense: 'License',
xosanMultipleLicenses: 'This XOSAN has more than 1 license!',
xosanNeedPack: 'XOSAN pack needs to be installed on each host of the pool.',
xosanNeedPack:
'XOSAN pack needs to be installed and up to date on each host of the pool.',
xosanInstallIt: 'Install it now!',
xosanNeedRestart:
'Some hosts need their toolstack to be restarted before you can create an XOSAN',
@ -1794,6 +1795,14 @@ const messages = {
xosanPbdsDetached: 'Some SRs are detached from the XOSAN',
xosanBadStatus: 'Something is wrong with: {badStatuses}',
xosanRunning: 'Running',
xosanUpdatePacks: 'Update packs',
xosanPackUpdateChecking: 'Checking for updates',
xosanPackUpdateError:
'Error while checking XOSAN packs. Please make sure that the Cloud plugin is installed and loaded and that the updater is reachable.',
xosanPackUpdateUnavailable: 'XOSAN resources are unavailable',
xosanPackUpdateUnregistered: 'Not registered for XOSAN resources',
xosanPackUpdateUpToDate: "✓ This pool's XOSAN packs are up to date!",
xosanPackUpdateVersion: 'Update pool with latest pack v{version}',
xosanDelete: 'Delete XOSAN',
xosanFixIssue: 'Fix',
xosanCreatingOn: 'Creating XOSAN on {pool}',
@ -1810,12 +1819,8 @@ const messages = {
xosanRegister: 'Register your appliance first',
xosanLoading: 'Loading…',
xosanNotAvailable: 'XOSAN is not available at the moment',
xosanInstallPackOnHosts: 'Install XOSAN pack on these hosts:',
xosanInstallPack: 'Install {pack} v{version}?',
xosanNoPackFound:
'No compatible XOSAN pack found for your XenServer versions.',
xosanPackRequirements:
'At least one of these version requirements must be satisfied by all the hosts in this pool:',
// SR tab XOSAN
xosanVmsNotRunning: 'Some XOSAN Virtual Machines are not running',
xosanVmsNotFound: 'Some XOSAN Virtual Machines could not be found',

View File

@ -20,6 +20,7 @@ import {
mapValues,
replace,
sample,
some,
startsWith,
} from 'lodash'
@ -28,6 +29,7 @@ import * as actions from './store/actions'
import invoke from './invoke'
import store from './store'
import { getObject } from './selectors'
import { satisfies as versionSatisfies } from 'semver'
export const EMPTY_ARRAY = Object.freeze([])
export const EMPTY_OBJECT = Object.freeze({})
@ -523,6 +525,40 @@ export const ShortDate = ({ timestamp }) => (
<FormattedDate value={timestamp} month='short' day='numeric' year='numeric' />
)
export const findLatestPack = (packs, hostsVersions) => {
const checkVersion = version =>
!version ||
every(hostsVersions, hostVersion => versionSatisfies(hostVersion, version))
let latestPack = { version: '0' }
forEach(packs, pack => {
if (
pack.type === 'iso' &&
compareVersions(pack.version, '>', latestPack.version) &&
checkVersion(pack.requirements && pack.requirements.xenserver)
) {
latestPack = pack
}
})
if (latestPack.version === '0') {
// No compatible pack was found
return
}
return latestPack
}
export const isLatestXosanPackInstalled = (latestXosanPack, hosts) =>
latestXosanPack !== undefined &&
every(hosts, host =>
some(
host.supplementalPacks,
({ name, version }) =>
name === 'XOSAN' && version === latestXosanPack.version
)
)
// ===================================================================
export const getMemoryUsedMetric = ({ memory, memoryFree = memory }) =>

View File

@ -2412,20 +2412,6 @@ export const removeXosanBricks = (xosansr, bricks) =>
export const computeXosanPossibleOptions = (lvmSrs, brickSize) =>
_call('xosan.computeXosanPossibleOptions', { lvmSrs, brickSize })
import InstallXosanPackModal from './install-xosan-pack-modal' // eslint-disable-line import/first
export const downloadAndInstallXosanPack = pool =>
confirm({
title: _('xosanInstallPackTitle', { pool: pool.name_label }),
icon: 'export',
body: <InstallXosanPackModal pool={pool} />,
}).then(pack =>
_call('xosan.downloadAndInstallXosanPack', {
id: pack.id,
version: pack.version,
pool: resolveId(pool),
})
)
export const registerXosan = () =>
_call('cloud.registerResource', { namespace: 'xosan' })::tap(
subscribeResourceCatalog.forceRefresh
@ -2434,6 +2420,31 @@ export const registerXosan = () =>
export const fixHostNotInXosanNetwork = (xosanSr, host) =>
_call('xosan.fixHostNotInNetwork', { xosanSr, host })
// XOSAN packs -----------------------------------------------------------------
export const getResourceCatalog = () => _call('cloud.getResourceCatalog')
const downloadAndInstallXosanPack = (pack, pool, { version }) =>
_call('xosan.downloadAndInstallXosanPack', {
id: resolveId(pack),
version,
pool: resolveId(pool),
})
import UpdateXosanPacksModal from './update-xosan-packs-modal' // eslint-disable-line import/first
export const updateXosanPacks = pool =>
confirm({
title: _('xosanUpdatePacks'),
icon: 'host-patch-update',
body: <UpdateXosanPacksModal pool={pool} />,
}).then(pack => {
if (pack === undefined) {
return
}
return downloadAndInstallXosanPack(pack, pool, { version: pack.version })
})
// Licenses --------------------------------------------------------------------
export const getLicenses = productId => _call('xoa.getLicenses', { productId })

View File

@ -1,130 +0,0 @@
import _ from 'intl'
import Component from 'base-component'
import React from 'react'
import { connectStore, compareVersions, isXosanPack } from 'utils'
import { subscribeResourceCatalog, subscribePlugins } from 'xo'
import {
createGetObjectsOfType,
createSelector,
createCollectionWrapper,
} from 'selectors'
import { satisfies as versionSatisfies } from 'semver'
import { every, filter, forEach, map, some } from 'lodash'
const findLatestPack = (packs, hostsVersions) => {
const checkVersion = version =>
every(hostsVersions, hostVersion => versionSatisfies(hostVersion, version))
let latestPack = { version: '0' }
forEach(packs, pack => {
const xsVersionRequirement =
pack.requirements && pack.requirements.xenserver
if (
pack.type === 'iso' &&
compareVersions(pack.version, latestPack.version) > 0 &&
(!xsVersionRequirement || checkVersion(xsVersionRequirement))
) {
latestPack = pack
}
})
if (latestPack.version === '0') {
// No compatible pack was found
return
}
return latestPack
}
@connectStore(
() => ({
hosts: createGetObjectsOfType('host').filter(
createSelector(
(_, { pool }) => pool != null && pool.id,
poolId =>
poolId
? host =>
host.$pool === poolId &&
!some(host.supplementalPacks, isXosanPack)
: false
)
),
}),
{ withRef: true }
)
export default class InstallXosanPackModal extends Component {
componentDidMount () {
this._unsubscribePlugins = subscribePlugins(plugins =>
this.setState({ plugins })
)
this._unsubscribeResourceCatalog = subscribeResourceCatalog(catalog =>
this.setState({ catalog })
)
}
componentWillUnmount () {
this._unsubscribePlugins()
this._unsubscribeResourceCatalog()
}
_getXosanLatestPack = createSelector(
() => this.state.catalog && this.state.catalog.xosan,
createSelector(
() => this.props.hosts,
createCollectionWrapper(hosts => map(hosts, 'version'))
),
findLatestPack
)
_getXosanPacks = createSelector(
() => this.state.catalog && this.state.catalog.xosan,
packs => filter(packs, ({ type }) => type === 'iso')
)
get value () {
return this._getXosanLatestPack()
}
render () {
const { hosts } = this.props
const latestPack = this._getXosanLatestPack()
return (
<div>
{latestPack ? (
<div>
{_('xosanInstallPackOnHosts')}
<ul>
{map(hosts, host => <li key={host.id}>{host.name_label}</li>)}
</ul>
<div className='mt-1'>
{_('xosanInstallPack', {
pack: latestPack.name,
version: latestPack.version,
})}
</div>
</div>
) : (
<div>
{_('xosanNoPackFound')}
<br />
{_('xosanPackRequirements')}
<ul>
{map(this._getXosanPacks(), ({ name, requirements }, key) => (
<li key={key}>
{_.keyValue(
name,
requirements && requirements.xenserver
? requirements.xenserver
: '/'
)}
</li>
))}
</ul>
</div>
)}
</div>
)
}
}

View File

@ -0,0 +1,89 @@
import _ from 'intl'
import React from 'react'
import Component from 'base-component'
import { createGetObjectsOfType, createSelector } from 'selectors'
import { map } from 'lodash'
import { subscribeResourceCatalog } from 'xo'
import {
addSubscriptions,
isLatestXosanPackInstalled,
connectStore,
findLatestPack,
} from 'utils'
@connectStore(
{
hosts: createGetObjectsOfType('host').filter((_, { pool }) => host =>
host.$pool === pool.id
),
},
{ withRef: true }
)
@addSubscriptions(() => ({
catalog: subscribeResourceCatalog,
}))
export default class UpdateXosanPacksModal extends Component {
state = {
status: 'checking',
}
get value () {
return this.state.pack
}
_getStatus = createSelector(
() => this.props.catalog,
() => this.props.hosts,
(catalog, hosts) => {
if (catalog === undefined) {
return { status: 'error' }
}
if (catalog._namespaces.xosan === undefined) {
return { status: 'unavailable' }
}
if (!catalog._namespaces.xosan.registered) {
return { status: 'unregistered' }
}
const pack = findLatestPack(catalog.xosan, map(hosts, 'version'))
if (pack === undefined) {
return { status: 'noPack' }
}
if (isLatestXosanPackInstalled(pack, hosts)) {
return { status: 'upToDate' }
}
return { status: 'packFound', pack }
}
)
render () {
const { status, pack } = this._getStatus()
switch (status) {
case 'checking':
return <em>{_('xosanPackUpdateChecking')}</em>
case 'error':
return <em>{_('xosanPackUpdateError')}</em>
case 'unavailable':
return <em>{_('xosanPackUpdateUnavailable')}</em>
case 'unregistered':
return <em>{_('xosanPackUpdateUnregistered')}</em>
case 'noPack':
return <em>{_('xosanNoPackFound')}</em>
case 'upToDate':
return <em>{_('xosanPackUpdateUpToDate')}</em>
case 'packFound':
return (
<div>
{_('xosanPackUpdateVersion', {
version: pack.version,
})}
</div>
)
}
}
}

View File

@ -1,10 +1,11 @@
import _ from 'intl'
import Component from 'base-component'
import Copiable from 'copiable'
import React from 'react'
import TabButton from 'tab-button'
import SelectFiles from 'select-files'
import Upgrade from 'xoa-upgrade'
import { connectStore } from 'utils'
import { compareVersions, connectStore } from 'utils'
import { Toggle } from 'form'
import {
enableHost,
@ -17,7 +18,7 @@ import {
import { FormattedRelative, FormattedTime } from 'react-intl'
import { Container, Row, Col } from 'grid'
import { createGetObjectsOfType, createSelector } from 'selectors'
import { map, noop } from 'lodash'
import { forEach, map, noop } from 'lodash'
const ALLOW_INSTALL_SUPP_PACK = process.env.XOA_PLAN > 1
@ -31,7 +32,9 @@ const formatPack = ({ name, author, description, version }, key) => (
</tr>
)
export default connectStore(() => {
const getPackId = ({ author, name }) => `${author}\0${name}`
@connectStore(() => {
const getPgpus = createGetObjectsOfType('PGPU')
.pick((_, { host }) => host.$PGPUs)
.sort()
@ -44,7 +47,29 @@ export default connectStore(() => {
pcis: getPcis,
pgpus: getPgpus,
}
})(({ host, pcis, pgpus }) => (
})
export default class extends Component {
_getPacks = createSelector(
() => this.props.host.supplementalPacks,
packs => {
const uniqPacks = {}
let packId, previousPack
forEach(packs, pack => {
packId = getPackId(pack)
if (
(previousPack = uniqPacks[packId]) === undefined ||
compareVersions(pack.version, previousPack.version) > 0
) {
uniqPacks[packId] = pack
}
})
return uniqPacks
}
)
render () {
const { host, pcis, pgpus } = this.props
return (
<Container>
<Row>
<Col className='text-xs-right'>
@ -135,7 +160,9 @@ export default connectStore(() => {
<th>{_('hostStackStartedSince')}</th>
<td>
{_('started', {
ago: <FormattedRelative value={host.agentStartTime * 1000} />,
ago: (
<FormattedRelative value={host.agentStartTime * 1000} />
),
})}
</td>
</tr>
@ -224,7 +251,7 @@ export default connectStore(() => {
<h3>{_('supplementalPacks')}</h3>
<table className='table'>
<tbody>
{map(host.supplementalPacks, formatPack)}
{map(this._getPacks(), formatPack)}
{ALLOW_INSTALL_SUPP_PACK && (
<tr>
<th>{_('supplementalPackNew')}</th>
@ -247,4 +274,6 @@ export default connectStore(() => {
</Col>
</Row>
</Container>
))
)
}
}

View File

@ -10,24 +10,13 @@ import Tooltip from 'tooltip'
import { Container, Col, Row } from 'grid'
import { get } from 'xo-defined'
import { ignoreErrors } from 'promise-toolbox'
import {
every,
filter,
find,
flatten,
forEach,
isEmpty,
map,
mapValues,
some,
} from 'lodash'
import { every, filter, find, flatten, forEach, isEmpty, map } from 'lodash'
import { createGetObjectsOfType, createSelector, isAdmin } from 'selectors'
import {
addSubscriptions,
connectStore,
cowSet,
formatSize,
isXosanPack,
ShortDate,
} from 'utils'
import {
@ -37,6 +26,7 @@ import {
subscribePlugins,
subscribeResourceCatalog,
subscribeVolumeInfo,
updateXosanPacks,
} from 'xo'
import NewXosan from './new-xosan'
@ -208,6 +198,12 @@ const XOSAN_COLUMNS = [
]
const XOSAN_INDIVIDUAL_ACTIONS = [
{
handler: (xosan, { pools }) => updateXosanPacks(pools[xosan.$pool]),
icon: 'host-patch-update',
label: _('xosanUpdatePacks'),
level: 'primary',
},
{
handler: deleteSr,
icon: 'delete',
@ -221,14 +217,6 @@ const XOSAN_INDIVIDUAL_ACTIONS = [
const getHostsByPool = getHosts.groupBy('$pool')
const getPools = createGetObjectsOfType('pool')
const noPacksByPool = createSelector(getHostsByPool, hostsByPool =>
mapValues(
hostsByPool,
(poolHosts, poolId) =>
!every(poolHosts, host => some(host.supplementalPacks, isXosanPack))
)
)
const getPbdsBySr = createGetObjectsOfType('PBD').groupBy('SR')
const getXosanSrs = createSelector(
createGetObjectsOfType('SR').filter([
@ -291,7 +279,6 @@ const XOSAN_INDIVIDUAL_ACTIONS = [
isAdmin,
isMasterOfflineByPool: getIsMasterOfflineByPool,
hostsNeedRestartByPool: getHostsNeedRestartByPool,
noPacksByPool,
poolPredicate: getPoolPredicate,
pools: getPools,
xoaRegistration: state => state.xoaRegisterState,
@ -419,8 +406,8 @@ export default class Xosan extends Component {
const {
hostsNeedRestartByPool,
isAdmin,
noPacksByPool,
poolPredicate,
pools,
xoaRegistration,
xosanSrs,
} = this.props
@ -456,7 +443,6 @@ export default class Xosan extends Component {
(this._isXosanRegistered() ? (
<NewXosan
hostsNeedRestartByPool={hostsNeedRestartByPool}
noPacksByPool={noPacksByPool}
poolPredicate={poolPredicate}
onSrCreationFinished={this._updateLicenses}
onSrCreationStarted={this._onSrCreationStarted}
@ -498,6 +484,7 @@ export default class Xosan extends Component {
isAdmin,
licensesByXosan: this._getLicensesByXosan(),
licenseError,
pools,
status: this.state.status,
}}
/>

View File

@ -29,15 +29,18 @@ import {
} from 'selectors'
import {
addSubscriptions,
isLatestXosanPackInstalled,
compareVersions,
connectStore,
findLatestPack,
formatSize,
mapPlus,
} from 'utils'
import {
computeXosanPossibleOptions,
createXosanSR,
downloadAndInstallXosanPack,
updateXosanPacks,
getResourceCatalog,
restartHostsAgents,
subscribeResourceCatalog,
} from 'xo'
@ -76,14 +79,47 @@ export default class NewXosan extends Component {
suggestion: 0,
}
_checkPacks = pool =>
getResourceCatalog().then(
catalog => {
if (catalog === undefined || catalog.xosan === undefined) {
this.setState({
checkPackError: true,
})
return
}
const hosts = filter(this.props.hosts, { $pool: pool.id })
const pack = findLatestPack(catalog.xosan, map(hosts, 'version'))
if (isLatestXosanPackInstalled(pack, hosts)) {
this.setState({
needsUpdate: true,
})
}
},
() => {
this.setState({
checkPackError: true,
})
}
)
_updateXosanPacks = pool =>
updateXosanPacks(pool).then(() => this._checkPacks(pool))
_selectPool = pool => {
this.setState({
selectedSrs: {},
brickSize: DEFAULT_BRICKSIZE,
checkPackError: false,
memorySize: DEFAULT_MEMORY,
needsUpdate: false,
pif: undefined,
pool,
selectedSrs: {},
})
return this._checkPacks(pool)
}
componentDidUpdate () {
@ -243,10 +279,12 @@ export default class NewXosan extends Component {
const {
brickSize,
checkPackError,
customBrickSize,
customIpRange,
ipRange,
memorySize,
needsUpdate,
pif,
pool,
selectedSrs,
@ -256,12 +294,7 @@ export default class NewXosan extends Component {
vlan,
} = this.state
const {
hostsNeedRestartByPool,
noPacksByPool,
poolPredicate,
notRegistered,
} = this.props
const { hostsNeedRestartByPool, poolPredicate, notRegistered } = this.props
if (notRegistered) {
return (
@ -296,9 +329,7 @@ export default class NewXosan extends Component {
<Col size={4}>
<SelectPif
disabled={
pool == null ||
noPacksByPool[pool.id] ||
!isEmpty(hostsNeedRestart)
pool == null || needsUpdate || !isEmpty(hostsNeedRestart)
}
onChange={this.linkState('pif')}
predicate={this._getPifPredicate()}
@ -307,22 +338,26 @@ export default class NewXosan extends Component {
</Col>
</Row>
{pool != null &&
noPacksByPool[pool.id] && (
(checkPackError ? (
<em>{_('xosanPackUpdateError')}</em>
) : needsUpdate ? (
<Row>
<Col>
<Icon icon='error' /> {_('xosanNeedPack')}
<br />
<ActionButton
btnStyle='success'
handler={downloadAndInstallXosanPack}
handler={this._updateXosanPacks}
handlerParam={pool}
icon='export'
>
{_('xosanInstallIt')}
</ActionButton>
</Col>
</Row>
)}
{!isEmpty(hostsNeedRestart) && (
) : !isEmpty(hostsNeedRestart) ? (
<Row>
<Col>
<Icon icon='error' /> {_('xosanNeedRestart')}
<br />
<ActionButton
@ -333,12 +368,12 @@ export default class NewXosan extends Component {
>
{_('xosanRestartAgents')}
</ActionButton>
</Col>
</Row>
)}
{pool != null &&
!noPacksByPool[pool.id] &&
isEmpty(hostsNeedRestart) && [
) : (
[
<Row>
<Col>
<em>{_('xosanSelect2Srs')}</em>
<table className='table table-striped'>
<thead>
@ -380,9 +415,13 @@ export default class NewXosan extends Component {
<Tooltip
content={_('spaceLeftTooltip', {
used: String(
Math.round(sr.physical_usage / sr.size * 100)
Math.round(
sr.physical_usage / sr.size * 100
)
),
free: formatSize(
sr.size - sr.physical_usage
),
free: formatSize(sr.size - sr.physical_usage),
})}
>
<progress
@ -398,8 +437,10 @@ export default class NewXosan extends Component {
})}
</tbody>
</table>
</Col>
</Row>,
<Row>
<Col>
{!isEmpty(suggestions) && (
<div>
<h3>{_('xosanSuggestions')}</h3>
@ -548,6 +589,7 @@ export default class NewXosan extends Component {
<hr />
</div>
)}
</Col>
</Row>,
<Row>
<Col>
@ -561,7 +603,8 @@ export default class NewXosan extends Component {
</ActionButton>
</Col>
</Row>,
]}
]
))}
<hr />
</Container>
)