Enhance Proxmox API client with cluster detection and improved error handling

This commit is contained in:
courtmanr@gmail.com
2025-03-12 10:10:49 +00:00
parent 9898a1cf11
commit e35a44fd84
3 changed files with 155 additions and 75 deletions
+17 -7
View File
@@ -15,16 +15,26 @@ export async function isNodeInCluster(this: ProxmoxClient): Promise<{ isCluster:
// Try to access the cluster status endpoint
const response = await this.client.get('/cluster/status');
if (response.data && response.data.data && Array.isArray(response.data.data) && response.data.data.length > 0) {
// If we get a valid response with data, the node is part of a cluster
// Find the cluster name from the response
// Log the full response for debugging
this.logger.debug(`Cluster status response: ${JSON.stringify(response.data)}`);
if (response.data && response.data.data && Array.isArray(response.data.data)) {
// Only consider it a cluster if we find an item with type: "cluster"
const clusterInfo = response.data.data.find((item: any) => item.type === 'cluster');
const clusterName = clusterInfo?.name || 'proxmox-cluster';
this.logger.info(`Node is part of cluster: ${clusterName}`);
return { isCluster: true, clusterName };
// Log the cluster info for debugging
this.logger.debug(`Cluster info: ${JSON.stringify(clusterInfo)}`);
if (clusterInfo && clusterInfo.type === 'cluster') {
const clusterName = clusterInfo.name || 'proxmox-cluster';
this.logger.info(`Node is part of cluster: ${clusterName}`);
return { isCluster: true, clusterName };
} else {
this.logger.info('Node has cluster API but no cluster type found - not part of a cluster');
return { isCluster: false, clusterName: '' };
}
} else {
this.logger.info('Node is not part of a cluster');
this.logger.info('Node is not part of a cluster (empty response data)');
return { isCluster: false, clusterName: '' };
}
} catch (error: any) {
+84 -67
View File
@@ -1,5 +1,27 @@
import { ProxmoxClient } from './index';
import { ProxmoxVM, ProxmoxContainer } from '../../types';
import config from '../../config';
/**
* Generate a unique ID for a VM or container
* @param type The type of guest ('qemu' or 'lxc')
* @param vmid The VM ID
* @param nodeId The node ID
* @returns A unique ID string
*/
function generateGuestId(type: 'qemu' | 'lxc', vmid: number, nodeId: string): string {
// In cluster mode, use the cluster name instead of the node ID
if (config.clusterMode) {
return type === 'qemu'
? `${config.clusterName}-vm-${vmid}`
: `${config.clusterName}-ct-${vmid}`;
} else {
// In non-cluster mode, use the node ID
return type === 'qemu'
? `${nodeId}-vm-${vmid}`
: `${nodeId}-ct-${vmid}`;
}
}
/**
* Get all virtual machines for the node
@@ -30,7 +52,7 @@ export async function getVirtualMachines(this: ProxmoxClient): Promise<ProxmoxVM
const maxdisk = resourceData.maxdisk !== undefined ? resourceData.maxdisk : vm.maxdisk;
return {
id: `${this.config.id}-vm-${vm.vmid}`,
id: generateGuestId('qemu', vm.vmid, this.config.id),
name: vm.name,
status: vm.status,
node: this.config.id,
@@ -48,22 +70,23 @@ export async function getVirtualMachines(this: ProxmoxClient): Promise<ProxmoxVM
diskwrite: resourceData.diskwrite || vm.diskwrite || 0,
template: vm.template === 1,
type: 'qemu'
} as ProxmoxVM;
};
} catch (error) {
// If we fail to get detailed resource usage, just return basic VM data
this.logger.warn(`Failed to get detailed resource usage for VM ${vm.vmid}`, { error });
// If we can't get resource usage, return basic VM info
this.logger.error(`Error getting resource usage for VM ${vm.vmid}`, { error });
return {
id: `${this.config.id}-vm-${vm.vmid}`,
id: generateGuestId('qemu', vm.vmid, this.config.id),
name: vm.name,
status: vm.status,
node: this.config.id,
vmid: vm.vmid,
cpus: vm.cpus,
memory: vm.mem,
maxmem: vm.maxmem,
disk: vm.disk,
maxdisk: vm.maxdisk,
cpu: 0,
memory: vm.mem || 0,
maxmem: vm.maxmem || 0,
disk: vm.disk || 0,
maxdisk: vm.maxdisk || 0,
uptime: vm.uptime || 0,
netin: vm.netin || 0,
netout: vm.netout || 0,
@@ -71,20 +94,22 @@ export async function getVirtualMachines(this: ProxmoxClient): Promise<ProxmoxVM
diskwrite: vm.diskwrite || 0,
template: vm.template === 1,
type: 'qemu'
} as ProxmoxVM;
};
}
});
// Wait for all VM resource data to be fetched
return await Promise.all(vmPromises);
// Wait for all VM promises to resolve
const vmResults = await Promise.all(vmPromises);
return vmResults;
} catch (error) {
this.logger.error('Failed to get virtual machines', { error });
throw error;
this.logger.error('Error getting virtual machines', { error });
return [];
}
}
/**
* Get all containers on the node with optimized polling
* Get all containers for the node
*/
export async function getContainers(this: ProxmoxClient): Promise<ProxmoxContainer[]> {
try {
@@ -119,56 +144,53 @@ export async function getContainers(this: ProxmoxClient): Promise<ProxmoxContain
// Log the raw status data for debugging
this.logger.debug(`Raw container status for ${container.vmid}:`, { status });
// Get detailed resource usage for this container
let resourceData = { cpu: 0, netin: 0, netout: 0, diskread: 0, diskwrite: 0 };
try {
resourceData = await this.getGuestResourceUsage('lxc', container.vmid);
// Log the raw resource data for debugging
this.logger.debug(`Raw resource data for container ${container.vmid}:`, { resourceData });
} catch (error) {
this.logger.warn(`Failed to get detailed resource usage for Container ${container.vmid}`, { error });
}
// Get resource usage for this container
const resourceData = await this.getGuestResourceUsage('lxc', container.vmid);
// Use memory values from resource data if available, otherwise fall back to container data
const memory = resourceData.mem !== undefined ? resourceData.mem : (status.mem || 0);
const maxmem = resourceData.maxmem !== undefined ? resourceData.maxmem : (status.maxmem || 0);
// Use disk values from resource data if available
const disk = resourceData.disk !== undefined ? resourceData.disk : (status.disk || 0);
const maxdisk = resourceData.maxdisk !== undefined ? resourceData.maxdisk : (status.maxdisk || 0);
// Use fallback values from status if resource data is missing or zero
return {
id: `${this.config.id}-ct-${container.vmid}`,
name: container.name,
status: status.status,
node: this.config.id,
vmid: container.vmid,
cpus: status.cpus || container.cpus || 1,
// Use CPU usage from detailed resource data or fallback to status
// Do NOT multiply by 100 - use the raw value from Proxmox
cpu: (resourceData.cpu !== undefined && resourceData.cpu !== null) ?
resourceData.cpu :
(status.cpu !== undefined && status.cpu !== null) ?
status.cpu : 0,
memory: status.mem || 0,
maxmem: status.maxmem || container.maxmem || 0,
disk: status.disk || 0,
maxdisk: status.maxdisk || container.maxdisk || 0,
uptime: status.uptime || 0,
netin: resourceData.netin || status.netin || 0,
netout: resourceData.netout || status.netout || 0,
diskread: resourceData.diskread || status.diskread || 0,
diskwrite: resourceData.diskwrite || status.diskwrite || 0,
template: container.template === 1,
type: 'lxc'
} as ProxmoxContainer;
} catch (error) {
this.logger.error(`Failed to get status for container ${container.vmid}`, { error });
// Return a basic container object with limited information
return {
id: `${this.config.id}-ct-${container.vmid}`,
id: generateGuestId('lxc', container.vmid, this.config.id),
name: container.name,
status: container.status,
node: this.config.id,
vmid: container.vmid,
cpus: container.cpus || 1,
memory: container.mem || 0,
maxmem: container.maxmem || 0,
disk: container.disk || 0,
maxdisk: container.maxdisk || 0,
cpus: status.cpus || 1,
cpu: resourceData.cpu,
memory: memory,
maxmem: maxmem,
disk: disk,
maxdisk: maxdisk,
uptime: status.uptime || 0,
netin: resourceData.netin || 0,
netout: resourceData.netout || 0,
diskread: resourceData.diskread || 0,
diskwrite: resourceData.diskwrite || 0,
template: container.template === 1,
type: 'lxc'
};
} catch (error) {
// If we can't get resource usage, return basic container info
this.logger.error(`Error getting resource usage for container ${container.vmid}`, { error });
return {
id: generateGuestId('lxc', container.vmid, this.config.id),
name: container.name,
status: container.status,
node: this.config.id,
vmid: container.vmid,
cpus: 1,
cpu: 0,
memory: 0,
maxmem: 0,
disk: 0,
maxdisk: 0,
uptime: 0,
netin: 0,
netout: 0,
@@ -176,23 +198,18 @@ export async function getContainers(this: ProxmoxClient): Promise<ProxmoxContain
diskwrite: 0,
template: container.template === 1,
type: 'lxc'
} as ProxmoxContainer;
};
}
});
// Wait for this batch to complete before moving to the next
// Wait for this batch to complete
const batchResults = await Promise.all(batchPromises);
results.push(...batchResults);
// Add a small delay between batches to avoid overwhelming the API
if (i + batchSize < containers.length) {
await new Promise(resolve => setTimeout(resolve, 500));
}
}
return results;
} catch (error) {
this.logger.error('Failed to get containers', { error });
this.logger.error('Error getting containers', { error });
return [];
}
}
+54 -1
View File
@@ -37,6 +37,21 @@ export class ProxmoxClient extends EventEmitter implements ProxmoxClientMethods
this.retryAttempts = parseInt(process.env.API_RETRY_ATTEMPTS || '3', 10);
this.retryDelayMs = parseInt(process.env.API_RETRY_DELAY_MS || '5000', 10);
// Determine if SSL verification should be disabled
// Check multiple environment variables that could control SSL verification
const disableSSLVerification =
ignoreSSLErrors ||
process.env.PROXMOX_REJECT_UNAUTHORIZED === 'false' ||
process.env.NODE_TLS_REJECT_UNAUTHORIZED === '0' ||
process.env.HTTPS_REJECT_UNAUTHORIZED === 'false' ||
process.env.PROXMOX_INSECURE === 'true' ||
process.env.PROXMOX_VERIFY_SSL === 'false' ||
process.env.IGNORE_SSL_ERRORS === 'true';
if (disableSSLVerification) {
this.logger.warn('SSL certificate verification is disabled. This is insecure and should only be used with trusted networks.');
}
// Create axios instance with base configuration
const axiosConfig = {
baseURL: `${config.host}/api2/json`,
@@ -45,7 +60,7 @@ export class ProxmoxClient extends EventEmitter implements ProxmoxClientMethods
},
timeout: apiTimeoutMs,
httpsAgent: new https.Agent({
rejectUnauthorized: !ignoreSSLErrors
rejectUnauthorized: !disableSSLVerification
})
};
@@ -100,6 +115,11 @@ export class ProxmoxClient extends EventEmitter implements ProxmoxClientMethods
return Promise.reject(error);
}
);
// Initialize cluster detection if auto-detection is enabled
if (config.autoDetectCluster) {
this.initializeClusterDetection();
}
}
}
@@ -139,6 +159,39 @@ export class ProxmoxClient extends EventEmitter implements ProxmoxClientMethods
return false;
}
}
/**
* Initialize cluster detection
* This method checks if the node is part of a cluster and updates the configuration accordingly
*/
private async initializeClusterDetection(): Promise<void> {
try {
// Only auto-detect if the setting is enabled
if (!config.autoDetectCluster) {
this.logger.info('Cluster auto-detection is disabled. Using manual cluster mode setting.');
return;
}
// Check if the node is part of a cluster
const { isCluster, clusterName } = await this.isNodeInCluster();
if (isCluster) {
// If the node is part of a cluster, update the global config
this.logger.info(`Node is part of cluster: ${clusterName}. Enabling cluster mode.`);
// Update the global config to enable cluster mode
// This will affect how IDs are generated for VMs and containers
config.clusterMode = true;
config.clusterName = clusterName;
} else {
this.logger.info('Node is not part of a cluster. Cluster mode will not be enabled.');
// Explicitly disable cluster mode when not in a cluster
config.clusterMode = false;
}
} catch (error) {
this.logger.error('Error initializing cluster detection', { error });
}
}
}
// Import functionality after the class definition to avoid circular dependencies