From c09a3d7d4929bab7668a2c577bb832420bfdedd2 Mon Sep 17 00:00:00 2001 From: Michal Fox Date: Fri, 25 Sep 2026 07:15:08 +0200 Subject: [PATCH] fix #7860: api: backup info: skip jobs with invalid guest selection A backup job in pool mode keeps referencing its pool after the pool got deleted. Resolving the guests of such a job dies with "pool '' does not exist", and since get_included_vmids() did not catch this, the whole /cluster/backup-info/not-backed-up call failed. The GUI then hides the "Show guests without backup job" button, so users lose exactly the overview that would tell them some guests are no longer backed up. Catch the error per job, warn about it and continue with the remaining jobs. The broken job does not back up anything, so its guests correctly show up as not covered by any backup job. Add a test for get_included_vmids() covering this case. Signed-off-by: Michal Fox --- PVE/API2/Cluster/BackupInfo.pm | 7 +++- test/Makefile | 6 ++- test/backup_info_test.pl | 71 ++++++++++++++++++++++++++++++++++ 3 files changed, 82 insertions(+), 2 deletions(-) create mode 100644 test/backup_info_test.pl diff --git a/PVE/API2/Cluster/BackupInfo.pm b/PVE/API2/Cluster/BackupInfo.pm index 4ef59ac..1ac5966 100644 --- a/PVE/API2/Cluster/BackupInfo.pm +++ b/PVE/API2/Cluster/BackupInfo.pm @@ -25,7 +25,12 @@ sub get_included_vmids { my $all_vmids = {}; for my $job ($legacy_jobs->@*, grep { $_->{type} eq 'vzdump' } values $jobs->{ids}->%*) { - my $job_included_guests = PVE::VZDump::get_included_guests($job); + # a job referencing e.g. a deleted pool backs up nothing, but must not break the overview + my $job_included_guests = eval { PVE::VZDump::get_included_guests($job) }; + if (my $err = $@) { + warn "ignoring backup job with invalid guest selection - $err"; + next; + } $all_vmids->{$_} = 1 for map { $_->@* } values %{$job_included_guests}; } diff --git a/test/Makefile b/test/Makefile index 2dfd20a..adc1f17 100644 --- a/test/Makefile +++ b/test/Makefile @@ -18,7 +18,7 @@ replication%.t: replication_test%.pl ./$< .PHONY: test-vzdump -test-vzdump: test-vzdump-guest-included test-vzdump-new +test-vzdump: test-vzdump-guest-included test-vzdump-new test-vzdump-backup-info .PHONY: test-vzdump-guest-included test-vzdump-guest-included: @@ -28,6 +28,10 @@ test-vzdump-guest-included: test-vzdump-new: ./vzdump_new_test.pl +.PHONY: test-vzdump-backup-info +test-vzdump-backup-info: + ./backup_info_test.pl + .PHONY: test-osd test-osd: ./OSD_test.pl diff --git a/test/backup_info_test.pl b/test/backup_info_test.pl new file mode 100644 index 0000000..54aece2 --- /dev/null +++ b/test/backup_info_test.pl @@ -0,0 +1,71 @@ +#!/usr/bin/perl + +use strict; +use warnings; + +use lib '..'; + +use Test::More; +use Test::MockModule; + +use PVE::API2::Cluster::BackupInfo; + +my $vmlist = { + ids => { + 100 => { type => 'qemu', node => 'node1' }, + 101 => { type => 'qemu', node => 'node1' }, + 102 => { type => 'lxc', node => 'node1' }, + }, +}; + +my $pools = { + testpool => [100], +}; + +my $jobs_cfg = { + ids => { + 'backup-pool' => { type => 'vzdump', pool => 'testpool' }, + 'backup-vmid' => { type => 'vzdump', vmid => '101' }, + }, +}; + +my $cluster_module = Test::MockModule->new('PVE::Cluster'); +$cluster_module->mock(get_vmlist => sub { return $vmlist }); + +my $api2tools_module = Test::MockModule->new('PVE::API2Tools'); +$api2tools_module->mock( + get_resource_pool_guest_members => sub { + my ($pool) = @_; + die "pool '$pool' does not exist\n" if !$pools->{$pool}; + return $pools->{$pool}; + }, +); + +my $backup_info_module = Test::MockModule->new('PVE::API2::Cluster::BackupInfo'); +$backup_info_module->mock( + cfs_read_file => sub { + my ($filename) = @_; + return { jobs => [] } if $filename eq 'vzdump.cron'; + return $jobs_cfg if $filename eq 'jobs.cfg'; + die "unexpected file '$filename'\n"; + }, +); + +is_deeply( + PVE::API2::Cluster::BackupInfo::get_included_vmids(), + { 100 => 1, 101 => 1 }, + 'guests of all backup jobs are included', +); + +$jobs_cfg->{ids}->{'backup-deleted-pool'} = { type => 'vzdump', pool => 'deletedpool' }; + +my @warnings; +my $included = eval { + local $SIG{__WARN__} = sub { push @warnings, $_[0] }; + PVE::API2::Cluster::BackupInfo::get_included_vmids(); +}; +is($@, '', 'job referencing a deleted pool does not make the lookup fail'); +is_deeply($included, { 100 => 1, 101 => 1 }, 'guests of the other jobs are still included'); +like($warnings[0] // '', qr/pool 'deletedpool' does not exist/, 'broken job is warned about'); + +done_testing();