feat(xo-web/pool): XCP-ng license binding (#6453)
This commit is contained in:
parent
afd47f5522
commit
1269ddfeae
@ -8,6 +8,7 @@
|
||||
> Users must be able to say: “Nice enhancement, I'm eager to test it”
|
||||
|
||||
- [Delta Backup] Use [NBD](https://en.wikipedia.org/wiki/Network_block_device) to download disks (PR [#6461](https://github.com/vatesfr/xen-orchestra/pull/6461))
|
||||
- [License] Possibility to bind XCP-ng license to hosts at pool level (PR [#6453](https://github.com/vatesfr/xen-orchestra/pull/6453))
|
||||
|
||||
### Bug fixes
|
||||
|
||||
|
@ -811,6 +811,9 @@ const messages = {
|
||||
noActiveVdi: 'No active VDI',
|
||||
|
||||
// ----- Pool general -----
|
||||
earliestExpirationDate: 'Earliest expiration: {dateString}',
|
||||
poolPartialSupport:
|
||||
'Only {nHostsLicense, number} host{nHostsLicense, plural, one {} other {s}} under license on {nHosts, number} host{nHosts, plural, one {} other {s}}. This means this pool is not supported at all until you license all its hosts.',
|
||||
poolTitleRamUsage: 'Pool RAM usage:',
|
||||
poolRamUsage: '{used} used of {total} ({free} free)',
|
||||
poolMaster: 'Master:',
|
||||
@ -835,6 +838,9 @@ const messages = {
|
||||
poolHaDisabled: 'Disabled',
|
||||
poolGpuGroups: 'GPU groups',
|
||||
poolRemoteSyslogPlaceHolder: 'Logging host',
|
||||
poolSupportSourceUsers: 'Pool support not available for source users',
|
||||
poolSupportXcpngOnly: 'Only available for pool of XCP-ng hosts',
|
||||
poolLicenseAlreadyFullySupported: 'The pool is already fully supported',
|
||||
setpoolMaster: 'Master',
|
||||
syslogRemoteHost: 'Remote syslog host',
|
||||
defaultMigrationNetwork: 'Default migration network',
|
||||
@ -2400,6 +2406,16 @@ const messages = {
|
||||
auditInactiveUserActionsRecord: 'User actions recording is currently inactive',
|
||||
|
||||
// Licenses
|
||||
allHostsMustBeBound: 'All hosts must be bound to a license',
|
||||
bound: 'Bound',
|
||||
bindXcpngLicenses: 'Bind XCP-ng licenses',
|
||||
confirmBindingOnUnsupportedHost:
|
||||
'You are about to bind {nLicenses, number} professional support license{nLicenses, plural, one {} other {s}} on older and unsupported XCP-ng version{nLicenses, plural, one {} other {s}}. Are you sure you want to continue?',
|
||||
confirmRebindLicenseFromFullySupportedPool: 'The following pools will no longer be fully supported',
|
||||
licenses: 'Licenses',
|
||||
licensesBinding: 'Licenses binding',
|
||||
notEnoughXcpngLicenses: 'Not enough XCP-ng licenses',
|
||||
notBound: 'Not bound',
|
||||
xosanUnregisteredDisclaimer:
|
||||
'You are not registered and therefore will not be able to create or manage your XOSAN SRs. {link}',
|
||||
xosanSourcesDisclaimer:
|
||||
|
@ -6,16 +6,46 @@ import { injectIntl } from 'react-intl'
|
||||
import { injectState, provideState } from 'reaclette'
|
||||
import { map } from 'lodash'
|
||||
|
||||
import { renderXoItemFromId } from './render-xo-item'
|
||||
|
||||
const LicenseOptions = ({ license, formatTime }) =>
|
||||
_(
|
||||
'expiresOn',
|
||||
{
|
||||
date:
|
||||
license.expires !== undefined
|
||||
? formatTime(license.expires, {
|
||||
day: 'numeric',
|
||||
month: 'numeric',
|
||||
year: 'numeric',
|
||||
})
|
||||
: '',
|
||||
},
|
||||
expirationDate => (
|
||||
<option value={license.id}>
|
||||
<span>
|
||||
{license.id.slice(-4)} {expirationDate} {license.boundObjectId && renderXoItemFromId(license.boundObjectId)}
|
||||
</span>
|
||||
</option>
|
||||
)
|
||||
)
|
||||
|
||||
const SelectLicense = decorate([
|
||||
injectIntl,
|
||||
provideState({
|
||||
computed: {
|
||||
licenses: async (state, { productType }) => {
|
||||
try {
|
||||
return (await getLicenses({ productType }))?.filter(
|
||||
({ boundObjectId, expires }) =>
|
||||
boundObjectId === undefined && (expires === undefined || expires > Date.now())
|
||||
)
|
||||
const availableLicenses = {
|
||||
bound: [],
|
||||
notBound: [],
|
||||
}
|
||||
;(await getLicenses({ productType })).forEach(license => {
|
||||
if (license.expires === undefined || license.expires > Date.now()) {
|
||||
availableLicenses[license.boundObjectId === undefined ? 'notBound' : 'bound'].push(license)
|
||||
}
|
||||
})
|
||||
return availableLicenses
|
||||
} catch (error) {
|
||||
return { licenseError: error }
|
||||
}
|
||||
@ -23,7 +53,7 @@ const SelectLicense = decorate([
|
||||
},
|
||||
}),
|
||||
injectState,
|
||||
({ state: { licenses }, intl: { formatTime }, onChange }) =>
|
||||
({ state: { licenses }, intl: { formatTime }, onChange, showBoundLicenses }) =>
|
||||
licenses?.licenseError !== undefined ? (
|
||||
<span>
|
||||
<em className='text-danger'>{_('getLicensesError')}</em>
|
||||
@ -35,26 +65,22 @@ const SelectLicense = decorate([
|
||||
{message}
|
||||
</option>
|
||||
))}
|
||||
{map(licenses, license =>
|
||||
_(
|
||||
'expiresOn',
|
||||
{
|
||||
date:
|
||||
license.expires !== undefined
|
||||
? formatTime(license.expires, {
|
||||
day: 'numeric',
|
||||
month: 'numeric',
|
||||
year: 'numeric',
|
||||
})
|
||||
: '',
|
||||
},
|
||||
message => (
|
||||
<option key={license.id} value={license.id}>
|
||||
{license.id.slice(-4)} {license.expires ? `(${message})` : ''}
|
||||
</option>
|
||||
)
|
||||
)
|
||||
)}
|
||||
|
||||
{_('notBound', i18nNotBound => (
|
||||
<optgroup label={i18nNotBound}>
|
||||
{map(licenses?.notBound, license => (
|
||||
<LicenseOptions formatTime={formatTime} key={license.id} license={license} />
|
||||
))}
|
||||
</optgroup>
|
||||
))}
|
||||
{showBoundLicenses &&
|
||||
_('bound', i18nBound => (
|
||||
<optgroup label={i18nBound}>
|
||||
{map(licenses?.bound, license => (
|
||||
<LicenseOptions formatTime={formatTime} key={license.id} license={license} />
|
||||
))}
|
||||
</optgroup>
|
||||
))}
|
||||
</select>
|
||||
),
|
||||
])
|
||||
|
@ -146,6 +146,11 @@ export { default as Debug } from './debug'
|
||||
// -------------------------------------------------------------------
|
||||
|
||||
// Returns the current XOA Plan or the Plan name if number given
|
||||
/**
|
||||
* @deprecated
|
||||
*
|
||||
* Use `getXoaPlan` from `xoa-plans` instead
|
||||
*/
|
||||
export const getXoaPlan = plan => {
|
||||
switch (plan || +process.env.XOA_PLAN) {
|
||||
case 1:
|
||||
|
@ -31,6 +31,7 @@ import store from 'store'
|
||||
import { alert, chooseAction, confirm } from '../modal'
|
||||
import { error, info, success } from '../notification'
|
||||
import { getObject } from 'selectors'
|
||||
import { getXoaPlan, SOURCES } from '../xoa-plans'
|
||||
import { noop, resolveId, resolveIds } from '../utils'
|
||||
import {
|
||||
connected,
|
||||
@ -3236,6 +3237,16 @@ export const unlockXosan = (licenseId, srId) => _call('xosan.unlock', { licenseI
|
||||
|
||||
export const bindLicense = (licenseId, boundObjectId) => _call('xoa.licenses.bind', { licenseId, boundObjectId })
|
||||
|
||||
export const bindXcpngLicense = (licenseId, boundObjectId) =>
|
||||
bindLicense(licenseId, boundObjectId)::tap(subscribeXcpngLicenses.forceRefresh)
|
||||
|
||||
export const rebindLicense = (licenseType, licenseId, oldBoundObjectId, newBoundObjectId) =>
|
||||
_call('xoa.licenses.rebind', { licenseId, oldBoundObjectId, newBoundObjectId })::tap(() => {
|
||||
if (licenseType === 'xcpng-standard' || licenseType === 'xcpng-enterprise') {
|
||||
return subscribeXcpngLicenses.forceRefresh()
|
||||
}
|
||||
})
|
||||
|
||||
export const selfBindLicense = ({ id, plan, oldXoaId }) =>
|
||||
confirm({
|
||||
title: _('bindXoaLicense'),
|
||||
@ -3251,6 +3262,12 @@ export const selfBindLicense = ({ id, plan, oldXoaId }) =>
|
||||
|
||||
export const subscribeSelfLicenses = createSubscription(() => _call('xoa.licenses.getSelf'))
|
||||
|
||||
export const subscribeXcpngLicenses = createSubscription(() =>
|
||||
getXoaPlan() !== SOURCES && store.getState().user.permission === 'admin'
|
||||
? _call('xoa.licenses.getAll', { productType: 'xcpng' })
|
||||
: undefined
|
||||
)
|
||||
|
||||
// Support --------------------------------------------------------------------
|
||||
|
||||
export const clearXoaCheckCache = () => _call('xoa.clearCheckCache')
|
||||
|
@ -0,0 +1,29 @@
|
||||
import React from 'react'
|
||||
import SelectLicense from 'select-license'
|
||||
|
||||
import BaseComponent from '../../base-component'
|
||||
import { Host } from '../../render-xo-item'
|
||||
|
||||
export default class PoolBindLicenseModal extends BaseComponent {
|
||||
licenseByHost = {}
|
||||
|
||||
get value() {
|
||||
return this.licenseByHost
|
||||
}
|
||||
|
||||
onSelectLicense = hostId => event => (this.licenseByHost[hostId] = event.target.value)
|
||||
|
||||
render() {
|
||||
const { hosts } = this.props
|
||||
return (
|
||||
<div>
|
||||
{hosts.map(({ id }) => (
|
||||
<div key={id}>
|
||||
<Host id={id} link newTab />
|
||||
<SelectLicense productType='xcpng' showBoundLicenses onChange={this.onSelectLicense(id)} />
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
)
|
||||
}
|
||||
}
|
@ -1,4 +1,8 @@
|
||||
.xo-icon {
|
||||
&-pro-support {
|
||||
@extend .fa;
|
||||
@extend .fa-file-text;
|
||||
}
|
||||
&-pool {
|
||||
@extend .fa;
|
||||
@extend .fa-cloud;
|
||||
|
@ -13,9 +13,13 @@ 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 { injectState } from 'reaclette'
|
||||
|
||||
import styles from './index.css'
|
||||
|
||||
import { isAdmin } from '../../common/selectors'
|
||||
import { ShortDate } from '../../common/utils'
|
||||
|
||||
@connectStore(() => {
|
||||
const getPoolHosts = createGetObjectsOfType('host').filter(
|
||||
createSelector(
|
||||
@ -48,12 +52,14 @@ import styles from './index.css'
|
||||
|
||||
return {
|
||||
hostMetrics: getHostMetrics,
|
||||
isAdmin,
|
||||
missingPatches: getMissingPatches,
|
||||
poolHosts: getPoolHosts,
|
||||
nSrs: getNumberOfSrs,
|
||||
nVms: getNumberOfVms,
|
||||
}
|
||||
})
|
||||
@injectState
|
||||
export default class PoolItem extends Component {
|
||||
_addTag = tag => addTag(this.props.item.id, tag)
|
||||
_removeTag = tag => removeTag(this.props.item.id, tag)
|
||||
@ -66,9 +72,25 @@ export default class PoolItem extends Component {
|
||||
this.props.missingPatches.then(patches => this.setState({ missingPatchCount: size(patches) }))
|
||||
}
|
||||
|
||||
_getPoolLicenseIcon() {
|
||||
const { state: reacletteState, item: pool } = this.props
|
||||
let tooltip
|
||||
const { icon, earliestExpirationDate, nHostsUnderLicense, nHosts, supportLevel } =
|
||||
reacletteState.poolLicenseInfoByPoolId[pool.id]
|
||||
|
||||
if (supportLevel === 'total') {
|
||||
tooltip = _('earliestExpirationDate', { dateString: <ShortDate timestamp={earliestExpirationDate} /> })
|
||||
}
|
||||
if (supportLevel === 'partial') {
|
||||
tooltip = _('poolPartialSupport', { nHostsLicense: nHostsUnderLicense, nHosts })
|
||||
}
|
||||
return icon(tooltip)
|
||||
}
|
||||
|
||||
render() {
|
||||
const { item: pool, expandAll, selected, hostMetrics, poolHosts, nSrs, nVms } = this.props
|
||||
const { item: pool, expandAll, isAdmin, selected, hostMetrics, poolHosts, nSrs, nVms } = this.props
|
||||
const { missingPatchCount } = this.state
|
||||
|
||||
return (
|
||||
<div className={styles.item}>
|
||||
<BlockLink to={`/pools/${pool.id}`}>
|
||||
@ -80,6 +102,7 @@ export default class PoolItem extends Component {
|
||||
<Ellipsis>
|
||||
<Text value={pool.name_label} onChange={this._setNameLabel} useLongClick />
|
||||
</Ellipsis>
|
||||
{isAdmin && <span className='ml-1'>{this._getPoolLicenseIcon()}</span>}
|
||||
|
||||
{missingPatchCount > 0 && (
|
||||
<span>
|
||||
|
@ -9,9 +9,11 @@ import React from 'react'
|
||||
import Shortcuts from 'shortcuts'
|
||||
import themes from 'themes'
|
||||
import _, { IntlProvider } from 'intl'
|
||||
// TODO: Replace all `getXoaPlan` by `getXoaPlan` from "xoa-plans"
|
||||
import { addSubscriptions, connectStore, getXoaPlan, noop, routes } from 'utils'
|
||||
import { blockXoaAccess, isTrialRunning } from 'xoa-updater'
|
||||
import { checkXoa, clearXoaCheckCache } from 'xo'
|
||||
import { connectStore, getXoaPlan, noop, routes } from 'utils'
|
||||
import { forEach, groupBy, keyBy, pick } from 'lodash'
|
||||
import { Notification } from 'notification'
|
||||
import { productId2Plan } from 'xoa-plans'
|
||||
import { provideState } from 'reaclette'
|
||||
@ -48,6 +50,10 @@ import Xosan from './xosan'
|
||||
import Import from './import'
|
||||
|
||||
import keymap, { help } from '../keymap'
|
||||
import Tooltip from '../common/tooltip'
|
||||
import { createCollectionWrapper, createGetObjectsOfType } from '../common/selectors'
|
||||
import { bindXcpngLicense, rebindLicense, subscribeXcpngLicenses } from '../common/xo'
|
||||
import { SOURCES } from '../common/xoa-plans'
|
||||
|
||||
const shortcutManager = new ShortcutManager(keymap)
|
||||
|
||||
@ -76,6 +82,20 @@ const BODY_STYLE = {
|
||||
width: '100%',
|
||||
}
|
||||
|
||||
export const ICON_POOL_LICENSE = {
|
||||
total: tooltip => (
|
||||
<Tooltip content={tooltip}>
|
||||
<Icon icon='pro-support' className='text-success' />
|
||||
</Tooltip>
|
||||
),
|
||||
partial: tooltip => (
|
||||
<Tooltip content={tooltip}>
|
||||
<Icon icon='alarm' className='text-warning' />
|
||||
</Tooltip>
|
||||
),
|
||||
any: () => <Icon icon='alarm' className='text-danger' />,
|
||||
}
|
||||
|
||||
@routes('home', {
|
||||
about: About,
|
||||
backup: Backup,
|
||||
@ -101,11 +121,16 @@ const BODY_STYLE = {
|
||||
hub: Hub,
|
||||
proxies: Proxies,
|
||||
})
|
||||
@addSubscriptions({
|
||||
xcpLicenses: subscribeXcpngLicenses,
|
||||
})
|
||||
@connectStore(state => {
|
||||
const getHosts = createGetObjectsOfType('host')
|
||||
return {
|
||||
trial: state.xoaTrialState,
|
||||
registerNeeded: state.xoaUpdaterState === 'registerNeeded',
|
||||
signedUp: !!state.user,
|
||||
hosts: getHosts(state),
|
||||
}
|
||||
})
|
||||
@provideState({
|
||||
@ -118,8 +143,71 @@ const BODY_STYLE = {
|
||||
refreshXoaStatus() {
|
||||
this.state.checkXoaCount += 1
|
||||
},
|
||||
async bindXcpngLicenses(_, xcpngLicensesByHost) {
|
||||
await Promise.all(
|
||||
map(xcpngLicensesByHost, ({ productId, id, boundObjectId }, hostId) =>
|
||||
boundObjectId !== undefined
|
||||
? rebindLicense(productId, id, boundObjectId, hostId)
|
||||
: bindXcpngLicense(id, hostId)
|
||||
)
|
||||
)
|
||||
},
|
||||
},
|
||||
computed: {
|
||||
// In case an host have more than 1 license, it's an issue.
|
||||
// poolLicenseInfoByPoolId can be impacted because the license expiration check may not yield the right information.
|
||||
xcpngLicenseByBoundObjectId: (_, { xcpLicenses }) => keyBy(xcpLicenses, 'boundObjectId'),
|
||||
xcpngLicenseById: (_, { xcpLicenses }) => keyBy(xcpLicenses, 'id'),
|
||||
hostsByPoolId: createCollectionWrapper((_, { hosts }) =>
|
||||
groupBy(
|
||||
map(hosts, host => pick(host, ['$poolId', 'id'])),
|
||||
'$poolId'
|
||||
)
|
||||
),
|
||||
poolLicenseInfoByPoolId: ({ hostsByPoolId, xcpngLicenseByBoundObjectId }) => {
|
||||
const poolLicenseInfoByPoolId = {}
|
||||
|
||||
forEach(hostsByPoolId, (hosts, poolId) => {
|
||||
const nHosts = hosts.length
|
||||
let earliestExpirationDate
|
||||
let nHostsUnderLicense = 0
|
||||
|
||||
if (getXoaPlan() === SOURCES.name) {
|
||||
poolLicenseInfoByPoolId[poolId] = {
|
||||
nHostsUnderLicense,
|
||||
icon: () => (
|
||||
<Tooltip content={_('poolSupportSourceUsers')}>
|
||||
<Icon icon='unknown-status' className='text-danger' />
|
||||
</Tooltip>
|
||||
),
|
||||
nHosts,
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
for (const host of hosts) {
|
||||
const license = xcpngLicenseByBoundObjectId[host.id]
|
||||
if (license !== undefined && license.expires > Date.now()) {
|
||||
nHostsUnderLicense++
|
||||
if (earliestExpirationDate === undefined || license.expires < earliestExpirationDate) {
|
||||
earliestExpirationDate = license.expires
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
const supportLevel = nHostsUnderLicense === 0 ? 'any' : nHostsUnderLicense === nHosts ? 'total' : 'partial'
|
||||
|
||||
poolLicenseInfoByPoolId[poolId] = {
|
||||
earliestExpirationDate,
|
||||
icon: ICON_POOL_LICENSE[supportLevel],
|
||||
nHosts,
|
||||
nHostsUnderLicense,
|
||||
supportLevel,
|
||||
}
|
||||
})
|
||||
|
||||
return poolLicenseInfoByPoolId
|
||||
},
|
||||
xoaStatus: {
|
||||
get({ checkXoaCount }) {
|
||||
// To avoid aggressive minification which would remove destructuration
|
||||
|
@ -13,7 +13,7 @@ import { addSubscriptions, connectStore } from 'utils'
|
||||
import { Container, Row, Col } from 'grid'
|
||||
import { CustomFields } from 'custom-fields'
|
||||
import { injectIntl } from 'react-intl'
|
||||
import { map } from 'lodash'
|
||||
import { forEach, map, values } from 'lodash'
|
||||
import { Text, XoSelect } from 'editable'
|
||||
import {
|
||||
createGetObject,
|
||||
@ -29,10 +29,142 @@ import {
|
||||
setPoolMaster,
|
||||
setRemoteSyslogHost,
|
||||
setRemoteSyslogHosts,
|
||||
subscribeHvSupportedVersions,
|
||||
subscribePlugins,
|
||||
subscribeXcpngLicenses,
|
||||
synchronizeNetbox,
|
||||
} from 'xo'
|
||||
import { injectState, provideState } from 'reaclette'
|
||||
import { SelectSuspendSr } from 'select-suspend-sr'
|
||||
import { satisfies } from 'semver'
|
||||
|
||||
import decorate from '../../common/apply-decorators'
|
||||
import PoolBindLicenseModal from '../../common/xo/pool-bind-licenses-modal/ index'
|
||||
import { confirm } from '../../common/modal'
|
||||
import { error } from '../../common/notification'
|
||||
import { Host, Pool } from '../../common/render-xo-item'
|
||||
import { isAdmin } from '../../common/selectors'
|
||||
import { SOURCES, getXoaPlan } from '../../common/xoa-plans'
|
||||
|
||||
const BindLicensesButton = decorate([
|
||||
addSubscriptions({
|
||||
hvSupportedVersions: subscribeHvSupportedVersions,
|
||||
xcpLicenses: subscribeXcpngLicenses,
|
||||
}),
|
||||
connectStore({
|
||||
hosts: createGetObjectsOfType('host'),
|
||||
}),
|
||||
provideState({
|
||||
effects: {
|
||||
async handleBindLicense() {
|
||||
const { poolHosts, xcpLicenses } = this.props
|
||||
|
||||
if (xcpLicenses.length < poolHosts.length) {
|
||||
return error(_('licensesBinding'), _('notEnoughXcpngLicenses'))
|
||||
}
|
||||
|
||||
const hostsWithoutLicense = poolHosts.filter(
|
||||
host => this.state.xcpngLicenseByBoundObjectId[host.id] === undefined
|
||||
)
|
||||
const licenseIdByHost = await confirm({
|
||||
body: <PoolBindLicenseModal hosts={hostsWithoutLicense} />,
|
||||
icon: 'connect',
|
||||
title: _('licensesBinding'),
|
||||
})
|
||||
const licensesByHost = {}
|
||||
|
||||
// Pass values into a Set in order to remove duplicated licenseId
|
||||
const nLicensesToBind = new Set(values(licenseIdByHost)).size
|
||||
|
||||
if (nLicensesToBind !== hostsWithoutLicense.length) {
|
||||
return error(_('licensesBinding'), _('allHostsMustBeBound'))
|
||||
}
|
||||
|
||||
const fullySupportedPoolIds = []
|
||||
const unsupportedXcpngHostIds = []
|
||||
forEach(licenseIdByHost, (licenseId, hostId) => {
|
||||
const license = this.state.xcpngLicenseById[licenseId]
|
||||
const boundHost = this.props.hosts[license.boundObjectId]
|
||||
const hostToBind = this.props.hosts[hostId]
|
||||
const poolId = boundHost?.$pool
|
||||
const poolLicenseInfo = this.state.poolLicenseInfoByPoolId[poolId]
|
||||
licensesByHost[hostId] = license
|
||||
|
||||
if (poolLicenseInfo !== undefined && poolLicenseInfo.supportLevel === 'total' && poolLicenseInfo.nHosts > 1) {
|
||||
fullySupportedPoolIds.push(poolId)
|
||||
}
|
||||
|
||||
if (!satisfies(hostToBind.version, this.props.hvSupportedVersions['XCP-ng'])) {
|
||||
unsupportedXcpngHostIds.push(hostToBind.id)
|
||||
}
|
||||
})
|
||||
|
||||
if (fullySupportedPoolIds.length !== 0) {
|
||||
await confirm({
|
||||
body: (
|
||||
<div>
|
||||
<p>{_('confirmRebindLicenseFromFullySupportedPool')}</p>
|
||||
<ul>
|
||||
{fullySupportedPoolIds.map(poolId => (
|
||||
<li key={poolId}>
|
||||
<Pool id={poolId} link newTab />
|
||||
</li>
|
||||
))}
|
||||
</ul>
|
||||
</div>
|
||||
),
|
||||
title: _('licensesBinding'),
|
||||
})
|
||||
}
|
||||
|
||||
if (unsupportedXcpngHostIds.length !== 0) {
|
||||
await confirm({
|
||||
body: (
|
||||
<div>
|
||||
<p>{_('confirmBindingOnUnsupportedHost', { nLicenses: unsupportedXcpngHostIds.length })}</p>
|
||||
<ul>
|
||||
{unsupportedXcpngHostIds.map(hostId => (
|
||||
<li key={hostId}>
|
||||
<Host id={hostId} link newTab />
|
||||
</li>
|
||||
))}
|
||||
</ul>
|
||||
</div>
|
||||
),
|
||||
title: _('licensesBinding'),
|
||||
})
|
||||
}
|
||||
|
||||
await this.effects.bindXcpngLicenses(licensesByHost)
|
||||
},
|
||||
},
|
||||
computed: {
|
||||
isBindLicenseAvailable: (state, props) =>
|
||||
getXoaPlan() !== SOURCES && state.poolLicenseInfoByPoolId[props.pool.id].supportLevel !== 'total',
|
||||
isXcpngPool: (_, { poolHosts }) => poolHosts[0].productBrand === 'XCP-ng',
|
||||
},
|
||||
}),
|
||||
injectState,
|
||||
({ effects, state }) => (
|
||||
<ActionButton
|
||||
btnStyle='primary'
|
||||
disabled={!state.isXcpngPool || !state.isBindLicenseAvailable}
|
||||
handler={effects.handleBindLicense}
|
||||
icon='connect'
|
||||
tooltip={
|
||||
getXoaPlan() === SOURCES
|
||||
? _('poolSupportSourceUsers')
|
||||
: !state.isXcpngPool
|
||||
? _('poolSupportXcpngOnly')
|
||||
: state.isBindLicenseAvailable
|
||||
? undefined
|
||||
: _('poolLicenseAlreadyFullySupported')
|
||||
}
|
||||
>
|
||||
{_('bindXcpngLicenses')}
|
||||
</ActionButton>
|
||||
),
|
||||
])
|
||||
|
||||
@connectStore(() => ({
|
||||
master: createGetObjectsOfType('host').find((_, { pool }) => ({
|
||||
@ -72,6 +204,7 @@ class PoolMaster extends Component {
|
||||
gpuGroups: createGetObjectsOfType('gpuGroup')
|
||||
.filter((_, { pool }) => ({ $pool: pool.id }))
|
||||
.sort(),
|
||||
isAdmin,
|
||||
migrationNetwork: createGetObject((_, { pool }) => pool.otherConfig['xo:migrationNetwork']),
|
||||
}
|
||||
})
|
||||
@ -116,7 +249,7 @@ export default class TabAdvanced extends Component {
|
||||
)
|
||||
|
||||
render() {
|
||||
const { backupNetwork, hosts, gpuGroups, pool, hostsByMultipathing, migrationNetwork } = this.props
|
||||
const { backupNetwork, hosts, isAdmin, gpuGroups, pool, hostsByMultipathing, migrationNetwork } = this.props
|
||||
const { state } = this
|
||||
const { editRemoteSyslog } = state
|
||||
const { enabled: hostsEnabledMultipathing, disabled: hostsDisabledMultipathing } = hostsByMultipathing
|
||||
@ -214,6 +347,12 @@ export default class TabAdvanced extends Component {
|
||||
</Col>
|
||||
</Row>
|
||||
</Container>
|
||||
{isAdmin && (
|
||||
<div>
|
||||
<h3>{_('licenses')}</h3>
|
||||
<BindLicensesButton poolHosts={hosts} pool={pool} />
|
||||
</div>
|
||||
)}
|
||||
<h3 className='mt-1 mb-1'>{_('poolGpuGroups')}</h3>
|
||||
<Container>
|
||||
<Row>
|
||||
|
@ -171,7 +171,7 @@ export default class Licenses extends Component {
|
||||
|
||||
return getLicenses()
|
||||
.then(licenses => {
|
||||
const { proxy, xoa, xosan } = groupBy(licenses, license => {
|
||||
const { proxy, xcpng, xoa, xosan } = groupBy(licenses, license => {
|
||||
for (const productType of license.productTypes) {
|
||||
if (productType === 'xo') {
|
||||
return 'xoa'
|
||||
@ -182,12 +182,16 @@ export default class Licenses extends Component {
|
||||
if (productType === 'xoproxy') {
|
||||
return 'proxy'
|
||||
}
|
||||
if (productType === 'xcpng') {
|
||||
return 'xcpng'
|
||||
}
|
||||
}
|
||||
return 'other'
|
||||
})
|
||||
this.setState({
|
||||
licenses: {
|
||||
proxy,
|
||||
xcpng,
|
||||
xoa,
|
||||
xosan,
|
||||
},
|
||||
@ -256,6 +260,21 @@ export default class Licenses extends Component {
|
||||
}
|
||||
})
|
||||
|
||||
// --- xcpng
|
||||
forEach(licenses.xcpng, license => {
|
||||
// When `expires` is undefined, the license isn't expired
|
||||
if (!(license.expires < now)) {
|
||||
products.push({
|
||||
buyer: license.buyer,
|
||||
expires: license.expires,
|
||||
id: license.id,
|
||||
product: 'XCP-ng',
|
||||
type: 'xcpng',
|
||||
hostId: license.boundObjectId,
|
||||
})
|
||||
}
|
||||
})
|
||||
|
||||
return products
|
||||
}
|
||||
)
|
||||
|
Loading…
Reference in New Issue
Block a user