import { Buffer } from 'node:buffer'; import { Agent } from 'undici'; import { config } from '../config.js'; 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']; export function getVCenterSettings() { 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), allowUntrustedTls: getBooleanSetting('vcenter_allow_untrusted_tls', config.vcenter.allowUntrustedTls) }; } export function normalizeBaseUrl(value) { return String(value || '').trim().replace(/\/+$/, ''); } export function vcenterStatus() { const settings = getVCenterSettings(); return { enabled: settings.enabled, configured: Boolean(settings.baseUrl && settings.username && settings.password), baseUrl: settings.baseUrl, username: settings.username, allowUntrustedTls: settings.allowUntrustedTls }; } function assertConfigured(settings) { if (!settings.enabled) throw new Error('VMware vCenter integration is disabled in Configuration.'); if (!settings.baseUrl) throw new Error('VMware vCenter base URL is missing. Set vcenter_base_url in Configuration.'); if (!settings.username || !settings.password) throw new Error('VMware vCenter credentials are missing. Set vcenter_username and vcenter_password in Configuration.'); } function dispatcherFor(settings) { return settings.allowUntrustedTls ? new Agent({ connect: { rejectUnauthorized: false } }) : undefined; } async function parseVCenterResponse(response) { const text = await response.text(); if (!text) return null; try { return JSON.parse(text); } catch { return text.replace(/^"|"$/g, ''); } } async function vcenterFetch(settings, path, { method = 'GET', sessionId, allow404 = false } = {}) { 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}`, { method, headers, dispatcher: dispatcherFor(settings) }); 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}`); } return payload; } function unwrapList(payload) { if (Array.isArray(payload)) return payload; if (Array.isArray(payload?.value)) return payload.value; return []; } async function createSession(settings) { const payload = await vcenterFetch(settings, '/api/session', { method: 'POST' }); 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.'); return sessionId; } async function optionalVCenterFetch(settings, path, sessionId) { try { return await vcenterFetch(settings, path, { sessionId, allow404: true }); } catch (err) { return { unavailable: true, reason: err.message }; } } function firstIpFromInterfaces(interfaces = []) { for (const nic of interfaces) { const addresses = nic?.ip?.ip_addresses || nic?.ip_addresses || []; const firstUsable = addresses.find((entry) => { const address = entry?.ip_address || entry?.address || entry; return address && !String(address).startsWith('fe80:'); }); if (firstUsable) return firstUsable.ip_address || firstUsable.address || firstUsable; } return ''; } function allIpsFromInterfaces(interfaces = []) { return interfaces.flatMap((nic) => { const addresses = nic?.ip?.ip_addresses || nic?.ip_addresses || []; return addresses .map((entry) => entry?.ip_address || entry?.address || entry) .filter((address) => address && !String(address).startsWith('fe80:')); }); } function inferOsFamily(identity = {}) { const haystack = [identity.family, identity.name, identity.full_name, identity.guest_OS].filter(Boolean).join(' ').toLowerCase(); if (WINDOWS_HINTS.some((hint) => haystack.includes(hint))) return 'windows'; if (LINUX_HINTS.some((hint) => haystack.includes(hint))) return 'linux'; return 'windows'; } export function normalizeVCenterVm(vm, identity = null, interfacesPayload = null) { const interfaces = unwrapList(interfacesPayload); const interfaceIp = firstIpFromInterfaces(interfaces); const identityIp = identity?.ip_address || identity?.ipAddress || ''; const fqdn = identity?.host_name || identity?.hostName || ''; const name = vm.name || fqdn || vm.vm || vm.id; const address = fqdn || identityIp || interfaceIp || name; const ipAddresses = [...new Set([identityIp, interfaceIp, ...allIpsFromInterfaces(interfaces)].filter(Boolean))]; return { id: vm.vm || vm.id, name, fqdn, address, ipAddress: ipAddresses[0] || '', ipAddresses, powerState: vm.power_state || vm.powerState || '', osFamily: inferOsFamily(identity || {}), guestFullName: identity?.full_name || identity?.name || '', toolsAvailable: Boolean(identity || interfaces.length), source: 'vcenter' }; } function normalizeVCenterSummaryVm(vm) { return { id: vm.vm || vm.id, name: vm.name || vm.vm || vm.id || '', fqdn: '', address: vm.name || vm.vm || vm.id || '', ipAddress: '', ipAddresses: [], powerState: vm.power_state || vm.powerState || '', osFamily: 'windows', guestFullName: '', toolsAvailable: false, source: 'vcenter' }; } export function vmMatchesFilter(candidate, filter = {}) { const operator = filter.operator || 'contains'; const needle = String(filter.value || '').trim().toLowerCase(); if (!needle) return true; const field = filter.field || 'hostname'; if (field === 'ip') { const values = [candidate.ipAddress, ...(candidate.ipAddresses || [])] .filter(Boolean) .map((value) => String(value).toLowerCase()); if (operator === 'equals') return values.some((value) => value === needle); if (operator === 'startsWith') return values.some((value) => value.startsWith(needle)); if (operator === 'endsWith') return values.some((value) => value.endsWith(needle)); return values.some((value) => value.includes(needle)); } const haystack = { name: candidate.name, hostname: candidate.name || candidate.fqdn, fqdn: candidate.fqdn || candidate.address, }[field] || candidate.name; const value = String(haystack || '').toLowerCase(); if (operator === 'equals') return value === needle; if (operator === 'startsWith') return value.startsWith(needle); if (operator === 'endsWith') return value.endsWith(needle); return value.includes(needle); } export function filterSummaryVms(vms = [], filter = {}) { if ((filter?.field || 'hostname') !== 'name') return vms; return vms.filter((vm) => vmMatchesFilter(normalizeVCenterSummaryVm(vm), filter)); } export function buildHostPayloadFromVm(candidate, options = {}) { const tags = [...new Set(['vmware', 'vcenter', `vcenter:${candidate.id}`, ...(options.tags || [])].filter(Boolean))]; return { name: candidate.name, fqdn: candidate.fqdn || '', address: candidate.address || candidate.ipAddress || candidate.name, osFamily: candidate.osFamily || 'windows', transport: options.transport || 'winrm', port: options.port || null, credentialId: options.credentialId || null, tags, notes: [ `Imported from VMware vCenter VM ${candidate.id}.`, candidate.powerState ? `Power state: ${candidate.powerState}.` : '', candidate.guestFullName ? `Guest OS: ${candidate.guestFullName}.` : '', candidate.ipAddresses?.length ? `IP addresses: ${candidate.ipAddresses.join(', ')}.` : 'IP address unavailable from VMware Tools at import time.' ].filter(Boolean).join(' '), visibility: options.visibility || 'shared', groupId: options.visibility === 'group' ? options.groupId || null : null }; } export async function previewVCenterHosts(options = {}) { const settings = getVCenterSettings(); assertConfigured(settings); const sessionId = await createSession(settings); const vmPayload = await vcenterFetch(settings, '/api/vcenter/vm', { sessionId }); const allVms = unwrapList(vmPayload); const vms = filterSummaryVms(allVms, options.filter); const candidates = []; const diagnostics = { totalFromVCenter: allVms.length, scanned: 0, summaryMatched: vms.length, resultLimit: options.limit || 100, sampleNames: allVms.slice(0, 12).map((vm) => vm.name || vm.vm || vm.id).filter(Boolean), filter: options.filter || {} }; for (const vm of vms) { 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 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; } return { status: vcenterStatus(), filter: options.filter || {}, count: candidates.length, diagnostics, candidates }; }