This commit is contained in:
2026-06-25 14:01:52 -05:00
parent acc94dc6a1
commit 6f77fac58e
3 changed files with 44 additions and 14 deletions

View File

@@ -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=<vm>` for `/api/vcenter/vm` and `filter.names=<vm>` 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=<vm>` for `/api/vcenter/vm` and `filter.names=<vm>` 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.

View File

@@ -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);
}

View File

@@ -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);