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