Commit df9b9fc9 authored by Dietmar Maurer's avatar Dietmar Maurer

moved vzdump sources to this packages

We want to share code and use new PVE 2 framework features. So it is no longer possible to distribute this a separate packages.
parent 0c3e3ca3
include ../defines.mk
SUBDIRS=API2
SUBDIRS=API2 VZDump
PERLSOURCE = \
API2.pm \
......@@ -8,6 +8,7 @@ PERLSOURCE = \
APIDaemon.pm \
REST.pm \
OpenVZ.pm \
VZDump.pm \
APLInfo.pm
all: pvecfg.pm ${SUBDIRS}
......
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 IO::File;
use IO::Select;
use IPC::Open3;
use POSIX qw(strftime);
use File::Path;
use PVE::VZDump::OpenVZ;
use Time::localtime;
use Time::Local;
my @posix_filesystems = qw(ext3 ext4 nfs nfs4 reiserfs xfs);
my $lockfile = '/var/run/vzdump.lock';
my $logdir = '/var/log/vzdump';
my @plugins = qw (PVE::VZDump::OpenVZ);
# Load available plugins
my $pveplug = "/usr/share/perl5/PVE/VZDump/QemuServer.pm";
if (-f $pveplug) {
eval { require $pveplug; };
if (!$@) {
PVE::VZDump::QemuServer->import ();
push @plugins, "PVE::VZDump::QemuServer";
} else {
warn $@;
}
}
# helper functions
my $debugstattxt = {
err => 'ERROR:',
info => 'INFO:',
warn => 'WARN:',
};
sub debugmsg {
my ($mtype, $msg, $logfd, $syslog) = @_;
chomp $msg;
return if !$msg;
my $pre = $debugstattxt->{$mtype} || $debugstattxt->{'err'};
my $timestr = strftime ("%b %d %H:%M:%S", CORE::localtime);
syslog ($mtype eq 'info' ? 'info' : 'err', "$pre $msg") if $syslog;
foreach my $line (split (/\n/, $msg)) {
print STDERR "$pre $line\n";
print $logfd "$timestr $pre $line\n" if $logfd;
}
}
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 $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;
}
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 $type = $scfg->{type};
die "can't use storage type '$type' for backup\n"
if (!($type eq 'dir' || $type eq 'nfs'));
die "can't use storage for backups - wrong content type\n"
if (!$scfg->{content}->{backup});
PVE::Storage::activate_storage ($cfg, $storage);
return {
dumpdir => $scfg->{path},
};
}
sub format_size {
my $size = shift;
my $kb = $size / 1024;
if ($kb < 1024) {
return int ($kb) . "KB";
}
my $mb = $size / (1024*1024);
if ($mb < 1024) {
return int ($mb) . "MB";
} else {
my $gb = $mb / 1024;
return sprintf ("%.2fGB", $gb);
}
}
sub format_time {
my $seconds = shift;
my $hours = int ($seconds/3600);
$seconds = $seconds - $hours*3600;
my $min = int ($seconds/60);
$seconds = $seconds - $min*60;
return sprintf ("%02d:%02d:%02d", $hours, $min, $seconds);
}
sub encode8bit {
my ($str) = @_;
$str =~ s/^(.{990})/$1\n/mg; # reduce line length
return $str;
}
sub escape_html {
my ($str) = @_;
$str =~ s/&/&amp;/g;
$str =~ s/</&lt;/g;
$str =~ s/>/&gt;/g;
return $str;
}
sub check_bin {
my ($bin) = @_;
foreach my $p (split (/:/, $ENV{PATH})) {
my $fn = "$p/$bin";
if (-x $fn) {
return $fn;
}
}
die "unable to find command '$bin'\n";
}
sub check_vmids {
my (@vmids) = @_;
my $res = [];
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;
push @$res, $vmid;
}
return $res;
}
sub read_vzdump_defaults {
my $fn = "/etc/vzdump.conf";
my $res = {
bwlimit => 0,
ionice => 7,
size => 1024,
lockwait => 3*60, # 3 hours
stopwait => 10, # 10 minutes
mode => 'snapshot',
maxfiles => 1,
};
my $fh = IO::File->new ("<$fn");
return $res if !$fh;
my $line;
while (defined ($line = <$fh>)) {
next if $line =~ m/^\s*$/;
next if $line =~ m/^\#/;
if ($line =~ m/tmpdir:\s*(.*\S)\s*$/) {
$res->{tmpdir} = $1;
} elsif ($line =~ m/dumpdir:\s*(.*\S)\s*$/) {
$res->{dumpdir} = $1;
} elsif ($line =~ m/storage:\s*(\S+)\s*$/) {
$res->{storage} = $1;
} elsif ($line =~ m/script:\s*(.*\S)\s*$/) {
$res->{script} = $1;
} elsif ($line =~ m/bwlimit:\s*(\d+)\s*$/) {
$res->{bwlimit} = int($1);
} elsif ($line =~ m/ionice:\s*([0-8])\s*$/) {
$res->{ionice} = int($1);
} elsif ($line =~ m/lockwait:\s*(\d+)\s*$/) {
$res->{lockwait} = int($1);
} elsif ($line =~ m/stopwait:\s*(\d+)\s*$/) {
$res->{stopwait} = int($1);
} elsif ($line =~ m/size:\s*(\d+)\s*$/) {
$res->{size} = int($1);
} elsif ($line =~ m/maxfiles:\s*(\d+)\s*$/) {
$res->{maxfiles} = int($1);
} elsif ($line =~ m/mode:\s*(stop|snapshot|suspend)\s*$/) {
$res->{mode} = $1;
} else {
debugmsg ('warn', "unable to parse configuration file '$fn' - error at line " . $., undef, 1);
}
}
close ($fh);
return $res;
}
sub find_add_exclude {
my ($self, $excltype, $value) = @_;
if (($excltype eq '-regex') || ($excltype eq '-files')) {
$value = "\.$value";
}
if ($excltype eq '-files') {
push @{$self->{findexcl}}, "'('", '-not', '-type', 'd', '-regex' , "'$value'", "')'", '-o';
} else {
push @{$self->{findexcl}}, "'('", $excltype , "'$value'", '-prune', "')'", '-o';
}
}
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 $opts = $self->{opts};
my $mailto = $opts->{mailto};
return if !$mailto;
my $cmdline = $self->{cmdline};
my $ecount = 0;
foreach my $task (@$tasklist) {
$ecount++ if $task->{state} ne 'ok';
chomp $task->{msg} if $task->{msg};
$task->{backuptime} = 0 if !$task->{backuptime};
$task->{size} = 0 if !$task->{size};
$task->{tarfile} = 'unknown' if !$task->{tarfile};
$task->{hostname} = "VM $task->{vmid}" if !$task->{hostname};
if ($task->{state} eq 'todo') {
$task->{msg} = 'aborted';
}
}
my $stat = $ecount ? 'backup failed' : 'backup successful';
my $hostname = `hostname -f` || hostname();
chomp $hostname;
my $boundary = "----_=_NextPart_001_".int(time).$$;
my $rcvrarg = '';
foreach my $r (@$mailto) {
$rcvrarg .= " '$r'";
}
open (MAIL,"|sendmail -B 8BITMIME $rcvrarg") ||
die "unable to open 'sendmail' - $!";
my $rcvrtxt = join (', ', @$mailto);
print MAIL "Content-Type: multipart/alternative;\n";
print MAIL "\tboundary=\"$boundary\"\n";
print MAIL "FROM: vzdump backup tool <root>\n";
print MAIL "TO: $rcvrtxt\n";
print MAIL "SUBJECT: vzdump backup status ($hostname) : $stat\n";
print MAIL "\n";
print MAIL "This is a multi-part message in MIME format.\n\n";
print MAIL "--$boundary\n";
print MAIL "Content-Type: text/plain;\n";
print MAIL "\tcharset=\"UTF8\"\n";
print MAIL "Content-Transfer-Encoding: 8bit\n";
print MAIL "\n";
# text part
my $fill = ' '; # Avoid The Remove Extra Line Breaks Issue (MS Outlook)
print MAIL sprintf ("${fill}%-10s %-6s %10s %10s %s\n", qw(VMID STATUS TIME SIZE FILENAME));
foreach my $task (@$tasklist) {
my $vmid = $task->{vmid};
if ($task->{state} eq 'ok') {
print MAIL sprintf ("${fill}%-10s %-6s %10s %10s %s\n", $vmid,
$task->{state},
format_time($task->{backuptime}),
format_size ($task->{size}),
$task->{tarfile});
} else {
print MAIL sprintf ("${fill}%-10s %-6s %10s %8.2fMB %s\n", $vmid,
$task->{state},
format_time($task->{backuptime}),
0, '-');
}
}
print MAIL "${fill}\n";
print MAIL "${fill}Detailed backup logs:\n";
print MAIL "${fill}\n";
print MAIL "$fill$cmdline\n";
print MAIL "${fill}\n";
foreach my $task (@$tasklist) {
my $vmid = $task->{vmid};
my $log = $task->{tmplog};
if (!$log) {
print MAIL "${fill}$vmid: no log available\n\n";
next;
}
open (TMP, "$log");
while (my $line = <TMP>) { print MAIL encode8bit ("${fill}$vmid: $line"); }
close (TMP);
print MAIL "${fill}\n";
}
# end text part
print MAIL "\n--$boundary\n";
print MAIL "Content-Type: text/html;\n";
print MAIL "\tcharset=\"UTF8\"\n";
print MAIL "Content-Transfer-Encoding: 8bit\n";
print MAIL "\n";
# html part
print MAIL "<html><body>\n";
print MAIL "<table border=1 cellpadding=3>\n";
print MAIL "<tr><td>VMID<td>NAME<td>STATUS<td>TIME<td>SIZE<td>FILENAME</tr>\n";
my $ssize = 0;
foreach my $task (@$tasklist) {
my $vmid = $task->{vmid};
my $name = $task->{hostname};
if ($task->{state} eq 'ok') {
$ssize += $task->{size};
print MAIL sprintf ("<tr><td>%s<td>%s<td>OK<td>%s<td align=right>%s<td>%s</tr>\n",
$vmid, $name,
format_time($task->{backuptime}),
format_size ($task->{size}),
escape_html ($task->{tarfile}));
} else {
print MAIL sprintf ("<tr><td>%s<td>%s<td><font color=red>FAILED<td>%s<td colspan=2>%s</tr>\n",
$vmid, $name, format_time($task->{backuptime}),
escape_html ($task->{msg}));
}
}
print MAIL sprintf ("<tr><td align=left colspan=3>TOTAL<td>%s<td>%s<td></tr>",
format_time ($totaltime), format_size ($ssize));
print MAIL "</table><br><br>\n";
print MAIL "Detailed backup logs:<br>\n";
print MAIL "<br>\n";
print MAIL "<pre>\n";
print MAIL escape_html($cmdline) . "\n";
print MAIL "\n";
foreach my $task (@$tasklist) {
my $vmid = $task->{vmid};
my $log = $task->{tmplog};
if (!$log) {
print MAIL "$vmid: no log available\n\n";
next;
}
open (TMP, "$log");
while (my $line = <TMP>) {
if ($line =~ m/^\S+\s\d+\s+\d+:\d+:\d+\s+(ERROR|WARN):/) {
print MAIL encode8bit ("$vmid: <font color=red>".
escape_html ($line) . "</font>");
} else {
print MAIL encode8bit ("$vmid: " . escape_html ($line));
}
}
close (TMP);
print MAIL "\n";
}
print MAIL "</pre>\n";
print MAIL "</body></html>\n";
# end html part
print MAIL "\n--$boundary--\n";
};
sub new {
my ($class, $cmdline, $opts) = @_;
mkpath $logdir;
check_bin ('cp');
check_bin ('df');
check_bin ('sendmail');
check_bin ('rsync');
check_bin ('tar');
check_bin ('mount');
check_bin ('umount');
check_bin ('cstream');
check_bin ('ionice');
if ($opts->{snapshot}) {
check_bin ('lvcreate');
check_bin ('lvs');
check_bin ('lvremove');
}
my $defaults = read_vzdump_defaults();
foreach my $k (keys %$defaults) {
if ($k eq 'dumpdir' || $k eq 'storage') {
$opts->{$k} = $defaults->{$k} if !defined ($opts->{dumpdir}) &&
!defined ($opts->{storage});
} else {
$opts->{$k} = $defaults->{$k} if !defined ($opts->{$k});
}
}
$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 };
#always skip '.'
push @{$self->{findexcl}}, "'('", '-regex' , "'^\\.\$'", "')'", '-o';
$self->find_add_exclude ('-type', 's'); # skip sockets
if ($opts->{'exclude-path'}) {
foreach my $path (@{$opts->{'exclude-path'}}) {
$self->find_add_exclude ('-regex', $path);
}
}
if ($opts->{stdexcludes}) {
$self->find_add_exclude ('-files', '/var/log/.+');
$self->find_add_exclude ('-regex', '/tmp/.+');
$self->find_add_exclude ('-regex', '/var/tmp/.+');
$self->find_add_exclude ('-regex', '/var/run/.+pid');
}
foreach my $p (@plugins) {
my $pd = $p->new ($self);
push @{$self->{plugins}}, $pd;
if (!$opts->{dumpdir} && !$opts->{storage} &&
($p eq 'PVE::VZDump::OpenVZ')) {
$opts->{dumpdir} = $pd->{dumpdir};
}
}
if (!$opts->{dumpdir} && !$opts->{storage}) {
die "no dumpdir/storage specified - use option '--dumpdir' or option '--storage'\n";
}
if ($opts->{storage}) {
my $info = storage_info ($opts->{storage});
$opts->{dumpdir} = $info->{dumpdir};
} elsif ($opts->{dumpdir}) {
die "dumpdir '$opts->{dumpdir}' does not exist\n"
if ! -d $opts->{dumpdir};
} else {
die "internal error";
}
if ($opts->{tmpdir} && ! -d $opts->{tmpdir}) {
die "tmpdir '$opts->{tmpdir}' does not exist\n";
}
return $self;
}
sub get_lvm_mapping {
my $devmapper;
my $cmd = "lvs --units m --separator ':' --noheadings -o vg_name,lv_name,lv_size";
if (my $fd = IO::File->new ("$cmd 2>/dev/null|")) {
while (my $line = <$fd>) {
if ($line =~ m|^\s*(\S+):(\S+):(\d+(\.\d+))[Mm]$|) {
my $vg = $1;
my $lv = $2;
$devmapper->{"/dev/$vg/$lv"} = [$vg, $lv];
my $qlv = $lv;
$qlv =~ s/-/--/g;
my $qvg = $vg;
$qvg =~ s/-/--/g;
$devmapper->{"/dev/mapper/$qvg-$qlv"} = [$vg, $lv];
}
}
close ($fd);
}
return $devmapper;
}
sub get_mount_info {
my ($dir) = @_;
my $out;
if (my $fd = IO::File->new ("df -P -T '$dir' 2>/dev/null|")) {
<$fd>; #skip first line
$out = <$fd>;
close ($fd);
}
return undef if !$out;
my @res = split (/\s+/, $out);
return undef if scalar (@res) != 7;
return {
device => $res[0],
fstype => $res[1],
mountpoint => $res[6]
};
}
sub get_lvm_device {
my ($dir, $mapping) = @_;
my $info = get_mount_info ($dir);
return undef if !$info;
my $dev = $info->{device};
my ($vg, $lv);
($vg, $lv) = @{$mapping->{$dev}} if defined $mapping->{$dev};
return wantarray ? ($dev, $info->{mountpoint}, $vg, $lv, $info->{fstype}) : $dev;
}
sub getlock {
my ($self) = @_;
my $maxwait = $self->{opts}->{lockwait} || $self->{lockwait};
if (!open (SERVER_FLCK, ">>$lockfile")) {
debugmsg ('err', "can't open lock on file '$lockfile' - $!", undef, 1);
exit (-1);
}
if (flock (SERVER_FLCK, LOCK_EX|LOCK_NB)) {
return;
}
if (!$maxwait) {
debugmsg ('err', "can't aquire lock '$lockfile' (wait = 0)", undef, 1);
exit (-1);
}
debugmsg('info', "trying to get global lock - waiting...", undef, 1);
eval {
alarm ($maxwait * 60);
local $SIG{ALRM} = sub { alarm (0); die "got timeout\n"; };
if (!flock (SERVER_FLCK, LOCK_EX)) {
my $err = $!;
close (SERVER_FLCK);
alarm (0);
die "$err\n";
}
alarm (0);
};
alarm (0);
my $err = $@;
if ($err) {
debugmsg ('err', "can't aquire lock '$lockfile' - $err", undef, 1);
exit (-1);
}
debugmsg('info', "got global lock", undef, 1);
}
sub run_hook_script {
my ($self, $phase, $task, $logfd) = @_;
my $opts = $self->{opts};
my $script = $opts->{script};
return if !$script;
my $cmd = "$script $phase";
$cmd .= " $task->{mode} $task->{vmid}" if ($task);
local %ENV;
foreach my $ek (qw(vmtype dumpdir hostname tarfile logfile)) {
$ENV{uc($ek)} = $task->{$ek} if $task->{$ek};
}
run_command ($logfd, $cmd);
}
sub exec_backup_task {
my ($self, $task) = @_;
my $opts = $self->{opts};
my $vmid = $task->{vmid};
my $plugin = $task->{plugin};
my $vmstarttime = time ();
my $logfd;
my $cleanup = {};
my $vmstoptime = 0;
eval {
die "unable to find VM '$vmid'\n" if !$plugin;
my $vmtype = $plugin->type();
my $tmplog = "$logdir/$vmtype-$vmid.log";
my $lt = localtime();
my $bkname = "vzdump-$vmtype-$vmid";
my $basename = sprintf "${bkname}-%04d_%02d_%02d-%02d_%02d_%02d",
$lt->year + 1900, $lt->mon + 1, $lt->mday,
$lt->hour, $lt->min, $lt->sec;
my $logfile = $task->{logfile} = "$opts->{dumpdir}/$basename.log";
my $ext = $opts->{compress} ? '.tgz' : '.tar';
if ($opts->{stdout}) {
$task->{tarfile} = '-';
} else {
my $tarfile = $task->{tarfile} = "$opts->{dumpdir}/$basename$ext";
$task->{tmptar} = $task->{tarfile};
$task->{tmptar} =~ s/\.[^\.]+$/\.dat/;
unlink $task->{tmptar};
}
$task->{vmtype} = $vmtype;
if ($opts->{tmpdir}) {
$task->{tmpdir} = "$opts->{tmpdir}/vzdumptmp$$";
} else {
# dumpdir is posix? then use it as temporary dir
my $info = get_mount_info ($opts->{dumpdir});
if ($vmtype eq 'qemu' ||
grep ($_ eq $info->{fstype}, @posix_filesystems)) {
$task->{tmpdir} = "$opts->{dumpdir}/$basename.tmp";
} else {
$task->{tmpdir} = "/var/tmp/vzdumptmp$$";
debugmsg ('info', "filesystem type on dumpdir is '$info->{fstype}' -" .
"using $task->{tmpdir} for temporary files", $logfd);
}
}
rmtree $task->{tmpdir};
mkdir $task->{tmpdir};
-d $task->{tmpdir} ||
die "unable to create temporary directory '$task->{tmpdir}'";
$logfd = IO::File->new (">$tmplog") ||
die "unable to create log file '$tmplog'";
$task->{dumpdir} = $opts->{dumpdir};
$task->{tmplog} = $tmplog;
unlink $logfile;
debugmsg ('info', "Starting Backup of VM $vmid ($vmtype)", $logfd, 1);
$plugin->set_logfd ($logfd);
# test is VM is running
my ($running, $status_text) = $plugin->vm_status ($vmid);
debugmsg ('info', "status = ${status_text}", $logfd);
# lock VM (prevent config changes)
$plugin->lock_vm ($vmid);
$cleanup->{unlock} = 1;
# prepare
my $mode = $running ? $opts->{mode} : 'stop';
if ($mode eq 'snapshot') {
my %saved_task = %$task;
eval { $plugin->prepare ($task, $vmid, $mode); };
if (my $err = $@) {
die $err if $err !~ m/^mode failure/;
debugmsg ('info', $err, $logfd);
debugmsg ('info', "trying 'suspend' mode instead", $logfd);
$mode = 'suspend'; # so prepare is called again below
%$task = %saved_task;
}
}
$task->{mode} = $mode;
debugmsg ('info', "backup mode: $mode", $logfd);
debugmsg ('info', "bandwidth limit: $opts->{bwlimit} KB/s", $logfd)
if $opts->{bwlimit};
debugmsg ('info', "ionice priority: $opts->{ionice}", $logfd);
if ($mode eq 'stop') {
$plugin->prepare ($task, $vmid, $mode);
$self->run_hook_script ('backup-start', $task, $logfd);
if ($running) {
debugmsg ('info', "stopping vm", $logfd);
$vmstoptime = time ();
$self->run_hook_script ('pre-stop', $task, $logfd);
$plugin->stop_vm ($task, $vmid);
$cleanup->{restart} = 1;
}
} elsif ($mode eq 'suspend') {
$plugin->prepare ($task, $vmid, $mode);
$self->run_hook_script ('backup-start', $task, $logfd);
if ($vmtype eq 'openvz') {
# pre-suspend rsync
$plugin->copy_data_phase1 ($task, $vmid);
}
debugmsg ('info', "suspend vm", $logfd);
$vmstoptime = time ();
$self->run_hook_script ('pre-stop', $task, $logfd);
$plugin->suspend_vm ($task, $vmid);
$cleanup->{resume} = 1;
if ($vmtype eq 'openvz') {
# post-suspend rsync
$plugin->copy_data_phase2 ($task, $vmid);
debugmsg ('info', "resume vm", $logfd);
$cleanup->{resume} = 0;
$self->run_hook_script ('pre-restart', $task, $logfd);
$plugin->resume_vm ($task, $vmid);
my $delay = time () - $vmstoptime;
debugmsg ('info', "vm is online again after $delay seconds", $logfd);
}
} elsif ($mode eq 'snapshot') {
my $snapshot_count = $task->{snapshot_count} || 0;
$self->run_hook_script ('pre-stop', $task, $logfd);
if ($snapshot_count > 1) {
debugmsg ('info', "suspend vm to make snapshot", $logfd);
$vmstoptime = time ();
$plugin->suspend_vm ($task, $vmid);
$cleanup->{resume} = 1;
}
$plugin->snapshot ($task, $vmid);
$self->run_hook_script ('pre-restart', $task, $logfd);
if ($snapshot_count > 1) {
debugmsg ('info', "resume vm", $logfd);
$cleanup->{resume} = 0;
$plugin->resume_vm ($task, $vmid);
my $delay = time () - $vmstoptime;
debugmsg ('info', "vm is online again after $delay seconds", $logfd);
}
} else {
die "internal error - unknown mode '$mode'\n";
}
# assemble archive image
$plugin->assemble ($task, $vmid);
# produce archive
if ($opts->{stdout}) {
debugmsg ('info', "sending archive to stdout", $logfd);
$plugin->archive ($task, $vmid, $task->{tmptar});
$self->run_hook_script ('backup-end', $task, $logfd);
return;
}
debugmsg ('info', "creating archive '$task->{tarfile}'", $logfd);
$plugin->archive ($task, $vmid, $task->{tmptar});
rename ($task->{tmptar}, $task->{tarfile}) ||
die "unable to rename '$task->{tmptar}' to '$task->{tarfile}'\n";
# determine size
$task->{size} = (-s $task->{tarfile}) || 0;
my $cs = format_size ($task->{size});
debugmsg ('info', "archive file size: $cs", $logfd);
# purge older backup
my $maxfiles = $opts->{maxfiles};
if ($maxfiles) {
my @bklist = ();
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);
push @bklist, [$fn, $t];
}
}
@bklist = sort { $b->[1] <=> $a->[1] } @bklist;
my $ind = scalar (@bklist);
while (scalar (@bklist) >= $maxfiles) {
my $d = pop @bklist;
debugmsg ('info', "delete old backup '$d->[0]'", $logfd);
unlink $d->[0];
my $logfn = $d->[0];
$logfn =~ s/\.(tgz|tar)$/\.log/;
unlink $logfn;
}
}
$self->run_hook_script ('backup-end', $task, $logfd);
};
my $err = $@;
if ($plugin) {
# clean-up
if ($cleanup->{unlock}) {
eval { $plugin->unlock_vm ($vmid); };
warn $@ if $@;
}
eval { $plugin->cleanup ($task, $vmid) };
warn $@ if $@;
eval { $plugin->set_logfd (undef); };
warn $@ if $@;
if ($cleanup->{resume} || $cleanup->{restart}) {
eval {
$self->run_hook_script ('pre-restart', $task, $logfd);
if ($cleanup->{resume}) {
debugmsg ('info', "resume vm", $logfd);
$plugin->resume_vm ($task, $vmid);
} else {
debugmsg ('info', "restarting vm", $logfd);
$plugin->start_vm ($task, $vmid);
}
};
my $err = $@;
if ($err) {
warn $err;
} else {
my $delay = time () - $vmstoptime;
debugmsg ('info', "vm is online again after $delay seconds", $logfd);
}
}
}
eval { unlink $task->{tmptar} if $task->{tmptar} && -f $task->{tmptar}; };
warn $@ if $@;
eval { rmtree $task->{tmpdir} if $task->{tmpdir} && -d $task->{tmpdir}; };
warn $@ if $@;
my $delay = $task->{backuptime} = time () - $vmstarttime;
if ($err) {
$task->{state} = 'err';
$task->{msg} = $err;
debugmsg ('err', "Backup of VM $vmid failed - $err", $logfd, 1);
eval { $self->run_hook_script ('backup-abort', $task, $logfd); };
} else {
$task->{state} = 'ok';
my $tstr = format_time ($delay);
debugmsg ('info', "Finished Backup of VM $vmid ($tstr)", $logfd, 1);
}
close ($logfd) if $logfd;
if ($task->{tmplog} && $task->{logfile}) {
system ("cp '$task->{tmplog}' '$task->{logfile}'");
}
eval { $self->run_hook_script ('log-end', $task); };
die $err if $err && $err =~ m/^interrupted by signal$/;
}
sub exec_backup {
my ($self) = @_;
my $opts = $self->{opts};
debugmsg ('info', "starting new backup job: $self->{cmdline}", undef, 1);
my $tasklist = [];
if ($opts->{all}) {
foreach my $plugin (@{$self->{plugins}}) {
my $vmlist = $plugin->vmlist();
foreach my $vmid (sort @$vmlist) {
next if grep { $_ eq $vmid } @{$opts->{exclude}};
push @$tasklist, { vmid => $vmid, state => 'todo', plugin => $plugin };
}
}
} else {
foreach my $vmid (sort @{$opts->{vmids}}) {
my $plugin;
foreach my $pg (@{$self->{plugins}}) {
my $vmlist = $pg->vmlist();
if (grep { $_ eq $vmid } @$vmlist) {
$plugin = $pg;
last;
}
}
push @$tasklist, { vmid => $vmid, state => 'todo', plugin => $plugin };
}
}
my $starttime = time();
my $errcount = 0;
eval {
$self->run_hook_script ('job-start');
foreach my $task (@$tasklist) {
$self->exec_backup_task ($task);
$errcount += 1 if $task->{state} ne 'ok';
}
$self->run_hook_script ('job-end');
};
my $err = $@;
$self->run_hook_script ('job-abort') if $err;
if ($err) {
debugmsg ('err', "Backup job failed - $err", undef, 1);
} else {
if ($errcount) {
debugmsg ('info', "Backup job finished with errors", undef, 1);
} else {
debugmsg ('info', "Backup job finished successfuly", undef, 1);
}
}
my $totaltime = time() - $starttime;
eval { $self->$sendmail ($tasklist, $totaltime); };
debugmsg ('err', $@) if $@;
}
1;
include ../../defines.mk
PERLSOURCE = \
OpenVZ.pm \
Plugin.pm
all:
.PHONY: distclean
distclean: clean
.PHONY: clean
clean:
rm -rf *~
.PHONY: install
install: ${PERLSOURCE}
install -d ${PERLLIBDIR}/PVE/VZDump
install -m 0644 ${PERLSOURCE} ${PERLLIBDIR}/PVE/VZDump
package PVE::VZDump::OpenVZ;
# 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 File::Path;
use File::Basename;
use PVE::VZDump;
use Sys::Hostname;
use LockFile::Simple;
use base qw (PVE::VZDump::Plugin);
use constant SCRIPT_EXT => qw (start stop mount umount);
use constant VZDIR => '/etc/vz';
my $remove_quotes = sub {
my $str = shift;
$str =~ s/^\s*\"?//;
$str =~ s/\"?\s*$//;
return $str;
};
# read global vz.conf
sub read_global_vz_config {
local $/;
my $res = {
rootdir => '/vz/root/$VEID', # note '$VEID' is a place holder
privatedir => '/vz/private/$VEID', # note '$VEID' is a place holder
dumpdir => '/vz/dump',
lockdir => '/var/lib/vz/lock',
};
my $filename = VZDIR . "/vz.conf";
my $fh = IO::File->new ($filename, "r");
return $res if !$fh;
my $data = <$fh> || '';
$fh->close();
if ($data =~ m/^\s*VE_PRIVATE=(.*)$/m) {
my $dir = &$remove_quotes ($1);
if ($dir !~ m/\$VEID/) {
warn "VE_PRIVATE does not contain '\$VEID' ('$dir')\n";
} else {
$res->{privatedir} = $dir;
}
}
if ($data =~ m/^\s*VE_ROOT=(.*)$/m) {
my $dir = &$remove_quotes ($1);
if ($dir !~ m/\$VEID/) {
warn "VE_ROOT does not contain '\$VEID' ('$dir')\n";
} else {
$res->{rootdir} = $dir;
}
}
if ($data =~ m/^\s*DUMPDIR=(.*)$/m) {
my $dir = &$remove_quotes ($1);
$dir =~ s|/\$VEID$||;
$res->{dumpdir} = $dir;
}
if ($data =~ m/^\s*LOCKDIR=(.*)$/m) {
my $dir = &$remove_quotes ($1);
$res->{lockdir} = $dir;
}
return $res;
}
my $load_vz_conf = sub {
my ($self, $vmid) = @_;
local $/;
my $conf = $self->{vmlist}->{$vmid}->{conffile};
my $fh = IO::File->new ($conf, "r") ||
die "unable to open config file '$conf'\n";
my $data = <$fh>;
$fh->close();
my $dir;
if ($data =~ m/^\s*VE_PRIVATE=(.*)$/m) {
$dir = &$remove_quotes ($1);
} else {
$dir = $self->{privatedir};
}
$dir =~ s/\$VEID/$vmid/;
$self->{vmlist}->{$vmid}->{dir} = $dir;
if ($data =~ m/^\s*HOSTNAME=(.*)/m) {
$self->{vmlist}->{$vmid}->{hostname} = &$remove_quotes ($1);
} else {
$self->{vmlist}->{$vmid}->{hostname} = "VM $vmid";
}
};
sub read_vz_list {
my $vmlist = {};
my $dir = VZDIR . "/conf";
foreach my $conf (<$dir/*.conf>) {
next if $conf !~ m|/(\d\d\d+)\.conf$|;
my $vmid = $1;
$vmlist->{$vmid}->{conffile} = $conf;
}
return $vmlist;
}
my $rsync_vm = sub {
my ($self, $task, $from, $to, $text) = @_;
$self->loginfo ("starting $text sync $from to $to");
my $starttime = time();
my $opts = $self->{vzdump}->{opts};
my $rsyncopts = "--stats -x --numeric-ids";
$rsyncopts .= " --bwlimit=$opts->{bwlimit}" if $opts->{bwlimit};
$self->cmd ("rsync $rsyncopts -aH --delete --no-whole-file --inplace '$from' '$to'");
my $delay = time () - $starttime;
$self->loginfo ("$text sync finished ($delay seconds)");
};
sub new {
my ($class, $vzdump) = @_;
PVE::VZDump::check_bin ('vzctl');
my $self = bless read_global_vz_config ();
$self->{vzdump} = $vzdump;
$self->{vmlist} = read_vz_list ();
return $self;
};
sub type {
return 'openvz';
}
sub vm_status {
my ($self, $vmid) = @_;
my $status_text = $self->cmd ("vzctl status $vmid");
chomp $status_text;
my $running = $status_text =~ m/running/ ? 1 : 0;
return wantarray ? ($running, $status_text) : $running;
}
sub prepare {
my ($self, $task, $vmid, $mode) = @_;
$self->$load_vz_conf ($vmid);
my $dir = $self->{vmlist}->{$vmid}->{dir};
my $diskinfo = { dir => $dir };
$task->{hostname} = $self->{vmlist}->{$vmid}->{hostname};
$task->{diskinfo} = $diskinfo;
my $hostname = hostname();
if ($mode eq 'snapshot') {
my $lvmmap = PVE::VZDump::get_lvm_mapping();
my ($srcdev, $lvmpath, $lvmvg, $lvmlv, $fstype) =
PVE::VZDump::get_lvm_device ($dir, $lvmmap);
my $targetdev = PVE::VZDump::get_lvm_device ($task->{dumpdir}, $lvmmap);
die ("mode failure - unable to detect lvm volume group\n") if !$lvmvg;
die ("mode failure - wrong lvm mount point '$lvmpath'\n") if $dir !~ m|/?$lvmpath/?|;
die ("mode failure - unable to dump into snapshot (use option --dumpdir)\n")
if $targetdev eq $srcdev;
$diskinfo->{snapname} = "vzsnap-$hostname-0";
$diskinfo->{snapdev} = "/dev/$lvmvg/$diskinfo->{snapname}";
$diskinfo->{srcdev} = $srcdev;
$diskinfo->{lvmvg} = $lvmvg;
$diskinfo->{lvmlv} = $lvmlv;
$diskinfo->{fstype} = $fstype;
$diskinfo->{lvmpath} = $lvmpath;
$diskinfo->{mountpoint} = "/mnt/vzsnap0";
$task->{snapdir} = $dir;
$task->{snapdir} =~ s|/?$lvmpath/?|$diskinfo->{mountpoint}/|;
} elsif ($mode eq 'suspend') {
$task->{snapdir} = $task->{tmpdir};
} else {
$task->{snapdir} = $dir;
}
}
sub lock_vm {
my ($self, $vmid) = @_;
my $filename = "$self->{lockdir}/103.lck";
my $lockmgr = LockFile::Simple->make(-format => '%f',
-autoclean => 1,
-max => 30,
-delay => 2,
-stale => 1,
-nfs => 0);
$self->{lock} = $lockmgr->lock($filename) || die "can't lock VM $vmid\n";
}
sub unlock_vm {
my ($self, $vmid) = @_;
$self->{lock}->release();
}
sub copy_data_phase1 {
my ($self, $task) = @_;
$self->$rsync_vm ($task, "$task->{diskinfo}->{dir}/", $task->{snapdir}, "first");
}
# we use --skiplock for vzctl because we have already locked the VM
# by calling lock_vm()
sub stop_vm {
my ($self, $task, $vmid) = @_;
$self->cmd ("vzctl --skiplock stop $vmid");
}
sub start_vm {
my ($self, $task, $vmid) = @_;
$self->cmd ("vzctl --skiplock start $vmid");
}
sub suspend_vm {
my ($self, $task, $vmid) = @_;
$self->cmd ("vzctl --skiplock chkpnt $vmid --suspend");
}
sub snapshot {
my ($self, $task) = @_;
my $opts = $self->{vzdump}->{opts};
my $di = $task->{diskinfo};
mkpath $di->{mountpoint}; # create mount point for lvm snapshot
if (-b $di->{snapdev}) {
$self->loginfo ("trying to remove stale snapshot '$di->{snapdev}'");
$self->cmd_noerr ("umount $di->{mountpoint}");
$self->cmd_noerr ("lvremove -f $di->{snapdev}");
}
$self->loginfo ("creating lvm snapshot of $di->{srcdev} ('$di->{snapdev}')");
$task->{cleanup}->{lvm_snapshot} = 1;
$self->cmd ("lvcreate --size $opts->{size}M --snapshot" .
" --name $di->{snapname} /dev/$di->{lvmvg}/$di->{lvmlv}");
my $mopts = $di->{fstype} eq 'xfs' ? "-o nouuid" : '';
$task->{cleanup}->{snapshot_mount} = 1;
$self->cmd ("mount -t $di->{fstype} $mopts $di->{snapdev} $di->{mountpoint}");
}
sub copy_data_phase2 {
my ($self, $task) = @_;
$self->$rsync_vm ($task, "$task->{diskinfo}->{dir}/", $task->{snapdir}, "final");
}
sub resume_vm {
my ($self, $task, $vmid) = @_;
$self->cmd ("vzctl --skiplock chkpnt $vmid --resume");
}
sub assemble {
my ($self, $task, $vmid) = @_;
my $conffile = $self->{vmlist}->{$vmid}->{conffile};
my $dir = $task->{snapdir};
$task->{cleanup}->{etc_vzdump} = 1;
mkpath "$dir/etc/vzdump/";
$self->cmd ("cp '$conffile' '$dir/etc/vzdump/vps.conf'");
my $cfgdir = dirname ($conffile);
foreach my $s (SCRIPT_EXT) {
my $fn = "$cfgdir/$vmid.$s";
$self->cmd ("cp '$fn' '$dir/etc/vzdump/vps.$s'") if -f $fn;
}
}
sub archive {
my ($self, $task, $vmid, $filename) = @_;
my $findexcl = $self->{vzdump}->{findexcl};
my $findargs = join (' ', @$findexcl) . ' -print0';
my $opts = $self->{vzdump}->{opts};
my $srcdir = $self->{vmlist}->{$vmid}->{dir};
my $snapdir = $task->{snapdir};
my $zflag = $opts->{compress} ? 'z' : '';
my $taropts = "--totals --sparse --numeric-owner --no-recursion --ignore-failed-read --one-file-system";
if ($snapdir eq $task->{tmpdir} && $snapdir =~ m|^$opts->{dumpdir}/|) {
$taropts .= " --remove-files"; # try to save space
}
my $cmd = "(";
$cmd .= "cd $snapdir;find . $findargs|sed 's/\\\\/\\\\\\\\/g'|";
$cmd .= "tar c${zflag}pf - $taropts --null -T -";
if ($opts->{bwlimit}) {
my $bwl = $opts->{bwlimit}*1024; # bandwidth limit for cstream
$cmd .= "|cstream -t $bwl";
}
$cmd .= ")";
if ($opts->{stdout}) {
$self->cmd ($cmd, output => ">&=" . fileno($opts->{stdout}));
} else {
$self->cmd ("$cmd >$filename");
}
}
sub cleanup {
my ($self, $task, $vmid) = @_;
my $di = $task->{diskinfo};
if ($task->{cleanup}->{snapshot_mount}) {
$self->cmd_noerr ("umount $di->{mountpoint}");
}
if ($task->{cleanup}->{lvm_snapshot}) {
$self->cmd_noerr ("lvremove -f $di->{snapdev}") if -b $di->{snapdev};
}
if ($task->{cleanup}->{etc_vzdump}) {
my $dir = "$task->{snapdir}/etc/vzdump";
eval { rmtree $dir if -d $dir; };
$self->logerr ($@) if $@;
}
}
1;
package PVE::VZDump::Plugin;
# 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;
sub set_logfd {
my ($self, $logfd) = @_;
$self->{logfd} = $logfd;
}
sub cmd {
my ($self, $cmdstr, %param) = @_;
return PVE::VZDump::run_command($self->{logfd}, $cmdstr, %param);
}
sub cmd_noerr {
my ($self, $cmdstr, %param) = @_;
my $res;
eval { $res = $self->cmd($cmdstr, %param); };
$self->logerr ($@) if $@;
return $res;
}
sub loginfo {
my ($self, $msg) = @_;
PVE::VZDump::debugmsg ('info', $msg, $self->{logfd}, 0);
}
sub logerr {
my ($self, $msg) = @_;
PVE::VZDump::debugmsg ('err', $msg, $self->{logfd}, 0);
}
sub type {
return 'unknown';
};
sub vmlist {
my ($self) = @_;
return [ keys %{$self->{vmlist}} ] if $self->{vmlist};
return [];
}
sub vm_status {
my ($self, $vmid) = @_;
die "internal error"; # implement in subclass
}
sub prepare {
my ($self, $task, $vmid, $mode) = @_;
die "internal error"; # implement in subclass
}
sub lock_vm {
my ($self, $vmid) = @_;
die "internal error"; # implement in subclass
}
sub unlock_vm {
my ($self, $vmid) = @_;
die "internal error"; # implement in subclass
}
sub stop_vm {
my ($self, $task, $vmid) = @_;
die "internal error"; # implement in subclass
}
sub start_vm {
my ($self, $task, $vmid) = @_;
die "internal error"; # implement in subclass
}
sub suspend_vm {
my ($self, $task, $vmid) = @_;
die "internal error"; # implement in subclass
}
sub resume_vm {
my ($self, $task, $vmid) = @_;
die "internal error"; # implement in subclass
}
sub snapshot {
my ($self, $task, $vmid) = @_;
die "internal error"; # implement in subclass
}
sub copy_data_phase2 {
my ($self, $task, $vmid) = @_;
die "internal error"; # implement in subclass
}
sub assemble {
my ($self, $task, $vmid) = @_;
die "internal error"; # implement in subclass
}
sub archive {
my ($self, $task, $vmid, $filename) = @_;
die "internal error"; # implement in subclass
}
sub cleanup {
my ($self, $task, $vmid) = @_;
die "internal error"; # implement in subclass
}
1;
......@@ -3,6 +3,8 @@ include ../defines.mk
SUBDIRS = init.d cron test
SCRIPTS = \
vzdump \
vzrestore \
pvestatd \
pvesh \
pveam \
......
#!/usr/bin/perl -w
#
# 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 Getopt::Long;
use Sys::Syslog;
use PVE::VZDump;
$ENV{LANG} = "C"; # avoid locale related issues/warnings
# by default we set --rsyncable for gzip
$ENV{GZIP} = "--rsyncable" if !$ENV{GZIP};
# just to be sure that we have a resonable path
$ENV{PATH} = "/sbin:/bin:/usr/sbin:/usr/bin:/usr/local/sbin:/usr/local/bin";
my $cmdline = join (' ', 'vzdump', @ARGV);
openlog ('vzdump', 'cons,pid', 'daemon');
$SIG{INT} = $SIG{TERM} = $SIG{QUIT} = $SIG{HUP} = $SIG{PIPE} = sub {
die "interrupted by signal\n";
};
my @std_opts = (
'all',
'exclude=s@',
'exclude-path=s@',
'stdexcludes',
'compress',
'mailto=s@',
'quiet',
'stop',
'suspend',
'snapshot',
'size=i',
'node=i',
'bwlimit=i',
'ionice=i',
'lockwait=i',
'stopwait=i',
'tmpdir=s',
'dumpdir=s',
'maxfiles=i',
'script=s',
'storage=s',
'stdout',
);
sub print_usage {
my $msg = shift;
print STDERR "ERROR: $msg\n\n" if $msg;
print STDERR "usage: $0 OPTIONS [--all | VMID]\n\n";
print STDERR "\t--exclude VMID\t\texclude VMID (assumes --all)\n";
print STDERR "\t--exclude-path REGEX\texclude certain files/directories\n"; print STDERR "\t--stdexcludes\t\texclude temorary files and logs\n\n";
print STDERR "\t--compress\t\tcompress dump file (gzip)\n";
print STDERR "\t--dumpdir DIR\t\tstore resulting files in DIR\n";
print STDERR "\t--maxfiles N\t\tmaximal number of backup files per VM\n";
print STDERR "\t--script FILENAME\texecute hook script\n";
print STDERR "\t--stdout write to stdout, not to a file\n";
print STDERR "\t--storage STORAGE_ID\tstore resulting files to STORAGE_ID (PVE only)\n";
print STDERR "\t--tmpdir DIR\t\tstore temporary files in DIR\n\n";
print STDERR "\t--mailto EMAIL\t\tsend notification mail to EMAIL.\n";
print STDERR "\t--quiet\t\t\tbe quiet.\n";
print STDERR "\t--stop\t\t\tstop/start VM if running\n";
print STDERR "\t--suspend\t\tsuspend/resume VM when running\n";
print STDERR "\t--snapshot\t\tuse LVM snapshot when running\n";
print STDERR "\t--size MB\t\tLVM snapshot size\n\n";
print STDERR "\t--node CID\t\tonly run on pve cluster node CID\n";
print STDERR "\t--lockwait MINUTES\tmaximal time to wait for the global lock\n";
print STDERR "\t--stopwait MINUTES\tmaximal time to wait until a VM is stopped\n";
print STDERR "\t--bwlimit KBPS\t\tlimit I/O bandwidth; KBytes per second\n";
print STDERR "\t--ionice PRI\t\tset ionice priority (0-8)\n\n";
print STDERR "\n";
}
my $opts = {};
if (!GetOptions ($opts, @std_opts)) {
print_usage ();
exit (-1);
}
if ($opts->{node}) {
PVE::VZDump::check_bin ('pveca');
my $info = `pveca -i`;
chomp $info;
die "unable to parse pveca info" if $info !~ m/^(\d+)\s+\S+\s+\S+\s+\S+$/;
my $cid = $1;
# silent exit if we run on wrong node
exit (0) if $cid != $opts->{node};
}
$opts->{all} = 1 if $opts->{exclude};
if ($opts->{all} && $#ARGV >= 0) {
print_usage ();
exit (-1);
}
if (!$opts->{all} && $#ARGV == -1) {
print_usage ();
exit (-1);
}
open STDOUT, '>/dev/null' if $opts->{quiet} && !$opts->{stdout};
open STDERR, '>/dev/null' if $opts->{quiet};
if ($opts->{stdout}) {
open my $saved_stdout, ">&STDOUT"
|| die "can't dup STDOUT: $!\n";
open STDOUT, '>&STDERR' ||
die "unable to redirect STDOUT: $!\n";
$opts->{stdout} = $saved_stdout;
die "you can only backup a single VM with option --stdout\n"
if scalar(@ARGV) != 1;
}
$opts->{vmids} = PVE::VZDump::check_vmids (@ARGV) if !$opts->{all};
$opts->{exclude} = PVE::VZDump::check_vmids (@{$opts->{exclude}}) if $opts->{exclude};
my $vzdump = PVE::VZDump->new ($cmdline, $opts);
$vzdump->getlock (); # only one process allowed
# parameters are OK - now start real work and log everything
eval {
if (defined($opts->{ionice})) {
if ($opts->{ionice} > 7) {
PVE::VZDump::run_command (undef, "ionice -c3 -p $$");
} else {
PVE::VZDump::run_command (undef, "ionice -c2 -n$opts->{ionice} -p $$");
}
}
$vzdump->exec_backup();
};
my $err = $@;
if ($err) {
PVE::VZDump::debugmsg ('err', $err, undef, 1);
exit (-1);
}
exit 0;
__END__
=head1 NAME
vzdump - backup utility for virtual machine
=head1 SYNOPSIS
vzdump OPTIONS [--all | <VMID>]
--exclude VMID exclude VMID (assumes --all)
--exclude-path REGEX exclude certain files/directories. You
can use this option more than once to specify
multiple exclude paths
--stdexcludes exclude temporary files and logs
--compress compress dump file (gzip)
--storage STORAGE_ID store resulting files to STORAGE_ID (PVE only)
--script execute hook script
--dumpdir DIR store resulting files in DIR
--stdout write to stdout, not to a file. You can only
backup a single VM when using this mode.
--maxfiles N maximal number of backup files per VM.
--tmpdir DIR store temporary files in DIR. --suspend and --stop
are using this directory to store a copy of the VM.
--mailto EMAIL send notification mail to EMAIL. You can use
this option more than once to specify multiple
receivers
--stop stop/start VM if running
--suspend suspend/resume VM when running
--snapshot use LVM snapshot when running
--size MB LVM snapshot size (default 1024)
--bwlimit KBPS limit I/O bandwidth; KBytes per second
--ionice PRI set ionice priority (0-8). default is 7 (lowest 'best
effort' priority). Value 8 uses the ionice
'idle' scheduling class.
--lockwait MINUTES maximal time to wait for the global
lock. vzdump uses a global lock file to make
sure that only one instance is running
(running several instance puts too much load
on a server). Default is 180 (3 hours).
--stopwait MINUTES maximal time to wait until a VM is stopped.
=head1 DESCRIPTION
vzdump is an utility to make consistent snapshots of running virtual
machines (VMs). It basically creates a tar archive of the VM private area,
which also includes the VM configuration files. vzdump currently
supports OpenVZ and QemuServer VMs.
There are several ways to provide consistency:
=over 2
=item C<stop> mode
Stop the VM during backup. This results in a very long downtime.
=item C<suspend> mode
For OpenVZ, this mode uses rsync to copy the VM to a temporary
location (see option --tmpdir). Then the VM is suspended and a second
rsync copies changed files. After that, the VM is started (resume)
again. This results in a minimal downtime, but needs additional space
to hold the VM copy.
For QemuServer, this mode work like C<stop> mode, but uses
suspend/resume instead of stop/start.
=item C<snapshot> mode
This mode uses LVM2 snapshots. There is no downtime, but snapshot mode
needs LVM2 and some free space on the corresponding volume group to
create the LVM snapshot.
=back
=head1 BACKUP FILE NAMES
Newer version of vzdump encodes the virtual machine type and the
backup time into the filename, for example
vzdump-openvz-105-2009_10_09-11_04_43.tar
That way it is possible to store several backup into the same
directory. The parameter C<maxfiles> can be used to specify the maximal
number of backups to keep.
=head1 RESTORE
The resulting tar files can be restored with the following programs.
=over 1
=item vzrestore: OpenVZ restore utility
=item qmrestore: QemuServer restore utility
=back
For details see the corresponding manual pages.
=head1 CONFIGURATION
Global configuration is stored in /etc/vzdump.conf.
tmpdir: DIR
dumpdir: DIR
storage: STORAGE_ID
mode: snapshot|suspend|stop
bwlimit: KBPS
ionize: PRI
lockwait: MINUTES
stopwait: MINUTES
size: MB
maxfiles: N
script: FILENAME
=head1 HOOK SCRIPT
You can specify a hook script with option C<--script>. This script is called at various phases of the backup process, with parameters accordingly set. You can find an example in the documentation directory (C<hook-script.pl>).
=head1 EXCLUSIONS (OpenVZ only)
vzdump skips the following files wit option --stdexcludes
/var/log/.+
/tmp/.+
/var/tmp/.+
/var/run/.+pid
You can manually specify exclude paths, for example:
> vzdump --exclude-path C</tmp/.+> --exclude-path C</var/tmp/.+> 777
(only excludes tmp directories)
Configuration files are also stored inside the backup archive (/etc/vzdump), and will be correctly restored.
=head1 LIMITATIONS
VZDump does not save ACLs.
=head1 EXAMPLES
Simply dump VM 777 - no snapshot, just archive the VM private area and configuration files to the default dump directory (usually /vz/dump/).
> vzdump 777
Use rsync and suspend/resume to create an snapshot (minimal downtime).
> vzdump --suspend 777
Backup all VMs and send notification mails to root.
> vzdump --suspend --all --mailto root
Use LVM2 to create snapshots (no downtime).
> vzdump --dumpdir /mnt/backup --snapshot 777
Backup all VMs excluding VM 101 and 102
> vzdump --suspend --exclude 101 --exclude 102
Restore an OpenVZ machine to VM 600
> vzrestore /mnt/backup/vzdump-openvz-777.tar 600
Restore an Qemu/KVM machine to VM 601
> qmrestore /mnt/backup/vzdump-qemu-888.tar 601
=head1 SEE ALSO
vzrestore(1) qmrestore(1)
=head1 AUTHOR
Dietmar Maurer <dietmar@proxmox.com>
Many thanks to Proxmox Server Solutions (www.proxmox.com) for sponsoring
this work.
=head1 COPYRIGHT AND DISCLAIMER
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.
#!/usr/bin/perl -w
#
# 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 Getopt::Long;
use Sys::Syslog;
use File::Path;
use PVE::VZDump;
use PVE::VZDump::OpenVZ;
$ENV{LANG} = "C"; # avoid locale related issues/warnings
openlog ('vzdump', 'cons,pid', 'daemon');
my $force = 0;
sub print_usage {
my $msg = shift;
print STDERR "ERROR: $msg\n\n" if $msg;
print STDERR "usage: $0 [OPTIONS] <ARCHIVE> <VMID>\n";
print STDERR "\n";
print STDERR "\t--force overwrite existing conf file, private and root directory\n\n";
}
if (!GetOptions ('force' => \$force)) {
print_usage ();
exit (-1);
}
if ($#ARGV != 1) {
print_usage ();
exit (-1);
}
my $archive = shift;
my $vmid = PVE::VZDump::check_vmids ((shift))->[0];
$SIG{INT} = $SIG{TERM} = $SIG{QUIT} = $SIG{HUP} = $SIG{PIPE} = sub {
die "interrupted by signal\n";
};
sub debugmsg { PVE::VZDump::debugmsg (@_); } # just a shortcut
sub run_command { PVE::VZDump::run_command (undef, @_); } # just a shortcut
sub restore_openvz {
my ($archive, $vmid) = @_;
my $vzconf = PVE::VZDump::OpenVZ::read_global_vz_config ();
my $cfgdir = PVE::VZDump::OpenVZ::VZDIR . "/conf";
my $conffile = "$cfgdir/${vmid}.conf";
my $private = $vzconf->{privatedir};
$private =~ s/\$VEID/$vmid/;
my $root = $vzconf->{rootdir};
$root =~ s/\$VEID/$vmid/;
print "you choose to force overwriting VPS config file, private and root directories.\n" if $force;
die "unable to restore VM '$vmid' - VM already exists\n"
if !$force && -f $conffile; ;
die "unable to restore VPS '${vmid}' - " .
"directory '$private' already exists\n"
if !$force && -d $private;
die "unable to restore VPS '${vmid}' - " .
"directory '$root' already exists\n"
if !$force && -d $root;
eval {
mkpath $private || die "unable to create private dir '$private'";
mkpath $root || die "unable to create private dir '$private'";
my $cmd = "tar xpf $archive --totals --sparse -C $private";
if ($archive eq '-') {
debugmsg ('info', "extracting archive from STDIN");
run_command ($cmd, input => "<&STDIN");
} else {
debugmsg ('info', "extracting archive '$archive'");
run_command ($cmd);
}
debugmsg ('info', "extracting configuration to '$conffile'");
my $qroot = $vzconf->{rootdir};
$qroot =~ s|/|\\\/|g;
my $qprivate = $vzconf->{privatedir};
$qprivate =~ s|/|\\\/|g;
my $scmd = "sed -r -e 's/VE_ROOT=.*/VE_ROOT=\\\"$qroot\\\"/' -e 's/VE_PRIVATE=.*/VE_PRIVATE=\\\"$qprivate\\\"/' -e 's/host_ifname=veth[0-9]+\./host_ifname=veth${vmid}./' <'$private/etc/vzdump/vps.conf' >'$conffile'";
run_command ($scmd);
foreach my $s (PVE::VZDump::OpenVZ::SCRIPT_EXT) {
my $tfn = "$cfgdir/${vmid}.$s";
my $sfn = "$private/etc/vzdump/vps.$s";
if (-f $sfn) {
run_command ("cp '$sfn' '$tfn'");
}
}
rmtree "$private/etc/vzdump";
};
my $err = $@;
if ($err) {
rmtree $private;
rmtree $root;
unlink $conffile;
die $err;
}
}
my $plugin = PVE::VZDump::OpenVZ->new();
if ($archive ne '-') {
my $firstfile = PVE::VZDump::read_firstfile ($archive);
if ($firstfile eq 'qemu-server.conf') {
die "ERROR: please use 'qmrestore' to restore QemuServer VMs\n";
}
}
my $lock = $plugin->lock_vm ($vmid);
eval {
debugmsg ('info', "restore openvz backup '$archive' using ID $vmid", undef, 1);
restore_openvz ($archive, $vmid);
debugmsg ('info', "restore openvz backup '$archive' successful", undef, 1);
};
my $err = $@;
$plugin->unlock_vm ($vmid);
if ($err) {
debugmsg ('err', "restore openvz backup '$archive' failed - $err", undef, 1);
exit (-1);
}
exit (0);
__END__
=head1 NAME
vzrestore - restore OpenVZ vzdump backups
=head1 SYNOPSIS
vzrestore <archive> <VMID>
=head1 DESCRIPTION
Restore the OpenVZ vzdump backup <archive> to virtual machine <VMID>.
=head1 SEE ALSO
vzdump(1) qmrestore(1)
......@@ -4,7 +4,9 @@ Section: admin
Priority: optional
Architecture: all
Depends: perl5, libtimedate-perl, apache2-mpm-prefork, libauthen-pam-perl, libintl-perl, rsync, libapache2-request-perl, libjson-perl, libdigest-sha1-perl, liblockfile-simple-perl, vncterm, qemu-server (>= 1.1-1), libwww-perl, wget, libnet-dns-perl, vlan, ifenslave-2.6 (>= 1.1.0-10), liblinux-inotify2-perl, debconf (>= 0.5) | debconf-2.0, netcat-traditional, pve-cluster, libpve-common-perl, libpve-storage-perl, libterm-readline-gnu-perl, libpve-access-control, libio-socket-ssl-perl, libfilesys-df-perl, libfile-readbackwards-perl, libfile-sync-perl, redhat-cluster-pve
Conflicts: netcat-openbsd
Conflicts: netcat-openbsd, vzdump
Replaces: vzdump
Provides: vzdump
Maintainer: Proxmox Support Team <support@proxmox.com>
Description: The Proxmox Virtual Environment
This package contains the Proxmox Virtual Environment management tools.
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment