feat(vm.import): supports OVA (#375)

See vatesfr/xo-web#709
This commit is contained in:
Ronan Abhamon 2016-09-01 14:11:15 +02:00 committed by Julien Fontanet
parent fcaf6b7923
commit 63c676ebfe
4 changed files with 194 additions and 10 deletions

View File

@ -95,6 +95,7 @@
"serve-static": "^1.9.2",
"stack-chain": "^1.3.3",
"struct-fu": "^1.0.0",
"tar-stream": "^1.5.2",
"through2": "^2.0.0",
"trace": "^2.0.1",
"ws": "^1.1.1",
@ -102,7 +103,8 @@
"xml2js": "~0.4.6",
"xo-acl-resolver": "^0.2.1",
"xo-collection": "^0.4.0",
"xo-remote-parser": "^0.3"
"xo-remote-parser": "^0.3",
"xo-vmdk-to-vhd": "0.0.5"
},
"devDependencies": {
"babel-eslint": "^6.0.4",

View File

@ -943,13 +943,13 @@ exports.export = export_;
#---------------------------------------------------------------------
handleVmImport = $coroutine (req, res, { xapi, srId }) ->
handleVmImport = $coroutine (req, res, { data, srId, type, xapi }) ->
# Timeout seems to be broken in Node 4.
# See https://github.com/nodejs/node/issues/3319
req.setTimeout(43200000) # 12 hours
try
vm = yield xapi.importVm(req, { srId })
vm = yield xapi.importVm(req, { data, srId, type })
res.end(format.response(0, vm.$id))
catch e
res.writeHead(500)
@ -958,7 +958,7 @@ handleVmImport = $coroutine (req, res, { xapi, srId }) ->
return
# TODO: "sr_id" can be passed in URL to target a specific SR
import_ = $coroutine ({host, sr}) ->
import_ = $coroutine ({ data, host, sr, type }) ->
if not sr
if not host
throw new InvalidParameters('you must provide either host or SR')
@ -974,13 +974,45 @@ import_ = $coroutine ({host, sr}) ->
return {
$sendTo: yield @registerHttpRequest(handleVmImport, {
data,
srId: sr._xapiId,
type,
xapi
})
}
import_.params = {
data: {
type: 'object',
optional: true,
properties: {
descriptionLabel: { type: 'string' },
disks: {
type: 'array',
items: {
type: 'object',
properties: {
capacity: { type: 'integer' },
descriptionLabel: { type: 'string' },
nameLabel: { type: 'string' },
path: { type: 'string' },
position: { type: 'integer' }
}
},
optional: true
},
memory: { type: 'integer' },
nameLabel: { type: 'string' },
nCpus: { type: 'integer' },
networks: {
type: 'array',
items: { type: 'string' },
optional: true
},
}
},
host: { type: 'string', optional: true },
type: { type: 'string', optional: true },
sr: { type: 'string', optional: true }
}

View File

@ -6,7 +6,9 @@ import fatfs from 'fatfs'
import find from 'lodash/find'
import includes from 'lodash/includes'
import sortBy from 'lodash/sortBy'
import tarStream from 'tar-stream'
import unzip from 'julien-f-unzip'
import vmdkToVhd from 'xo-vmdk-to-vhd'
import { defer } from 'promise-toolbox'
import {
wrapError as wrapXapiError,
@ -48,6 +50,7 @@ import {
} from '../api-errors'
import mixins from './mixins'
import OTHER_CONFIG_TEMPLATE from './other-config-template'
import {
asBoolean,
asInteger,
@ -1428,18 +1431,114 @@ export default class Xapi extends XapiBase {
return vmRef
}
@deferrable.onFailure
async _importOvaVm ($onFailure, stream, {
descriptionLabel,
disks,
memory,
nameLabel,
networks,
nCpus
}, sr) {
// 1. Create VM.
const vm = await this._getOrWaitObject(
await this._createVmRecord({
...OTHER_CONFIG_TEMPLATE,
memory_dynamic_max: memory,
memory_dynamic_min: memory,
memory_static_max: memory,
name_description: descriptionLabel,
name_label: nameLabel,
VCPUs_at_startup: nCpus,
VCPUs_max: nCpus
})
)
$onFailure(() => this._deleteVm(vm))
// Disable start and change the VM name label during import.
await Promise.all([
this.addForbiddenOperationToVm(vm.$id, 'start', 'OVA import in progress...'),
this._setObjectProperties(vm, { name_label: `[Importing...] ${nameLabel}` })
])
// 2. Create VDIs & Vifs.
const vdis = {}
const vifDevices = await this.call('VM.get_allowed_VIF_devices', vm.$ref)
await Promise.all(
map(disks, async disk => {
const vdi = vdis[disk.path] = await this.createVdi(disk.capacity, {
name_description: disk.descriptionLabel,
name_label: disk.nameLabel,
sr: sr.$ref
})
$onFailure(() => this._deleteVdi(vdi)::pCatch(noop))
return this._createVbd(vm, vdi, { position: disk.position })
}).concat(map(networks, (networkId, i) => (
this._createVif(vm, this.getObject(networkId), {
device: vifDevices[i]
})
)))
)
// 3. Import VDIs contents.
await new Promise((resolve, reject) => {
const extract = tarStream.extract()
stream.on('error', reject)
extract.on('finish', resolve)
extract.on('error', reject)
extract.on('entry', async (entry, stream, cb) => {
// Not a disk to import.
const vdi = vdis[entry.name]
if (!vdi) {
stream.on('end', cb)
stream.resume()
return
}
const vhdStream = await vmdkToVhd(stream)
await this._importVdiContent(vdi, vhdStream, VDI_FORMAT_RAW)
// See: https://github.com/mafintosh/tar-stream#extracting
// No import parallelization.
cb()
})
stream.pipe(extract)
})
// Enable start and restore the VM name label after import.
await Promise.all([
this.removeForbiddenOperationFromVm(vm.$id, 'start'),
this._setObjectProperties(vm, { name_label: nameLabel })
])
return vm
}
// TODO: an XVA can contain multiple VMs
async importVm (stream, {
data,
onlyMetadata = false,
srId
srId,
type = 'xva'
} = {}) {
const sr = srId && this.getObject(srId)
if (type === 'xva') {
return /* await */ this._getOrWaitObject(await this._importVm(
stream,
srId && this.getObject(srId),
sr,
onlyMetadata
))
}
if (type === 'ova') {
return this._getOrWaitObject(await this._importOvaVm(stream, data, sr))
}
throw new Error(`unsupported type: '${type}'`)
}
async migrateVm (vmId, hostXapi, hostId, {
migrationNetworkId,
mapVifsNetworks,

View File

@ -0,0 +1,51 @@
const OTHER_CONFIG_TEMPLATE = {
actions_after_crash: 'restart',
actions_after_reboot: 'restart',
actions_after_shutdown: 'destroy',
affinity: null,
blocked_operations: {},
ha_always_run: false,
HVM_boot_params: {
order: 'cdn'
},
HVM_boot_policy: 'BIOS order',
HVM_shadow_multiplier: 1,
is_a_template: false,
memory_dynamic_max: 4294967296,
memory_dynamic_min: 4294967296,
memory_static_max: 4294967296,
memory_static_min: 134217728,
order: 0,
other_config: {
vgpu_pci: '',
base_template_name: 'Other install media',
mac_seed: '5e88eb6a-d680-c47f-a94a-028886971ba4',
'install-methods': 'cdrom'
},
PCI_bus: '',
platform: {
timeoffset: '0',
nx: 'true',
acpi: '1',
apic: 'true',
pae: 'true',
hpet: 'true',
viridian: 'true'
},
protection_policy: 'OpaqueRef:NULL',
PV_args: '',
PV_bootloader: '',
PV_bootloader_args: '',
PV_kernel: '',
PV_legacy_args: '',
PV_ramdisk: '',
recommendations: '<restrictions><restriction field="memory-static-max" max="137438953472" /><restriction field="vcpus-max" max="32" /><restriction property="number-of-vbds" max="255" /><restriction property="number-of-vifs" max="7" /><restriction field="has-vendor-device" value="false" /></restrictions>',
shutdown_delay: 0,
start_delay: 0,
user_version: 1,
VCPUs_at_startup: 1,
VCPUs_max: 1,
VCPUs_params: {},
version: 0
}
export { OTHER_CONFIG_TEMPLATE as default }