diff --git a/CHANGELOG.unreleased.md b/CHANGELOG.unreleased.md index b302fff0b57..7dc998ae283 100644 --- a/CHANGELOG.unreleased.md +++ b/CHANGELOG.unreleased.md @@ -7,6 +7,8 @@ > Users must be able to say: “Nice enhancement, I'm eager to test it” +- [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 > Users must be able to say: “I had this issue, happy to know it's fixed” @@ -28,5 +30,6 @@ - xo-server patch +- xo-web minor diff --git a/packages/xo-server/src/api/xostor.mjs b/packages/xo-server/src/api/xostor.mjs index 2e8b2ecbd32..657b0471c9e 100644 --- a/packages/xo-server/src/api/xostor.mjs +++ b/packages/xo-server/src/api/xostor.mjs @@ -411,3 +411,18 @@ destroyInterface.params = { destroyInterface.resolve = { sr: ['sr', 'SR', 'administrate'], } +export async function healthCheck({ 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..2a80aa6d659 100644 --- a/packages/xo-web/src/common/xo/index.js +++ b/packages/xo-web/src/common/xo/index.js @@ -628,6 +628,17 @@ 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 })) + } + + 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..17ef5395777 --- /dev/null +++ b/packages/xo-web/src/xo-app/sr/tab-xostor.js @@ -0,0 +1,100 @@ +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: ({ volume }) => , + }, + { + name: _('inUse'), + itemRenderer: resource => , + sortCriteria: resource => resource['in-use'], + }, + { + 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.entries(healthCheck.resources).flatMap(([resourceName, resourceByHostname]) => { + return Object.entries(resourceByHostname).map(([hostname, resource]) => { + const host = this.props.hostByHostname[hostname][0] + const nodeStatus = healthCheck.nodes[hostname] + const volume = resource.volumes[0] // Always only one volume + + return { + ...resource, + host, + nodeStatus, + resourceName, + volume, + } + }) + }) + } + ) + + render() { + const resourceInfos = this.getResourceInfos() + + return ( + + + + + + {_('resourceList')} + + + + + + + + + ) + } +}