From 91bd6c909b29421410997ce341e7ef0a5fc889f6 Mon Sep 17 00:00:00 2001 From: Dietmar Maurer Date: Wed, 12 Dec 2012 15:35:26 +0100 Subject: [PATCH] include new qemu backup feature We can still restore old tar files. But new backups always use new vma format. Also moved rescan code from qm into PVE::Qemuserver bump version to 2.3-1 --- Makefile | 6 +- PVE/QemuServer.pm | 421 ++++++++++++++++++++++++++++++---- PVE/VZDump/QemuServer.pm | 477 ++++++++++++++++++++------------------- changelog.Debian | 6 + qm | 65 +----- 5 files changed, 629 insertions(+), 346 deletions(-) diff --git a/Makefile b/Makefile index 40f523fc..ba415ba0 100644 --- a/Makefile +++ b/Makefile @@ -1,8 +1,8 @@ -RELEASE=2.2 +RELEASE=2.3 -VERSION=2.0 +VERSION=2.3 PACKAGE=qemu-server -PKGREL=71 +PKGREL=1 DESTDIR= PREFIX=/usr diff --git a/PVE/QemuServer.pm b/PVE/QemuServer.pm index 81cc682f..8784fc94 100644 --- a/PVE/QemuServer.pm +++ b/PVE/QemuServer.pm @@ -27,6 +27,7 @@ use PVE::Cluster qw(cfs_register_file cfs_read_file cfs_write_file cfs_lock_file use PVE::INotify; use PVE::ProcFSTools; use PVE::QMPClient; +use PVE::RPCEnvironment; use Time::HiRes qw(gettimeofday); my $cpuinfo = PVE::ProcFSTools::read_cpuinfo(); @@ -444,7 +445,6 @@ my $nic_model_list = ['rtl8139', 'ne2k_pci', 'e1000', 'pcnet', 'virtio', 'ne2k_isa', 'i82551', 'i82557b', 'i82559er']; my $nic_model_list_txt = join(' ', sort @$nic_model_list); -# fixme: my $netdesc = { optional => 1, type => 'string', format => 'pve-qm-net', @@ -1249,8 +1249,6 @@ sub add_unused_volume { return $key; } -# fixme: remove all thos $noerr parameters? - PVE::JSONSchema::register_format('pve-qm-bootdisk', \&verify_bootdisk); sub verify_bootdisk { my ($value, $noerr) = @_; @@ -2883,7 +2881,7 @@ sub qga_unfreezefs { } sub vm_start { - my ($storecfg, $vmid, $statefile, $skiplock, $migratedfrom) = @_; + my ($storecfg, $vmid, $statefile, $skiplock, $migratedfrom, $paused) = @_; lock_config($vmid, sub { my $conf = load_config($vmid, $migratedfrom); @@ -2910,6 +2908,8 @@ sub vm_start { } else { push @$cmd, '-loadstate', $statefile; } + } elsif ($paused) { + push @$cmd, '-S'; } # host pci devices @@ -3446,6 +3446,369 @@ sub restore_cleanup { sub restore_archive { my ($archive, $vmid, $user, $opts) = @_; + my $format = $opts->{format}; + my $comp; + + if ($archive =~ m/\.tgz$/ || $archive =~ m/\.tar\.gz$/) { + $format = 'tar' if !$format; + $comp = 'gzip'; + } elsif ($archive =~ m/\.tar$/) { + $format = 'tar' if !$format; + } elsif ($archive =~ m/.tar.lzo$/) { + $format = 'tar' if !$format; + $comp = 'lzop'; + } elsif ($archive =~ m/\.vma$/) { + $format = 'vma' if !$format; + } elsif ($archive =~ m/\.vma\.gz$/) { + $format = 'vma' if !$format; + $comp = 'gzip'; + } elsif ($archive =~ m/\.vma\.lzo$/) { + $format = 'vma' if !$format; + $comp = 'lzop'; + } else { + $format = 'vma' if !$format; # default + } + + # try to detect archive format + if ($format eq 'tar') { + return restore_tar_archive($archive, $vmid, $user, $opts); + } else { + return restore_vma_archive($archive, $vmid, $user, $opts, $comp); + } +} + +sub restore_update_config_line { + my ($outfd, $cookie, $vmid, $map, $line, $unique) = @_; + + return if $line =~ m/^\#qmdump\#/; + return if $line =~ m/^\#vzdump\#/; + return if $line =~ m/^lock:/; + return if $line =~ m/^unused\d+:/; + return if $line =~ m/^parent:/; + + if (($line =~ m/^(vlan(\d+)):\s*(\S+)\s*$/)) { + # try to convert old 1.X settings + my ($id, $ind, $ethcfg) = ($1, $2, $3); + foreach my $devconfig (PVE::Tools::split_list($ethcfg)) { + my ($model, $macaddr) = split(/\=/, $devconfig); + $macaddr = PVE::Tools::random_ether_addr() if !$macaddr || $unique; + my $net = { + model => $model, + bridge => "vmbr$ind", + macaddr => $macaddr, + }; + my $netstr = print_net($net); + + print $outfd "net$cookie->{netcount}: $netstr\n"; + $cookie->{netcount}++; + } + } elsif (($line =~ m/^(net\d+):\s*(\S+)\s*$/) && $unique) { + my ($id, $netstr) = ($1, $2); + my $net = parse_net($netstr); + $net->{macaddr} = PVE::Tools::random_ether_addr() if $net->{macaddr}; + $netstr = print_net($net); + print $outfd "$id: $netstr\n"; + } elsif ($line =~ m/^((ide|scsi|virtio|sata)\d+):\s*(\S+)\s*$/) { + my $virtdev = $1; + my $value = $2; + if ($line =~ m/backup=no/) { + print $outfd "#$line"; + } elsif ($virtdev && $map->{$virtdev}) { + my $di = PVE::QemuServer::parse_drive($virtdev, $value); + $di->{file} = $map->{$virtdev}; + $value = PVE::QemuServer::print_drive($vmid, $di); + print $outfd "$virtdev: $value\n"; + } else { + print $outfd $line; + } + } else { + print $outfd $line; + } +} + +sub scan_volids { + my ($cfg, $vmid) = @_; + + my $info = PVE::Storage::vdisk_list($cfg, undef, $vmid); + + my $volid_hash = {}; + foreach my $storeid (keys %$info) { + foreach my $item (@{$info->{$storeid}}) { + next if !($item->{volid} && $item->{size}); + $volid_hash->{$item->{volid}} = $item; + } + } + + return $volid_hash; +} + +sub update_disksize { + my ($vmid, $conf, $volid_hash) = @_; + + my $changes; + + my $used = {}; + + # update size info + foreach my $opt (keys %$conf) { + if (PVE::QemuServer::valid_drivename($opt)) { + my $drive = PVE::QemuServer::parse_drive($opt, $conf->{$opt}); + my $volid = $drive->{file}; + next if !$volid; + + $used->{$volid} = 1; + + next if PVE::QemuServer::drive_is_cdrom($drive); + next if !$volid_hash->{$volid}; + + $drive->{size} = $volid_hash->{$volid}->{size}; + $changes = 1; + $conf->{$opt} = PVE::QemuServer::print_drive($vmid, $drive); + } + } + + foreach my $volid (sort keys %$volid_hash) { + next if $volid =~ m/vm-$vmid-state-/; + next if $used->{$volid}; + $changes = 1; + PVE::QemuServer::add_unused_volume($conf, $volid); + } + + return $changes; +} + +sub rescan { + my ($vmid, $nolock) = @_; + + my $cfg = PVE::Cluster::cfs_read_file("storage.cfg"); + + my $volid_hash = scan_volids($cfg, $vmid); + + my $updatefn = sub { + my ($vmid) = @_; + + my $conf = PVE::QemuServer::load_config($vmid); + + PVE::QemuServer::check_lock($conf); + + my $changes = PVE::QemuServer::update_disksize($vmid, $conf, $volid_hash); + + PVE::QemuServer::update_config_nolock($vmid, $conf, 1) if $changes; + }; + + if (defined($vmid)) { + if ($nolock) { + &$updatefn($vmid); + } else { + PVE::QemuServer::lock_config($vmid, $updatefn, $vmid); + } + } else { + my $vmlist = config_list(); + foreach my $vmid (keys %$vmlist) { + if ($nolock) { + &$updatefn($vmid); + } else { + PVE::QemuServer::lock_config($vmid, $updatefn, $vmid); + } + } + } +} + +sub restore_vma_archive { + my ($archive, $vmid, $user, $opts, $comp) = @_; + + my $input = $archive eq '-' ? "<&STDIN" : undef; + my $readfrom = $archive; + + my $uncomp = ''; + if ($comp) { + $readfrom = '-'; + my $qarchive = PVE::Tools::shellquote($archive); + if ($comp eq 'gzip') { + $uncomp = "zcat $qarchive|"; + } elsif ($comp eq 'lzop') { + $uncomp = "lzop -d -c $qarchive|"; + } else { + die "unknown compression method '$comp'\n"; + } + + } + + my $tmpdir = "/var/tmp/vzdumptmp$$"; + rmtree $tmpdir; + + # disable interrupts (always do cleanups) + local $SIG{INT} = $SIG{TERM} = $SIG{QUIT} = $SIG{HUP} = sub { + warn "got interrupt - ignored\n"; + }; + + my $mapfifo = "/var/tmp/vzdumptmp$$.fifo"; + POSIX::mkfifo($mapfifo, 0600); + my $fifofh; + + my $openfifo = sub { + open($fifofh, '>', $mapfifo) || die $!; + }; + + my $cmd = "${uncomp}vma extract -v -r $mapfifo $readfrom $tmpdir"; + + my $oldtimeout; + my $timeout = 5; + + my $devinfo = {}; + + my $rpcenv = PVE::RPCEnvironment::get(); + + my $conffile = PVE::QemuServer::config_file($vmid); + my $tmpfn = "$conffile.$$.tmp"; + + my $print_devmap = sub { + my $virtdev_hash = {}; + + my $cfgfn = "$tmpdir/qemu-server.conf"; + + # we can read the config - that is already extracted + my $fh = IO::File->new($cfgfn, "r") || + "unable to read qemu-server.conf - $!\n"; + + while (defined(my $line = <$fh>)) { + if ($line =~ m/^\#qmdump\#map:(\S+):(\S+):(\S*):(\S*):$/) { + my ($virtdev, $devname, $storeid, $format) = ($1, $2, $3, $4); + die "archive does not contain data for drive '$virtdev'\n" + if !$devinfo->{$devname}; + if (defined($opts->{storage})) { + $storeid = $opts->{storage} || 'local'; + } elsif (!$storeid) { + $storeid = 'local'; + } + $format = 'raw' if !$format; + $devinfo->{$devname}->{devname} = $devname; + $devinfo->{$devname}->{virtdev} = $virtdev; + $devinfo->{$devname}->{format} = $format; + $devinfo->{$devname}->{storeid} = $storeid; + + # check permission on storage + my $pool = $opts->{pool}; # todo: do we need that? + if ($user ne 'root@pam') { + $rpcenv->check($user, "/storage/$storeid", ['Datastore.AllocateSpace']); + } + + $virtdev_hash->{$virtdev} = $devinfo->{$devname}; + } + } + + foreach my $devname (keys %$devinfo) { + die "found no device mapping information for device '$devname'\n" + if !$devinfo->{$devname}->{virtdev}; + } + + my $map = {}; + my $cfg = cfs_read_file('storage.cfg'); + foreach my $virtdev (sort keys %$virtdev_hash) { + my $d = $virtdev_hash->{$virtdev}; + my $alloc_size = int(($d->{size} + 1024 - 1)/1024); + my $scfg = PVE::Storage::storage_config($cfg, $d->{storeid}); + my $volid = PVE::Storage::vdisk_alloc($cfg, $d->{storeid}, $vmid, + $d->{format}, undef, $alloc_size); + print STDERR "new volume ID is '$volid'\n"; + $d->{volid} = $volid; + my $path = PVE::Storage::path($cfg, $volid); + + my $write_zeros = 1; + # fixme: what other storages types initialize volumes with zero? + if ($scfg->{type} eq 'dir' || $scfg->{type} eq 'nfs') { + $write_zeros = 0; + } + + print $fifofh "${write_zeros}:$d->{devname}=$path\n"; + + print "map '$d->{devname}' to '$path' (write zeros = ${write_zeros})\n"; + $map->{$virtdev} = $volid; + } + + $fh->seek(0, 0) || die "seek failed - $!\n"; + + my $outfd = new IO::File ($tmpfn, "w") || + die "unable to write config for VM $vmid\n"; + + my $cookie = { netcount => 0 }; + while (defined(my $line = <$fh>)) { + restore_update_config_line($outfd, $cookie, $vmid, $map, $line, $opts->{unique}); + } + + $fh->close(); + $outfd->close(); + }; + + eval { + # enable interrupts + local $SIG{INT} = $SIG{TERM} = $SIG{QUIT} = $SIG{HUP} = $SIG{PIPE} = sub { + die "interrupted by signal\n"; + }; + local $SIG{ALRM} = sub { die "got timeout\n"; }; + + $oldtimeout = alarm($timeout); + + my $parser = sub { + my $line = shift; + + print "$line\n"; + + if ($line =~ m/^DEV:\sdev_id=(\d+)\ssize:\s(\d+)\sdevname:\s(\S+)$/) { + my ($dev_id, $size, $devname) = ($1, $2, $3); + $devinfo->{$devname} = { size => $size, dev_id => $dev_id }; + } elsif ($line =~ m/^CTIME: /) { + &$print_devmap(); + print $fifofh "done\n"; + my $tmp = $oldtimeout || 0; + $oldtimeout = undef; + alarm($tmp); + close($fifofh); + } + }; + + print "restore vma archive: $cmd\n"; + run_command($cmd, input => $input, outfunc => $parser, afterfork => $openfifo); + }; + my $err = $@; + + alarm($oldtimeout) if $oldtimeout; + + unlink $mapfifo; + + if ($err) { + rmtree $tmpdir; + unlink $tmpfn; + + my $cfg = cfs_read_file('storage.cfg'); + foreach my $devname (keys %$devinfo) { + my $volid = $devinfo->{$devname}->{volid}; + next if !$volid; + eval { + if ($volid =~ m|^/|) { + unlink $volid || die 'unlink failed\n'; + } else { + PVE::Storage::vdisk_free($cfg, $volid); + } + print STDERR "temporary volume '$volid' sucessfuly removed\n"; + }; + print STDERR "unable to cleanup '$volid' - $@" if $@; + } + die $err; + } + + rmtree $tmpdir; + + rename $tmpfn, $conffile || + die "unable to commit configuration file '$conffile'\n"; + + eval { rescan($vmid, 1); }; + warn $@ if $@; +} + +sub restore_tar_archive { + my ($archive, $vmid, $user, $opts) = @_; + if ($archive ne '-') { my $firstfile = archive_read_firstfile($archive); die "ERROR: file '$archive' dos not lock like a QemuServer vzdump backup\n" @@ -3517,50 +3880,9 @@ sub restore_archive { my $outfd = new IO::File ($tmpfn, "w") || die "unable to write config for VM $vmid\n"; - my $netcount = 0; - + my $cookie = { netcount => 0 }; while (defined (my $line = <$srcfd>)) { - next if $line =~ m/^\#vzdump\#/; - next if $line =~ m/^lock:/; - next if $line =~ m/^unused\d+:/; - - if (($line =~ m/^(vlan(\d+)):\s*(\S+)\s*$/)) { - # try to convert old 1.X settings - my ($id, $ind, $ethcfg) = ($1, $2, $3); - foreach my $devconfig (PVE::Tools::split_list($ethcfg)) { - my ($model, $macaddr) = split(/\=/, $devconfig); - $macaddr = PVE::Tools::random_ether_addr() if !$macaddr || $opts->{unique}; - my $net = { - model => $model, - bridge => "vmbr$ind", - macaddr => $macaddr, - }; - my $netstr = print_net($net); - print $outfd "net${netcount}: $netstr\n"; - $netcount++; - } - } elsif (($line =~ m/^(net\d+):\s*(\S+)\s*$/) && ($opts->{unique})) { - my ($id, $netstr) = ($1, $2); - my $net = parse_net($netstr); - $net->{macaddr} = PVE::Tools::random_ether_addr() if $net->{macaddr}; - $netstr = print_net($net); - print $outfd "$id: $netstr\n"; - } elsif ($line =~ m/^((ide|scsi|virtio|sata)\d+):\s*(\S+)\s*$/) { - my $virtdev = $1; - my $value = $2; - if ($line =~ m/backup=no/) { - print $outfd "#$line"; - } elsif ($virtdev && $map->{$virtdev}) { - my $di = PVE::QemuServer::parse_drive($virtdev, $value); - $di->{file} = $map->{$virtdev}; - $value = PVE::QemuServer::print_drive($vmid, $di); - print $outfd "$virtdev: $value\n"; - } else { - print $outfd $line; - } - } else { - print $outfd $line; - } + restore_update_config_line($outfd, $cookie, $vmid, $map, $line, $opts->{unique}); } $srcfd->close(); @@ -3581,6 +3903,9 @@ sub restore_archive { rename $tmpfn, $conffile || die "unable to commit configuration file '$conffile'\n"; + + eval { rescan($vmid, 1); }; + warn $@ if $@; }; @@ -3826,10 +4151,8 @@ sub snapshot_rollback { if (!$prepare && $snap->{vmstate}) { my $statefile = PVE::Storage::path($storecfg, $snap->{vmstate}); - # fixme: this only forws for files currently vm_start($storecfg, $vmid, $statefile); } - }; lock_config($vmid, $updatefn); diff --git a/PVE/VZDump/QemuServer.pm b/PVE/VZDump/QemuServer.pm index b2bbc324..0e902bd9 100644 --- a/PVE/VZDump/QemuServer.pm +++ b/PVE/VZDump/QemuServer.pm @@ -6,12 +6,14 @@ 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); @@ -46,22 +48,15 @@ sub prepare { my $conf = $self->{vmlist}->{$vmid} = PVE::QemuServer::load_config($vmid); - if (scalar(keys %{$conf->{snapshots}})) { - die "VM contains snapshots - unable to backup\n"; + $self->{vm_was_running} = 1; + if (!PVE::QemuServer::check_running($vmid)) { + $self->{vm_was_running} = 0; } $task->{hostname} = $conf->{name}; - my $lvmmap = PVE::VZDump::get_lvm_mapping(); - my $hostname = PVE::INotify::nodename(); - my $ind = {}; - my $mountinfo = {}; - my $mountind = 0; - - my $snapshot_count = 0; - my $vollist = []; my $drivehash = {}; PVE::QemuServer::foreach_drive($conf, sub { @@ -101,86 +96,19 @@ sub prepare { die "no such volume '$volid'\n" if ! -e $path; + my ($size, $format) = PVE::Storage::Plugin::file_size_info($path); + my $diskinfo = { path => $path , volid => $volid, storeid => $storeid, - snappath => $path, virtdev => $ds }; + format => $format, virtdev => $ds, qmdevice => "drive-$ds" }; if (-b $path) { - $diskinfo->{type} = 'block'; - - $diskinfo->{filename} = "vm-disk-$ds.raw"; - - if ($mode eq 'snapshot') { - my ($lvmvg, $lvmlv) = @{$lvmmap->{$path}} if defined ($lvmmap->{$path}); - die ("mode failure - unable to detect lvm volume group\n") if !$lvmvg; - - $ind->{$lvmvg} = 0 if !defined $ind->{$lvmvg}; - $diskinfo->{snapname} = "vzsnap-$hostname-$ind->{$lvmvg}"; - $diskinfo->{snapdev} = "/dev/$lvmvg/$diskinfo->{snapname}"; - $diskinfo->{lvmvg} = $lvmvg; - $diskinfo->{lvmlv} = $lvmlv; - $diskinfo->{snappath} = $diskinfo->{snapdev}; - $ind->{$lvmvg}++; - - $snapshot_count++; - } - } else { - $diskinfo->{type} = 'file'; - - my (undef, $dir, $ext) = fileparse ($path, qr/\.[^.]*/); - - $diskinfo->{filename} = "vm-disk-$ds$ext"; - - if ($mode eq 'snapshot') { - - 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; - - $ind->{$lvmvg} = 0 if !defined $ind->{$lvmvg}; - - my $info = $mountinfo->{$lvmpath}; - if (!$info) { - my $snapname = "vzsnap-$hostname-$ind->{$lvmvg}"; - my $snapdev = "/dev/$lvmvg/$snapname"; - $mountinfo->{$lvmpath} = $info = { - snapdev => $snapdev, - snapname => $snapname, - mountpoint => "/mnt/vzsnap$mountind", - }; - $ind->{$lvmvg}++; - $mountind++; - - $snapshot_count++; - } - - $diskinfo->{snapdev} = $info->{snapdev}; - $diskinfo->{snapname} = $info->{snapname}; - $diskinfo->{mountpoint} = $info->{mountpoint}; - - $diskinfo->{lvmvg} = $lvmvg; - $diskinfo->{lvmlv} = $lvmlv; - - $diskinfo->{fstype} = $fstype; - $diskinfo->{lvmpath} = $lvmpath; - - $diskinfo->{snappath} = $path; - $diskinfo->{snappath} =~ s|/?$lvmpath/?|$diskinfo->{mountpoint}/|; - } } push @{$task->{disks}}, $diskinfo; } - - $task->{snapshot_count} = $snapshot_count; } sub vm_status { @@ -231,123 +159,6 @@ sub resume_vm { $self->cmd ("qm resume $vmid --skiplock"); } -sub snapshot_alloc { - my ($self, $storeid, $name, $size, $srcdev) = @_; - - my $cmd = "lvcreate --size ${size}M --snapshot --name '$name' '$srcdev'"; - - if ($storeid) { - - my $scfg = PVE::Storage::storage_config($self->{storecfg}, $storeid); - - # lock shared storage - return PVE::Storage::Plugin->cluster_lock_storage($storeid, $scfg->{shared}, undef, sub { - $self->cmd ($cmd); - }); - } else { - $self->cmd ($cmd); - } -} - -sub snapshot_free { - my ($self, $storeid, $name, $snapdev, $noerr) = @_; - - my $cmd = ['lvremove', '-f', $snapdev]; - - # loop, because we often get 'LV in use: not deactivating' - # we use run_command() because we do not want to log errors here - my $wait = 1; - while(-b $snapdev) { - eval { - if ($storeid) { - my $scfg = PVE::Storage::storage_config($self->{storecfg}, $storeid); - # lock shared storage - return PVE::Storage::Plugin->cluster_lock_storage($storeid, $scfg->{shared}, undef, sub { - PVE::Tools::run_command($cmd, outfunc => sub {}, errfunc => sub {}); - }); - } else { - PVE::Tools::run_command($cmd, outfunc => sub {}, errfunc => sub {}); - } - }; - my $err = $@; - last if !$err; - if ($wait >= 64) { - $self->logerr($err); - die $@ if !$noerr; - last; - } - $self->loginfo("lvremove failed - trying again in $wait seconds") if $wait >= 8; - sleep($wait); - $wait = $wait*2; - } -} - -sub snapshot { - my ($self, $task, $vmid) = @_; - - my $opts = $self->{vzdump}->{opts}; - - my $mounts = {}; - - foreach my $di (@{$task->{disks}}) { - if ($di->{type} eq 'block') { - - if (-b $di->{snapdev}) { - $self->loginfo ("trying to remove stale snapshot '$di->{snapdev}'"); - $self->snapshot_free ($di->{storeid}, $di->{snapname}, $di->{snapdev}, 1); - } - - $di->{cleanup_lvm} = 1; - $self->snapshot_alloc ($di->{storeid}, $di->{snapname}, $opts->{size}, - "/dev/$di->{lvmvg}/$di->{lvmlv}"); - - } elsif ($di->{type} eq 'file') { - - next if defined ($mounts->{$di->{mountpoint}}); # already mounted - - if (-b $di->{snapdev}) { - $self->loginfo ("trying to remove stale snapshot '$di->{snapdev}'"); - - $self->cmd_noerr ("umount $di->{mountpoint}"); - $self->snapshot_free ($di->{storeid}, $di->{snapname}, $di->{snapdev}, 1); - } - - mkpath $di->{mountpoint}; # create mount point for lvm snapshot - - $di->{cleanup_lvm} = 1; - - $self->snapshot_alloc ($di->{storeid}, $di->{snapname}, $opts->{size}, - "/dev/$di->{lvmvg}/$di->{lvmlv}"); - - my $mopts = $di->{fstype} eq 'xfs' ? "-o nouuid" : ''; - - $di->{snapshot_mount} = 1; - - $self->cmd ("mount -n -t $di->{fstype} $mopts $di->{snapdev} $di->{mountpoint}"); - - $mounts->{$di->{mountpoint}} = 1; - - } else { - die "implement me"; - } - } -} - -sub get_size { - my $path = shift; - - if (-f $path) { - return -s $path; - } elsif (-b $path) { - my $fh = IO::File->new ($path, "r"); - die "unable to open '$path' to detect device size\n" if !$fh; - my $size = sysseek $fh, 0, 2; - $fh->close(); - die "unable to detect device size for '$path'\n" if !$size; - return $size; - } -} - sub assemble { my ($self, $task, $vmid) = @_; @@ -365,20 +176,36 @@ sub assemble { $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 $size = get_size ($di->{snappath}); my $storeid = $di->{storeid} || ''; - print $outfd "#vzdump#map:$di->{virtdev}:$di->{filename}:$size:$storeid:\n"; + 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 = $@; @@ -397,53 +224,243 @@ sub archive { my $starttime = time (); - my $fh; + my $speed = 0; + if ($opts->{bwlimit}) { + $speed = $opts->{bwlimit}*1024; + } - my @filea = ($conffile, 'qemu-server.conf'); # always first file in tar + my $devlist = ''; foreach my $di (@{$task->{disks}}) { if ($di->{type} eq 'block' || $di->{type} eq 'file') { - push @filea, $di->{snappath}, $di->{filename}; + $devlist .= $devlist ? ",$di->{qmdevice}" : $di->{qmdevice}; } else { die "implement me"; } } - my $files = join (' ', map { "'$_'" } @filea); - - # no sparse file scan when we use compression - my $sparse = $comp ? '' : '-s'; + my $stop_after_backup; + my $resume_on_backup; - my $cmd = "/usr/lib/qemu-server/vmtar $sparse $files"; - my $bwl = $opts->{bwlimit}*1024; # bandwidth limit for cstream - $cmd .= "|cstream -t $bwl" if $opts->{bwlimit}; - $cmd .= "|$comp" if $comp; + my $skiplock = 1; - if ($opts->{stdout}) { - $self->cmd ($cmd, output => ">&=" . fileno($opts->{stdout})); - } else { - $self->cmd ("$cmd >$filename"); + if (!PVE::QemuServer::check_running($vmid)) { + 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 = $@) { + warn $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', + backupfile => "/dev/fdname/backup", + speed => $speed, + 'config-filename' => $conffile, + devlist => $devlist); + }; + + + $qmpclient->queue_cmd($vmid, $add_fd_cb, 'getfd', + fd => $outfileno, fdname => "backup"); + $qmpclient->queue_execute(); + + 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}; + $transferred = $status->{transferred}; + 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} 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"; + if ($status->{status} ne 'active') { + $self->loginfo($statusline); + die(($status->{errmsg} || "unknown error") . "\n") + if $status->{status} eq 'error'; + 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 ($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("stoping 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) { + $self->loginfo("aborting backup job"); + eval { PVE::QemuServer::vm_mon_cmd($vmid, 'backup_cancel'); }; + warn $@ if $@; + 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) = @_; - foreach my $di (@{$task->{disks}}) { - - if ($di->{snapshot_mount}) { - $self->cmd_noerr ("umount $di->{mountpoint}"); - } - - if ($di->{cleanup_lvm}) { - if (-b $di->{snapdev}) { - if ($di->{type} eq 'block') { - $self->snapshot_free ($di->{storeid}, $di->{snapname}, $di->{snapdev}, 1); - } elsif ($di->{type} eq 'file') { - $self->snapshot_free ($di->{storeid}, $di->{snapname}, $di->{snapdev}, 1); - } - } - } - } + # nothing to do ? } 1; diff --git a/changelog.Debian b/changelog.Debian index 4104956b..d32fa8aa 100644 --- a/changelog.Debian +++ b/changelog.Debian @@ -1,3 +1,9 @@ +qemu-server (2.3-1) unstable; urgency=low + + * include new qemu backup feature + + -- Proxmox Support Team Wed, 12 Dec 2012 15:14:59 +0100 + qemu-server (2.0-71) unstable; urgency=low * show better error message if bridge does not exist diff --git a/qm b/qm index 25f84ea0..76bc2296 100755 --- a/qm +++ b/qm @@ -318,70 +318,7 @@ __PACKAGE__->register_method ({ code => sub { my ($param) = @_; - my $cfg = PVE::Cluster::cfs_read_file("storage.cfg"); - - my $info = PVE::Storage::vdisk_list($cfg, undef, $param->{vmid}); - - my $volid_hash = {}; - foreach my $storeid (keys %$info) { - foreach my $item (@{$info->{$storeid}}) { - next if !($item->{volid} && $item->{size}); - $volid_hash->{$item->{volid}} = $item; - } - } - - my $updatefn = sub { - my ($vmid) = @_; - - my $conf = PVE::QemuServer::load_config($vmid); - - PVE::QemuServer::check_lock($conf); - - my $changes; - - my $used = {}; - - # update size info - foreach my $opt (keys %$conf) { - if (PVE::QemuServer::valid_drivename($opt)) { - my $drive = PVE::QemuServer::parse_drive($opt, $conf->{$opt}); - my $volid = $drive->{file}; - next if !$volid; - - $used->{$volid} = 1; - - next if PVE::QemuServer::drive_is_cdrom($drive); - next if !$volid_hash->{$volid}; - - $drive->{size} = $volid_hash->{$volid}->{size}; - $changes = 1; - $conf->{$opt} = PVE::QemuServer::print_drive($vmid, $drive); - } - } - - # add unused volumes - foreach my $storeid (keys %$info) { - foreach my $item (@{$info->{$storeid}}) { - next if !($item->{volid} && $item->{vmid}); - next if $item->{vmid} ne $vmid; - next if $item->{volid} =~ m/vm-$vmid-state-/; - next if $used->{$item->{volid}}; - $changes = 1; - PVE::QemuServer::add_unused_volume($conf, $item->{volid}); - } - } - - PVE::QemuServer::update_config_nolock($vmid, $conf, 1) if $changes; - }; - - if (defined($param->{vmid})) { - PVE::QemuServer::lock_config($param->{vmid}, $updatefn, $param->{vmid}); - } else { - my $vmlist = PVE::QemuServer::config_list(); - foreach my $vmid (keys %$vmlist) { - PVE::QemuServer::lock_config($vmid, $updatefn, $vmid); - } - } + PVE::QemuServer::rescan($param->{vmid}); return undef; }});