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/<CT_ID>' to 'snapshot list ct/<CT_ID>'
- 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
This commit is contained in:
Michel Roegl-Brunner
2025-11-14 13:21:53 +01:00
parent eda41e5101
commit 63174d2ea1

View File

@@ -320,12 +320,16 @@ class BackupService {
// Build login command
// Format: proxmox-backup-client login --repository root@pam@<IP>:<DATASTORE>
// 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@<IP>:<DATASTORE>
const repository = `root@pam@${pbsIp}:${pbsDatastore}`;
// Use correct command: snapshot list ct/<CT_ID> --repository <full_repo_string>
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<void>((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);
// Try to extract timestamp if available
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"
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;