diff --git a/CHANGELOG.unreleased.md b/CHANGELOG.unreleased.md
index 3b756f52f02..e7b94e9ff5b 100644
--- a/CHANGELOG.unreleased.md
+++ b/CHANGELOG.unreleased.md
@@ -9,6 +9,7 @@
- [Backups] Make health check timeout configurable: property `healthCheckTimeout` of config file (PR [#7561](https://github.com/vatesfr/xen-orchestra/pull/7561))
- [Plugin/audit] Expose records in the REST API at `/rest/v0/plugins/audit/records`
+- [XOSTOR] List linstor resources in the XOSTOR tab of an SR's view (PR [#7542](https://github.com/vatesfr/xen-orchestra/pull/7542))
### Bug fixes
@@ -43,9 +44,9 @@
- @xen-orchestra/backups minor
- @xen-orchestra/proxy minor
- @xen-orchestra/vmware-explorer minor
-- xo-server patch
+- xo-server minor
- xo-server-audit minor
- xo-server-load-balancer patch
-- xo-web patch
+- xo-web minor
diff --git a/packages/xo-server/src/api/xostor.mjs b/packages/xo-server/src/api/xostor.mjs
index 2e8b2ecbd32..b7f4e1a21d0 100644
--- a/packages/xo-server/src/api/xostor.mjs
+++ b/packages/xo-server/src/api/xostor.mjs
@@ -411,3 +411,19 @@ destroyInterface.params = {
destroyInterface.resolve = {
sr: ['sr', 'SR', 'administrate'],
}
+export async function healthCheck({ sr }) {
+ checkIfLinstorSr(sr)
+ const xapi = this.getXapi(sr)
+ const pool = this.getObject(sr.$pool)
+ const groupName = this.getObject(sr.$PBDs[0]).device_config['group-name']
+
+ return JSON.parse(
+ await pluginCall(xapi, this.getObject(pool.master), 'linstor-manager', 'healthCheck', { groupName })
+ )
+}
+healthCheck.params = {
+ sr: { type: 'string' },
+}
+healthCheck.resolve = {
+ sr: ['sr', 'SR', 'view'],
+}
diff --git a/packages/xo-web/src/common/intl/messages.js b/packages/xo-web/src/common/intl/messages.js
index d1980ce28e0..dd76416f7af 100644
--- a/packages/xo-web/src/common/intl/messages.js
+++ b/packages/xo-web/src/common/intl/messages.js
@@ -10,6 +10,7 @@ const messages = {
description: 'Description',
deleteSourceVm: 'Delete source VM',
disable: 'Disable',
+ diskState: 'Disk state',
download: 'Download',
enable: 'Enable',
expiration: 'Expiration',
@@ -23,12 +24,16 @@ const messages = {
esxiImportStopSourceDescription:
'Source VM stopped before the last delta transfer (after final snapshot). Needed to fully transfer a running VM',
esxiImportStopOnErrorDescription: 'Stop on the first error when importing VMs',
+ inUse: 'In use',
nImportVmsInParallel: 'Number of VMs to import in parallel',
+ node: 'Node',
stopOnError: 'Stop on error',
uuid: 'UUID',
+ vdi: 'VDI',
vmSrUsage: 'Storage: {used} used of {total} ({free} free)',
new: 'New',
+ nodeStatus: 'Node status',
notDefined: 'Not defined',
status: 'Status',
statusConnecting: 'Connecting',
@@ -2593,6 +2598,7 @@ const messages = {
pifsNotAttached: 'Not all PIFs are attached',
pifsNotStatic: 'Not all PIFs are static',
replication: 'Replication',
+ resourceList: 'Resource list',
rpuNoLongerAvailableIfXostor:
'As long as a XOSTOR storage is present in the pool, Rolling Pool Update will not be available',
selectDisks: 'Select disk(s)…',
diff --git a/packages/xo-web/src/common/xo/index.js b/packages/xo-web/src/common/xo/index.js
index 02e3e888c9c..49c87c9b05c 100644
--- a/packages/xo-web/src/common/xo/index.js
+++ b/packages/xo-web/src/common/xo/index.js
@@ -628,6 +628,19 @@ export const subscribeCloudXoConfig = createSubscription(() =>
fetch('./rest/v0/cloud/xo-config').then(resp => resp.json())
)
+const subscribeSrsXostorHealthCheck = {}
+export const subscribeXostorHealthCheck = sr => {
+ const srId = resolveId(sr)
+
+ if (subscribeSrsXostorHealthCheck[srId] === undefined) {
+ subscribeSrsXostorHealthCheck[srId] = createSubscription(() => _call('xostor.healthCheck', { sr: srId }), {
+ polling: 6e4, // To avoid spamming the linstor controller
+ })
+ }
+
+ return subscribeSrsXostorHealthCheck[srId]
+}
+
// System ============================================================
export const apiMethods = _call('system.getMethodsInfo')
diff --git a/packages/xo-web/src/xo-app/sr/index.js b/packages/xo-web/src/xo-app/sr/index.js
index 086dd01fe0a..b101641cdda 100644
--- a/packages/xo-web/src/xo-app/sr/index.js
+++ b/packages/xo-web/src/xo-app/sr/index.js
@@ -22,6 +22,7 @@ import TabHosts from './tab-host'
import TabLogs from './tab-logs'
import TabStats from './tab-stats'
import TabXosan from './tab-xosan'
+import TabXostor from './tab-xostor'
// ===================================================================
@@ -33,6 +34,7 @@ import TabXosan from './tab-xosan'
logs: TabLogs,
stats: TabStats,
xosan: TabXosan,
+ xostor: TabXostor,
})
@connectStore(() => {
const getSr = createGetObject()
@@ -127,6 +129,7 @@ export default class Sr extends Component {
{sr.SR_type === 'xosan' && XOSAN}
{_('hostsTabName')}
{_('logsTabName')}
+ {sr.SR_type === 'linstor' && {_('xostor')}}
{_('advancedTabName')}
diff --git a/packages/xo-web/src/xo-app/sr/tab-xostor.js b/packages/xo-web/src/xo-app/sr/tab-xostor.js
new file mode 100644
index 00000000000..b859669b158
--- /dev/null
+++ b/packages/xo-web/src/xo-app/sr/tab-xostor.js
@@ -0,0 +1,104 @@
+import _ from 'intl'
+import Component from 'base-component'
+import Icon from 'icon'
+import React from 'react'
+import SortedTable from 'sorted-table'
+
+import { addSubscriptions, connectStore } from 'utils'
+import { Card, CardHeader, CardBlock } from 'card'
+import { Container, Row, Col } from 'grid'
+import { createSelector, createGetObjectsOfType } from 'selectors'
+import { Host, Vdi } from 'render-xo-item'
+import { subscribeXostorHealthCheck } from 'xo'
+
+const RESOURCE_COLUMNS = [
+ {
+ name: 'Resource name',
+ itemRenderer: ({ resourceName }) => resourceName,
+ sortCriteria: ({ resourceName }) => resourceName,
+ },
+ {
+ name: _('node'),
+ itemRenderer: ({ host }) => ,
+ sortCriteria: ({ host }) => host.name_label,
+ },
+ {
+ name: _('nodeStatus'),
+ itemRenderer: ({ nodeStatus }) => nodeStatus,
+ sortCriteria: ({ nodeStatus }) => nodeStatus,
+ },
+ {
+ name: _('vdi'),
+ itemRenderer: ({ vdiId }) => vdiId !== '' && ,
+ },
+ {
+ name: _('inUse'),
+ itemRenderer: ({ inUse }) => ,
+ sortCriteria: ({ inUse }) => inUse,
+ },
+ {
+ name: _('diskState'),
+ itemRenderer: ({ volume }) => volume['disk-state'],
+ sortCriteria: ({ volume }) => volume['disk-state'],
+ },
+]
+
+@connectStore({
+ hostByHostname: createGetObjectsOfType('host')
+ .filter((_, props) => host => host.$pool === props.sr.$pool)
+ .groupBy('hostname'),
+})
+@addSubscriptions(({ sr }) => ({
+ healthCheck: subscribeXostorHealthCheck(sr),
+}))
+export default class TabXostor extends Component {
+ getResourceInfos = createSelector(
+ () => this.props.healthCheck,
+ healthCheck => {
+ if (healthCheck === undefined) {
+ return []
+ }
+
+ return Object.keys(healthCheck.resources).flatMap(resourceName => {
+ return Object.entries(healthCheck.resources[resourceName].nodes).reduce((acc, [hostname, nodeInfo]) => {
+ const volume = nodeInfo.volumes[0] // Max only one volume
+ if (volume !== undefined) {
+ const nodeStatus = healthCheck.nodes[hostname]
+ const host = this.props.hostByHostname[hostname][0]
+
+ acc.push({
+ inUse: nodeInfo['in-use'],
+ vdiId: healthCheck.resources[resourceName].uuid,
+ volume,
+ nodeStatus,
+ host,
+ resourceName,
+ })
+ }
+ return acc
+ }, [])
+ })
+ }
+ )
+
+ render() {
+ const resourceInfos = this.getResourceInfos()
+
+ return (
+
+
+
+
+
+ {_('resourceList')}
+
+
+
+
+
+
+
+
+ )
+ }
+}