@@ -583,6 +583,148 @@ async function fetchAllPbsTasksForProcessing({ client, config }, nodeName) {
583
583
}
584
584
}
585
585
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
+
586
728
/**
587
729
* Fetches and processes all data for configured PBS instances.
588
730
* @param {Object } currentPbsApiClients - Initialized PBS API clients.
@@ -669,12 +811,91 @@ async function fetchPbsData(currentPbsApiClients) {
669
811
return pbsDataResults ;
670
812
}
671
813
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
+
672
893
/**
673
894
* Fetches structural data: PVE nodes/VMs/CTs and all PBS data.
674
895
* @param {Object } currentApiClients - Initialized PVE clients.
675
896
* @param {Object } currentPbsApiClients - Initialized PBS clients.
676
897
* @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 }
678
899
*/
679
900
async function fetchDiscoveryData ( currentApiClients , currentPbsApiClients , _fetchPbsDataInternal = fetchPbsData ) {
680
901
// console.log("[DataFetcher] Starting full discovery cycle...");
@@ -694,14 +915,23 @@ async function fetchDiscoveryData(currentApiClients, currentPbsApiClients, _fetc
694
915
return [ { nodes : [ ] , vms : [ ] , containers : [ ] } , [ ] ] ;
695
916
} ) ;
696
917
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
+
697
926
const aggregatedResult = {
698
927
nodes : pveResult . nodes || [ ] ,
699
928
vms : pveResult . vms || [ ] ,
700
929
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
702
932
} ;
703
933
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 .` ) ;
705
935
706
936
return aggregatedResult ;
707
937
}
0 commit comments