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);
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"
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),
};
}
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;
}
$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 {
}
}
-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) = @_;
my $mailto = $opts->{mailto};
- return if !$mailto;
+ return if !($mailto && scalar(@$mailto));
my $cmdline = $self->{cmdline};
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 = '';
# end html part
print MAIL "\n--$boundary--\n";
+ close(MAIL);
};
sub new {
- my ($class, $cmdline, $opts) = @_;
+ my ($class, $cmdline, $opts, $skiplist) = @_;
mkpath $logdir;
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');
}
}
- $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);
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;
sub get_lvm_device {
my ($dir, $mapping) = @_;
- my $info = get_mount_info ($dir);
+ my $info = get_mount_info($dir);
return undef if !$info;
$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";
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;
}
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];
}
}
}
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}) {
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 };
}
}
last;
}
}
+ $rpcenv->check($authuser, "/vms/$vmid", [ 'VM.Backup' ]);
push @$tasklist, { vmid => $vmid, state => 'todo', plugin => $plugin };
}
}
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);
}
}
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;