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”
|
> 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))
|
- [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
|
### Bug fixes
|
||||||
|
|
||||||
@ -35,6 +36,7 @@
|
|||||||
|
|
||||||
- @xen-orchestra/backups patch
|
- @xen-orchestra/backups patch
|
||||||
- @xen-orchestra/vmware-explorer minor
|
- @xen-orchestra/vmware-explorer minor
|
||||||
|
- @xen-orchestra/xva major
|
||||||
- xo-server minor
|
- xo-server minor
|
||||||
- xo-web minor
|
- xo-web minor
|
||||||
|
|
||||||
|
@ -1382,19 +1382,9 @@ import_.resolve = {
|
|||||||
|
|
||||||
export { import_ as import }
|
export { import_ as import }
|
||||||
|
|
||||||
export async function importFromEsxi({
|
export async function importFromEsxi({ host, network, password, sr, sslVerify = true, stopSource = false, user, vm }) {
|
||||||
host,
|
|
||||||
network,
|
|
||||||
password,
|
|
||||||
sr,
|
|
||||||
sslVerify = true,
|
|
||||||
stopSource = false,
|
|
||||||
thin = false,
|
|
||||||
user,
|
|
||||||
vm,
|
|
||||||
}) {
|
|
||||||
const task = await this.tasks.create({ name: `importing vm ${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 = {
|
importFromEsxi.params = {
|
||||||
@ -1404,7 +1394,6 @@ importFromEsxi.params = {
|
|||||||
sr: { type: 'string' },
|
sr: { type: 'string' },
|
||||||
sslVerify: { type: 'boolean', optional: true },
|
sslVerify: { type: 'boolean', optional: true },
|
||||||
stopSource: { type: 'boolean', optional: true },
|
stopSource: { type: 'boolean', optional: true },
|
||||||
thin: { type: 'boolean', optional: true },
|
|
||||||
user: { type: 'string' },
|
user: { type: 'string' },
|
||||||
vm: { type: 'string' },
|
vm: { type: 'string' },
|
||||||
}
|
}
|
||||||
|
@ -4,12 +4,13 @@ import { fromEvent } from 'promise-toolbox'
|
|||||||
import { createRunner } from '@xen-orchestra/backups/Backup.mjs'
|
import { createRunner } from '@xen-orchestra/backups/Backup.mjs'
|
||||||
import { Task } from '@xen-orchestra/mixins/Tasks.mjs'
|
import { Task } from '@xen-orchestra/mixins/Tasks.mjs'
|
||||||
import { v4 as generateUuid } from 'uuid'
|
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 asyncMapSettled from '@xen-orchestra/async-map/legacy.js'
|
||||||
import Esxi from '@xen-orchestra/vmware-explorer/esxi.mjs'
|
import Esxi from '@xen-orchestra/vmware-explorer/esxi.mjs'
|
||||||
import openDeltaVmdkasVhd from '@xen-orchestra/vmware-explorer/openDeltaVmdkAsVhd.mjs'
|
import openDeltaVmdkasVhd from '@xen-orchestra/vmware-explorer/openDeltaVmdkAsVhd.mjs'
|
||||||
import OTHER_CONFIG_TEMPLATE from '../xapi/other-config-template.mjs'
|
import OTHER_CONFIG_TEMPLATE from '../xapi/other-config-template.mjs'
|
||||||
import VhdEsxiRaw from '@xen-orchestra/vmware-explorer/VhdEsxiRaw.mjs'
|
import VhdEsxiRaw from '@xen-orchestra/vmware-explorer/VhdEsxiRaw.mjs'
|
||||||
|
import { importVdi as importVdiThroughXva } from '@xen-orchestra/xva/importVdi.mjs'
|
||||||
|
|
||||||
export default class MigrateVm {
|
export default class MigrateVm {
|
||||||
constructor(app) {
|
constructor(app) {
|
||||||
@ -169,7 +170,7 @@ export default class MigrateVm {
|
|||||||
@decorateWith(deferrable)
|
@decorateWith(deferrable)
|
||||||
async migrationfromEsxi(
|
async migrationfromEsxi(
|
||||||
$defer,
|
$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 app = this._app
|
||||||
const esxi = await this.#connectToEsxi(host, user, password, sslVerify)
|
const esxi = await this.#connectToEsxi(host, user, password, sslVerify)
|
||||||
@ -220,7 +221,7 @@ export default class MigrateVm {
|
|||||||
xapi.VIF_create(
|
xapi.VIF_create(
|
||||||
{
|
{
|
||||||
device: vifDevices[i],
|
device: vifDevices[i],
|
||||||
network: xapi.getObject(networkId).$ref,
|
network: app.getXapiObject(networkId).$ref,
|
||||||
VM: vm.$ref,
|
VM: vm.$ref,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
@ -231,29 +232,13 @@ export default class MigrateVm {
|
|||||||
)
|
)
|
||||||
return vm
|
return vm
|
||||||
})
|
})
|
||||||
|
|
||||||
$defer.onFailure.call(xapi, 'VM_destroy', vm.$ref)
|
$defer.onFailure.call(xapi, 'VM_destroy', vm.$ref)
|
||||||
|
|
||||||
const vhds = await Promise.all(
|
const vhds = await Promise.all(
|
||||||
Object.keys(chainsByNodes).map(async (node, userdevice) =>
|
Object.keys(chainsByNodes).map(async (node, userdevice) =>
|
||||||
Task.run({ properties: { name: `Cold import of disks ${node}` } }, async () => {
|
Task.run({ properties: { name: `Cold import of disks ${node}` } }, async () => {
|
||||||
const chainByNode = chainsByNodes[node]
|
const chainByNode = chainsByNodes[node]
|
||||||
const vdi = await xapi._getOrWaitObject(
|
let vdi
|
||||||
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 parentVhd, vhd
|
let parentVhd, vhd
|
||||||
// if the VM is running we'll transfer everything before the last , which is an active disk
|
// 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
|
// 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++) {
|
for (let diskIndex = 0; diskIndex < nbColdDisks; diskIndex++) {
|
||||||
// the first one is a RAW disk ( full )
|
// the first one is a RAW disk ( full )
|
||||||
const disk = chainByNode[diskIndex]
|
const disk = chainByNode[diskIndex]
|
||||||
const { fileName, path, datastore, isFull } = disk
|
const { capacity, descriptionLabel, fileName, nameLabel, path, datastore, isFull } = disk
|
||||||
if (isFull) {
|
if (isFull) {
|
||||||
vhd = await VhdEsxiRaw.open(esxi, datastore, path + '/' + fileName, { thin })
|
vhd = await VhdEsxiRaw.open(esxi, datastore, path + '/' + fileName)
|
||||||
await vhd.readBlockAllocationTable()
|
// we don't need to read the BAT with the importVdiThroughXva process
|
||||||
} else {
|
const vdiMetadata = {
|
||||||
vhd = await openDeltaVmdkasVhd(esxi, datastore, path + '/' + fileName, parentVhd)
|
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, {
|
||||||
|
lookMissingBlockInParent: false,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
vhd.label = fileName
|
||||||
parentVhd = vhd
|
parentVhd = vhd
|
||||||
}
|
}
|
||||||
|
if (nbColdDisks > 1 /* got a cold snapshot chain */) {
|
||||||
// it can be empty if the VM don't have a snapshot and is running
|
// it can be empty if the VM don't have a snapshot and is running
|
||||||
if (vhd !== undefined) {
|
|
||||||
if (thin) {
|
|
||||||
const stream = vhd.stream()
|
const stream = vhd.stream()
|
||||||
await vdi.$importContent(stream, { format: VDI_FORMAT_VHD })
|
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 })
|
|
||||||
}
|
}
|
||||||
}
|
return vhd
|
||||||
return { vdi, vhd }
|
|
||||||
})
|
})
|
||||||
)
|
)
|
||||||
)
|
)
|
||||||
@ -296,15 +291,28 @@ export default class MigrateVm {
|
|||||||
await Task.run({ properties: { name: `Transfering deltas of ${userdevice}` } }, async () => {
|
await Task.run({ properties: { name: `Transfering deltas of ${userdevice}` } }, async () => {
|
||||||
const chainByNode = chainsByNodes[node]
|
const chainByNode = chainsByNodes[node]
|
||||||
const disk = chainByNode[chainByNode.length - 1]
|
const disk = chainByNode[chainByNode.length - 1]
|
||||||
const { fileName, path, datastore, isFull } = disk
|
const { capacity, descriptionLabel, fileName, nameLabel, path, datastore, isFull } = disk
|
||||||
const { vdi, vhd: parentVhd } = vhds[userdevice]
|
let { vdi, vhd: parentVhd } = vhds[userdevice]
|
||||||
let vhd
|
let vhd
|
||||||
if (vdi === undefined) {
|
if (vdi === undefined) {
|
||||||
throw new Error(`Can't import delta of a running VM without its parent vdi`)
|
throw new Error(`Can't import delta of a running VM without its parent vdi`)
|
||||||
}
|
}
|
||||||
if (isFull) {
|
if (isFull) {
|
||||||
vhd = await VhdEsxiRaw.open(esxi, datastore, path + '/' + fileName, { thin })
|
vhd = await VhdEsxiRaw.open(esxi, datastore, path + '/' + fileName, { thin: false })
|
||||||
await vhd.readBlockAllocationTable()
|
// 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 {
|
} else {
|
||||||
if (parentVhd === undefined) {
|
if (parentVhd === undefined) {
|
||||||
throw new Error(`Can't import delta of a running VM without its parent VHD`)
|
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"
|
resolved "https://registry.yarnpkg.com/lodash.debounce/-/lodash.debounce-4.0.8.tgz#82d79bff30a67c4005ffd5e2515300ad9ca4d7af"
|
||||||
integrity sha512-FT1yDzDYEoYWhnSGnpE/4Kj1fLZkDFyqRb7fNt6FdYOSxlUWAtp42Eh6Wb0rGIv/m9Bgo7x4GhQbm5Ys4SG5ow==
|
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:
|
lodash.escape@^3.0.0:
|
||||||
version "3.2.0"
|
version "3.2.0"
|
||||||
resolved "https://registry.yarnpkg.com/lodash.escape/-/lodash.escape-3.2.0.tgz#995ee0dc18c1b48cc92effae71a10aab5b487698"
|
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"
|
resolved "https://registry.yarnpkg.com/mute-stream/-/mute-stream-0.0.8.tgz#1630c42b2251ff81e2a283de96a5497ea92e5e0d"
|
||||||
integrity sha512-nnbWWOkoWyUsTjKrhgD0dcz22mdkSnpYqbEjIm2nhwhuxlSkpywJmBo8h0ZqJdkp73mb90SssHkN4rsRaBAfAA==
|
integrity sha512-nnbWWOkoWyUsTjKrhgD0dcz22mdkSnpYqbEjIm2nhwhuxlSkpywJmBo8h0ZqJdkp73mb90SssHkN4rsRaBAfAA==
|
||||||
|
|
||||||
nan@^2.12.1:
|
nan@^2.12.1, nan@^2.13.2:
|
||||||
version "2.18.0"
|
version "2.18.0"
|
||||||
resolved "https://registry.yarnpkg.com/nan/-/nan-2.18.0.tgz#26a6faae7ffbeb293a39660e88a76b82e30b7554"
|
resolved "https://registry.yarnpkg.com/nan/-/nan-2.18.0.tgz#26a6faae7ffbeb293a39660e88a76b82e30b7554"
|
||||||
integrity sha512-W7tfG7vMOGtD30sHoZSSc/JVYiyDPEyQVso/Zz+/uQd0B0L46gtC+pHha5FFMRpil6fm/AoEcRWyOVi4+E/f8w==
|
integrity sha512-W7tfG7vMOGtD30sHoZSSc/JVYiyDPEyQVso/Zz+/uQd0B0L46gtC+pHha5FFMRpil6fm/AoEcRWyOVi4+E/f8w==
|
||||||
@ -22422,6 +22427,13 @@ xtend@~2.1.1:
|
|||||||
dependencies:
|
dependencies:
|
||||||
object-keys "~0.4.0"
|
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:
|
xxhashjs@^0.2.1:
|
||||||
version "0.2.2"
|
version "0.2.2"
|
||||||
resolved "https://registry.yarnpkg.com/xxhashjs/-/xxhashjs-0.2.2.tgz#8a6251567621a1c46a5ae204da0249c7f8caa9d8"
|
resolved "https://registry.yarnpkg.com/xxhashjs/-/xxhashjs-0.2.2.tgz#8a6251567621a1c46a5ae204da0249c7f8caa9d8"
|
||||||
|
Loading…
Reference in New Issue
Block a user