Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat(xo-server): implement warm migration backend #6549

Merged
merged 2 commits into from
Nov 28, 2022
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
16 changes: 16 additions & 0 deletions packages/xo-server/src/api/vm.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -596,6 +596,22 @@ migrate.resolve = {
migrationNetwork: ['migrationNetwork', 'network', 'administrate'],
}

export async function warmMigration({ vm, sr, startVm, deleteSource }) {
await this.warmMigrateVm(vm, sr, startVm, deleteSource)
}
warmMigration.permission = 'admin'

warmMigration.params = {
vm: {
type: 'string',
},
sr: {
type: 'string',
},
startDestinationVm: { type: 'boolean' },
deleteSourceVm: { type: 'boolean' },
}

// -------------------------------------------------------------------

export const set = defer(async function ($defer, params) {
Expand Down
109 changes: 109 additions & 0 deletions packages/xo-server/src/xo-mixins/migrate-vm.mjs
Original file line number Diff line number Diff line change
@@ -0,0 +1,109 @@
import { Backup } from '@xen-orchestra/backups/Backup.js'
import { uuid } from 'uuid'

export default class MigrateVm {
constructor(app) {
this._app = app
}

// Backup should be reinstentiated each time
#createWarmBackup(sourceVmId, srId, jobId) {
const app = this._app
const config = {
snapshotNameLabelTpl: '[XO warm migration {job.name}] {vm.name_label}',
}
const job = {
type: 'backup',
id: jobId,
mode: 'delta',
vms: { id: sourceVmId },
name: `Warm migration`,
srs: { id: srId },
settings: {
'': {
// mandatory for delta replication writer
copyRetention: 1,
},
},
}
const schedule = { id: 'one-time' }

// for now we only support this from the main OA, no proxy
return new Backup({
config,
job,
schedule,
getAdapter: async remoteId => app.getBackupsRemoteAdapter(await app.getRemoteWithCredentials(remoteId)),

// `@xen-orchestra/backups/Backup` expect that `getConnectedRecord` returns a promise
getConnectedRecord: async (xapiType, uuid) => app.getXapiObject(uuid),
})
}

async warmMigrateVm(sourceVmId, srId, startDestVm = true, deleteSource = false) {
// we'll use a one time use continuous replication job with the VM to migrate
const jobId = uuid.v4()
const app = this._app
const sourceVm = app.getXapiObject(sourceVmId)
let backup = this.#createWarmBackup(sourceVmId, srId, jobId)
await backup.run()
const xapi = sourceVm.$xapi
const ref = sourceVm.$ref

// stop the source VM before
try {
await xapi.callAsync('VM.clean_shutdown', ref)
} catch (error) {
await xapi.callAsync('VM.hard_shutdown', ref)
}
// make it so it can't be restarted by error
const message =
'This VM has been migrated somewhere else and might not be up to date, check twice before starting it.'
await sourceVm.update_blocked_operations({
start: message,
start_on: message,
})

// run the transfer again to transfer the changed parts
// since the source is stopped, there won't be any new change after
backup = this.#createWarmBackup(sourceVmId, srId)
await backup.run()
// find the destination Vm
const targets = Object.keys(
app.getObjects({
filter: obj => {
return (
'other' in obj &&
obj.other['xo:backup:job'] === jobId &&
obj.other['xo:backup:sr'] === srId &&
obj.other['xo:backup:vm'] === sourceVm.uuid &&
'start' in obj.blockedOperations
)
},
})
)
if (targets.length === 0) {
throw new Error(`Vm target of warm migration not found for ${sourceVmId} on SR ${srId} `)
}
if (targets.length > 1) {
throw new Error(`Multiple target of warm migration found for ${sourceVmId} on SR ${srId} `)
}
const targetVm = app.getXapiObject(targets[0])

// new vm is ready to start
// delta replication writer as set this as blocked
await targetVm.update_blocked_operations({ start: null, start_on: null })

if (startDestVm) {
// boot it
await targetVm.$xapi.startVm(targetVm.$ref)
// wait for really started
// delete source
if (deleteSource) {
sourceVm.$xapi.VM_destroy(sourceVm.$ref)
} else {
// @todo should we delete the snapshot if we keep the source vm ?
}
}
}
}