mirror of
https://github.com/vatesfr/xen-orchestra.git
synced 2026-11-09 17:06:13 -06: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.
105 lines
4.5 KiB
JavaScript
105 lines
4.5 KiB
JavaScript
import assert from 'node:assert/strict'
|
|
import { describe, it } from 'node:test'
|
|
|
|
import { parseFault } from './_parseFault.mjs'
|
|
|
|
// captured from an ESXi 8 host
|
|
const MANAGED_OBJECT_NOT_FOUND = `<?xml version="1.0" encoding="UTF-8"?>
|
|
<soapenv:Envelope xmlns:soapenv="http://schemas.xmlsoap.org/soap/envelope/" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance">
|
|
<soapenv:Body>
|
|
<soapenv:Fault><faultcode>ServerFaultCode</faultcode><faultstring>The object 'vim.VirtualMachine:vm-42' has already been deleted or has not been completely created</faultstring><detail><ManagedObjectNotFoundFault xmlns="urn:vim25" xsi:type="ManagedObjectNotFound"><obj type="VirtualMachine">vm-42</obj></ManagedObjectNotFoundFault></detail></soapenv:Fault>
|
|
</soapenv:Body>
|
|
</soapenv:Envelope>`
|
|
|
|
// not what a vSphere host sends — SOAP 1.1 requires these children to be unqualified — but a body
|
|
// which node-soap failed to parse can come from anything sitting in front of vCenter
|
|
const NAMESPACE_PREFIXED = `<?xml version="1.0" encoding="UTF-8"?>
|
|
<soapenv:Envelope xmlns:soapenv="http://schemas.xmlsoap.org/soap/envelope/" xmlns:vim25="urn:vim25" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance">
|
|
<soapenv:Body>
|
|
<soapenv:Fault><soapenv:faultcode>ServerFaultCode</soapenv:faultcode><soapenv:faultstring>Unable to access file since it is locked</soapenv:faultstring><soapenv:detail xmlns="urn:vim25"><vim25:FileLockedFault xsi:type="FileLocked"><vim25:localizedMessage>Unable to access file [ds1] vm/vm.vmdk since it is locked</vim25:localizedMessage></vim25:FileLockedFault></soapenv:detail></soapenv:Fault>
|
|
</soapenv:Body>
|
|
</soapenv:Envelope>`
|
|
|
|
describe('parseFault', function () {
|
|
it('reads the fault from the envelope parsed by node-soap', function () {
|
|
const { code, faultcode, faultstring, localizedMessage } = parseFault({
|
|
root: {
|
|
Envelope: {
|
|
Body: {
|
|
Fault: {
|
|
faultcode: 'ServerFaultCode',
|
|
faultstring: 'Unable to access file since it is locked',
|
|
detail: {
|
|
FileLockedFault: {
|
|
attributes: { 'xsi:type': 'FileLocked' },
|
|
localizedMessage: 'Unable to access file [ds1] vm/vm.vmdk since it is locked',
|
|
},
|
|
},
|
|
},
|
|
},
|
|
},
|
|
},
|
|
})
|
|
|
|
assert.equal(code, 'FileLocked')
|
|
assert.equal(faultcode, 'ServerFaultCode')
|
|
assert.equal(faultstring, 'Unable to access file since it is locked')
|
|
assert.equal(localizedMessage, 'Unable to access file [ds1] vm/vm.vmdk since it is locked')
|
|
})
|
|
|
|
it('falls back to the element name when xsi:type is absent', function () {
|
|
const { code } = parseFault({
|
|
root: { Envelope: { Body: { Fault: { detail: { InvalidDeviceSpecFault: {} } } } } },
|
|
})
|
|
|
|
assert.equal(code, 'InvalidDeviceSpec')
|
|
})
|
|
|
|
it('falls back to the raw body when the envelope could not be parsed', function () {
|
|
const { code, faultcode, faultstring, body } = parseFault({
|
|
message: 'Request failed with status code 500',
|
|
response: { data: MANAGED_OBJECT_NOT_FOUND },
|
|
})
|
|
|
|
assert.equal(code, 'ManagedObjectNotFound')
|
|
assert.equal(faultcode, 'ServerFaultCode')
|
|
// entities are decoded
|
|
assert.match(faultstring, /^The object 'vim\.VirtualMachine:vm-42' has already been deleted/)
|
|
assert.equal(body, MANAGED_OBJECT_NOT_FOUND)
|
|
})
|
|
|
|
it('reads a namespace prefixed fault from the raw body', function () {
|
|
const { code, faultcode, faultstring, localizedMessage } = parseFault({
|
|
response: { data: NAMESPACE_PREFIXED },
|
|
})
|
|
|
|
assert.equal(code, 'FileLocked')
|
|
assert.equal(faultcode, 'ServerFaultCode')
|
|
assert.equal(faultstring, 'Unable to access file since it is locked')
|
|
assert.equal(localizedMessage, 'Unable to access file [ds1] vm/vm.vmdk since it is locked')
|
|
})
|
|
|
|
it('handles a `$value` wrapped text node', function () {
|
|
const { faultstring } = parseFault({
|
|
root: { Envelope: { Body: { Fault: { faultstring: { $value: 'boom' } } } } },
|
|
})
|
|
|
|
assert.equal(faultstring, 'boom')
|
|
})
|
|
|
|
it('does not throw on an error without any fault', function () {
|
|
assert.deepEqual(parseFault(new Error('socket hang up')), {
|
|
code: undefined,
|
|
faultcode: undefined,
|
|
faultstring: undefined,
|
|
localizedMessage: undefined,
|
|
body: undefined,
|
|
})
|
|
assert.deepEqual(parseFault(undefined).code, undefined)
|
|
})
|
|
|
|
it('ignores the placeholder body used by node-soap for streamed responses', function () {
|
|
assert.equal(parseFault({ body: '<stream>' }).body, undefined)
|
|
})
|
|
})
|