mirror of
https://github.com/vatesfr/xen-orchestra.git
synced 2026-09-12 15:10:10 -05:00
This branch is a reliability overhaul of the ESXi/vSphere source side of V2V, plus a scope reduction of the package. The SOAP plumbing is rewritten:
VimClient wraps @vates/node-vsphere-soap in a promise API with one emitter per call (no more cross-talk between concurrent calls, no listener leak) and a pTimeout;
vim25 faults are now parsed (_parseFault.mjs) so callers get a real fault type instead of task execution failed;
the library no longer throws from async callbacks (which killed xo-server) and its beforeExit logout hook is per-client instead of removeAllListeners.
Request specs are built in explicit schema order (soap/specs.mjs) with xsi:type, and property values are normalized (soap/normalize.mjs) so search() and the new #retrieveProperty share one shape.
search() now paginates, cancels the retrieval and destroys the container view.
Datastore HTTP downloads get session-cookie reuse, retry with jittered backoff, a headers-only timeout, and range-honouring validation.
Task waiting polls with backoff and per-operation timeouts (6 h for snapshot consolidation).
nbdkit servers are memoized by a key that includes the export options, the readiness wait races the process death instead of sleeping 2 s
the password file is 0600 and unlinked
the thumbprint is read over TLS in-process instead of two openssl spawns.
The old VHD-over-datastore readers (VhdEsxi*, Datastore*, openDeltaVmdkAsVhd) are deleted, hence the majo, rleaving the vddk/NBD path with a COWD grain-directory parser as data-map fallback.
~1.7k lines of unit tests are added.
103 lines
3.7 KiB
JavaScript
103 lines
3.7 KiB
JavaScript
const XML_ENTITIES = {
|
|
'&': '&',
|
|
''': "'",
|
|
'>': '>',
|
|
'<': '<',
|
|
'"': '"',
|
|
}
|
|
|
|
const unescapeXml = text => text.replace(/&(?:amp|apos|gt|lt|quot);/g, entity => XML_ENTITIES[entity])
|
|
|
|
// node-soap wraps text nodes either as a plain string or as `{ $value }` depending on
|
|
// whether the element carries attributes
|
|
const valueOf = node => {
|
|
if (typeof node === 'string') {
|
|
return node
|
|
}
|
|
if (node !== null && typeof node === 'object' && typeof node.$value === 'string') {
|
|
return node.$value
|
|
}
|
|
}
|
|
|
|
const faultTypeOfElementName = name => name.replace(/^.*:/, '').replace(/Fault$/, '')
|
|
|
|
// SOAP 1.1 requires the children of `Fault` to be unqualified, and that is what ESXi and vCenter
|
|
// send, but the raw body is only read when node-soap failed to parse the response: it can then be
|
|
// anything, e.g. an error emitted by a reverse proxy in front of vCenter. Hence the tolerance to a
|
|
// namespace prefix and to attributes on the element
|
|
const textOfElement = name =>
|
|
new RegExp(`<(?:[\\w.-]+:)?${name}(?:\\s[^>]*)?>([\\s\\S]*?)</(?:[\\w.-]+:)?${name}>`, 'i')
|
|
|
|
const FAULTCODE_RE = textOfElement('faultcode')
|
|
const FAULTSTRING_RE = textOfElement('faultstring')
|
|
const LOCALIZED_MESSAGE_RE = textOfElement('localizedMessage')
|
|
const DETAIL_CHILD_RE = /<(?:[\w.-]+:)?detail(?:\s[^>]*)?>\s*<([^\s/>]+)/i
|
|
|
|
/**
|
|
* Extracts the vim25 fault of a failed SOAP call.
|
|
*
|
|
* The fault type (`FileLocked`, `InvalidDeviceSpec`, `ManagedObjectNotFound`, ...) is the only part
|
|
* a caller can branch on: `faultstring` and `localizedMessage` are meant for humans and change with
|
|
* the locale of the server.
|
|
*
|
|
* Two sources are used, in order of reliability:
|
|
* 1. `rawError.root`, the envelope parsed by node-soap ( see `wsdl.xmlToObject`, which throws on a
|
|
* fault and whose error is stored there by node-soap)
|
|
* 2. the raw response body, when the parsing itself failed
|
|
*
|
|
* @param {unknown} rawError - the error passed by node-soap to its callback
|
|
* @returns {{ code?: string, faultcode?: string, faultstring?: string, localizedMessage?: string, body?: string }}
|
|
*/
|
|
export function parseFault(rawError) {
|
|
const fault = rawError?.root?.Envelope?.Body?.Fault
|
|
const rawBody = rawError?.response?.data ?? rawError?.body
|
|
const body = typeof rawBody === 'string' && rawBody !== '<stream>' ? rawBody : undefined
|
|
|
|
let code, localizedMessage
|
|
let faultcode = valueOf(fault?.faultcode)
|
|
let faultstring = valueOf(fault?.faultstring)
|
|
|
|
const detail = fault?.detail
|
|
if (detail !== null && typeof detail === 'object') {
|
|
// the single child of `detail` names the fault, e.g.
|
|
// `<InvalidDeviceSpecFault xsi:type="InvalidDeviceSpec">`
|
|
for (const [name, value] of Object.entries(detail)) {
|
|
if (name === 'attributes') {
|
|
continue
|
|
}
|
|
code = value?.attributes?.['xsi:type'] ?? faultTypeOfElementName(name)
|
|
localizedMessage = valueOf(value?.localizedMessage)
|
|
break
|
|
}
|
|
}
|
|
|
|
if (body !== undefined) {
|
|
if (faultstring === undefined) {
|
|
const matches = body.match(FAULTSTRING_RE)
|
|
if (matches !== null) {
|
|
faultstring = unescapeXml(matches[1])
|
|
}
|
|
}
|
|
if (faultcode === undefined) {
|
|
const matches = body.match(FAULTCODE_RE)
|
|
if (matches !== null) {
|
|
faultcode = unescapeXml(matches[1])
|
|
}
|
|
}
|
|
if (code === undefined) {
|
|
const matches = body.match(DETAIL_CHILD_RE)
|
|
if (matches !== null) {
|
|
code = faultTypeOfElementName(matches[1])
|
|
}
|
|
}
|
|
if (localizedMessage === undefined) {
|
|
const matches = body.match(LOCALIZED_MESSAGE_RE)
|
|
if (matches !== null) {
|
|
localizedMessage = unescapeXml(matches[1])
|
|
}
|
|
}
|
|
}
|
|
|
|
return { code, faultcode, faultstring, localizedMessage, body }
|
|
}
|