Skip to content

Commit e2dc865

Browse files
committed
Update dataFetcher and add pbsUtils tests
1 parent 3d5c680 commit e2dc865

File tree

3 files changed

+640
-285
lines changed

3 files changed

+640
-285
lines changed

server/dataFetcher.js

Lines changed: 103 additions & 173 deletions
Original file line numberDiff line numberDiff line change
@@ -44,152 +44,123 @@ async function fetchDataForNode(apiClient, endpointId, nodeName) {
4444
};
4545
}
4646

47+
/**
48+
* Fetches and processes discovery data for a single PVE endpoint.
49+
* @param {string} endpointId - The unique ID of the PVE endpoint.
50+
* @param {Object} apiClient - The initialized Axios client instance for this endpoint.
51+
* @param {Object} config - The configuration object for this endpoint.
52+
* @returns {Promise<Object>} - { nodes: Array, vms: Array, containers: Array } for this endpoint.
53+
*/
54+
async function fetchDataForPveEndpoint(endpointId, apiClientInstance, config) {
55+
const endpointName = config.name || endpointId; // Use configured name or ID
56+
try {
57+
const nodesResponse = await apiClientInstance.get('/nodes');
58+
const nodes = nodesResponse.data.data;
59+
if (!nodes || !Array.isArray(nodes)) {
60+
console.warn(`[DataFetcher - ${endpointName}] No nodes found or unexpected format.`);
61+
return { nodes: [], vms: [], containers: [] };
62+
}
63+
64+
const guestPromises = nodes.map(node => fetchDataForNode(apiClientInstance, endpointName, node.node));
65+
const guestResults = await Promise.allSettled(guestPromises);
66+
67+
let endpointVms = [];
68+
let endpointContainers = [];
69+
let processedNodes = [];
70+
71+
guestResults.forEach((result, index) => {
72+
const correspondingNodeInfo = nodes[index];
73+
if (!correspondingNodeInfo || !correspondingNodeInfo.node) return;
74+
75+
const finalNode = {
76+
cpu: null, mem: null, disk: null, maxdisk: null, uptime: 0, loadavg: null, storage: [],
77+
node: correspondingNodeInfo.node,
78+
maxcpu: correspondingNodeInfo.maxcpu,
79+
maxmem: correspondingNodeInfo.maxmem,
80+
level: correspondingNodeInfo.level,
81+
status: correspondingNodeInfo.status || 'unknown',
82+
id: `${endpointName}-${correspondingNodeInfo.node}`,
83+
endpointId: endpointName, // Use the actual endpointName here
84+
};
85+
86+
if (result.status === 'fulfilled' && result.value) {
87+
const nodeData = result.value;
88+
// Use endpointName for constructing IDs
89+
endpointVms.push(...(nodeData.vms || []).map(vm => ({ ...vm, endpointId: endpointName, id: `${endpointName}-${vm.node}-${vm.vmid}` })));
90+
endpointContainers.push(...(nodeData.containers || []).map(ct => ({ ...ct, endpointId: endpointName, id: `${endpointName}-${ct.node}-${ct.vmid}` })));
91+
92+
if (nodeData.nodeStatus && Object.keys(nodeData.nodeStatus).length > 0) {
93+
const statusData = nodeData.nodeStatus;
94+
finalNode.cpu = statusData.cpu;
95+
finalNode.mem = statusData.memory?.used || statusData.mem;
96+
finalNode.disk = statusData.rootfs?.used || statusData.disk;
97+
finalNode.maxdisk = statusData.rootfs?.total || statusData.maxdisk;
98+
finalNode.uptime = statusData.uptime;
99+
finalNode.loadavg = statusData.loadavg;
100+
if (statusData.uptime > 0) {
101+
finalNode.status = 'online';
102+
}
103+
}
104+
finalNode.storage = nodeData.storage && nodeData.storage.length > 0 ? nodeData.storage : finalNode.storage;
105+
processedNodes.push(finalNode);
106+
} else {
107+
if (result.status === 'rejected') {
108+
console.error(`[DataFetcher - ${endpointName}] Error processing node ${correspondingNodeInfo.node}: ${result.reason?.message || result.reason}`);
109+
} else {
110+
console.warn(`[DataFetcher - ${endpointName}] Unexpected result status for node ${correspondingNodeInfo.node}: ${result.status}`);
111+
}
112+
processedNodes.push(finalNode); // Push node with defaults on failure
113+
}
114+
});
115+
116+
return { nodes: processedNodes, vms: endpointVms, containers: endpointContainers };
117+
118+
} catch (error) {
119+
const status = error.response?.status ? ` (Status: ${error.response.status})` : '';
120+
console.error(`[DataFetcher - ${endpointName}] Error fetching PVE discovery data${status}: ${error.message}`);
121+
// Return empty structure on endpoint-level failure
122+
return { nodes: [], vms: [], containers: [] };
123+
}
124+
}
125+
126+
47127
/**
48128
* Fetches structural PVE data: node list, statuses, VM/CT lists.
49129
* @param {Object} currentApiClients - Initialized PVE API clients.
50130
* @returns {Promise<Object>} - { nodes, vms, containers }
51131
*/
52132
async function fetchPveDiscoveryData(currentApiClients) {
53133
const pveEndpointIds = Object.keys(currentApiClients);
54-
let tempNodes = [], tempVms = [], tempContainers = [];
134+
let allNodes = [], allVms = [], allContainers = [];
55135

56136
if (pveEndpointIds.length === 0) {
57137
console.log("[DataFetcher] No PVE endpoints configured or initialized.");
58138
return { nodes: [], vms: [], containers: [] };
59139
}
60140

61141
console.log(`[DataFetcher] Fetching PVE discovery data for ${pveEndpointIds.length} endpoints...`);
62-
const pvePromises = pveEndpointIds.map(endpointId =>
63-
(async () => {
64-
const { client: apiClientInstance, config } = currentApiClients[endpointId];
65-
const endpointName = config.name || endpointId;
66-
try {
67-
const nodesResponse = await apiClientInstance.get('/nodes');
68-
const nodes = nodesResponse.data.data;
69-
if (!nodes || !Array.isArray(nodes)) {
70-
console.warn(`[DataFetcher - ${endpointName}] No nodes found or unexpected format.`);
71-
return { endpointId: endpointName, status: 'fulfilled', value: { nodes: [], vms: [], containers: [] } };
72-
}
73142

74-
const guestPromises = nodes.map(node => fetchDataForNode(apiClientInstance, endpointName, node.node));
75-
const guestResults = await Promise.allSettled(guestPromises);
76-
77-
let endpointVms = [];
78-
let endpointContainers = [];
79-
let processedNodes = []; // New array to store results
80-
81-
// Initialize endpointNodes with basic info from the /nodes call
82-
const defaultNode = {
83-
cpu: null,
84-
mem: null,
85-
disk: null,
86-
maxdisk: null,
87-
uptime: 0,
88-
loadavg: null,
89-
status: 'unknown', // Default status
90-
storage: [], // Default storage
91-
};
92-
93-
// Process guest results and merge status/storage into processedNodes
94-
guestResults.forEach((result, index) => {
95-
const correspondingNodeInfo = nodes[index]; // Get the original info from /nodes
96-
if (!correspondingNodeInfo || !correspondingNodeInfo.node) return;
97-
98-
// Initialize finalNode with defaults and info from /nodes endpoint
99-
const finalNode = {
100-
cpu: null,
101-
mem: null,
102-
disk: null,
103-
maxdisk: null,
104-
uptime: 0,
105-
loadavg: null,
106-
storage: [],
107-
node: correspondingNodeInfo.node,
108-
maxcpu: correspondingNodeInfo.maxcpu,
109-
maxmem: correspondingNodeInfo.maxmem,
110-
level: correspondingNodeInfo.level,
111-
status: correspondingNodeInfo.status || 'unknown',
112-
id: `${endpointName}-${correspondingNodeInfo.node}`,
113-
endpointId: endpointName,
114-
};
115-
116-
if (result.status === 'fulfilled' && result.value) {
117-
const nodeData = result.value;
118-
const currentEndpointId = endpointId; // Renamed from endpointId to avoid conflict in map
119-
120-
endpointVms.push(...(nodeData.vms || []).map(vm => ({ ...vm, endpointId: currentEndpointId, id: `${endpointName}-${vm.node}-${vm.vmid}` })));
121-
endpointContainers.push(...(nodeData.containers || []).map(ct => ({ ...ct, endpointId: currentEndpointId, id: `${endpointName}-${ct.node}-${ct.vmid}` })));
122-
123-
if (nodeData.nodeStatus && Object.keys(nodeData.nodeStatus).length > 0) {
124-
const statusData = nodeData.nodeStatus;
125-
finalNode.cpu = statusData.cpu;
126-
finalNode.mem = statusData.memory?.used || statusData.mem;
127-
finalNode.disk = statusData.rootfs?.used || statusData.disk;
128-
finalNode.maxdisk = statusData.rootfs?.total || statusData.maxdisk;
129-
finalNode.uptime = statusData.uptime;
130-
finalNode.loadavg = statusData.loadavg;
131-
// Update status only if uptime indicates online, otherwise keep the status from /nodes
132-
if (statusData.uptime > 0) {
133-
finalNode.status = 'online';
134-
}
135-
}
136-
finalNode.storage = nodeData.storage && nodeData.storage.length > 0 ? nodeData.storage : finalNode.storage;
137-
138-
processedNodes.push(finalNode);
139-
} else {
140-
if (result.status === 'rejected') {
141-
console.error(`[DataFetcher - ${endpointName}] Error processing node ${correspondingNodeInfo.node}: ${result.reason?.message || result.reason}`);
142-
} else {
143-
console.warn(`[DataFetcher - ${endpointName}] Unexpected result status for node ${correspondingNodeInfo.node}: ${result.status}`);
144-
}
145-
// Push the node with defaults if fetching detailed data failed
146-
processedNodes.push(finalNode);
147-
}
148-
});
149-
150-
// Return the newly constructed processedNodes array
151-
return { endpointId: endpointName, status: 'fulfilled', value: { nodes: processedNodes, vms: endpointVms, containers: endpointContainers } };
152-
}
153-
/* istanbul ignore next */ // Ignore this catch block - tested via side effects (logging, filtering)
154-
catch (error) {
155-
// This catch block handles failures in the initial /nodes call
156-
const status = error.response?.status ? ` (Status: ${error.response.status})` : '';
157-
console.error(`[DataFetcher - ${endpointName}] Error fetching PVE discovery data${status}: ${error.message}`);
158-
// Return a specific structure indicating failure for THIS endpoint
159-
return { endpointId: endpointName, status: 'rejected', reason: error.message || String(error) };
160-
}
161-
})()
162-
);
163-
164-
const pveOutcomes = await Promise.allSettled(pvePromises);
165-
166-
// Aggregate results from all endpoints, including partially successful ones
167-
pveOutcomes.forEach(endpointOutcome => {
168-
if (endpointOutcome.status === 'fulfilled') {
169-
if (endpointOutcome.value.status === 'fulfilled' && endpointOutcome.value.value) {
170-
const { nodes, vms, containers } = endpointOutcome.value.value;
171-
tempNodes.push(...nodes);
172-
if (vms && Array.isArray(vms)) {
173-
vms.forEach(vm => tempVms.push(vm));
174-
}
175-
if (containers && Array.isArray(containers)) {
176-
containers.forEach(ct => tempContainers.push(ct));
177-
}
178-
} else if (endpointOutcome.value.status === 'rejected') {
179-
// Log the reason for endpoint-level failure (e.g., /nodes failed)
180-
console.error(`[DataFetcher] PVE discovery failed for endpoint: ${endpointOutcome.value.endpointId}. Reason: ${endpointOutcome.value.reason}`);
181-
}
182-
} else {
183-
// This handles cases where the outer promise itself rejected (less likely with current structure)
184-
const reason = endpointOutcome.reason?.message || endpointOutcome.reason;
185-
// We might not know the endpoint ID here easily
186-
console.error(`[DataFetcher] Unhandled error processing PVE endpoint: ${reason}`);
143+
const pvePromises = pveEndpointIds.map(endpointId => {
144+
const { client: apiClientInstance, config } = currentApiClients[endpointId];
145+
// Pass endpointId, client, and config to the helper
146+
return fetchDataForPveEndpoint(endpointId, apiClientInstance, config);
147+
});
148+
149+
const pveResults = await Promise.all(pvePromises); // Wait for all endpoint fetches
150+
151+
// Aggregate results from all endpoints
152+
pveResults.forEach(result => {
153+
if (result) { // Check if result is not null/undefined (error handled in helper)
154+
allNodes.push(...(result.nodes || []));
155+
allVms.push(...(result.vms || []));
156+
allContainers.push(...(result.containers || []));
187157
}
188158
});
189159

190-
return { nodes: tempNodes, vms: tempVms, containers: tempContainers };
160+
return { nodes: allNodes, vms: allVms, containers: allContainers };
191161
}
192162

163+
193164
// --- PBS Data Fetching Functions ---
194165

195166
/**
@@ -360,13 +331,17 @@ async function fetchPbsData(currentPbsApiClients) {
360331
const allTasksResult = await fetchAllPbsTasksForProcessing(pbsClient, nodeName);
361332
console.log(`INFO: [DataFetcher - ${instanceName}] Tasks fetched. Result error: ${allTasksResult.error}, Tasks found: ${allTasksResult.tasks ? allTasksResult.tasks.length : 'null'}`);
362333

363-
if (allTasksResult.tasks) {
334+
if (allTasksResult.tasks && !allTasksResult.error) {
364335
console.log(`INFO: [DataFetcher - ${instanceName}] Processing tasks...`);
365336
const processedTasks = processPbsTasks(allTasksResult.tasks); // Assumes processPbsTasks is imported
366337
instanceData = { ...instanceData, ...processedTasks }; // Merge task summaries
367338
console.log(`INFO: [DataFetcher - ${instanceName}] Tasks processed.`);
368339
} else {
369-
console.warn(`WARN: [DataFetcher - ${instanceName}] No tasks to process or task fetching failed.`);
340+
console.warn(`WARN: [DataFetcher - ${instanceName}] No tasks to process or task fetching failed. Error flag: ${allTasksResult.error}, Tasks array: ${allTasksResult.tasks === null ? 'null' : (Array.isArray(allTasksResult.tasks) ? `array[${allTasksResult.tasks.length}]` : typeof allTasksResult.tasks)}`);
341+
// If tasks failed to fetch or process, ensure task-specific fields are not from a stale/default mock
342+
// instanceData.backupTasks = instanceData.backupTasks || []; // Or undefined/null if preferred by consumers
343+
// instanceData.verifyTasks = instanceData.verifyTasks || [];
344+
// instanceData.gcTasks = instanceData.gcTasks || [];
370345
}
371346

372347
instanceData.status = 'ok';
@@ -587,51 +562,6 @@ async function fetchMetricsData(runningVms, runningContainers, currentApiClients
587562
return allMetrics;
588563
}
589564

590-
async function fetchQemuAgentMemoryInfo(apiClient, nodeName, vmid, guestName, endpointName, currentMetrics) {
591-
try {
592-
const agentMemInfoResponse = await apiClient.post(`/nodes/${nodeName}/qemu/${vmid}/agent/get-memory-block-info`, {});
593-
594-
if (agentMemInfoResponse?.data?.data?.result) {
595-
const agentMem = agentMemInfoResponse.data.data.result;
596-
let guestMemoryDetails = null;
597-
598-
if (Array.isArray(agentMem) && agentMem.length > 0 && agentMem[0].hasOwnProperty('total') && agentMem[0].hasOwnProperty('free')) {
599-
guestMemoryDetails = agentMem[0];
600-
} else if (typeof agentMem === 'object' && agentMem !== null && agentMem.hasOwnProperty('total') && agentMem.hasOwnProperty('free')) {
601-
guestMemoryDetails = agentMem;
602-
}
603-
604-
if (guestMemoryDetails) {
605-
currentMetrics.guest_mem_total_bytes = guestMemoryDetails.total;
606-
currentMetrics.guest_mem_free_bytes = guestMemoryDetails.free;
607-
currentMetrics.guest_mem_available_bytes = guestMemoryDetails.available;
608-
currentMetrics.guest_mem_cached_bytes = guestMemoryDetails.cached;
609-
currentMetrics.guest_mem_buffers_bytes = guestMemoryDetails.buffers;
610-
611-
if (guestMemoryDetails.available !== undefined) {
612-
currentMetrics.guest_mem_actual_used_bytes = guestMemoryDetails.total - guestMemoryDetails.available;
613-
} else if (guestMemoryDetails.cached !== undefined && guestMemoryDetails.buffers !== undefined) {
614-
currentMetrics.guest_mem_actual_used_bytes = guestMemoryDetails.total - guestMemoryDetails.free - guestMemoryDetails.cached - guestMemoryDetails.buffers;
615-
} else {
616-
currentMetrics.guest_mem_actual_used_bytes = guestMemoryDetails.total - guestMemoryDetails.free;
617-
}
618-
console.log(`[Metrics Cycle - ${endpointName}] VM ${vmid} (${guestName}): Guest agent memory fetched: Actual Used: ${((currentMetrics.guest_mem_actual_used_bytes || 0) / (1024*1024)).toFixed(0)}MB`);
619-
} else {
620-
console.warn(`[Metrics Cycle - ${endpointName}] VM ${vmid} (${guestName}): Guest agent memory command 'get-memory-block-info' response format not as expected. Data:`, agentMemInfoResponse.data.data);
621-
}
622-
} else {
623-
console.warn(`[Metrics Cycle - ${endpointName}] VM ${vmid} (${guestName}): Guest agent memory command 'get-memory-block-info' did not return expected data structure. Response:`, agentMemInfoResponse.data);
624-
}
625-
} catch (agentError) {
626-
if (agentError.response && agentError.response.status === 500 && agentError.response.data && agentError.response.data.data && agentError.response.data.data.exitcode === -2) {
627-
console.log(`[Metrics Cycle - ${endpointName}] VM ${vmid} (${guestName}): QEMU Guest Agent not responsive or command 'get-memory-block-info' not available/supported. Error: ${agentError.message}`);
628-
} else {
629-
console.warn(`[Metrics Cycle - ${endpointName}] VM ${vmid} (${guestName}): Error fetching guest agent memory info: ${agentError.message}. Status: ${agentError.response?.status}`);
630-
}
631-
}
632-
return currentMetrics;
633-
}
634-
635565
module.exports = {
636566
fetchDiscoveryData,
637567
fetchPbsData, // Keep exporting the real one

0 commit comments

Comments
 (0)