#!/usr/bin/perl -w
#
#    Copyright (C) 2007 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., 59 Temple Place - Suite 330, Boston, MA
#    02111-1307, USA.
#
#    Author: Dietmar Maurer <dietmar@proxmox.com>
#

use strict;
use Getopt::Long;
use File::Path;
use IO::File;
use IO::Select;
use IPC::Open3;
use POSIX ":sys_wait_h";
use POSIX ':signal_h';
use POSIX qw(strftime);

my $vzdir = '/etc/vz';

my $vpslist;

my $opt_all;
my $opt_exclude;
my $opt_exclude_path;
my $opt_debug = 1;
my $opt_dumpdir;
my $opt_compress = 0;
my $opt_restore;
my $opt_mailto;
my $opt_xdelta;
my $opt_stop;
my $opt_suspend;
my $opt_snap;
    
my $vzctl = 'vzctl';
my $rsync = 'rsync';
my $xdelta = 'xdelta';
my $lvcreate = 'lvcreate';
my $lvscan = 'lvscan';
my $lvremove = 'lvremove';
my $sendmail = 'sendmail';

my @script_ext = qw (start stop mount umount);

my @findexcl;

sub find_add_exclude {
    my ($excltype, $value) = @_;

    if (($excltype eq '-regex') || ($excltype eq '-files')) {
	$value = "\.$value";
    }

    if ($excltype eq '-files') {
	push @findexcl, "'('", '-not', '-type', 'd', '-regex' , "'$value'", "')'", '-o';

    } else {
	push @findexcl, "'('", $excltype , "'$value'", '-prune', "')'", '-o';
    }
}

find_add_exclude ('-type', 's'); # skip sockets

my $debugstattxt = {
    err =>  'ERROR:',
    info => 'INFO:',
    warn => 'WARN:',
};

sub logmsg {
    my ($mtype, $msg, $logfd) = @_;

    chomp $msg;

    return if !$msg || !$logfd;

    my $pre = $debugstattxt->{$mtype};

    foreach my $line (split (/\n/, $msg)) {
	print $logfd strftime ("%b %d %H:%M:%S", localtime) . " $pre $line\n";
    }
}
    
sub debugmsg {
    my ($mtype, $msg, $logfd) = @_;

    chomp $msg;

    return if !$msg;

    my $pre = $debugstattxt->{$mtype};
    print STDERR "$pre $msg\n" if $opt_debug;
    logmsg ($mtype, $msg, $logfd) if $logfd;
}

sub escape_html {
    my ($str) = @_;

    $str =~ s/&/&amp;/g;
    $str =~ s/</&lt;/g;
    $str =~ s/>/&gt;/g;

    return $str;
}

sub encode8bit {
    my ($str) = @_;

    $str =~ s/^(.{990})/$1\n/mg; # reduce line length

    return $str;
}

# send nicely formated multipart/alternative mail
sub send_mail {
    my ($mailto, $vpsid, $logfile, $backuptime, $size, $deltasize, $finalname, $err) = @_;

    my $stat = $err ? 'backup failed' : 'backup successful';

    my $hostname = $vpslist->{$vpsid}->{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: VPS Backup Status for $vpsid ($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

    if ($err) {
	print MAIL "ERRORS during backup - backup FAILED!\n\n";
    } else {
	print MAIL sprintf ("Backup successful - %.2f MB in %.2f minutes\n\n",
			    $size / (1024*1024), $backuptime);
    }

    open (TMP, "$logfile");
    while (my $line = <TMP>) { print MAIL encode8bit ($line); }
    close (TMP);


    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";

    if ($err) {
	print MAIL "<p><b>ERRORS during backup - backup <font color=red>FAILED!</font></b></p>\n";
    } else {
	print MAIL "<p><b>Backup successful!</b></p>\n";

	print MAIL "<table border=1 cellpadding=3>\n";
	print MAIL "<tr><td width=100>VPSID</td><td>$vpsid ($hostname)</td></tr>\n";
	print MAIL "<tr><td>Status</td><td>$stat</td></tr>\n";
	print MAIL sprintf ("<tr><td>Size</td><td>%.2f MB</td></tr>\n", $size / (1024*1024));
	print MAIL sprintf ("<tr><td>Delta</td><td>%.2f MB</td></tr>\n", $deltasize / (1024*1024)) if $opt_xdelta;
	print MAIL sprintf ("<tr><td>Time</td><td>%.2f minutes</td></tr>\n", $backuptime);
	print MAIL "<tr><td>Path</td><td>". escape_html ($finalname) . "</td></tr>\n";
	print MAIL "</table><br>\n";
    }

    print MAIL "<b>Detailed backup log:</b>\n";
    print MAIL "<pre>\n";
    open (TMP, "$logfile");
    while (my $line = <TMP>) { 

	if ($line =~ m/^\S+\s\d+\s+\d+:\d+:\d+\s+(ERROR|WARN):/) {
	    print MAIL encode8bit ("<font color=red>". 
				   escape_html ($line) . "</font>"); 
	} else {
	    print MAIL encode8bit (escape_html ($line)); 
	}
    }
    close (TMP);
    print MAIL "</pre>\n";
    print MAIL "</body></html>\n";

    print MAIL "\n--$boundary--\n";

    close (MAIL);
}

sub run_command {
    my ($logfd, $cmdstr, $input, $timeout) = @_;

    my $reader = IO::File->new();
    my $writer = IO::File->new();
    my $error  = IO::File->new();

    my $pid = open3 ($writer, $reader, $error, ($cmdstr)) || die $!;

    print $writer $input if defined $input;
    close $writer;

    my $select = new IO::Select;
    $select->add ($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";
	}

	foreach my $h (@handles) {
	    my $buf = '';
	    my $count = sysread ($h, $buf, 4096);
	    if (!defined ($count)) {
		waitpid ($pid, 0);
		die "command '$cmdstr' failed: $!";
	    }
	    $select->remove ($h) if !$count;

	    if ($h eq $reader) {
		$ostream .= $buf;
		$logout .= $buf;
		while ($logout =~ s/^([^\n]*\n)//s) {
		    my $line = $1;
		    print STDOUT $line;
		    logmsg ('info', $line, $logfd);
		}
	    } elsif ($h eq $error) {
		$estream .= $buf;
		$logerr .= $buf;
		while ($logerr =~  s/^([^\n]*\n)//s) {
		    my $line = $1;
		    print STDERR $line;
		    logmsg ('info', $line, $logfd);
		}
	    }
	}
    }

    logmsg ('info', $logout, $logfd);
    logmsg ('info', $logerr, $logfd);

    waitpid ($pid, 0);
    my $ec = ($? >> 8);

    return $ostream if $ec == 24 && ($cmdstr =~ m/^$rsync/);

    return $ostream if $ec == 1 && ($cmdstr =~ m/^$xdelta/);

    die "command '$cmdstr' failed with exit code $ec" if $ec;

    return $ostream;
}

sub print_usage {
    my $msg = shift;

    print STDERR "ERROR: $msg\n\n" if $msg;

    print STDERR "usage: $0 OPTIONS [--all | VPSID]\n\n";
    print STDERR "\t--exclude VPSID\t\texclude VPSID (assumes --all)\n";
    print STDERR "\t--exclude-path REGEX\texclude certain files/directories\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--xdelta\t\tcreate differential backup using xdelta\n";

    print STDERR "\t--mailto EMAIL\t\tsend notification mail to EMAIL.\n";
    print STDERR "\t--stop\t\t\tstop/start VPS if running\n";
    print STDERR "\t--suspend\t\tsuspend/resume VPS when running\n";
    print STDERR "\t--snapshot\t\tuse LVM snapshot when running\n";
    print STDERR "\t--restore FILENAME\trestore FILENAME\n";
    print STDERR "\n";

}

my $devmapper;

sub get_device {
    my $dir = shift;

    open (TMP, "df -P '$dir'|");
    <TMP>; #skip first line
    my $out = <TMP>;
    close (TMP);

    my @res = split (/\s+/, $out);

    my $dev = $res[0];
    my $mp = $res[5];
    my ($vg, $lv);

    ($vg, $lv) = @{$devmapper->{$dev}} if defined $devmapper->{$dev};

    return wantarray ? ($dev, $mp, $vg, $lv) : $dev;
}

sub check_vpsid {
    my $vpsid = shift;

    if ($vpsid !~ m/^\d\d\d+$/) {
	print_usage ("strange VPS ID '${vpsid}'");
	exit (-1); 
    }

}

if (!GetOptions ('all' => \$opt_all,
		 'exclude=s@' => \$opt_exclude,
		 'exclude-path=s@' => \$opt_exclude_path,
		 'compress' => \$opt_compress,
		 'restore=s' => \$opt_restore,
		 'mailto=s@' => \$opt_mailto,
		 'xdelta' => \$opt_xdelta,
		 'stop' =>\$opt_stop,
		 'suspend' =>\$opt_suspend,
		 'snapshot' =>\$opt_snap,
		 'dumpdir=s' => \$opt_dumpdir)) {
    print_usage ();
    exit (-1);
}

$opt_all = 1 if $opt_exclude;

if ($opt_all && ($#ARGV >= 0 || $opt_restore)) {
    print_usage ();
    exit (-1);
} 


if (!$opt_all && $#ARGV != 0) {
    print_usage ();
    exit (-1);
}

if ($opt_restore && ! -f $opt_restore) {
    print_usage ("unable to access file '${opt_restore}'");
    exit (-1);
}

my $opt_vpsid;

if (!$opt_all) {
    $opt_vpsid = $ARGV[0];
    check_vpsid ($opt_vpsid);
}

if ($opt_exclude) {
    foreach my $vpsid (@$opt_exclude) { 
	check_vpsid ($vpsid); 
    }
}

if ($opt_exclude_path) {
    foreach my $path (@$opt_exclude_path) {
	find_add_exclude ('-regex', $path);
    }
}
 


sub check_bin {
    my ($bin, $msg)  = @_;

    my $v = $$bin;

    my $path = "/sbin:/bin:/usr/sbin:/usr/bin:/usr/local/sbin:/usr/local/bin";

    foreach my $p (split (/:/, $path)) {
	my $fn = "$p/$v";
	if (-x $fn) {
	    $$bin = $fn;
	    return;
	}
    }

    die "unable to find '$v' - $msg\n";
}

check_bin (\$vzctl, "OpenVZ not installed?");
check_bin (\$rsync, "rsync not installed?") if $opt_suspend;
check_bin (\$xdelta, "xdelta not installed?") if $opt_xdelta;
check_bin (\$lvcreate, "lvm2 not installed?") if $opt_snap;
check_bin (\$lvscan, "lvm2 not installed?") if $opt_snap;
check_bin (\$lvremove, "lvm2 not installed?") if $opt_snap;
check_bin (\$sendmail, "sendmail not installed?") if $opt_mailto;

if ($opt_snap) {
    open (TMP, "$lvscan|") || die "unable to exec $lvscan\n";
    while (my $line = <TMP>) {
	if ($line =~ m|^\s+ACTIVE\s+\'/dev/([^/]+)/([^\']+)\'\s|) {
	    my $vg = $1;
	    my $lv = $2;
	    $devmapper->{"/dev/$vg/$lv"} = [$vg, $lv];
	    $devmapper->{"/dev/mapper/$vg-$lv"} = [$vg, $lv];
	}
    }
    close (TMP);
}


my $stopve;
$stopve = 'stop' if $opt_stop;
$stopve = 'suspend' if $opt_suspend;

my $vzconf = {
    vzdir =>  $vzdir,
    confdir => "$vzdir/conf",
    conffile => "$vzdir/vz.conf",
    rootdir => '/vz/root',
    privatedir => '/vz/private',
    dumpdir => $opt_dumpdir || '/vz/dump',
};

$SIG{INT} = $SIG{TERM} = $SIG{QUIT} = $SIG{HUP} = sub {
    die "interrupted by signal\n";
};

sub remove_quotes {
    my $str = shift;

    $str =~ s/^\s*\"?//;
    $str =~ s/\"?\s*$//;

    return $str;
}

# read global vz.conf
sub read_global_config {
 
    local $/;
    
    my $filename = $vzconf->{conffile};

    die "unable to open file '$filename' - $!\n" if ! -f $filename;

    open (TMP, "<$filename");
    my $data = <TMP> || '';
    close (TMP);

    if ($data =~ m/^\s*VE_PRIVATE=(.*)$/m) {
	my $dir = remove_quotes ($1);
	$dir =~ s|/\$VEID$||;
	$vzconf->{privatedir} = $dir;
    }
    if ($data =~ m/^\s*VE_ROOT=(.*)$/m) {
	my $dir = remove_quotes ($1);
	$dir =~ s|/\$VEID$||;
	$vzconf->{rootdir} = $dir;
    }
    if ($data =~ m/^\s*DUMPDIR=(.*)$/m) {
	my $dir = remove_quotes ($1);
	$dir =~ s|/\$VEID$||;
	$vzconf->{dumpdir} = $opt_dumpdir || $dir;
    }
}

read_global_config ();

my $cfgdir = $vzconf->{confdir};
foreach my $conf (<$cfgdir/*.conf>) {
    if ($conf =~ m|/(\d\d\d+)\.conf$|) {
	my $vpsid = $1;
	local $/;

	open (TMP, "<$conf");
	my $data = <TMP>;
	close (TMP);

	$vpslist->{$vpsid}->{conffile} = $conf;
	$vpslist->{$vpsid}->{conf} = $data;

	if ($data =~ m/^\s*VE_PRIVATE=(.*)$/m) {
	    my $dir = remove_quotes ($1);
	    $dir =~ s/\$VEID/$vpsid/;
	    $vpslist->{$vpsid}->{dir} = $dir;
	} else {
	    $vpslist->{$vpsid}->{dir} = "$vzconf->{privatedir}/$vpsid";
	}

	if ($data =~ m/^\s*VE_ROOT=(.*)$/m) {
	    my $dir = remove_quotes ($1);
	    $dir =~ s/\$VEID/$vpsid/;
	    $vpslist->{$vpsid}->{root} = $dir;
	} else {
	    $vpslist->{$vpsid}->{root} = "$vzconf->{rootdir}/$vpsid";
	}

	if ($data =~ m/^\s*HOSTNAME=(.*)/m) {
	    $vpslist->{$vpsid}->{hostname} = remove_quotes ($1);
	}
    }
}

if ($opt_restore) {

    my $conffile = "$vzconf->{confdir}/${opt_vpsid}.conf";

    if (-f $conffile) {
	print_usage ("unable to restore VPS '${opt_vpsid}' - VPS already exists");
	exit (-1);
    }

    my $private = "$vzconf->{privatedir}/${opt_vpsid}";
    my $root = "$vzconf->{rootdir}/${opt_vpsid}";

    if (-d $private) {
	print_usage ("unable to restore VPS '${opt_vpsid}' - directory '$private' already exists");
	exit (-1);
    }
    if (-d $root) {
	print_usage ("unable to restore VPS '${opt_vpsid}' - directory '$root' already exists");
	exit (-1);
    }

    eval {
	mkpath $private || die "unable to create private dir '$private'";
	mkpath $root || die "unable to create private dir '$private'";

	my $cmd = "zcat -f ${opt_restore}| tar xpf - --totals -C $private";

	debugmsg ('info', "extracting archive '${opt_restore}'");

	system ($cmd) == 0 || die "unable to extract archive";

	debugmsg ('info', "extracting configuration to '$conffile'");

	my $qroot = $root;
	$qroot =~ s|/|\\\/|g;
	$qroot =~ s|/${opt_vpsid}$|/\$VEID|;
	my $qprivate = $private;
	$qprivate =~ s|/|\\\/|g;
	$qprivate =~ s|/${opt_vpsid}$|/\$VEID|;

	my $scmd = "sed -e 's/VE_ROOT=.*/VE_ROOT=\\\"$qroot\\\"/' -e 's/VE_PRIVATE=.*/VE_PRIVATE=\\\"$qprivate\\\"/'  <'$private/etc/vzdump/vps.conf' >'$conffile'";

	run_command (undef, $scmd);

	foreach my $s (@script_ext) {
	    my $cfgdir = $vzconf->{confdir};
	    my $tfn = "$cfgdir/${opt_vpsid}.$s";
	    my $sfn = "$private/etc/vzdump/vps.$s";
	    if (-f $sfn) {
		run_command (undef, "cp '$sfn' '$tfn'");
	    }
	}

	rmtree "$private/etc/vzdump";

	debugmsg ('info', "restore successful");

    };

    my $err = $@;

    if ($err) {
	system ("rm -rf '$private' '$root' '$conffile'");
	die $err;
    }

    exit (0);
}

if ($opt_vpsid && !exists ($vpslist->{$opt_vpsid})) {
    print_usage ("unable to find VPS '${opt_vpsid}'");
    exit (-1);
}

foreach my $vpsid (keys %$vpslist) {

    next if $opt_vpsid && $vpsid ne $opt_vpsid;

    next if grep { $_ eq  $vpsid } @$opt_exclude;

    my $backuptime = time ();

    my $targetdir = $vzconf->{dumpdir};

    my $private = $vpslist->{$vpsid}->{dir};

    my $root = $vpslist->{$vpsid}->{root};

    my $basename = "vzdump-${vpsid}";

    my $tarfile = "$targetdir/$basename". ($opt_compress ? '.tgz' : '.tar');
    my $logfile = "$targetdir/$basename.log";
    my $deltafile = "$targetdir/$basename.xdelta";

    my $oldtarfile;

    if ($opt_xdelta) {
	if (! -f $tarfile) {
	    $opt_xdelta = undef;
	} else {
	    $oldtarfile = $tarfile;
	    $tarfile = "$tarfile.$$";
	}
    }

    unlink $tarfile;
    unlink $logfile;

    unlink $deltafile if !$opt_xdelta;

    my $dir = $private;

    my $tmpdir = "$targetdir/tmp$$";

    open (LOG, ">$logfile");

    my ($lvmpath, $lvmvg, $lvmlv);
    if ($opt_snap) {
	my ($dev, $mp, $vg, $lv) = get_device ($dir);
	$lvmpath = $mp;
	$lvmvg = $vg;
	$lvmlv = $lv;
    }

    my $snapdev;

    my $status = `$vzctl status $vpsid`;
    my $running = 0;
    $running = 1 if $status =~ m/running/;

    my $size = 0;
    my $deltasize = 0;
    my $finalname;

    eval {

	debugmsg ('info', "starting backup for VPS $vpsid ($dir)", \*LOG);

	if ($running) {
	    if ($opt_snap) {

		my $targetdev = get_device ($targetdir);
		my $srcdev = get_device ($dir);

		die "unable to dump into snapshot (use option --dumpdir)" 
		    if ($targetdev eq $srcdev);

		mkdir "/vzsnap"; # create mount point for lvm snapshot

		$snapdev = "/dev/$lvmvg/vzsnap";
		debugmsg ('info', "creating lvm snapshot of $srcdev ('$snapdev')", \*LOG);
		run_command (\*LOG, "$lvcreate --size 500m --snapshot --name vzsnap /dev/$lvmvg/$lvmlv");

		debugmsg ('info', "mounting lvm snapshot", \*LOG);

		run_command (\*LOG, "mount $snapdev /vzsnap");

		$dir =~ s|/?$lvmpath/?|/vzsnap/|;

		die "wrong lvm mount point '$lvmpath'" if ($dir !~ m|/vzsnap|) || (! -d $dir);

	    } elsif ($stopve) {

		system ("rm -rf $tmpdir");

		mkdir $tmpdir || die "unable to create tmpdir";

		my $synccmd = "$rsync -aH --delete $dir $tmpdir";
		debugmsg ('info', "starting first sync $dir to $tmpdir", \*LOG);
		run_command (\*LOG, $synccmd);

		my $starttime = time();

		if ($stopve eq 'stop') {
		    debugmsg ('info', "stopping vps", \*LOG);
		    run_command (\*LOG, "$vzctl stop $vpsid");
		} elsif ($stopve eq 'suspend') {
		    debugmsg ('info', "suspend vps", \*LOG);
		    run_command (\*LOG, "$vzctl chkpnt $vpsid --suspend");
		} else {
		    die "unknown mode '$stopve'";
		}

		debugmsg ('info', "final sync $dir to $tmpdir", \*LOG);
		run_command (\*LOG, $synccmd);

		if ($stopve eq 'stop') {
		    debugmsg ('info', "restarting vps", \*LOG);
		    run_command (\*LOG, "$vzctl start $vpsid");
		} elsif ($stopve eq 'suspend') {
		    debugmsg ('info', "resume vps", \*LOG);
		    run_command (\*LOG, "$vzctl chkpnt $vpsid --resume");
		}

		my $delay = time () - $starttime;

		debugmsg ('info', "vps is online again after $delay seconds", \*LOG);
	    
		$dir = "$tmpdir/$vpsid";
	    } else {
		debugmsg ('warn', "online backup without suspend/snapshot", \*LOG);
		debugmsg ('warn', "this can lead to inconsistent data", \*LOG);
	    }
	}

	debugmsg ('info', "Creating archive '$tarfile' ($dir)", \*LOG);

	mkpath "$dir/etc/vzdump/";
	my $srcconf = $vpslist->{$vpsid}->{conffile};
	run_command (\*LOG, "cp $srcconf $dir/etc/vzdump/vps.conf");

	foreach my $s (@script_ext) {
	    my $cfgdir = $vzconf->{confdir};
	    my $fn = "$cfgdir/$vpsid.$s";
	    if (-f $fn) {
		run_command (\*LOG, "cp $fn $dir/etc/vzdump/vps.$s");
	    }
	}

	if (!$opt_exclude_path) {
	    find_add_exclude ('-files', '/var/log/.+');
	    find_add_exclude ('-regex', '/tmp/.+');
	    find_add_exclude ('-regex', '/var/tmp/.+');
	    find_add_exclude ('-regex', '/var/run/.+pid');
	}

	my $zflag = $opt_compress ? 'z' : '';

	# backup all types except sockets
	my $findargs = join (' ', @findexcl) . ' -print0';

	my $cmd = "(cd $dir; find . $findargs|tar c${zflag}pf $tarfile --totals --numeric-owner --no-recursion --ignore-failed-read --null -T -)";

	run_command (\*LOG, $cmd);

	rmtree "$dir/etc/vzdump";

	system ("rm -rf $tmpdir"); # no longer needed

	$size = -s $tarfile;
	$finalname = $tarfile;

	if ($opt_xdelta) {
	    debugmsg ('info', "creating delta", \*LOG);
	    run_command (\*LOG, "$xdelta delta $oldtarfile $tarfile $deltafile");
	    unlink $tarfile;
	    $deltasize = -s $deltafile;
	    $finalname = $deltafile;
	}

    };

    my $err = $@;

    system ("rm -rf $tmpdir");

    if ($running && $opt_snap && $snapdev) {
	eval { run_command (\*LOG, "umount /vzsnap"); };

	rmdir "/vzsnap";

	eval { run_command (\*LOG, "$lvremove -f $snapdev"); };
    }

    $backuptime = (time () - $backuptime) / 60.0;
 
    if ($err) {
	unlink $tarfile;
	unlink $deltafile;
	debugmsg ('err', sprintf ("creating backup for  VPS %s failed (%.2f minutes): %s", 
			   $vpsid, $backuptime, $err), \*LOG);
    } else {
	debugmsg ('info', sprintf ("backup for VPS %s finished successful  (%.2f minutes)", 
			   $vpsid, $backuptime), \*LOG);
    }
 
    close (LOG);

    eval {
	send_mail ($opt_mailto, $vpsid, $logfile, $backuptime, 
		   $size, $deltasize, $finalname, $err) if ($opt_mailto);
    };

    debugmsg ('err', $@) if $@;

    last if $err =~ m/interrupted by signal/;
}

exit 0;

__END__

=head1 NAME
                                          
vzdump - openvz backup and restore utility

=head1 SYNOPSIS

vzdump OPTIONS [--all | <VEID>]

--exclude VPSID         exclude VPSID (assumes --all)

--exclude-path REGEX    exclude certain files/directories. Standard 
                        exclusions are disabled in this case. You 
                        can use this option more than once to specify 
                        multiple exclude paths

--compress              compress dump file (gzip)

--dumpdir DIR           store resulting files in DIR

--xdelta                create a differential backup using xdelta

--mailto EMAIL          send notification mail to EMAIL. You can use 
                        this option more than once to specify multiple 
                        receivers

--stop                  stop/start VPS if running

--suspend               suspend/resume VPS when running

--snapshot              use LVM snapshot when running

--restore FILENAME      restore FILENAME

=head1 DESCRIPTION

vzdump is an utility to make consistent snapshots of running OpenVZ VEs. It basically creates a tar archive of the VE private area, which also includes the VE configuration files.

There are several ways to provide consistency:

- stop the VE during backup (very long downtime)

- use rsync and suspend/resume (minimal downtime).

- use LVM2 (no downtime, but needs LVM2 and 500m free space on the corresponding volume group to create the LVM snapshot)

=head1 FILES

vzdump skips the following files by default

 /var/log/.+
 /tmp/.+
 /var/tmp/.+
 /var/run/.+pid

You can overwrite those exclusions by manually specifying exclude paths, for example:

> vzdump --exclude-path '/tmp/.+' --exclude-path '/var/tmp/.+' 777

(only excludes tmp directories)

OpenVZ configuration files are also stored inside the backup archive (/etc/vzdump), and will be correctly restored with --restore

=head1 EXAMPLES

Simply dump VE 777 - no snapshot, just archive the VE 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 VEs and send notification mails to root.

> vzdump --suspend --all --mailto root

Use LVM2 to create snapshots (no downtime).

> vzdump --dumpdir /space/backup --snapshot 777

Restore above backup to VE 600

> vzdump --restore /space/backup/vzdump-777.tar 600

Backup all VESs excluding VE 101 and 102

> vzdump --suspend --exclude 101 --exclude 102

=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 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., 59 Temple Place - Suite 330, Boston, MA
02111-1307, USA.

