feat(vmware/import): use xva to load base disk
This commit is contained in:
parent
0f1dcda7db
commit
2d047c4fef
1
@xen-orchestra/xva/.npmignore
Symbolic link
1
@xen-orchestra/xva/.npmignore
Symbolic link
@ -0,0 +1 @@
|
||||
../../scripts/npmignore
|
5
@xen-orchestra/xva/_isNotEmptyRef.mjs
Normal file
5
@xen-orchestra/xva/_isNotEmptyRef.mjs
Normal file
@ -0,0 +1,5 @@
|
||||
export function isNotEmptyRef(val) {
|
||||
const EMPTY = 'OpaqueRef:NULL'
|
||||
const PREFIX = 'OpaqueRef:'
|
||||
return val !== EMPTY && typeof val === 'string' && val.startsWith(PREFIX)
|
||||
}
|
42
@xen-orchestra/xva/_toOvaXml.mjs
Normal file
42
@xen-orchestra/xva/_toOvaXml.mjs
Normal file
@ -0,0 +1,42 @@
|
||||
// from package xml-escape
|
||||
function escape(string) {
|
||||
if (string === null || string === undefined) return
|
||||
if (typeof string === 'number') {
|
||||
return string
|
||||
}
|
||||
const map = {
|
||||
'>': '>',
|
||||
'<': '<',
|
||||
"'": ''',
|
||||
'"': '"',
|
||||
'&': '&',
|
||||
}
|
||||
|
||||
const pattern = '([&"<>\'])'
|
||||
return string.replace(new RegExp(pattern, 'g'), function (str, item) {
|
||||
return map[item]
|
||||
})
|
||||
}
|
||||
|
||||
function formatDate(d) {
|
||||
return d.toISOString().replaceAll('-', '').replace('.000Z', 'Z')
|
||||
}
|
||||
|
||||
export default function toOvaXml(obj) {
|
||||
if (Array.isArray(obj)) {
|
||||
return `<value><array><data>${obj.map(val => toOvaXml(val)).join('')}</data></array></value>`
|
||||
}
|
||||
|
||||
if (typeof obj === 'object') {
|
||||
if (obj instanceof Date) {
|
||||
return `<value><dateTime.iso8601>${escape(formatDate(obj))}</dateTime.iso8601></value>`
|
||||
}
|
||||
return `<value><struct>${Object.entries(obj)
|
||||
.map(([key, value]) => `<member><name>${escape(key)}</name>${toOvaXml(value)}</member>`)
|
||||
.join('')}</struct></value>`
|
||||
}
|
||||
if (typeof obj === 'boolean') {
|
||||
return `<value><boolean>${obj ? 1 : 0}</boolean></value>`
|
||||
}
|
||||
return `<value>${escape(obj)}</value>`
|
||||
}
|
39
@xen-orchestra/xva/_writeDisk.mjs
Normal file
39
@xen-orchestra/xva/_writeDisk.mjs
Normal file
@ -0,0 +1,39 @@
|
||||
import { fromCallback } from 'promise-toolbox'
|
||||
import { readChunkStrict } from '@vates/read-chunk'
|
||||
import { XXHash64 } from 'xxhash'
|
||||
|
||||
async function writeBlock(pack, data, name) {
|
||||
await fromCallback.call(pack, pack.entry, { name }, data)
|
||||
const hasher = new XXHash64(0)
|
||||
hasher.update(data)
|
||||
// weirdly, ocaml and xxhash return the bytes in reverse order to each other
|
||||
const hash = hasher.digest().reverse().toString('hex').toUpperCase()
|
||||
await fromCallback.call(pack, pack.entry, { name: `${name}.xxhash` }, Buffer.from(hash, 'utf8'))
|
||||
}
|
||||
export default async function addDisk(pack, vhd, basePath) {
|
||||
let counter = 0
|
||||
let written
|
||||
const chunk_length = 1024 * 1024
|
||||
const empty = Buffer.alloc(chunk_length, 0)
|
||||
const stream = await vhd.rawContent()
|
||||
let lastBlockLength
|
||||
const diskSize = vhd.footer.currentSize
|
||||
let remaining = diskSize
|
||||
while (remaining > 0) {
|
||||
const data = await readChunkStrict(stream, Math.min(chunk_length, remaining))
|
||||
lastBlockLength = data.length
|
||||
remaining -= lastBlockLength
|
||||
|
||||
if (counter === 0 || !data.equals(empty)) {
|
||||
written = true
|
||||
await writeBlock(pack, data, `${basePath}/${('' + counter).padStart(8, '0')}`)
|
||||
} else {
|
||||
written = false
|
||||
}
|
||||
counter++
|
||||
}
|
||||
if (!written) {
|
||||
// last block must be present
|
||||
writeBlock(pack, empty.slice(0, lastBlockLength), `${basePath}/${counter}`)
|
||||
}
|
||||
}
|
154
@xen-orchestra/xva/_writeOvaXml.mjs
Normal file
154
@xen-orchestra/xva/_writeOvaXml.mjs
Normal file
@ -0,0 +1,154 @@
|
||||
import assert from 'node:assert'
|
||||
|
||||
import { fromCallback } from 'promise-toolbox'
|
||||
import { v4 as uuid } from 'uuid'
|
||||
import defaultsDeep from 'lodash.defaultsdeep'
|
||||
|
||||
import { DEFAULT_VBD } from './templates/vbd.mjs'
|
||||
import { DEFAULT_VDI } from './templates/vdi.mjs'
|
||||
import { DEFAULT_VIF } from './templates/vif.mjs'
|
||||
import { DEFAULT_VM } from './templates/vm.mjs'
|
||||
import toOvaXml from './_toOvaXml.mjs'
|
||||
|
||||
export default async function writeOvaXml(
|
||||
pack,
|
||||
{ memory, networks, nCpus, firmware, vdis, vhds, ...vmSnapshot },
|
||||
{ sr, network }
|
||||
) {
|
||||
let refId = 0
|
||||
function nextRef() {
|
||||
return 'Ref:' + String(refId++).padStart(3, '0')
|
||||
}
|
||||
const data = {
|
||||
version: {
|
||||
hostname: 'localhost',
|
||||
date: '2022-01-01',
|
||||
product_version: '8.2.1',
|
||||
product_brand: 'XCP-ng',
|
||||
build_number: 'release/yangtze/master/58',
|
||||
xapi_major: 1,
|
||||
xapi_minor: 20,
|
||||
export_vsn: 2,
|
||||
},
|
||||
objects: [],
|
||||
}
|
||||
const vm = defaultsDeep(
|
||||
{
|
||||
id: nextRef(),
|
||||
// you can pass a full snapshot and nothing more to do
|
||||
snapshot: vmSnapshot,
|
||||
},
|
||||
{
|
||||
// some data need a little more work to be usable
|
||||
// if they are not already in vm
|
||||
snapshot: {
|
||||
HVM_boot_params: {
|
||||
firmware,
|
||||
},
|
||||
memory_static_max: memory,
|
||||
memory_static_min: memory,
|
||||
memory_dynamic_max: memory,
|
||||
memory_dynamic_min: memory,
|
||||
other_config: {
|
||||
mac_seed: uuid(),
|
||||
},
|
||||
uuid: uuid(),
|
||||
VCPUs_at_startup: nCpus,
|
||||
VCPUs_max: nCpus,
|
||||
},
|
||||
},
|
||||
DEFAULT_VM
|
||||
)
|
||||
|
||||
data.objects.push(vm)
|
||||
const srObj = defaultsDeep(
|
||||
{
|
||||
class: 'SR',
|
||||
id: nextRef(),
|
||||
snapshot: sr,
|
||||
},
|
||||
{
|
||||
snapshot: {
|
||||
VDIs: [],
|
||||
},
|
||||
}
|
||||
)
|
||||
|
||||
data.objects.push(srObj)
|
||||
assert.strictEqual(vhds.length, vdis.length)
|
||||
for (let index = 0; index < vhds.length; index++) {
|
||||
const userdevice = index + 1
|
||||
const vhd = vhds[index]
|
||||
const vdi = defaultsDeep(
|
||||
{
|
||||
id: nextRef(),
|
||||
// overwrite SR from an opaqref to a ref:
|
||||
snapshot: { ...vdis[index], SR: srObj.id },
|
||||
},
|
||||
{
|
||||
snapshot: {
|
||||
uuid: uuid(),
|
||||
},
|
||||
},
|
||||
DEFAULT_VDI
|
||||
)
|
||||
|
||||
data.objects.push(vdi)
|
||||
srObj.snapshot.VDIs.push(vdi.id)
|
||||
vhd.ref = vdi.id
|
||||
|
||||
const vbd = defaultsDeep(
|
||||
{
|
||||
id: nextRef(),
|
||||
snapshot: {
|
||||
device: `xvd${String.fromCharCode('a'.charCodeAt(0) + index)}`,
|
||||
uuid: uuid(),
|
||||
userdevice,
|
||||
VM: vm.id,
|
||||
VDI: vdi.id,
|
||||
},
|
||||
},
|
||||
DEFAULT_VBD
|
||||
)
|
||||
data.objects.push(vbd)
|
||||
vdi.snapshot.vbds.push(vbd.id)
|
||||
vm.snapshot.VBDs.push(vbd.id)
|
||||
}
|
||||
|
||||
if (network && networks?.length) {
|
||||
const networkObj = defaultsDeep(
|
||||
{
|
||||
class: 'network',
|
||||
id: nextRef(),
|
||||
snapshot: network,
|
||||
},
|
||||
{
|
||||
snapshot: {
|
||||
vifs: [],
|
||||
},
|
||||
}
|
||||
)
|
||||
data.objects.push(networkObj)
|
||||
let vifIndex = 0
|
||||
for (const sourceNetwork of networks) {
|
||||
const vif = defaultsDeep(
|
||||
{
|
||||
id: nextRef(),
|
||||
snapshot: {
|
||||
device: ++vifIndex,
|
||||
MAC: sourceNetwork.macAddress,
|
||||
MAC_autogenerated: sourceNetwork.isGenerated,
|
||||
uuid: uuid(),
|
||||
VM: vm.id,
|
||||
network: networkObj.id,
|
||||
},
|
||||
},
|
||||
DEFAULT_VIF
|
||||
)
|
||||
data.objects.push(vif)
|
||||
networkObj.snapshot.vifs.push(vif.id)
|
||||
}
|
||||
}
|
||||
const xml = toOvaXml(data)
|
||||
await fromCallback.call(pack, pack.entry, { name: `ova.xml` }, xml)
|
||||
}
|
32
@xen-orchestra/xva/importVdi.mjs
Normal file
32
@xen-orchestra/xva/importVdi.mjs
Normal file
@ -0,0 +1,32 @@
|
||||
import { isNotEmptyRef } from './_isNotEmptyRef.mjs'
|
||||
import { importVm } from './importVm.mjs'
|
||||
|
||||
export async function importVdi(vdi, vhd, xapi, sr) {
|
||||
// create a fake VM
|
||||
const vmRef = await importVm(
|
||||
{
|
||||
name_label: `[xva-disp-import]${vdi.name_label}`,
|
||||
memory: 1024 * 1024 * 32,
|
||||
nCpus: 1,
|
||||
firmware: 'bios',
|
||||
vdis: [vdi],
|
||||
vhds: [vhd],
|
||||
},
|
||||
xapi,
|
||||
sr
|
||||
)
|
||||
// wait for the VM to be loaded if necessary
|
||||
xapi.getObject(vmRef, undefined) ?? (await xapi.waitObject(vmRef))
|
||||
|
||||
const vbdRefs = await xapi.getField('VM', vmRef, 'VBDs')
|
||||
// get the disk
|
||||
const disks = { __proto__: null }
|
||||
;(await xapi.getRecords('VBD', vbdRefs)).forEach(vbd => {
|
||||
if (vbd.type === 'Disk' && isNotEmptyRef(vbd.VDI)) {
|
||||
disks[vbd.VDI] = true
|
||||
}
|
||||
})
|
||||
// destroy the VM and VBD
|
||||
await xapi.call('VM.destroy', vmRef)
|
||||
return await xapi.getRecord('VDI', Object.keys(disks)[0])
|
||||
}
|
33
@xen-orchestra/xva/importVm.mjs
Normal file
33
@xen-orchestra/xva/importVm.mjs
Normal file
@ -0,0 +1,33 @@
|
||||
import tar from 'tar-stream'
|
||||
|
||||
import writeOvaXml from './_writeOvaXml.mjs'
|
||||
import writeDisk from './_writeDisk.mjs'
|
||||
|
||||
export async function importVm(vm, xapi, sr, network) {
|
||||
const pack = tar.pack()
|
||||
const taskRef = await xapi.task_create('VM import')
|
||||
const query = {
|
||||
sr_id: sr.$ref,
|
||||
}
|
||||
|
||||
const promise = xapi
|
||||
.putResource(pack, '/import/', {
|
||||
query,
|
||||
task: taskRef,
|
||||
})
|
||||
.catch(err => console.error(err))
|
||||
|
||||
await writeOvaXml(pack, vm, { sr, network })
|
||||
for (const vhd of vm.vhds) {
|
||||
await writeDisk(pack, vhd, vhd.ref)
|
||||
}
|
||||
pack.finalize()
|
||||
const str = await promise
|
||||
const matches = /OpaqueRef:[0-9a-z-]+/.exec(str)
|
||||
if (!matches) {
|
||||
const error = new Error('no opaque ref found')
|
||||
error.haystack = str
|
||||
throw error
|
||||
}
|
||||
return matches[0]
|
||||
}
|
29
@xen-orchestra/xva/package.json
Normal file
29
@xen-orchestra/xva/package.json
Normal file
@ -0,0 +1,29 @@
|
||||
{
|
||||
"name": "@xen-orchestra/xva-generator",
|
||||
"version": "1.0.0",
|
||||
"main": "index.js",
|
||||
"author": "",
|
||||
"license": "ISC",
|
||||
"private": false,
|
||||
"homepage": "https://github.com/vatesfr/xen-orchestra/tree/master/@xen-orchestra/xva-generator",
|
||||
"bugs": "https://github.com/vatesfr/xen-orchestra/issues",
|
||||
"repository": {
|
||||
"directory": "@xen-orchestra/xva-generator",
|
||||
"type": "git",
|
||||
"url": "https://github.com/vatesfr/xen-orchestra.git"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=14.0"
|
||||
},
|
||||
"dependencies": {
|
||||
"@vates/read-chunk": "^1.2.0",
|
||||
"lodash.defaultsdeep": "^4.6.1",
|
||||
"promise-toolbox": "^0.21.0",
|
||||
"tar-stream": "^3.1.6",
|
||||
"uuid": "^9.0.0",
|
||||
"xxhash": "^0.3.0"
|
||||
},
|
||||
"scripts": {
|
||||
"postversion": "npm publish --access public"
|
||||
}
|
||||
}
|
22
@xen-orchestra/xva/templates/vbd.mjs
Normal file
22
@xen-orchestra/xva/templates/vbd.mjs
Normal file
@ -0,0 +1,22 @@
|
||||
export const DEFAULT_VBD = {
|
||||
class: 'VBD',
|
||||
snapshot: {
|
||||
allowed_operations: [],
|
||||
bootable: true, // @todo : fix it
|
||||
current_operations: {},
|
||||
currently_attached: false,
|
||||
empty: false,
|
||||
metrics: 'OpaqueRef:NULL',
|
||||
mode: 'RW',
|
||||
other_config: {},
|
||||
qos_algorithm_params: {},
|
||||
qos_algorithm_type: '',
|
||||
qos_supported_algorithms: [],
|
||||
runtime_properties: {},
|
||||
status_code: 0,
|
||||
status_detail: '',
|
||||
storage_lock: false,
|
||||
type: 'Disk',
|
||||
unpluggable: false,
|
||||
},
|
||||
}
|
29
@xen-orchestra/xva/templates/vdi.mjs
Normal file
29
@xen-orchestra/xva/templates/vdi.mjs
Normal file
@ -0,0 +1,29 @@
|
||||
export const DEFAULT_VDI = {
|
||||
class: 'VDI',
|
||||
snapshot: {
|
||||
allow_caching: false,
|
||||
cbt_enabled: false,
|
||||
descriptionLabel: 'description',
|
||||
is_a_snapshot: false,
|
||||
managed: true,
|
||||
metrics: 'OpaqueRef:NULL',
|
||||
missing: false,
|
||||
name_label: 'name_label',
|
||||
on_boot: 'persist',
|
||||
other_config: {},
|
||||
parent: 'OpaqueRef:NULL',
|
||||
physical_utilisation: 1024 * 1024,
|
||||
read_only: false,
|
||||
sharable: false,
|
||||
snapshot_of: 'OpaqueRef:NULL',
|
||||
snapshots: [],
|
||||
SR: 'OpaqueRef:NULL',
|
||||
storage_lock: false,
|
||||
tags: [],
|
||||
type: 'user',
|
||||
uuid: '',
|
||||
vbds: [],
|
||||
virtual_size: 0,
|
||||
xenstore_data: {},
|
||||
},
|
||||
}
|
26
@xen-orchestra/xva/templates/vif.mjs
Normal file
26
@xen-orchestra/xva/templates/vif.mjs
Normal file
@ -0,0 +1,26 @@
|
||||
export const DEFAULT_VIF = {
|
||||
class: 'VIF',
|
||||
snapshot: {
|
||||
allowed_operations: [],
|
||||
currently_attached: false,
|
||||
current_operations: {},
|
||||
ipv4_addresses: [],
|
||||
ipv4_allowed: [],
|
||||
ipv4_configuration_mode: 'None',
|
||||
ipv4_gateway: '',
|
||||
ipv6_addresses: [],
|
||||
ipv6_allowed: [],
|
||||
ipv6_configuration_mode: 'None',
|
||||
ipv6_gateway: '',
|
||||
locking_mode: 'network_default',
|
||||
MTU: 1500,
|
||||
metrics: 'OpaqueRef:NULL',
|
||||
other_config: {},
|
||||
qos_algorithm_params: {},
|
||||
qos_algorithm_type: '',
|
||||
qos_supported_algorithms: [],
|
||||
runtime_properties: {},
|
||||
status_code: 0,
|
||||
status_detail: '',
|
||||
},
|
||||
}
|
106
@xen-orchestra/xva/templates/vm.mjs
Normal file
106
@xen-orchestra/xva/templates/vm.mjs
Normal file
@ -0,0 +1,106 @@
|
||||
export const DEFAULT_VM = {
|
||||
class: 'VM',
|
||||
id: null,
|
||||
snapshot: {
|
||||
actions_after_crash: 'restart',
|
||||
actions_after_reboot: 'restart',
|
||||
actions_after_shutdown: 'destroy',
|
||||
affinity: 'Ref:53',
|
||||
allowed_operations: [],
|
||||
// appliance:'OpaqueRef:NULL',
|
||||
attached_PCIs: [],
|
||||
blobs: {},
|
||||
blocked_operations: {},
|
||||
children: [],
|
||||
consoles: [],
|
||||
crash_dumps: [],
|
||||
current_operations: {},
|
||||
domain_type: 'hvm',
|
||||
domarch: '',
|
||||
domid: -1,
|
||||
generation_id: '',
|
||||
guest_metrics: 'Ref:53',
|
||||
ha_always_run: false,
|
||||
ha_restart_priority: '',
|
||||
hardware_platform_version: 0,
|
||||
has_vendor_device: false,
|
||||
HVM_boot_params: {
|
||||
firmware: 'bios',
|
||||
order: 'dcn',
|
||||
},
|
||||
HVM_boot_policy: 'BIOS order',
|
||||
HVM_shadow_multiplier: 1,
|
||||
is_a_template: false,
|
||||
is_control_domain: false,
|
||||
is_default_template: false,
|
||||
is_snapshot_from_vmpp: false,
|
||||
is_vmss_snapshot: false,
|
||||
last_booted_record: '',
|
||||
memory_dynamic_max: 1,
|
||||
memory_dynamic_min: 1,
|
||||
memory_overhead: 11534336,
|
||||
memory_static_max: 1,
|
||||
memory_static_min: 1,
|
||||
memory_target: 0,
|
||||
metrics: 'OpaqueRef:NULL',
|
||||
name_label: 'from xva',
|
||||
NVRAM: {},
|
||||
name_description: ' from xva',
|
||||
order: 0,
|
||||
other_config: {
|
||||
base_template_name: 'Other install media',
|
||||
// mac_seed,
|
||||
'install-methods': 'cdrom',
|
||||
},
|
||||
parent: 'OpaqueRef:NULL',
|
||||
PCI_bus: '',
|
||||
platform: {
|
||||
timeoffset: 1,
|
||||
'device-model': 'qemu-upstream-compat',
|
||||
secureboot: 'false',
|
||||
hpet: 'true',
|
||||
nx: 'true',
|
||||
pae: 'true',
|
||||
apic: 'true',
|
||||
viridian: 'true',
|
||||
acpi: 1,
|
||||
},
|
||||
power_state: 'halted',
|
||||
// protection_policy:'OpaqueRef:NULL',
|
||||
PV_args: '',
|
||||
PV_bootloader_args: '',
|
||||
PV_bootloader: '',
|
||||
PV_kernel: '',
|
||||
PV_legacy_args: '',
|
||||
PV_ramdisk: '',
|
||||
recommendations: '',
|
||||
reference_label: 'other-install-media',
|
||||
requires_reboot: false,
|
||||
resident_on: 'Ref:53',
|
||||
// scheduled_to_be_resident_on:'OpaqueRef:NULL',
|
||||
shutdown_delay: 0,
|
||||
// snapshot_schedule: 'OpaqueRef:NULL',
|
||||
snapshot_info: {},
|
||||
snapshot_metadata: '',
|
||||
snapshot_of: 'OpaqueRef:NULL',
|
||||
snapshot_time: new Date(0),
|
||||
snapshots: [],
|
||||
start_delay: 0,
|
||||
// suspend_VDI:'OpaqueRef:NULL',
|
||||
// suspend_SR:'OpaqueRef:NULL',
|
||||
tags: [],
|
||||
transportable_snapshot_id: '',
|
||||
// uuid,
|
||||
user_version: 1,
|
||||
VBDs: [],
|
||||
VCPUs_at_startup: 1,
|
||||
VCPUs_max: 1,
|
||||
VCPUs_params: {},
|
||||
version: 0,
|
||||
VGPUs: [],
|
||||
VIFs: [],
|
||||
VTPMs: [],
|
||||
VUSBs: [],
|
||||
xenstore_data: {},
|
||||
},
|
||||
}
|
@ -8,6 +8,7 @@
|
||||
> Users must be able to say: “Nice enhancement, I'm eager to test it”
|
||||
|
||||
- [SR] Possibility to create SMB shared SR [#991](https://github.com/vatesfr/xen-orchestra/issues/991) (PR [#7330](https://github.com/vatesfr/xen-orchestra/pull/7330))
|
||||
- [Import/VMWare] Speed up import and make all imports thin [#7323](https://github.com/vatesfr/xen-orchestra/issues/7323)
|
||||
|
||||
### Bug fixes
|
||||
|
||||
@ -35,6 +36,7 @@
|
||||
|
||||
- @xen-orchestra/backups patch
|
||||
- @xen-orchestra/vmware-explorer minor
|
||||
- @xen-orchestra/xva major
|
||||
- xo-server minor
|
||||
- xo-web minor
|
||||
|
||||
|
@ -1382,19 +1382,9 @@ import_.resolve = {
|
||||
|
||||
export { import_ as import }
|
||||
|
||||
export async function importFromEsxi({
|
||||
host,
|
||||
network,
|
||||
password,
|
||||
sr,
|
||||
sslVerify = true,
|
||||
stopSource = false,
|
||||
thin = false,
|
||||
user,
|
||||
vm,
|
||||
}) {
|
||||
export async function importFromEsxi({ host, network, password, sr, sslVerify = true, stopSource = false, user, vm }) {
|
||||
const task = await this.tasks.create({ name: `importing vm ${vm}` })
|
||||
return task.run(() => this.migrationfromEsxi({ host, user, password, sslVerify, thin, vm, sr, network, stopSource }))
|
||||
return task.run(() => this.migrationfromEsxi({ host, user, password, sslVerify, vm, sr, network, stopSource }))
|
||||
}
|
||||
|
||||
importFromEsxi.params = {
|
||||
@ -1404,7 +1394,6 @@ importFromEsxi.params = {
|
||||
sr: { type: 'string' },
|
||||
sslVerify: { type: 'boolean', optional: true },
|
||||
stopSource: { type: 'boolean', optional: true },
|
||||
thin: { type: 'boolean', optional: true },
|
||||
user: { type: 'string' },
|
||||
vm: { type: 'string' },
|
||||
}
|
||||
|
@ -4,12 +4,13 @@ import { fromEvent } from 'promise-toolbox'
|
||||
import { createRunner } from '@xen-orchestra/backups/Backup.mjs'
|
||||
import { Task } from '@xen-orchestra/mixins/Tasks.mjs'
|
||||
import { v4 as generateUuid } from 'uuid'
|
||||
import { VDI_FORMAT_RAW, VDI_FORMAT_VHD } from '@xen-orchestra/xapi'
|
||||
import { VDI_FORMAT_VHD } from '@xen-orchestra/xapi'
|
||||
import asyncMapSettled from '@xen-orchestra/async-map/legacy.js'
|
||||
import Esxi from '@xen-orchestra/vmware-explorer/esxi.mjs'
|
||||
import openDeltaVmdkasVhd from '@xen-orchestra/vmware-explorer/openDeltaVmdkAsVhd.mjs'
|
||||
import OTHER_CONFIG_TEMPLATE from '../xapi/other-config-template.mjs'
|
||||
import VhdEsxiRaw from '@xen-orchestra/vmware-explorer/VhdEsxiRaw.mjs'
|
||||
import { importVdi as importVdiThroughXva } from '@xen-orchestra/xva/importVdi.mjs'
|
||||
|
||||
export default class MigrateVm {
|
||||
constructor(app) {
|
||||
@ -169,7 +170,7 @@ export default class MigrateVm {
|
||||
@decorateWith(deferrable)
|
||||
async migrationfromEsxi(
|
||||
$defer,
|
||||
{ host, user, password, sslVerify, sr: srId, network: networkId, vm: vmId, thin, stopSource }
|
||||
{ host, user, password, sslVerify, sr: srId, network: networkId, vm: vmId, stopSource }
|
||||
) {
|
||||
const app = this._app
|
||||
const esxi = await this.#connectToEsxi(host, user, password, sslVerify)
|
||||
@ -220,7 +221,7 @@ export default class MigrateVm {
|
||||
xapi.VIF_create(
|
||||
{
|
||||
device: vifDevices[i],
|
||||
network: xapi.getObject(networkId).$ref,
|
||||
network: app.getXapiObject(networkId).$ref,
|
||||
VM: vm.$ref,
|
||||
},
|
||||
{
|
||||
@ -231,29 +232,13 @@ export default class MigrateVm {
|
||||
)
|
||||
return vm
|
||||
})
|
||||
|
||||
$defer.onFailure.call(xapi, 'VM_destroy', vm.$ref)
|
||||
|
||||
const vhds = await Promise.all(
|
||||
Object.keys(chainsByNodes).map(async (node, userdevice) =>
|
||||
Task.run({ properties: { name: `Cold import of disks ${node}` } }, async () => {
|
||||
const chainByNode = chainsByNodes[node]
|
||||
const vdi = await xapi._getOrWaitObject(
|
||||
await xapi.VDI_create({
|
||||
name_description: 'fromESXI' + chainByNode[0].descriptionLabel,
|
||||
name_label: '[ESXI]' + chainByNode[0].nameLabel,
|
||||
SR: sr.$ref,
|
||||
virtual_size: chainByNode[0].capacity,
|
||||
})
|
||||
)
|
||||
// it can fail before the vdi is connected to the vm
|
||||
|
||||
$defer.onFailure.call(xapi, 'VDI_destroy', vdi.$ref)
|
||||
|
||||
await xapi.VBD_create({
|
||||
VDI: vdi.$ref,
|
||||
VM: vm.$ref,
|
||||
})
|
||||
let vdi
|
||||
let parentVhd, vhd
|
||||
// if the VM is running we'll transfer everything before the last , which is an active disk
|
||||
// the esxi api does not allow us to read an active disk
|
||||
@ -262,27 +247,37 @@ export default class MigrateVm {
|
||||
for (let diskIndex = 0; diskIndex < nbColdDisks; diskIndex++) {
|
||||
// the first one is a RAW disk ( full )
|
||||
const disk = chainByNode[diskIndex]
|
||||
const { fileName, path, datastore, isFull } = disk
|
||||
const { capacity, descriptionLabel, fileName, nameLabel, path, datastore, isFull } = disk
|
||||
if (isFull) {
|
||||
vhd = await VhdEsxiRaw.open(esxi, datastore, path + '/' + fileName, { thin })
|
||||
await vhd.readBlockAllocationTable()
|
||||
vhd = await VhdEsxiRaw.open(esxi, datastore, path + '/' + fileName)
|
||||
// we don't need to read the BAT with the importVdiThroughXva process
|
||||
const vdiMetadata = {
|
||||
name_description: 'fromESXI' + descriptionLabel,
|
||||
name_label: '[ESXI]' + nameLabel,
|
||||
SR: sr.$ref,
|
||||
virtual_size: capacity,
|
||||
}
|
||||
vdi = await importVdiThroughXva(vdiMetadata, vhd, xapi, sr)
|
||||
// it can fail before the vdi is connected to the vm
|
||||
$defer.onFailure.call(xapi, 'VDI_destroy', vdi.$ref)
|
||||
await xapi.VBD_create({
|
||||
VDI: vdi.$ref,
|
||||
VM: vm.$ref,
|
||||
})
|
||||
} else {
|
||||
vhd = await openDeltaVmdkasVhd(esxi, datastore, path + '/' + fileName, parentVhd)
|
||||
vhd = await openDeltaVmdkasVhd(esxi, datastore, path + '/' + fileName, parentVhd, {
|
||||
lookMissingBlockInParent: false,
|
||||
})
|
||||
}
|
||||
vhd.label = fileName
|
||||
parentVhd = vhd
|
||||
}
|
||||
// it can be empty if the VM don't have a snapshot and is running
|
||||
if (vhd !== undefined) {
|
||||
if (thin) {
|
||||
const stream = vhd.stream()
|
||||
await vdi.$importContent(stream, { format: VDI_FORMAT_VHD })
|
||||
} else {
|
||||
// no transformation when there is no snapshot in thick mode
|
||||
const stream = await vhd.rawContent()
|
||||
await vdi.$importContent(stream, { format: VDI_FORMAT_RAW })
|
||||
}
|
||||
if (nbColdDisks > 1 /* got a cold snapshot chain */) {
|
||||
// it can be empty if the VM don't have a snapshot and is running
|
||||
const stream = vhd.stream()
|
||||
await vdi.$importContent(stream, { format: VDI_FORMAT_VHD })
|
||||
}
|
||||
return { vdi, vhd }
|
||||
return vhd
|
||||
})
|
||||
)
|
||||
)
|
||||
@ -296,15 +291,28 @@ export default class MigrateVm {
|
||||
await Task.run({ properties: { name: `Transfering deltas of ${userdevice}` } }, async () => {
|
||||
const chainByNode = chainsByNodes[node]
|
||||
const disk = chainByNode[chainByNode.length - 1]
|
||||
const { fileName, path, datastore, isFull } = disk
|
||||
const { vdi, vhd: parentVhd } = vhds[userdevice]
|
||||
const { capacity, descriptionLabel, fileName, nameLabel, path, datastore, isFull } = disk
|
||||
let { vdi, vhd: parentVhd } = vhds[userdevice]
|
||||
let vhd
|
||||
if (vdi === undefined) {
|
||||
throw new Error(`Can't import delta of a running VM without its parent vdi`)
|
||||
}
|
||||
if (isFull) {
|
||||
vhd = await VhdEsxiRaw.open(esxi, datastore, path + '/' + fileName, { thin })
|
||||
await vhd.readBlockAllocationTable()
|
||||
vhd = await VhdEsxiRaw.open(esxi, datastore, path + '/' + fileName, { thin: false })
|
||||
// we don't need to read the BAT with the importVdiThroughXva process
|
||||
const vdiMetadata = {
|
||||
name_description: 'fromESXI' + descriptionLabel,
|
||||
name_label: '[ESXI]' + nameLabel,
|
||||
SR: sr.$ref,
|
||||
virtual_size: capacity,
|
||||
}
|
||||
vdi = await importVdiThroughXva(vdiMetadata, vhd, xapi, sr)
|
||||
// it can fail before the vdi is connected to the vm
|
||||
$defer.onFailure.call(xapi, 'VDI_destroy', vdi.$ref)
|
||||
await xapi.VBD_create({
|
||||
VDI: vdi.$ref,
|
||||
VM: vm.$ref,
|
||||
})
|
||||
} else {
|
||||
if (parentVhd === undefined) {
|
||||
throw new Error(`Can't import delta of a running VM without its parent VHD`)
|
||||
|
14
yarn.lock
14
yarn.lock
@ -14483,6 +14483,11 @@ lodash.debounce@^4.0.8:
|
||||
resolved "https://registry.yarnpkg.com/lodash.debounce/-/lodash.debounce-4.0.8.tgz#82d79bff30a67c4005ffd5e2515300ad9ca4d7af"
|
||||
integrity sha512-FT1yDzDYEoYWhnSGnpE/4Kj1fLZkDFyqRb7fNt6FdYOSxlUWAtp42Eh6Wb0rGIv/m9Bgo7x4GhQbm5Ys4SG5ow==
|
||||
|
||||
lodash.defaultsdeep@^4.6.1:
|
||||
version "4.6.1"
|
||||
resolved "https://registry.yarnpkg.com/lodash.defaultsdeep/-/lodash.defaultsdeep-4.6.1.tgz#512e9bd721d272d94e3d3a63653fa17516741ca6"
|
||||
integrity sha512-3j8wdDzYuWO3lM3Reg03MuQR957t287Rpcxp1njpEa8oDrikb+FwGdW3n+FELh/A6qib6yPit0j/pv9G/yeAqA==
|
||||
|
||||
lodash.escape@^3.0.0:
|
||||
version "3.2.0"
|
||||
resolved "https://registry.yarnpkg.com/lodash.escape/-/lodash.escape-3.2.0.tgz#995ee0dc18c1b48cc92effae71a10aab5b487698"
|
||||
@ -15454,7 +15459,7 @@ mute-stream@0.0.8:
|
||||
resolved "https://registry.yarnpkg.com/mute-stream/-/mute-stream-0.0.8.tgz#1630c42b2251ff81e2a283de96a5497ea92e5e0d"
|
||||
integrity sha512-nnbWWOkoWyUsTjKrhgD0dcz22mdkSnpYqbEjIm2nhwhuxlSkpywJmBo8h0ZqJdkp73mb90SssHkN4rsRaBAfAA==
|
||||
|
||||
nan@^2.12.1:
|
||||
nan@^2.12.1, nan@^2.13.2:
|
||||
version "2.18.0"
|
||||
resolved "https://registry.yarnpkg.com/nan/-/nan-2.18.0.tgz#26a6faae7ffbeb293a39660e88a76b82e30b7554"
|
||||
integrity sha512-W7tfG7vMOGtD30sHoZSSc/JVYiyDPEyQVso/Zz+/uQd0B0L46gtC+pHha5FFMRpil6fm/AoEcRWyOVi4+E/f8w==
|
||||
@ -22422,6 +22427,13 @@ xtend@~2.1.1:
|
||||
dependencies:
|
||||
object-keys "~0.4.0"
|
||||
|
||||
xxhash@^0.3.0:
|
||||
version "0.3.0"
|
||||
resolved "https://registry.yarnpkg.com/xxhash/-/xxhash-0.3.0.tgz#d20893a62c5b0f7260597dd55859b12a1e02c559"
|
||||
integrity sha512-1ud2yyPiR1DJhgyF1ZVMt+Ijrn0VNS/wzej1Z8eSFfkNfRPp8abVZNV2u9tYy9574II0ZayZYZgJm8KJoyGLCw==
|
||||
dependencies:
|
||||
nan "^2.13.2"
|
||||
|
||||
xxhashjs@^0.2.1:
|
||||
version "0.2.2"
|
||||
resolved "https://registry.yarnpkg.com/xxhashjs/-/xxhashjs-0.2.2.tgz#8a6251567621a1c46a5ae204da0249c7f8caa9d8"
|
||||
|
Loading…
Reference in New Issue
Block a user