]> git.proxmox.com Git - qemu-server.git/blobdiff - PVE/QemuServer.pm
vmconfig_apply_pending: add error handling
[qemu-server.git] / PVE / QemuServer.pm
index 6267cae09538413f2b014c8a80d94e9c508e5da3..2de83768d3957ca3473a91b4a5997fa34aef367f 100644 (file)
@@ -108,7 +108,11 @@ sub cgroups_write {
 
 }
 
-my $nodename = PVE::INotify::nodename();
+my $nodename_cache;
+sub nodename {
+    $nodename_cache //= PVE::INotify::nodename();
+    return $nodename_cache;
+}
 
 my $cpu_vendor_list = {
     # Intel CPUs
@@ -1751,7 +1755,7 @@ sub parse_drive {
 }
 
 sub print_drive {
-    my ($vmid, $drive) = @_;
+    my ($drive) = @_;
     my $data = { %$drive };
     delete $data->{$_} for qw(index interface);
     return PVE::JSONSchema::print_property_string($data, $alldrive_fmt);
@@ -2247,9 +2251,7 @@ sub parse_hostpci {
     delete $res->{host};
     foreach my $id (@idlist) {
        my $devs = PVE::SysFSTools::lspci($id);
-       if (!scalar(@$devs)) {
-           die "no pci device found for '$id'\n";
-       }
+       die "no PCI device found for '$id'\n" if !scalar(@$devs);
        push @{$res->{pciid}}, @$devs;
     }
     return $res;
@@ -2665,7 +2667,7 @@ sub parse_vm_config {
                    my $v = parse_drive($key, $value);
                    if (my $volid = filename_to_volume_id($vmid, $v->{file}, $v->{media})) {
                        $v->{file} = $volid;
-                       $value = print_drive($vmid, $v);
+                       $value = print_drive($v);
                    } else {
                        warn "vm $vmid - unable to parse value of '$key'\n";
                        next;
@@ -2807,6 +2809,7 @@ sub config_list {
     my $res = {};
     return $res if !$vmlist || !$vmlist->{ids};
     my $ids = $vmlist->{ids};
+    my $nodename = nodename();
 
     foreach my $vmid (keys %$ids) {
        my $d = $ids->{$vmid};
@@ -2865,7 +2868,7 @@ sub shared_nodes {
 
     my $nodelist = PVE::Cluster::get_nodelist();
     my $nodehash = { map { $_ => 1 } @$nodelist };
-    my $nodename = PVE::INotify::nodename();
+    my $nodename = nodename();
 
     foreach_drive($conf, sub {
        my ($ds, $drive) = @_;
@@ -3463,6 +3466,7 @@ sub config_to_command {
     my $ostype = $conf->{ostype};
     my $winversion = windows_version($ostype);
     my $kvm = $conf->{kvm};
+    my $nodename = nodename();
 
     my $arch = get_vm_arch($conf);
     my $kvm_binary = get_command_for_arch($arch);
@@ -3474,6 +3478,10 @@ sub config_to_command {
     my $machine_version = PVE::QemuServer::Machine::extract_version($machine_type, $kvmver);
     $kvm //= 1 if is_native($arch);
 
+    $machine_version =~ m/(\d+)\.(\d+)/;
+    die "Installed QEMU version '$kvmver' is too old to run machine type '$machine_type', please upgrade node '$nodename'\n"
+       if !PVE::QemuServer::min_version($kvmver, $1, $2);
+
     if ($kvm) {
        die "KVM virtualisation configured, but not available. Either disable in VM configuration or enable in BIOS.\n"
            if !defined kvm_version();
@@ -3882,7 +3890,6 @@ sub config_to_command {
 
        my $pciaddr = print_pci_addr("spice", $bridges, $arch, $machine_type);
 
-       my $nodename = PVE::INotify::nodename();
        my $pfamily = PVE::Tools::get_host_address_family($nodename);
        my @nodeaddrs = PVE::Tools::getaddrinfo_all('localhost', family => $pfamily);
        die "failed to get an ip address of type $pfamily for 'localhost'\n" if !@nodeaddrs;
@@ -4772,7 +4779,6 @@ sub vmconfig_hotplug_pending {
 
     if ($changes) {
        PVE::QemuConfig->write_config($vmid, $conf);
-       $conf = PVE::QemuConfig->load_config($vmid); # update/reload
     }
 
     my $hotplug_features = parse_hotplug_features(defined($conf->{hotplug}) ? $conf->{hotplug} : '1');
@@ -4832,11 +4838,8 @@ sub vmconfig_hotplug_pending {
        if (my $err = $@) {
            &$add_error($opt, $err) if $err ne "skip\n";
        } else {
-           # save new config if hotplug was successful
            delete $conf->{$opt};
            PVE::QemuConfig->remove_from_pending_delete($conf, $opt);
-           PVE::QemuConfig->write_config($vmid, $conf);
-           $conf = PVE::QemuConfig->load_config($vmid); # update/reload
        }
     }
 
@@ -4924,13 +4927,12 @@ sub vmconfig_hotplug_pending {
        if (my $err = $@) {
            &$add_error($opt, $err) if $err ne "skip\n";
        } else {
-           # save new config if hotplug was successful
            $conf->{$opt} = $value;
            delete $conf->{pending}->{$opt};
-           PVE::QemuConfig->write_config($vmid, $conf);
-           $conf = PVE::QemuConfig->load_config($vmid); # update/reload
        }
     }
+
+    PVE::QemuConfig->write_config($vmid, $conf);
 }
 
 sub try_deallocate_drive {
@@ -4975,23 +4977,39 @@ sub vmconfig_delete_or_detach_drive {
 
 
 sub vmconfig_apply_pending {
-    my ($vmid, $conf, $storecfg) = @_;
+    my ($vmid, $conf, $storecfg, $errors) = @_;
+
+    my $add_apply_error = sub {
+       my ($opt, $msg) = @_;
+       my $err_msg = "unable to apply pending change $opt : $msg";
+       $errors->{$opt} = $err_msg;
+       warn $err_msg;
+    };
 
     # cold plug
 
     my $pending_delete_hash = PVE::QemuConfig->parse_pending_delete($conf->{pending}->{delete});
     foreach my $opt (sort keys %$pending_delete_hash) {
-       die "internal error" if $opt =~ m/^unused/;
        my $force = $pending_delete_hash->{$opt}->{force};
-       $conf = PVE::QemuConfig->load_config($vmid); # update/reload
-       if (!defined($conf->{$opt})) {
-           PVE::QemuConfig->remove_from_pending_delete($conf, $opt);
-           PVE::QemuConfig->write_config($vmid, $conf);
-       } elsif (is_valid_drivename($opt)) {
-           vmconfig_delete_or_detach_drive($vmid, $storecfg, $conf, $opt, $force);
-           PVE::QemuConfig->remove_from_pending_delete($conf, $opt);
-           delete $conf->{$opt};
-           PVE::QemuConfig->write_config($vmid, $conf);
+       eval {
+           die "internal error" if $opt =~ m/^unused/;
+           $conf = PVE::QemuConfig->load_config($vmid); # update/reload
+           if (!defined($conf->{$opt})) {
+               PVE::QemuConfig->remove_from_pending_delete($conf, $opt);
+               PVE::QemuConfig->write_config($vmid, $conf);
+           } elsif (is_valid_drivename($opt)) {
+               vmconfig_delete_or_detach_drive($vmid, $storecfg, $conf, $opt, $force);
+               PVE::QemuConfig->remove_from_pending_delete($conf, $opt);
+               delete $conf->{$opt};
+               PVE::QemuConfig->write_config($vmid, $conf);
+           } else {
+               PVE::QemuConfig->remove_from_pending_delete($conf, $opt);
+               delete $conf->{$opt};
+               PVE::QemuConfig->write_config($vmid, $conf);
+           }
+       };
+       if (my $err = $@) {
+           $add_apply_error->($opt, $err);
        } else {
            PVE::QemuConfig->remove_from_pending_delete($conf, $opt);
            delete $conf->{$opt};
@@ -5004,17 +5022,24 @@ sub vmconfig_apply_pending {
     foreach my $opt (keys %{$conf->{pending}}) { # add/change
        $conf = PVE::QemuConfig->load_config($vmid); # update/reload
 
-       if (defined($conf->{$opt}) && ($conf->{$opt} eq $conf->{pending}->{$opt})) {
-           # skip if nothing changed
-       } elsif (is_valid_drivename($opt)) {
-           vmconfig_register_unused_drive($storecfg, $vmid, $conf, parse_drive($opt, $conf->{$opt}))
-               if defined($conf->{$opt});
-           $conf->{$opt} = $conf->{pending}->{$opt};
+       eval {
+           if (defined($conf->{$opt}) && ($conf->{$opt} eq $conf->{pending}->{$opt})) {
+               # skip if nothing changed
+           } elsif (is_valid_drivename($opt)) {
+               vmconfig_register_unused_drive($storecfg, $vmid, $conf, parse_drive($opt, $conf->{$opt}))
+                   if defined($conf->{$opt});
+               $conf->{$opt} = $conf->{pending}->{$opt};
+           } else {
+               $conf->{$opt} = $conf->{pending}->{$opt};
+           }
+       };
+       if (my $err = $@) {
+           $add_apply_error->($opt, $err);
        } else {
-           $conf->{$opt} = $conf->{pending}->{$opt};
+           $conf->{$opt} = delete $conf->{pending}->{$opt};
+           PVE::QemuConfig->cleanup_pending($conf);
        }
 
-       delete $conf->{pending}->{$opt};
        PVE::QemuConfig->write_config($vmid, $conf);
     }
 }
@@ -5265,7 +5290,7 @@ sub vm_start {
                my $newdrive = $drive;
                $newdrive->{format} = $format;
                $newdrive->{file} = $newvolid;
-               my $drivestr = PVE::QemuServer::print_drive($vmid, $newdrive);
+               my $drivestr = print_drive($newdrive);
                $local_volumes->{$opt} = $drivestr;
                #pass drive to conf for command line
                $conf->{$opt} = $drivestr;
@@ -5316,7 +5341,7 @@ sub vm_start {
            if ($statefile eq 'tcp') {
                my $localip = "localhost";
                my $datacenterconf = PVE::Cluster::cfs_read_file('datacenter.cfg');
-               my $nodename = PVE::INotify::nodename();
+               my $nodename = nodename();
 
                if (!defined($migration_type)) {
                    if (defined($datacenterconf->{migration}->{type})) {
@@ -5398,7 +5423,18 @@ sub vm_start {
                                                  : $defaults->{cpuunits};
 
        my $start_timeout = ($conf->{hugepages} || $is_suspended) ? 300 : 30;
-       my %run_params = (timeout => $statefile ? undef : $start_timeout, umask => 0077);
+       my %run_params = (
+           timeout => $statefile ? undef : $start_timeout,
+           umask => 0077,
+           noerr => 1,
+       );
+
+       # when migrating, prefix QEMU output so other side can pick up any
+       # errors that might occur and show the user
+       if ($migratedfrom) {
+           $run_params{quiet} = 1;
+           $run_params{logfunc} = sub { print "QEMU: $_[0]\n" };
+       }
 
        my %properties = (
            Slice => 'qemu.slice',
@@ -5414,7 +5450,9 @@ sub vm_start {
        my $run_qemu = sub {
            PVE::Tools::run_fork sub {
                PVE::Systemd::enter_systemd_scope($vmid, "Proxmox VE VM $vmid", %properties);
-               run_command($cmd, %run_params);
+
+               my $exitcode = run_command($cmd, %run_params);
+               die "QEMU exited with code $exitcode\n" if $exitcode;
            };
        };
 
@@ -5456,7 +5494,7 @@ sub vm_start {
 
        #start nbd server for storage migration
        if ($targetstorage) {
-           my $nodename = PVE::INotify::nodename();
+           my $nodename = nodename();
            my $localip = $get_migration_ip->($migration_network, $nodename);
            my $pfamily = PVE::Tools::get_host_address_family($nodename);
            my $storage_migrate_port = PVE::Tools::next_migrate_port($pfamily);
@@ -5735,6 +5773,7 @@ sub vm_reboot {
    });
 }
 
+# note: if using the statestorage parameter, the caller has to check privileges
 sub vm_suspend {
     my ($vmid, $skiplock, $includestate, $statestorage) = @_;
 
@@ -5758,6 +5797,17 @@ sub vm_suspend {
            $conf->{lock} = 'suspending';
            my $date = strftime("%Y-%m-%d", localtime(time()));
            $storecfg = PVE::Storage::config();
+           if (!$statestorage) {
+               $statestorage = find_vmstate_storage($conf, $storecfg);
+               # check permissions for the storage
+               my $rpcenv = PVE::RPCEnvironment::get();
+               if ($rpcenv->{type} ne 'cli') {
+                   my $authuser = $rpcenv->get_user();
+                   $rpcenv->check($authuser, "/storage/$statestorage", ['Datastore.AllocateSpace']);
+               }
+           }
+
+
            $vmstate = PVE::QemuConfig->__snapshot_save_vmstate($vmid, $conf, "suspend-$date", $storecfg, $statestorage, 1);
            $path = PVE::Storage::path($storecfg, $vmstate);
            PVE::QemuConfig->write_config($vmid, $conf);
@@ -5973,7 +6023,7 @@ sub restore_update_config_line {
        } elsif ($map->{$virtdev}) {
            delete $di->{format}; # format can change on restore
            $di->{file} = $map->{$virtdev};
-           $value = print_drive($vmid, $di);
+           $value = print_drive($di);
            print $outfd "$virtdev: $value\n";
        } else {
            print $outfd $line;
@@ -6056,6 +6106,27 @@ sub is_volume_in_use {
 }
 
 sub update_disksize {
+    my ($drive, $volid_hash) = @_;
+
+    my $volid = $drive->{file};
+    return undef if !defined($volid);
+
+    my $oldsize = $drive->{size};
+    my $newsize = $volid_hash->{$volid}->{size};
+
+    if (defined($newsize) && defined($oldsize) && $newsize != $oldsize) {
+       $drive->{size} = $newsize;
+
+       my $old_fmt = PVE::JSONSchema::format_size($oldsize);
+       my $new_fmt = PVE::JSONSchema::format_size($newsize);
+
+       return wantarray ? ($drive, $old_fmt, $new_fmt) : $drive;
+    }
+
+    return undef;
+}
+
+sub update_disk_config {
     my ($vmid, $conf, $volid_hash) = @_;
 
     my $changes;
@@ -6077,6 +6148,7 @@ sub update_disksize {
            my $volid = $drive->{file};
            next if !$volid;
 
+           # mark volid as "in-use" for next step
            $referenced->{$volid} = 1;
            if ($volid_hash->{$volid} &&
                (my $path = $volid_hash->{$volid}->{path})) {
@@ -6086,12 +6158,11 @@ sub update_disksize {
            next if drive_is_cdrom($drive);
            next if !$volid_hash->{$volid};
 
-           $drive->{size} = $volid_hash->{$volid}->{size};
-           my $new = print_drive($vmid, $drive);
-           if ($new ne $conf->{$opt}) {
+           my ($updated, $old_size, $new_size) = update_disksize($drive, $volid_hash);
+           if (defined($updated)) {
                $changes = 1;
-               $conf->{$opt} = $new;
-               print "$prefix update disk '$opt' information.\n";
+               $conf->{$opt} = print_drive($updated);
+               print "$prefix size of disk '$volid' ($opt) updated from $old_size to $new_size\n";
            }
        }
     }
@@ -6102,7 +6173,7 @@ sub update_disksize {
        my $volid = $conf->{$opt};
        my $path = $volid_hash->{$volid}->{path} if $volid_hash->{$volid};
        if ($referenced->{$volid} || ($path && $referencedpath->{$path})) {
-           print "$prefix remove entry '$opt', its volume '$volid' is in use.\n";
+           print "$prefix remove entry '$opt', its volume '$volid' is in use\n";
            $changes = 1;
            delete $conf->{$opt};
        }
@@ -6119,7 +6190,7 @@ sub update_disksize {
        next if $referencedpath->{$path};
        $changes = 1;
        my $key = PVE::QemuConfig->add_unused_volume($conf, $volid);
-       print "$prefix add unreferenced volume '$volid' as '$key' to config.\n";
+       print "$prefix add unreferenced volume '$volid' as '$key' to config\n";
        $referencedpath->{$path} = 1; # avoid to add more than once (aliases)
     }
 
@@ -6153,7 +6224,7 @@ sub rescan {
            $vm_volids->{$volid} = $info if $info->{vmid} && $info->{vmid} == $vmid;
        }
 
-       my $changes = update_disksize($vmid, $conf, $vm_volids);
+       my $changes = update_disk_config($vmid, $conf, $vm_volids);
 
        PVE::QemuConfig->write_config($vmid, $conf) if $changes && !$dryrun;
     };
@@ -6253,7 +6324,7 @@ sub restore_vma_archive {
     my $conffile = PVE::QemuConfig->config_file($vmid);
     my $tmpfn = "$conffile.$$.tmp";
 
-    # Note: $oldconf is undef if VM does not exists
+    # Note: $oldconf is undef if VM does not exist
     my $cfs_path = PVE::QemuConfig->cfs_config_path($vmid);
     my $oldconf = PVE::Cluster::cfs_read_file($cfs_path);
 
@@ -6685,7 +6756,7 @@ sub template_create {
 
        my $voliddst = PVE::Storage::vdisk_create_base($storecfg, $volid);
        $drive->{file} = $voliddst;
-       $conf->{$ds} = print_drive($vmid, $drive);
+       $conf->{$ds} = print_drive($drive);
        PVE::QemuConfig->write_config($vmid, $conf);
     });
 }
@@ -7203,6 +7274,30 @@ sub resolve_first_disk {
     return $firstdisk;
 }
 
+# NOTE: if this logic changes, please update docs & possibly gui logic
+sub find_vmstate_storage {
+    my ($conf, $storecfg) = @_;
+
+    # first, return storage from conf if set
+    return $conf->{vmstatestorage} if $conf->{vmstatestorage};
+
+    my ($target, $shared, $local);
+
+    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';
+
+    return $target;
+}
+
 sub generate_uuid {
     my ($uuid, $uuid_str);
     UUID::generate($uuid);