diff --git a/PVE/Makefile b/PVE/Makefile index a5d3a37d..eafa3773 100644 --- a/PVE/Makefile +++ b/PVE/Makefile @@ -1,6 +1,6 @@ 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} diff --git a/PVE/VZDump.pm b/PVE/VZDump.pm new file mode 100644 index 00000000..dd6d6602 --- /dev/null +++ b/PVE/VZDump.pm @@ -0,0 +1,1141 @@ +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 + +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/&/&/g; + $str =~ s//>/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 = ; + 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 \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 = ) { 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 "\n"; + + print MAIL "\n"; + + print MAIL "\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 ("\n", + $vmid, $name, + format_time($task->{backuptime}), + format_size ($task->{size}), + escape_html ($task->{tarfile})); + } else { + print MAIL sprintf ("\n", + + $vmid, $name, format_time($task->{backuptime}), + escape_html ($task->{msg})); + } + } + + print MAIL sprintf ("", + format_time ($totaltime), format_size ($ssize)); + + print MAIL "
VMIDNAMESTATUSTIMESIZEFILENAME
%s%sOK%s%s%s
%s%sFAILED%s%s
TOTAL%s%s


\n"; + print MAIL "Detailed backup logs:
\n"; + print MAIL "
\n"; + print MAIL "
\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 = ) {
+	    if ($line =~ m/^\S+\s\d+\s+\d+:\d+:\d+\s+(ERROR|WARN):/) {
+		print MAIL encode8bit ("$vmid: ". 
+				       escape_html ($line) . ""); 
+	    } else {
+		print MAIL encode8bit ("$vmid: " . escape_html ($line)); 
+	    }
+	}
+	close (TMP);
+	print MAIL "\n";
+    }
+    print MAIL "
\n"; + + print MAIL "\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; diff --git a/PVE/VZDump/Makefile b/PVE/VZDump/Makefile new file mode 100644 index 00000000..672e4e81 --- /dev/null +++ b/PVE/VZDump/Makefile @@ -0,0 +1,19 @@ +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 diff --git a/PVE/VZDump/OpenVZ.pm b/PVE/VZDump/OpenVZ.pm new file mode 100644 index 00000000..3bdba7c7 --- /dev/null +++ b/PVE/VZDump/OpenVZ.pm @@ -0,0 +1,400 @@ +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 + +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; diff --git a/PVE/VZDump/Plugin.pm b/PVE/VZDump/Plugin.pm new file mode 100644 index 00000000..0a27b467 --- /dev/null +++ b/PVE/VZDump/Plugin.pm @@ -0,0 +1,149 @@ +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 + +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; diff --git a/bin/Makefile b/bin/Makefile index 0f657d87..4b037469 100644 --- a/bin/Makefile +++ b/bin/Makefile @@ -3,6 +3,8 @@ include ../defines.mk SUBDIRS = init.d cron test SCRIPTS = \ + vzdump \ + vzrestore \ pvestatd \ pvesh \ pveam \ diff --git a/bin/vzdump b/bin/vzdump new file mode 100755 index 00000000..59f74007 --- /dev/null +++ b/bin/vzdump @@ -0,0 +1,399 @@ +#!/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 +# + +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 | ] + +--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 mode + +Stop the VM during backup. This results in a very long downtime. + +=item C 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 mode, but uses +suspend/resume instead of stop/start. + +=item C 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 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). + +=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
--exclude-path C 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 + +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. + diff --git a/bin/vzrestore b/bin/vzrestore new file mode 100755 index 00000000..6b851c79 --- /dev/null +++ b/bin/vzrestore @@ -0,0 +1,182 @@ +#!/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 +# + +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] \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 + +=head1 DESCRIPTION + +Restore the OpenVZ vzdump backup to virtual machine . + +=head1 SEE ALSO + + vzdump(1) qmrestore(1) diff --git a/debian/control.in b/debian/control.in index 003e57f6..f7208e5d 100644 --- a/debian/control.in +++ b/debian/control.in @@ -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 Description: The Proxmox Virtual Environment This package contains the Proxmox Virtual Environment management tools.