Initial
This commit is contained in:
@@ -5,6 +5,17 @@ import { getBooleanSetting, getStringSetting } from '../models/settingsModel.js'
|
||||
|
||||
const WINDOWS_HINTS = ['windows', 'microsoft'];
|
||||
const LINUX_HINTS = ['linux', 'ubuntu', 'debian', 'red hat', 'rhel', 'centos', 'suse', 'oracle linux'];
|
||||
const MAX_TRACE_ENTRIES = 700;
|
||||
const MAX_TRACE_BODY_CHARS = 6000;
|
||||
|
||||
export class VCenterTraceError extends Error {
|
||||
constructor(message, trace = [], statusCode = 502) {
|
||||
super(message);
|
||||
this.name = 'VCenterTraceError';
|
||||
this.trace = trace;
|
||||
this.statusCode = statusCode;
|
||||
}
|
||||
}
|
||||
|
||||
export function getVCenterSettings() {
|
||||
return {
|
||||
@@ -53,22 +64,89 @@ async function parseVCenterResponse(response) {
|
||||
}
|
||||
}
|
||||
|
||||
async function vcenterFetch(settings, path, { method = 'GET', sessionId, allow404 = false } = {}) {
|
||||
function summarizePayload(payload) {
|
||||
if (Array.isArray(payload)) return { type: 'array', count: payload.length };
|
||||
if (Array.isArray(payload?.value)) return { type: 'value-array', count: payload.value.length };
|
||||
if (payload && typeof payload === 'object') return { type: 'object', keys: Object.keys(payload).slice(0, 20) };
|
||||
return { type: typeof payload };
|
||||
}
|
||||
|
||||
function bodyPreview(payload, path) {
|
||||
if (path === '/api/session') return '[redacted vCenter session id]';
|
||||
const raw = typeof payload === 'string' ? payload : JSON.stringify(payload, null, 2);
|
||||
const text = raw || '';
|
||||
return text.length > MAX_TRACE_BODY_CHARS
|
||||
? `${text.slice(0, MAX_TRACE_BODY_CHARS)}\n... truncated ${text.length - MAX_TRACE_BODY_CHARS} character(s)`
|
||||
: text;
|
||||
}
|
||||
|
||||
function pushTrace(trace, entry) {
|
||||
if (!trace) return;
|
||||
if (trace.length < MAX_TRACE_ENTRIES) {
|
||||
trace.push(entry);
|
||||
return;
|
||||
}
|
||||
if (!trace.some((item) => item.truncated)) {
|
||||
trace.push({
|
||||
truncated: true,
|
||||
message: `vCenter trace limited to ${MAX_TRACE_ENTRIES} entries. Narrow the filter to capture fewer guest enrichment calls.`
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
async function vcenterFetch(settings, path, { method = 'GET', sessionId, allow404 = false, trace = null } = {}) {
|
||||
const headers = { Accept: 'application/json' };
|
||||
if (sessionId) headers['vmware-api-session-id'] = sessionId;
|
||||
else headers.Authorization = `Basic ${Buffer.from(`${settings.username}:${settings.password}`).toString('base64')}`;
|
||||
|
||||
const response = await fetch(`${settings.baseUrl}${path}`, {
|
||||
const startedAt = Date.now();
|
||||
let response;
|
||||
let payload = null;
|
||||
try {
|
||||
response = await fetch(`${settings.baseUrl}${path}`, {
|
||||
method,
|
||||
headers,
|
||||
dispatcher: dispatcherFor(settings)
|
||||
});
|
||||
payload = await parseVCenterResponse(response);
|
||||
} catch (err) {
|
||||
pushTrace(trace, {
|
||||
method,
|
||||
url: `${settings.baseUrl}${path}`,
|
||||
requestHeaders: {
|
||||
Accept: headers.Accept,
|
||||
Authorization: headers.Authorization ? '[redacted basic auth]' : undefined,
|
||||
'vmware-api-session-id': headers['vmware-api-session-id'] ? '[redacted session id]' : undefined
|
||||
},
|
||||
durationMs: Date.now() - startedAt,
|
||||
networkError: err.message
|
||||
});
|
||||
throw new VCenterTraceError(`Unable to reach vCenter ${method} ${path}: ${err.message}`, trace);
|
||||
}
|
||||
|
||||
pushTrace(trace, {
|
||||
method,
|
||||
headers,
|
||||
dispatcher: dispatcherFor(settings)
|
||||
url: `${settings.baseUrl}${path}`,
|
||||
requestHeaders: {
|
||||
Accept: headers.Accept,
|
||||
Authorization: headers.Authorization ? '[redacted basic auth]' : undefined,
|
||||
'vmware-api-session-id': headers['vmware-api-session-id'] ? '[redacted session id]' : undefined
|
||||
},
|
||||
status: response.status,
|
||||
statusText: response.statusText,
|
||||
durationMs: Date.now() - startedAt,
|
||||
responseHeaders: {
|
||||
'content-type': response.headers.get('content-type') || '',
|
||||
date: response.headers.get('date') || ''
|
||||
},
|
||||
responseSummary: summarizePayload(payload),
|
||||
responseBody: bodyPreview(payload, path)
|
||||
});
|
||||
|
||||
if (allow404 && response.status === 404) return null;
|
||||
const payload = await parseVCenterResponse(response);
|
||||
if (!response.ok) {
|
||||
const message = payload?.messages?.[0]?.default_message || payload?.error || payload?.message || response.statusText;
|
||||
throw new Error(`vCenter ${method} ${path} failed with HTTP ${response.status}: ${message}`);
|
||||
throw new VCenterTraceError(`vCenter ${method} ${path} failed with HTTP ${response.status}: ${message}`, trace, response.status);
|
||||
}
|
||||
return payload;
|
||||
}
|
||||
@@ -79,16 +157,16 @@ function unwrapList(payload) {
|
||||
return [];
|
||||
}
|
||||
|
||||
async function createSession(settings) {
|
||||
const payload = await vcenterFetch(settings, '/api/session', { method: 'POST' });
|
||||
async function createSession(settings, trace = null) {
|
||||
const payload = await vcenterFetch(settings, '/api/session', { method: 'POST', trace });
|
||||
const sessionId = typeof payload === 'string' ? payload : payload?.value || payload?.session_id || payload?.id;
|
||||
if (!sessionId) throw new Error('vCenter session authentication succeeded but no session id was returned.');
|
||||
if (!sessionId) throw new VCenterTraceError('vCenter session authentication succeeded but no session id was returned.', trace);
|
||||
return sessionId;
|
||||
}
|
||||
|
||||
async function optionalVCenterFetch(settings, path, sessionId) {
|
||||
async function optionalVCenterFetch(settings, path, sessionId, trace = null) {
|
||||
try {
|
||||
return await vcenterFetch(settings, path, { sessionId, allow404: true });
|
||||
return await vcenterFetch(settings, path, { sessionId, allow404: true, trace });
|
||||
} catch (err) {
|
||||
return { unavailable: true, reason: err.message };
|
||||
}
|
||||
@@ -189,8 +267,14 @@ export function vmMatchesFilter(candidate, filter = {}) {
|
||||
}
|
||||
|
||||
export function filterSummaryVms(vms = [], filter = {}) {
|
||||
if ((filter?.field || 'hostname') !== 'name') return vms;
|
||||
return vms.filter((vm) => vmMatchesFilter(normalizeVCenterSummaryVm(vm), filter));
|
||||
const field = filter?.field || 'hostname';
|
||||
if (!['name', 'hostname'].includes(field)) return { vms, summaryMatched: vms.length, summaryFilterApplied: false, summaryFilterFallback: false };
|
||||
const summaryFilter = { ...filter, field: 'name' };
|
||||
const matched = vms.filter((vm) => vmMatchesFilter(normalizeVCenterSummaryVm(vm), summaryFilter));
|
||||
if (field === 'hostname' && !matched.length) {
|
||||
return { vms, summaryMatched: 0, summaryFilterApplied: true, summaryFilterFallback: true };
|
||||
}
|
||||
return { vms: matched, summaryMatched: matched.length, summaryFilterApplied: true, summaryFilterFallback: false };
|
||||
}
|
||||
|
||||
export function buildHostPayloadFromVm(candidate, options = {}) {
|
||||
@@ -216,17 +300,25 @@ export function buildHostPayloadFromVm(candidate, options = {}) {
|
||||
}
|
||||
|
||||
export async function previewVCenterHosts(options = {}) {
|
||||
const trace = [];
|
||||
const settings = getVCenterSettings();
|
||||
assertConfigured(settings);
|
||||
const sessionId = await createSession(settings);
|
||||
const vmPayload = await vcenterFetch(settings, '/api/vcenter/vm', { sessionId });
|
||||
try {
|
||||
assertConfigured(settings);
|
||||
} catch (err) {
|
||||
throw new VCenterTraceError(err.message, trace, 400);
|
||||
}
|
||||
const sessionId = await createSession(settings, trace);
|
||||
const vmPayload = await vcenterFetch(settings, '/api/vcenter/vm', { sessionId, trace });
|
||||
const allVms = unwrapList(vmPayload);
|
||||
const vms = filterSummaryVms(allVms, options.filter);
|
||||
const summaryFilter = filterSummaryVms(allVms, options.filter);
|
||||
const vms = summaryFilter.vms;
|
||||
const candidates = [];
|
||||
const diagnostics = {
|
||||
totalFromVCenter: allVms.length,
|
||||
scanned: 0,
|
||||
summaryMatched: vms.length,
|
||||
summaryMatched: summaryFilter.summaryMatched,
|
||||
summaryFilterApplied: summaryFilter.summaryFilterApplied,
|
||||
summaryFilterFallback: summaryFilter.summaryFilterFallback,
|
||||
resultLimit: options.limit || 100,
|
||||
sampleNames: allVms.slice(0, 12).map((vm) => vm.name || vm.vm || vm.id).filter(Boolean),
|
||||
filter: options.filter || {}
|
||||
@@ -236,8 +328,8 @@ export async function previewVCenterHosts(options = {}) {
|
||||
const vmId = vm.vm || vm.id;
|
||||
if (!vmId) continue;
|
||||
diagnostics.scanned += 1;
|
||||
const identity = await optionalVCenterFetch(settings, `/api/vcenter/vm/${encodeURIComponent(vmId)}/guest/identity`, sessionId);
|
||||
const interfaces = await optionalVCenterFetch(settings, `/api/vcenter/vm/${encodeURIComponent(vmId)}/guest/networking/interfaces`, sessionId);
|
||||
const identity = await optionalVCenterFetch(settings, `/api/vcenter/vm/${encodeURIComponent(vmId)}/guest/identity`, sessionId, trace);
|
||||
const interfaces = await optionalVCenterFetch(settings, `/api/vcenter/vm/${encodeURIComponent(vmId)}/guest/networking/interfaces`, sessionId, trace);
|
||||
const candidate = normalizeVCenterVm(vm, identity?.unavailable ? null : identity, interfaces?.unavailable ? null : interfaces);
|
||||
if (vmMatchesFilter(candidate, options.filter)) candidates.push(candidate);
|
||||
if (candidates.length >= (options.limit || 100)) break;
|
||||
@@ -248,6 +340,7 @@ export async function previewVCenterHosts(options = {}) {
|
||||
filter: options.filter || {},
|
||||
count: candidates.length,
|
||||
diagnostics,
|
||||
trace,
|
||||
candidates
|
||||
};
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user