Compare commits

...

7 Commits

Author SHA1 Message Date
Nicolas Raynaud
2f03168517 add forgotten changelog 2021-06-02 16:34:44 +02:00
Nicolas Raynaud
0f14c9663f improve OVA import error handling
- log it in settings/logs
- report the failure in UI
2021-06-02 14:24:52 +02:00
Nicolas Raynaud
3cf4bc595c improve OVA import error handling
- log it in settings/logs
- report the failure in UI
2021-06-02 14:24:20 +02:00
Nicolas Raynaud
15fe3ba0f6 catch imports trying to bring too many network cards. 2021-05-31 20:01:01 +02:00
Nicolas Raynaud
5614a5166c improve OVA import error handling 2021-05-31 18:25:29 +02:00
Nicolas Raynaud
23da04b376 fix gremlins 2021-05-31 17:41:03 +02:00
Julien Fontanet
2bddba964f feat(xo-server/registerApiHttpRequest): wrapper with custom error handling 2021-05-31 16:26:45 +02:00
5 changed files with 101 additions and 45 deletions

View File

@@ -7,6 +7,8 @@
> Users must be able to say: “Nice enhancement, I'm eager to test it”
- [OVA import] improve OVA import error reporting (PR [#5797](https://github.com/vatesfr/xen-orchestra/pull/5797))
### Bug fixes
> Users must be able to say: “I had this issue, happy to know it's fixed”
@@ -27,3 +29,6 @@
> - major: if the change breaks compatibility
>
> In case of conflict, the highest (lowest in previous list) `$version` wins.
- xo-server patch
- xo-web patch

View File

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

View File

@@ -6,7 +6,7 @@ import kindOf from 'kindof'
import ms from 'ms'
import schemaInspector from 'schema-inspector'
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 sensitiveValues from '../sensitive-values.mjs'
@@ -376,4 +376,47 @@ export default class Api {
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 }) => {
formData.append('file', file)
return post($sendTo, formData)
.then(res => {
if (res.status !== 200) {
throw res.status
}
success(_('vmImportSuccess'), name)
return res.json().then(body => body.result)
})
.catch(err => {
error(_('vmImportFailed'), name)
throw err
})
})
const result = await _call('vm.import', { type, data, sr: resolveId(sr) })
formData.append('file', file)
const res = await post(result.$sendTo, formData)
const json = await res.json()
if (res.status !== 200) {
error(_('vmImportFailed'), name)
throw json.error
}
success(_('vmImportSuccess'), name)
return json.result
}
import ImportVdiModalBody from './import-vdi-modal' // eslint-disable-line import/first