Initial
This commit is contained in:
@@ -46,6 +46,7 @@ export const config = {
|
||||
username: process.env.VCENTER_USERNAME || '',
|
||||
password: process.env.VCENTER_PASSWORD || '',
|
||||
apiMode: process.env.VCENTER_API_MODE || 'auto',
|
||||
requestTimeoutMs: Number(process.env.VCENTER_REQUEST_TIMEOUT_MS || 20000),
|
||||
allowUntrustedTls: (process.env.VCENTER_ALLOW_UNTRUSTED_TLS || 'false').toLowerCase() === 'true'
|
||||
},
|
||||
entra: {
|
||||
|
||||
@@ -39,12 +39,14 @@ export class VCenterTraceError extends Error {
|
||||
|
||||
export function getVCenterSettings() {
|
||||
const apiMode = getStringSetting('vcenter_api_mode', config.vcenter.apiMode || 'auto').toLowerCase();
|
||||
const requestTimeoutMs = Number(getStringSetting('vcenter_request_timeout_ms', String(config.vcenter.requestTimeoutMs || 20000)));
|
||||
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',
|
||||
requestTimeoutMs: Number.isFinite(requestTimeoutMs) && requestTimeoutMs > 0 ? requestTimeoutMs : 20000,
|
||||
allowUntrustedTls: getBooleanSetting('vcenter_allow_untrusted_tls', config.vcenter.allowUntrustedTls)
|
||||
};
|
||||
}
|
||||
@@ -61,6 +63,7 @@ export function vcenterStatus() {
|
||||
baseUrl: settings.baseUrl,
|
||||
username: settings.username,
|
||||
apiMode: settings.apiMode,
|
||||
requestTimeoutMs: settings.requestTimeoutMs,
|
||||
allowUntrustedTls: settings.allowUntrustedTls
|
||||
};
|
||||
}
|
||||
@@ -137,7 +140,8 @@ async function vcenterFetch(settings, path, { method = 'GET', sessionId, allow40
|
||||
response = await fetch(`${settings.baseUrl}${path}`, {
|
||||
method,
|
||||
headers,
|
||||
dispatcher: dispatcherFor(settings)
|
||||
dispatcher: dispatcherFor(settings),
|
||||
signal: AbortSignal.timeout(settings.requestTimeoutMs)
|
||||
});
|
||||
payload = await parseVCenterResponse(response);
|
||||
} catch (err) {
|
||||
@@ -150,9 +154,15 @@ async function vcenterFetch(settings, path, { method = 'GET', sessionId, allow40
|
||||
'vmware-api-session-id': headers['vmware-api-session-id'] ? '[redacted session id]' : undefined
|
||||
},
|
||||
durationMs: Date.now() - startedAt,
|
||||
networkError: err.message
|
||||
timeoutMs: settings.requestTimeoutMs,
|
||||
networkError: err.name === 'TimeoutError'
|
||||
? `Timed out after ${settings.requestTimeoutMs} ms waiting for vCenter.`
|
||||
: err.message
|
||||
});
|
||||
throw new VCenterTraceError(`Unable to reach vCenter ${method} ${path}: ${err.message}`, trace);
|
||||
const message = err.name === 'TimeoutError'
|
||||
? `Timed out after ${settings.requestTimeoutMs} ms waiting for vCenter ${method} ${path}.`
|
||||
: `Unable to reach vCenter ${method} ${path}: ${err.message}`;
|
||||
throw new VCenterTraceError(message, trace);
|
||||
}
|
||||
|
||||
pushTrace(trace, {
|
||||
@@ -225,12 +235,19 @@ function allIpsFromInterfaces(interfaces = []) {
|
||||
}
|
||||
|
||||
function inferOsFamily(identity = {}) {
|
||||
const haystack = [identity.family, identity.name, identity.full_name, identity.guest_OS].filter(Boolean).join(' ').toLowerCase();
|
||||
const haystack = [identity.family, identity.name, localizedText(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';
|
||||
}
|
||||
|
||||
function localizedText(value) {
|
||||
if (!value) return '';
|
||||
if (typeof value === 'string') return value;
|
||||
if (typeof value === 'object') return value.default_message || value.id || '';
|
||||
return String(value);
|
||||
}
|
||||
|
||||
export function normalizeVCenterVm(vm, identity = null, interfacesPayload = null) {
|
||||
const interfaces = unwrapList(interfacesPayload);
|
||||
const interfaceIp = firstIpFromInterfaces(interfaces);
|
||||
@@ -249,7 +266,7 @@ export function normalizeVCenterVm(vm, identity = null, interfacesPayload = null
|
||||
ipAddresses,
|
||||
powerState: vm.power_state || vm.powerState || '',
|
||||
osFamily: inferOsFamily(identity || {}),
|
||||
guestFullName: identity?.full_name || identity?.name || '',
|
||||
guestFullName: localizedText(identity?.full_name) || identity?.name || '',
|
||||
toolsAvailable: Boolean(identity || interfaces.length),
|
||||
source: 'vcenter'
|
||||
};
|
||||
@@ -285,16 +302,16 @@ export function vmMatchesFilter(candidate, filter = {}) {
|
||||
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);
|
||||
const haystacks = {
|
||||
name: [candidate.name],
|
||||
hostname: [candidate.name, candidate.fqdn, candidate.address].filter(Boolean),
|
||||
fqdn: [candidate.fqdn, candidate.address].filter(Boolean),
|
||||
}[field] || [candidate.name];
|
||||
const values = haystacks.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));
|
||||
}
|
||||
|
||||
export function filterSummaryVms(vms = [], filter = {}) {
|
||||
|
||||
@@ -40,6 +40,7 @@ export function settingDefinitions() {
|
||||
{ key: 'vcenter_username', value: config.vcenter.username, pinned: envProvided('VCENTER_USERNAME') },
|
||||
{ key: 'vcenter_password', value: config.vcenter.password, pinned: envProvided('VCENTER_PASSWORD') },
|
||||
{ key: 'vcenter_api_mode', value: config.vcenter.apiMode, pinned: envProvided('VCENTER_API_MODE') },
|
||||
{ key: 'vcenter_request_timeout_ms', value: String(config.vcenter.requestTimeoutMs), pinned: envProvided('VCENTER_REQUEST_TIMEOUT_MS') },
|
||||
{ key: 'vcenter_allow_untrusted_tls', value: String(config.vcenter.allowUntrustedTls), pinned: envProvided('VCENTER_ALLOW_UNTRUSTED_TLS') }
|
||||
];
|
||||
}
|
||||
|
||||
@@ -19,7 +19,7 @@ test('normalizeBaseUrl trims trailing slashes', () => {
|
||||
test('normalizeVCenterVm maps guest identity and networking into a host candidate', () => {
|
||||
const candidate = normalizeVCenterVm(
|
||||
{ vm: 'vm-42', name: 'ENG-ENT-APP01', power_state: 'POWERED_ON' },
|
||||
{ host_name: 'eng-ent-app01.contoso.local', ip_address: '10.42.1.20', full_name: 'Microsoft Windows Server 2022' },
|
||||
{ host_name: 'eng-ent-app01.contoso.local', ip_address: '10.42.1.20', full_name: { default_message: 'Microsoft Windows Server 2022' } },
|
||||
[{ ip: { ip_addresses: [{ ip_address: 'fe80::1' }, { ip_address: '10.42.1.21' }] } }]
|
||||
);
|
||||
|
||||
@@ -29,16 +29,29 @@ test('normalizeVCenterVm maps guest identity and networking into a host candidat
|
||||
assert.equal(candidate.address, 'eng-ent-app01.contoso.local');
|
||||
assert.deepEqual(candidate.ipAddresses, ['10.42.1.20', '10.42.1.21']);
|
||||
assert.equal(candidate.osFamily, 'windows');
|
||||
assert.equal(candidate.guestFullName, 'Microsoft Windows Server 2022');
|
||||
});
|
||||
|
||||
test('vmMatchesFilter supports hostname contains and IP equals matching', () => {
|
||||
const candidate = { name: 'ENG-ENT-APP01', fqdn: 'app01.contoso.local', ipAddress: '10.42.1.20', ipAddresses: ['10.42.1.20'] };
|
||||
const candidate = { name: 'ENG-ENT-APP01', fqdn: 'app01.contoso.local', address: 'app01.contoso.local', ipAddress: '10.42.1.20', ipAddresses: ['10.42.1.20'] };
|
||||
|
||||
assert.equal(vmMatchesFilter(candidate, { field: 'hostname', operator: 'contains', value: 'eng-ent' }), true);
|
||||
assert.equal(vmMatchesFilter(candidate, { field: 'hostname', operator: 'contains', value: 'contoso' }), true);
|
||||
assert.equal(vmMatchesFilter(candidate, { field: 'ip', operator: 'equals', value: '10.42.1.20' }), true);
|
||||
assert.equal(vmMatchesFilter(candidate, { field: 'fqdn', operator: 'startsWith', value: 'db' }), false);
|
||||
});
|
||||
|
||||
test('vmMatchesFilter matches guest-only host names after enrichment', () => {
|
||||
const candidate = normalizeVCenterVm(
|
||||
{ vm: 'vm-200', name: 'KSUS01PAPP01', power_state: 'POWERED_ON' },
|
||||
{ host_name: 'MCS-WIN-APP01.ent.mitekindustries.com', ip_address: '10.42.5.44', family: 'WINDOWS' },
|
||||
[]
|
||||
);
|
||||
|
||||
assert.equal(vmMatchesFilter(candidate, { field: 'hostname', operator: 'contains', value: 'MCS' }), true);
|
||||
assert.equal(vmMatchesFilter(candidate, { field: 'name', operator: 'contains', value: 'MCS' }), false);
|
||||
});
|
||||
|
||||
test('filterSummaryVms scans the full VM summary list for VM-name filters', () => {
|
||||
const vms = [
|
||||
...Array.from({ length: 900 }, (_, index) => ({ vm: `vm-${index}`, name: `ZZZ-${index}` })),
|
||||
|
||||
Reference in New Issue
Block a user