diff --git a/package.json b/package.json index 310d12f53..0fa83e779 100644 --- a/package.json +++ b/package.json @@ -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", diff --git a/src/api/vm.coffee b/src/api/vm.coffee index 182685e22..e7206de33 100644 --- a/src/api/vm.coffee +++ b/src/api/vm.coffee @@ -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 } } diff --git a/src/xapi/index.js b/src/xapi/index.js index 38020174d..22243b92d 100644 --- a/src/xapi/index.js +++ b/src/xapi/index.js @@ -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,16 +1431,112 @@ 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' } = {}) { - return /* await */ this._getOrWaitObject(await this._importVm( - stream, - srId && this.getObject(srId), - onlyMetadata - )) + const sr = srId && this.getObject(srId) + + if (type === 'xva') { + return /* await */ this._getOrWaitObject(await this._importVm( + stream, + 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, { diff --git a/src/xapi/other-config-template.js b/src/xapi/other-config-template.js new file mode 100644 index 000000000..ec86bb35f --- /dev/null +++ b/src/xapi/other-config-template.js @@ -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: '', + 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 }