Initial
This commit is contained in:
@@ -533,6 +533,8 @@ 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). The backend creates a vCenter REST session with `POST /api/session`, lists VMs from `/api/vcenter/vm`, and then best-effort enriches each candidate from guest identity/networking endpoints. 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.
|
VMware vCenter import uses the effective Config / Integrations values (`vcenter_*` settings or the `VCENTER_*` environment variables). The backend creates a vCenter REST session with `POST /api/session`, lists VMs from `/api/vcenter/vm`, and then best-effort enriches each candidate from guest identity/networking endpoints. 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.
|
||||||
|
|
||||||
Preview payload:
|
Preview payload:
|
||||||
|
|
||||||
```json
|
```json
|
||||||
|
|||||||
@@ -425,6 +425,7 @@
|
|||||||
:loading="vcenterImportLoading"
|
:loading="vcenterImportLoading"
|
||||||
:error="vcenterImportError"
|
:error="vcenterImportError"
|
||||||
:status="vcenterImportStatus"
|
:status="vcenterImportStatus"
|
||||||
|
:diagnostics="vcenterImportDiagnostics"
|
||||||
@close="vcenterImportOpen = false"
|
@close="vcenterImportOpen = false"
|
||||||
@preview="previewVCenterImport"
|
@preview="previewVCenterImport"
|
||||||
@import="importVCenterHosts"
|
@import="importVCenterHosts"
|
||||||
@@ -980,6 +981,7 @@ const vcenterImportLoading = ref(false);
|
|||||||
const vcenterImportError = ref('');
|
const vcenterImportError = ref('');
|
||||||
const vcenterPreviewRows = ref([]);
|
const vcenterPreviewRows = ref([]);
|
||||||
const vcenterImportStatus = ref(null);
|
const vcenterImportStatus = ref(null);
|
||||||
|
const vcenterImportDiagnostics = ref(null);
|
||||||
const credentialModalOpen = ref(false);
|
const credentialModalOpen = ref(false);
|
||||||
const runPlanModalOpen = ref(false);
|
const runPlanModalOpen = ref(false);
|
||||||
const variableModalOpen = ref(false);
|
const variableModalOpen = ref(false);
|
||||||
@@ -2349,6 +2351,7 @@ async function saveHost() {
|
|||||||
async function openVCenterImport() {
|
async function openVCenterImport() {
|
||||||
vcenterImportOpen.value = true;
|
vcenterImportOpen.value = true;
|
||||||
vcenterImportError.value = '';
|
vcenterImportError.value = '';
|
||||||
|
vcenterImportDiagnostics.value = null;
|
||||||
try {
|
try {
|
||||||
vcenterImportStatus.value = await api.get('/api/hosts/import/vcenter/status');
|
vcenterImportStatus.value = await api.get('/api/hosts/import/vcenter/status');
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
@@ -2362,8 +2365,9 @@ async function previewVCenterImport(payload) {
|
|||||||
try {
|
try {
|
||||||
const result = await api.post('/api/hosts/import/vcenter/preview', payload);
|
const result = await api.post('/api/hosts/import/vcenter/preview', payload);
|
||||||
vcenterImportStatus.value = result.status;
|
vcenterImportStatus.value = result.status;
|
||||||
|
vcenterImportDiagnostics.value = result.diagnostics || null;
|
||||||
vcenterPreviewRows.value = result.candidates || [];
|
vcenterPreviewRows.value = result.candidates || [];
|
||||||
notify(`Found ${result.count || 0} vCenter host candidate(s)`);
|
notify(`Found ${result.count || 0} vCenter host candidate(s) from ${result.diagnostics?.totalFromVCenter ?? 'unknown'} VM(s)`);
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
vcenterImportError.value = err.message;
|
vcenterImportError.value = err.message;
|
||||||
notify(err.message, 'error');
|
notify(err.message, 'error');
|
||||||
@@ -2379,6 +2383,7 @@ async function importVCenterHosts(payload) {
|
|||||||
const result = await api.post('/api/hosts/import/vcenter', payload);
|
const result = await api.post('/api/hosts/import/vcenter', payload);
|
||||||
await refreshAll();
|
await refreshAll();
|
||||||
vcenterPreviewRows.value = [];
|
vcenterPreviewRows.value = [];
|
||||||
|
vcenterImportDiagnostics.value = null;
|
||||||
vcenterImportOpen.value = false;
|
vcenterImportOpen.value = false;
|
||||||
const skipped = result.skipped?.length || 0;
|
const skipped = result.skipped?.length || 0;
|
||||||
notify(`Imported ${result.imported?.length || 0} host(s)${skipped ? `, skipped ${skipped} duplicate(s)` : ''}`);
|
notify(`Imported ${result.imported?.length || 0} host(s)${skipped ? `, skipped ${skipped} duplicate(s)` : ''}`);
|
||||||
|
|||||||
@@ -115,6 +115,13 @@
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div v-if="error" class="inline-error">{{ error }}</div>
|
<div v-if="error" class="inline-error">{{ error }}</div>
|
||||||
|
<div v-if="diagnostics" class="vcenter-diagnostics">
|
||||||
|
<strong>Preview diagnostics</strong>
|
||||||
|
<span>{{ diagnostics.totalFromVCenter }} VM(s) returned by vCenter</span>
|
||||||
|
<span>{{ diagnostics.summaryMatched }} VM(s) matched before guest enrichment</span>
|
||||||
|
<span>{{ diagnostics.scanned }} VM(s) scanned for guest FQDN/IP details</span>
|
||||||
|
<small v-if="diagnostics.sampleNames?.length">Sample inventory names: {{ diagnostics.sampleNames.join(', ') }}</small>
|
||||||
|
</div>
|
||||||
<div class="preview-toolbar">
|
<div class="preview-toolbar">
|
||||||
<button class="ghost-button compact" type="button" :disabled="!previewRows.length" @click="toggleAll">
|
<button class="ghost-button compact" type="button" :disabled="!previewRows.length" @click="toggleAll">
|
||||||
<CheckSquare :size="15" />{{ allSelected ? 'Clear selection' : 'Select all' }}
|
<CheckSquare :size="15" />{{ allSelected ? 'Clear selection' : 'Select all' }}
|
||||||
@@ -177,7 +184,8 @@ const props = defineProps({
|
|||||||
groups: { type: Array, default: () => [] },
|
groups: { type: Array, default: () => [] },
|
||||||
loading: Boolean,
|
loading: Boolean,
|
||||||
error: { type: String, default: '' },
|
error: { type: String, default: '' },
|
||||||
status: { type: Object, default: null }
|
status: { type: Object, default: null },
|
||||||
|
diagnostics: { type: Object, default: null }
|
||||||
});
|
});
|
||||||
|
|
||||||
const emit = defineEmits(['close', 'preview', 'import']);
|
const emit = defineEmits(['close', 'preview', 'import']);
|
||||||
|
|||||||
@@ -6981,6 +6981,40 @@ input:read-only {
|
|||||||
margin-bottom: 12px;
|
margin-bottom: 12px;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.vcenter-diagnostics {
|
||||||
|
display: flex;
|
||||||
|
flex-wrap: wrap;
|
||||||
|
gap: 8px;
|
||||||
|
align-items: center;
|
||||||
|
margin-bottom: 12px;
|
||||||
|
padding: 10px 12px;
|
||||||
|
border: 1px solid rgba(117, 216, 255, .16);
|
||||||
|
border-radius: 14px;
|
||||||
|
color: rgba(219, 227, 255, .7);
|
||||||
|
background: rgba(88, 221, 255, .055);
|
||||||
|
}
|
||||||
|
|
||||||
|
.vcenter-diagnostics strong {
|
||||||
|
color: rgba(255, 255, 255, .9);
|
||||||
|
}
|
||||||
|
|
||||||
|
.vcenter-diagnostics span {
|
||||||
|
display: inline-flex;
|
||||||
|
align-items: center;
|
||||||
|
min-height: 24px;
|
||||||
|
padding: 4px 8px;
|
||||||
|
border-radius: 999px;
|
||||||
|
border: 1px solid rgba(139, 224, 255, .14);
|
||||||
|
background: rgba(3, 8, 18, .35);
|
||||||
|
font-size: .76rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.vcenter-diagnostics small {
|
||||||
|
flex-basis: 100%;
|
||||||
|
color: rgba(219, 227, 255, .52);
|
||||||
|
overflow-wrap: anywhere;
|
||||||
|
}
|
||||||
|
|
||||||
.vcenter-preview-table {
|
.vcenter-preview-table {
|
||||||
position: relative;
|
position: relative;
|
||||||
overflow: auto;
|
overflow: auto;
|
||||||
|
|||||||
@@ -146,6 +146,22 @@ export function normalizeVCenterVm(vm, identity = null, interfacesPayload = null
|
|||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
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 = {}) {
|
export function vmMatchesFilter(candidate, filter = {}) {
|
||||||
const operator = filter.operator || 'contains';
|
const operator = filter.operator || 'contains';
|
||||||
const needle = String(filter.value || '').trim().toLowerCase();
|
const needle = String(filter.value || '').trim().toLowerCase();
|
||||||
@@ -172,6 +188,11 @@ export function vmMatchesFilter(candidate, filter = {}) {
|
|||||||
return value.includes(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 = {}) {
|
export function buildHostPayloadFromVm(candidate, options = {}) {
|
||||||
const tags = [...new Set(['vmware', 'vcenter', `vcenter:${candidate.id}`, ...(options.tags || [])].filter(Boolean))];
|
const tags = [...new Set(['vmware', 'vcenter', `vcenter:${candidate.id}`, ...(options.tags || [])].filter(Boolean))];
|
||||||
return {
|
return {
|
||||||
@@ -199,12 +220,22 @@ export async function previewVCenterHosts(options = {}) {
|
|||||||
assertConfigured(settings);
|
assertConfigured(settings);
|
||||||
const sessionId = await createSession(settings);
|
const sessionId = await createSession(settings);
|
||||||
const vmPayload = await vcenterFetch(settings, '/api/vcenter/vm', { sessionId });
|
const vmPayload = await vcenterFetch(settings, '/api/vcenter/vm', { sessionId });
|
||||||
const vms = unwrapList(vmPayload).slice(0, Math.max(options.limit || 100, 1) * 4);
|
const allVms = unwrapList(vmPayload);
|
||||||
|
const vms = filterSummaryVms(allVms, options.filter);
|
||||||
const candidates = [];
|
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) {
|
for (const vm of vms) {
|
||||||
const vmId = vm.vm || vm.id;
|
const vmId = vm.vm || vm.id;
|
||||||
if (!vmId) continue;
|
if (!vmId) continue;
|
||||||
|
diagnostics.scanned += 1;
|
||||||
const identity = await optionalVCenterFetch(settings, `/api/vcenter/vm/${encodeURIComponent(vmId)}/guest/identity`, sessionId);
|
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 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);
|
const candidate = normalizeVCenterVm(vm, identity?.unavailable ? null : identity, interfaces?.unavailable ? null : interfaces);
|
||||||
@@ -216,6 +247,7 @@ export async function previewVCenterHosts(options = {}) {
|
|||||||
status: vcenterStatus(),
|
status: vcenterStatus(),
|
||||||
filter: options.filter || {},
|
filter: options.filter || {},
|
||||||
count: candidates.length,
|
count: candidates.length,
|
||||||
|
diagnostics,
|
||||||
candidates
|
candidates
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -5,6 +5,7 @@ import { load } from './setup.mjs';
|
|||||||
|
|
||||||
const {
|
const {
|
||||||
buildHostPayloadFromVm,
|
buildHostPayloadFromVm,
|
||||||
|
filterSummaryVms,
|
||||||
normalizeBaseUrl,
|
normalizeBaseUrl,
|
||||||
normalizeVCenterVm,
|
normalizeVCenterVm,
|
||||||
vmMatchesFilter
|
vmMatchesFilter
|
||||||
@@ -37,6 +38,17 @@ test('vmMatchesFilter supports hostname contains and IP equals matching', () =>
|
|||||||
assert.equal(vmMatchesFilter(candidate, { field: 'fqdn', operator: 'startsWith', value: 'db' }), false);
|
assert.equal(vmMatchesFilter(candidate, { field: 'fqdn', operator: 'startsWith', value: 'db' }), 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}` })),
|
||||||
|
{ vm: 'vm-901', name: 'MCS-WIN-APP01' }
|
||||||
|
];
|
||||||
|
const matched = filterSummaryVms(vms, { field: 'name', operator: 'contains', value: 'MCS' });
|
||||||
|
|
||||||
|
assert.equal(matched.length, 1);
|
||||||
|
assert.equal(matched[0].name, 'MCS-WIN-APP01');
|
||||||
|
});
|
||||||
|
|
||||||
test('buildHostPayloadFromVm attaches credential, tags, transport, and import notes', () => {
|
test('buildHostPayloadFromVm attaches credential, tags, transport, and import notes', () => {
|
||||||
const payload = buildHostPayloadFromVm(
|
const payload = buildHostPayloadFromVm(
|
||||||
{
|
{
|
||||||
|
|||||||
Reference in New Issue
Block a user