]> git.proxmox.com Git - pve-manager.git/blobdiff - PVE/VZDump.pm
set permissions for VZDump API
[pve-manager.git] / PVE / VZDump.pm
index dd6d6602ad8b6f1be95e5aa94551c4185462ec37..0647f308bd46cc2b304e9ceb68af5e7eaf05b8c4 100644 (file)
@@ -1,38 +1,22 @@
 package PVE::VZDump;
 
-#    Copyright (C) 2007-2009 Proxmox Server Solutions GmbH
-#
-#    Copyright: vzdump is under GNU GPL, the GNU General Public License.
-#
-#    This program is free software; you can redistribute it and/or modify
-#    it under the terms of the GNU General Public License as published by
-#    the Free Software Foundation; version 2 dated June, 1991.
-#
-#    This program is distributed in the hope that it will be useful,
-#    but WITHOUT ANY WARRANTY; without even the implied warranty of
-#    MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
-#    GNU General Public License for more details.
-#
-#    You should have received a copy of the GNU General Public License
-#    along with this program; if not, write to the
-#    Free Software Foundation, Inc., 51 Franklin St, Fifth Floor, Boston,
-#    MA 02110-1301, USA.
-#
-#    Author: Dietmar Maurer <dietmar@proxmox.com>
-
 use strict;
 use warnings;
 use Fcntl ':flock';
-use Sys::Hostname;
-use Sys::Syslog;
+use PVE::Exception qw(raise_param_exc);
+use PVE::SafeSyslog;
 use IO::File;
 use IO::Select;
 use IPC::Open3;
 use POSIX qw(strftime);
 use File::Path;
+use PVE::RPCEnvironment;
+use PVE::Storage;
+use PVE::Cluster qw(cfs_read_file);
 use PVE::VZDump::OpenVZ;
 use Time::localtime;
 use Time::Local;
+use PVE::JSONSchema qw(get_standard_option);
 
 my @posix_filesystems = qw(ext3 ext4 nfs nfs4 reiserfs xfs);
 
@@ -84,112 +68,19 @@ sub debugmsg {
 sub run_command {
     my ($logfd, $cmdstr, %param) = @_;
 
-    my $timeout;
-    my $input;
-    my $output;
-
-    foreach my $p (keys %param) {
-       if ($p eq 'timeout') {
-           $timeout = $param{$p};
-       } elsif ($p eq 'input') {
-           $input = $param{$p};
-       } elsif ($p eq 'output') {
-           $output = $param{$p};
-       } else {
-           die "got unknown parameter '$p' for run_command\n";
-       }
-    }
-
-    my $reader = $output && $output =~ m/^>&/ ? $output : IO::File->new();
-    my $writer = $input && $input =~ m/^<&/ ? $input : IO::File->new();
-    my $error  = IO::File->new();
-
-    my $orig_pid = $$;
-
-    my $pid;
-    eval {
-       # suppress LVM warnings like: "File descriptor 3 left open";
-       local $ENV{LVM_SUPPRESS_FD_WARNINGS} = "1";
-
-       $pid = open3 ($writer, $reader, $error, ($cmdstr)) || die $!;
+    my $logfunc = sub {
+       my $line = shift;
+       debugmsg ('info', $line, $logfd);
     };
 
-    my $err = $@;
-
-    # catch exec errors
-    if ($orig_pid != $$) {
-       debugmsg ('err', "command '$cmdstr' failed - fork failed: $!", $logfd);
-       POSIX::_exit (1); 
-       kill ('KILL', $$); 
-    }
-
-    die $err if $err;
-
-    if (ref($writer)) {
-       print $writer $input if defined $input;
-       close $writer;
-    }
-
-    my $select = new IO::Select;
-    $select->add ($reader) if ref($reader);
-    $select->add ($error);
-
-    my ($ostream, $estream, $logout, $logerr) = ('', '', '', '');
-
-    while ($select->count) {
-       my @handles = $select->can_read ($timeout);
-
-       if (defined ($timeout) && (scalar (@handles) == 0)) {
-           die "command '$cmdstr' failed: timeout\n";
-       }
-
-       foreach my $h (@handles) {
-           my $buf = '';
-           my $count = sysread ($h, $buf, 4096);
-           if (!defined ($count)) {
-               waitpid ($pid, 0);
-               die "command '$cmdstr' failed: $!\n";
-           }
-           $select->remove ($h) if !$count;
-
-           if ($h eq $reader) {
-               $ostream .= $buf;
-               $logout .= $buf;
-               while ($logout =~ s/^([^\n]*\n)//s) {
-                   my $line = $1;
-                   debugmsg ('info', $line, $logfd);
-               }
-           } elsif ($h eq $error) {
-               $estream .= $buf;
-               $logerr .= $buf;
-               while ($logerr =~  s/^([^\n]*\n)//s) {
-                   my $line = $1;
-                   debugmsg ('info', $line, $logfd);
-               }
-           }
-       }
-    }
-
-    debugmsg ('info', $logout, $logfd);
-    debugmsg ('info', $logerr, $logfd);
-
-    waitpid ($pid, 0);
-    my $ec = ($? >> 8);
-
-    return $ostream if $ec == 24 && ($cmdstr =~ m|^(\S+/)?rsync\s|);
-
-    die "command '$cmdstr' failed with exit code $ec\n" if $ec;
-
-    return $ostream;
+    PVE::Tools::run_command($cmdstr, %param, logfunc => $logfunc);
 }
 
 sub storage_info {
     my $storage = shift;
 
-    eval { require PVE::Storage; };
-    die "unable to query storage info for '$storage' - $@\n" if $@;
-    my $cfg = PVE::Storage::load_config();
-    my $scfg = PVE::Storage::storage_config ($cfg, $storage);
+    my $cfg = cfs_read_file('storage.cfg');
+    my $scfg = PVE::Storage::storage_config($cfg, $storage);
     my $type = $scfg->{type};
  
     die "can't use storage type '$type' for backup\n" 
@@ -197,10 +88,10 @@ sub storage_info {
     die "can't use storage for backups - wrong content type\n" 
        if (!$scfg->{content}->{backup});
 
-    PVE::Storage::activate_storage ($cfg, $storage);
+    PVE::Storage::activate_storage($cfg, $storage);
 
     return {
-       dumpdir => $scfg->{path},
+       dumpdir => PVE::Storage::get_backup_dir($cfg, $storage),
     };
 }
 
@@ -272,7 +163,7 @@ sub check_vmids {
     foreach my $vmid (@vmids) {
        die "ERROR: strange VM ID '${vmid}'\n" if $vmid !~ m/^\d+$/;
        $vmid = int ($vmid); # remove leading zeros
-       die "ERROR: got reserved VM ID '${vmid}'\n" if $vmid < 100;
+       next if !$vmid;
        push @$res, $vmid;
     }
 
@@ -322,6 +213,8 @@ sub read_vzdump_defaults {
            $res->{size} = int($1);
        } elsif ($line =~ m/maxfiles:\s*(\d+)\s*$/) {
            $res->{maxfiles} = int($1);
+       } elsif ($line =~ m/exclude-path:\s*(.*)\s*$/) {
+           $res->{'exclude-path'} = PVE::Tools::split_args($1); 
        } elsif ($line =~ m/mode:\s*(stop|snapshot|suspend)\s*$/) {
            $res->{mode} = $1;
        } else {
@@ -349,24 +242,6 @@ sub find_add_exclude {
     }
 }
 
-sub read_firstfile {
-    my $archive = shift;
-    
-    die "ERROR: file '$archive' does not exist\n" if ! -f $archive;
-
-    # try to detect archive type first
-    my $pid = open (TMP, "tar tf '$archive'|") ||
-       die "unable to open file '$archive'\n";
-    my $firstfile = <TMP>;
-    kill 15, $pid;
-    close TMP;
-
-    die "ERROR: archive contaions no data\n" if !$firstfile;
-    chomp $firstfile;
-
-    return $firstfile;
-}
-
 my $sendmail = sub {
     my ($self, $tasklist, $totaltime) = @_;
 
@@ -374,7 +249,7 @@ my $sendmail = sub {
 
     my $mailto = $opts->{mailto};
 
-    return if !$mailto;
+    return if !($mailto && scalar(@$mailto));
 
     my $cmdline = $self->{cmdline};
 
@@ -394,10 +269,9 @@ my $sendmail = sub {
 
     my $stat = $ecount ? 'backup failed' : 'backup successful';
 
-    my $hostname = `hostname -f` || hostname();
+    my $hostname = `hostname -f` || PVE::INotify::nodename();
     chomp $hostname;
 
-
     my $boundary = "----_=_NextPart_001_".int(time).$$;
 
     my $rcvrarg = '';
@@ -539,10 +413,11 @@ my $sendmail = sub {
     # end html part
     print MAIL "\n--$boundary--\n";
 
+    close(MAIL);
 };
 
 sub new {
-    my ($class, $cmdline, $opts) = @_;
+    my ($class, $cmdline, $opts, $skiplist) = @_;
 
     mkpath $logdir;
 
@@ -556,7 +431,7 @@ sub new {
     check_bin ('cstream');
     check_bin ('ionice');
 
-    if ($opts->{snapshot}) {
+    if ($opts->{mode} && $opts->{mode} eq 'snapshot') {
        check_bin ('lvcreate');
        check_bin ('lvs');
        check_bin ('lvremove');
@@ -573,20 +448,23 @@ sub new {
        }
     }
 
-    $opts->{mode} = 'stop' if $opts->{stop};
-    $opts->{mode} = 'suspend' if $opts->{suspend};
-    $opts->{mode} = 'snapshot' if $opts->{snapshot};
-
     $opts->{dumpdir} =~ s|/+$|| if ($opts->{dumpdir});
     $opts->{tmpdir} =~ s|/+$|| if ($opts->{tmpdir});
 
-    my $self = bless { cmdline => $cmdline, opts => $opts };
+    $skiplist = [] if !$skiplist;
+    my $self = bless { cmdline => $cmdline, opts => $opts, skiplist => $skiplist };
 
     #always skip '.'
     push @{$self->{findexcl}}, "'('", '-regex' , "'^\\.\$'", "')'", '-o';
 
     $self->find_add_exclude ('-type', 's'); # skip sockets
 
+    if ($defaults->{'exclude-path'}) {
+       foreach my $path (@{$defaults->{'exclude-path'}}) {
+           $self->find_add_exclude ('-regex', $path);
+       }
+    }
+
     if ($opts->{'exclude-path'}) {
        foreach my $path (@{$opts->{'exclude-path'}}) {
            $self->find_add_exclude ('-regex', $path);
@@ -670,7 +548,7 @@ sub get_mount_info {
 
     return undef if !$out;
    
-    my @res = split (/\s+/, $out);
+    my @res = $out =~ m/^(\S+)\s+(\S+)\s+(\d+)\s+(\d+)\s+(\d+)\s+(\d+)%\s+(.*)$/;
 
     return undef if scalar (@res) != 7;
 
@@ -684,7 +562,7 @@ sub get_mount_info {
 sub get_lvm_device {
     my ($dir, $mapping) = @_;
 
-    my $info = get_mount_info ($dir);
+    my $info = get_mount_info($dir);
 
     return undef if !$info;
    
@@ -814,7 +692,7 @@ sub exec_backup_task {
            $task->{tmpdir} = "$opts->{tmpdir}/vzdumptmp$$"; 
        } else {
            # dumpdir is posix? then use it as temporary dir
-           my $info = get_mount_info ($opts->{dumpdir});
+           my $info = get_mount_info($opts->{dumpdir});
            if ($vmtype eq 'qemu' || 
                grep ($_ eq $info->{fstype}, @posix_filesystems)) {
                $task->{tmpdir} = "$opts->{dumpdir}/$basename.tmp";
@@ -958,7 +836,7 @@ sub exec_backup_task {
 
        if ($opts->{stdout}) {
            debugmsg ('info', "sending archive to stdout", $logfd);
-           $plugin->archive ($task, $vmid, $task->{tmptar});
+           $plugin->archive($task, $vmid, $task->{tmptar});
            $self->run_hook_script ('backup-end', $task, $logfd);
            return;
        }
@@ -983,8 +861,9 @@ sub exec_backup_task {
            my $dir = $opts->{dumpdir};
            foreach my $fn (<$dir/${bkname}-*>) {
                next if $fn eq $task->{tarfile};
-               if ($fn =~ m!/${bkname}-(\d{4})_(\d{2})_(\d{2})-(\d{2})_(\d{2})_(\d{2})\.(tgz|tar)$!) {
-                   my $t = timelocal ($6, $5, $4, $3, $2 - 1, $1 - 1900);
+               if ($fn =~ m!/(${bkname}-(\d{4})_(\d{2})_(\d{2})-(\d{2})_(\d{2})_(\d{2})\.(tgz|tar))$!) {
+                   $fn = "$dir/$1"; # untaint
+                   my $t = timelocal ($7, $6, $5, $4, $3 - 1, $2 - 1900);
                    push @bklist, [$fn, $t];
                }
            }
@@ -1075,12 +954,14 @@ sub exec_backup_task {
 }
 
 sub exec_backup {
-    my ($self) = @_;
+    my ($rpcenv, $authuser, $self) = @_;
 
     my $opts = $self->{opts};
 
     debugmsg ('info', "starting new backup job: $self->{cmdline}", undef, 1);
-
+    debugmsg ('info', "skip external VMs: " . join(', ', @{$self->{skiplist}}))
+       if scalar(@{$self->{skiplist}});
     my $tasklist = [];
 
     if ($opts->{all}) {
@@ -1088,6 +969,7 @@ sub exec_backup {
            my $vmlist = $plugin->vmlist();
            foreach my $vmid (sort @$vmlist) {
                next if grep { $_ eq  $vmid } @{$opts->{exclude}};
+               next if !$rpcenv->check($authuser, "/vms/$vmid", [ 'VM.Backup' ], 1);
                push @$tasklist, { vmid => $vmid,  state => 'todo', plugin => $plugin };
            }
        }
@@ -1101,6 +983,7 @@ sub exec_backup {
                    last;
                }
            }
+           $rpcenv->check($authuser, "/vms/$vmid", [ 'VM.Backup' ]);
            push @$tasklist, { vmid => $vmid,  state => 'todo', plugin => $plugin };
        }
     }
@@ -1128,7 +1011,7 @@ sub exec_backup {
        if ($errcount) {
            debugmsg ('info', "Backup job finished with errors", undef, 1);
        } else {
-           debugmsg ('info', "Backup job finished successfuly", undef, 1);
+           debugmsg ('info', "Backup job finished successfully", undef, 1);
        }
     }
 
@@ -1136,6 +1019,183 @@ sub exec_backup {
 
     eval { $self->$sendmail ($tasklist, $totaltime); };
     debugmsg ('err', $@) if $@;
+
+    die $err if $err;
+
+    die "job errors\n" if $errcount; 
+}
+
+my $confdesc = {
+    vmid => {
+       type => 'string', format => 'pve-vmid-list',            
+       description => "The ID of the VM you want to backup.",
+       optional => 1,
+    },
+    node => get_standard_option('pve-node', { 
+       description => "Only run if executed on this node.",
+       optional => 1,
+    }),
+    all => {
+       type => 'boolean',
+       description => "Backup all known VMs on this host.",
+       optional => 1,
+       default => 0,
+    },
+    stdexcludes => {
+       type => 'boolean',
+       description => "Exclude temorary files and logs.",
+       optional => 1,
+       default => 1,
+    },
+    compress => {
+       type => 'boolean',
+       description => "Compress dump file (gzip).",
+       optional => 1,
+       default => 0,
+    },
+    quiet => {
+       type => 'boolean',
+       description => "Be quiet.",
+       optional => 1,
+       default => 0,
+    },
+    mode => {
+       type => 'string',
+       description => "Backup mode.",
+       optional => 1,
+       default => 'stop',
+       enum => [ 'snapshot', 'suspend', 'stop' ],
+    },
+    exclude => {
+       type => 'string', format => 'pve-vmid-list',
+       description => "exclude specified VMs (assumes --all)",
+       optional => 1,
+    },
+    'exclude-path' => {
+       type => 'string', format => 'string-alist',
+       description => "exclude certain files/directories (regex).",
+       optional => 1,
+    },
+    mailto => {
+       type => 'string', format => 'string-list',
+       description => "",
+       optional => 1,
+    },
+    tmpdir => {
+       type => 'string',
+       description => "Store temporary files to specified directory.",
+       optional => 1,
+    },
+    dumpdir => {
+       type => 'string',
+       description => "Store resulting files to specified directory.",
+       optional => 1,
+    },
+    script => {
+       type => 'string',
+       description => "Use specified hook script.",
+       optional => 1,
+    },
+    storage => get_standard_option('pve-storage-id', {
+       description => "Store resulting file to this storage.",
+       optional => 1,
+    }),
+    size => {
+       type => 'integer',
+       description => "LVM snapshot size im MB.",
+       optional => 1,
+       minimum => 500,
+    },
+    bwlimit => {
+       type => 'integer',
+       description => "Limit I/O bandwidth (KBytes per second).",
+       optional => 1,
+       minimum => 0,
+    },
+    ionice => {
+       type => 'integer',
+       description => "Set CFQ ionice priority.",
+       optional => 1,
+       minimum => 0,
+       maximum => 8,
+    },
+    lockwait => {
+       type => 'integer',
+       description => "Maximal time to wait for the global lock (minutes).",
+       optional => 1,
+       minimum => 0,
+    },
+    stopwait => {
+       type => 'integer',
+       description => "Maximal time to wait until a VM is stopped (minutes).",
+       optional => 1,
+       minimum => 0,
+    },
+    maxfiles => {
+       type => 'integer',
+       description => "Maximal number of backup files per VM.",
+       optional => 1,
+       minimum => 1,
+    },
+};
+
+sub option_exists {
+    my $key = shift;
+    return defined($confdesc->{$key});
+}
+
+# add JSON properties for create and set function
+sub json_config_properties {
+    my $prop = shift;
+
+    foreach my $opt (keys %$confdesc) {
+       $prop->{$opt} = $confdesc->{$opt};
+    }
+
+    return $prop;
+}
+
+sub verify_vzdump_parameters {
+    my ($param, $check_missing) = @_;
+
+    raise_param_exc({ all => "option conflicts with option 'vmid'"})
+       if $param->{all} && $param->{vmid};
+
+    raise_param_exc({ exclude => "option conflicts with option 'vmid'"})
+       if $param->{exclude} && $param->{vmid};
+
+    $param->{all} = 1 if defined($param->{exclude});
+
+    return if !$check_missing;
+
+    raise_param_exc({ vmid => "property is missing"})
+       if !$param->{all} && !$param->{vmid};
+
+}
+
+sub command_line {
+    my ($param) = @_;
+
+    my $cmd = "vzdump";
+
+    if ($param->{vmid}) {
+       $cmd .= " " . join(' ', PVE::Tools::split_list($param->{vmid}));
+    }
+
+    foreach my $p (keys %$param) {
+       next if $p eq 'id' || $p eq 'vmid' || $p eq 'starttime' || $p eq 'dow';
+       my $v = $param->{$p};
+       my $pd = $confdesc->{$p} || die "no such vzdump option '$p'\n";
+       if ($p eq 'exclude-path') {
+           foreach my $path (split(/\0/, $v || '')) {
+               $cmd .= " --$p " . PVE::Tools::shellquote($path);
+           }
+       } else {
+           $cmd .= " --$p " . PVE::Tools::shellquote($v) if defined($v) && $v ne '';
+       }
+    }
+
+    return $cmd;
 }
 
 1;