254 lines
9.8 KiB
JavaScript
254 lines
9.8 KiB
JavaScript
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
|
|
};
|
|
}
|