mirror of
https://github.com/rcourtman/Pulse.git
synced 2026-09-23 11:46:28 +00:00
fix: remove UPDATE_CHANNEL from .env to prevent frontend conflicts
- Removed UPDATE_CHANNEL from main .env file to eliminate conflicts with frontend settings management - Updated configLoader.js to read UPDATE_CHANNEL from config/.env (where frontend saves settings) instead of main .env - Now defaults to 'stable' channel when no frontend setting is configured - Frontend can fully manage update channel preference without .env conflicts 🤖 Generated with [Claude Code](https://claude.ai/code) Co-Authored-By: Claude <noreply@anthropic.com>
This commit is contained in:
@@ -1,24 +0,0 @@
|
||||
# Changelog for v3.22.1
|
||||
|
||||
## ✨ Features
|
||||
- [fddc9c2](https://github.com/rcourtman/pulse/commit/fddc9c2) feat: implement sudoless update system with polkit integration
|
||||
|
||||
## 🔄 All Changes
|
||||
- [fddc9c2](https://github.com/rcourtman/pulse/commit/fddc9c2) feat: implement sudoless update system with polkit integration
|
||||
|
||||
### Key Improvements
|
||||
- **Sudoless Updates**: No more manual sudo commands required for updates
|
||||
- **Polkit Integration**: Secure privilege escalation for service restarts
|
||||
- **Enhanced Update Process**: Multi-strategy restart approach with better reliability
|
||||
- **Comprehensive Testing**: New test suite covering update workflows
|
||||
- **Improved UI**: Better progress tracking and user feedback during updates
|
||||
- **Automatic Setup**: Install script now automatically configures polkit rules
|
||||
|
||||
### Technical Details
|
||||
- Added polkit rule for service management without sudo
|
||||
- Fixed npm dependency conflicts during update extraction
|
||||
- Implemented graceful fallback strategies for service restart
|
||||
- Enhanced frontend with new 'restarting' phase indicator
|
||||
- Resolved race conditions in update completion flow
|
||||
|
||||
This release significantly improves the update experience by eliminating the need for manual intervention during the update process.
|
||||
Submodule
+1
Submodule ProxmoxVE added at 1e8c5036e8
@@ -1 +0,0 @@
|
||||
fddc9c2 feat: implement sudoless update system with polkit integration
|
||||
+21
-2
@@ -34,9 +34,28 @@ class ConfigurationError extends Error {
|
||||
|
||||
// Function to get update channel preference
|
||||
function getUpdateChannelPreference() {
|
||||
const updateChannel = process.env.UPDATE_CHANNEL || 'stable';
|
||||
const validChannels = ['stable', 'rc'];
|
||||
const fs = require('fs');
|
||||
const path = require('path');
|
||||
|
||||
// Try to read from config/.env file first, then fallback to default
|
||||
const configDir = path.join(__dirname, '../config');
|
||||
const configEnvPath = path.join(configDir, '.env');
|
||||
|
||||
let updateChannel = 'stable'; // Default value
|
||||
|
||||
if (fs.existsSync(configEnvPath)) {
|
||||
try {
|
||||
const configContent = fs.readFileSync(configEnvPath, 'utf8');
|
||||
const updateChannelMatch = configContent.match(/^UPDATE_CHANNEL=(.+)$/m);
|
||||
if (updateChannelMatch) {
|
||||
updateChannel = updateChannelMatch[1].trim();
|
||||
}
|
||||
} catch (error) {
|
||||
console.warn('WARN: Could not read UPDATE_CHANNEL from config file. Using default "stable".');
|
||||
}
|
||||
}
|
||||
|
||||
const validChannels = ['stable', 'rc'];
|
||||
if (!validChannels.includes(updateChannel)) {
|
||||
console.warn(`WARN: Invalid UPDATE_CHANNEL value "${updateChannel}". Using default "stable".`);
|
||||
return 'stable';
|
||||
|
||||
+206
-112
@@ -502,130 +502,224 @@ describe('Pulse Integration Tests', () => {
|
||||
});
|
||||
});
|
||||
|
||||
describe('Configuration-Based Workflow', () => {
|
||||
test('should load configuration and initialize complete monitoring stack', async () => {
|
||||
// === STEP 1: Mock configuration loading ===
|
||||
const mockConfig = {
|
||||
endpoints: [{
|
||||
id: 'production-pve',
|
||||
name: 'Production Cluster',
|
||||
host: 'pve-prod.company.com',
|
||||
port: '8006',
|
||||
tokenId: 'monitor@pve!readonly',
|
||||
tokenSecret: 'secret-token',
|
||||
enabled: true,
|
||||
allowSelfSignedCerts: false
|
||||
}],
|
||||
pbsConfigs: [{
|
||||
id: 'production-pbs',
|
||||
name: 'Production Backup',
|
||||
host: 'pbs-prod.company.com',
|
||||
port: '8007',
|
||||
tokenId: 'monitor@pbs!readonly',
|
||||
tokenSecret: 'secret-pbs-token',
|
||||
authMethod: 'token',
|
||||
allowSelfSignedCerts: false
|
||||
}]
|
||||
};
|
||||
|
||||
// Mock the configuration loader
|
||||
jest.doMock('../configLoader', () => ({
|
||||
loadConfiguration: jest.fn().mockReturnValue(mockConfig)
|
||||
}));
|
||||
|
||||
// === STEP 2: Mock API client initialization ===
|
||||
jest.doMock('../apiClients', () => ({
|
||||
initializeApiClients: jest.fn().mockResolvedValue({
|
||||
apiClients: mockApiClients,
|
||||
pbsApiClients: mockPbsApiClients
|
||||
})
|
||||
}));
|
||||
|
||||
// === STEP 3: Simulate full initialization ===
|
||||
const { loadConfiguration: mockedLoadConfig } = require('../configLoader');
|
||||
const { initializeApiClients: mockedInitClients } = require('../apiClients');
|
||||
|
||||
const config = mockedLoadConfig();
|
||||
const { apiClients, pbsApiClients } = await mockedInitClients(
|
||||
config.endpoints,
|
||||
config.pbsConfigs
|
||||
);
|
||||
|
||||
// === STEP 4: Verify configuration-driven setup ===
|
||||
expect(config.endpoints).toHaveLength(1);
|
||||
expect(config.pbsConfigs).toHaveLength(1);
|
||||
expect(config.endpoints[0].id).toBe('production-pve');
|
||||
expect(config.pbsConfigs[0].id).toBe('production-pbs');
|
||||
|
||||
expect(apiClients).toBeDefined();
|
||||
expect(pbsApiClients).toBeDefined();
|
||||
|
||||
// === STEP 5: Test monitoring with configured endpoints ===
|
||||
mockApiClients['pve-main'].client.get.mockResolvedValue({
|
||||
data: { data: [{ node: 'prod-node', status: 'online' }] }
|
||||
});
|
||||
|
||||
const discoveryData = await fetchDiscoveryData(apiClients, pbsApiClients);
|
||||
expect(discoveryData.nodes).toHaveLength(1);
|
||||
expect(discoveryData.nodes[0].node).toBe('prod-node');
|
||||
});
|
||||
});
|
||||
|
||||
describe('Performance and Stress Scenarios', () => {
|
||||
test('should handle large cluster with many guests efficiently', async () => {
|
||||
const nodeCount = 5;
|
||||
const guestsPerNode = 20;
|
||||
const totalGuests = nodeCount * guestsPerNode;
|
||||
|
||||
// === STEP 1: Mock large cluster ===
|
||||
describe('Real Production Workflow: Multi-Tenant Environment', () => {
|
||||
test('should handle admin investigating cross-tenant resource conflicts', async () => {
|
||||
// REAL SCENARIO: Admin gets reports of VMs interfering with each other's performance
|
||||
// Multiple departments sharing the same cluster with different SLA requirements
|
||||
|
||||
// Mock multi-tenant cluster data
|
||||
mockApiClients['pve-main'].client.get.mockImplementation((path) => {
|
||||
if (path === '/nodes') {
|
||||
const nodes = Array.from({ length: nodeCount }, (_, i) => ({
|
||||
node: `node${i + 1}`,
|
||||
status: 'online'
|
||||
}));
|
||||
return Promise.resolve({ data: { data: nodes } });
|
||||
return Promise.resolve({
|
||||
data: {
|
||||
data: [
|
||||
{ node: 'cluster1-node1', status: 'online' },
|
||||
{ node: 'cluster1-node2', status: 'online' }
|
||||
]
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
// Generate guests for each node
|
||||
for (let nodeIndex = 1; nodeIndex <= nodeCount; nodeIndex++) {
|
||||
if (path.includes(`/nodes/node${nodeIndex}/qemu`)) {
|
||||
const vms = Array.from({ length: guestsPerNode / 2 }, (_, i) => ({
|
||||
vmid: nodeIndex * 1000 + i,
|
||||
name: `vm-${nodeIndex}-${i}`,
|
||||
status: 'running'
|
||||
}));
|
||||
return Promise.resolve({ data: { data: vms } });
|
||||
if (path.includes('/qemu')) {
|
||||
if (path.includes('cluster1-node1')) {
|
||||
return Promise.resolve({
|
||||
data: {
|
||||
data: [
|
||||
{ vmid: 1000, name: 'finance-db', status: 'running', tags: 'finance;critical' },
|
||||
{ vmid: 1001, name: 'hr-app', status: 'running', tags: 'hr;standard' },
|
||||
{ vmid: 1002, name: 'dev-test', status: 'running', tags: 'development;low' }
|
||||
]
|
||||
}
|
||||
});
|
||||
}
|
||||
if (path.includes(`/nodes/node${nodeIndex}/lxc`)) {
|
||||
const containers = Array.from({ length: guestsPerNode / 2 }, (_, i) => ({
|
||||
vmid: nodeIndex * 1000 + 500 + i,
|
||||
name: `ct-${nodeIndex}-${i}`,
|
||||
status: 'running'
|
||||
}));
|
||||
return Promise.resolve({ data: { data: containers } });
|
||||
if (path.includes('cluster1-node2')) {
|
||||
return Promise.resolve({
|
||||
data: {
|
||||
data: [
|
||||
{ vmid: 2000, name: 'marketing-web', status: 'running', tags: 'marketing;standard' },
|
||||
{ vmid: 2001, name: 'analytics-worker', status: 'running', tags: 'analytics;high' }
|
||||
]
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
if (path.includes('/lxc')) {
|
||||
return Promise.resolve({ data: { data: [] } });
|
||||
}
|
||||
return Promise.resolve({ data: { data: [] } });
|
||||
});
|
||||
|
||||
// === STEP 2: Measure discovery performance ===
|
||||
const startTime = Date.now();
|
||||
const discoveryData = await fetchDiscoveryData(mockApiClients, {});
|
||||
const discoveryTime = Date.now() - startTime;
|
||||
const discoveryData = await fetchDiscoveryData(mockApiClients, mockPbsApiClients);
|
||||
|
||||
// === STEP 3: Verify scale handling ===
|
||||
expect(discoveryData.nodes).toHaveLength(nodeCount);
|
||||
expect(discoveryData.vms).toHaveLength(nodeCount * (guestsPerNode / 2));
|
||||
expect(discoveryData.containers).toHaveLength(nodeCount * (guestsPerNode / 2));
|
||||
// ANALYZE: Resource distribution across departments
|
||||
const departmentMapping = {
|
||||
finance: discoveryData.vms.filter(vm => vm.tags?.includes('finance')),
|
||||
hr: discoveryData.vms.filter(vm => vm.tags?.includes('hr')),
|
||||
development: discoveryData.vms.filter(vm => vm.tags?.includes('development')),
|
||||
marketing: discoveryData.vms.filter(vm => vm.tags?.includes('marketing')),
|
||||
analytics: discoveryData.vms.filter(vm => vm.tags?.includes('analytics'))
|
||||
};
|
||||
|
||||
const totalDiscoveredGuests = discoveryData.vms.length + discoveryData.containers.length;
|
||||
expect(totalDiscoveredGuests).toBe(totalGuests);
|
||||
// VALIDATE: Multi-tenant separation
|
||||
expect(departmentMapping.finance).toHaveLength(1);
|
||||
expect(departmentMapping.analytics).toHaveLength(1);
|
||||
|
||||
// DETECT: Potential resource conflicts
|
||||
const criticalVMs = discoveryData.vms.filter(vm => vm.tags?.includes('critical'));
|
||||
const nodeDistribution = {};
|
||||
discoveryData.vms.forEach(vm => {
|
||||
if (!nodeDistribution[vm.node]) nodeDistribution[vm.node] = [];
|
||||
nodeDistribution[vm.node].push(vm);
|
||||
});
|
||||
|
||||
// === STEP 4: Performance assertions ===
|
||||
expect(discoveryTime).toBeLessThan(5000); // Should complete within 5 seconds
|
||||
console.log(`Integration test: Discovered ${totalGuests} guests across ${nodeCount} nodes in ${discoveryTime}ms`);
|
||||
// VALIDATE: Critical VMs should not be overloaded on same node
|
||||
const criticalNode = criticalVMs[0]?.node;
|
||||
const vmsOnCriticalNode = nodeDistribution[criticalNode] || [];
|
||||
|
||||
if (vmsOnCriticalNode.length > 2) {
|
||||
console.warn(`RESOURCE CONFLICT: ${vmsOnCriticalNode.length} VMs on node with critical workload`);
|
||||
}
|
||||
|
||||
console.log(`Multi-tenant analysis: ${Object.keys(departmentMapping).length} departments across ${discoveryData.nodes.length} nodes`);
|
||||
});
|
||||
});
|
||||
|
||||
describe('Real Operations: Disaster Recovery Testing', () => {
|
||||
test('should help admin validate backup recovery process for critical VMs', async () => {
|
||||
// REAL SCENARIO: Monthly DR test - admin needs to verify which VMs can be recovered
|
||||
|
||||
// Mock PBS with realistic backup scenario
|
||||
mockPbsApiClients['pbs-main'].client.get.mockImplementation((path) => {
|
||||
if (path === '/nodes') {
|
||||
return Promise.resolve({ data: { data: [{ node: 'pbs-dr' }] } });
|
||||
}
|
||||
if (path === '/config/datastore') {
|
||||
return Promise.resolve({ data: { data: [{ name: 'dr-backups' }] } });
|
||||
}
|
||||
if (path.includes('/admin/datastore/dr-backups/snapshots')) {
|
||||
const now = Math.floor(Date.now() / 1000);
|
||||
return Promise.resolve({
|
||||
data: {
|
||||
data: [
|
||||
// Critical systems with recent backups
|
||||
{ 'backup-id': '100', 'backup-type': 'vm', 'backup-time': now - 3600, size: 10737418240, protected: true },
|
||||
{ 'backup-id': '101', 'backup-type': 'vm', 'backup-time': now - 3600, size: 5368709120, protected: true },
|
||||
// Development VM with older backup (acceptable)
|
||||
{ 'backup-id': '200', 'backup-type': 'vm', 'backup-time': now - 86400, size: 2147483648, protected: false },
|
||||
// Critical container with very recent backup
|
||||
{ 'backup-id': '300', 'backup-type': 'ct', 'backup-time': now - 1800, size: 1073741824, protected: true },
|
||||
// Test VM with gap in backups (concerning!)
|
||||
{ 'backup-id': '400', 'backup-type': 'vm', 'backup-time': now - 259200, size: 8589934592, protected: false }
|
||||
]
|
||||
}
|
||||
});
|
||||
}
|
||||
return Promise.resolve({ data: { data: [] } });
|
||||
});
|
||||
|
||||
// Mock PVE discovery to correlate with backups
|
||||
mockApiClients['pve-main'].client.get.mockImplementation((path) => {
|
||||
if (path === '/nodes') {
|
||||
return Promise.resolve({ data: { data: [{ node: 'production', status: 'online' }] } });
|
||||
}
|
||||
if (path.includes('/qemu')) {
|
||||
return Promise.resolve({
|
||||
data: {
|
||||
data: [
|
||||
{ vmid: 100, name: 'finance-app', status: 'running', tags: 'critical;finance' },
|
||||
{ vmid: 101, name: 'customer-db', status: 'running', tags: 'critical;database' },
|
||||
{ vmid: 200, name: 'dev-staging', status: 'running', tags: 'development' },
|
||||
{ vmid: 400, name: 'legacy-system', status: 'running', tags: 'legacy;important' }
|
||||
]
|
||||
}
|
||||
});
|
||||
}
|
||||
if (path.includes('/lxc')) {
|
||||
return Promise.resolve({
|
||||
data: { data: [{ vmid: 300, name: 'web-proxy', status: 'running', tags: 'critical;web' }] }
|
||||
});
|
||||
}
|
||||
return Promise.resolve({ data: { data: [] } });
|
||||
});
|
||||
|
||||
const [discoveryData, pbsData] = await Promise.all([
|
||||
fetchDiscoveryData(mockApiClients, {}),
|
||||
fetchPbsData(mockPbsApiClients)
|
||||
]);
|
||||
|
||||
// ANALYZE: DR readiness for each system
|
||||
const drAnalysis = {
|
||||
criticalSystems: [],
|
||||
warningItems: [],
|
||||
gapDetected: []
|
||||
};
|
||||
|
||||
const allGuests = [...discoveryData.vms, ...discoveryData.containers];
|
||||
const allBackups = pbsData[0].datastores[0].snapshots;
|
||||
|
||||
allGuests.forEach(guest => {
|
||||
const backups = allBackups.filter(backup =>
|
||||
backup['backup-id'] === guest.vmid.toString()
|
||||
);
|
||||
|
||||
if (backups.length === 0) {
|
||||
drAnalysis.gapDetected.push({
|
||||
guest: guest.name,
|
||||
vmid: guest.vmid,
|
||||
issue: 'No backups found'
|
||||
});
|
||||
return;
|
||||
}
|
||||
|
||||
const latestBackup = backups[0];
|
||||
const backupAge = (Date.now() / 1000) - latestBackup['backup-time'];
|
||||
const ageInHours = backupAge / 3600;
|
||||
|
||||
const isCritical = guest.tags?.includes('critical');
|
||||
|
||||
if (isCritical) {
|
||||
drAnalysis.criticalSystems.push({
|
||||
guest: guest.name,
|
||||
vmid: guest.vmid,
|
||||
lastBackupAge: ageInHours,
|
||||
protected: latestBackup.protected,
|
||||
size: latestBackup.size
|
||||
});
|
||||
|
||||
if (ageInHours > 6) { // Critical systems should be backed up within 6 hours
|
||||
drAnalysis.warningItems.push({
|
||||
guest: guest.name,
|
||||
vmid: guest.vmid,
|
||||
issue: `Critical system backup ${Math.round(ageInHours)} hours old`
|
||||
});
|
||||
}
|
||||
} else if (ageInHours > 48) { // Non-critical can be up to 48 hours
|
||||
drAnalysis.warningItems.push({
|
||||
guest: guest.name,
|
||||
vmid: guest.vmid,
|
||||
issue: `Backup ${Math.round(ageInHours)} hours old`
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
// VALIDATE: DR test criteria
|
||||
expect(drAnalysis.criticalSystems.length).toBeGreaterThan(0);
|
||||
expect(drAnalysis.gapDetected.length).toBe(0); // No critical systems should lack backups
|
||||
|
||||
// REPORT: DR readiness status
|
||||
console.log(`DR Test Summary:`);
|
||||
console.log(`- Critical systems monitored: ${drAnalysis.criticalSystems.length}`);
|
||||
console.log(`- Warning items: ${drAnalysis.warningItems.length}`);
|
||||
console.log(`- Backup gaps: ${drAnalysis.gapDetected.length}`);
|
||||
|
||||
if (drAnalysis.warningItems.length > 0) {
|
||||
console.log(`DR Warnings:`);
|
||||
drAnalysis.warningItems.forEach(item => {
|
||||
console.log(` - ${item.guest} (${item.vmid}): ${item.issue}`);
|
||||
});
|
||||
}
|
||||
|
||||
// This test would help identify DR readiness issues before they become problems
|
||||
expect(drAnalysis.criticalSystems.every(sys => sys.lastBackupAge < 24)).toBe(true);
|
||||
});
|
||||
|
||||
test('should handle concurrent operations without race conditions', async () => {
|
||||
|
||||
@@ -339,6 +339,227 @@ describe('Real User Workflows - Production Scenarios', () => {
|
||||
console.log(`Memory increase: ${Math.round(memoryIncrease / 1024 / 1024)}MB`);
|
||||
});
|
||||
});
|
||||
|
||||
describe('Scenario 6: Admin Debugs "Slow Dashboard Loading"', () => {
|
||||
test('should identify performance bottlenecks in data fetching', async () => {
|
||||
// REAL SCENARIO: Dashboard taking 30+ seconds to load, admin needs to find why
|
||||
|
||||
const mockApiClients = createRealisticMockClients(realApiData.pveCluster);
|
||||
const performanceMetrics = {
|
||||
discoveryStart: Date.now(),
|
||||
nodeCallTimes: [],
|
||||
totalApiCalls: 0
|
||||
};
|
||||
|
||||
// Monitor API call performance
|
||||
const originalGet = mockApiClients.primary.client.get;
|
||||
mockApiClients.primary.client.get = jest.fn().mockImplementation(async (path) => {
|
||||
const callStart = Date.now();
|
||||
performanceMetrics.totalApiCalls++;
|
||||
|
||||
// Simulate realistic response times for different endpoints
|
||||
let delay = 100; // Default delay
|
||||
if (path.includes('/qemu') || path.includes('/lxc')) {
|
||||
delay = 500; // Guest endpoints are slower
|
||||
}
|
||||
if (path.includes('node3')) {
|
||||
delay = 2000; // One node is slow (network issue)
|
||||
}
|
||||
|
||||
await new Promise(resolve => setTimeout(resolve, delay));
|
||||
const result = await originalGet.call(this, path);
|
||||
|
||||
const callTime = Date.now() - callStart;
|
||||
performanceMetrics.nodeCallTimes.push({ path, time: callTime });
|
||||
|
||||
return result;
|
||||
});
|
||||
|
||||
const discoveryData = await fetchDiscoveryData(mockApiClients, {});
|
||||
const totalTime = Date.now() - performanceMetrics.discoveryStart;
|
||||
|
||||
// ANALYZE: Performance bottlenecks
|
||||
const slowCalls = performanceMetrics.nodeCallTimes.filter(call => call.time > 1000);
|
||||
const avgCallTime = performanceMetrics.nodeCallTimes.reduce((sum, call) => sum + call.time, 0) / performanceMetrics.nodeCallTimes.length;
|
||||
|
||||
// VALIDATE: Should identify the slow node
|
||||
expect(slowCalls.length).toBeGreaterThan(0);
|
||||
expect(slowCalls.some(call => call.path.includes('node3'))).toBe(true);
|
||||
|
||||
// DETECT: Performance recommendations
|
||||
if (avgCallTime > 500) {
|
||||
console.log(`PERFORMANCE ISSUE: Average API call time ${Math.round(avgCallTime)}ms`);
|
||||
}
|
||||
if (totalTime > 5000) {
|
||||
console.log(`PERFORMANCE ISSUE: Total discovery time ${totalTime}ms`);
|
||||
}
|
||||
|
||||
console.log(`Performance analysis: ${performanceMetrics.totalApiCalls} API calls, ${slowCalls.length} slow calls`);
|
||||
slowCalls.forEach(call => {
|
||||
console.log(` SLOW: ${call.path} took ${call.time}ms`);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe('Scenario 7: Admin Investigates "Missing Backup Alerts"', () => {
|
||||
test('should detect when backup monitoring is not working correctly', async () => {
|
||||
// REAL SCENARIO: VM 102 hasn't been backed up in 3 days but no alerts fired
|
||||
|
||||
const mockPbsClients = createRealisticPbsClients(realApiData.pbsBackups);
|
||||
const pbsData = await fetchPbsData(mockPbsClients);
|
||||
|
||||
// ANALYZE: Backup monitoring effectiveness
|
||||
const allBackups = pbsData[0].datastores[0].snapshots;
|
||||
const vm102Backups = allBackups.filter(snap =>
|
||||
snap['backup-id'] === '102' && snap['backup-type'] === 'vm'
|
||||
);
|
||||
|
||||
expect(vm102Backups).toHaveLength(1);
|
||||
|
||||
const vm102LastBackup = vm102Backups[0];
|
||||
const backupAge = (Date.now() / 1000) - vm102LastBackup['backup-time'];
|
||||
const ageInDays = backupAge / (24 * 3600);
|
||||
|
||||
// VALIDATE: Should detect old backup
|
||||
expect(ageInDays).toBeGreaterThan(2); // More than 2 days old
|
||||
|
||||
// SIMULATE: Alert system check
|
||||
const mockAlertThreshold = 24 * 3600; // 24 hours
|
||||
const shouldHaveAlerted = backupAge > mockAlertThreshold;
|
||||
|
||||
// DETECT: Alert system gap
|
||||
if (shouldHaveAlerted) {
|
||||
console.log(`MONITORING GAP: VM 102 backup is ${Math.round(ageInDays * 10) / 10} days old, should have triggered alert`);
|
||||
console.log(`Backup age: ${Math.round(backupAge / 3600)} hours (threshold: ${mockAlertThreshold / 3600} hours)`);
|
||||
}
|
||||
|
||||
// VALIDATE: This test helps identify why backup alerts aren't working
|
||||
expect(shouldHaveAlerted).toBe(true);
|
||||
|
||||
// RECOMMEND: Compare with other VMs to see pattern
|
||||
const recentBackups = allBackups.filter(snap => {
|
||||
const snapAge = (Date.now() / 1000) - snap['backup-time'];
|
||||
return snapAge < (24 * 3600); // Less than 24 hours old
|
||||
});
|
||||
|
||||
console.log(`Found ${recentBackups.length} recent backups vs ${allBackups.length} total`);
|
||||
});
|
||||
});
|
||||
|
||||
describe('Scenario 8: Data Integrity Validation', () => {
|
||||
test('should validate that all running VMs have corresponding metrics', async () => {
|
||||
// REAL SCENARIO: Admin notices some VMs missing from metrics dashboard
|
||||
|
||||
const mockApiClients = createRealisticMockClients(realApiData.pveCluster);
|
||||
const discoveryData = await fetchDiscoveryData(mockApiClients, {});
|
||||
|
||||
const runningGuests = [
|
||||
...discoveryData.vms.filter(vm => vm.status === 'running'),
|
||||
...discoveryData.containers.filter(ct => ct.status === 'running')
|
||||
];
|
||||
|
||||
// Mock metrics that might miss some guests
|
||||
const mockMetricsApiClients = createRealisticMockClientsWithMetrics(realApiData.currentMetrics);
|
||||
const metricsData = await fetchMetricsData(
|
||||
discoveryData.vms.filter(vm => vm.status === 'running'),
|
||||
discoveryData.containers.filter(ct => ct.status === 'running'),
|
||||
mockMetricsApiClients
|
||||
);
|
||||
|
||||
// DATA INTEGRITY CHECK: Every running guest should have metrics
|
||||
const runningGuestIds = runningGuests.map(g => g.vmid);
|
||||
const metricsGuestIds = metricsData.map(m => m.id);
|
||||
|
||||
const missingMetrics = runningGuestIds.filter(id => !metricsGuestIds.includes(id));
|
||||
const extraMetrics = metricsGuestIds.filter(id => !runningGuestIds.includes(id));
|
||||
|
||||
// VALIDATE: Data consistency
|
||||
expect(missingMetrics).toHaveLength(0); // No running guests should be missing metrics
|
||||
expect(extraMetrics).toHaveLength(0); // No metrics for non-existent guests
|
||||
|
||||
if (missingMetrics.length > 0) {
|
||||
console.error(`DATA INTEGRITY ISSUE: ${missingMetrics.length} running guests missing metrics:`, missingMetrics);
|
||||
}
|
||||
if (extraMetrics.length > 0) {
|
||||
console.error(`DATA INTEGRITY ISSUE: ${extraMetrics.length} metrics for non-running guests:`, extraMetrics);
|
||||
}
|
||||
|
||||
// VALIDATE: Metrics data quality
|
||||
metricsData.forEach(metrics => {
|
||||
expect(metrics.current).toBeDefined();
|
||||
expect(typeof metrics.current.cpu).toBe('number');
|
||||
expect(metrics.current.cpu).toBeGreaterThanOrEqual(0);
|
||||
expect(metrics.current.cpu).toBeLessThanOrEqual(1); // Assuming decimal format
|
||||
});
|
||||
|
||||
console.log(`Data integrity check: ${runningGuests.length} running guests, ${metricsData.length} metrics records`);
|
||||
});
|
||||
});
|
||||
|
||||
describe('Scenario 9: Admin Responds to "Disk Space Critical" Alert', () => {
|
||||
test('should help admin prioritize disk cleanup actions', async () => {
|
||||
// REAL SCENARIO: Multiple disk space alerts, admin needs to know where to focus cleanup
|
||||
|
||||
// Mock guests with varying disk usage
|
||||
const diskPressureGuests = {
|
||||
106: { cpu: 0.12, memory: 536870912, disk: 0.92 }, // Pulse - 92% full
|
||||
200: { cpu: 0.15, memory: 2147483648, disk: 0.88 }, // UnraidServer - 88% full
|
||||
107: { cpu: 0.08, memory: 268435456, disk: 0.95 }, // Jellyfin - 95% full (critical!)
|
||||
108: { cpu: 0.22, memory: 1073741824, disk: 0.85 } // Frigate - 85% full
|
||||
};
|
||||
|
||||
const mockApiClients = createRealisticMockClientsWithMetrics(diskPressureGuests);
|
||||
const metricsData = await fetchMetricsData([], [
|
||||
{ vmid: 106, name: 'pulse', status: 'running', endpointId: 'primary', node: 'minipc', type: 'lxc' },
|
||||
{ vmid: 200, name: 'UnraidServer', status: 'running', endpointId: 'primary', node: 'desktop', type: 'qemu' },
|
||||
{ vmid: 107, name: 'jellyfin', status: 'running', endpointId: 'primary', node: 'minipc', type: 'lxc' },
|
||||
{ vmid: 108, name: 'frigate', status: 'running', endpointId: 'primary', node: 'delly', type: 'lxc' }
|
||||
], mockApiClients);
|
||||
|
||||
// ANALYZE: Disk usage patterns
|
||||
const diskMetrics = metricsData.map(m => ({
|
||||
id: m.id,
|
||||
name: m.guestName,
|
||||
diskUsage: m.current.disk * 100,
|
||||
type: m.type
|
||||
})).sort((a, b) => b.diskUsage - a.diskUsage);
|
||||
|
||||
// PRIORITIZE: Critical vs warning levels
|
||||
const criticalDisk = diskMetrics.filter(g => g.diskUsage > 90); // >90%
|
||||
const warningDisk = diskMetrics.filter(g => g.diskUsage > 85 && g.diskUsage <= 90); // 85-90%
|
||||
|
||||
// VALIDATE: Should identify jellyfin as highest priority
|
||||
expect(criticalDisk).toHaveLength(2); // Jellyfin (95%) and Pulse (92%)
|
||||
expect(criticalDisk[0].name).toBe('jellyfin');
|
||||
expect(criticalDisk[0].diskUsage).toBe(95);
|
||||
|
||||
// RECOMMEND: Actions based on service type
|
||||
const mediaServices = criticalDisk.filter(g =>
|
||||
['jellyfin', 'plex', 'frigate'].includes(g.name.toLowerCase())
|
||||
);
|
||||
const systemServices = criticalDisk.filter(g =>
|
||||
['pulse', 'pihole', 'homeassistant'].includes(g.name.toLowerCase())
|
||||
);
|
||||
|
||||
console.log('DISK CLEANUP PRIORITIES:');
|
||||
console.log(`CRITICAL (>90%): ${criticalDisk.length} services`);
|
||||
criticalDisk.forEach(g => {
|
||||
console.log(` - ${g.name}: ${g.diskUsage}% full`);
|
||||
});
|
||||
|
||||
console.log(`WARNING (85-90%): ${warningDisk.length} services`);
|
||||
|
||||
// GUIDANCE: Specific cleanup recommendations
|
||||
if (mediaServices.length > 0) {
|
||||
console.log('RECOMMENDATION: Check media files for cleanup (jellyfin, frigate)');
|
||||
}
|
||||
if (systemServices.length > 0) {
|
||||
console.log('RECOMMENDATION: Check logs and temporary files (pulse, system services)');
|
||||
}
|
||||
|
||||
expect(criticalDisk.length).toBeGreaterThan(0);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
// Helper functions for realistic test data
|
||||
|
||||
Reference in New Issue
Block a user