qemu-server/PVE/VZDump/QemuServer.pm
Alexandre Derumier 7d930a0e7e vzdump : abord backup if disk have iothread enabled.
Currently backup don't work with iothread feature, and crash qemu

For now, abord the backup if one of the vm drives have iothread enabled until backup code is fixed.

Upstream qemu backup cde already support iothread.
http://git.qemu.org/?p=qemu.git;a=commit;h=761731b1805f6ef64eb615e5b82a0801db3cde78

Signed-off-by: Alexandre Derumier <aderumier@odiso.com>
2015-07-22 12:28:52 +02:00

539 lines
12 KiB
Perl

package PVE::VZDump::QemuServer;
use strict;
use warnings;
use File::Path;
use File::Basename;
use PVE::INotify;
use PVE::VZDump;
use PVE::IPCC;
use PVE::Cluster qw(cfs_read_file);
use PVE::Tools;
use PVE::Storage::Plugin;
use PVE::Storage;
use PVE::QemuServer;
use IO::File;
use IPC::Open3;
use base qw (PVE::VZDump::Plugin);
sub new {
my ($class, $vzdump) = @_;
PVE::VZDump::check_bin('qm');
my $self = bless { vzdump => $vzdump };
$self->{vmlist} = PVE::QemuServer::vzlist();
$self->{storecfg} = PVE::Storage::config();
return $self;
};
sub type {
return 'qemu';
}
sub vmlist {
my ($self) = @_;
return [ keys %{$self->{vmlist}} ];
}
sub prepare {
my ($self, $task, $vmid, $mode) = @_;
$task->{disks} = [];
my $conf = $self->{vmlist}->{$vmid} = PVE::QemuServer::load_config($vmid);
$self->{vm_was_running} = 1;
if (!PVE::QemuServer::check_running($vmid)) {
$self->{vm_was_running} = 0;
}
$task->{hostname} = $conf->{name};
my $hostname = PVE::INotify::nodename();
my $vollist = [];
my $drivehash = {};
PVE::QemuServer::foreach_drive($conf, sub {
my ($ds, $drive) = @_;
return if PVE::QemuServer::drive_is_cdrom($drive);
if (defined($drive->{backup}) && $drive->{backup} eq "no") {
$self->loginfo("exclude disk '$ds' (backup=no)");
return;
} elsif (defined($drive->{iothread}) && $drive->{iothread} eq "on") {
die "disk '$ds' (iothread=on) can't use backup feature currently. Please set backup=no for this drive";
}
my $volid = $drive->{file};
my ($storeid, $volname) = PVE::Storage::parse_volume_id($volid, 1);
push @$vollist, $volid if $storeid;
$drivehash->{$ds} = $drive;
});
PVE::Storage::activate_volumes($self->{storecfg}, $vollist);
foreach my $ds (sort keys %$drivehash) {
my $drive = $drivehash->{$ds};
my $volid = $drive->{file};
my $path;
my ($storeid, $volname) = PVE::Storage::parse_volume_id($volid, 1);
if ($storeid) {
$path = PVE::Storage::path($self->{storecfg}, $volid);
} else {
$path = $volid;
}
next if !$path;
my $format = undef;
my $size = undef;
eval{
($size, $format) = PVE::Storage::volume_size_info($self->{storecfg}, $volid, 5);
};
die "no such volume '$volid'\n" if $@;
my $diskinfo = { path => $path , volid => $volid, storeid => $storeid,
format => $format, virtdev => $ds, qmdevice => "drive-$ds" };
if (-b $path) {
$diskinfo->{type} = 'block';
} else {
$diskinfo->{type} = 'file';
}
push @{$task->{disks}}, $diskinfo;
}
}
sub vm_status {
my ($self, $vmid) = @_;
my $running = PVE::QemuServer::check_running($vmid) ? 1 : 0;
return wantarray ? ($running, $running ? 'running' : 'stopped') : $running;
}
sub lock_vm {
my ($self, $vmid) = @_;
$self->cmd ("qm set $vmid --lock backup");
}
sub unlock_vm {
my ($self, $vmid) = @_;
$self->cmd ("qm unlock $vmid");
}
sub stop_vm {
my ($self, $task, $vmid) = @_;
my $opts = $self->{vzdump}->{opts};
my $wait = $opts->{stopwait} * 60;
# send shutdown and wait
$self->cmd ("qm shutdown $vmid --skiplock --keepActive --timeout $wait");
}
sub start_vm {
my ($self, $task, $vmid) = @_;
$self->cmd ("qm start $vmid --skiplock");
}
sub suspend_vm {
my ($self, $task, $vmid) = @_;
$self->cmd ("qm suspend $vmid --skiplock");
}
sub resume_vm {
my ($self, $task, $vmid) = @_;
$self->cmd ("qm resume $vmid --skiplock");
}
sub assemble {
my ($self, $task, $vmid) = @_;
my $conffile = PVE::QemuServer::config_file ($vmid);
my $outfile = "$task->{tmpdir}/qemu-server.conf";
my $outfd;
my $conffd;
eval {
$outfd = IO::File->new (">$outfile") ||
die "unable to open '$outfile'";
$conffd = IO::File->new ($conffile, 'r') ||
die "unable open '$conffile'";
my $found_snapshot;
while (defined (my $line = <$conffd>)) {
next if $line =~ m/^\#vzdump\#/; # just to be sure
next if $line =~ m/^\#qmdump\#/; # just to be sure
if ($line =~ m/^\[.*\]\s*$/) {
$found_snapshot = 1;
}
next if $found_snapshot; # skip all snapshots data
if ($line =~ m/^unused\d+:\s*(\S+)\s*/) {
$self->loginfo("skip unused drive '$1' (not included into backup)");
next;
}
next if $line =~ m/^lock:/ || $line =~ m/^parent:/;
print $outfd $line;
}
foreach my $di (@{$task->{disks}}) {
if ($di->{type} eq 'block' || $di->{type} eq 'file') {
my $storeid = $di->{storeid} || '';
my $format = $di->{format} || '';
print $outfd "#qmdump#map:$di->{virtdev}:$di->{qmdevice}:$storeid:$format:\n";
} else {
die "internal error";
}
}
if ($found_snapshot) {
$self->loginfo("snapshots found (not included into backup)");
}
};
my $err = $@;
close ($outfd) if $outfd;
close ($conffd) if $conffd;
die $err if $err;
}
sub archive {
my ($self, $task, $vmid, $filename, $comp) = @_;
my $conffile = "$task->{tmpdir}/qemu-server.conf";
my $opts = $self->{vzdump}->{opts};
my $starttime = time ();
my $speed = 0;
if ($opts->{bwlimit}) {
$speed = $opts->{bwlimit}*1024;
}
my $diskcount = scalar(@{$task->{disks}});
if (PVE::QemuServer::is_template($self->{vmlist}->{$vmid}) || !$diskcount) {
my @pathlist;
foreach my $di (@{$task->{disks}}) {
if ($di->{type} eq 'block' || $di->{type} eq 'file') {
push @pathlist, "$di->{qmdevice}=$di->{path}";
} else {
die "implement me";
}
}
if (!$diskcount) {
$self->loginfo("backup contains no disks");
}
my $outcmd;
if ($comp) {
$outcmd = "exec:$comp";
} else {
$outcmd = "exec:cat";
}
$outcmd .= ">$filename" if !$opts->{stdout};
my $cmd = ['/usr/bin/vma', 'create', '-v', '-c', $conffile, $outcmd, @pathlist];
$self->loginfo("starting template backup");
$self->loginfo(join(' ', @$cmd));
if ($opts->{stdout}) {
$self->cmd($cmd, output => ">&=" . fileno($opts->{stdout}));
} else {
$self->cmd($cmd);
}
return;
}
my $devlist = '';
foreach my $di (@{$task->{disks}}) {
if ($di->{type} eq 'block' || $di->{type} eq 'file') {
$devlist .= $devlist ? ",$di->{qmdevice}" : $di->{qmdevice};
} else {
die "implement me";
}
}
my $stop_after_backup;
my $resume_on_backup;
my $skiplock = 1;
my $vm_is_running = PVE::QemuServer::check_running($vmid);
if (!$vm_is_running) {
eval {
$self->loginfo("starting kvm to execute backup task");
PVE::QemuServer::vm_start($self->{storecfg}, $vmid, undef,
$skiplock, undef, 1);
if ($self->{vm_was_running}) {
$resume_on_backup = 1;
} else {
$stop_after_backup = 1;
}
};
if (my $err = $@) {
die $err;
}
}
my $cpid;
my $interrupt_msg = "interrupted by signal\n";
eval {
$SIG{INT} = $SIG{TERM} = $SIG{QUIT} = $SIG{HUP} = $SIG{PIPE} = sub {
die $interrupt_msg;
};
my $qmpclient = PVE::QMPClient->new();
my $uuid;
my $backup_cb = sub {
my ($vmid, $resp) = @_;
$uuid = $resp->{return};
};
my $outfh;
if ($opts->{stdout}) {
$outfh = $opts->{stdout};
} else {
$outfh = IO::File->new($filename, "w") ||
die "unable to open file '$filename' - $!\n";
}
my $outfileno;
if ($comp) {
my @pipefd = POSIX::pipe();
$cpid = fork();
die "unable to fork worker - $!" if !defined($cpid);
if ($cpid == 0) {
eval {
POSIX::close($pipefd[1]);
# redirect STDIN
my $fd = fileno(STDIN);
close STDIN;
POSIX::close(0) if $fd != 0;
die "unable to redirect STDIN - $!"
if !open(STDIN, "<&", $pipefd[0]);
# redirect STDOUT
$fd = fileno(STDOUT);
close STDOUT;
POSIX::close (1) if $fd != 1;
die "unable to redirect STDOUT - $!"
if !open(STDOUT, ">&", fileno($outfh));
exec($comp);
die "fork compressor '$comp' failed\n";
};
if (my $err = $@) {
$self->logerr($err);
POSIX::_exit(1);
}
POSIX::_exit(0);
kill(-9, $$);
} else {
POSIX::close($pipefd[0]);
$outfileno = $pipefd[1];
}
} else {
$outfileno = fileno($outfh);
}
my $add_fd_cb = sub {
my ($vmid, $resp) = @_;
$qmpclient->queue_cmd($vmid, $backup_cb, 'backup',
'backup-file' => "/dev/fdname/backup",
speed => $speed,
'config-file' => $conffile,
devlist => $devlist);
};
$qmpclient->queue_cmd($vmid, $add_fd_cb, 'getfd',
fd => $outfileno, fdname => "backup");
if ($self->{vmlist}->{$vmid}->{agent} && $vm_is_running){
eval { PVE::QemuServer::vm_mon_cmd($vmid, "guest-fsfreeze-freeze"); };
if (my $err = $@) {
$self->logerr($err);
}
}
$qmpclient->queue_execute();
if ($self->{vmlist}->{$vmid}->{agent} && $vm_is_running ){
eval { PVE::QemuServer::vm_mon_cmd($vmid, "guest-fsfreeze-thaw"); };
if (my $err = $@) {
$self->logerr($err);
}
}
die $qmpclient->{errors}->{$vmid} if $qmpclient->{errors}->{$vmid};
if ($cpid) {
POSIX::close($outfileno) == 0 ||
die "close output file handle failed\n";
}
die "got no uuid for backup task\n" if !$uuid;
$self->loginfo("started backup task '$uuid'");
if ($resume_on_backup) {
$self->loginfo("resume VM");
PVE::QemuServer::vm_mon_cmd($vmid, 'cont');
}
my $status;
my $starttime = time ();
my $last_per = -1;
my $last_total = 0;
my $last_zero = 0;
my $last_transferred = 0;
my $last_time = time();
my $transferred;
while(1) {
$status = PVE::QemuServer::vm_mon_cmd($vmid, 'query-backup');
my $total = $status->{total} || 0;
$transferred = $status->{transferred} || 0;
my $per = $total ? int(($transferred * 100)/$total) : 0;
my $zero = $status->{'zero-bytes'} || 0;
my $zero_per = $total ? int(($zero * 100)/$total) : 0;
die "got unexpected uuid\n" if !$status->{uuid} || ($status->{uuid} ne $uuid);
my $ctime = time();
my $duration = $ctime - $starttime;
my $rbytes = $transferred - $last_transferred;
my $wbytes = $rbytes - ($zero - $last_zero);
my $timediff = ($ctime - $last_time) || 1; # fixme
my $mbps_read = ($rbytes > 0) ?
int(($rbytes/$timediff)/(1000*1000)) : 0;
my $mbps_write = ($wbytes > 0) ?
int(($wbytes/$timediff)/(1000*1000)) : 0;
my $statusline = "status: $per% ($transferred/$total), " .
"sparse ${zero_per}% ($zero), duration $duration, " .
"$mbps_read/$mbps_write MB/s";
my $res = $status->{status} || 'unknown';
if ($res ne 'active') {
$self->loginfo($statusline);
die(($status->{errmsg} || "unknown error") . "\n")
if $res eq 'error';
die "got unexpected status '$res'\n"
if $res ne 'done';
die "got wrong number of transfered bytes ($total != $transferred)\n"
if ($res eq 'done') && ($total != $transferred);
last;
}
if ($per != $last_per && ($timediff > 2)) {
$self->loginfo($statusline);
$last_per = $per;
$last_total = $total if $total;
$last_zero = $zero if $zero;
$last_transferred = $transferred if $transferred;
$last_time = $ctime;
}
sleep(1);
}
my $duration = time() - $starttime;
if ($transferred && $duration) {
my $mb = int($transferred/(1000*1000));
my $mbps = int(($transferred/$duration)/(1000*1000));
$self->loginfo("transferred $mb MB in $duration seconds ($mbps MB/s)");
}
};
my $err = $@;
if ($err) {
$self->logerr($err);
$self->loginfo("aborting backup job");
eval { PVE::QemuServer::vm_mon_cmd($vmid, 'backup-cancel'); };
if (my $err1 = $@) {
$self->logerr($err1);
}
}
if ($stop_after_backup) {
# stop if not running
eval {
my $resp = PVE::QemuServer::vm_mon_cmd($vmid, 'query-status');
my $status = $resp && $resp->{status} ? $resp->{status} : 'unknown';
if ($status eq 'prelaunch') {
$self->loginfo("stopping kvm after backup task");
PVE::QemuServer::vm_stop($self->{storecfg}, $vmid, $skiplock);
} else {
$self->loginfo("kvm status changed after backup ('$status')" .
" - keep VM running");
}
}
}
if ($err) {
if ($cpid) {
kill(9, $cpid);
waitpid($cpid, 0);
}
die $err;
}
if ($cpid && (waitpid($cpid, 0) > 0)) {
my $stat = $?;
my $ec = $stat >> 8;
my $signal = $stat & 127;
if ($ec || $signal) {
die "$comp failed - wrong exit status $ec" .
($signal ? " (signal $signal)\n" : "\n");
}
}
}
sub snapshot {
my ($self, $task, $vmid) = @_;
# nothing to do
}
sub cleanup {
my ($self, $task, $vmid) = @_;
# nothing to do ?
}
1;