Skip to content

Commit

Permalink
feat(xo-web/home/SR): display pro support status for XOSTOR SR (#7601)
Browse files Browse the repository at this point in the history
  • Loading branch information
MathieuRA authored Apr 29, 2024
1 parent 0c67610 commit 08d3105
Show file tree
Hide file tree
Showing 8 changed files with 161 additions and 12 deletions.
3 changes: 2 additions & 1 deletion CHANGELOG.unreleased.md
Original file line number Diff line number Diff line change
Expand Up @@ -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

Expand Down Expand Up @@ -45,6 +46,6 @@
- xen-api patch
- xo-cli patch
- xo-server minor
- xo-web patch
- xo-web minor

<!--packages-end-->
6 changes: 5 additions & 1 deletion packages/xo-web/src/common/intl/messages.js
Original file line number Diff line number Diff line change
Expand Up @@ -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',
Expand All @@ -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',
Expand Down Expand Up @@ -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',
Expand Down
15 changes: 10 additions & 5 deletions packages/xo-web/src/common/xo/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -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 --------------------------------------------------------------------

Expand Down
2 changes: 1 addition & 1 deletion packages/xo-web/src/xo-app/home/host-item.js
Original file line number Diff line number Diff line change
Expand Up @@ -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
}
Expand Down
17 changes: 17 additions & 0 deletions packages/xo-web/src/xo-app/home/sr-item.js
Original file line number Diff line number Diff line change
@@ -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'
Expand All @@ -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'

Expand Down Expand Up @@ -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)
Expand Down Expand Up @@ -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 (
<div className={styles.item}>
<BlockLink to={`/srs/${sr.id}`}>
Expand All @@ -134,6 +145,12 @@ export default class SrItem extends Component {
</Tooltip>
)}
{sr.inMaintenanceMode && <span className='tag tag-pill tag-warning ml-1'>{_('maintenanceMode')}</span>}
{xostorLicenseInfo?.supportEnabled && (
<Tooltip content={_('xostorProSupportEnabled')}>
<Icon icon='pro-support' fixedWidth className='text-success ml-1' />
</Tooltip>
)}
{xostorLicenseInfo?.alerts.length > 0 && <BulkIcons alerts={xostorLicenseInfo.alerts} />}
</EllipsisContainer>
</Col>
<Col largeSize={1} className='hidden-md-down'>
Expand Down
98 changes: 95 additions & 3 deletions packages/xo-web/src/xo-app/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -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'
Expand Down Expand Up @@ -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)
Expand Down Expand Up @@ -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({
Expand All @@ -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(
Expand All @@ -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
}
Expand Down Expand Up @@ -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: (
<p>
{_('hostNoSupport')} <HostItem id={hostId} />
</p>
),
})
}

if (xostorLicenses === undefined) {
supportEnabled = false
alerts.push({
level: 'danger',
render: <p>{_('hostHasNoXostorLicense', { host: <HostItem id={hostId} /> })}</p>,
})
}

if (xostorLicenses?.length > 1) {
alerts.push({
level: 'warning',
render: (
<p>
{_('hostBoundToMultipleXostorLicenses', { host: <HostItem id={hostId} /> })}
<br />
{xostorLicenses.map(license => license.id.slice(-4)).join(',')}
</p>
),
})
}

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: (
<p>
{_('licenseExpiredXostorWarning', {
licenseIds: expiredXostorLicenses.map(license => license.id.slice(-4)).join(','),
nLicenseIds: expiredXostorLicenses.length,
host: <HostItem id={hostId} />,
})}
</p>
),
})
}
})

xostorLicenseInfoByXostorId[xostorId] = {
alerts,
supportEnabled,
}
})

return xostorLicenseInfoByXostorId
},
},
})
export default class XoApp extends Component {
Expand Down
2 changes: 1 addition & 1 deletion packages/xo-web/src/xo-app/pool/tab-advanced.js
Original file line number Diff line number Diff line change
Expand Up @@ -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({
Expand Down
30 changes: 30 additions & 0 deletions packages/xo-web/src/xo-app/sr/tab-xostor.js
Original file line number Diff line number Diff line change
Expand Up @@ -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 = [
{
Expand Down Expand Up @@ -74,6 +75,7 @@ const INTERFACES_COLUMNS = [
healthCheck: subscribeXostorHealthCheck(sr),
interfaces: subscribeXostorInterfaces(sr),
}))
@injectState
export default class TabXostor extends Component {
_actions = [
{
Expand Down Expand Up @@ -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 (
<div>
<p>{_('manageXostorWarning')}</p>
<ul>
{xostorLicenseInfo.alerts
.filter(alert => alert.level === 'danger')
.map((alert, index) => (
<li key={index} className='text-danger'>
{alert.render}
</li>
))}
</ul>
</div>
)
}

return (
<Container>
Expand Down

0 comments on commit 08d3105

Please sign in to comment.