Skip to content

Commit

Permalink
refactor(xva-generator): split index.js
Browse files Browse the repository at this point in the history
  • Loading branch information
fbeauchamp committed Jan 29, 2024
1 parent ef81621 commit cddb316
Show file tree
Hide file tree
Showing 7 changed files with 265 additions and 253 deletions.
5 changes: 5 additions & 0 deletions @xen-orchestra/xva-generator/_isNotEmptyRef.mjs
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
export function isNotEmptyRef(val) {
const EMPTY = 'OpaqueRef:NULL'
const PREFIX = 'OpaqueRef:'
return val !== EMPTY && typeof val === 'string' && val.startsWith(PREFIX)
}
39 changes: 39 additions & 0 deletions @xen-orchestra/xva-generator/_writeDisk.mjs
Original file line number Diff line number Diff line change
@@ -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 changes: 154 additions & 0 deletions @xen-orchestra/xva-generator/_writeOvaXml.mjs
Original file line number Diff line number Diff line change
@@ -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)
}
31 changes: 31 additions & 0 deletions @xen-orchestra/xva-generator/importVdi.mjs
Original file line number Diff line number Diff line change
@@ -0,0 +1,31 @@
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
)
// ensure VM is laoded
await xapi.getRecord('VM', 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 changes: 33 additions & 0 deletions @xen-orchestra/xva-generator/importVm.mjs
Original file line number Diff line number Diff line change
@@ -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]
}
Loading

0 comments on commit cddb316

Please sign in to comment.