From 63174d2ea12d3ffb7cd05366e6b1622c39777ff3 Mon Sep 17 00:00:00 2001 From: Michel Roegl-Brunner Date: Fri, 14 Nov 2025 13:21:53 +0100 Subject: [PATCH] Fix PBS backup discovery command and authentication - Fix PBS login to use PBS_PASSWORD environment variable instead of stdin - Change backup discovery command from 'snapshots host/' to 'snapshot list ct/' - Use full repository string (root@pam@IP:DATASTORE) instead of storage name - Parse table format output correctly (snapshot | size | files) - Extract snapshot name, size, and date from table output - Convert size units (MiB/GiB) to bytes for storage - Fix TypeScript errors with proper null checks --- src/server/services/backupService.ts | 129 ++++++++++++++++++++------- 1 file changed, 97 insertions(+), 32 deletions(-) diff --git a/src/server/services/backupService.ts b/src/server/services/backupService.ts index 3382cfe..f65e4cd 100644 --- a/src/server/services/backupService.ts +++ b/src/server/services/backupService.ts @@ -320,12 +320,16 @@ class BackupService { // Build login command // Format: proxmox-backup-client login --repository root@pam@: + // PBS supports PBS_PASSWORD and PBS_REPOSITORY environment variables for non-interactive login const repository = `root@pam@${pbsIp}:${pbsDatastore}`; - // Auto-accept fingerprint using echo "y" - // Provide password via stdin - // proxmox-backup-client accepts password via stdin - const fullCommand = `echo -e "y\\n${credential.pbs_password}" | timeout 10 proxmox-backup-client login --repository ${repository} 2>&1`; + // Escape password for shell safety (single quotes) + const escapedPassword = credential.pbs_password.replace(/'/g, "'\\''"); + + // Use PBS_PASSWORD environment variable for non-interactive authentication + // Auto-accept fingerprint by piping "y" to stdin + // PBS will use PBS_PASSWORD env var if available, avoiding interactive prompt + const fullCommand = `echo "y" | PBS_PASSWORD='${escapedPassword}' PBS_REPOSITORY='${repository}' timeout 10 proxmox-backup-client login --repository ${repository} 2>&1`; console.log(`[BackupService] Logging into PBS: ${repository}`); @@ -391,12 +395,32 @@ class BackupService { return backups; } - // Use storage name as repository name (e.g., "PBS1") - const repositoryName = storage.name; - const command = `timeout 30 proxmox-backup-client snapshots host/${ctId} --repository ${repositoryName} 2>&1 || echo "PBS_ERROR"`; + // Get PBS credentials to build full repository string + const db = getDatabase(); + const credential = await db.getPBSCredential(server.id, storage.name); + if (!credential) { + console.log(`[BackupService] No PBS credentials found for storage ${storage.name}`); + return backups; + } + + const storageService = getStorageService(); + const pbsInfo = storageService.getPBSStorageInfo(storage); + const pbsIp = credential.pbs_ip || pbsInfo.pbs_ip; + const pbsDatastore = credential.pbs_datastore || pbsInfo.pbs_datastore; + + if (!pbsIp || !pbsDatastore) { + console.log(`[BackupService] Missing PBS IP or datastore for storage ${storage.name}`); + return backups; + } + + // Build full repository string: root@pam@: + const repository = `root@pam@${pbsIp}:${pbsDatastore}`; + + // Use correct command: snapshot list ct/ --repository + const command = `timeout 30 proxmox-backup-client snapshot list ct/${ctId} --repository ${repository} 2>&1 || echo "PBS_ERROR"`; let output = ''; - console.log(`[BackupService] Discovering PBS backups for CT ${ctId} on repository ${repositoryName}`); + console.log(`[BackupService] Discovering PBS backups for CT ${ctId} on repository ${repository}`); try { // Add timeout to prevent hanging @@ -409,18 +433,18 @@ class BackupService { output += data; }, (error: string) => { - console.log(`[BackupService] PBS command error for ${repositoryName}: ${error}`); + console.log(`[BackupService] PBS command error: ${error}`); resolve(); }, (exitCode: number) => { - console.log(`[BackupService] PBS command completed for ${repositoryName} with exit code ${exitCode}`); + console.log(`[BackupService] PBS command completed with exit code ${exitCode}`); resolve(); } ); }), new Promise((resolve) => { setTimeout(() => { - console.log(`[BackupService] PBS discovery timeout for ${repositoryName}, continuing...`); + console.log(`[BackupService] PBS discovery timeout, continuing...`); resolve(); }, 35000); // 35 second timeout (command has 30s timeout, so this is a safety net) }) @@ -428,35 +452,74 @@ class BackupService { // Check if PBS command failed if (output.includes('PBS_ERROR') || output.includes('error') || output.includes('Error')) { - console.log(`[BackupService] PBS discovery failed or no backups found for CT ${ctId} on ${repositoryName}`); + console.log(`[BackupService] PBS discovery failed or no backups found for CT ${ctId}`); return backups; } - // Parse PBS snapshot output - // Format is typically: snapshot_name timestamp (optional size info) + // Parse PBS snapshot list output (table format) + // Format: snapshot | size | files + // Example: ct/148/2025-10-21T19:14:55Z | 994.944 MiB | catalog.pcat1 client.log ... const lines = output.trim().split('\n').filter(line => line.trim()); - console.log(`[BackupService] Parsing ${lines.length} lines from PBS output for ${repositoryName}`); + console.log(`[BackupService] Parsing ${lines.length} lines from PBS output`); for (const line of lines) { - // Skip header lines or error messages + // Skip header lines, separators, or error messages + if (line.includes('snapshot') && line.includes('size') && line.includes('files')) { + continue; // Skip header row + } + if (line.includes('═') || line.includes('─') || line.includes('│') && line.match(/^[│═─╞╪╡├┼┤└┴┘]+$/)) { + continue; // Skip table separator lines + } if (line.includes('repository') || line.includes('error') || line.includes('Error') || line.includes('PBS_ERROR')) { continue; } - // Parse snapshot line - format varies, try to extract snapshot name and timestamp - const parts = line.trim().split(/\s+/); - if (parts.length > 0) { - const snapshotName = parts[0]; + // Parse table row - format: snapshot | size | files + // Example: │ ct/148/2025-10-21T19:14:55Z │ 994.944 MiB │ catalog.pcat1 client.log index.json pct.conf root.pxar │ + const parts = line.split('│').map(p => p.trim()).filter(p => p); + + if (parts.length >= 2) { + const snapshotPath = parts[0]; // e.g., "ct/148/2025-10-21T19:14:55Z" + const sizeStr = parts[1]; // e.g., "994.944 MiB" - // Try to extract timestamp if available + if (!snapshotPath) { + continue; // Skip if no snapshot path + } + + // Extract snapshot name (last part after /) + const snapshotParts = snapshotPath.split('/'); + const snapshotName = snapshotParts[snapshotParts.length - 1] || snapshotPath; + + if (!snapshotName) { + continue; // Skip if no snapshot name + } + + // Parse date from snapshot name (format: 2025-10-21T19:14:55Z) let createdAt: Date | undefined; - if (parts.length > 1 && parts[1]) { - const timestampMatch = parts[1].match(/\d+/); - if (timestampMatch && timestampMatch[0]) { - const timestamp = parseInt(timestampMatch[0], 10); - // PBS timestamps might be in seconds or milliseconds - createdAt = new Date(timestamp > 1000000000000 ? timestamp : timestamp * 1000); + const dateMatch = snapshotName.match(/(\d{4}-\d{2}-\d{2}T\d{2}:\d{2}:\d{2}Z)/); + if (dateMatch && dateMatch[1]) { + try { + createdAt = new Date(dateMatch[1]); + } catch (e) { + // Invalid date, leave undefined + } + } + + // Parse size (convert MiB/GiB to bytes) + let size: bigint | undefined; + if (sizeStr) { + const sizeMatch = sizeStr.match(/([\d.]+)\s*(MiB|GiB|KiB|B)/i); + if (sizeMatch && sizeMatch[1] && sizeMatch[2]) { + const sizeValue = parseFloat(sizeMatch[1]); + const unit = sizeMatch[2].toUpperCase(); + let bytes = sizeValue; + + if (unit === 'KIB') bytes = sizeValue * 1024; + else if (unit === 'MIB') bytes = sizeValue * 1024 * 1024; + else if (unit === 'GIB') bytes = sizeValue * 1024 * 1024 * 1024; + + size = BigInt(Math.floor(bytes)); } } @@ -464,17 +527,19 @@ class BackupService { container_id: ctId, server_id: server.id, hostname, - backup_name: snapshotName || 'unknown', - backup_path: `pbs://${repositoryName}/host/${ctId}/${snapshotName || 'unknown'}`, - size: undefined, // PBS doesn't always provide size in snapshot list + backup_name: snapshotName, + backup_path: `pbs://${repository}/${snapshotPath}`, + size, created_at: createdAt, - storage_name: repositoryName, + storage_name: storage.name, storage_type: 'pbs', }); } } + + console.log(`[BackupService] Found ${backups.length} PBS backups for CT ${ctId}`); } catch (error) { - console.error(`Error discovering PBS backups for CT ${ctId} on ${repositoryName}:`, error); + console.error(`Error discovering PBS backups for CT ${ctId}:`, error); } return backups;