Add backup discovery tab with support for local and storage backups

- Add Backup model to Prisma schema with fields for container_id, server_id, hostname, backup info
- Create backupService with discovery methods for local (/var/lib/vz/dump/) and storage (/mnt/pve/<storage>/dump/) backups
- Add database methods for backup CRUD operations and grouping by container
- Create backupsRouter with getAllBackupsGrouped and discoverBackups procedures
- Add BackupsTab component with collapsible cards grouped by CT_ID and hostname
- Integrate backups tab into main page navigation
- Filter storages by node hostname matching to only show applicable storages
- Skip PBS backups discovery (temporarily disabled)
- Add comprehensive logging for backup discovery process
This commit is contained in:
Michel Roegl-Brunner
2025-11-14 13:04:59 +01:00
parent d50ea55e6d
commit 4a50da4968
9 changed files with 1192 additions and 6 deletions

View File

@@ -2063,7 +2063,9 @@ EOFCONFIG`;
const storageService = getStorageService();
const { default: SSHService } = await import('~/server/ssh-service');
const { getSSHExecutionService } = await import('~/server/ssh-execution-service');
const sshService = new SSHService();
const sshExecutionService = getSSHExecutionService();
// Test SSH connection first
const connectionTest = await sshService.testSSHConnection(server as Server);
@@ -2076,16 +2078,62 @@ EOFCONFIG`;
};
}
// Get server hostname to filter storages
let serverHostname = '';
try {
await new Promise<void>((resolve, reject) => {
sshExecutionService.executeCommand(
server as Server,
'hostname',
(data: string) => {
serverHostname += data;
},
(error: string) => {
reject(new Error(`Failed to get hostname: ${error}`));
},
(exitCode: number) => {
if (exitCode === 0) {
resolve();
} else {
reject(new Error(`hostname command failed with exit code ${exitCode}`));
}
}
);
});
} catch (error) {
console.error('Error getting server hostname:', error);
// Continue without filtering if hostname can't be retrieved
}
const normalizedHostname = serverHostname.trim().toLowerCase();
// Check if we have cached data
const wasCached = !input.forceRefresh;
// Fetch storages (will use cache if not forcing refresh)
const allStorages = await storageService.getStorages(server as Server, input.forceRefresh);
// Filter storages by node hostname matching
const applicableStorages = allStorages.filter(storage => {
// If storage has no nodes specified, it's available on all nodes
if (!storage.nodes || storage.nodes.length === 0) {
return true;
}
// If we couldn't get hostname, include all storages (fallback)
if (!normalizedHostname) {
return true;
}
// Check if server hostname is in the nodes array (case-insensitive, trimmed)
const normalizedNodes = storage.nodes.map(node => node.trim().toLowerCase());
return normalizedNodes.includes(normalizedHostname);
});
return {
success: true,
storages: allStorages,
cached: wasCached && allStorages.length > 0
storages: applicableStorages,
cached: wasCached && applicableStorages.length > 0
};
} catch (error) {
console.error('Error in getBackupStorages:', error);