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))
- [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

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)
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],
}

View File

@ -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,

View File

@ -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)
)
)

View File

@ -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 ---------------------------------------------------------------

View File

@ -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 (
<Container>
<SingleLineRow>
@ -51,6 +55,8 @@ export default class MigrateVdiModalBody extends Component {
onChange={this.linkState('sr')}
predicate={this._getSrPredicate()}
required
resourceSet={resourceSet}
value={this.state.sr}
/>
</Col>
</SingleLineRow>

View File

@ -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 && (
<XoSelect
compareContainers={this._getCompareContainers()}
compareOptions={compareSrs}
labelProp='name_label'
onChange={this._onChangeSr}
predicate={this._getSrPredicate()}
resourceSet={self ? resourceSet : undefined}
useLongClick
value={sr}
xoType='SR'
value={vdiSr}
xoType={self ? 'resourceSetSr' : 'SR'}
>
<Sr id={sr.id} link />
<Sr id={vdiSr.id} link={!self} self={self} />
</XoSelect>
)
)
@ -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: (
<MigrateVdiModalBody
pool={this.props.vm.$pool}
resourceSet={this.props.resolvedResourceSet}
warningBeforeMigrate={this._getGenerateWarningBeforeMigrate()}
/>
),
@ -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'