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,207 +47,233 @@ export default connectStore(() => {
pcis: getPcis,
pgpus: getPgpus,
}
})(({ host, pcis, pgpus }) => (
<Container>
<Row>
<Col className='text-xs-right'>
{host.power_state === 'Running' && (
<TabButton
btnStyle='warning'
handler={forceReboot}
handlerParam={host}
icon='host-force-reboot'
labelId='forceRebootHostLabel'
/>
)}
{host.enabled ? (
<TabButton
btnStyle='warning'
handler={disableHost}
handlerParam={host}
icon='host-disable'
labelId='disableHostLabel'
/>
) : (
<TabButton
btnStyle='success'
handler={enableHost}
handlerParam={host}
icon='host-enable'
labelId='enableHostLabel'
/>
)}
<TabButton
btnStyle='danger'
handler={detachHost}
handlerParam={host}
icon='host-eject'
labelId='detachHost'
/>
{host.power_state !== 'Running' && (
<TabButton
btnStyle='danger'
handler={forgetHost}
handlerParam={host}
icon='host-forget'
labelId='forgetHost'
/>
)}
</Col>
</Row>
<Row>
<Col>
<h3>{_('xenSettingsLabel')}</h3>
<table className='table'>
<tbody>
<tr>
<th>{_('uuid')}</th>
<Copiable tagName='td'>{host.uuid}</Copiable>
</tr>
<tr>
<th>{_('hostAddress')}</th>
<Copiable tagName='td'>{host.address}</Copiable>
</tr>
<tr>
<th>{_('hostStatus')}</th>
<td>
{host.enabled
? _('hostStatusEnabled')
: _('hostStatusDisabled')}
</td>
</tr>
<tr>
<th>{_('hostPowerOnMode')}</th>
<td>
<Toggle
disabled
onChange={noop}
value={Boolean(host.powerOnMode)}
/>
</td>
</tr>
<tr>
<th>{_('hostStartedSince')}</th>
<td>
{_('started', {
ago: <FormattedRelative value={host.startTime * 1000} />,
})}
</td>
</tr>
<tr>
<th>{_('hostStackStartedSince')}</th>
<td>
{_('started', {
ago: <FormattedRelative value={host.agentStartTime * 1000} />,
})}
</td>
</tr>
<tr>
<th>{_('hostXenServerVersion')}</th>
<Copiable tagName='td' data={host.version}>
{host.license_params.sku_marketing_name} {host.version} ({
host.license_params.sku_type
})
</Copiable>
</tr>
<tr>
<th>{_('hostBuildNumber')}</th>
<Copiable tagName='td'>{host.build}</Copiable>
</tr>
<tr>
<th>{_('hostIscsiName')}</th>
<Copiable tagName='td'>{host.iSCSI_name}</Copiable>
</tr>
</tbody>
</table>
<br />
<h3>{_('hardwareHostSettingsLabel')}</h3>
<table className='table'>
<tbody>
<tr>
<th>{_('hostCpusModel')}</th>
<Copiable tagName='td'>{host.CPUs.modelname}</Copiable>
</tr>
<tr>
<th>{_('hostGpus')}</th>
<td>
{map(pgpus, pgpu => pcis[pgpu.pci].device_name).join(', ')}
</td>
</tr>
<tr>
<th>{_('hostCpusNumber')}</th>
<td>
{host.cpus.cores} ({host.cpus.sockets})
</td>
</tr>
<tr>
<th>{_('hostManufacturerinfo')}</th>
<Copiable tagName='td'>
{host.bios_strings['system-manufacturer']} ({
host.bios_strings['system-product-name']
})
</Copiable>
</tr>
<tr>
<th>{_('hostBiosinfo')}</th>
<td>
{host.bios_strings['bios-vendor']} ({
host.bios_strings['bios-version']
})
</td>
</tr>
</tbody>
</table>
<br />
<h3>{_('licenseHostSettingsLabel')}</h3>
<table className='table'>
<tbody>
<tr>
<th>{_('hostLicenseType')}</th>
<td>{host.license_params.sku_type}</td>
</tr>
<tr>
<th>{_('hostLicenseSocket')}</th>
<td>{host.license_params.sockets}</td>
</tr>
<tr>
<th>{_('hostLicenseExpiry')}</th>
<td>
<FormattedTime
value={host.license_expiry * 1000}
day='numeric'
month='long'
year='numeric'
/>
<br />
</td>
</tr>
</tbody>
</table>
<h3>{_('supplementalPacks')}</h3>
<table className='table'>
<tbody>
{map(host.supplementalPacks, formatPack)}
{ALLOW_INSTALL_SUPP_PACK && (
<tr>
<th>{_('supplementalPackNew')}</th>
<td>
<SelectFiles
type='file'
onChange={file => installSupplementalPack(host, file)}
/>
</td>
</tr>
})
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'>
{host.power_state === 'Running' && (
<TabButton
btnStyle='warning'
handler={forceReboot}
handlerParam={host}
icon='host-force-reboot'
labelId='forceRebootHostLabel'
/>
)}
</tbody>
</table>
{!ALLOW_INSTALL_SUPP_PACK && [
<h3>{_('supplementalPackNew')}</h3>,
<Container>
<Upgrade place='supplementalPacks' available={2} />
</Container>,
]}
</Col>
</Row>
</Container>
))
{host.enabled ? (
<TabButton
btnStyle='warning'
handler={disableHost}
handlerParam={host}
icon='host-disable'
labelId='disableHostLabel'
/>
) : (
<TabButton
btnStyle='success'
handler={enableHost}
handlerParam={host}
icon='host-enable'
labelId='enableHostLabel'
/>
)}
<TabButton
btnStyle='danger'
handler={detachHost}
handlerParam={host}
icon='host-eject'
labelId='detachHost'
/>
{host.power_state !== 'Running' && (
<TabButton
btnStyle='danger'
handler={forgetHost}
handlerParam={host}
icon='host-forget'
labelId='forgetHost'
/>
)}
</Col>
</Row>
<Row>
<Col>
<h3>{_('xenSettingsLabel')}</h3>
<table className='table'>
<tbody>
<tr>
<th>{_('uuid')}</th>
<Copiable tagName='td'>{host.uuid}</Copiable>
</tr>
<tr>
<th>{_('hostAddress')}</th>
<Copiable tagName='td'>{host.address}</Copiable>
</tr>
<tr>
<th>{_('hostStatus')}</th>
<td>
{host.enabled
? _('hostStatusEnabled')
: _('hostStatusDisabled')}
</td>
</tr>
<tr>
<th>{_('hostPowerOnMode')}</th>
<td>
<Toggle
disabled
onChange={noop}
value={Boolean(host.powerOnMode)}
/>
</td>
</tr>
<tr>
<th>{_('hostStartedSince')}</th>
<td>
{_('started', {
ago: <FormattedRelative value={host.startTime * 1000} />,
})}
</td>
</tr>
<tr>
<th>{_('hostStackStartedSince')}</th>
<td>
{_('started', {
ago: (
<FormattedRelative value={host.agentStartTime * 1000} />
),
})}
</td>
</tr>
<tr>
<th>{_('hostXenServerVersion')}</th>
<Copiable tagName='td' data={host.version}>
{host.license_params.sku_marketing_name} {host.version} ({
host.license_params.sku_type
})
</Copiable>
</tr>
<tr>
<th>{_('hostBuildNumber')}</th>
<Copiable tagName='td'>{host.build}</Copiable>
</tr>
<tr>
<th>{_('hostIscsiName')}</th>
<Copiable tagName='td'>{host.iSCSI_name}</Copiable>
</tr>
</tbody>
</table>
<br />
<h3>{_('hardwareHostSettingsLabel')}</h3>
<table className='table'>
<tbody>
<tr>
<th>{_('hostCpusModel')}</th>
<Copiable tagName='td'>{host.CPUs.modelname}</Copiable>
</tr>
<tr>
<th>{_('hostGpus')}</th>
<td>
{map(pgpus, pgpu => pcis[pgpu.pci].device_name).join(', ')}
</td>
</tr>
<tr>
<th>{_('hostCpusNumber')}</th>
<td>
{host.cpus.cores} ({host.cpus.sockets})
</td>
</tr>
<tr>
<th>{_('hostManufacturerinfo')}</th>
<Copiable tagName='td'>
{host.bios_strings['system-manufacturer']} ({
host.bios_strings['system-product-name']
})
</Copiable>
</tr>
<tr>
<th>{_('hostBiosinfo')}</th>
<td>
{host.bios_strings['bios-vendor']} ({
host.bios_strings['bios-version']
})
</td>
</tr>
</tbody>
</table>
<br />
<h3>{_('licenseHostSettingsLabel')}</h3>
<table className='table'>
<tbody>
<tr>
<th>{_('hostLicenseType')}</th>
<td>{host.license_params.sku_type}</td>
</tr>
<tr>
<th>{_('hostLicenseSocket')}</th>
<td>{host.license_params.sockets}</td>
</tr>
<tr>
<th>{_('hostLicenseExpiry')}</th>
<td>
<FormattedTime
value={host.license_expiry * 1000}
day='numeric'
month='long'
year='numeric'
/>
<br />
</td>
</tr>
</tbody>
</table>
<h3>{_('supplementalPacks')}</h3>
<table className='table'>
<tbody>
{map(this._getPacks(), formatPack)}
{ALLOW_INSTALL_SUPP_PACK && (
<tr>
<th>{_('supplementalPackNew')}</th>
<td>
<SelectFiles
type='file'
onChange={file => installSupplementalPack(host, file)}
/>
</td>
</tr>
)}
</tbody>
</table>
{!ALLOW_INSTALL_SUPP_PACK && [
<h3>{_('supplementalPackNew')}</h3>,
<Container>
<Upgrade place='supplementalPacks' available={2} />
</Container>,
]}
</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,261 +338,273 @@ export default class NewXosan extends Component {
</Col>
</Row>
{pool != null &&
noPacksByPool[pool.id] && (
(checkPackError ? (
<em>{_('xosanPackUpdateError')}</em>
) : needsUpdate ? (
<Row>
<Icon icon='error' /> {_('xosanNeedPack')}
<br />
<ActionButton
btnStyle='success'
handler={downloadAndInstallXosanPack}
handlerParam={pool}
icon='export'
>
{_('xosanInstallIt')}
</ActionButton>
<Col>
<Icon icon='error' /> {_('xosanNeedPack')}
<br />
<ActionButton
btnStyle='success'
handler={this._updateXosanPacks}
handlerParam={pool}
icon='export'
>
{_('xosanInstallIt')}
</ActionButton>
</Col>
</Row>
)}
{!isEmpty(hostsNeedRestart) && (
<Row>
<Icon icon='error' /> {_('xosanNeedRestart')}
<br />
<ActionButton
btnStyle='success'
handler={restartHostsAgents}
handlerParam={hostsNeedRestart}
icon='host-restart-agent'
>
{_('xosanRestartAgents')}
</ActionButton>
</Row>
)}
{pool != null &&
!noPacksByPool[pool.id] &&
isEmpty(hostsNeedRestart) && [
) : !isEmpty(hostsNeedRestart) ? (
<Row>
<em>{_('xosanSelect2Srs')}</em>
<table className='table table-striped'>
<thead>
<tr>
<th />
<th>{_('xosanName')}</th>
<th>{_('xosanHost')}</th>
<th>{_('xosanSize')}</th>
<th>{_('xosanUsedSpace')}</th>
</tr>
</thead>
<tbody>
{map(lvmsrs, sr => {
const host = find(hosts, ['id', sr.$container])
return (
<tr key={sr.id}>
<td>
<input
checked={selectedSrs[sr.id] || false}
disabled={disableSrCheckbox(sr)}
onChange={event => this._selectSr(event, sr)}
type='checkbox'
/>
</td>
<td>
<Link to={`/srs/${sr.id}/general`}>
{sr.name_label}
</Link>
</td>
<td>
<Link to={`/hosts/${host.id}/general`}>
{host.name_label}
</Link>
</td>
<td>{formatSize(sr.size)}</td>
<td>
{sr.size > 0 && (
<Tooltip
content={_('spaceLeftTooltip', {
used: String(
Math.round(sr.physical_usage / sr.size * 100)
),
free: formatSize(sr.size - sr.physical_usage),
})}
>
<progress
className='progress'
max='100'
value={sr.physical_usage / sr.size * 100}
/>
</Tooltip>
)}
</td>
</tr>
)
})}
</tbody>
</table>
</Row>,
<Row>
{!isEmpty(suggestions) && (
<div>
<h3>{_('xosanSuggestions')}</h3>
<Col>
<Icon icon='error' /> {_('xosanNeedRestart')}
<br />
<ActionButton
btnStyle='success'
handler={restartHostsAgents}
handlerParam={hostsNeedRestart}
icon='host-restart-agent'
>
{_('xosanRestartAgents')}
</ActionButton>
</Col>
</Row>
) : (
[
<Row>
<Col>
<em>{_('xosanSelect2Srs')}</em>
<table className='table table-striped'>
<thead>
<tr>
<th />
<th>{_('xosanLayout')}</th>
<th>{_('xosanRedundancy')}</th>
<th>{_('xosanCapacity')}</th>
<th>{_('xosanAvailableSpace')}</th>
<th>{_('xosanName')}</th>
<th>{_('xosanHost')}</th>
<th>{_('xosanSize')}</th>
<th>{_('xosanUsedSpace')}</th>
</tr>
</thead>
<tbody>
{map(
suggestions,
(
{ layout, redundancy, capacity, availableSpace },
index
) => (
<tr key={index}>
{map(lvmsrs, sr => {
const host = find(hosts, ['id', sr.$container])
return (
<tr key={sr.id}>
<td>
<input
checked={+suggestion === index}
name={`suggestion_${pool.id}`}
onChange={this.linkState('suggestion')}
type='radio'
value={index}
checked={selectedSrs[sr.id] || false}
disabled={disableSrCheckbox(sr)}
onChange={event => this._selectSr(event, sr)}
type='checkbox'
/>
</td>
<td>{layout}</td>
<td>{redundancy}</td>
<td>{capacity}</td>
<td>
{availableSpace === 0 ? (
<strong className='text-danger'>0</strong>
) : (
formatSize(availableSpace)
<Link to={`/srs/${sr.id}/general`}>
{sr.name_label}
</Link>
</td>
<td>
<Link to={`/hosts/${host.id}/general`}>
{host.name_label}
</Link>
</td>
<td>{formatSize(sr.size)}</td>
<td>
{sr.size > 0 && (
<Tooltip
content={_('spaceLeftTooltip', {
used: String(
Math.round(
sr.physical_usage / sr.size * 100
)
),
free: formatSize(
sr.size - sr.physical_usage
),
})}
>
<progress
className='progress'
max='100'
value={sr.physical_usage / sr.size * 100}
/>
</Tooltip>
)}
</td>
</tr>
)
)}
})}
</tbody>
</table>
{architecture.layout === 'disperse' && (
<div className='alert alert-danger'>
{_('xosanDisperseWarning', {
link: (
<a href='https://xen-orchestra.com/docs/xosan_types.html'>
xen-orchestra.com/docs/xosan_types.html
</a>
),
})}
</Col>
</Row>,
<Row>
<Col>
{!isEmpty(suggestions) && (
<div>
<h3>{_('xosanSuggestions')}</h3>
<table className='table table-striped'>
<thead>
<tr>
<th />
<th>{_('xosanLayout')}</th>
<th>{_('xosanRedundancy')}</th>
<th>{_('xosanCapacity')}</th>
<th>{_('xosanAvailableSpace')}</th>
</tr>
</thead>
<tbody>
{map(
suggestions,
(
{ layout, redundancy, capacity, availableSpace },
index
) => (
<tr key={index}>
<td>
<input
checked={+suggestion === index}
name={`suggestion_${pool.id}`}
onChange={this.linkState('suggestion')}
type='radio'
value={index}
/>
</td>
<td>{layout}</td>
<td>{redundancy}</td>
<td>{capacity}</td>
<td>
{availableSpace === 0 ? (
<strong className='text-danger'>0</strong>
) : (
formatSize(availableSpace)
)}
</td>
</tr>
)
)}
</tbody>
</table>
{architecture.layout === 'disperse' && (
<div className='alert alert-danger'>
{_('xosanDisperseWarning', {
link: (
<a href='https://xen-orchestra.com/docs/xosan_types.html'>
xen-orchestra.com/docs/xosan_types.html
</a>
),
})}
</div>
)}
<Graph
height={160}
layout={architecture.layout}
nSrs={this._getNSelectedSrs()}
redundancy={architecture.redundancy}
width={600}
/>
<hr />
<Toggle
onChange={this.toggleState('showAdvanced')}
value={this.state.showAdvanced}
/>{' '}
{_('xosanAdvanced')}{' '}
{this.state.showAdvanced && (
<Container className='mb-1'>
<SingleLineRow>
<Col>{_('xosanVlan')}</Col>
</SingleLineRow>
<SingleLineRow>
<Col size={1}>
<Toggle
onChange={this.linkState('useVlan')}
value={useVlan}
/>
</Col>
<Col size={3}>
<input
className='form-control'
disabled={!useVlan}
onChange={this.linkState('vlan')}
placeholder='VLAN'
type='text'
value={vlan}
/>
</Col>
</SingleLineRow>
<SingleLineRow>
<Col>{_('xosanCustomIpNetwork')}</Col>
</SingleLineRow>
<SingleLineRow>
<Col size={1}>
<Toggle
onChange={this.linkState('customIpRange')}
value={customIpRange}
/>
</Col>
<Col size={3}>
<input
className='form-control'
disabled={!customIpRange}
onChange={this.linkState('ipRange')}
placeholder='ipRange'
type='text'
value={ipRange}
/>
</Col>
</SingleLineRow>
<SingleLineRow>
<Col>{_('xosanBrickSize')}</Col>
</SingleLineRow>
<SingleLineRow>
<Col size={1}>
<Toggle
className='mr-1'
onChange={this._onCustomBrickSizeChange}
value={customBrickSize}
/>
</Col>
<Col size={3}>
<SizeInput
readOnly={!customBrickSize}
value={brickSize}
onChange={this._onBrickSizeChange}
required
/>
</Col>
</SingleLineRow>
<SingleLineRow>
<Col size={4}>
<label>{_('xosanMemorySize')}</label>
<SizeInput
value={memorySize}
onChange={this.linkState('memorySize')}
required
/>
</Col>
</SingleLineRow>
</Container>
)}
<hr />
</div>
)}
<Graph
height={160}
layout={architecture.layout}
nSrs={this._getNSelectedSrs()}
redundancy={architecture.redundancy}
width={600}
/>
<hr />
<Toggle
onChange={this.toggleState('showAdvanced')}
value={this.state.showAdvanced}
/>{' '}
{_('xosanAdvanced')}{' '}
{this.state.showAdvanced && (
<Container className='mb-1'>
<SingleLineRow>
<Col>{_('xosanVlan')}</Col>
</SingleLineRow>
<SingleLineRow>
<Col size={1}>
<Toggle
onChange={this.linkState('useVlan')}
value={useVlan}
/>
</Col>
<Col size={3}>
<input
className='form-control'
disabled={!useVlan}
onChange={this.linkState('vlan')}
placeholder='VLAN'
type='text'
value={vlan}
/>
</Col>
</SingleLineRow>
<SingleLineRow>
<Col>{_('xosanCustomIpNetwork')}</Col>
</SingleLineRow>
<SingleLineRow>
<Col size={1}>
<Toggle
onChange={this.linkState('customIpRange')}
value={customIpRange}
/>
</Col>
<Col size={3}>
<input
className='form-control'
disabled={!customIpRange}
onChange={this.linkState('ipRange')}
placeholder='ipRange'
type='text'
value={ipRange}
/>
</Col>
</SingleLineRow>
<SingleLineRow>
<Col>{_('xosanBrickSize')}</Col>
</SingleLineRow>
<SingleLineRow>
<Col size={1}>
<Toggle
className='mr-1'
onChange={this._onCustomBrickSizeChange}
value={customBrickSize}
/>
</Col>
<Col size={3}>
<SizeInput
readOnly={!customBrickSize}
value={brickSize}
onChange={this._onBrickSizeChange}
required
/>
</Col>
</SingleLineRow>
<SingleLineRow>
<Col size={4}>
<label>{_('xosanMemorySize')}</label>
<SizeInput
value={memorySize}
onChange={this.linkState('memorySize')}
required
/>
</Col>
</SingleLineRow>
</Container>
)}
<hr />
</div>
)}
</Row>,
<Row>
<Col>
<ActionButton
btnStyle='success'
disabled={this._getDisableCreation()}
handler={this._createXosanVm}
icon='add'
>
{_('xosanCreate')}
</ActionButton>
</Col>
</Row>,
]}
</Col>
</Row>,
<Row>
<Col>
<ActionButton
btnStyle='success'
disabled={this._getDisableCreation()}
handler={this._createXosanVm}
icon='add'
>
{_('xosanCreate')}
</ActionButton>
</Col>
</Row>,
]
))}
<hr />
</Container>
)