]> git.proxmox.com Git - qemu-server.git/blobdiff - PVE/API2/Qemu.pm
fix #3784: config: Parameter for guest vIOMMU + test-cases
[qemu-server.git] / PVE / API2 / Qemu.pm
index 0999a9ef5b7ad9a3b82228db48d83109a7efa46f..3eabddd58b77bea49f81d73e88159d62f390a05a 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;
@@ -45,6 +48,7 @@ use PVE::DataCenterConfig;
 use PVE::SSHInfo;
 use PVE::Replication;
 use PVE::StorageTunnel;
+use PVE::RESTEnvironment qw(log_warn);
 
 BEGIN {
     if (!$ENV{PVE_GENERATING_DOCS}) {
@@ -83,8 +87,6 @@ my $foreach_volume_with_alloc = sub {
     }
 };
 
-my $NEW_DISK_RE = qr!^(([^/:\s]+):)?(\d+(\.\d+)?)$!;
-
 my $check_drive_param = sub {
     my ($param, $storecfg, $extra_checks) = @_;
 
@@ -95,7 +97,7 @@ my $check_drive_param = sub {
        raise_param_exc({ $opt => "unable to parse drive options" }) if !$drive;
 
        if ($drive->{'import-from'}) {
-           if ($drive->{file} !~ $NEW_DISK_RE || $3 != 0) {
+           if ($drive->{file} !~ $PVE::QemuServer::Drive::NEW_DISK_RE || $3 != 0) {
                raise_param_exc({
                    $opt => "'import-from' requires special syntax - ".
                        "use <storage ID>:0,import-from=<source>",
@@ -139,7 +141,7 @@ my $check_storage_access = sub {
            # nothing to check
        } elsif ($isCDROM && ($volid eq 'cdrom')) {
            $rpcenv->check($authuser, "/", ['Sys.Console']);
-       } elsif (!$isCDROM && ($volid =~ $NEW_DISK_RE)) {
+       } elsif (!$isCDROM && ($volid =~ $PVE::QemuServer::Drive::NEW_DISK_RE)) {
            my ($storeid, $size) = ($2 || $default_storage, $3);
            die "no storage ID specified (and no default storage)\n" if !$storeid;
            $rpcenv->check($authuser, "/storage/$storeid", ['Datastore.AllocateSpace']);
@@ -314,13 +316,26 @@ my $import_from_volid = sub {
 
 # Note: $pool is only needed when creating a VM, because pool permissions
 # are automatically inherited if VM already exists inside a pool.
-my $create_disks = sub {
-    my ($rpcenv, $authuser, $conf, $arch, $storecfg, $vmid, $pool, $settings, $default_storage) = @_;
+my sub create_disks : prototype($$$$$$$$$$) {
+    my (
+       $rpcenv,
+       $authuser,
+       $conf,
+       $arch,
+       $storecfg,
+       $vmid,
+       $pool,
+       $settings,
+       $default_storage,
+       $is_live_import,
+    ) = @_;
 
     my $vollist = [];
 
     my $res = {};
 
+    my $live_import_mapping = {};
+
     my $code = sub {
        my ($ds, $disk) = @_;
 
@@ -362,56 +377,86 @@ my $create_disks = sub {
            delete $disk->{format}; # no longer needed
            $res->{$ds} = PVE::QemuServer::print_drive($disk);
            print "$ds: successfully created disk '$res->{$ds}'\n";
-       } elsif ($volid =~ $NEW_DISK_RE) {
+       } elsif ($volid =~ $PVE::QemuServer::Drive::NEW_DISK_RE) {
            my ($storeid, $size) = ($2 || $default_storage, $3);
            die "no storage ID specified (and no default storage)\n" if !$storeid;
 
+           $size = PVE::Tools::convert_size($size, 'gb' => 'kb'); # vdisk_alloc uses kb
+
+           my $live_import = $is_live_import && $ds ne 'efidisk0';
+           my $needs_creation = 1;
+
            if (my $source = delete $disk->{'import-from'}) {
                my $dst_volid;
 
+               $needs_creation = $live_import;
+
                if (PVE::Storage::parse_volume_id($source, 1)) { # PVE-managed volume
-                   my $dest_info = {
-                       vmid => $vmid,
-                       drivename => $ds,
-                       storage => $storeid,
-                       format => $disk->{format},
-                   };
+                   if ($live_import && $ds ne 'efidisk0') {
+                       my $path = PVE::Storage::path($storecfg, $source)
+                           or die "failed to get a path for '$source'\n";
+                       $source = $path;
+                       ($size, my $source_format) = PVE::Storage::file_size_info($source);
+                       die "could not get file size of $source\n" if !$size;
+                       $live_import_mapping->{$ds} = {
+                           path => $source,
+                           format => $source_format,
+                       };
+                   } else {
+                       my $dest_info = {
+                           vmid => $vmid,
+                           drivename => $ds,
+                           storage => $storeid,
+                           format => $disk->{format},
+                       };
 
-                   $dest_info->{efisize} = PVE::QemuServer::get_efivars_size($conf, $disk)
-                       if $ds eq 'efidisk0';
+                       $dest_info->{efisize} = PVE::QemuServer::get_efivars_size($conf, $disk)
+                           if $ds eq 'efidisk0';
 
-                   ($dst_volid, $size) = eval {
-                       $import_from_volid->($storecfg, $source, $dest_info, $vollist);
-                   };
-                   die "cannot import from '$source' - $@" if $@;
+                       ($dst_volid, $size) = eval {
+                           $import_from_volid->($storecfg, $source, $dest_info, $vollist);
+                       };
+                       die "cannot import from '$source' - $@" if $@;
+                   }
                } else {
                    $source = PVE::Storage::abs_filesystem_path($storecfg, $source, 1);
-                   $size = PVE::Storage::file_size_info($source);
+                   ($size, my $source_format) = PVE::Storage::file_size_info($source);
                    die "could not get file size of $source\n" if !$size;
 
-                   (undef, $dst_volid) = PVE::QemuServer::ImportDisk::do_import(
-                       $source,
-                       $vmid,
-                       $storeid,
-                       {
-                           drive_name => $ds,
-                           format => $disk->{format},
-                           'skip-config-update' => 1,
-                       },
-                   );
-                   push @$vollist, $dst_volid;
+                   if ($live_import && $ds ne 'efidisk0') {
+                       $live_import_mapping->{$ds} = {
+                           path => $source,
+                           format => $source_format,
+                       };
+                   } else {
+                       (undef, $dst_volid) = PVE::QemuServer::ImportDisk::do_import(
+                           $source,
+                           $vmid,
+                           $storeid,
+                           {
+                               drive_name => $ds,
+                               format => $disk->{format},
+                               'skip-config-update' => 1,
+                           },
+                       );
+                       push @$vollist, $dst_volid;
+                   }
                }
 
-               $disk->{file} = $dst_volid;
-               $disk->{size} = $size;
-               delete $disk->{format}; # no longer needed
-               $res->{$ds} = PVE::QemuServer::print_drive($disk);
-           } else {
+               if ($needs_creation) {
+                   $size = PVE::Tools::convert_size($size, 'b' => 'kb'); # vdisk_alloc uses kb
+               } else {
+                   $disk->{file} = $dst_volid;
+                   $disk->{size} = $size;
+                   delete $disk->{format}; # no longer needed
+                   $res->{$ds} = PVE::QemuServer::print_drive($disk);
+               }
+           }
+
+           if ($needs_creation) {
                my $defformat = PVE::Storage::storage_default_format($storecfg, $storeid);
                my $fmt = $disk->{format} || $defformat;
 
-               $size = PVE::Tools::convert_size($size, 'gb' => 'kb'); # vdisk_alloc uses kb
-
                my $volid;
                if ($ds eq 'efidisk0') {
                    my $smm = PVE::QemuServer::Machine::machine_type_is_q35($conf);
@@ -472,7 +517,10 @@ my $create_disks = sub {
        die $err;
     }
 
-    return ($vollist, $res);
+    # don't return empty import mappings
+    $live_import_mapping = undef if !%$live_import_mapping;
+
+    return ($vollist, $res, $live_import_mapping);
 };
 
 my $check_cpu_model_access = sub {
@@ -560,6 +608,7 @@ my $cloudinitoptions = {
     cipassword => 1,
     citype => 1,
     ciuser => 1,
+    ciupgrade => 1,
     nameserver => 1,
     searchdomain => 1,
     sshkeys => 1,
@@ -583,19 +632,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 +705,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 +734,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";
        }
@@ -649,6 +743,33 @@ my $check_vm_modify_config_perm = sub {
     return 1;
 };
 
+sub assert_scsi_feature_compatibility {
+    my ($opt, $conf, $storecfg, $drive_attributes) = @_;
+
+    my $drive = PVE::QemuServer::Drive::parse_drive($opt, $drive_attributes, 1);
+
+    my $machine_type = PVE::QemuServer::get_vm_machine($conf, undef, $conf->{arch});
+    my $machine_version = PVE::QemuServer::Machine::extract_version(
+       $machine_type, PVE::QemuServer::kvm_user_version());
+    my $drivetype = PVE::QemuServer::Drive::get_scsi_device_type(
+       $drive, $storecfg, $machine_version);
+
+    if ($drivetype ne 'hd' && $drivetype ne 'cd') {
+       if ($drive->{product}) {
+           raise_param_exc({
+               $opt => "Passing of product information is only supported for 'scsi-hd' and "
+                   ."'scsi-cd' devices (e.g. not pass-through).",
+           });
+       }
+       if ($drive->{vendor}) {
+           raise_param_exc({
+               $opt => "Passing of vendor information is only supported for 'scsi-hd' and "
+                   ."'scsi-cd' devices (e.g. not pass-through).",
+           });
+       }
+    }
+}
+
 __PACKAGE__->register_method({
     name => 'vmlist',
     path => '',
@@ -703,23 +824,22 @@ 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;
 };
 
-
 __PACKAGE__->register_method({
     name => 'create_vm',
     path => '',
@@ -728,7 +848,8 @@ __PACKAGE__->register_method({
     permissions => {
        description => "You need 'VM.Allocate' permissions on /vms/{vmid} or on the VM pool /pool/{pool}. " .
            "For restore (option 'archive'), it is enough if the user has 'VM.Backup' permission and the VM already exists. " .
-           "If you create disks you need 'Datastore.AllocateSpace' on any used storage.",
+           "If you create disks you need 'Datastore.AllocateSpace' on any used storage." .
+           "If you use a bridge/vlan, you need 'SDN.Use' on any used bridge/vlan.",
         user => 'all', # check inside
     },
     protected => 1,
@@ -766,8 +887,7 @@ __PACKAGE__->register_method({
                'live-restore' => {
                    optional => 1,
                    type => 'boolean',
-                   description => "Start the VM immediately from the backup and restore in background. PBS only.",
-                   requires => 'archive',
+                   description => "Start the VM immediately while importing or restoring in the background.",
                },
                pool => {
                    optional => 1,
@@ -877,8 +997,10 @@ __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);
 
            $check_drive_param->($param, $storecfg);
@@ -908,6 +1030,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;
@@ -926,6 +1061,8 @@ __PACKAGE__->register_method({
                    eval { PVE::QemuServer::template_create($vmid, $restored_conf) };
                    warn $@ if $@;
                }
+
+               PVE::QemuServer::create_ifaces_ipams_ips($restored_conf, $vmid) if $unique;
            };
 
            # ensure no old replication state are exists
@@ -941,6 +1078,8 @@ __PACKAGE__->register_method({
        };
 
        my $createfn = sub {
+           my $live_import_mapping = {};
+
            # ensure no old replication state are exists
            PVE::ReplicationState::delete_guest_states($vmid);
 
@@ -948,11 +1087,16 @@ __PACKAGE__->register_method({
                my $conf = $param;
                my $arch = PVE::QemuServer::get_vm_arch($conf);
 
+               for my $opt (sort keys $param->%*) {
+                   next if $opt !~ m/^scsi\d+$/;
+                   assert_scsi_feature_compatibility($opt, $conf, $storecfg, $param->{$opt});
+               }
+
                $conf->{meta} = PVE::QemuServer::new_meta_info_string();
 
                my $vollist = [];
                eval {
-                   ($vollist, my $created_opts) = $create_disks->(
+                   ($vollist, my $created_opts, $live_import_mapping) = create_disks(
                        $rpcenv,
                        $authuser,
                        $conf,
@@ -962,6 +1106,7 @@ __PACKAGE__->register_method({
                        $pool,
                        $param,
                        $storage,
+                       $live_restore,
                    );
                    $conf->{$_} = $created_opts->{$_} for keys $created_opts->%*;
 
@@ -970,6 +1115,9 @@ __PACKAGE__->register_method({
                        $conf->{boot} = PVE::QemuServer::print_bootorder($devs);
                    }
 
+                   my $vga = PVE::QemuServer::parse_vga($conf->{vga});
+                   PVE::QemuServer::assert_clipboard_config($vga);
+
                    # auto generate uuid if user did not specify smbios1 option
                    if (!$conf->{smbios1}) {
                        $conf->{smbios1} = PVE::QemuServer::generate_smbios1_uuid();
@@ -979,16 +1127,20 @@ __PACKAGE__->register_method({
                        $conf->{vmgenid} = PVE::QemuServer::generate_uuid();
                    }
 
-                   my $machine = $conf->{machine};
+                   my $machine_conf = PVE::QemuServer::Machine::parse_machine($conf->{machine});
+                   my $machine = $machine_conf->{type};
                    if (!$machine || $machine =~ m/^(?:pc|q35|virt)$/) {
                        # always pin Windows' machine version on create, they get to easily confused
                        if (PVE::QemuServer::Helpers::windows_version($conf->{ostype})) {
-                           $conf->{machine} = PVE::QemuServer::windows_get_pinned_machine_version($machine);
+                           $machine_conf->{type} = PVE::QemuServer::windows_get_pinned_machine_version($machine);
+                           $conf->{machine} = PVE::QemuServer::Machine::print_machine($machine_conf);
                        }
                    }
+                   PVE::QemuServer::Machine::assert_valid_machine_property($conf, $machine_conf);
 
-                   PVE::QemuConfig->write_config($vmid, $conf);
+                   $conf->{lock} = 'import' if $live_import_mapping;
 
+                   PVE::QemuConfig->write_config($vmid, $conf);
                };
                my $err = $@;
 
@@ -1001,14 +1153,19 @@ __PACKAGE__->register_method({
                }
 
                PVE::AccessControl::add_vm_to_pool($vmid, $pool) if $pool;
+
+               PVE::QemuServer::create_ifaces_ipams_ips($conf, $vmid);
            };
 
            PVE::QemuConfig->lock_config_full($vmid, 1, $realcmd);
 
-           if ($start_after_create) {
+           if ($start_after_create && !$live_restore) {
                print "Execute autostart\n";
                eval { PVE::API2::Qemu->vm_start({vmid => $vmid, node => $node}) };
                warn $@ if $@;
+               return;
+           } else {
+               return $live_import_mapping;
            }
        };
 
@@ -1033,7 +1190,9 @@ __PACKAGE__->register_method({
        } else {
            $worker_name = 'qmcreate';
            $code = sub {
-               eval { $createfn->() };
+               # If a live import was requested the create function returns
+               # the mapping for the startup.
+               my $live_import_mapping = eval { $createfn->() };
                if (my $err = $@) {
                    eval {
                        my $conffile = PVE::QemuConfig->config_file($vmid);
@@ -1042,6 +1201,21 @@ __PACKAGE__->register_method({
                    warn $@ if $@;
                    die $err;
                }
+
+               if ($live_import_mapping) {
+                   my $import_options = {
+                       bwlimit => $bwlimit,
+                       live => 1,
+                   };
+
+                   my $conf = PVE::QemuConfig->load_config($vmid);
+                   PVE::QemuServer::live_import_from_files(
+                       $live_import_mapping,
+                       $vmid,
+                       $conf,
+                       $import_options,
+                   );
+               }
            };
        }
 
@@ -1344,16 +1518,23 @@ __PACKAGE__->register_method({
                    description => "Configuration option name.",
                    type => 'string',
                },
-               old => {
+               value => {
                    description => "Value as it was used to generate the current cloudinit image.",
                    type => 'string',
                    optional => 1,
                },
-               new => {
+               pending => {
                    description => "The new pending value.",
                    type => 'string',
                    optional => 1,
                },
+               delete => {
+                   description => "Indicates a pending delete request if present and not 0. ",
+                   type => 'integer',
+                   minimum => 0,
+                   maximum => 1,
+                   optional => 1,
+               },
            },
        },
     },
@@ -1365,26 +1546,39 @@ __PACKAGE__->register_method({
 
        my $ci = $conf->{cloudinit};
 
-       my $res = {};
+       $conf->{cipassword} = '**********' if exists $conf->{cipassword};
+       $ci->{cipassword} = '**********' if exists $ci->{cipassword};
+
+       my $res = [];
+
+       # All the values that got added
        my $added = delete($ci->{added}) // '';
        for my $key (PVE::Tools::split_list($added)) {
-           $res->{$key} = { new => $conf->{$key} };
+           push @$res, { key => $key, pending => $conf->{$key} };
        }
 
-       for my $key (keys %$ci) {
-           if (!exists($conf->{$key})) {
-               $res->{$key} = { old => $ci->{$key} };
+       # All already existing values (+ their new value, if it exists)
+       for my $opt (keys %$cloudinitoptions) {
+           next if !$conf->{$opt};
+           next if $added =~ m/$opt/;
+           my $item = {
+               key => $opt,
+           };
+
+           if (my $pending = $ci->{$opt}) {
+               $item->{value} = $pending;
+               $item->{pending} = $conf->{$opt};
            } else {
-               $res->{$key} = {
-                   old => $ci->{$key},
-                   new => $conf->{$key},
-               };
+               $item->{value} = $conf->{$opt},
            }
+
+           push @$res, $item;
        }
 
-       if (defined(my $pw = $res->{cipassword})) {
-           $pw->{old} = '**********' if exists $pw->{old};
-           $pw->{new} = '**********' if exists $pw->{new};
+       # Now, we'll find the deleted ones
+       for my $opt (keys %$ci) {
+           next if $conf->{$opt};
+           push @$res, { key => $opt, delete => 1 };
        }
 
        return $res;
@@ -1398,7 +1592,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,
@@ -1489,8 +1683,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
@@ -1543,7 +1735,7 @@ my $update_vm_api  = sub {
        return if defined($volname) && $volname eq 'cloudinit';
 
        my $format;
-       if ($volid =~ $NEW_DISK_RE) {
+       if ($volid =~ $PVE::QemuServer::Drive::NEW_DISK_RE) {
            $storeid = $2;
            $format = $drive->{format} || PVE::Storage::storage_default_format($storecfg, $storeid);
        } else {
@@ -1578,6 +1770,8 @@ my $update_vm_api  = sub {
 
     &$check_storage_access($rpcenv, $authuser, $storecfg, $vmid, $param);
 
+    PVE::QemuServer::check_bridge_access($rpcenv, $authuser, $param);
+
     my $updatefn =  sub {
 
        my $conf = PVE::QemuConfig->load_config($vmid);
@@ -1608,7 +1802,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"
@@ -1696,17 +1892,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);
@@ -1731,7 +1937,10 @@ my $update_vm_api  = sub {
                    PVE::QemuServer::vmconfig_register_unused_drive($storecfg, $vmid, $conf, PVE::QemuServer::parse_drive($opt, $conf->{pending}->{$opt}))
                        if defined($conf->{pending}->{$opt});
 
-                   my (undef, $created_opts) = $create_disks->(
+                   assert_scsi_feature_compatibility($opt, $conf, $storecfg, $param->{$opt})
+                       if $opt =~ m/^scsi\d+$/;
+
+                   my (undef, $created_opts) = create_disks(
                        $rpcenv,
                        $authuser,
                        $conf,
@@ -1740,6 +1949,8 @@ my $update_vm_api  = sub {
                        $vmid,
                        undef,
                        {$opt => $param->{$opt}},
+                       undef,
+                       undef,
                    );
                    $conf->{pending}->{$_} = $created_opts->{$_} for keys $created_opts->%*;
 
@@ -1760,16 +1971,38 @@ my $update_vm_api  = sub {
                        die "only root can modify '$opt' config for real devices\n";
                    }
                    $conf->{pending}->{$opt} = $param->{$opt};
+               } elsif ($opt eq 'vga') {
+                   my $vga = PVE::QemuServer::parse_vga($param->{$opt});
+                   PVE::QemuServer::assert_clipboard_config($vga);
+                   $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};
+               } elsif ($opt eq 'machine') {
+                   my $machine_conf = PVE::QemuServer::Machine::parse_machine($param->{$opt});
+                   PVE::QemuServer::Machine::assert_valid_machine_property($conf, $machine_conf);
+                   $conf->{pending}->{$opt} = $param->{$opt};
                } else {
                    $conf->{pending}->{$opt} = $param->{$opt};
 
@@ -2156,7 +2389,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,
@@ -2254,7 +2488,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];
 
@@ -2580,6 +2814,13 @@ __PACKAGE__->register_method({
                type => 'boolean',
                optional => 1,
            },
+           clipboard => {
+               description => 'Enable a specific clipboard. If not set, depending on'
+                   .' the display type the SPICE one will be added.',
+               type => 'string',
+               enum => ['vnc'],
+               optional => 1,
+           },
        },
     },
     code => sub {
@@ -2598,6 +2839,7 @@ __PACKAGE__->register_method({
            my $spice = defined($vga->{type}) && $vga->{type} =~ /^virtio/;
            $spice ||= PVE::QemuServer::vga_conf_has_spice($conf->{vga});
            $status->{spice} = 1 if $spice;
+           $status->{clipboard} = $vga->{clipboard};
        }
        $status->{agent} = 1 if PVE::QemuServer::get_qga_key($conf, 'enabled');
 
@@ -2970,13 +3212,9 @@ __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
-       if (PVE::QemuServer::vm_is_paused($vmid)) {
+       # 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, 1)) {
            if ($param->{forceStop}) {
                warn "VM is paused - stop instead of shutdown\n";
                $shutdown = 0;
@@ -3052,7 +3290,7 @@ __PACKAGE__->register_method({
        my $node = extract_param($param, 'node');
        my $vmid = extract_param($param, 'vmid');
 
-       die "VM is paused - cannot shutdown\n" if PVE::QemuServer::vm_is_paused($vmid);
+       die "VM is paused - cannot shutdown\n" if PVE::QemuServer::vm_is_paused($vmid, 1);
 
        die "VM $vmid not running\n" if !PVE::QemuServer::check_running($vmid);
 
@@ -3355,7 +3593,7 @@ __PACKAGE__->register_method({
     permissions => {
        description => "You need 'VM.Clone' permissions on /vms/{vmid}, and 'VM.Allocate' permissions " .
            "on /vms/{newid} (or on the VM pool /pool/{pool}). You also need " .
-           "'Datastore.AllocateSpace' on any used storage.",
+           "'Datastore.AllocateSpace' on any used storage and 'SDN.Use' on any used bridge/vnet",
        check =>
        [ 'and',
          ['perm', '/vms/{vmid}', [ 'VM.Clone' ]],
@@ -3488,6 +3726,9 @@ __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);
 
            die "can't clone VM to node '$target' (VM uses local storage)\n"
                if $target && !$sharedvm;
@@ -3652,9 +3893,16 @@ __PACKAGE__->register_method({
 
                PVE::QemuConfig->write_config($newid, $newconf);
 
+               PVE::QemuServer::create_ifaces_ipams_ips($newconf, $newid);
+
                if ($target) {
-                   # always deactivate volumes - avoid lvm LVs to be active on several nodes
-                   PVE::Storage::deactivate_volumes($storecfg, $vollist, $snapname) if !$running;
+                   if (!$running) {
+                       # always deactivate volumes – avoids that LVM LVs are active on several nodes
+                       eval { PVE::Storage::deactivate_volumes($storecfg, $vollist, $snapname) };
+                       # but only warn when that fails (e.g., parallel clones keeping them active)
+                       log_warn($@) if $@;
+                   }
+
                    PVE::Storage::deactivate_volumes($storecfg, $newvollist);
 
                    my $newconffile = PVE::QemuConfig->config_file($newid, $target);
@@ -4207,7 +4455,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 {
@@ -4236,7 +4488,16 @@ __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};
+
+       my $vga = PVE::QemuServer::parse_vga($vmconf->{vga});
+       if ($res->{running} && $vga->{'clipboard'} && $vga->{'clipboard'} eq 'vnc') {
+           push $local_resources->@*, "clipboard=vnc";
+       }
+
+       # if vm is not running, return target nodes where local storage/mapped devices are available
        # for offline migration
        if (!$res->{running}) {
            $res->{allowed_nodes} = [];
@@ -4244,7 +4505,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;
                }
 
@@ -4252,13 +4519,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;
 
@@ -4680,7 +4945,10 @@ __PACKAGE__->register_method({
            },
        },
     },
-    returns => { type => 'null'},
+    returns => {
+       type => 'string',
+       description => "the task ID.",
+    },
     code => sub {
         my ($param) = @_;
 
@@ -4719,9 +4987,6 @@ __PACKAGE__->register_method({
            my (undef, undef, undef, undef, undef, undef, $format) =
                PVE::Storage::parse_volname($storecfg, $drive->{file});
 
-           die "can't resize volume: $disk if snapshot exists\n"
-               if %{$conf->{snapshots}} && $format eq 'qcow2';
-
            my $volid = $drive->{file};
 
            die "disk '$disk' has no associated volume\n" if !$volid;
@@ -4767,8 +5032,11 @@ __PACKAGE__->register_method({
            PVE::QemuConfig->write_config($vmid, $conf);
        };
 
-        PVE::QemuConfig->lock_config($vmid, $updatefn);
-        return;
+       my $worker = sub {
+           PVE::QemuConfig->lock_config($vmid, $updatefn);
+       };
+
+       return $rpcenv->fork_worker('resize', $vmid, $authuser, $worker);
     }});
 
 __PACKAGE__->register_method({
@@ -5526,7 +5794,6 @@ __PACKAGE__->register_method({
                        'disk' => [
                            undef,
                            $storeid,
-                           undef,
                            $drive,
                            0,
                            $format,