]> git.proxmox.com Git - qemu-server.git/blobdiff - PVE/QemuConfig.pm
suspend: continue cleanup even if savevm-end QMP command fails
[qemu-server.git] / PVE / QemuConfig.pm
index 9a29b53872b877dbfda4606b8b9ab99594a4126d..8e8a7828e10e7f0ce2f57318e25d506755f46b89 100644 (file)
@@ -5,22 +5,40 @@ use warnings;
 
 use PVE::AbstractConfig;
 use PVE::INotify;
+use PVE::JSONSchema;
+use PVE::QemuServer::CPUConfig;
+use PVE::QemuServer::Drive;
+use PVE::QemuServer::Helpers;
+use PVE::QemuServer::Monitor qw(mon_cmd);
 use PVE::QemuServer;
+use PVE::QemuServer::Machine;
+use PVE::QemuServer::Memory qw(get_current_memory);
 use PVE::Storage;
 use PVE::Tools;
+use PVE::Format qw(render_bytes render_duration);
 
 use base qw(PVE::AbstractConfig);
 
 my $nodename = PVE::INotify::nodename();
 
 mkdir "/etc/pve/nodes/$nodename";
-my $confdir = "/etc/pve/nodes/$nodename/qemu-server";
-mkdir $confdir;
+mkdir "/etc/pve/nodes/$nodename/qemu-server";
 
 my $lock_dir = "/var/lock/qemu-server";
 mkdir $lock_dir;
 
-my $MAX_UNUSED_DISKS = 8;
+sub assert_config_exists_on_node {
+    my ($vmid, $node) = @_;
+
+    $node //= $nodename;
+
+    my $filename = __PACKAGE__->config_file($vmid, $node);
+    my $exists = -f $filename;
+
+    my $type = guest_type();
+    die "unable to find configuration file for $type $vmid on node '$node'\n"
+       if !$exists;
+}
 
 # BEGIN implemented abstract methods from PVE::AbstractConfig
 
@@ -31,7 +49,7 @@ sub guest_type {
 sub __config_max_unused_disks {
     my ($class) = @_;
 
-    return $MAX_UNUSED_DISKS;
+    return $PVE::QemuServer::Drive::MAX_UNUSED_DISKS;
 }
 
 sub config_file_lock {
@@ -51,7 +69,7 @@ sub has_feature {
     my ($class, $feature, $conf, $storecfg, $snapname, $running, $backup_only) = @_;
 
     my $err;
-    PVE::QemuServer::foreach_drive($conf, sub {
+    $class->foreach_volume($conf, sub {
        my ($ds, $drive) = @_;
 
        return if PVE::QemuServer::drive_is_cdrom($drive);
@@ -63,6 +81,47 @@ sub has_feature {
     return $err ? 0 : 1;
 }
 
+sub valid_volume_keys {
+    my ($class, $reverse) = @_;
+
+    my @keys = PVE::QemuServer::Drive::valid_drive_names();
+
+    return $reverse ? reverse @keys : @keys;
+}
+
+# FIXME: adapt parse_drive to use $noerr for better error messages
+sub parse_volume {
+    my ($class, $key, $volume_string, $noerr) = @_;
+
+    my $volume;
+    if ($key eq 'vmstate') {
+       eval { PVE::JSONSchema::check_format('pve-volume-id', $volume_string) };
+       if (my $err = $@) {
+           return if $noerr;
+           die $err;
+       }
+       $volume = { 'file' => $volume_string };
+    } else {
+       $volume = PVE::QemuServer::Drive::parse_drive($key, $volume_string);
+    }
+
+    die "unable to parse volume\n" if !defined($volume) && !$noerr;
+
+    return $volume;
+}
+
+sub print_volume {
+    my ($class, $key, $volume) = @_;
+
+    return PVE::QemuServer::Drive::print_drive($volume);
+}
+
+sub volid_key {
+    my ($class) = @_;
+
+    return 'file';
+}
+
 sub get_replicatable_volumes {
     my ($class, $storecfg, $vmid, $conf, $cleanup, $noerr) = @_;
 
@@ -105,55 +164,105 @@ sub get_replicatable_volumes {
 
     PVE::QemuServer::foreach_volid($conf, $test_volid);
 
-    # add 'unusedX' volumes to volhash
-    foreach my $key (keys %$conf) {
-       if ($key =~ m/^unused/) {
-           $test_volid->($conf->{$key}, { replicate => 1 });
+    return $volhash;
+}
+
+sub get_backup_volumes {
+    my ($class, $conf) = @_;
+
+    my $return_volumes = [];
+
+    my $test_volume = sub {
+       my ($key, $drive) = @_;
+
+       return if PVE::QemuServer::drive_is_cdrom($drive);
+
+       my $included = $drive->{backup} // 1;
+       my $reason = "backup=";
+       $reason .= defined($drive->{backup}) ? 'no' : 'yes';
+
+       if ($key =~ m/^efidisk/ && (!defined($conf->{bios}) || $conf->{bios} ne 'ovmf')) {
+           $included = 0;
+           $reason = "efidisk but no OMVF BIOS";
        }
-    }
 
-    return $volhash;
+       push @$return_volumes, {
+           key => $key,
+           included => $included,
+           reason => $reason,
+           volume_config => $drive,
+       };
+    };
+
+    PVE::QemuConfig->foreach_volume($conf, $test_volume);
+
+    return $return_volumes;
 }
 
 sub __snapshot_save_vmstate {
-    my ($class, $vmid, $conf, $snapname, $storecfg) = @_;
-
-    my $snap = $conf->{snapshots}->{$snapname};
+    my ($class, $vmid, $conf, $snapname, $storecfg, $statestorage, $suspend) = @_;
 
-    # first, use explicitly configured storage
-    my $target = $conf->{vmstatestorage};
+    # use given storage or search for one from the config
+    my $target = $statestorage;
 
     if (!$target) {
-       my ($shared, $local);
-       PVE::QemuServer::foreach_storage_used_by_vm($conf, sub {
-           my ($sid) = @_;
-           my $scfg = PVE::Storage::storage_config($storecfg, $sid);
-           my $dst = $scfg->{shared} ? \$shared : \$local;
-           $$dst = $sid if !$$dst || $scfg->{path}; # prefer file based storage
-       });
-
-       # second, use shared storage where VM has at least one disk
-       # third, use local storage where VM has at least one disk
-       # fall back to local storage
-       $target = $shared // $local // 'local';
+       $target = PVE::QemuServer::find_vmstate_storage($conf, $storecfg);
     }
 
-    my $driver_state_size = 500; # assume 32MB is enough to safe all driver state;
-    # we abort live save after $conf->{memory}, so we need at max twice that space
-    my $size = $conf->{memory}*2 + $driver_state_size;
+    my $mem_size = get_current_memory($conf->{memory});
+    my $driver_state_size = 500; # assume 500MB is enough to safe all driver state;
+    # our savevm-start does live-save of the memory until the space left in the
+    # volume is just enough for the remaining memory content + internal state
+    # then it stops the vm and copies the rest so we reserve twice the
+    # memory content + state to minimize vm downtime
+    my $size = $mem_size*2 + $driver_state_size;
+    my $scfg = PVE::Storage::storage_config($storecfg, $target);
 
     my $name = "vm-$vmid-state-$snapname";
-    my $scfg = PVE::Storage::storage_config($storecfg, $target);
     $name .= ".raw" if $scfg->{path}; # add filename extension for file base storage
-    $snap->{vmstate} = PVE::Storage::vdisk_alloc($storecfg, $target, $vmid, 'raw', $name, $size*1024);
-    # always overwrite machine if we save vmstate. This makes sure we
-    # can restore it later using correct machine type
-    $snap->{machine} = PVE::QemuServer::get_current_qemu_machine($vmid);
+
+    my $statefile = PVE::Storage::vdisk_alloc($storecfg, $target, $vmid, 'raw', $name, $size*1024);
+    my $runningmachine = PVE::QemuServer::Machine::get_current_qemu_machine($vmid);
+
+    # get current QEMU -cpu argument to ensure consistency of custom CPU models
+    my $runningcpu;
+    if (my $pid = PVE::QemuServer::check_running($vmid)) {
+       $runningcpu = PVE::QemuServer::CPUConfig::get_cpu_from_running_vm($pid);
+    }
+
+    if (!$suspend) {
+       $conf = $conf->{snapshots}->{$snapname};
+    }
+
+    $conf->{vmstate} = $statefile;
+    $conf->{runningmachine} = $runningmachine;
+    $conf->{runningcpu} = $runningcpu;
+
+    return $statefile;
+}
+
+sub __snapshot_activate_storages {
+    my ($class, $conf, $include_vmstate) = @_;
+
+    my $storecfg = PVE::Storage::config();
+    my $opts = $include_vmstate ? { 'extra_keys' => ['vmstate'] } : {};
+    my $storage_hash = {};
+
+    $class->foreach_volume_full($conf, $opts, sub {
+       my ($key, $drive) = @_;
+
+       return if PVE::QemuServer::drive_is_cdrom($drive);
+
+       my ($storeid) = PVE::Storage::parse_volume_id($drive->{file});
+       $storage_hash->{$storeid} = 1;
+    });
+
+    PVE::Storage::activate_storage_list($storecfg, [ sort keys $storage_hash->%* ]);
 }
 
 sub __snapshot_check_running {
     my ($class, $vmid) = @_;
-    return PVE::QemuServer::check_running($vmid);
+    return PVE::QemuServer::Helpers::vm_running_locally($vmid);
 }
 
 sub __snapshot_check_freeze_needed {
@@ -161,7 +270,7 @@ sub __snapshot_check_freeze_needed {
 
     my $running = $class->__snapshot_check_running($vmid);
     if (!$save_vmstate) {
-       return ($running, $running && $config->{agent} && PVE::QemuServer::qga_check_running($vmid));
+       return ($running, $running && PVE::QemuServer::parse_guest_agent($config)->{enabled} && PVE::QemuServer::qga_check_running($vmid));
     } else {
        return ($running, 0);
     }
@@ -171,10 +280,10 @@ sub __snapshot_freeze {
     my ($class, $vmid, $unfreeze) = @_;
 
     if ($unfreeze) {
-       eval { PVE::QemuServer::vm_mon_cmd($vmid, "guest-fsfreeze-thaw"); };
+       eval { mon_cmd($vmid, "guest-fsfreeze-thaw"); };
        warn "guest-fsfreeze-thaw problems - $@" if $@;
     } else {
-       eval { PVE::QemuServer::vm_mon_cmd($vmid, "guest-fsfreeze-freeze"); };
+       eval { mon_cmd($vmid, "guest-fsfreeze-freeze"); };
        warn "guest-fsfreeze-freeze problems - $@" if $@;
     }
 }
@@ -189,34 +298,55 @@ sub __snapshot_create_vol_snapshots_hook {
            if ($snap->{vmstate}) {
                my $path = PVE::Storage::path($storecfg, $snap->{vmstate});
                PVE::Storage::activate_volumes($storecfg, [$snap->{vmstate}]);
-
-               PVE::QemuServer::vm_mon_cmd($vmid, "savevm-start", statefile => $path);
+               my $state_storage_id = PVE::Storage::parse_volume_id($snap->{vmstate});
+
+               PVE::QemuServer::set_migration_caps($vmid, 1);
+               mon_cmd($vmid, "savevm-start", statefile => $path);
+               print "saving VM state and RAM using storage '$state_storage_id'\n";
+               my $render_state = sub {
+                   my ($stat) = @_;
+                   my $b = render_bytes($stat->{bytes});
+                   my $t = render_duration($stat->{'total-time'} / 1000);
+                   return ($b, $t);
+               };
+               my $round = 0;
                for(;;) {
-                   my $stat = PVE::QemuServer::vm_mon_cmd_nocheck($vmid, "query-savevm");
+                   $round++;
+                   my $stat = mon_cmd($vmid, "query-savevm");
                    if (!$stat->{status}) {
                        die "savevm not active\n";
                    } elsif ($stat->{status} eq 'active') {
+                       if ($round < 60 || $round % 10 == 0) {
+                           my ($b, $t) = $render_state->($stat);
+                           print "$b in $t\n";
+                       }
+                       print "reducing reporting rate to every 10s\n" if $round == 60;
                        sleep(1);
                        next;
                    } elsif ($stat->{status} eq 'completed') {
+                       my ($b, $t) = $render_state->($stat);
+                       print "completed saving the VM state in $t, saved $b\n";
                        last;
+                   } elsif ($stat->{status} eq 'failed') {
+                       my $err = $stat->{error} || 'unknown error';
+                       die "unable to save VM state and RAM - $err\n";
                    } else {
-                       die "query-savevm returned status '$stat->{status}'\n";
+                       die "query-savevm returned unexpected status '$stat->{status}'\n";
                    }
                }
            } else {
-               PVE::QemuServer::vm_mon_cmd($vmid, "savevm-start");
+               mon_cmd($vmid, "savevm-start");
            }
        } elsif ($hook eq "after") {
-           eval { 
-               PVE::QemuServer::vm_mon_cmd($vmid, "savevm-end");
+           eval {
+               mon_cmd($vmid, "savevm-end");
                PVE::Storage::deactivate_volumes($storecfg, [$snap->{vmstate}]) if $snap->{vmstate};
            };
            warn $@ if $@;
        } elsif ($hook eq "after-freeze") {
            # savevm-end is async, we need to wait
            for (;;) {
-               my $stat = PVE::QemuServer::vm_mon_cmd_nocheck($vmid, "query-savevm");
+               my $stat = mon_cmd($vmid, "query-savevm");
                if (!$stat->{bytes}) {
                    last;
                } else {
@@ -238,6 +368,8 @@ sub __snapshot_create_vol_snapshot {
     my $device = "drive-$ds";
     my $storecfg = PVE::Storage::config();
 
+    print "snapshotting '$device' ($drive->{file})\n";
+
     PVE::QemuServer::qemu_volume_snapshot($vmid, $device, $storecfg, $volid, $snapname);
 }
 
@@ -274,22 +406,60 @@ sub __snapshot_delete_vol_snapshot {
     return if PVE::QemuServer::drive_is_cdrom($drive);
     my $storecfg = PVE::Storage::config();
     my $volid = $drive->{file};
-    my $device = "drive-$ds";
 
-    PVE::QemuServer::qemu_volume_snapshot_delete($vmid, $device, $storecfg, $volid, $snapname);
+    PVE::QemuServer::qemu_volume_snapshot_delete($vmid, $storecfg, $volid, $snapname);
 
     push @$unused, $volid;
 }
 
+sub __snapshot_rollback_hook {
+    my ($class, $vmid, $conf, $snap, $prepare, $data) = @_;
+
+    if ($prepare) {
+       # we save the machine of the current config
+       $data->{oldmachine} = $conf->{machine};
+    } else {
+       # if we have a 'runningmachine' entry in the snapshot we use that
+       # for the forcemachine parameter, else we use the old logic
+       if (defined($conf->{runningmachine})) {
+           $data->{forcemachine} = $conf->{runningmachine};
+           delete $conf->{runningmachine};
+
+           # runningcpu is newer than runningmachine, so assume it only exists
+           # here, if at all
+           $data->{forcecpu} = delete $conf->{runningcpu}
+               if defined($conf->{runningcpu});
+       } else {
+           # Note: old code did not store 'machine', so we try to be smart
+           # and guess the snapshot was generated with kvm 1.4 (pc-i440fx-1.4).
+           my $machine_conf = PVE::QemuServer::Machine::parse_machine($conf->{machine});
+           $data->{forcemachine} = $machine_conf->{type} || 'pc-i440fx-1.4';
+
+           # we remove the 'machine' configuration if not explicitly specified
+           # in the original config.
+           delete $conf->{machine} if $snap->{vmstate} && !defined($data->{oldmachine});
+       }
+
+       if ($conf->{vmgenid}) {
+           # tell the VM that it's another generation, so it can react
+           # appropriately, e.g. dirty-mark copies of distributed databases or
+           # re-initializing its random number generator
+           $conf->{vmgenid} = PVE::QemuServer::generate_uuid();
+       }
+    }
+
+    return;
+}
+
 sub __snapshot_rollback_vol_possible {
-    my ($class, $drive, $snapname) = @_;
+    my ($class, $drive, $snapname, $blockers) = @_;
 
     return if PVE::QemuServer::drive_is_cdrom($drive);
 
     my $storecfg = PVE::Storage::config();
     my $volid = $drive->{file};
 
-    PVE::Storage::volume_rollback_is_possible($storecfg, $volid, $snapname);
+    PVE::Storage::volume_rollback_is_possible($storecfg, $volid, $snapname, $blockers);
 }
 
 sub __snapshot_rollback_vol_rollback {
@@ -309,11 +479,15 @@ sub __snapshot_rollback_vm_stop {
 }
 
 sub __snapshot_rollback_vm_start {
-    my ($class, $vmid, $vmstate, $forcemachine) = @_;
+    my ($class, $vmid, $vmstate, $data) = @_;
 
     my $storecfg = PVE::Storage::config();
-    my $statefile = PVE::Storage::path($storecfg, $vmstate);
-    PVE::QemuServer::vm_start($storecfg, $vmid, $statefile, undef, undef, undef, $forcemachine);
+    my $params = {
+       statefile => $vmstate,
+       forcemachine => $data->{forcemachine},
+       forcecpu => $data->{forcecpu},
+    };
+    PVE::QemuServer::vm_start($storecfg, $vmid, $params);
 }
 
 sub __snapshot_rollback_get_unused {
@@ -321,19 +495,19 @@ sub __snapshot_rollback_get_unused {
 
     my $unused = [];
 
-    $class->__snapshot_foreach_volume($conf, sub {
+    $class->foreach_volume($conf, sub {
        my ($vs, $volume) = @_;
 
-       return if PVE::QemuServer::drive_is_cdrom($volume);
+       return if PVE::QemuServer::drive_is_cdrom($volume, 1);
 
        my $found = 0;
        my $volid = $volume->{file};
 
-       $class->__snapshot_foreach_volume($snap, sub {
+       $class->foreach_volume($snap, sub {
            my ($ds, $drive) = @_;
 
            return if $found;
-           return if PVE::QemuServer::drive_is_cdrom($drive);
+           return if PVE::QemuServer::drive_is_cdrom($drive, 1);
 
            $found = 1
                if ($drive->{file} && $drive->{file} eq $volid);
@@ -345,11 +519,58 @@ sub __snapshot_rollback_get_unused {
     return $unused;
 }
 
-sub __snapshot_foreach_volume {
-    my ($class, $conf, $func) = @_;
+sub add_unused_volume {
+    my ($class, $config, $volid) = @_;
+
+    if ($volid =~ m/vm-\d+-cloudinit/) {
+       print "found unused cloudinit disk '$volid', removing it\n";
+       my $storecfg = PVE::Storage::config();
+       PVE::Storage::vdisk_free($storecfg, $volid);
+       return undef;
+    } else {
+        return $class->SUPER::add_unused_volume($config, $volid);
+    }
+}
+
+sub load_current_config {
+    my ($class, $vmid, $current) = @_;
 
-    PVE::QemuServer::foreach_drive($conf, $func);
+    my $conf = $class->SUPER::load_current_config($vmid, $current);
+    delete $conf->{cloudinit};
+    return $conf;
 }
+
+sub get_derived_property {
+    my ($class, $conf, $name) = @_;
+
+    my $defaults = PVE::QemuServer::load_defaults();
+
+    if ($name eq 'max-cpu') {
+       my $cpus =
+           ($conf->{sockets} || $defaults->{sockets}) * ($conf->{cores} || $defaults->{cores});
+       return $conf->{vcpus} || $cpus;
+    } elsif ($name eq 'max-memory') { # current usage maximum, not maximum hotpluggable
+       return get_current_memory($conf->{memory}) * 1024 * 1024;
+    } else {
+       die "unknown derived property - $name\n";
+    }
+}
+
 # END implemented abstract methods from PVE::AbstractConfig
 
+sub has_cloudinit {
+    my ($class, $conf, $skip) = @_;
+
+    my $found;
+
+    $class->foreach_volume($conf, sub {
+       my ($key, $volume) = @_;
+
+       return if ($skip && $skip eq $key) || $found;
+       $found = $key if PVE::QemuServer::Drive::drive_is_cloudinit($volume);
+    });
+
+    return $found;
+}
+
 1;