Files
proxmox-git-pve-manager/PVE/API2/Cluster/BackupInfo.pm
T
dualfroz c09a3d7d49 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 '<name>'
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 <me@dualfroz.com>
2026-09-25 07:15:08 +02:00

149 lines
4.2 KiB
Perl

package PVE::API2::Cluster::BackupInfo;
use strict;
use warnings;
use Digest::SHA;
use PVE::SafeSyslog;
use PVE::Tools qw(extract_param);
use PVE::Cluster qw(cfs_lock_file cfs_read_file cfs_write_file);
use PVE::RESTHandler;
use PVE::RPCEnvironment;
use PVE::JSONSchema;
use PVE::Storage;
use PVE::Exception qw(raise_param_exc);
use PVE::VZDump;
use PVE::VZDump::Common;
use base qw(PVE::RESTHandler);
sub get_included_vmids {
my $legacy_vzdump_job_cfg = cfs_read_file('vzdump.cron');
my $legacy_jobs = $legacy_vzdump_job_cfg->{jobs} || [];
my $jobs = cfs_read_file('jobs.cfg');
my $all_vmids = {};
for my $job ($legacy_jobs->@*, grep { $_->{type} eq 'vzdump' } values $jobs->{ids}->%*) {
# 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};
}
return $all_vmids;
}
__PACKAGE__->register_method({
name => 'index',
path => '',
method => 'GET',
description => "Index for backup info related endpoints",
parameters => {
additionalProperties => 0,
properties => {},
},
returns => {
type => 'array',
description => 'Directory index.',
items => {
type => "object",
properties => {
subdir => {
type => 'string',
description => 'API sub-directory endpoint',
},
},
},
links => [{ rel => 'child', href => "{subdir}" }],
},
code => sub {
return [
{ subdir => 'not-backed-up' },
];
},
});
__PACKAGE__->register_method({
name => 'get_guests_not_in_backup',
path => 'not-backed-up',
method => 'GET',
protected => 1,
description => "Shows all guests which are not covered by any backup job.",
permissions => {
check => ['perm', '/', ['Sys.Audit']],
},
parameters => {
additionalProperties => 0,
properties => {},
},
returns => {
type => 'array',
description => 'Contains the guest objects.',
items => {
type => 'object',
properties => {
vmid => {
type => 'integer',
description => 'VMID of the guest.',
},
name => {
type => 'string',
description => 'Name of the guest',
optional => 1,
},
type => {
type => 'string',
description => 'Type of the guest.',
enum => ['qemu', 'lxc'],
},
},
},
},
code => sub {
my $rpcenv = PVE::RPCEnvironment::get();
my $user = $rpcenv->get_user();
my $included_vmids = get_included_vmids();
my $vmlist = PVE::Cluster::get_vmlist();
# remove VMIDs to which the user has no permission to not leak infos like the guest name
my @allowed_vmids =
grep { $rpcenv->check($user, "/vms/$_", ['VM.Audit'], 1) } keys $vmlist->{ids}->%*;
my $result = [];
for my $vmid (@allowed_vmids) {
next if $included_vmids->{$vmid};
my ($type, $node) = $vmlist->{ids}->{$vmid}->@{ 'type', 'node' };
my ($conf, $name);
if ($type eq 'qemu') {
$conf = PVE::QemuConfig->load_config($vmid, $node);
$name = $conf->{name};
} elsif ($type eq 'lxc') {
$conf = PVE::LXC::Config->load_config($vmid, $node);
$name = $conf->{hostname};
} else {
die
"Unexpected error: unknown guest type for VMID $vmid, neither QEMU nor LXC\n";
}
my $entry = {
vmid => int($vmid),
type => $type,
};
$entry->{name} = $name if defined($name);
push @{$result}, $entry;
}
return $result;
},
});
1;