]> git.proxmox.com Git - pve-manager.git/blobdiff - PVE/API2/Backup.pm
api: backup: add endpoint to list included guests and volumes
[pve-manager.git] / PVE / API2 / Backup.pm
index 4c77345ae1042dcaa189666d91542347280b99df..3b3436367b4256c91a6a46f117c3c3d6af1bcc3e 100644 (file)
@@ -2,23 +2,21 @@ package PVE::API2::Backup;
 
 use strict;
 use warnings;
+use Digest::SHA;
 
 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 base qw(PVE::RESTHandler);
 
-cfs_register_file ('vzdump.cron', 
-                  \&parse_vzdump_cron_config, 
-                  \&write_vzdump_cron_config); 
-
 PVE::JSONSchema::register_format('pve-day-of-week', \&verify_day_of_week);
 sub verify_day_of_week {
     my ($value, $noerr) = @_;
@@ -30,174 +28,29 @@ 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 $assert_param_permission = sub {
+    my ($param, $user) = @_;
+    return if $user eq 'root@pam'; # always OK
 
-    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 '*';
-
-    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, undef, undef, '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'");
-       }
-    }
-
-    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 ($hour, $minute);
-
-       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";
-       }
-
-       my $param = "";
-       foreach my $p (keys %$job) {
-           next if $p eq 'id' || $p eq 'vmid' || $p eq 'starttime' || $p eq 'dow';
-           my $v = $job->{$p};
-           $param .= " --$p " . PVE::Tools::shellquote($v) if defined($v) && $v ne '';
-       }
-
-       if ($job->{vmid}) {
-           $param .= " " . join(' ', PVE::Tools::split_list($job->{vmid}));
-       }
-
-       $out .= sprintf "$minute $hour * * %-11s root vzdump$param\n", $dow;
-    }
-
-    my $ejobs = $cfg->{ejobs} || [];
-    foreach my $job (@$ejobs) {
-       $out .= "$job->{line}\n" if $job->{line};
-    }
-
-    return $out;
-}
-
 __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 => {},
@@ -207,7 +60,7 @@ __PACKAGE__->register_method({
        items => {
            type => "object",
            properties => {
-               id => { type => 'string' },
+               id => $vzdump_job_id_prop
            },
        },
        links => [ { rel => 'child', href => "{id}" } ],
@@ -226,14 +79,18 @@ __PACKAGE__->register_method({
     }});
 
 __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({
            starttime => {
                type => 'string',
                description => "Job Start time.",
@@ -246,6 +103,12 @@ __PACKAGE__->register_method({
                description => "Day of week selection.",
                default => 'mon,tue,wed,thu,fri,sat,sun',
            },
+           enabled => {
+               type => 'boolean',
+               optional => 1,
+               description => "Enable or disable the job.",
+               default => '1',
+           },
        }),
     },
     returns => { type => 'null' },
@@ -255,37 +118,43 @@ __PACKAGE__->register_method({
        my $rpcenv = PVE::RPCEnvironment::get();
        my $user = $rpcenv->get_user();
 
-       my $data = cfs_read_file('vzdump.cron');
+       $assert_param_permission->($param, $user);
+
+       if (my $pool = $param->{pool}) {
+           $rpcenv->check_pool_exist($pool);
+           $rpcenv->check($user, "/pool/$pool", ['VM.Backup']);
+       }
 
-       $param->{dow} = 'mon,tue,wed,thu,fri,sat,sun' if !defined($param->{dow});
 
-       $param->{all} = 1 if defined($param->{exclude});
-       raise_param_exc({ all => "option conflicts with option 'vmid'"})
-           if $param->{all} && $param->{vmid};
+       my $create_job = sub {
+           my $data = cfs_read_file('vzdump.cron');
 
-       raise_param_exc({ vmid => "property is missing"})
-           if !$param->{all} && !$param->{vmid};
+           $param->{dow} = 'mon,tue,wed,thu,fri,sat,sun' if !defined($param->{dow});
+           $param->{enabled} = 1 if !defined($param->{enabled});
+           PVE::VZDump::verify_vzdump_parameters($param, 1);
 
-       push @{$data->{jobs}}, $param;
+           push @{$data->{jobs}}, $param;
 
-       cfs_write_file('vzdump.cron', $data);
+           cfs_write_file('vzdump.cron', $data);
+       };
+       cfs_lock_file('vzdump.cron', undef, $create_job);
+       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 => {
@@ -310,19 +179,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' },
@@ -332,43 +200,47 @@ __PACKAGE__->register_method({
        my $rpcenv = PVE::RPCEnvironment::get();
        my $user = $rpcenv->get_user();
 
-       my $data = cfs_read_file('vzdump.cron');
+       my $delete_job = sub {
+           my $data = cfs_read_file('vzdump.cron');
 
-       my $jobs = $data->{jobs} || [];
-       my $newjobs = [];
+           my $jobs = $data->{jobs} || [];
+           my $newjobs = [];
 
-       my $found;
-       foreach my $job (@$jobs) {
-           if ($job->{id} eq $param->{id}) {
-               $found = 1;
-           } else {
-               push @$newjobs, $job;
+           my $found;
+           foreach my $job (@$jobs) {
+               if ($job->{id} eq $param->{id}) {
+                   $found = 1;
+               } else {
+                   push @$newjobs, $job;
+               }
            }
-       }
 
-       raise_param_exc({ id => "No such job '$param->{id}'" }) if !$found;
+           raise_param_exc({ id => "No such job '$param->{id}'" }) if !$found;
 
-       $data->{jobs} = $newjobs;
+           $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,
            starttime => {
                type => 'string',
                description => "Job Start time.",
@@ -385,6 +257,12 @@ __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',
+           },
        }),
     },
     returns => { type => 'null' },
@@ -394,55 +272,239 @@ __PACKAGE__->register_method({
        my $rpcenv = PVE::RPCEnvironment::get();
        my $user = $rpcenv->get_user();
 
-       my $data = cfs_read_file('vzdump.cron');
+       $assert_param_permission->($param, $user);
 
-       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);
+       my $update_job = sub {
+           my $data = cfs_read_file('vzdump.cron');
 
-       raise_param_exc({ all => "option conflicts with option 'vmid'"})
-           if $param->{all} && $param->{vmid};
+           my $jobs = $data->{jobs} || [];
 
-       my $delete = extract_param($param, 'delete');
+           die "no options specified\n" if !scalar(keys %$param);
 
-       foreach my $job (@$jobs) {
-           if ($job->{id} eq $param->{id}) {
+           PVE::VZDump::verify_vzdump_parameters($param);
+
+           my @delete = PVE::Tools::split_list(extract_param($param, 'delete'));
 
-               foreach my $k (PVE::Tools::split_list($delete)) {
-                   if (!PVE::VZDump::option_exists($k)) {
-                       raise_param_exc({ delete => "unknown option '$k'" });
+           foreach my $job (@$jobs) {
+               if ($job->{id} eq $param->{id}) {
+
+                   foreach my $k (@delete) {
+                       if (!PVE::VZDump::option_exists($k)) {
+                           raise_param_exc({ delete => "unknown option '$k'" });
+                       }
+
+                       delete $job->{$k};
                    }
 
-                   delete $job->{$k};
-               }
+                   foreach my $k (keys %$param) {
+                       $job->{$k} = $param->{$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);
 
-               $job->{all} = 1 if defined($job->{exclude});
+                   cfs_write_file('vzdump.cron', $data);
 
-               if ($param->{vmid}) {
-                   delete $job->{all};
-                   delete $job->{exclude};
-               } elsif ($param->{all}) {
-                   delete $job->{vmid};
+                   return undef;
                }
+           }
+           raise_param_exc({ id => "No such job '$param->{id}'" });
+       };
+       cfs_lock_file('vzdump.cron', undef, $update_job);
+       die "$@" if ($@);
+    }});
 
-               raise_param_exc({ all => "option conflicts with option 'vmid'"})
-                   if $job->{all} && $job->{vmid};
+__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) = @_;
 
-               raise_param_exc({ vmid => "property is missing"})
-                   if !$job->{all} && !$job->{vmid};
+       my $rpcenv = PVE::RPCEnvironment::get();
+
+       my $user = $rpcenv->get_user();
 
-               cfs_write_file('vzdump.cron', $data);
+       my $vzconf = cfs_read_file('vzdump.cron');
+       my $all_jobs = $vzconf->{jobs} || [];
+       my $job;
+       my $rrd = PVE::Cluster::rrd_dump();
 
-               return undef;
+       for my $j (@$all_jobs) {
+           if ($j->{id} eq $param->{id}) {
+              $job = $j;
+              last;
            }
        }
+       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;