feat(xo-web/vm/disks): ability to migrate VDIs to other SRs within resource set (#5201)

See #5020
This commit is contained in:
Rajaa.BARHTAOUI 2020-09-29 16:07:10 +02:00 committed by GitHub
parent 8cfaabedeb
commit 1116530a6b
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
7 changed files with 129 additions and 41 deletions

View File

@ -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)) - [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)) - [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)) - [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 ### Bug fixes

View File

@ -116,9 +116,17 @@ set.resolve = {
// ------------------------------------------------------------------- // -------------------------------------------------------------------
export async function migrate({ vdi, sr }) { export async function migrate({ vdi, sr, resourceSet }) {
const xapi = this.getXapi(vdi) 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) await xapi.moveVdi(vdi._xapiRef, sr._xapiRef)
return true return true
@ -126,10 +134,11 @@ export async function migrate({ vdi, sr }) {
migrate.params = { migrate.params = {
id: { type: 'string' }, id: { type: 'string' },
resourceSet: { type: 'string', optional: true },
sr_id: { type: 'string' }, sr_id: { type: 'string' },
} }
migrate.resolve = { migrate.resolve = {
vdi: ['id', ['VDI', 'VDI-snapshot'], 'administrate'], vdi: ['id', ['VDI', 'VDI-snapshot'], 'administrate'],
sr: ['sr_id', 'SR', 'administrate'], sr: ['sr_id', 'SR', false],
} }

View File

@ -18,6 +18,7 @@ import {
SelectProxy, SelectProxy,
SelectRemote, SelectRemote,
SelectResourceSetIp, SelectResourceSetIp,
SelectResourceSetsSr,
SelectSr, SelectSr,
SelectSubject, SelectSubject,
SelectTag, SelectTag,
@ -434,6 +435,7 @@ const MAP_TYPE_SELECT = {
proxy: SelectProxy, proxy: SelectProxy,
remote: SelectRemote, remote: SelectRemote,
resourceSetIp: SelectResourceSetIp, resourceSetIp: SelectResourceSetIp,
resourceSetSr: SelectResourceSetsSr,
SR: SelectSr, SR: SelectSr,
subject: SelectSubject, subject: SelectSubject,
tag: SelectTag, tag: SelectTag,

View File

@ -589,13 +589,11 @@ export const getLoneSnapshots = create(
) )
) )
export const getResolvedResourceSets = create( const _getResolvedResourceSet = (resourceSet, networks, srs, vms) => {
(_, props) => props.resourceSets, if (resourceSet === undefined) {
createGetObjectsOfType('network'), return
createGetObjectsOfType('SR'), }
createGetObjectsOfType('VM-template'),
(resourceSets, networks, srs, vms) =>
map(resourceSets, resourceSet => {
const { objects, ...attrs } = resourceSet const { objects, ...attrs } = resourceSet
const objectsByType = {} const objectsByType = {}
const objectsFound = [] const objectsFound = []
@ -616,5 +614,23 @@ export const getResolvedResourceSets = create(
missingObjects: difference(objectsFound, objects), missingObjects: difference(objectsFound, objects),
objectsByType, 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 =>
_getResolvedResourceSet(resourceSet, networks, srs, vms)
)
) )

View File

@ -1743,8 +1743,12 @@ export const deleteOrphanedVdis = vdis =>
noop noop
) )
export const migrateVdi = (vdi, sr) => export const migrateVdi = (vdi, sr, resourceSet) =>
_call('vdi.migrate', { id: resolveId(vdi), sr_id: resolveId(sr) }) _call('vdi.migrate', {
id: resolveId(vdi),
resourceSet,
sr_id: resolveId(sr),
})
// VBD --------------------------------------------------------------- // VBD ---------------------------------------------------------------

View File

@ -6,7 +6,7 @@ import SingleLineRow from 'single-line-row'
import { Container, Col } from 'grid' import { Container, Col } from 'grid'
import { createCompare, createCompareContainers } from 'utils' import { createCompare, createCompareContainers } from 'utils'
import { createSelector } from 'selectors' import { createSelector } from 'selectors'
import { SelectSr } from 'select-objects' import { SelectResourceSetsSr, SelectSr as SelectAnySr } from 'select-objects'
import { isSrShared, isSrWritable } from '../' import { isSrShared, isSrWritable } from '../'
@ -15,6 +15,7 @@ const compareSrs = createCompare([isSrShared])
export default class MigrateVdiModalBody extends Component { export default class MigrateVdiModalBody extends Component {
static propTypes = { static propTypes = {
pool: PropTypes.string.isRequired, pool: PropTypes.string.isRequired,
resourceSet: PropTypes.object,
warningBeforeMigrate: PropTypes.func.isRequired, warningBeforeMigrate: PropTypes.func.isRequired,
} }
@ -39,7 +40,10 @@ export default class MigrateVdiModalBody extends Component {
) )
render() { render() {
const { resourceSet } = this.props
const warningBeforeMigrate = this._getWarningBeforeMigrate() const warningBeforeMigrate = this._getWarningBeforeMigrate()
const SelectSr =
resourceSet !== undefined ? SelectResourceSetsSr : SelectAnySr
return ( return (
<Container> <Container>
<SingleLineRow> <SingleLineRow>
@ -51,6 +55,8 @@ export default class MigrateVdiModalBody extends Component {
onChange={this.linkState('sr')} onChange={this.linkState('sr')}
predicate={this._getSrPredicate()} predicate={this._getSrPredicate()}
required required
resourceSet={resourceSet}
value={this.state.sr}
/> />
</Col> </Col>
</SingleLineRow> </SingleLineRow>

View File

@ -2,7 +2,7 @@ import _, { messages } from 'intl'
import ActionButton from 'action-button' import ActionButton from 'action-button'
import Component from 'base-component' import Component from 'base-component'
import copy from 'copy-to-clipboard' 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 Icon from 'icon'
import IsoDevice from 'iso-device' import IsoDevice from 'iso-device'
import MigrateVdiModalBody from 'xo/migrate-vdi-modal' import MigrateVdiModalBody from 'xo/migrate-vdi-modal'
@ -19,6 +19,7 @@ import {
createSelector, createSelector,
createFinder, createFinder,
getCheckPermissions, getCheckPermissions,
getResolvedResourceSet,
isAdmin, isAdmin,
} from 'selectors' } from 'selectors'
import { injectIntl } from 'react-intl' import { injectIntl } from 'react-intl'
@ -40,6 +41,7 @@ import {
compact, compact,
every, every,
filter, filter,
find,
forEach, forEach,
get, get,
map, map,
@ -69,6 +71,9 @@ import {
const compareSrs = createCompare([isSrShared]) const compareSrs = createCompare([isSrShared])
@connectStore(() => ({
isAdmin,
}))
class VdiSr extends Component { class VdiSr extends Component {
_getCompareContainers = createSelector( _getCompareContainers = createSelector(
() => this.props.userData.vm.$pool, () => this.props.userData.vm.$pool,
@ -80,23 +85,39 @@ class VdiSr extends Component {
poolId => sr => sr.$pool === poolId && isSrWritable(sr) 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() { render() {
const sr = this.props.item.vdiSr const {
isAdmin,
item: { vdiSr },
userData: { resourceSet },
} = this.props
const self = !isAdmin && resourceSet !== undefined
return ( return (
sr !== undefined && ( vdiSr !== undefined && (
<XoSelect <XoSelect
compareContainers={this._getCompareContainers()} compareContainers={this._getCompareContainers()}
compareOptions={compareSrs} compareOptions={compareSrs}
labelProp='name_label' labelProp='name_label'
onChange={this._onChangeSr} onChange={this._onChangeSr}
predicate={this._getSrPredicate()} predicate={this._getSrPredicate()}
resourceSet={self ? resourceSet : undefined}
useLongClick useLongClick
value={sr} value={vdiSr}
xoType='SR' xoType={self ? 'resourceSetSr' : 'SR'}
> >
<Sr id={sr.id} link /> <Sr id={vdiSr.id} link={!self} self={self} />
</XoSelect> </XoSelect>
) )
) )
@ -468,11 +489,27 @@ class AttachDisk extends Component {
} }
} }
@connectStore(() => ({ @addSubscriptions(props => ({
checkPermissions: getCheckPermissions, // used by getResolvedResourceSet
isAdmin, resourceSet: cb =>
allVbds: createGetObjectsOfType('VBD'), 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 { export default class TabDisks extends Component {
constructor(props) { constructor(props) {
super(props) super(props)
@ -526,6 +563,7 @@ export default class TabDisks extends Component {
body: ( body: (
<MigrateVdiModalBody <MigrateVdiModalBody
pool={this.props.vm.$pool} pool={this.props.vm.$pool}
resourceSet={this.props.resolvedResourceSet}
warningBeforeMigrate={this._getGenerateWarningBeforeMigrate()} warningBeforeMigrate={this._getGenerateWarningBeforeMigrate()}
/> />
), ),
@ -584,14 +622,25 @@ export default class TabDisks extends Component {
() => this.props.vbds, () => this.props.vbds,
() => this.props.vdis, () => this.props.vdis,
() => this.props.srs, () => this.props.srs,
(vbds, vdis, srs) => () => this.props.resolvedResourceSet,
(vbds, vdis, srs, resourceSet) =>
compact( compact(
map(vbds, vbd => { map(vbds, vbd => {
let vdi let vdi
return ( return (
!vbd.is_cd_drive && !vbd.is_cd_drive &&
((vdi = vdis[vbd.VDI]), ((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() { render() {
const { allVbds, vm } = this.props const { allVbds, resolvedResourceSet, vm } = this.props
const { attachDisk, newDisk } = this.state const { attachDisk, newDisk } = this.state
@ -699,6 +748,7 @@ export default class TabDisks extends Component {
actions={this.actions} actions={this.actions}
collection={this._getVbds()} collection={this._getVbds()}
columns={vm.virtualizationMode === 'pv' ? COLUMNS_VM_PV : COLUMNS} columns={vm.virtualizationMode === 'pv' ? COLUMNS_VM_PV : COLUMNS}
data-resourceSet={resolvedResourceSet}
data-vm={vm} data-vm={vm}
individualActions={INDIVIDUAL_ACTIONS} individualActions={INDIVIDUAL_ACTIONS}
shortcutsTarget='body' shortcutsTarget='body'