]> git.proxmox.com Git - qemu-server.git/blobdiff - PVE/API2/Qemu.pm
fix #4522: api: vncproxy: also set environment variable for ticket without websocket
[qemu-server.git] / PVE / API2 / Qemu.pm
index c92734a6db5c5ad5bb7ba07ef60be36931c3779e..9877ce24a9b7e2e581d894881787e29aa51657d2 100644 (file)
@@ -32,6 +32,9 @@ use PVE::QemuServer::Drive;
 use PVE::QemuServer::ImportDisk;
 use PVE::QemuServer::Monitor qw(mon_cmd);
 use PVE::QemuServer::Machine;
+use PVE::QemuServer::Memory qw(get_current_memory);
+use PVE::QemuServer::PCI;
+use PVE::QemuServer::USB;
 use PVE::QemuMigrate;
 use PVE::RPCEnvironment;
 use PVE::AccessControl;
@@ -560,6 +563,7 @@ my $cloudinitoptions = {
     cipassword => 1,
     citype => 1,
     ciuser => 1,
+    ciupgrade => 1,
     nameserver => 1,
     searchdomain => 1,
     sshkeys => 1,
@@ -583,19 +587,64 @@ my $check_vm_create_serial_perm = sub {
     return 1;
 };
 
-my $check_vm_create_usb_perm = sub {
+my sub check_usb_perm {
+    my ($rpcenv, $authuser, $vmid, $pool, $opt, $value) = @_;
+
+    return 1 if $authuser eq 'root@pam';
+
+    $rpcenv->check_vm_perm($authuser, $vmid, $pool, ['VM.Config.HWType']);
+
+    my $device = PVE::JSONSchema::parse_property_string('pve-qm-usb', $value);
+    if ($device->{host} && $device->{host} !~ m/^spice$/i) {
+       die "only root can set '$opt' config for real devices\n";
+    } elsif ($device->{mapping}) {
+       $rpcenv->check_full($authuser, "/mapping/usb/$device->{mapping}", ['Mapping.Use']);
+    } else {
+       die "either 'host' or 'mapping' must be set.\n";
+    }
+
+    return 1;
+}
+
+my sub check_vm_create_usb_perm {
     my ($rpcenv, $authuser, $vmid, $pool, $param) = @_;
 
     return 1 if $authuser eq 'root@pam';
 
     foreach my $opt (keys %{$param}) {
        next if $opt !~ m/^usb\d+$/;
+       check_usb_perm($rpcenv, $authuser, $vmid, $pool, $opt, $param->{$opt});
+    }
 
-       if ($param->{$opt} =~ m/spice/) {
-           $rpcenv->check_vm_perm($authuser, $vmid, $pool, ['VM.Config.HWType']);
-       } else {
-           die "only root can set '$opt' config for real devices\n";
-       }
+    return 1;
+};
+
+my sub check_hostpci_perm {
+    my ($rpcenv, $authuser, $vmid, $pool, $opt, $value) = @_;
+
+    return 1 if $authuser eq 'root@pam';
+
+    my $device = PVE::JSONSchema::parse_property_string('pve-qm-hostpci', $value);
+    if ($device->{host}) {
+       die "only root can set '$opt' config for non-mapped devices\n";
+    } elsif ($device->{mapping}) {
+       $rpcenv->check_full($authuser, "/mapping/pci/$device->{mapping}", ['Mapping.Use']);
+       $rpcenv->check_vm_perm($authuser, $vmid, $pool, ['VM.Config.HWType']);
+    } else {
+       die "either 'host' or 'mapping' must be set.\n";
+    }
+
+    return 1;
+}
+
+my sub check_vm_create_hostpci_perm {
+    my ($rpcenv, $authuser, $vmid, $pool, $param) = @_;
+
+    return 1 if $authuser eq 'root@pam';
+
+    foreach my $opt (keys %{$param}) {
+       next if $opt !~ m/^hostpci\d+$/;
+       check_hostpci_perm($rpcenv, $authuser, $vmid, $pool, $opt, $param->{$opt});
     }
 
     return 1;
@@ -611,7 +660,7 @@ my $check_vm_modify_config_perm = sub {
        # else, as there the permission can be value dependend
        next if PVE::QemuServer::is_valid_drivename($opt);
        next if $opt eq 'cdrom';
-       next if $opt =~ m/^(?:unused|serial|usb)\d+$/;
+       next if $opt =~ m/^(?:unused|serial|usb|hostpci)\d+$/;
        next if $opt eq 'tags';
 
 
@@ -640,7 +689,7 @@ my $check_vm_modify_config_perm = sub {
            # also needs privileges on the storage, that will be checked later
            $rpcenv->check_vm_perm($authuser, $vmid, $pool, ['VM.Config.Disk', 'VM.PowerMgmt' ]);
        } else {
-           # catches hostpci\d+, args, lock, etc.
+           # catches args, lock, etc.
            # new options will be checked here
            die "only root can set '$opt' config\n";
        }
@@ -703,20 +752,20 @@ my $parse_restore_archive = sub {
 
     my ($archive_storeid, $archive_volname) = PVE::Storage::parse_volume_id($archive, 1);
 
+    my $res = {};
+
     if (defined($archive_storeid)) {
        my $scfg =  PVE::Storage::storage_config($storecfg, $archive_storeid);
+       $res->{volid} = $archive;
        if ($scfg->{type} eq 'pbs') {
-           return {
-               type => 'pbs',
-               volid => $archive,
-           };
+           $res->{type} = 'pbs';
+           return $res;
        }
     }
     my $path = PVE::Storage::abs_filesystem_path($storecfg, $archive);
-    return {
-       type => 'file',
-       path => $path,
-    };
+    $res->{type} = 'file';
+    $res->{path} = $path;
+    return $res;
 };
 
 
@@ -878,7 +927,9 @@ __PACKAGE__->register_method({
            &$check_vm_modify_config_perm($rpcenv, $authuser, $vmid, $pool, [ keys %$param]);
 
            &$check_vm_create_serial_perm($rpcenv, $authuser, $vmid, $pool, $param);
-           &$check_vm_create_usb_perm($rpcenv, $authuser, $vmid, $pool, $param);
+           check_vm_create_usb_perm($rpcenv, $authuser, $vmid, $pool, $param);
+           check_vm_create_hostpci_perm($rpcenv, $authuser, $vmid, $pool, $param);
+
            PVE::QemuServer::check_bridge_access($rpcenv, $authuser, $param);
            &$check_cpu_model_access($rpcenv, $authuser, $param);
 
@@ -909,6 +960,19 @@ __PACKAGE__->register_method({
                    live => $live_restore,
                    override_conf => $param,
                };
+               if (my $volid = $archive->{volid}) {
+                   # best effort, real check is after restoring!
+                   my $merged = eval {
+                       my $old_conf = PVE::Storage::extract_vzdump_config($storecfg, $volid);
+                       PVE::QemuServer::restore_merge_config("backup/qemu-server/$vmid.conf", $old_conf, $param);
+                   };
+                   if ($@) {
+                       warn "Could not extract backed up config: $@\n";
+                       warn "Skipping early checks!\n";
+                   } else {
+                       PVE::QemuServer::check_restore_permissions($rpcenv, $authuser, $merged);
+                   }
+               }
                if ($archive->{type} eq 'file' || $archive->{type} eq 'pipe') {
                    die "live-restore is only compatible with backup images from a Proxmox Backup Server\n"
                        if $live_restore;
@@ -1419,7 +1483,7 @@ __PACKAGE__->register_method({
     proxyto => 'node',
     description => "Regenerate and change cloudinit config drive.",
     permissions => {
-       check => ['perm', '/vms/{vmid}', 'VM.Config.Cloudinit'],
+       check => ['perm', '/vms/{vmid}', ['VM.Config.Cloudinit']],
     },
     parameters => {
        additionalProperties => 0,
@@ -1510,8 +1574,6 @@ my $update_vm_api  = sub {
 
     my $storecfg = PVE::Storage::config();
 
-    my $defaults = PVE::QemuServer::load_defaults();
-
     &$resolve_cdrom_alias($param);
 
     # now try to verify all parameters
@@ -1631,7 +1693,9 @@ my $update_vm_api  = sub {
        }
 
        if ($param->{memory} || defined($param->{balloon})) {
-           my $maxmem = $param->{memory} || $conf->{pending}->{memory} || $conf->{memory} || $defaults->{memory};
+
+           my $memory = $param->{memory} || $conf->{pending}->{memory} || $conf->{memory};
+           my $maxmem = get_current_memory($memory);
            my $balloon = defined($param->{balloon}) ? $param->{balloon} : $conf->{pending}->{balloon} || $conf->{balloon};
 
            die "balloon value too large (must be smaller than assigned memory)\n"
@@ -1719,17 +1783,27 @@ my $update_vm_api  = sub {
                    PVE::QemuConfig->add_to_pending_delete($conf, $opt, $force);
                    PVE::QemuConfig->write_config($vmid, $conf);
                } elsif ($opt =~ m/^usb\d+$/) {
-                   if ($val =~ m/spice/) {
-                       $rpcenv->check_vm_perm($authuser, $vmid, undef, ['VM.Config.HWType']);
-                   } elsif ($authuser ne 'root@pam') {
-                       die "only root can delete '$opt' config for real devices\n";
-                   }
+                   check_usb_perm($rpcenv, $authuser, $vmid, undef, $opt, $val);
+                   PVE::QemuConfig->add_to_pending_delete($conf, $opt, $force);
+                   PVE::QemuConfig->write_config($vmid, $conf);
+               } elsif ($opt =~ m/^hostpci\d+$/) {
+                   check_hostpci_perm($rpcenv, $authuser, $vmid, undef, $opt, $val);
                    PVE::QemuConfig->add_to_pending_delete($conf, $opt, $force);
                    PVE::QemuConfig->write_config($vmid, $conf);
                } elsif ($opt eq 'tags') {
                    assert_tag_permissions($vmid, $val, '', $rpcenv, $authuser);
                    delete $conf->{$opt};
                    PVE::QemuConfig->write_config($vmid, $conf);
+               } elsif ($opt =~ m/^net\d+$/) {
+                   if ($conf->{$opt}) {
+                       PVE::QemuServer::check_bridge_access(
+                           $rpcenv,
+                           $authuser,
+                           { $opt => $conf->{$opt} },
+                       );
+                   }
+                   PVE::QemuConfig->add_to_pending_delete($conf, $opt, $force);
+                   PVE::QemuConfig->write_config($vmid, $conf);
                } else {
                    PVE::QemuConfig->add_to_pending_delete($conf, $opt, $force);
                    PVE::QemuConfig->write_config($vmid, $conf);
@@ -1784,15 +1858,29 @@ my $update_vm_api  = sub {
                    }
                    $conf->{pending}->{$opt} = $param->{$opt};
                } elsif ($opt =~ m/^usb\d+/) {
-                   if ((!defined($conf->{$opt}) || $conf->{$opt} =~ m/spice/) && $param->{$opt} =~ m/spice/) {
-                       $rpcenv->check_vm_perm($authuser, $vmid, undef, ['VM.Config.HWType']);
-                   } elsif ($authuser ne 'root@pam') {
-                       die "only root can modify '$opt' config for real devices\n";
+                   if (my $olddevice = $conf->{$opt}) {
+                       check_usb_perm($rpcenv, $authuser, $vmid, undef, $opt, $conf->{$opt});
+                   }
+                   check_usb_perm($rpcenv, $authuser, $vmid, undef, $opt, $param->{$opt});
+                   $conf->{pending}->{$opt} = $param->{$opt};
+               } elsif ($opt =~ m/^hostpci\d+$/) {
+                   if (my $oldvalue = $conf->{$opt}) {
+                       check_hostpci_perm($rpcenv, $authuser, $vmid, undef, $opt, $oldvalue);
                    }
+                   check_hostpci_perm($rpcenv, $authuser, $vmid, undef, $opt, $param->{$opt});
                    $conf->{pending}->{$opt} = $param->{$opt};
                } elsif ($opt eq 'tags') {
                    assert_tag_permissions($vmid, $conf->{$opt}, $param->{$opt}, $rpcenv, $authuser);
                    $conf->{pending}->{$opt} = PVE::GuestHelpers::get_unique_tags($param->{$opt});
+               } elsif ($opt =~ m/^net\d+$/) {
+                   if ($conf->{$opt}) {
+                       PVE::QemuServer::check_bridge_access(
+                           $rpcenv,
+                           $authuser,
+                           { $opt => $conf->{$opt} },
+                       );
+                   }
+                   $conf->{pending}->{$opt} = $param->{$opt};
                } else {
                    $conf->{pending}->{$opt} = $param->{$opt};
 
@@ -2179,7 +2267,8 @@ __PACKAGE__->register_method({
            websocket => {
                optional => 1,
                type => 'boolean',
-               description => "starts websockify instead of vncproxy",
+               description => "Prepare for websocket upgrade (only required when using "
+                   ."serial terminal, otherwise upgrade is always possible).",
            },
            'generate-password' => {
                optional => 1,
@@ -2277,7 +2366,7 @@ __PACKAGE__->register_method({
 
            } else {
 
-               $ENV{LC_PVE_TICKET} = $password if $websocket; # set ticket with "qm vncproxy"
+               $ENV{LC_PVE_TICKET} = $password; # set ticket with "qm vncproxy"
 
                $cmd = [@$remcmd, "/usr/sbin/qm", 'vncproxy', $vmid];
 
@@ -2993,12 +3082,8 @@ __PACKAGE__->register_method({
 
        my $shutdown = 1;
 
-       # if vm is paused, do not shutdown (but stop if forceStop = 1)
-       # otherwise, we will infer a shutdown command, but run into the timeout,
-       # then when the vm is resumed, it will instantly shutdown
-       #
-       # checking the qmp status here to get feedback to the gui/cli/api
-       # and the status query should not take too long
+       # sending a graceful shutdown command to paused VMs runs into timeouts, and even worse, when
+       # the VM gets resumed later, it still gets the request delivered and powers off
        if (PVE::QemuServer::vm_is_paused($vmid)) {
            if ($param->{forceStop}) {
                warn "VM is paused - stop instead of shutdown\n";
@@ -3511,6 +3596,7 @@ __PACKAGE__->register_method({
            my $oldconf = $snapname ? $conf->{snapshots}->{$snapname} : $conf;
 
            my $sharedvm = &$check_storage_access_clone($rpcenv, $authuser, $storecfg, $oldconf, $storage);
+           PVE::QemuServer::check_mapping_access($rpcenv, $authuser, $oldconf);
 
            PVE::QemuServer::check_bridge_access($rpcenv, $authuser, $oldconf);
 
@@ -4232,7 +4318,11 @@ __PACKAGE__->register_method({
            local_resources => {
                type => 'array',
                description => "List local resources e.g. pci, usb"
-           }
+           },
+           'mapped-resources' => {
+               type => 'array',
+               description => "List of mapped resources e.g. pci, usb"
+           },
        },
     },
     code => sub {
@@ -4261,7 +4351,11 @@ __PACKAGE__->register_method({
 
        $res->{running} = PVE::QemuServer::check_running($vmid) ? 1:0;
 
-       # if vm is not running, return target nodes where local storage is available
+       my ($local_resources, $mapped_resources, $missing_mappings_by_node) =
+           PVE::QemuServer::check_local_resources($vmconf, 1);
+       delete $missing_mappings_by_node->{$localnode};
+
+       # if vm is not running, return target nodes where local storage/mapped devices are available
        # for offline migration
        if (!$res->{running}) {
            $res->{allowed_nodes} = [];
@@ -4269,7 +4363,13 @@ __PACKAGE__->register_method({
            delete $checked_nodes->{$localnode};
 
            foreach my $node (keys %$checked_nodes) {
-               if (!defined $checked_nodes->{$node}->{unavailable_storages}) {
+               my $missing_mappings = $missing_mappings_by_node->{$node};
+               if (scalar($missing_mappings->@*)) {
+                   $checked_nodes->{$node}->{'unavailable-resources'} = $missing_mappings;
+                   next;
+               }
+
+               if (!defined($checked_nodes->{$node}->{unavailable_storages})) {
                    push @{$res->{allowed_nodes}}, $node;
                }
 
@@ -4277,13 +4377,11 @@ __PACKAGE__->register_method({
            $res->{not_allowed_nodes} = $checked_nodes;
        }
 
-
        my $local_disks = &$check_vm_disks_local($storecfg, $vmconf, $vmid);
        $res->{local_disks} = [ values %$local_disks ];;
 
-       my $local_resources =  PVE::QemuServer::check_local_resources($vmconf, 1);
-
        $res->{local_resources} = $local_resources;
+       $res->{'mapped-resources'} = $mapped_resources;
 
        return $res;
 
@@ -5554,7 +5652,6 @@ __PACKAGE__->register_method({
                        'disk' => [
                            undef,
                            $storeid,
-                           undef,
                            $drive,
                            0,
                            $format,