diff --git a/CHANGELOG.unreleased.md b/CHANGELOG.unreleased.md index e717b20b3b8..a24dd21260b 100644 --- a/CHANGELOG.unreleased.md +++ b/CHANGELOG.unreleased.md @@ -9,6 +9,7 @@ - [i18n] Japanese translation (PR [#7582](https://github.com/vatesfr/xen-orchestra/pull/7582)) - [REST API] [Watch mode for the tasks collection](./packages/xo-server/docs/rest-api.md#all-tasks) (PR [#7565](https://github.com/vatesfr/xen-orchestra/pull/7565)) +- [Home/SR] Display _Pro Support_ status for XOSTOR SR (PR [#7601](https://github.com/vatesfr/xen-orchestra/pull/7601)) ### Bug fixes @@ -45,6 +46,6 @@ - xen-api patch - xo-cli patch - xo-server minor -- xo-web patch +- xo-web minor diff --git a/packages/xo-web/src/common/intl/messages.js b/packages/xo-web/src/common/intl/messages.js index e65404a8f38..78d88135b80 100644 --- a/packages/xo-web/src/common/intl/messages.js +++ b/packages/xo-web/src/common/intl/messages.js @@ -2580,6 +2580,8 @@ const messages = { disks: 'Disks', fieldRequired: '{field} is required', fieldsMissing: 'Some fields are missing', + hostBoundToMultipleXostorLicenses: 'More than 1 XOSTOR license on {host}', + hostHasNoXostorLicense: 'No XOSTOR license on {host}', hostsNotSameNumberOfDisks: 'Hosts do not have the same number of disks', ignoreFileSystems: 'Ignore file systems', ignoreFileSystemsInfo: 'Force LINSTOR group creation on existing filesystem', @@ -2590,7 +2592,8 @@ const messages = { licenseBoundUnknownXostor: 'License attached to an unknown XOSTOR', licenseNotBoundXostor: 'No XOSTOR attached', licenseExpiredXostorWarning: - 'The license {licenseId} has expired. You can still use the SR but cannot administrate it anymore.', + 'License{nLicenseIds, plural, one {} other {s}} {licenseIds} ha{nLicenseIds, plural, one {s} other {ve}} expired on {host}', + manageXostorWarning: 'To manage this XOSTOR storage, you must resolve the following issues:', networkNoPifs: 'The network does not have PIFs', networks: 'Networks', notXcpPool: 'Not an XCP-ng pool', @@ -2747,6 +2750,7 @@ const messages = { trialLicenseInfo: 'You are currently in a {edition} trial period that will end on {date, date, medium}', proxyMultipleLicenses: 'This proxy has more than 1 license!', proxyUnknownVm: 'Unknown proxy VM.', + xostorProSupportEnabled: 'XOSTOR Pro Support enabled', // ----- plan ----- onlyAvailableToEnterprise: 'Only available to Enterprise users', diff --git a/packages/xo-web/src/common/xo/index.js b/packages/xo-web/src/common/xo/index.js index 80d36ab6548..28e98ec766c 100644 --- a/packages/xo-web/src/common/xo/index.js +++ b/packages/xo-web/src/common/xo/index.js @@ -3826,11 +3826,16 @@ 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 -) +const createLicenseSubscription = productType => + createSubscription(() => + getXoaPlan() !== SOURCES && store.getState().user.permission === 'admin' + ? _call('xoa.licenses.getAll', { productType }) + : undefined + ) + +export const subscribeXcpngLicenses = createLicenseSubscription('xcpng') + +export const subscribeXostorLicenses = createLicenseSubscription('xostor') // Support -------------------------------------------------------------------- diff --git a/packages/xo-web/src/xo-app/home/host-item.js b/packages/xo-web/src/xo-app/home/host-item.js index 167f57f233f..c802777d67b 100644 --- a/packages/xo-web/src/xo-app/home/host-item.js +++ b/packages/xo-web/src/xo-app/home/host-item.js @@ -98,7 +98,7 @@ export default class HostItem extends Component { } const { supportLevel } = reacletteState.poolLicenseInfoByPoolId[host.$poolId] - const license = reacletteState.xcpngLicenseByBoundObjectId[host.id] + const license = reacletteState.xcpngLicenseByBoundObjectId?.[host.id] if (license !== undefined) { license.expires = license.expires ?? Infinity } diff --git a/packages/xo-web/src/xo-app/home/sr-item.js b/packages/xo-web/src/xo-app/home/sr-item.js index e48916513b9..89345de9d52 100644 --- a/packages/xo-web/src/xo-app/home/sr-item.js +++ b/packages/xo-web/src/xo-app/home/sr-item.js @@ -1,4 +1,5 @@ import _ from 'intl' +import BulkIcons from 'bulk-icons' import Component from 'base-component' import Ellipsis, { EllipsisContainer } from 'ellipsis' import Icon from 'icon' @@ -13,6 +14,7 @@ import { Text } from 'editable' import { createGetObject, createGetObjectsOfType, createSelector } from 'selectors' import { addTag, editSr, isSrShared, reconnectAllHostsSr, removeTag, setDefaultSr } from 'xo' import { connectStore, formatSizeShort, getIscsiPaths } from 'utils' +import { injectState } from 'reaclette' import styles from './index.css' @@ -61,6 +63,7 @@ import styles from './index.css' } ), }) +@injectState export default class SrItem extends Component { _addTag = tag => addTag(this.props.item.id, tag) _removeTag = tag => removeTag(this.props.item.id, tag) @@ -106,9 +109,17 @@ export default class SrItem extends Component { } } + getXostorLicenseInfo = createSelector( + () => this.props.state.xostorLicenseInfoByXostorId, + () => this.props.item, + (xostorLicenseInfoByXostorId, sr) => xostorLicenseInfoByXostorId?.[sr.id] + ) + render() { const { coalesceTask, container, expandAll, isDefaultSr, isHa, isShared, item: sr, selected } = this.props + const xostorLicenseInfo = this.getXostorLicenseInfo() + return (
@@ -134,6 +145,12 @@ export default class SrItem extends Component { )} {sr.inMaintenanceMode && {_('maintenanceMode')}} + {xostorLicenseInfo?.supportEnabled && ( + + + + )} + {xostorLicenseInfo?.alerts.length > 0 && } diff --git a/packages/xo-web/src/xo-app/index.js b/packages/xo-web/src/xo-app/index.js index 7f4c46e78a6..fe05d4990a0 100644 --- a/packages/xo-web/src/xo-app/index.js +++ b/packages/xo-web/src/xo-app/index.js @@ -16,6 +16,7 @@ import { addSubscriptions, connectStore, getXoaPlan, noop, routes } from 'utils' import { blockXoaAccess, isTrialRunning } from 'xoa-updater' import { checkXoa, clearXoaCheckCache } from 'xo' import { forEach, groupBy, keyBy, pick } from 'lodash' +import { Host as HostItem } from 'render-xo-item' import { Notification } from 'notification' import { productId2Plan } from 'xoa-plans' import { provideState } from 'reaclette' @@ -54,7 +55,7 @@ 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 { bindXcpngLicense, rebindLicense, subscribeXcpngLicenses, subscribeXostorLicenses } from '../common/xo' import { SOURCES } from '../common/xoa-plans' const shortcutManager = new ShortcutManager(keymap) @@ -137,14 +138,17 @@ export const ICON_POOL_LICENSE = { }) @addSubscriptions({ xcpLicenses: subscribeXcpngLicenses, + xostorLicenses: subscribeXostorLicenses, }) @connectStore(state => { const getHosts = createGetObjectsOfType('host') + const getXostors = createGetObjectsOfType('SR').filter([sr => sr.SR_type === 'linstor']) return { trial: state.xoaTrialState, registerNeeded: state.xoaUpdaterState === 'registerNeeded', signedUp: !!state.user, hosts: getHosts(state), + xostors: getXostors(state), } }) @provideState({ @@ -170,7 +174,10 @@ export const ICON_POOL_LICENSE = { 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'), + xcpngLicenseByBoundObjectId: (_, { xcpLicenses }) => + xcpLicenses === undefined ? undefined : keyBy(xcpLicenses, 'boundObjectId'), + xostorLicensesByBoundObjectId: (_, { xostorLicenses }) => + xostorLicenses === undefined ? undefined : groupBy(xostorLicenses, 'boundObjectId'), xcpngLicenseById: (_, { xcpLicenses }) => keyBy(xcpLicenses, 'id'), hostsByPoolId: createCollectionWrapper((_, { hosts }) => groupBy( @@ -196,7 +203,7 @@ export const ICON_POOL_LICENSE = { } for (const host of hosts) { - const license = xcpngLicenseByBoundObjectId[host.id] + const license = xcpngLicenseByBoundObjectId?.[host.id] if (license === undefined) { continue } @@ -234,6 +241,91 @@ export const ICON_POOL_LICENSE = { isXoaStatusOk: ({ xoaStatus }) => !xoaStatus.includes('✖'), areHostsVersionsEqualByPool: ({ hostsByPoolId }) => mapValues(hostsByPoolId, hosts => every(hosts, host => host.version === hosts[0].version)), + xostorLicenseInfoByXostorId: ( + { xcpngLicenseByBoundObjectId, xostorLicensesByBoundObjectId, hostsByPoolId }, + { xostors } + ) => { + if (xcpngLicenseByBoundObjectId === undefined || xostorLicensesByBoundObjectId === undefined) { + return + } + const xostorLicenseInfoByXostorId = {} + const now = Date.now() + + forEach(xostors, xostor => { + const xostorId = xostor.id + const hosts = hostsByPoolId[xostor.$pool] + + const alerts = [] + let supportEnabled = true + + hosts.forEach(host => { + const hostId = host.id + const xcpngLicense = xcpngLicenseByBoundObjectId[hostId] + const xostorLicenses = xostorLicensesByBoundObjectId[hostId] + + if (xcpngLicense === undefined || xcpngLicense.expires < now) { + supportEnabled = false + alerts.push({ + level: 'danger', + render: ( +

+ {_('hostNoSupport')} +

+ ), + }) + } + + if (xostorLicenses === undefined) { + supportEnabled = false + alerts.push({ + level: 'danger', + render:

{_('hostHasNoXostorLicense', { host: })}

, + }) + } + + if (xostorLicenses?.length > 1) { + alerts.push({ + level: 'warning', + render: ( +

+ {_('hostBoundToMultipleXostorLicenses', { host: })} +
+ {xostorLicenses.map(license => license.id.slice(-4)).join(',')} +

+ ), + }) + } + + const expiredXostorLicenses = xostorLicenses?.filter(license => license.expires < now) + if (expiredXostorLicenses?.length > 0) { + let level = 'warning' + if (expiredXostorLicenses.length === xostorLicenses.length) { + supportEnabled = false + level = 'danger' + } + alerts.push({ + level, + render: ( +

+ {_('licenseExpiredXostorWarning', { + licenseIds: expiredXostorLicenses.map(license => license.id.slice(-4)).join(','), + nLicenseIds: expiredXostorLicenses.length, + host: , + })} +

+ ), + }) + } + }) + + xostorLicenseInfoByXostorId[xostorId] = { + alerts, + supportEnabled, + } + }) + + return xostorLicenseInfoByXostorId + }, }, }) export default class XoApp extends Component { diff --git a/packages/xo-web/src/xo-app/pool/tab-advanced.js b/packages/xo-web/src/xo-app/pool/tab-advanced.js index 18c7cb62c8e..0766548ec06 100644 --- a/packages/xo-web/src/xo-app/pool/tab-advanced.js +++ b/packages/xo-web/src/xo-app/pool/tab-advanced.js @@ -72,7 +72,7 @@ const BindLicensesButton = decorate([ } const hostsWithoutLicense = poolHosts.filter(host => { - const license = this.state.xcpngLicenseByBoundObjectId[host.id] + const license = this.state.xcpngLicenseByBoundObjectId?.[host.id] return license === undefined || license.expires < Date.now() }) const licenseIdByHost = await confirm({ diff --git a/packages/xo-web/src/xo-app/sr/tab-xostor.js b/packages/xo-web/src/xo-app/sr/tab-xostor.js index 9ddb344e911..2687f32f9f5 100644 --- a/packages/xo-web/src/xo-app/sr/tab-xostor.js +++ b/packages/xo-web/src/xo-app/sr/tab-xostor.js @@ -19,6 +19,7 @@ import { import { find } from 'lodash' import { generateId } from 'reaclette-utils' import { Host, Vdi } from 'render-xo-item' +import { injectState } from 'reaclette' const RESOURCE_COLUMNS = [ { @@ -74,6 +75,7 @@ const INTERFACES_COLUMNS = [ healthCheck: subscribeXostorHealthCheck(sr), interfaces: subscribeXostorInterfaces(sr), })) +@injectState export default class TabXostor extends Component { _actions = [ { @@ -166,8 +168,36 @@ export default class TabXostor extends Component { } ) + getXostorLicenseInfo = createSelector( + () => this.props.state.xostorLicenseInfoByXostorId, + () => this.props.sr, + (xostorLicenseInfoByXostorId, sr) => xostorLicenseInfoByXostorId?.[sr.id] + ) + render() { const resourceInfos = this.getResourceInfos() + const xostorLicenseInfo = this.getXostorLicenseInfo() + + if (xostorLicenseInfo === undefined) { + return _('statusLoading') + } + + if (!xostorLicenseInfo.supportEnabled) { + return ( +
+

{_('manageXostorWarning')}

+
    + {xostorLicenseInfo.alerts + .filter(alert => alert.level === 'danger') + .map((alert, index) => ( +
  • + {alert.render} +
  • + ))} +
+
+ ) + } return (