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 (