feat(xo-web/vm/disks): ability to migrate VDIs to other SRs within resource set (#5201)
See #5020
This commit is contained in:
parent
8cfaabedeb
commit
1116530a6b
@ -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
|
||||||
|
|
||||||
|
@ -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],
|
||||||
}
|
}
|
||||||
|
@ -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,
|
||||||
|
@ -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(
|
export const getResolvedResourceSets = create(
|
||||||
(_, props) => props.resourceSets,
|
(_, props) => props.resourceSets,
|
||||||
createGetObjectsOfType('network'),
|
createGetObjectsOfType('network'),
|
||||||
createGetObjectsOfType('SR'),
|
createGetObjectsOfType('SR'),
|
||||||
createGetObjectsOfType('VM-template'),
|
createGetObjectsOfType('VM-template'),
|
||||||
(resourceSets, networks, srs, vms) =>
|
(resourceSets, networks, srs, vms) =>
|
||||||
map(resourceSets, resourceSet => {
|
map(resourceSets, resourceSet =>
|
||||||
const { objects, ...attrs } = resourceSet
|
_getResolvedResourceSet(resourceSet, networks, srs, vms)
|
||||||
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,
|
|
||||||
}
|
|
||||||
})
|
|
||||||
)
|
)
|
||||||
|
@ -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 ---------------------------------------------------------------
|
||||||
|
|
||||||
|
@ -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>
|
||||||
|
@ -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'
|
||||||
|
Loading…
Reference in New Issue
Block a user