feat(xo-{server,web}): improve OVA import error reporting (#5797)

This commit is contained in:
Nicolas Raynaud 2021-06-02 16:23:08 +02:00 committed by GitHub
parent cbd650c5ef
commit 1c91fb9dd5
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
4 changed files with 96 additions and 45 deletions

View File

@ -1256,38 +1256,42 @@ async function handleVmImport(req, res, { data, srId, type, xapi }) {
// Timeout seems to be broken in Node 4. // Timeout seems to be broken in Node 4.
// See https://github.com/nodejs/node/issues/3319 // See https://github.com/nodejs/node/issues/3319
req.setTimeout(43200000) // 12 hours req.setTimeout(43200000) // 12 hours
await new Promise((resolve, reject) => { const vm = await new Promise((resolve, reject) => {
const form = new multiparty.Form() const form = new multiparty.Form()
const promises = [] const promises = []
const tables = {} const tables = {}
form.on('error', reject) form.on('error', reject)
form.on('part', async part => { form.on('part', async part => {
if (part.name !== 'file') { try {
promises.push( if (part.name !== 'file') {
(async () => { promises.push(
if (!(part.filename in tables)) { (async () => {
tables[part.filename] = {} if (!(part.filename in tables)) {
} tables[part.filename] = {}
const view = new DataView((await getStream.buffer(part)).buffer) }
const result = new Uint32Array(view.byteLength / 4) const view = new DataView((await getStream.buffer(part)).buffer)
for (const i in result) { const result = new Uint32Array(view.byteLength / 4)
result[i] = view.getUint32(i * 4, true) for (const i in result) {
} result[i] = view.getUint32(i * 4, true)
tables[part.filename][part.name] = result }
data.tables = tables tables[part.filename][part.name] = result
})() data.tables = tables
) })()
} else { )
await Promise.all(promises) } else {
// XVA files are directly sent to xcp-ng who wants a content-length await Promise.all(promises)
part.length = part.byteCount // XVA files are directly sent to xcp-ng who wants a content-length
const vm = await xapi.importVm(part, { data, srId, type }) part.length = part.byteCount
res.end(format.response(0, vm.$id)) resolve(xapi.importVm(part, { data, srId, type }))
resolve() }
} catch (e) {
// multiparty is not promise-aware, we have to chain errors ourselves.
reject(e)
} }
}) })
form.parse(req) form.parse(req)
}) })
res.end(format.response(0, vm.$id))
} }
// TODO: "sr_id" can be passed in URL to target a specific SR // TODO: "sr_id" can be passed in URL to target a specific SR
@ -1297,12 +1301,18 @@ async function import_({ data, sr, type }) {
} }
return { return {
$sendTo: await this.registerHttpRequest(handleVmImport, { $sendTo: await this.registerApiHttpRequest(
data, 'vm.import',
srId: sr._xapiId, this.session,
type, handleVmImport,
xapi: this.getXapi(sr), {
}), data,
srId: sr._xapiId,
type,
xapi: this.getXapi(sr),
},
{ exposeAllErrors: true }
),
} }
} }

View File

@ -1102,6 +1102,9 @@ export default class Xapi extends XapiBase {
const vdis = {} const vdis = {}
const compression = {} const compression = {}
const vifDevices = await this.call('VM.get_allowed_VIF_devices', vm.$ref) const vifDevices = await this.call('VM.get_allowed_VIF_devices', vm.$ref)
if (networks.length > vifDevices.length) {
throw operationFailed({ objectId: vm.id, code: 'TOO_MANY_VIFs' })
}
await Promise.all( await Promise.all(
map(disks, async disk => { map(disks, async disk => {
const vdi = (vdis[disk.path] = await this.createVdi({ const vdi = (vdis[disk.path] = await this.createVdi({

View File

@ -6,7 +6,7 @@ import kindOf from 'kindof'
import ms from 'ms' import ms from 'ms'
import schemaInspector from 'schema-inspector' import schemaInspector from 'schema-inspector'
import { getBoundPropertyDescriptor } from 'bind-property-descriptor' import { getBoundPropertyDescriptor } from 'bind-property-descriptor'
import { MethodNotFound } from 'json-rpc-peer' import { format, JsonRpcError, MethodNotFound } from 'json-rpc-peer'
import * as methods from '../api/index.mjs' import * as methods from '../api/index.mjs'
import * as sensitiveValues from '../sensitive-values.mjs' import * as sensitiveValues from '../sensitive-values.mjs'
@ -376,4 +376,47 @@ export default class Api {
throw error throw error
} }
} }
registerApiHttpRequest(method, session, fn, data, { exposeAllErrors = false, ...opts } = {}) {
const app = this._app
const logger = this._logger
return app.registerHttpRequest(
async function (req, res) {
const timestamp = Date.now()
try {
return await fn.apply(this, arguments)
} catch (error) {
const userId = session.get('user_id', undefined)
const user = userId && (await app.getUser(userId))
logger.error(`handleVmImport =!> ${error}`, {
callId: Math.random().toString(36).slice(2),
// userId,
userName: user?.email ?? '(unknown user)',
userIp: session.get('user_ip', undefined),
method: `HTTP handler of ${method}`,
timestamp,
duration: Date.now() - timestamp,
error: serializeError(error),
})
if (!res.headersSent) {
res.statusCode = 500
res.setHeader('Content-Type', 'application/json')
res.write(
format.error(
0,
exposeAllErrors && error != null && typeof error.toJsonRpcError !== 'function'
? new JsonRpcError(error.message, error.code, error.data)
: error
)
)
}
res.end()
}
},
data,
opts
)
}
} }

View File

@ -1465,21 +1465,16 @@ export const importVm = async (file, type = 'xva', data = undefined, sr) => {
} }
} }
} }
return _call('vm.import', { type, data, sr: resolveId(sr) }).then(async ({ $sendTo }) => { const result = await _call('vm.import', { type, data, sr: resolveId(sr) })
formData.append('file', file) formData.append('file', file)
return post($sendTo, formData) const res = await post(result.$sendTo, formData)
.then(res => { const json = await res.json()
if (res.status !== 200) { if (res.status !== 200) {
throw res.status error(_('vmImportFailed'), name)
} throw json.error
success(_('vmImportSuccess'), name) }
return res.json().then(body => body.result) success(_('vmImportSuccess'), name)
}) return json.result
.catch(err => {
error(_('vmImportFailed'), name)
throw err
})
})
} }
import ImportVdiModalBody from './import-vdi-modal' // eslint-disable-line import/first import ImportVdiModalBody from './import-vdi-modal' // eslint-disable-line import/first