diff --git a/README.md b/README.md index badc44f..5368de2 100644 --- a/README.md +++ b/README.md @@ -535,7 +535,7 @@ target hosts visible to the operator who creates or edits them. VMware vCenter import uses the effective Config / Integrations values (`vcenter_*` settings or the `VCENTER_*` environment variables). `vcenter_api_mode` can be `auto`, `api`, or `rest`. `api` uses the current vSphere Automation API flow (`POST /api/session`, `GET /api/vcenter/vm`). `rest` uses the legacy/community vCenter REST flow (`POST /rest/com/vmware/cis/session`, `GET /rest/vcenter/vm`). `auto` tries the current `/api` profile first and falls back to `/rest` when the endpoint is not supported. -For exact VM-name searches (`field: "name"`, `operator: "equals"`), POSHManager uses the documented server-side name filters: `names=` for `/api/vcenter/vm` and `filter.names=` for `/rest/vcenter/vm`. Contains/starts-with/ends-with searches are filtered locally because vCenter VM list filters do not provide wildcard contains semantics. The combined `hostname` field searches both the VM inventory name and the VMware Tools guest `host_name`/FQDN after enrichment. FQDN and IP discovery depends on VMware Tools data; when guest data is unavailable, the import falls back to the VM name as the host address and records the limitation in host notes. +For exact VM-name searches (`field: "name"`, `operator: "equals"`), POSHManager uses the documented server-side name filters: `names=` for `/api/vcenter/vm` and `filter.names=` for `/rest/vcenter/vm`. Contains/starts-with/ends-with searches are filtered locally because vCenter VM list filters do not provide wildcard contains semantics. The combined `hostname` field searches both the VM inventory name and the VMware Tools guest `host_name`/FQDN after enrichment. VM display-name matches are checked first, then the rest of the inventory is still enriched for guest-only hostname matches. Networking/IP enrichment is only requested for candidates that match the filter. FQDN and IP discovery depends on VMware Tools data; when guest data is unavailable, the import falls back to the VM name as the host address and records the limitation in host notes. For `field: "name"` filters, the API scans the full VM summary list returned by vCenter before guest enrichment, so large inventories are not accidentally missed by the preview limit. The preview response includes `diagnostics` with `totalFromVCenter`, `summaryMatched`, `scanned`, `resultLimit`, and `sampleNames`; the wizard displays those values when troubleshooting a no-result preview. diff --git a/server/services/vcenterService.js b/server/services/vcenterService.js index e628710..f3b0d9f 100644 --- a/server/services/vcenterService.js +++ b/server/services/vcenterService.js @@ -7,6 +7,7 @@ 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 VM_ENRICHMENT_CONCURRENCY = 8; const API_PROFILES = { api: { mode: 'api', @@ -319,8 +320,14 @@ export function filterSummaryVms(vms = [], filter = {}) { 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 }; + if (field === 'hostname') { + const matchedIds = new Set(matched.map((vm) => vm.vm || vm.id)); + return { + vms: [...matched, ...vms.filter((vm) => !matchedIds.has(vm.vm || vm.id))], + summaryMatched: matched.length, + summaryFilterApplied: true, + summaryFilterFallback: matched.length === 0 + }; } return { vms: matched, summaryMatched: matched.length, summaryFilterApplied: true, summaryFilterFallback: false }; } @@ -385,16 +392,17 @@ export async function previewVCenterHosts(options = {}) { 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, 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; - } + await enrichVmsForPreview({ + vms, + settings, + profile, + sessionId, + trace, + filter: options.filter, + limit: options.limit || 100, + diagnostics, + candidates + }); return { status: vcenterStatus(), @@ -406,6 +414,28 @@ export async function previewVCenterHosts(options = {}) { }; } +async function enrichVmsForPreview({ vms, settings, profile, sessionId, trace, filter, limit, diagnostics, candidates }) { + let index = 0; + async function worker() { + while (index < vms.length && candidates.length < limit) { + const vm = vms[index++]; + const vmId = vm.vm || vm.id; + if (!vmId) continue; + diagnostics.scanned += 1; + const identity = await optionalVCenterFetch(settings, profile.guestIdentityPath(vmId), sessionId, trace); + const candidateWithoutNetworking = normalizeVCenterVm(vm, identity?.unavailable ? null : identity, null); + if (!vmMatchesFilter(candidateWithoutNetworking, filter)) continue; + const interfaces = await optionalVCenterFetch(settings, profile.guestNetworkingPath(vmId), sessionId, trace); + const candidate = normalizeVCenterVm(vm, identity?.unavailable ? null : identity, interfaces?.unavailable ? null : interfaces); + if (candidates.length < limit) candidates.push(candidate); + } + } + + await Promise.all( + Array.from({ length: Math.min(VM_ENRICHMENT_CONCURRENCY, Math.max(vms.length, 1)) }, () => worker()) + ); +} + function shouldTryFallback(err) { return err instanceof VCenterTraceError && [404, 405, 501].includes(err.statusCode); } diff --git a/server/test/vcenter.test.mjs b/server/test/vcenter.test.mjs index 4752306..abd109a 100644 --- a/server/test/vcenter.test.mjs +++ b/server/test/vcenter.test.mjs @@ -72,7 +72,7 @@ test('filterSummaryVms prefilters hostname searches by VM name and falls back wh assert.equal(matched.summaryMatched, 1); assert.equal(matched.summaryFilterFallback, false); - assert.deepEqual(matched.vms.map((vm) => vm.vm), ['vm-1']); + assert.deepEqual(matched.vms.map((vm) => vm.vm), ['vm-1', 'vm-2']); assert.equal(fallback.summaryMatched, 0); assert.equal(fallback.summaryFilterFallback, true); assert.equal(fallback.vms.length, 2);