diff --git a/CHANGELOG.unreleased.md b/CHANGELOG.unreleased.md index 1324cba85..3779db55e 100644 --- a/CHANGELOG.unreleased.md +++ b/CHANGELOG.unreleased.md @@ -15,6 +15,7 @@ - [Backup logs] Ability to filter by VM/pool name [#4406](https://github.com/vatesfr/xen-orchestra/issues/4406) (PR [#5208](https://github.com/vatesfr/xen-orchestra/pull/5208)) - [Tasks] Show XO objects linked to pending/finished tasks [#4275](https://github.com/vatesfr/xen-orchestra/issues/4275) (PR [#5267](https://github.com/vatesfr/xen-orchestra/pull/5267)) - [LDAP] Ability to import LDAP groups to XO [#1884](https://github.com/vatesfr/xen-orchestra/issues/1884) (PR [#5279](https://github.com/vatesfr/xen-orchestra/pull/5279)) +- [Self/VDI migration] Ability to migrate VDIs to other SRs within a resource set [#5020](https://github.com/vatesfr/xen-orchestra/issues/5020) (PR [#5201](https://github.com/vatesfr/xen-orchestra/pull/5201)) ### Bug fixes diff --git a/packages/xo-server/src/api/vdi.js b/packages/xo-server/src/api/vdi.js index 8b002bacc..bc07cc22b 100644 --- a/packages/xo-server/src/api/vdi.js +++ b/packages/xo-server/src/api/vdi.js @@ -116,9 +116,17 @@ set.resolve = { // ------------------------------------------------------------------- -export async function migrate({ vdi, sr }) { +export async function migrate({ vdi, sr, resourceSet }) { const xapi = this.getXapi(vdi) + if (this.user.permission !== 'admin') { + if (resourceSet !== undefined) { + await this.checkResourceSetConstraints(resourceSet, this.user.id, [sr.id]) + } else { + await this.checkPermissions(this.user.id, [[sr.id, 'administrate']]) + } + } + await xapi.moveVdi(vdi._xapiRef, sr._xapiRef) return true @@ -126,10 +134,11 @@ export async function migrate({ vdi, sr }) { migrate.params = { id: { type: 'string' }, + resourceSet: { type: 'string', optional: true }, sr_id: { type: 'string' }, } migrate.resolve = { vdi: ['id', ['VDI', 'VDI-snapshot'], 'administrate'], - sr: ['sr_id', 'SR', 'administrate'], + sr: ['sr_id', 'SR', false], } diff --git a/packages/xo-web/src/common/editable/index.js b/packages/xo-web/src/common/editable/index.js index 62392f3f3..b9d48d552 100644 --- a/packages/xo-web/src/common/editable/index.js +++ b/packages/xo-web/src/common/editable/index.js @@ -18,6 +18,7 @@ import { SelectProxy, SelectRemote, SelectResourceSetIp, + SelectResourceSetsSr, SelectSr, SelectSubject, SelectTag, @@ -434,6 +435,7 @@ const MAP_TYPE_SELECT = { proxy: SelectProxy, remote: SelectRemote, resourceSetIp: SelectResourceSetIp, + resourceSetSr: SelectResourceSetsSr, SR: SelectSr, subject: SelectSubject, tag: SelectTag, diff --git a/packages/xo-web/src/common/selectors.js b/packages/xo-web/src/common/selectors.js index ce25dd7b3..42b345876 100644 --- a/packages/xo-web/src/common/selectors.js +++ b/packages/xo-web/src/common/selectors.js @@ -589,32 +589,48 @@ export const getLoneSnapshots = create( ) ) +const _getResolvedResourceSet = (resourceSet, networks, srs, vms) => { + if (resourceSet === undefined) { + return + } + + const { objects, ...attrs } = resourceSet + const objectsByType = {} + const objectsFound = [] + + const resolve = (type, _objects) => { + const resolvedObjects = pick(_objects, objects) + if (!isEmpty(resolvedObjects)) { + objectsFound.push(...Object.keys(resolvedObjects)) + objectsByType[type] = Object.values(resolvedObjects) + } + } + resolve('VM-template', vms) + resolve('SR', srs) + resolve('network', networks) + + return { + ...attrs, + missingObjects: difference(objectsFound, objects), + objectsByType, + } +} + +export const getResolvedResourceSet = create( + (_, props) => props.resourceSet, + createGetObjectsOfType('network'), + createGetObjectsOfType('SR'), + createGetObjectsOfType('VM-template'), + _getResolvedResourceSet +) + export const getResolvedResourceSets = create( (_, props) => props.resourceSets, createGetObjectsOfType('network'), createGetObjectsOfType('SR'), createGetObjectsOfType('VM-template'), (resourceSets, networks, srs, vms) => - map(resourceSets, resourceSet => { - const { objects, ...attrs } = resourceSet - const objectsByType = {} - const objectsFound = [] - - const resolve = (type, _objects) => { - const resolvedObjects = pick(_objects, objects) - if (!isEmpty(resolvedObjects)) { - objectsFound.push(...Object.keys(resolvedObjects)) - objectsByType[type] = Object.values(resolvedObjects) - } - } - resolve('VM-template', vms) - resolve('SR', srs) - resolve('network', networks) - - return { - ...attrs, - missingObjects: difference(objectsFound, objects), - objectsByType, - } - }) + map(resourceSets, resourceSet => + _getResolvedResourceSet(resourceSet, networks, srs, vms) + ) ) diff --git a/packages/xo-web/src/common/xo/index.js b/packages/xo-web/src/common/xo/index.js index cb96b665c..37916e910 100644 --- a/packages/xo-web/src/common/xo/index.js +++ b/packages/xo-web/src/common/xo/index.js @@ -1743,8 +1743,12 @@ export const deleteOrphanedVdis = vdis => noop ) -export const migrateVdi = (vdi, sr) => - _call('vdi.migrate', { id: resolveId(vdi), sr_id: resolveId(sr) }) +export const migrateVdi = (vdi, sr, resourceSet) => + _call('vdi.migrate', { + id: resolveId(vdi), + resourceSet, + sr_id: resolveId(sr), + }) // VBD --------------------------------------------------------------- diff --git a/packages/xo-web/src/common/xo/migrate-vdi-modal/index.js b/packages/xo-web/src/common/xo/migrate-vdi-modal/index.js index fb191d4fb..7f747e349 100644 --- a/packages/xo-web/src/common/xo/migrate-vdi-modal/index.js +++ b/packages/xo-web/src/common/xo/migrate-vdi-modal/index.js @@ -6,7 +6,7 @@ import SingleLineRow from 'single-line-row' import { Container, Col } from 'grid' import { createCompare, createCompareContainers } from 'utils' import { createSelector } from 'selectors' -import { SelectSr } from 'select-objects' +import { SelectResourceSetsSr, SelectSr as SelectAnySr } from 'select-objects' import { isSrShared, isSrWritable } from '../' @@ -15,6 +15,7 @@ const compareSrs = createCompare([isSrShared]) export default class MigrateVdiModalBody extends Component { static propTypes = { pool: PropTypes.string.isRequired, + resourceSet: PropTypes.object, warningBeforeMigrate: PropTypes.func.isRequired, } @@ -39,7 +40,10 @@ export default class MigrateVdiModalBody extends Component { ) render() { + const { resourceSet } = this.props const warningBeforeMigrate = this._getWarningBeforeMigrate() + const SelectSr = + resourceSet !== undefined ? SelectResourceSetsSr : SelectAnySr return ( @@ -51,6 +55,8 @@ export default class MigrateVdiModalBody extends Component { onChange={this.linkState('sr')} predicate={this._getSrPredicate()} required + resourceSet={resourceSet} + value={this.state.sr} /> diff --git a/packages/xo-web/src/xo-app/vm/tab-disks.js b/packages/xo-web/src/xo-app/vm/tab-disks.js index 8df579726..dce560465 100644 --- a/packages/xo-web/src/xo-app/vm/tab-disks.js +++ b/packages/xo-web/src/xo-app/vm/tab-disks.js @@ -2,7 +2,7 @@ import _, { messages } from 'intl' import ActionButton from 'action-button' import Component from 'base-component' import copy from 'copy-to-clipboard' -import defined from '@xen-orchestra/defined' +import defined, { get as getDefined } from '@xen-orchestra/defined' import Icon from 'icon' import IsoDevice from 'iso-device' import MigrateVdiModalBody from 'xo/migrate-vdi-modal' @@ -19,6 +19,7 @@ import { createSelector, createFinder, getCheckPermissions, + getResolvedResourceSet, isAdmin, } from 'selectors' import { injectIntl } from 'react-intl' @@ -40,6 +41,7 @@ import { compact, every, filter, + find, forEach, get, map, @@ -69,6 +71,9 @@ import { const compareSrs = createCompare([isSrShared]) +@connectStore(() => ({ + isAdmin, +})) class VdiSr extends Component { _getCompareContainers = createSelector( () => this.props.userData.vm.$pool, @@ -80,23 +85,39 @@ class VdiSr extends Component { poolId => sr => sr.$pool === poolId && isSrWritable(sr) ) - _onChangeSr = sr => migrateVdi(this.props.item.vdi, sr) + _onChangeSr = sr => { + const { + item: { vdi }, + userData: { resourceSet }, + } = this.props + return migrateVdi( + vdi, + sr, + getDefined(() => resourceSet.id) + ) + } render() { - const sr = this.props.item.vdiSr + const { + isAdmin, + item: { vdiSr }, + userData: { resourceSet }, + } = this.props + const self = !isAdmin && resourceSet !== undefined return ( - sr !== undefined && ( + vdiSr !== undefined && ( - + ) ) @@ -468,11 +489,27 @@ class AttachDisk extends Component { } } -@connectStore(() => ({ - checkPermissions: getCheckPermissions, - isAdmin, - allVbds: createGetObjectsOfType('VBD'), +@addSubscriptions(props => ({ + // used by getResolvedResourceSet + resourceSet: cb => + subscribeResourceSets(resourceSets => + cb(find(resourceSets, { id: props.vm.resourceSet })) + ), })) +@connectStore(() => { + const getAllVbds = createGetObjectsOfType('VBD') + + return (state, props) => ({ + allVbds: getAllVbds(state, props), + checkPermissions: getCheckPermissions(state, props), + isAdmin: isAdmin(state, props), + resolvedResourceSet: getResolvedResourceSet( + state, + props, + !props.isAdmin && props.resourceSet !== undefined + ), + }) +}) export default class TabDisks extends Component { constructor(props) { super(props) @@ -526,6 +563,7 @@ export default class TabDisks extends Component { body: ( ), @@ -584,14 +622,25 @@ export default class TabDisks extends Component { () => this.props.vbds, () => this.props.vdis, () => this.props.srs, - (vbds, vdis, srs) => + () => this.props.resolvedResourceSet, + (vbds, vdis, srs, resourceSet) => compact( map(vbds, vbd => { let vdi return ( !vbd.is_cd_drive && ((vdi = vdis[vbd.VDI]), - vdi !== undefined && { ...vbd, vdi, vdiSr: srs[vdi.$SR] }) + vdi !== undefined && { + ...vbd, + vdi, + vdiSr: defined( + srs[vdi.$SR], + find( + getDefined(() => resourceSet.objectsByType.SR), + { id: vdi.$SR } + ) + ), + }) ) }) ) @@ -637,7 +686,7 @@ export default class TabDisks extends Component { ] render() { - const { allVbds, vm } = this.props + const { allVbds, resolvedResourceSet, vm } = this.props const { attachDisk, newDisk } = this.state @@ -699,6 +748,7 @@ export default class TabDisks extends Component { actions={this.actions} collection={this._getVbds()} columns={vm.virtualizationMode === 'pv' ? COLUMNS_VM_PV : COLUMNS} + data-resourceSet={resolvedResourceSet} data-vm={vm} individualActions={INDIVIDUAL_ACTIONS} shortcutsTarget='body'