Initial
This commit is contained in:
@@ -1,5 +1,5 @@
|
||||
import { Buffer } from 'node:buffer';
|
||||
import { Agent } from 'undici';
|
||||
import { Agent, fetch as undiciFetch } from 'undici';
|
||||
import { config } from '../config.js';
|
||||
import { getBooleanSetting, getStringSetting } from '../models/settingsModel.js';
|
||||
import { normalizeOsFamily } from '../utils/osFamily.js';
|
||||
@@ -122,6 +122,12 @@ function dispatcherFor(settings) {
|
||||
: undefined;
|
||||
}
|
||||
|
||||
function fetchForDispatcher(dispatcher) {
|
||||
// Node's built-in fetch and the npm undici Agent can be different major
|
||||
// versions. Use package fetch whenever we pass a package dispatcher.
|
||||
return dispatcher ? undiciFetch : fetch;
|
||||
}
|
||||
|
||||
async function parseVCenterResponse(response) {
|
||||
const text = await response.text();
|
||||
if (!text) return null;
|
||||
@@ -179,10 +185,11 @@ async function vcenterFetch(settings, path, { method = 'GET', sessionId, allow40
|
||||
let response;
|
||||
let payload = null;
|
||||
try {
|
||||
response = await fetch(`${settings.baseUrl}${path}`, {
|
||||
const dispatcher = dispatcherFor(settings);
|
||||
response = await fetchForDispatcher(dispatcher)(`${settings.baseUrl}${path}`, {
|
||||
method,
|
||||
headers,
|
||||
dispatcher: dispatcherFor(settings),
|
||||
dispatcher,
|
||||
signal: AbortSignal.timeout(settings.requestTimeoutMs)
|
||||
});
|
||||
payload = await parseVCenterResponse(response);
|
||||
@@ -234,8 +241,27 @@ async function vcenterFetch(settings, path, { method = 'GET', sessionId, allow40
|
||||
return payload;
|
||||
}
|
||||
|
||||
function soapBaseUrl(settings) {
|
||||
return normalizeBaseUrl(settings.baseUrl)
|
||||
.replace(/\/sdk\/vimService\.wsdl$/i, '')
|
||||
.replace(/\/sdk\/vimService$/i, '')
|
||||
.replace(/\/sdk$/i, '');
|
||||
}
|
||||
|
||||
function soapEndpoint(settings) {
|
||||
return `${settings.baseUrl.replace(/\/sdk$/i, '')}/sdk`;
|
||||
return `${soapBaseUrl(settings)}/sdk`;
|
||||
}
|
||||
|
||||
function soapWsdlEndpoint(settings) {
|
||||
return `${soapBaseUrl(settings)}/sdk/vimService.wsdl`;
|
||||
}
|
||||
|
||||
function soapEndpointCandidates(settings) {
|
||||
return [...new Set([
|
||||
`${soapBaseUrl(settings)}/sdk`,
|
||||
`${soapBaseUrl(settings)}/sdk/`,
|
||||
`${soapBaseUrl(settings)}/sdk/vimService`
|
||||
])];
|
||||
}
|
||||
|
||||
function soapEnvelope(body) {
|
||||
@@ -266,6 +292,18 @@ function soapBodyPreview(value) {
|
||||
: text;
|
||||
}
|
||||
|
||||
function networkErrorMessage(err) {
|
||||
if (err.name === 'TimeoutError') return `Timed out after the configured request timeout.`;
|
||||
const cause = err.cause || {};
|
||||
const details = [
|
||||
err.message,
|
||||
cause.code ? `code=${cause.code}` : '',
|
||||
cause.reason ? `reason=${cause.reason}` : '',
|
||||
cause.message && cause.message !== err.message ? `cause=${cause.message}` : ''
|
||||
].filter(Boolean);
|
||||
return details.join('; ');
|
||||
}
|
||||
|
||||
function extractSoapTag(text, tagName) {
|
||||
const pattern = new RegExp(`<(?:[^:>]+:)?${tagName}(?:\\s[^>]*)?>([\\s\\S]*?)<\\/(?:[^:>]+:)?${tagName}>`, 'i');
|
||||
const match = String(text || '').match(pattern);
|
||||
@@ -301,51 +339,36 @@ function extractSoapFault(text) {
|
||||
return extractSoapTag(text, 'faultstring') || extractSoapTag(text, 'reason') || '';
|
||||
}
|
||||
|
||||
async function vSphereSoapFetch(settings, action, body, trace = null) {
|
||||
const url = soapEndpoint(settings);
|
||||
const requestBody = soapEnvelope(body);
|
||||
const headers = {
|
||||
Accept: 'text/xml',
|
||||
'Content-Type': 'text/xml; charset=utf-8',
|
||||
SOAPAction: `"urn:vim25/${action}"`
|
||||
};
|
||||
async function probeSoapWsdl(settings, trace = null) {
|
||||
const url = soapWsdlEndpoint(settings);
|
||||
const startedAt = Date.now();
|
||||
let response;
|
||||
let text = '';
|
||||
try {
|
||||
response = await fetch(url, {
|
||||
method: 'POST',
|
||||
headers,
|
||||
body: requestBody,
|
||||
dispatcher: dispatcherFor(settings),
|
||||
const dispatcher = dispatcherFor(settings);
|
||||
response = await fetchForDispatcher(dispatcher)(url, {
|
||||
method: 'GET',
|
||||
headers: { Accept: 'text/xml, application/xml, */*' },
|
||||
dispatcher,
|
||||
signal: AbortSignal.timeout(settings.requestTimeoutMs)
|
||||
});
|
||||
text = await response.text();
|
||||
} catch (err) {
|
||||
pushTrace(trace, {
|
||||
method: 'POST',
|
||||
method: 'GET',
|
||||
url,
|
||||
soapAction: action,
|
||||
requestHeaders: { ...headers, SOAPAction: headers.SOAPAction },
|
||||
requestBody: soapBodyPreview(requestBody),
|
||||
requestHeaders: { Accept: 'text/xml, application/xml, */*' },
|
||||
durationMs: Date.now() - startedAt,
|
||||
timeoutMs: settings.requestTimeoutMs,
|
||||
networkError: err.name === 'TimeoutError'
|
||||
? `Timed out after ${settings.requestTimeoutMs} ms waiting for VMware host SOAP endpoint.`
|
||||
: err.message
|
||||
networkError: networkErrorMessage(err),
|
||||
diagnosticOnly: true
|
||||
});
|
||||
const message = err.name === 'TimeoutError'
|
||||
? `Timed out after ${settings.requestTimeoutMs} ms waiting for VMware host SOAP action ${action}.`
|
||||
: `Unable to reach VMware host SOAP action ${action}: ${err.message}`;
|
||||
throw new VCenterTraceError(message, trace);
|
||||
return;
|
||||
}
|
||||
|
||||
pushTrace(trace, {
|
||||
method: 'POST',
|
||||
method: 'GET',
|
||||
url,
|
||||
soapAction: action,
|
||||
requestHeaders: { ...headers, SOAPAction: headers.SOAPAction },
|
||||
requestBody: soapBodyPreview(requestBody),
|
||||
requestHeaders: { Accept: 'text/xml, application/xml, */*' },
|
||||
status: response.status,
|
||||
statusText: response.statusText,
|
||||
durationMs: Date.now() - startedAt,
|
||||
@@ -353,19 +376,104 @@ async function vSphereSoapFetch(settings, action, body, trace = null) {
|
||||
'content-type': response.headers.get('content-type') || '',
|
||||
date: response.headers.get('date') || ''
|
||||
},
|
||||
responseSummary: {
|
||||
type: 'soap',
|
||||
bytes: text.length,
|
||||
fault: Boolean(extractSoapFault(text))
|
||||
},
|
||||
responseBody: soapBodyPreview(text)
|
||||
responseSummary: { type: 'wsdl', bytes: text.length, ok: response.ok },
|
||||
responseBody: soapBodyPreview(text),
|
||||
diagnosticOnly: true
|
||||
});
|
||||
}
|
||||
|
||||
const fault = extractSoapFault(text);
|
||||
if (!response.ok || fault) {
|
||||
throw new VCenterTraceError(`VMware host SOAP action ${action} failed${response.ok ? '' : ` with HTTP ${response.status}`}: ${fault || response.statusText}`, trace, response.status || 502);
|
||||
async function vSphereSoapFetch(settings, action, body, trace = null, options = {}) {
|
||||
const requestBody = soapEnvelope(body);
|
||||
const baseHeaders = {
|
||||
Accept: 'text/xml',
|
||||
'Content-Type': 'text/xml; charset=utf-8',
|
||||
SOAPAction: `"urn:vim25/${action}"`
|
||||
};
|
||||
const endpoints = options.endpoint ? [options.endpoint] : soapEndpointCandidates(settings);
|
||||
let lastError = null;
|
||||
for (const [index, url] of endpoints.entries()) {
|
||||
const headers = options.cookie ? { ...baseHeaders, Cookie: options.cookie } : baseHeaders;
|
||||
const startedAt = Date.now();
|
||||
let response;
|
||||
let text = '';
|
||||
try {
|
||||
const dispatcher = dispatcherFor(settings);
|
||||
response = await fetchForDispatcher(dispatcher)(url, {
|
||||
method: 'POST',
|
||||
headers,
|
||||
body: requestBody,
|
||||
dispatcher,
|
||||
signal: AbortSignal.timeout(settings.requestTimeoutMs)
|
||||
});
|
||||
text = await response.text();
|
||||
} catch (err) {
|
||||
const message = err.name === 'TimeoutError'
|
||||
? `Timed out after ${settings.requestTimeoutMs} ms waiting for VMware host SOAP action ${action}.`
|
||||
: `Unable to reach VMware host SOAP action ${action}: ${networkErrorMessage(err)}`;
|
||||
pushTrace(trace, {
|
||||
method: 'POST',
|
||||
url,
|
||||
soapAction: action,
|
||||
requestHeaders: { ...headers, SOAPAction: headers.SOAPAction, Cookie: headers.Cookie ? '[redacted session cookie]' : undefined },
|
||||
requestBody: soapBodyPreview(requestBody),
|
||||
durationMs: Date.now() - startedAt,
|
||||
timeoutMs: settings.requestTimeoutMs,
|
||||
networkError: networkErrorMessage(err),
|
||||
endpointAttempt: `${index + 1} of ${endpoints.length}`
|
||||
});
|
||||
lastError = new VCenterTraceError(message, trace);
|
||||
if (index < endpoints.length - 1) {
|
||||
pushTraceInfo(trace, `Retrying VMware SOAP action ${action} with alternate ESXi endpoint path.`, {
|
||||
failedEndpoint: url,
|
||||
nextEndpoint: endpoints[index + 1]
|
||||
});
|
||||
continue;
|
||||
}
|
||||
throw lastError;
|
||||
}
|
||||
|
||||
pushTrace(trace, {
|
||||
method: 'POST',
|
||||
url,
|
||||
soapAction: action,
|
||||
requestHeaders: { ...headers, SOAPAction: headers.SOAPAction, Cookie: headers.Cookie ? '[redacted session cookie]' : undefined },
|
||||
requestBody: soapBodyPreview(requestBody),
|
||||
status: response.status,
|
||||
statusText: response.statusText,
|
||||
durationMs: Date.now() - startedAt,
|
||||
responseHeaders: {
|
||||
'content-type': response.headers.get('content-type') || '',
|
||||
date: response.headers.get('date') || '',
|
||||
'set-cookie': response.headers.get('set-cookie') ? '[redacted session cookie]' : ''
|
||||
},
|
||||
responseSummary: {
|
||||
type: 'soap',
|
||||
bytes: text.length,
|
||||
fault: Boolean(extractSoapFault(text))
|
||||
},
|
||||
responseBody: soapBodyPreview(text),
|
||||
endpointAttempt: `${index + 1} of ${endpoints.length}`
|
||||
});
|
||||
|
||||
const fault = extractSoapFault(text);
|
||||
if ((!response.ok || fault) && [404, 405].includes(response.status) && index < endpoints.length - 1) {
|
||||
pushTraceInfo(trace, `Retrying VMware SOAP action ${action} after HTTP ${response.status} from ESXi endpoint path.`, {
|
||||
failedEndpoint: url,
|
||||
nextEndpoint: endpoints[index + 1],
|
||||
status: response.status
|
||||
});
|
||||
continue;
|
||||
}
|
||||
if (!response.ok || fault) {
|
||||
throw new VCenterTraceError(`VMware host SOAP action ${action} failed${response.ok ? '' : ` with HTTP ${response.status}`}: ${fault || response.statusText}`, trace, response.status || 502);
|
||||
}
|
||||
if (options.capture) {
|
||||
options.capture.cookie = response.headers.get('set-cookie') || options.capture.cookie || '';
|
||||
options.capture.endpoint = url;
|
||||
}
|
||||
return text;
|
||||
}
|
||||
return text;
|
||||
throw lastError || new VCenterTraceError(`VMware host SOAP action ${action} could not be completed.`, trace);
|
||||
}
|
||||
|
||||
function unwrapList(payload) {
|
||||
@@ -679,8 +787,11 @@ function soapRefElement(name, ref, fallbackType) {
|
||||
async function connectStandaloneSoap(settings, trace = null) {
|
||||
pushTraceInfo(trace, `Using ${WEB_SERVICES_PROFILE_LABEL}`, {
|
||||
mode: 'web-services',
|
||||
endpoint: soapEndpoint(settings)
|
||||
endpoint: soapEndpoint(settings),
|
||||
endpointCandidates: soapEndpointCandidates(settings),
|
||||
wsdl: soapWsdlEndpoint(settings)
|
||||
});
|
||||
await probeSoapWsdl(settings, trace);
|
||||
const serviceContent = await vSphereSoapFetch(
|
||||
settings,
|
||||
'RetrieveServiceContent',
|
||||
@@ -693,13 +804,15 @@ async function connectStandaloneSoap(settings, trace = null) {
|
||||
viewManager: extractManagedObject(serviceContent, 'viewManager', 'ViewManager'),
|
||||
sessionManager: extractManagedObject(serviceContent, 'sessionManager', 'SessionManager')
|
||||
};
|
||||
const loginMeta = {};
|
||||
await vSphereSoapFetch(
|
||||
settings,
|
||||
'Login',
|
||||
`<Login xmlns="urn:vim25">${soapRefElement('_this', refs.sessionManager, 'SessionManager')}<userName>${xmlEscape(settings.username)}</userName><password>${xmlEscape(settings.password)}</password></Login>`,
|
||||
trace
|
||||
trace,
|
||||
{ capture: loginMeta }
|
||||
);
|
||||
return refs;
|
||||
return { ...refs, sessionCookie: loginMeta.cookie || '', endpoint: loginMeta.endpoint || '' };
|
||||
}
|
||||
|
||||
async function createVmContainerView(settings, refs, trace = null) {
|
||||
@@ -707,7 +820,8 @@ async function createVmContainerView(settings, refs, trace = null) {
|
||||
settings,
|
||||
'CreateContainerView',
|
||||
`<CreateContainerView xmlns="urn:vim25">${soapRefElement('_this', refs.viewManager, 'ViewManager')}${soapRefElement('container', refs.rootFolder, 'Folder')}<type>VirtualMachine</type><recursive>true</recursive></CreateContainerView>`,
|
||||
trace
|
||||
trace,
|
||||
{ cookie: refs.sessionCookie, endpoint: refs.endpoint }
|
||||
);
|
||||
return extractManagedObject(response, 'returnval', 'ContainerView');
|
||||
}
|
||||
@@ -759,11 +873,11 @@ async function retrieveStandaloneVmInventory(settings, { connection = null, limi
|
||||
const refs = await connectStandaloneSoap(settings, trace);
|
||||
const viewRef = await createVmContainerView(settings, refs, trace);
|
||||
const objects = [];
|
||||
let response = await vSphereSoapFetch(settings, 'RetrievePropertiesEx', retrievePropertiesBody(refs.propertyCollector, viewRef, limit), trace);
|
||||
let response = await vSphereSoapFetch(settings, 'RetrievePropertiesEx', retrievePropertiesBody(refs.propertyCollector, viewRef, limit), trace, { cookie: refs.sessionCookie, endpoint: refs.endpoint });
|
||||
objects.push(...parseVmPropertyObjects(response));
|
||||
let token = decodeXml(extractSoapTag(response, 'token'));
|
||||
while (token && objects.length < limit) {
|
||||
response = await vSphereSoapFetch(settings, 'ContinueRetrievePropertiesEx', continuePropertiesBody(refs.propertyCollector, token), trace);
|
||||
response = await vSphereSoapFetch(settings, 'ContinueRetrievePropertiesEx', continuePropertiesBody(refs.propertyCollector, token), trace, { cookie: refs.sessionCookie, endpoint: refs.endpoint });
|
||||
objects.push(...parseVmPropertyObjects(response));
|
||||
token = decodeXml(extractSoapTag(response, 'token'));
|
||||
}
|
||||
@@ -778,7 +892,7 @@ async function retrieveStandaloneVmPowerState(settings, vmId, trace = null) {
|
||||
`<specSet><propSet><type>VirtualMachine</type><all>false><pathSet>runtime.powerState</pathSet></propSet>` +
|
||||
`<objectSet>${soapRefElement('obj', vmRef, 'VirtualMachine')}<skip>false</skip></objectSet></specSet>` +
|
||||
`<options><maxObjects>1</maxObjects></options></RetrievePropertiesEx>`;
|
||||
const response = await vSphereSoapFetch(settings, 'RetrievePropertiesEx', body, trace);
|
||||
const response = await vSphereSoapFetch(settings, 'RetrievePropertiesEx', body, trace, { cookie: refs.sessionCookie, endpoint: refs.endpoint });
|
||||
const object = parseVmPropertyObjects(response)[0];
|
||||
return normalizePowerState(object?.props?.['runtime.powerState'] || '');
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user