]> git.proxmox.com Git - pve-manager.git/blobdiff - PVE/API2/Backup.pm
api: backup: update: turn delete into a hash
[pve-manager.git] / PVE / API2 / Backup.pm
index d9cbd916c3c3e2026db830e3f4bc4b1f4d3d9928..1d3d68963ebd3a92f137d8a6747b922564cc9934 100644 (file)
@@ -2,22 +2,26 @@ package PVE::API2::Backup;
 
 use strict;
 use warnings;
+use Digest::SHA;
+use UUID qw(uuid);
 
 use PVE::SafeSyslog;
 use PVE::Tools qw(extract_param);
-use PVE::Cluster qw(cfs_register_file cfs_lock_file cfs_read_file cfs_write_file);
+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 PVE::VZDump::JobBase;
+use PVE::Jobs; # for VZDump Jobs
+use Proxmox::RS::CalendarEvent;
 
 use base qw(PVE::RESTHandler);
 
-cfs_register_file ('vzdump.cron', 
-                  \&parse_vzdump_cron_config, 
-                  \&write_vzdump_cron_config); 
+use constant ALL_DAYS => 'mon,tue,wed,thu,fri,sat,sun';
 
 PVE::JSONSchema::register_format('pve-day-of-week', \&verify_day_of_week);
 sub verify_day_of_week {
@@ -30,167 +34,64 @@ sub verify_day_of_week {
     die "invalid day '$value'\n";
 }
 
-
-my $dowhash_to_dow = sub {
-    my ($d, $num) = @_;
-
-    my @da = ();
-    push @da, $num ? 1 : 'mon' if $d->{mon};
-    push @da, $num ? 2 : 'tue' if $d->{tue};
-    push @da, $num ? 3 : 'wed' if $d->{wed};
-    push @da, $num ? 4 : 'thu' if $d->{thu};
-    push @da, $num ? 5 : 'fri' if $d->{fri};
-    push @da, $num ? 6 : 'sat' if $d->{sat};
-    push @da, $num ? 7 : 'sun' if $d->{sun};
-
-    return join ',', @da;
+my $vzdump_job_id_prop = {
+    type => 'string',
+    description => "The job ID.",
+    maxLength => 50
 };
 
-# parse crontab style day of week
-sub parse_dow {
-    my ($dowstr, $noerr) = @_;
-
-    my $dowmap = {mon => 1, tue => 2, wed => 3, thu => 4,
-                 fri => 5, sat => 6, sun => 7};
-    my $rdowmap = { '1' => 'mon', '2' => 'tue', '3' => 'wed', '4' => 'thu',
-                   '5' => 'fri', '6' => 'sat', '7' => 'sun', '0' => 'sun'};
-
-    my $res = {};
-
-    $dowstr = '1,2,3,4,5,6,7' if $dowstr eq '*';
+# NOTE: also used by the vzdump API call.
+sub assert_param_permission_common {
+    my ($rpcenv, $user, $param) = @_;
+    return if $user eq 'root@pam'; # always OK
 
-    foreach my $day (PVE::Tools::split_list($dowstr)) {
-       if ($day =~ m/^(mon|tue|wed|thu|fri|sat|sun)-(mon|tue|wed|thu|fri|sat|sun)$/i) {
-           for (my $i = $dowmap->{lc($1)}; $i <= $dowmap->{lc($2)}; $i++) {
-               my $r = $rdowmap->{$i};
-               $res->{$r} = 1; 
-           }
-       } elsif ($day =~ m/^(mon|tue|wed|thu|fri|sat|sun|[0-7])$/i) {
-           $day = $rdowmap->{$day} if $day =~ m/\d/;
-           $res->{lc($day)} = 1;
-       } else {
-           return undef if $noerr;
-           die "unable to parse day of week '$dowstr'\n";
-       }
+    for my $key (qw(tmpdir dumpdir script)) {
+       raise_param_exc({ $key => "Only root may set this option."}) if exists $param->{$key};
     }
 
-    return $res;
-};
-
-my $vzdump_propetries = {
-    additionalProperties => 0,
-    properties => PVE::VZDump::json_config_properties({}),
-};
-
-sub parse_vzdump_cron_config {
-    my ($filename, $raw) = @_;
-
-    my $jobs = []; # correct jobs
-
-    my $ejobs = []; # mailfomerd lines
-
-    my $jid = 1; # we start at 1
-    
-    my $digest = Digest::SHA1::sha1_hex(defined($raw) ? $raw : '');
-
-    while ($raw && $raw =~ s/^(.*?)(\n|$)//) {
-       my $line = $1;
-
-       next if $line =~ m/^\#/;
-       next if $line =~ m/^\s*$/;
-       next if $line =~ m/^PATH\s*=/; # we always overwrite path
-
-       if ($line =~ m|^(\d+)\s+(\d+)\s+\*\s+\*\s+(\S+)\s+root\s+(/\S+/)?vzdump(\s+(.*))?$|) {
-           eval {
-               my $minute = int($1);
-               my $hour = int($2);
-               my $dow = $3;
-               my $param = $6;
-
-               my $dowhash = parse_dow($dow, 1);
-               die "unable to parse day of week '$dow' in '$filename'\n" if !$dowhash;
-
-               my $args = PVE::Tools::split_args($param);
-               my $opts = PVE::JSONSchema::get_options($vzdump_propetries, $args, 'vmid');
-
-               $opts->{id} = "$digest:$jid";
-               $jid++;
-               $opts->{starttime} = sprintf "%02d:%02d", $hour, $minute;
-               $opts->{dow} = &$dowhash_to_dow($dowhash);
-
-               push @$jobs, $opts;
-           };
-           my $err = $@;
-           if ($err) {
-               syslog ('err', "parse error in '$filename': $err");
-               push @$ejobs, { line => $line };
-           }
-       } elsif ($line =~ m|^\S+\s+(\S+)\s+\S+\s+\S+\s+\S+\s+\S+\s+(\S.*)$|) {
-           syslog ('err', "warning: malformed line in '$filename'");
-           push @$ejobs, { line => $line };
-       } else {
-           syslog ('err', "ignoring malformed line in '$filename'");
-       }
+    if (defined($param->{bwlimit}) || defined($param->{ionice}) || defined($param->{performance})) {
+       $rpcenv->check($user, "/", [ 'Sys.Modify' ]);
     }
-
-    my $res = {};
-    $res->{digest} = $digest;
-    $res->{jobs} = $jobs;
-    $res->{ejobs} = $ejobs;
-
-    return $res;
 }
 
-sub write_vzdump_cron_config {
-    my ($filename, $cfg) = @_;
-
-    my $out = "# cluster wide vzdump cron schedule\n";
-    $out .= "# Atomatically generated file - do not edit\n\n";
-    $out .= "PATH=\"/usr/sbin:/usr/bin:/sbin:/bin\"\n\n";
-
-    my $jobs = $cfg->{jobs} || [];
-    foreach my $job (@$jobs) {
-       my $dh = parse_dow($job->{dow});
-       my $dow;
-       if ($dh->{mon} && $dh->{tue} && $dh->{wed} && $dh->{thu} &&
-           $dh->{fri} && $dh->{sat} && $dh->{sun}) {
-           $dow = '*';
-       } else {
-           $dow = &$dowhash_to_dow($dh, 1);
-           $dow = '*' if !$dow;
-       }
+my $convert_to_schedule = sub {
+    my ($job) = @_;
 
-       my ($hour, $minute);
+    my $starttime = $job->{starttime};
 
-       die "no job start time specified\n" if !$job->{starttime};
-       if ($job->{starttime} =~ m/^(\d{1,2}):(\d{1,2})$/) {
-           ($hour, $minute) = (int($1), int($2));
-           die "hour '$hour' out of range\n" if $hour < 0 || $hour > 23;
-           die "minute '$minute' out of range\n" if $minute < 0 || $minute > 59;
-       } else {
-           die "unable to parse job start time\n";
-       }
-       
-       $job->{quiet} = 1; # we do not want messages from cron
+    return "$starttime" if !$job->{dow}; # dow is restrictive, so none means all days
 
-       my $cmd = PVE::VZDump::command_line($job);
+    # normalize as it could be a null-separated list previously
+    my $dow = join(',', PVE::Tools::split_list($job->{dow}));
 
-       $out .= sprintf "$minute $hour * * %-11s root $cmd\n", $dow;
-    }
+    return $dow eq ALL_DAYS ? "$starttime" : "$dow $starttime";
+};
 
-    my $ejobs = $cfg->{ejobs} || [];
-    foreach my $job (@$ejobs) {
-       $out .= "$job->{line}\n" if $job->{line};
+my $schedule_param_check = sub {
+    my ($param, $required) = @_;
+    if (defined($param->{schedule})) {
+       if (defined($param->{starttime})) {
+           raise_param_exc({ starttime => "'starttime' and 'schedule' cannot both be set" });
+       }
+    } elsif (!defined($param->{starttime})) {
+       raise_param_exc({ schedule => "neither 'starttime' nor 'schedule' were set" })
+           if $required;
+    } else {
+       $param->{schedule} = $convert_to_schedule->($param);
     }
 
-    return $out;
-}
+    delete $param->{starttime};
+    delete $param->{dow};
+};
 
 __PACKAGE__->register_method({
-    name => 'index', 
-    path => '', 
+    name => 'index',
+    path => '',
     method => 'GET',
     description => "List vzdump backup schedule.",
+    permissions => {
+       check => ['perm', '/', ['Sys.Audit']],
+    },
     parameters => {
        additionalProperties => 0,
        properties => {},
@@ -200,7 +101,7 @@ __PACKAGE__->register_method({
        items => {
            type => "object",
            properties => {
-               id => { type => 'string' },
+               id => $vzdump_job_id_prop
            },
        },
        links => [ { rel => 'child', href => "{id}" } ],
@@ -212,32 +113,95 @@ __PACKAGE__->register_method({
        my $user = $rpcenv->get_user();
 
        my $data = cfs_read_file('vzdump.cron');
+       my $jobs_data = cfs_read_file('jobs.cfg');
+       my $order = $jobs_data->{order};
+       my $jobs = $jobs_data->{ids};
 
        my $res = $data->{jobs} || [];
+       foreach my $job (@$res) {
+           $job->{schedule} = $convert_to_schedule->($job);
+       }
+
+       foreach my $jobid (sort { $order->{$a} <=> $order->{$b} } keys %$jobs) {
+           my $job = $jobs->{$jobid};
+           next if $job->{type} ne 'vzdump';
+
+           if (my $schedule = $job->{schedule}) {
+               # vzdump jobs are cluster wide, there maybe was no local run
+               # so simply calculate from now
+               my $last_run = time();
+               my $calspec = Proxmox::RS::CalendarEvent->new($schedule);
+               my $next_run = $calspec->compute_next_event($last_run);
+               $job->{'next-run'} = $next_run if defined($next_run);
+           }
+
+           # FIXME remove in PVE 8.0?
+           # backwards compat: before moving the job registry to pve-common, id was auto-injected
+           $job->{id} = $jobid;
+
+           push @$res, $job;
+       }
 
        return $res;
     }});
 
 __PACKAGE__->register_method({
-    name => 'create_job', 
-    path => '', 
+    name => 'create_job',
+    path => '',
     method => 'POST',
     protected => 1,
     description => "Create new vzdump backup job.",
+    permissions => {
+       check => ['perm', '/', ['Sys.Modify']],
+       description => "The 'tmpdir', 'dumpdir' and 'script' parameters are additionally restricted to the 'root\@pam' user.",
+    },
     parameters => {
        additionalProperties => 0,
-       properties => PVE::VZDump::json_config_properties({
+       properties => PVE::VZDump::Common::json_config_properties({
+           id => {
+               type => 'string',
+               description => "Job ID (will be autogenerated).",
+               format => 'pve-configid',
+               optional => 1, # FIXME: make required on 8.0
+           },
+           schedule => {
+               description => "Backup schedule. The format is a subset of `systemd` calendar events.",
+               type => 'string', format => 'pve-calendar-event',
+               maxLength => 128,
+               optional => 1,
+           },
            starttime => {
                type => 'string',
                description => "Job Start time.",
                pattern => '\d{1,2}:\d{1,2}',
                typetext => 'HH:MM',
+               optional => 1,
            },
            dow => {
                type => 'string', format => 'pve-day-of-week-list',
                optional => 1,
                description => "Day of week selection.",
-               default => 'mon,tue,wed,thu,fri,sat,sun',
+               requires => 'starttime',
+               default => ALL_DAYS,
+           },
+           enabled => {
+               type => 'boolean',
+               optional => 1,
+               description => "Enable or disable the job.",
+               default => '1',
+           },
+           'repeat-missed' => {
+               optional => 1,
+               type => 'boolean',
+               description => "If true, the job will be run as soon as possible if it was missed".
+                   " while the scheduler was not running.",
+               default => 0,
+           },
+           comment => {
+               optional => 1,
+               type => 'string',
+               description => "Description for the Job.",
+               maxLength => 512,
            },
        }),
     },
@@ -248,32 +212,52 @@ __PACKAGE__->register_method({
        my $rpcenv = PVE::RPCEnvironment::get();
        my $user = $rpcenv->get_user();
 
-       my $data = cfs_read_file('vzdump.cron');
+       assert_param_permission_common($rpcenv, $user, $param);
+
+       if (my $pool = $param->{pool}) {
+           $rpcenv->check_pool_exist($pool);
+           $rpcenv->check($user, "/pool/$pool", ['VM.Backup']);
+       }
+
+       $schedule_param_check->($param, 1);
+
+       $param->{enabled} = 1 if !defined($param->{enabled});
+
+       # autogenerate id for api compatibility FIXME remove with 8.0
+       my $id = extract_param($param, 'id') // UUID::uuid();
 
-       $param->{dow} = 'mon,tue,wed,thu,fri,sat,sun' if !defined($param->{dow});
+       cfs_lock_file('jobs.cfg', undef, sub {
+           my $data = cfs_read_file('jobs.cfg');
 
-       PVE::VZDump::verify_vzdump_parameters($param, 1);
+           die "Job '$id' already exists\n"
+               if $data->{ids}->{$id};
 
-       push @{$data->{jobs}}, $param;
+           PVE::VZDump::verify_vzdump_parameters($param, 1);
+           my $opts = PVE::VZDump::JobBase->check_config($id, $param, 1, 1);
 
-       cfs_write_file('vzdump.cron', $data);
+           $data->{ids}->{$id} = $opts;
+
+           PVE::Jobs::create_job($id, 'vzdump', $opts);
+
+           cfs_write_file('jobs.cfg', $data);
+       });
+       die "$@" if ($@);
 
        return undef;
     }});
 
 __PACKAGE__->register_method({
-    name => 'read_job', 
-    path => '{id}', 
+    name => 'read_job',
+    path => '{id}',
     method => 'GET',
     description => "Read vzdump backup job definition.",
+    permissions => {
+       check => ['perm', '/', ['Sys.Audit']],
+    },
     parameters => {
        additionalProperties => 0,
        properties => {
-           id => {
-               type => 'string',
-               description => "The job ID.",
-               maxLength => 50,
-           }
+           id => $vzdump_job_id_prop
        },
     },
     returns => {
@@ -290,7 +274,19 @@ __PACKAGE__->register_method({
        my $jobs = $data->{jobs} || [];
 
        foreach my $job (@$jobs) {
-           return $job if $job->{id} eq $param->{id};
+           if ($job->{id} eq $param->{id}) {
+               $job->{schedule} = $convert_to_schedule->($job);
+               return $job;
+           }
+       }
+
+       my $jobs_data = cfs_read_file('jobs.cfg');
+       my $job = $jobs_data->{ids}->{$param->{id}};
+       if ($job && $job->{type} eq 'vzdump') {
+           # FIXME remove in PVE 8.0?
+           # backwards compat: before moving the job registry to pve-common, id was auto-injected
+           $job->{id} = $param->{id};
+           return $job;
        }
 
        raise_param_exc({ id => "No such job '$param->{id}'" });
@@ -298,19 +294,18 @@ __PACKAGE__->register_method({
     }});
 
 __PACKAGE__->register_method({
-    name => 'delete_job', 
-    path => '{id}', 
+    name => 'delete_job',
+    path => '{id}',
     method => 'DELETE',
     description => "Delete vzdump backup job definition.",
+    permissions => {
+       check => ['perm', '/', ['Sys.Modify']],
+    },
     protected => 1,
     parameters => {
        additionalProperties => 0,
        properties => {
-           id => {
-               type => 'string',
-               description => "The job ID.",
-               maxLength => 50,
-           }
+           id => $vzdump_job_id_prop
        },
     },
     returns => { type => 'null' },
@@ -320,52 +315,80 @@ __PACKAGE__->register_method({
        my $rpcenv = PVE::RPCEnvironment::get();
        my $user = $rpcenv->get_user();
 
-       my $data = cfs_read_file('vzdump.cron');
+       my $id = $param->{id};
 
-       my $jobs = $data->{jobs} || [];
-       my $newjobs = [];
+       my $delete_job = sub {
+           my $data = cfs_read_file('vzdump.cron');
 
-       my $found;
-       foreach my $job (@$jobs) {
-           if ($job->{id} eq $param->{id}) {
-               $found = 1;
-           } else {
-               push @$newjobs, $job;
+           my $jobs = $data->{jobs} || [];
+           my $newjobs = [];
+
+           my $found;
+           foreach my $job (@$jobs) {
+               if ($job->{id} eq $id) {
+                   $found = 1;
+               } else {
+                   push @$newjobs, $job;
+               }
            }
-       }
 
-       raise_param_exc({ id => "No such job '$param->{id}'" }) if !$found;
+           if (!$found) {
+               cfs_lock_file('jobs.cfg', undef, sub {
+                   my $jobs_data = cfs_read_file('jobs.cfg');
+
+                   if (!defined($jobs_data->{ids}->{$id})) {
+                       raise_param_exc({ id => "No such job '$id'" });
+                   }
+                   delete $jobs_data->{ids}->{$id};
+
+                   PVE::Jobs::remove_job($id, 'vzdump');
 
-       $data->{jobs} = $newjobs;
+                   cfs_write_file('jobs.cfg', $jobs_data);
+               });
+               die "$@" if $@;
+           } else {
+               $data->{jobs} = $newjobs;
 
-       cfs_write_file('vzdump.cron', $data);
+               cfs_write_file('vzdump.cron', $data);
+           }
+       };
+       cfs_lock_file('vzdump.cron', undef, $delete_job);
+       die "$@" if ($@);
 
        return undef;
     }});
 
 __PACKAGE__->register_method({
-    name => 'update_job', 
-    path => '{id}', 
+    name => 'update_job',
+    path => '{id}',
     method => 'PUT',
     protected => 1,
     description => "Update vzdump backup job definition.",
+    permissions => {
+       check => ['perm', '/', ['Sys.Modify']],
+       description => "The 'tmpdir', 'dumpdir' and 'script' parameters are additionally restricted to the 'root\@pam' user.",
+    },
     parameters => {
        additionalProperties => 0,
-       properties => PVE::VZDump::json_config_properties({
-           id => {
-               type => 'string',
-               description => "The job ID.",
-               maxLength => 50,
+       properties => PVE::VZDump::Common::json_config_properties({
+           id => $vzdump_job_id_prop,
+           schedule => {
+               description => "Backup schedule. The format is a subset of `systemd` calendar events.",
+               type => 'string', format => 'pve-calendar-event',
+               maxLength => 128,
+               optional => 1,
            },
            starttime => {
                type => 'string',
                description => "Job Start time.",
                pattern => '\d{1,2}:\d{1,2}',
                typetext => 'HH:MM',
+               optional => 1,
            },
            dow => {
                type => 'string', format => 'pve-day-of-week-list',
                optional => 1,
+               requires => 'starttime',
                description => "Day of week selection.",
            },
            delete => {
@@ -373,6 +396,25 @@ __PACKAGE__->register_method({
                description => "A list of settings you want to delete.",
                optional => 1,
            },
+           enabled => {
+               type => 'boolean',
+               optional => 1,
+               description => "Enable or disable the job.",
+               default => '1',
+           },
+           'repeat-missed' => {
+               optional => 1,
+               type => 'boolean',
+               description => "If true, the job will be run as soon as possible if it was missed".
+                   " while the scheduler was not running.",
+               default => 0,
+           },
+           comment => {
+               optional => 1,
+               type => 'string',
+               description => "Description for the Job.",
+               maxLength => 512,
+           },
        }),
     },
     returns => { type => 'null' },
@@ -382,50 +424,276 @@ __PACKAGE__->register_method({
        my $rpcenv = PVE::RPCEnvironment::get();
        my $user = $rpcenv->get_user();
 
-       my $data = cfs_read_file('vzdump.cron');
+       assert_param_permission_common($rpcenv, $user, $param);
 
-       my $jobs = $data->{jobs} || [];
+       if (my $pool = $param->{pool}) {
+           $rpcenv->check_pool_exist($pool);
+           $rpcenv->check($user, "/pool/$pool", ['VM.Backup']);
+       }
 
-       die "no options specified\n" if !scalar(keys %$param);
+       $schedule_param_check->($param);
 
-       PVE::VZDump::verify_vzdump_parameters($param);
+       my $id = extract_param($param, 'id');
+       my $delete = extract_param($param, 'delete');
+       $delete = { map { $_ => 1 } PVE::Tools::split_list($delete) } if $delete;
 
-       my @delete = PVE::Tools::split_list(extract_param($param, 'delete'));
+       my $update_job = sub {
+           my $data = cfs_read_file('vzdump.cron');
+           my $jobs_data = cfs_read_file('jobs.cfg');
 
-       foreach my $job (@$jobs) {
-           if ($job->{id} eq $param->{id}) {
+           my $jobs = $data->{jobs} || [];
 
-               foreach my $k (@delete) {
-                   if (!PVE::VZDump::option_exists($k)) {
-                       raise_param_exc({ delete => "unknown option '$k'" });
-                   }
+           die "no options specified\n" if !scalar(keys %$param);
 
-                   delete $job->{$k};
-               }
+           PVE::VZDump::verify_vzdump_parameters($param);
+           my $opts = PVE::VZDump::JobBase->check_config($id, $param, 0, 1);
 
-               foreach my $k (keys %$param) {
-                   $job->{$k} = $param->{$k};
-               }
+           # try to find it in old vzdump.cron and convert it to a job
+           my ($idx) = grep { $jobs->[$_]->{id} eq $id } (0 .. scalar(@$jobs) - 1);
+
+           my $job;
+           if (defined($idx)) {
+               $job = splice @$jobs, $idx, 1;
+               $job->{schedule} = $convert_to_schedule->($job);
+               delete $job->{starttime};
+               delete $job->{dow};
+               delete $job->{id};
+               $job->{type} = 'vzdump';
+               $jobs_data->{ids}->{$id} = $job;
+           } else {
+               $job = $jobs_data->{ids}->{$id};
+               die "no such vzdump job\n" if !$job || $job->{type} ne 'vzdump';
+           }
 
-               $job->{all} = 1 if defined($job->{exclude});
+           my $deletable = {
+               comment => 1,
+               'repeat-missed' => 1,
+           };
 
-               if (defined($param->{vmid})) {
-                   delete $job->{all};
-                   delete $job->{exclude};
-               } elsif ($param->{all}) {
-                   delete $job->{vmid};
+           for my $k (keys $delete->%*) {
+               if (!PVE::VZDump::option_exists($k) && !$deletable->{$k}) {
+                   raise_param_exc({ delete => "unknown option '$k'" });
                }
 
-               PVE::VZDump::verify_vzdump_parameters($job, 1);
+               delete $job->{$k};
+           }
+
+           foreach my $k (keys %$param) {
+               $job->{$k} = $param->{$k};
+           }
+
+           $job->{all} = 1 if (defined($job->{exclude}) && !defined($job->{pool}));
+
+           if (defined($param->{vmid})) {
+               delete $job->{all};
+               delete $job->{exclude};
+               delete $job->{pool};
+           } elsif ($param->{all}) {
+               delete $job->{vmid};
+               delete $job->{pool};
+           } elsif ($job->{pool}) {
+               delete $job->{vmid};
+               delete $job->{all};
+               delete $job->{exclude};
+           }
+
+           PVE::VZDump::verify_vzdump_parameters($job, 1);
 
+           if (defined($idx)) {
                cfs_write_file('vzdump.cron', $data);
+           }
+           cfs_write_file('jobs.cfg', $jobs_data);
+
+           PVE::Jobs::detect_changed_runtime_props($id, 'vzdump', $job);
 
-               return undef;
+           return;
+       };
+       cfs_lock_file('vzdump.cron', undef, sub {
+           cfs_lock_file('jobs.cfg', undef, $update_job);
+           die "$@" if ($@);
+       });
+       die "$@" if ($@);
+    }});
+
+__PACKAGE__->register_method({
+    name => 'get_volume_backup_included',
+    path => '{id}/included_volumes',
+    method => 'GET',
+    protected => 1,
+    description => "Returns included guests and the backup status of their disks. Optimized to be used in ExtJS tree views.",
+    permissions => {
+       check => ['perm', '/', ['Sys.Audit']],
+    },
+    parameters => {
+       additionalProperties => 0,
+       properties => {
+           id => $vzdump_job_id_prop
+       },
+    },
+    returns => {
+       type => 'object',
+       description => 'Root node of the tree object. Children represent guests, grandchildren represent volumes of that guest.',
+       properties => {
+           children => {
+               type => 'array',
+               items => {
+                   type => 'object',
+                   properties => {
+                       id => {
+                           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, VM, CT or unknown for removed but not purged guests.',
+                           enum => ['qemu', 'lxc', 'unknown'],
+                       },
+                       children => {
+                           type => 'array',
+                           optional => 1,
+                           description => 'The volumes of the guest with the information if they will be included in backups.',
+                           items => {
+                               type => 'object',
+                               properties => {
+                                   id => {
+                                       type => 'string',
+                                       description => 'Configuration key of the volume.',
+                                   },
+                                   name => {
+                                       type => 'string',
+                                       description => 'Name of the volume.',
+                                   },
+                                   included => {
+                                       type => 'boolean',
+                                       description => 'Whether the volume is included in the backup or not.',
+                                   },
+                                   reason => {
+                                       type => 'string',
+                                       description => 'The reason why the volume is included (or excluded).',
+                                   },
+                               },
+                           },
+                       },
+                   },
+               },
+           },
+       },
+    },
+    code => sub {
+       my ($param) = @_;
+
+       my $rpcenv = PVE::RPCEnvironment::get();
+
+       my $user = $rpcenv->get_user();
+
+       my $vzconf = cfs_read_file('vzdump.cron');
+       my $all_jobs = $vzconf->{jobs} || [];
+       my $job;
+       my $rrd = PVE::Cluster::rrd_dump();
+
+       for my $j (@$all_jobs) {
+           if ($j->{id} eq $param->{id}) {
+              $job = $j;
+              last;
+           }
+       }
+       if (!$job) {
+           my $jobs_data = cfs_read_file('jobs.cfg');
+           my $j = $jobs_data->{ids}->{$param->{id}};
+           if ($j && $j->{type} eq 'vzdump') {
+               $job = $j;
            }
        }
+       raise_param_exc({ id => "No such job '$param->{id}'" }) if !$job;
 
-       raise_param_exc({ id => "No such job '$param->{id}'" });
+       my $vmlist = PVE::Cluster::get_vmlist();
+
+       my @job_vmids;
+
+       my $included_guests = PVE::VZDump::get_included_guests($job);
+
+       for my $node (keys %{$included_guests}) {
+           my $node_vmids = $included_guests->{$node};
+           push(@job_vmids, @{$node_vmids});
+       }
+
+       # 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);
+       } @job_vmids;
+
+       my $result = {
+           children => [],
+       };
+
+       for my $vmid (@allowed_vmids) {
+
+           my $children = [];
+
+           # It's possible that a job has VMIDs configured that are not in
+           # vmlist. This could be because a guest was removed but not purged.
+           # Since there is no more data available we can only deliver the VMID
+           # and no volumes.
+           if (!defined $vmlist->{ids}->{$vmid}) {
+               push(@{$result->{children}}, {
+                   id => int($vmid),
+                   type => 'unknown',
+                   leaf => 1,
+               });
+               next;
+           }
+
+           my $type = $vmlist->{ids}->{$vmid}->{type};
+           my $node = $vmlist->{ids}->{$vmid}->{node};
+
+           my $conf;
+           my $volumes;
+           my $name = "";
+
+           if ($type eq 'qemu') {
+               $conf = PVE::QemuConfig->load_config($vmid, $node);
+               $volumes = PVE::QemuConfig->get_backup_volumes($conf);
+               $name = $conf->{name};
+           } elsif ($type eq 'lxc') {
+               $conf = PVE::LXC::Config->load_config($vmid, $node);
+               $volumes = PVE::LXC::Config->get_backup_volumes($conf);
+               $name = $conf->{hostname};
+           } else {
+               die "VMID $vmid is neither Qemu nor LXC guest\n";
+           }
+
+           foreach my $volume (@$volumes) {
+               my $disk = {
+                   # id field must be unique for ExtJS tree view
+                   id => "$vmid:$volume->{key}",
+                   name => $volume->{volume_config}->{file} // $volume->{volume_config}->{volume},
+                   included=> $volume->{included},
+                   reason => $volume->{reason},
+                   leaf => 1,
+               };
+               push(@{$children}, $disk);
+           }
+
+           my $leaf = 0;
+           # it's possible for a guest to have no volumes configured
+           $leaf = 1 if !@{$children};
+
+           push(@{$result->{children}}, {
+                   id => int($vmid),
+                   type => $type,
+                   name => $name,
+                   children => $children,
+                   leaf => $leaf,
+           });
+       }
 
+       return $result;
     }});
 
 1;