]> git.proxmox.com Git - qemu-server.git/blobdiff - PVE/VZDump/QemuServer.pm
suspend: continue cleanup even if savevm-end QMP command fails
[qemu-server.git] / PVE / VZDump / QemuServer.pm
index 30baa464cd3ff3a1ae87ede474448110b3225bde..8c97ee622b58b59d305eff8c488b5d33105f5799 100644 (file)
@@ -26,6 +26,7 @@ use PVE::Format qw(render_duration render_bytes);
 
 use PVE::QemuConfig;
 use PVE::QemuServer;
+use PVE::QemuServer::Helpers;
 use PVE::QemuServer::Machine;
 use PVE::QemuServer::Monitor qw(mon_cmd);
 
@@ -67,7 +68,9 @@ sub prepare {
     $self->{vm_was_paused} = 0;
     if (!PVE::QemuServer::check_running($vmid)) {
        $self->{vm_was_running} = 0;
-    } elsif (PVE::QemuServer::vm_is_paused($vmid)) {
+    } elsif (PVE::QemuServer::vm_is_paused($vmid, 0)) {
+       # Do not treat a suspended VM as paused, as it would cause us to skip
+       # fs-freeze even if the VM wakes up before we reach qga_fs_freeze.
        $self->{vm_was_paused} = 1;
     }
 
@@ -119,13 +122,26 @@ sub prepare {
        }
        next if !$path;
 
-       my ($size, $format) = eval { PVE::Storage::volume_size_info($self->{storecfg}, $volid, 5) };
-       die "no such volume '$volid'\n" if $@;
+       my ($size, $format);
+       if ($storeid) {
+           # The call in list context can be expensive for certain plugins like RBD, just get size
+           $size = eval { PVE::Storage::volume_size_info($self->{storecfg}, $volid, 5) };
+           die "cannot determine size of volume '$volid' - $@\n" if $@;
+
+           my $scfg = PVE::Storage::storage_config($self->{storecfg}, $storeid);
+           $format = PVE::QemuServer::qemu_img_format($scfg, $volname);
+       } else {
+           ($size, $format) = eval {
+               PVE::Storage::volume_size_info($self->{storecfg}, $volid, 5);
+           };
+           die "cannot determine size and format of volume '$volid' - $@\n" if $@;
+       }
 
        my $diskinfo = {
            path => $path,
            volid => $volid,
            storeid => $storeid,
+           size => $size,
            format => $format,
            virtdev => $ds,
            qmdevice => "drive-$ds",
@@ -208,10 +224,8 @@ sub assemble {
     my $firewall_src = "/etc/pve/firewall/$vmid.fw";
     my $firewall_dest = "$task->{tmpdir}/qemu-server.fw";
 
-    my $outfd = IO::File->new (">$outfile") ||
-       die "unable to open '$outfile'";
-    my $conffd = IO::File->new ($conffile, 'r') ||
-       die "unable open '$conffile'";
+    my $outfd = IO::File->new(">$outfile") or die "unable to open '$outfile' - $!\n";
+    my $conffd = IO::File->new($conffile, 'r') or die "unable to open '$conffile' - $!\n";
 
     my $found_snapshot;
     my $found_pending;
@@ -477,6 +491,156 @@ my sub add_backup_performance_options {
     }
 }
 
+sub get_and_check_pbs_encryption_config {
+    my ($self) = @_;
+
+    my $opts = $self->{vzdump}->{opts};
+    my $scfg = $opts->{scfg};
+
+    my $keyfile = PVE::Storage::PBSPlugin::pbs_encryption_key_file_name($scfg, $opts->{storage});
+    my $master_keyfile = PVE::Storage::PBSPlugin::pbs_master_pubkey_file_name($scfg, $opts->{storage});
+
+    if (-e $keyfile) {
+       if (-e $master_keyfile) {
+           $self->loginfo("enabling encryption with master key feature");
+           return ($keyfile, $master_keyfile);
+       } elsif ($scfg->{'master-pubkey'}) {
+           die "master public key configured but no key file found\n";
+       } else {
+           $self->loginfo("enabling encryption");
+           return ($keyfile, undef);
+       }
+    } else {
+       my $encryption_fp = $scfg->{'encryption-key'};
+       die "encryption configured ('$encryption_fp') but no encryption key file found!\n"
+           if $encryption_fp;
+       if (-e $master_keyfile) {
+           $self->log(
+               'warn',
+               "backup target storage is configured with master-key, but no encryption key set!"
+               ." Ignoring master key settings and creating unencrypted backup."
+           );
+       }
+       return (undef, undef);
+    }
+    die "internal error - unhandled case for getting & checking PBS encryption ($keyfile, $master_keyfile)!";
+}
+
+my sub cleanup_fleecing_images {
+    my ($self, $disks) = @_;
+
+    for my $di ($disks->@*) {
+       if (my $volid = $di->{'fleece-volid'}) {
+           eval { PVE::Storage::vdisk_free($self->{storecfg}, $volid); };
+           $self->log('warn', "error removing fleecing image '$volid' - $@") if $@;
+       }
+    }
+}
+
+my sub allocate_fleecing_images {
+    my ($self, $disks, $vmid, $fleecing_storeid, $format) = @_;
+
+    die "internal error - no fleecing storage specified\n" if !$fleecing_storeid;
+
+    # TODO what about potential left-over images from a failed attempt? Just
+    # auto-remove? While unlikely, could conflict with manually created image from user...
+
+    eval {
+       my $n = 0; # counter for fleecing image names
+
+       for my $di ($disks->@*) {
+           next if $di->{virtdev} =~ m/^(?:tpmstate|efidisk)\d$/; # too small to be worth it
+           if ($di->{type} eq 'block' || $di->{type} eq 'file') {
+               my $scfg = PVE::Storage::storage_config($self->{storecfg}, $fleecing_storeid);
+               my $name = "vm-$vmid-fleece-$n";
+               $name .= ".$format" if $scfg->{path};
+
+               my $size = PVE::Tools::convert_size($di->{size}, 'b' => 'kb');
+
+               $di->{'fleece-volid'} = PVE::Storage::vdisk_alloc(
+                   $self->{storecfg}, $fleecing_storeid, $vmid, $format, $name, $size);
+
+               $n++;
+           } else {
+               die "implement me (type '$di->{type}')";
+           }
+       }
+    };
+    if (my $err = $@) {
+       cleanup_fleecing_images($self, $disks);
+       die $err;
+    }
+}
+
+my sub detach_fleecing_images {
+    my ($disks, $vmid) = @_;
+
+    return if !PVE::QemuServer::Helpers::vm_running_locally($vmid);
+
+    for my $di ($disks->@*) {
+       if (my $volid = $di->{'fleece-volid'}) {
+           my $devid = "$di->{qmdevice}-fleecing";
+           $devid =~ s/^drive-//; # re-added by qemu_drivedel()
+           eval { PVE::QemuServer::qemu_drivedel($vmid, $devid) };
+       }
+    }
+}
+
+my sub attach_fleecing_images {
+    my ($self, $disks, $vmid, $format) = @_;
+
+    # unconditionally try to remove potential left-overs from a previous backup
+    detach_fleecing_images($disks, $vmid);
+
+    my $vollist = [ map { $_->{'fleece-volid'} } grep { $_->{'fleece-volid'} } $disks->@* ];
+    PVE::Storage::activate_volumes($self->{storecfg}, $vollist);
+
+    for my $di ($disks->@*) {
+       if (my $volid = $di->{'fleece-volid'}) {
+           $self->loginfo("$di->{qmdevice}: attaching fleecing image $volid to QEMU");
+
+           my $path = PVE::Storage::path($self->{storecfg}, $volid);
+           my $devid = "$di->{qmdevice}-fleecing";
+           my $drive = "file=$path,if=none,id=$devid,format=$format,discard=unmap";
+           # Specify size explicitly, to make it work if storage backend rounded up size for
+           # fleecing image when allocating.
+           $drive .= ",size=$di->{size}" if $format eq 'raw';
+           $drive =~ s/\\/\\\\/g;
+           my $ret = PVE::QemuServer::Monitor::hmp_cmd($vmid, "drive_add auto \"$drive\"");
+           die "attaching fleecing image $volid failed - $ret\n" if $ret !~ m/OK/s;
+       }
+    }
+}
+
+my sub check_and_prepare_fleecing {
+    my ($self, $vmid, $fleecing_opts, $disks, $is_template, $qemu_support) = @_;
+
+    # Even if the VM was started specifically for fleecing, it's possible that the VM is resumed and
+    # then starts doing IO. For VMs that are not resumed the fleecing images will just stay empty,
+    # so there is no big cost.
+
+    my $use_fleecing = $fleecing_opts && $fleecing_opts->{enabled} && !$is_template;
+
+    if ($use_fleecing && !defined($qemu_support->{'backup-fleecing'})) {
+       $self->log(
+           'warn',
+           "running QEMU version does not support backup fleecing - continuing without",
+       );
+       $use_fleecing = 0;
+    }
+
+    if ($use_fleecing) {
+       my ($default_format, $valid_formats) = PVE::Storage::storage_default_format(
+           $self->{storecfg}, $fleecing_opts->{storage});
+       my $format = scalar(grep { $_ eq 'qcow2' } $valid_formats->@*) ? 'qcow2' : 'raw';
+
+       allocate_fleecing_images($self, $disks, $vmid, $fleecing_opts->{storage}, $format);
+       attach_fleecing_images($self, $disks, $vmid, $format);
+    }
+
+    return $use_fleecing;
+}
+
 sub archive_pbs {
     my ($self, $task, $vmid) = @_;
 
@@ -491,14 +655,12 @@ sub archive_pbs {
     my $fingerprint = $scfg->{fingerprint};
     my $repo = PVE::PBSClient::get_repository($scfg);
     my $password = PVE::Storage::PBSPlugin::pbs_get_password($scfg, $opts->{storage});
-    my $keyfile = PVE::Storage::PBSPlugin::pbs_encryption_key_file_name($scfg, $opts->{storage});
-    my $master_keyfile = PVE::Storage::PBSPlugin::pbs_master_pubkey_file_name($scfg, $opts->{storage});
+    my ($keyfile, $master_keyfile) = $self->get_and_check_pbs_encryption_config();
 
     my $diskcount = scalar(@{$task->{disks}});
-    # proxmox-backup-client can only handle raw files and block devs
-    # only use it (directly) for disk-less VMs
+    # proxmox-backup-client can only handle raw files and block devs, so only use it (directly) for
+    # disk-less VMs
     if (!$diskcount) {
-       my @pathlist;
        $self->loginfo("backup contains no disks");
 
        local $ENV{PBS_PASSWORD} = $password;
@@ -514,6 +676,10 @@ sub archive_pbs {
        if (defined(my $ns = $scfg->{namespace})) {
            push @$cmd, '--ns', $ns;
        }
+       if (defined($keyfile)) {
+           push @$cmd, '--keyfile', $keyfile;
+           push @$cmd, '--master-pubkey-file', $master_keyfile if defined($master_keyfile);
+       }
 
        push @$cmd, "qemu-server.conf:$conffile";
        push @$cmd, "fw.conf:$firewall" if -e $firewall;
@@ -528,6 +694,7 @@ sub archive_pbs {
 
     # get list early so we die on unkown drive types before doing anything
     my $devlist = _get_task_devlist($task);
+    my $use_fleecing;
 
     $self->enforce_vm_running_for_backup($vmid);
     $self->{qmeventd_fh} = PVE::QemuServer::register_qmeventd_handle($vmid);
@@ -550,14 +717,17 @@ sub archive_pbs {
            }
        }
 
-       if (!defined($qemu_support->{"pbs-masterkey"}) && -e $master_keyfile) {
-           $self->loginfo("WARNING: backup target is configured with master key, but running QEMU version does not support master keys.");
-           $self->loginfo("Please make sure you've installed the latest version and the VM has been restarted to use master key feature.");
-           $master_keyfile = undef; # skip rest of master key handling below
-       }
+       # pve-qemu supports it since 5.2.0-1 (PVE 6.4), so safe to die since PVE 8
+       die "master key configured but running QEMU version does not support master keys\n"
+           if !defined($qemu_support->{'pbs-masterkey'}) && defined($master_keyfile);
 
        $attach_tpmstate_drive->($self, $task, $vmid);
 
+       my $is_template = PVE::QemuConfig->is_template($self->{vmlist}->{$vmid});
+
+       $use_fleecing = check_and_prepare_fleecing(
+           $self, $vmid, $opts->{fleecing}, $task->{disks}, $is_template, $qemu_support);
+
        my $fs_frozen = $self->qga_fs_freeze($task, $vmid);
 
        my $params = {
@@ -569,6 +739,8 @@ sub archive_pbs {
            devlist => $devlist,
            'config-file' => $conffile,
        };
+       $params->{fleecing} = JSON::true if $use_fleecing;
+
        if (defined(my $ns = $scfg->{namespace})) {
            $params->{'backup-ns'} = $ns;
        }
@@ -578,28 +750,13 @@ sub archive_pbs {
 
        $params->{fingerprint} = $fingerprint if defined($fingerprint);
        $params->{'firewall-file'} = $firewall if -e $firewall;
-       if (-e $keyfile) {
-           $self->loginfo("enabling encryption");
+
+       $params->{encrypt} = defined($keyfile) ? JSON::true : JSON::false;
+       if (defined($keyfile)) {
            $params->{keyfile} = $keyfile;
-           $params->{encrypt} = JSON::true;
-           if (defined($master_keyfile)) {
-               if (-e $master_keyfile) {
-                   $self->loginfo("enabling master key feature");
-                   $params->{"master-keyfile"} = $master_keyfile;
-               } elsif ($scfg->{'master-pubkey'}) {
-                   die "master public key configured but no key file found\n";
-               }
-           }
-       } else {
-           my $encryption_fp = $scfg->{'encryption-key'};
-           die "encryption configured ('$encryption_fp') but no encryption key file found!\n"
-               if $encryption_fp;
-           $self->loginfo("WARNING: backup target is configured with master key, but this backup is not encrypted - master key settings will be ignored!")
-               if defined($master_keyfile) && -e $master_keyfile;
-           $params->{encrypt} = JSON::false;
+           $params->{"master-keyfile"} = $master_keyfile if defined($master_keyfile);
        }
 
-       my $is_template = PVE::QemuConfig->is_template($self->{vmlist}->{$vmid});
        $params->{'use-dirty-bitmap'} = JSON::true
            if $qemu_support->{'pbs-dirty-bitmap'} && !$is_template;
 
@@ -631,6 +788,11 @@ sub archive_pbs {
     }
     $self->restore_vm_power_state($vmid);
 
+    if ($use_fleecing) {
+       detach_fleecing_images($task->{disks}, $vmid);
+       cleanup_fleecing_images($self, $task->{disks});
+    }
+
     die $err if $err;
 }
 
@@ -690,8 +852,10 @@ sub archive_vma {
        $speed = $opts->{bwlimit}*1024;
     }
 
+    my $is_template = PVE::QemuConfig->is_template($self->{vmlist}->{$vmid});
+
     my $diskcount = scalar(@{$task->{disks}});
-    if (PVE::QemuConfig->is_template($self->{vmlist}->{$vmid}) || !$diskcount) {
+    if ($is_template || !$diskcount) {
        my @pathlist;
        foreach my $di (@{$task->{disks}}) {
            if ($di->{type} eq 'block' || $di->{type} eq 'file') {
@@ -731,6 +895,7 @@ sub archive_vma {
     }
 
     my $devlist = _get_task_devlist($task);
+    my $use_fleecing;
 
     $self->enforce_vm_running_for_backup($vmid);
     $self->{qmeventd_fh} = PVE::QemuServer::register_qmeventd_handle($vmid);
@@ -750,6 +915,9 @@ sub archive_vma {
 
        $attach_tpmstate_drive->($self, $task, $vmid);
 
+       $use_fleecing = check_and_prepare_fleecing(
+           $self, $vmid, $opts->{fleecing}, $task->{disks}, $is_template, $qemu_support);
+
        my $outfh;
        if ($opts->{stdout}) {
            $outfh = $opts->{stdout};
@@ -778,6 +946,7 @@ sub archive_vma {
                devlist => $devlist
            };
            $params->{'firewall-file'} = $firewall if -e $firewall;
+           $params->{fleecing} = JSON::true if $use_fleecing;
            add_backup_performance_options($params, $opts->{performance}, $qemu_support);
 
            $qmpclient->queue_cmd($vmid, $backup_cb, 'backup', %$params);
@@ -819,6 +988,11 @@ sub archive_vma {
 
     $self->restore_vm_power_state($vmid);
 
+    if ($use_fleecing) {
+       detach_fleecing_images($task->{disks}, $vmid);
+       cleanup_fleecing_images($self, $task->{disks});
+    }
+
     if ($err) {
        if ($cpid) {
            kill(9, $cpid);