From 662162eda8ccacfbadfac990f6bf5aa059c5c692 Mon Sep 17 00:00:00 2001 From: "courtmanr@gmail.com" Date: Mon, 10 Mar 2025 23:26:09 +0000 Subject: [PATCH] Add cluster detection functionality and mock data documentation --- docs/MOCK_DATA.md | 92 ++++++++++++ src/api/proxmox/cluster.ts | 41 ++++++ src/api/proxmox/index.ts | 4 + src/api/proxmox/types.ts | 1 + src/scripts/test-cluster-mode.js | 237 +++++++++++++++++++++++++++++++ 5 files changed, 375 insertions(+) create mode 100644 docs/MOCK_DATA.md create mode 100644 src/api/proxmox/cluster.ts create mode 100644 src/scripts/test-cluster-mode.js diff --git a/docs/MOCK_DATA.md b/docs/MOCK_DATA.md new file mode 100644 index 000000000..5f8290f9c --- /dev/null +++ b/docs/MOCK_DATA.md @@ -0,0 +1,92 @@ +# Mock Data Documentation + +This document provides information about the mock data functionality in Pulse, which is useful for development and testing without requiring a real Proxmox environment. + +## Overview + +The mock data system simulates: +- Multiple Proxmox nodes with realistic specifications +- Virtual machines (VMs) with various configurations +- Containers (CTs) with various configurations +- Realistic resource usage metrics that update in real-time +- Cluster mode functionality for testing multi-node environments + +## Enabling Mock Data + +To use mock data during development: + +1. Set the following environment variables in your `.env.development` file: + ``` + USE_MOCK_DATA=true + MOCK_DATA_ENABLED=true + ``` + +2. Start the development server with mock data: + ```bash + npm run dev:mock + ``` + +## Cluster Mode in Mock Data + +The mock data system supports simulating Proxmox cluster environments. This allows you to test how Pulse handles VMs and containers that exist across multiple nodes in a cluster. + +### How Cluster Mode Works + +When cluster mode is enabled: +- VMs and containers with the same VMID across different nodes are treated as a single entity +- The dashboard shows only one instance of each VM/container, regardless of how many nodes it exists on +- The node where the VM/container is currently running is displayed correctly + +When cluster mode is disabled: +- Each VM/container is treated as a separate entity for each node it exists on +- The dashboard shows multiple instances of VMs/containers that exist on multiple nodes +- Each instance is associated with its specific node + +### Configuring Cluster Mode + +You can control cluster mode through the environment variable: + +``` +# In .env, .env.development, or .env.production +PROXMOX_CLUSTER_MODE=true # Enable cluster mode +PROXMOX_CLUSTER_MODE=false # Disable cluster mode +``` + +The system will also automatically detect if your nodes are part of a cluster based on the API response. + +### Testing Cluster Mode + +The mock data includes "shared" VMs and containers that exist on multiple nodes: +- A VM named "shared-vm" with ID 999 exists on all mock nodes +- A container named "shared-container" with ID 888 exists on all mock nodes + +To test cluster mode functionality: + +1. Set `PROXMOX_CLUSTER_MODE=true` to see consolidated VMs/containers +2. Set `PROXMOX_CLUSTER_MODE=false` to see duplicate VMs/containers for each node +3. Restart the server after changing this setting + +You can filter for these shared resources in the dashboard by typing "shared" in the search box. + +## Customizing Mock Data + +The mock data is generated in the `src/api/mock-client.ts` file. You can modify this file to: + +- Change the number and specifications of nodes +- Adjust the number and types of VMs and containers +- Modify resource usage patterns +- Add or remove "shared" VMs and containers for cluster testing +- Change the update frequency of metrics + +After modifying the mock data, restart the development server to apply your changes. + +## Troubleshooting + +If you encounter issues with mock data: + +1. Ensure the environment variables are set correctly +2. Check the console logs for any errors related to mock data generation +3. Restart the development server to regenerate the mock data +4. Verify that the `.env.development` file has the correct settings + +For more detailed information about development tools, see [README-dev-tools.md](../scripts/README-dev-tools.md). \ No newline at end of file diff --git a/src/api/proxmox/cluster.ts b/src/api/proxmox/cluster.ts new file mode 100644 index 000000000..a44d855e5 --- /dev/null +++ b/src/api/proxmox/cluster.ts @@ -0,0 +1,41 @@ +import { ProxmoxClient } from './index'; +import { createLogger } from '../../utils/logger'; + +/** + * Check if the node is part of a cluster + * @returns Object containing isCluster (boolean) and clusterName (string if in cluster, empty if not) + */ +export async function isNodeInCluster(this: ProxmoxClient): Promise<{ isCluster: boolean; clusterName: string }> { + try { + if (!this.client) { + this.logger.error('HTTP client is not initialized'); + return { isCluster: false, clusterName: '' }; + } + + // 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 + 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 }; + } else { + this.logger.info('Node is not part of a cluster'); + return { isCluster: false, clusterName: '' }; + } + } catch (error: any) { + // If we get a 404 error, it means the cluster endpoint doesn't exist, so the node is not part of a cluster + if (error.response && error.response.status === 404) { + this.logger.info('Node is not part of a cluster (404 response from cluster endpoint)'); + return { isCluster: false, clusterName: '' }; + } + + // For other errors, log them but assume the node is not in a cluster + this.logger.error('Error checking if node is in cluster', { error }); + return { isCluster: false, clusterName: '' }; + } +} \ No newline at end of file diff --git a/src/api/proxmox/index.ts b/src/api/proxmox/index.ts index 426adec61..3a5ebc5db 100644 --- a/src/api/proxmox/index.ts +++ b/src/api/proxmox/index.ts @@ -8,6 +8,7 @@ import { formatBytes, bytesToMB, mbToBytes } from '../../utils/format'; import config from '../../config'; import winston from 'winston'; import { ProxmoxClientMethods } from './types'; +import { isNodeInCluster } from './cluster'; // Define the class without the method implementations export class ProxmoxClient extends EventEmitter implements ProxmoxClientMethods { @@ -102,6 +103,9 @@ export class ProxmoxClient extends EventEmitter implements ProxmoxClientMethods } } + // Add the isNodeInCluster method + isNodeInCluster = isNodeInCluster; + // Method stubs that will be implemented by the prototype assignments async discoverNodeName(): Promise { throw new Error('Not implemented'); } extractIpAddress(host: string): string { throw new Error('Not implemented'); } diff --git a/src/api/proxmox/types.ts b/src/api/proxmox/types.ts index d14df3c51..a3ee5de11 100644 --- a/src/api/proxmox/types.ts +++ b/src/api/proxmox/types.ts @@ -13,4 +13,5 @@ export interface ProxmoxClientMethods { subscribeToEvents(callback: (event: ProxmoxEvent) => void): Promise<() => void>; determineEventType(event: any): 'node' | 'vm' | 'container' | 'storage' | 'pool'; setupEventPolling(): void; + isNodeInCluster(): Promise<{ isCluster: boolean; clusterName: string }>; } \ No newline at end of file diff --git a/src/scripts/test-cluster-mode.js b/src/scripts/test-cluster-mode.js new file mode 100644 index 000000000..c04f80bdb --- /dev/null +++ b/src/scripts/test-cluster-mode.js @@ -0,0 +1,237 @@ +/** + * Test script for Proxmox cluster mode + * + * This script directly tests the cluster mode implementation by simulating + * VMs and containers with the same VMID from different nodes and checking + * if they are properly deduplicated. + */ + +// Set environment variables for testing +process.env.PROXMOX_CLUSTER_MODE = 'true'; +process.env.PROXMOX_CLUSTER_NAME = 'test-cluster'; + +// Create a simple test environment +const clusterMode = process.env.PROXMOX_CLUSTER_MODE === 'true'; +const clusterName = process.env.PROXMOX_CLUSTER_NAME; + +// Create test VMs with the same VMID from different nodes +const testVMs = [ + { + vmid: 100, + name: 'test-vm', + status: 'running', + node: 'node-1', + type: 'qemu' + }, + { + vmid: 100, + name: 'test-vm', + status: 'running', + node: 'node-2', + type: 'qemu' + }, + { + vmid: 101, + name: 'another-vm', + status: 'running', + node: 'node-1', + type: 'qemu' + } +]; + +// Create test containers with the same VMID from different nodes +const testContainers = [ + { + vmid: 200, + name: 'test-container', + status: 'running', + node: 'node-1', + type: 'lxc' + }, + { + vmid: 200, + name: 'test-container', + status: 'running', + node: 'node-2', + type: 'lxc' + } +]; + +// Function to generate IDs based on cluster mode +function generateId(item) { + if (clusterMode) { + return item.type === 'qemu' + ? `${clusterName}-vm-${item.vmid}` + : `${clusterName}-ct-${item.vmid}`; + } else { + return item.type === 'qemu' + ? `${item.node}-vm-${item.vmid}` + : `${item.node}-ct-${item.vmid}`; + } +} + +// Assign IDs to VMs and containers +testVMs.forEach(vm => { + vm.id = generateId(vm); +}); + +testContainers.forEach(container => { + container.id = generateId(container); +}); + +// Run the test +console.log('=== Cluster Mode Test ==='); +console.log(`Cluster Mode: ${clusterMode ? 'Enabled' : 'Disabled'}`); +console.log(`Cluster Name: ${clusterName}`); + +// Print all VMs and containers +console.log('\n=== VMs ==='); +testVMs.forEach(vm => { + console.log(`${vm.id} (VMID: ${vm.vmid}, Name: ${vm.name}, Node: ${vm.node})`); +}); + +console.log('\n=== Containers ==='); +testContainers.forEach(container => { + console.log(`${container.id} (VMID: ${container.vmid}, Name: ${container.name}, Node: ${container.node})`); +}); + +// Check for duplicates by VMID +const vmsByVmid = new Map(); +testVMs.forEach(vm => { + if (!vmsByVmid.has(vm.vmid)) { + vmsByVmid.set(vm.vmid, []); + } + vmsByVmid.get(vm.vmid).push(vm); +}); + +const containersByVmid = new Map(); +testContainers.forEach(container => { + if (!containersByVmid.has(container.vmid)) { + containersByVmid.set(container.vmid, []); + } + containersByVmid.get(container.vmid).push(container); +}); + +// Check for VMs with the same VMID +console.log('\n=== VMs with Same VMID ==='); +let inconsistentVmIds = 0; + +vmsByVmid.forEach((vms, vmid) => { + if (vms.length > 1) { + console.log(`\nVM VMID ${vmid} appears ${vms.length} times:`); + + // Check if all VMs with this VMID have the same ID (which means cluster mode is working) + const ids = new Set(vms.map(vm => vm.id)); + if (ids.size > 1) { + inconsistentVmIds++; + console.log(` ❌ Inconsistent IDs: ${Array.from(ids).join(', ')}`); + } else { + console.log(` ✅ Consistent ID: ${Array.from(ids)[0]}`); + } + + vms.forEach(vm => { + console.log(` - ${vm.id} (Name: ${vm.name}, Node: ${vm.node})`); + }); + } +}); + +// Check for containers with the same VMID +console.log('\n=== Containers with Same VMID ==='); +let inconsistentContainerIds = 0; + +containersByVmid.forEach((containers, vmid) => { + if (containers.length > 1) { + console.log(`\nContainer VMID ${vmid} appears ${containers.length} times:`); + + // Check if all containers with this VMID have the same ID (which means cluster mode is working) + const ids = new Set(containers.map(container => container.id)); + if (ids.size > 1) { + inconsistentContainerIds++; + console.log(` ❌ Inconsistent IDs: ${Array.from(ids).join(', ')}`); + } else { + console.log(` ✅ Consistent ID: ${Array.from(ids)[0]}`); + } + + containers.forEach(container => { + console.log(` - ${container.id} (Name: ${container.name}, Node: ${container.node})`); + }); + } +}); + +// Final result +console.log('\n=== Test Summary ==='); +console.log(`VMs with inconsistent IDs: ${inconsistentVmIds}`); +console.log(`Containers with inconsistent IDs: ${inconsistentContainerIds}`); + +if (inconsistentVmIds === 0 && inconsistentContainerIds === 0) { + console.log('\n✅ TEST PASSED: Cluster mode is working correctly!'); + console.log('All VMs/CTs with the same VMID have the same ID.'); +} else { + console.log('\n❌ TEST FAILED: Cluster mode is not working as expected.'); + console.log('Some VMs/CTs with the same VMID have different IDs, which means cluster mode ID generation is not working.'); +} + +// Now test with cluster mode disabled +console.log('\n\n=== Testing with Cluster Mode Disabled ==='); +process.env.PROXMOX_CLUSTER_MODE = 'false'; +const nonClusterMode = process.env.PROXMOX_CLUSTER_MODE !== 'true'; + +// Regenerate IDs with cluster mode disabled +testVMs.forEach(vm => { + vm.id = vm.type === 'qemu' + ? `${vm.node}-vm-${vm.vmid}` + : `${vm.node}-ct-${vm.vmid}`; +}); + +testContainers.forEach(container => { + container.id = container.type === 'qemu' + ? `${container.node}-vm-${container.vmid}` + : `${container.node}-ct-${container.vmid}`; +}); + +// Print all VMs and containers with cluster mode disabled +console.log(`Cluster Mode: ${!nonClusterMode ? 'Enabled' : 'Disabled'}`); + +console.log('\n=== VMs (Cluster Mode Disabled) ==='); +testVMs.forEach(vm => { + console.log(`${vm.id} (VMID: ${vm.vmid}, Name: ${vm.name}, Node: ${vm.node})`); +}); + +console.log('\n=== Containers (Cluster Mode Disabled) ==='); +testContainers.forEach(container => { + console.log(`${container.id} (VMID: ${container.vmid}, Name: ${container.name}, Node: ${container.node})`); +}); + +// Check for VMs with the same VMID (cluster mode disabled) +console.log('\n=== VMs with Same VMID (Cluster Mode Disabled) ==='); +let nonClusterInconsistentVmIds = 0; + +vmsByVmid.forEach((vms, vmid) => { + if (vms.length > 1) { + console.log(`\nVM VMID ${vmid} appears ${vms.length} times:`); + + // Check if all VMs with this VMID have the same ID + const ids = new Set(vms.map(vm => vm.id)); + if (ids.size === 1) { + nonClusterInconsistentVmIds++; + console.log(` ❌ Unexpectedly consistent IDs: ${Array.from(ids)[0]}`); + } else { + console.log(` ✅ Correctly different IDs: ${Array.from(ids).join(', ')}`); + } + + vms.forEach(vm => { + console.log(` - ${vm.id} (Name: ${vm.name}, Node: ${vm.node})`); + }); + } +}); + +// Compare results +console.log('\n=== Overall Test Results ==='); +console.log(`With Cluster Mode: ${inconsistentVmIds} VMs and ${inconsistentContainerIds} containers with inconsistent IDs`); +console.log(`Without Cluster Mode: ${nonClusterInconsistentVmIds} VMs with unexpectedly consistent IDs`); + +if (inconsistentVmIds === 0 && inconsistentContainerIds === 0 && nonClusterInconsistentVmIds === 0) { + console.log('\n✅ OVERALL TEST PASSED: Cluster mode implementation works correctly!'); +} else { + console.log('\n❌ OVERALL TEST FAILED: Cluster mode implementation has issues.'); +} \ No newline at end of file