This commit is contained in:
2026-06-25 13:48:07 -05:00
parent d8342a6475
commit 7f925aa8e5
8 changed files with 128 additions and 9 deletions

View File

@@ -7,6 +7,26 @@ 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;
const API_PROFILES = {
api: {
mode: 'api',
label: 'vSphere Automation API',
sessionPath: '/api/session',
vmListPath: '/api/vcenter/vm',
vmNameParam: 'names',
guestIdentityPath: (vmId) => `/api/vcenter/vm/${encodeURIComponent(vmId)}/guest/identity`,
guestNetworkingPath: (vmId) => `/api/vcenter/vm/${encodeURIComponent(vmId)}/guest/networking/interfaces`
},
rest: {
mode: 'rest',
label: 'Legacy vCenter REST API',
sessionPath: '/rest/com/vmware/cis/session',
vmListPath: '/rest/vcenter/vm',
vmNameParam: 'filter.names',
guestIdentityPath: (vmId) => `/rest/vcenter/vm/${encodeURIComponent(vmId)}/guest/identity`,
guestNetworkingPath: (vmId) => `/rest/vcenter/vm/${encodeURIComponent(vmId)}/guest/networking/interfaces`
}
};
export class VCenterTraceError extends Error {
constructor(message, trace = [], statusCode = 502) {
@@ -18,11 +38,13 @@ export class VCenterTraceError extends Error {
}
export function getVCenterSettings() {
const apiMode = getStringSetting('vcenter_api_mode', config.vcenter.apiMode || 'auto').toLowerCase();
return {
enabled: getBooleanSetting('vcenter_enabled', config.vcenter.enabled),
baseUrl: normalizeBaseUrl(getStringSetting('vcenter_base_url', config.vcenter.baseUrl)),
username: getStringSetting('vcenter_username', config.vcenter.username),
password: getStringSetting('vcenter_password', config.vcenter.password),
apiMode: ['auto', 'api', 'rest'].includes(apiMode) ? apiMode : 'auto',
allowUntrustedTls: getBooleanSetting('vcenter_allow_untrusted_tls', config.vcenter.allowUntrustedTls)
};
}
@@ -38,6 +60,7 @@ export function vcenterStatus() {
configured: Boolean(settings.baseUrl && settings.username && settings.password),
baseUrl: settings.baseUrl,
username: settings.username,
apiMode: settings.apiMode,
allowUntrustedTls: settings.allowUntrustedTls
};
}
@@ -72,7 +95,7 @@ function summarizePayload(payload) {
}
function bodyPreview(payload, path) {
if (path === '/api/session') return '[redacted vCenter session id]';
if (path === '/api/session' || path === '/rest/com/vmware/cis/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
@@ -94,6 +117,14 @@ function pushTrace(trace, entry) {
}
}
function pushTraceInfo(trace, message, details = {}) {
pushTrace(trace, {
info: true,
message,
details
});
}
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;
@@ -157,8 +188,8 @@ function unwrapList(payload) {
return [];
}
async function createSession(settings, trace = null) {
const payload = await vcenterFetch(settings, '/api/session', { method: 'POST', trace });
async function createSession(settings, profile, trace = null) {
const payload = await vcenterFetch(settings, profile.sessionPath, { method: 'POST', trace });
const sessionId = typeof payload === 'string' ? payload : payload?.value || payload?.session_id || payload?.id;
if (!sessionId) throw new VCenterTraceError('vCenter session authentication succeeded but no session id was returned.', trace);
return sessionId;
@@ -277,6 +308,17 @@ export function filterSummaryVms(vms = [], filter = {}) {
return { vms: matched, summaryMatched: matched.length, summaryFilterApplied: true, summaryFilterFallback: false };
}
function canUseServerSideNameFilter(filter = {}) {
return filter.field === 'name' && filter.operator === 'equals' && String(filter.value || '').trim();
}
export function buildVmListPath(profile, filter = {}) {
if (!canUseServerSideNameFilter(filter)) return profile.vmListPath;
const params = new URLSearchParams();
params.append(profile.vmNameParam, String(filter.value).trim());
return `${profile.vmListPath}?${params.toString()}`;
}
export function buildHostPayloadFromVm(candidate, options = {}) {
const tags = [...new Set(['vmware', 'vcenter', `vcenter:${candidate.id}`, ...(options.tags || [])].filter(Boolean))];
return {
@@ -307,8 +349,7 @@ export async function previewVCenterHosts(options = {}) {
} 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 { profile, sessionId, vmPayload } = await connectAndListVms(settings, options.filter, trace);
const allVms = unwrapList(vmPayload);
const summaryFilter = filterSummaryVms(allVms, options.filter);
const vms = summaryFilter.vms;
@@ -319,6 +360,9 @@ export async function previewVCenterHosts(options = {}) {
summaryMatched: summaryFilter.summaryMatched,
summaryFilterApplied: summaryFilter.summaryFilterApplied,
summaryFilterFallback: summaryFilter.summaryFilterFallback,
apiMode: profile.mode,
apiProfile: profile.label,
serverSideNameFilter: canUseServerSideNameFilter(options.filter),
resultLimit: options.limit || 100,
sampleNames: allVms.slice(0, 12).map((vm) => vm.name || vm.vm || vm.id).filter(Boolean),
filter: options.filter || {}
@@ -328,8 +372,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, trace);
const interfaces = await optionalVCenterFetch(settings, `/api/vcenter/vm/${encodeURIComponent(vmId)}/guest/networking/interfaces`, sessionId, trace);
const identity = await optionalVCenterFetch(settings, profile.guestIdentityPath(vmId), sessionId, trace);
const interfaces = await optionalVCenterFetch(settings, profile.guestNetworkingPath(vmId), 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;
@@ -344,3 +388,37 @@ export async function previewVCenterHosts(options = {}) {
candidates
};
}
function shouldTryFallback(err) {
return err instanceof VCenterTraceError && [404, 405, 501].includes(err.statusCode);
}
async function connectAndListWithProfile(settings, filter, profile, trace) {
pushTraceInfo(trace, `Using ${profile.label}`, {
mode: profile.mode,
sessionPath: profile.sessionPath,
vmListPath: buildVmListPath(profile, filter)
});
const sessionId = await createSession(settings, profile, trace);
const vmPayload = await vcenterFetch(settings, buildVmListPath(profile, filter), { sessionId, trace });
return { profile, sessionId, vmPayload };
}
async function connectAndListVms(settings, filter, trace) {
const modes = settings.apiMode === 'auto' ? ['api', 'rest'] : [settings.apiMode];
let lastError = null;
for (const mode of modes) {
try {
return await connectAndListWithProfile(settings, filter, API_PROFILES[mode], trace);
} catch (err) {
lastError = err;
if (settings.apiMode !== 'auto' || mode === modes.at(-1) || !shouldTryFallback(err)) throw err;
pushTraceInfo(trace, `Falling back after ${API_PROFILES[mode].label} failed`, {
mode,
error: err.message,
statusCode: err.statusCode
});
}
}
throw lastError || new VCenterTraceError('No vCenter API profile could be attempted.', trace);
}