mirror of
https://github.com/vatesfr/xen-orchestra.git
synced 2026-11-09 17:06:13 -06:00
feat(rest-api): expose vms/:id/actions/migrate (#9414)
This commit is contained in:
@@ -87,6 +87,19 @@ export interface Xapi {
|
||||
deleteNetwork(id: XoNetwork['id']): Promise<void>
|
||||
deleteVif(vifId: XoVif['id']): Promise<void>
|
||||
exportVmOva(vmRef: XenApiVm['$ref']): Promise<PassThrough>
|
||||
migrateVm(
|
||||
vmId: XoVm['id'],
|
||||
hostXapi: Xapi,
|
||||
hostId: XoHost['id'],
|
||||
opts?: {
|
||||
force?: boolean
|
||||
mapVdisSrs?: Record<XoVdi['id'], XoSr['id']>
|
||||
mapVifsNetworks?: Record<XoVif['id'], XoNetwork['id']>
|
||||
migrationNetworkId?: XoNetwork['id']
|
||||
sr?: XoSr['id']
|
||||
bypassAssert?: boolean
|
||||
}
|
||||
): Promise<void>
|
||||
listMissingPatches(host: XoHost['id']): Promise<XcpPatches[] | XsPatches[]>
|
||||
pool_emergencyShutdown(): Promise<void>
|
||||
resumeVm(id: XoVm['id']): Promise<void>
|
||||
|
||||
@@ -31,6 +31,10 @@ import type {
|
||||
XoVm,
|
||||
XoVmSnapshot,
|
||||
XoMessage,
|
||||
XoSr,
|
||||
XoNetwork,
|
||||
XoVif,
|
||||
XoPif,
|
||||
} from '@vates/types'
|
||||
import { PassThrough, Readable } from 'node:stream'
|
||||
|
||||
@@ -41,6 +45,7 @@ import {
|
||||
forbiddenOperationResp,
|
||||
incorrectStateResp,
|
||||
internalServerErrorResp,
|
||||
invalidParameters as invalidParametersResp,
|
||||
noContentResp,
|
||||
notFoundResp,
|
||||
unauthorizedResp,
|
||||
@@ -690,4 +695,57 @@ export class VmController extends XapiXoController<XoVm> {
|
||||
stream?.end()
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* VIF mapping is not allowed for intra-pool migration
|
||||
*
|
||||
* Networks and SRs must belong to the same pool as the destination host
|
||||
*
|
||||
* @example id "613f541c-4bed-fc77-7ca8-2db6b68f079c"
|
||||
* @example body { "hostId": "b61a5c92-700e-4966-a13b-00633f03eea8" }
|
||||
*/
|
||||
@Example(taskLocation)
|
||||
@Post('{id}/actions/migrate')
|
||||
@Middlewares(json())
|
||||
@SuccessResponse(asynchronousActionResp.status, asynchronousActionResp.description)
|
||||
@Response(noContentResp.status, noContentResp.description)
|
||||
@Response(notFoundResp.status, notFoundResp.description)
|
||||
@Response(invalidParametersResp.status, invalidParametersResp.description)
|
||||
@Response(internalServerErrorResp.status, internalServerErrorResp.description)
|
||||
migrateVm(
|
||||
@Path() id: string,
|
||||
@Body()
|
||||
body:
|
||||
| { hostId: string; migrationNetworkId?: string }
|
||||
| {
|
||||
hostId: string
|
||||
migrationNetworkId?: string
|
||||
srId?: string
|
||||
srIdByVdiId?: { [key: string]: string }
|
||||
networkIdByVifId?: { [key: string]: string }
|
||||
},
|
||||
@Query() sync?: boolean
|
||||
): CreateActionReturnType<void> {
|
||||
const vmId = id as XoVm['id']
|
||||
const hostId = body.hostId as XoHost['id']
|
||||
|
||||
const action = async () => {
|
||||
const { migrationNetworkId } = body
|
||||
const xapiVm = this.getXapiObject(vmId)
|
||||
|
||||
await xapiVm.$xapi.migrateVm(vmId, this.getXapi(hostId), hostId, {
|
||||
mapVdisSrs: 'srIdByVdiId' in body ? (body.srIdByVdiId as Record<XoVdi['id'], XoSr['id']>) : undefined,
|
||||
mapVifsNetworks:
|
||||
'networkIdByVifId' in body ? (body.networkIdByVifId as Record<XoVif['id'], XoNetwork['id']>) : undefined,
|
||||
migrationNetworkId: migrationNetworkId as XoNetwork['id'] | undefined,
|
||||
sr: 'srId' in body ? (body.srId as XoSr['id']) : undefined,
|
||||
})
|
||||
}
|
||||
|
||||
return this.createAction(action, {
|
||||
sync,
|
||||
statusCode: 204,
|
||||
taskProperties: { name: 'Migrate VM', objectId: vmId, params: body },
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
@@ -17,6 +17,7 @@
|
||||
- [Warm Migration] the api call now return the new VM uuid (PR [#94653](https://github.com/vatesfr/xen-orchestra/pull/9465))
|
||||
- [Warm Migration] stopped VM can be warm migrated (PR [#94653](https://github.com/vatesfr/xen-orchestra/pull/9465))
|
||||
- [Plugins/load balancer] Add configurable VM migration cooldown to prevent oscillation (default 30min) (PR [#9388](https://github.com/vatesfr/xen-orchestra/pull/9388))
|
||||
- [REST API] Expose `POST /rest/v0/vms/:id/actions/migrate` to migrate a VM (PR [#9414](https://github.com/vatesfr/xen-orchestra/pull/9414))
|
||||
|
||||
### Bug fixes
|
||||
|
||||
@@ -38,6 +39,7 @@
|
||||
|
||||
<!--packages-start-->
|
||||
|
||||
- @vates/types minor
|
||||
- @xen-orchestra/fs patch
|
||||
- @xen-orchestra/rest-api minor
|
||||
- @xen-orchestra/web patch
|
||||
|
||||
@@ -1,5 +1,10 @@
|
||||
/* eslint eslint-comments/disable-enable-pair: [error, {allowWholeFile: true}] */
|
||||
/* eslint-disable camelcase */
|
||||
|
||||
/**
|
||||
* @import {XenApiNetworkWrapped, XenApiHostWrapped, XenApiSrWrapped } from "@vates/types"
|
||||
*/
|
||||
|
||||
import fatfs from '@vates/fatfs'
|
||||
import asyncMapSettled from '@xen-orchestra/async-map/legacy.js'
|
||||
import filter from 'lodash/filter.js'
|
||||
@@ -30,6 +35,7 @@ import { forbiddenOperation, operationFailed } from 'xo-common/api-errors.js'
|
||||
import { parseDateTime, Xapi as XapiBase, XapiDiskSource } from '@xen-orchestra/xapi'
|
||||
import { Ref } from 'xen-api'
|
||||
import { synchronized } from 'decorator-synchronized'
|
||||
import { Task } from '@xen-orchestra/mixins/Tasks.mjs'
|
||||
|
||||
import fatfsBuffer, { addMbr, init as fatfsBufferInit } from '../fatfs-buffer.mjs'
|
||||
import { camelToSnakeCase, forEach, map, pDelay, promisifyAll } from '../utils.mjs'
|
||||
@@ -512,7 +518,14 @@ export default class Xapi extends XapiBase {
|
||||
bypassAssert = false,
|
||||
}
|
||||
) {
|
||||
const srRef = sr !== undefined ? hostXapi.getObject(sr).$ref : undefined
|
||||
Task.info('migration with storage motion')
|
||||
|
||||
const targetSr = sr !== undefined ? hostXapi.getObject(sr) : undefined
|
||||
if (targetSr !== undefined) {
|
||||
this.#vmMigrationSrPreCheck(targetSr, host)
|
||||
}
|
||||
|
||||
const srRef = targetSr?.$ref
|
||||
const getDefaultSrRef = once(() => {
|
||||
const defaultSr = host.$pool.$default_SR
|
||||
if (defaultSr === undefined) {
|
||||
@@ -571,14 +584,23 @@ export default class Xapi extends XapiBase {
|
||||
if (vdi.is_a_snapshot && vdi.$snapshot_of !== undefined) {
|
||||
continue
|
||||
}
|
||||
vdis[vdi.$ref] = getMigrationSrRef(vdi)
|
||||
|
||||
const migrationSrRef = getMigrationSrRef(vdi)
|
||||
this.#vmMigrationSrPreCheck(hostXapi.getObject(migrationSrRef), host)
|
||||
|
||||
vdis[vdi.$ref] = migrationSrRef
|
||||
}
|
||||
}
|
||||
|
||||
// VIFs/Networks mapping
|
||||
const vifsMap = {}
|
||||
if (vm.$pool === host.$pool && mapVifsNetworks !== undefined && !isEmpty(mapVifsNetworks)) {
|
||||
// VIF mapping not allowed in intra-pool migration
|
||||
Task.info('VIF mapping is ignored because this is an intra-pool migration')
|
||||
}
|
||||
|
||||
if (vm.$pool !== host.$pool) {
|
||||
const defaultNetworkRef = find(host.$PIFs, pif => pif.management).$network.$ref
|
||||
const defaultNetwork = find(host.$PIFs, pif => pif.management).$network
|
||||
// Add snapshots' VIFs which VM has no VIFs on these devices
|
||||
const vmVifs = vm.$VIFs
|
||||
const vifDevices = new Set(vmVifs.map(_ => _.device))
|
||||
@@ -586,10 +608,10 @@ export default class Xapi extends XapiBase {
|
||||
.filter(vif => !vifDevices.has(vif.device))
|
||||
.concat(vmVifs)
|
||||
for (const vif of vifs) {
|
||||
vifsMap[vif.$ref] =
|
||||
mapVifsNetworks && mapVifsNetworks[vif.$id]
|
||||
? hostXapi.getObject(mapVifsNetworks[vif.$id]).$ref
|
||||
: defaultNetworkRef
|
||||
const migrationNetwork =
|
||||
mapVifsNetworks && mapVifsNetworks[vif.$id] ? hostXapi.getObject(mapVifsNetworks[vif.$id]) : defaultNetwork
|
||||
this.#vmMigrationNetworkPreCheck(migrationNetwork, host)
|
||||
vifsMap[vif.$ref] = migrationNetwork.$ref
|
||||
}
|
||||
}
|
||||
|
||||
@@ -845,6 +867,68 @@ export default class Xapi extends XapiBase {
|
||||
throw new Error(`unsupported type: '${type}'`)
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {XenApiNetworkWrapped} network
|
||||
* @param {XenApiHostWrapped} host
|
||||
*/
|
||||
#vmMigrationNetworkPreCheck(network, host) {
|
||||
const targetXapi = host.$xapi
|
||||
|
||||
let targetPif
|
||||
network.PIFs.forEach(pifRef => {
|
||||
const pif = targetXapi.getObject(pifRef)
|
||||
if (pif.host === host.$ref) {
|
||||
targetPif = pif
|
||||
}
|
||||
})
|
||||
|
||||
if (targetPif === undefined) {
|
||||
Task.warning('host have no PIF in the network', {
|
||||
host: host.uuid,
|
||||
network: network.uuid,
|
||||
})
|
||||
return
|
||||
}
|
||||
|
||||
if (!targetPif.attached) {
|
||||
Task.warning('PIF is not attached', {
|
||||
pif: targetPif.uuid,
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {XenApiSrWrapped} sr
|
||||
* @param {XenApiHostWrapped} host
|
||||
*/
|
||||
#vmMigrationSrPreCheck(sr, host) {
|
||||
const targetXapi = host.$xapi
|
||||
|
||||
let targetPbd
|
||||
if (!sr.shared) {
|
||||
sr.PBDs.forEach(pbdRef => {
|
||||
const pbd = targetXapi.getObject(pbdRef)
|
||||
if (pbd.host === host.$ref) {
|
||||
targetPbd = pbd
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
if (targetPbd === undefined) {
|
||||
Task.warning('host have no PBD in the SR', {
|
||||
host: host.uuid,
|
||||
sr: sr.uuid,
|
||||
})
|
||||
return
|
||||
}
|
||||
|
||||
if (!targetPbd.currently_attached) {
|
||||
Task.warning('PBD is not attached', {
|
||||
pbd: targetPbd.uuid,
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
async migrateVm(
|
||||
vmId,
|
||||
hostXapi,
|
||||
@@ -853,18 +937,26 @@ export default class Xapi extends XapiBase {
|
||||
) {
|
||||
const vm = this.getObject(vmId)
|
||||
const host = hostXapi.getObject(hostId)
|
||||
const targetPool = hostXapi.getObject(host.$pool)
|
||||
|
||||
const accrossPools = vm.$pool !== host.$pool
|
||||
const useStorageMotion =
|
||||
accrossPools ||
|
||||
sr !== undefined ||
|
||||
migrationNetworkId !== undefined ||
|
||||
!isEmpty(mapVifsNetworks) ||
|
||||
!isEmpty(mapVdisSrs)
|
||||
const useStorageMotion = accrossPools || sr !== undefined || !isEmpty(mapVifsNetworks) || !isEmpty(mapVdisSrs)
|
||||
|
||||
const defaultMigrationNetworkId = targetPool.other_config['xo:migrationNetwork']
|
||||
const migrationNetwork =
|
||||
migrationNetworkId !== undefined
|
||||
? hostXapi.getObject(migrationNetworkId)
|
||||
: defaultMigrationNetworkId !== undefined
|
||||
? hostXapi.getObject(defaultMigrationNetworkId)
|
||||
: undefined
|
||||
|
||||
if (migrationNetwork !== undefined) {
|
||||
this.#vmMigrationNetworkPreCheck(migrationNetwork, host)
|
||||
}
|
||||
|
||||
if (useStorageMotion) {
|
||||
await this._migrateVmWithStorageMotion(vm, hostXapi, host, {
|
||||
migrationNetwork: migrationNetworkId && hostXapi.getObject(migrationNetworkId),
|
||||
migrationNetwork,
|
||||
sr,
|
||||
mapVdisSrs,
|
||||
mapVifsNetworks,
|
||||
@@ -875,6 +967,7 @@ export default class Xapi extends XapiBase {
|
||||
try {
|
||||
await this.callAsync('VM.pool_migrate', vm.$ref, host.$ref, {
|
||||
force: force ? 'true' : 'false',
|
||||
network: migrationNetwork?.$ref,
|
||||
})
|
||||
} catch (error) {
|
||||
if (error.code !== 'VM_REQUIRES_SR') {
|
||||
@@ -882,7 +975,7 @@ export default class Xapi extends XapiBase {
|
||||
}
|
||||
|
||||
// Retry using motion storage.
|
||||
await this._migrateVmWithStorageMotion(vm, hostXapi, host, { force })
|
||||
await this._migrateVmWithStorageMotion(vm, hostXapi, host, { force, migrationNetwork })
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user