@@ -583,6 +583,148 @@ async function fetchAllPbsTasksForProcessing({ client, config }, nodeName) {
583583 }
584584}
585585
586+ /**
587+ * Fetches PVE backup tasks (vzdump) for a specific node.
588+ * @param {Object } apiClient - The PVE API client instance.
589+ * @param {string } endpointId - The endpoint identifier.
590+ * @param {string } nodeName - The name of the node.
591+ * @returns {Promise<Array> } - Array of backup task objects.
592+ */
593+ async function fetchPveBackupTasks ( apiClient , endpointId , nodeName ) {
594+ try {
595+ const response = await apiClient . get ( `/nodes/${ nodeName } /tasks` , {
596+ params : {
597+ typefilter : 'vzdump' ,
598+ limit : 1000
599+ }
600+ } ) ;
601+ const tasks = response . data ?. data || [ ] ;
602+
603+ // Calculate 30-day cutoff timestamp
604+ const thirtyDaysAgo = Math . floor ( ( Date . now ( ) - 30 * 24 * 60 * 60 * 1000 ) / 1000 ) ;
605+
606+ // Filter to last 30 days and transform to match PBS backup task format
607+ return tasks
608+ . filter ( task => task . starttime >= thirtyDaysAgo )
609+ . map ( task => {
610+ // Extract guest info from task description or ID
611+ let guestId = null ;
612+ let guestType = null ;
613+
614+ // Try to extract from task description (e.g., "vzdump VM 100")
615+ const vmMatch = task . type ?. match ( / V M \s + ( \d + ) / i) || task . id ?. match ( / V M \s + ( \d + ) / i) ;
616+ const ctMatch = task . type ?. match ( / C T \s + ( \d + ) / i) || task . id ?. match ( / C T \s + ( \d + ) / i) ;
617+
618+ if ( vmMatch ) {
619+ guestId = vmMatch [ 1 ] ;
620+ guestType = 'vm' ;
621+ } else if ( ctMatch ) {
622+ guestId = ctMatch [ 1 ] ;
623+ guestType = 'ct' ;
624+ } else if ( task . id ) {
625+ // Try to extract from task ID format
626+ const idMatch = task . id . match ( / v z d u m p - ( \w + ) - ( \d + ) / ) ;
627+ if ( idMatch ) {
628+ guestType = idMatch [ 1 ] === 'qemu' ? 'vm' : 'ct' ;
629+ guestId = idMatch [ 2 ] ;
630+ }
631+ }
632+
633+ return {
634+ type : 'backup' ,
635+ status : task . status || 'unknown' ,
636+ starttime : task . starttime ,
637+ endtime : task . endtime || ( task . starttime + 60 ) ,
638+ node : nodeName ,
639+ guest : guestId ? `${ guestType } /${ guestId } ` : task . id ,
640+ guestType : guestType ,
641+ guestId : guestId ,
642+ upid : task . upid ,
643+ user : task . user || 'unknown' ,
644+ // PVE-specific fields
645+ pveBackupTask : true ,
646+ endpointId : endpointId ,
647+ taskType : 'vzdump'
648+ } ;
649+ } ) ;
650+ } catch ( error ) {
651+ console . error ( `[DataFetcher - ${ endpointId } -${ nodeName } ] Error fetching PVE backup tasks: ${ error . message } ` ) ;
652+ return [ ] ;
653+ }
654+ }
655+
656+ /**
657+ * Fetches storage content (backup files) for a specific storage.
658+ * @param {Object } apiClient - The PVE API client instance.
659+ * @param {string } endpointId - The endpoint identifier.
660+ * @param {string } nodeName - The name of the node.
661+ * @param {string } storage - The storage name.
662+ * @returns {Promise<Array> } - Array of backup file objects.
663+ */
664+ async function fetchStorageBackups ( apiClient , endpointId , nodeName , storage ) {
665+ try {
666+ const response = await apiClient . get ( `/nodes/${ nodeName } /storage/${ storage } /content` , {
667+ params : { content : 'backup' }
668+ } ) ;
669+ const backups = response . data ?. data || [ ] ;
670+
671+ // Transform to a consistent format
672+ return backups . map ( backup => ( {
673+ volid : backup . volid ,
674+ size : backup . size ,
675+ vmid : backup . vmid ,
676+ ctime : backup . ctime ,
677+ format : backup . format ,
678+ notes : backup . notes ,
679+ protected : backup . protected || false ,
680+ storage : storage ,
681+ node : nodeName ,
682+ endpointId : endpointId
683+ } ) ) ;
684+ } catch ( error ) {
685+ // Storage might not support backups or might be inaccessible
686+ if ( error . response ?. status !== 501 ) { // 501 = not implemented
687+ console . warn ( `[DataFetcher - ${ endpointId } -${ nodeName } ] Error fetching backups from storage ${ storage } : ${ error . message } ` ) ;
688+ }
689+ return [ ] ;
690+ }
691+ }
692+
693+ /**
694+ * Fetches VM/CT snapshots for a specific guest.
695+ * @param {Object } apiClient - The PVE API client instance.
696+ * @param {string } endpointId - The endpoint identifier.
697+ * @param {string } nodeName - The name of the node.
698+ * @param {string } vmid - The VM/CT ID.
699+ * @param {string } type - 'qemu' or 'lxc'.
700+ * @returns {Promise<Array> } - Array of snapshot objects.
701+ */
702+ async function fetchGuestSnapshots ( apiClient , endpointId , nodeName , vmid , type ) {
703+ try {
704+ const endpoint = type === 'qemu' ? 'qemu' : 'lxc' ;
705+ const response = await apiClient . get ( `/nodes/${ nodeName } /${ endpoint } /${ vmid } /snapshot` ) ;
706+ const snapshots = response . data ?. data || [ ] ;
707+
708+ // Filter out the 'current' snapshot which is not a real snapshot
709+ return snapshots
710+ . filter ( snap => snap . name !== 'current' )
711+ . map ( snap => ( {
712+ name : snap . name ,
713+ description : snap . description ,
714+ snaptime : snap . snaptime ,
715+ vmstate : snap . vmstate || false ,
716+ parent : snap . parent ,
717+ vmid : vmid ,
718+ type : type ,
719+ node : nodeName ,
720+ endpointId : endpointId
721+ } ) ) ;
722+ } catch ( error ) {
723+ // Guest might not exist or snapshots not supported
724+ return [ ] ;
725+ }
726+ }
727+
586728/**
587729 * Fetches and processes all data for configured PBS instances.
588730 * @param {Object } currentPbsApiClients - Initialized PBS API clients.
@@ -669,12 +811,91 @@ async function fetchPbsData(currentPbsApiClients) {
669811 return pbsDataResults ;
670812}
671813
814+ /**
815+ * Fetches PVE backup data (backup tasks, storage backups, and snapshots).
816+ * @param {Object } currentApiClients - Initialized PVE API clients.
817+ * @param {Array } nodes - Array of node objects.
818+ * @param {Array } vms - Array of VM objects.
819+ * @param {Array } containers - Array of container objects.
820+ * @returns {Promise<Object> } - { backupTasks, storageBackups, guestSnapshots }
821+ */
822+ async function fetchPveBackupData ( currentApiClients , nodes , vms , containers ) {
823+ const allBackupTasks = [ ] ;
824+ const allStorageBackups = [ ] ;
825+ const allGuestSnapshots = [ ] ;
826+
827+ if ( ! nodes || nodes . length === 0 ) {
828+ return { backupTasks : [ ] , storageBackups : [ ] , guestSnapshots : [ ] } ;
829+ }
830+
831+ // Fetch backup tasks and storage backups for each node
832+ const nodeBackupPromises = nodes . map ( async node => {
833+ const endpointId = node . endpointId ;
834+ const nodeName = node . node ;
835+
836+ if ( ! currentApiClients [ endpointId ] ) {
837+ console . warn ( `[DataFetcher] No API client found for endpoint: ${ endpointId } ` ) ;
838+ return ;
839+ }
840+
841+ const { client : apiClient } = currentApiClients [ endpointId ] ;
842+
843+ // Fetch backup tasks for this node
844+ const backupTasks = await fetchPveBackupTasks ( apiClient , endpointId , nodeName ) ;
845+ allBackupTasks . push ( ...backupTasks ) ;
846+
847+ // Fetch backups from each storage on this node
848+ if ( node . storage && Array . isArray ( node . storage ) ) {
849+ const storagePromises = node . storage
850+ . filter ( storage => storage . content && storage . content . includes ( 'backup' ) )
851+ . map ( storage => fetchStorageBackups ( apiClient , endpointId , nodeName , storage . storage ) ) ;
852+
853+ const storageResults = await Promise . allSettled ( storagePromises ) ;
854+ storageResults . forEach ( result => {
855+ if ( result . status === 'fulfilled' && result . value ) {
856+ allStorageBackups . push ( ...result . value ) ;
857+ }
858+ } ) ;
859+ }
860+ } ) ;
861+
862+ // Fetch snapshots for all VMs and containers
863+ const guestSnapshotPromises = [ ] ;
864+
865+ [ ...vms , ...containers ] . forEach ( guest => {
866+ const endpointId = guest . endpointId ;
867+ const nodeName = guest . node ;
868+ const vmid = guest . vmid ;
869+ const type = guest . type || ( vms . includes ( guest ) ? 'qemu' : 'lxc' ) ;
870+
871+ if ( currentApiClients [ endpointId ] ) {
872+ const { client : apiClient } = currentApiClients [ endpointId ] ;
873+ guestSnapshotPromises . push (
874+ fetchGuestSnapshots ( apiClient , endpointId , nodeName , vmid , type )
875+ . then ( snapshots => allGuestSnapshots . push ( ...snapshots ) )
876+ . catch ( err => {
877+ // Silently handle errors for individual guests
878+ } )
879+ ) ;
880+ }
881+ } ) ;
882+
883+ // Wait for all promises to complete
884+ await Promise . allSettled ( [ ...nodeBackupPromises , ...guestSnapshotPromises ] ) ;
885+
886+ return {
887+ backupTasks : allBackupTasks ,
888+ storageBackups : allStorageBackups ,
889+ guestSnapshots : allGuestSnapshots
890+ } ;
891+ }
892+
672893/**
673894 * Fetches structural data: PVE nodes/VMs/CTs and all PBS data.
674895 * @param {Object } currentApiClients - Initialized PVE clients.
675896 * @param {Object } currentPbsApiClients - Initialized PBS clients.
676897 * @param {Function } [_fetchPbsDataInternal=fetchPbsData] - Internal override for testing.
677- * @returns {Promise<Object> } - { nodes, vms, containers, pbs: pbsDataArray }
898+ * @returns {Promise<Object> } - { nodes, vms, containers, pbs: pbsDataArray, pveBackups }
678899 */
679900async function fetchDiscoveryData ( currentApiClients , currentPbsApiClients , _fetchPbsDataInternal = fetchPbsData ) {
680901 // console.log("[DataFetcher] Starting full discovery cycle...");
@@ -694,14 +915,23 @@ async function fetchDiscoveryData(currentApiClients, currentPbsApiClients, _fetc
694915 return [ { nodes : [ ] , vms : [ ] , containers : [ ] } , [ ] ] ;
695916 } ) ;
696917
918+ // Now fetch PVE backup data using the discovered nodes, VMs, and containers
919+ const pveBackups = await fetchPveBackupData (
920+ currentApiClients ,
921+ pveResult . nodes || [ ] ,
922+ pveResult . vms || [ ] ,
923+ pveResult . containers || [ ]
924+ ) ;
925+
697926 const aggregatedResult = {
698927 nodes : pveResult . nodes || [ ] ,
699928 vms : pveResult . vms || [ ] ,
700929 containers : pveResult . containers || [ ] ,
701- pbs : pbsResult || [ ] // pbsResult is already the array we need
930+ pbs : pbsResult || [ ] , // pbsResult is already the array we need
931+ pveBackups : pveBackups // Add PVE backup data
702932 } ;
703933
704- console . log ( `[DataFetcher] Discovery cycle completed. Found: ${ aggregatedResult . nodes . length } PVE nodes, ${ aggregatedResult . vms . length } VMs, ${ aggregatedResult . containers . length } CTs, ${ aggregatedResult . pbs . length } PBS instances.` ) ;
934+ console . log ( `[DataFetcher] Discovery cycle completed. Found: ${ aggregatedResult . nodes . length } PVE nodes, ${ aggregatedResult . vms . length } VMs, ${ aggregatedResult . containers . length } CTs, ${ aggregatedResult . pbs . length } PBS instances, ${ pveBackups . backupTasks . length } PVE backup tasks .` ) ;
705935
706936 return aggregatedResult ;
707937}
0 commit comments