]> git.proxmox.com Git - qemu-server.git/blobdiff - PVE/QemuServer.pm
suspend: continue cleanup even if savevm-end QMP command fails
[qemu-server.git] / PVE / QemuServer.pm
index b9ebbbb74099972c159e943761f885f5fae66e51..5df0c96d5341a42e84f6827432895a0eded37792 100644 (file)
@@ -34,6 +34,7 @@ use PVE::DataCenterConfig;
 use PVE::Exception qw(raise raise_param_exc);
 use PVE::Format qw(render_duration render_bytes);
 use PVE::GuestHelpers qw(safe_string_ne safe_num_ne safe_boolean_ne);
+use PVE::HA::Config;
 use PVE::Mapping::PCI;
 use PVE::Mapping::USB;
 use PVE::INotify;
@@ -52,8 +53,8 @@ use PVE::QemuConfig;
 use PVE::QemuServer::Helpers qw(config_aware_timeout min_version windows_version);
 use PVE::QemuServer::Cloudinit;
 use PVE::QemuServer::CGroup;
-use PVE::QemuServer::CPUConfig qw(print_cpu_device get_cpu_options);
-use PVE::QemuServer::Drive qw(is_valid_drivename drive_is_cloudinit drive_is_cdrom drive_is_read_only parse_drive print_drive path_is_scsi);
+use PVE::QemuServer::CPUConfig qw(print_cpu_device get_cpu_options get_cpu_bitness is_native_arch);
+use PVE::QemuServer::Drive qw(is_valid_drivename drive_is_cloudinit drive_is_cdrom drive_is_read_only parse_drive print_drive);
 use PVE::QemuServer::Machine;
 use PVE::QemuServer::Memory qw(get_current_memory);
 use PVE::QemuServer::Monitor qw(mon_cmd);
@@ -123,14 +124,6 @@ PVE::JSONSchema::register_standard_option('pve-qm-stateuri', {
     optional => 1,
 });
 
-PVE::JSONSchema::register_standard_option('pve-qemu-machine', {
-       description => "Specifies the QEMU machine type.",
-       type => 'string',
-       pattern => '(pc|pc(-i440fx)?-\d+(\.\d+)+(\+pve\d+)?(\.pxe)?|q35|pc-q35-\d+(\.\d+)+(\+pve\d+)?(\.pxe)?|virt(?:-\d+(\.\d+)+)?(\+pve\d+)?)',
-       maxLength => 40,
-       optional => 1,
-});
-
 # FIXME: remove in favor of just using the INotify one, it's cached there exactly the same way
 my $nodename_cache;
 sub nodename {
@@ -411,6 +404,7 @@ my $confdesc = {
     ostype => {
        optional => 1,
        type => 'string',
+       # NOTE: When extending, also consider extending `%guest_types` in `Import/ESXi.pm`.
        enum => [qw(other wxp w2k w2k3 w2k8 wvista win7 win8 win10 win11 l24 l26 solaris)],
        description => "Specify guest operating system.",
        verbose_description => <<EODESC,
@@ -427,7 +421,7 @@ wvista;; Microsoft Windows Vista
 win7;; Microsoft Windows 7
 win8;; Microsoft Windows 8/2012/2012r2
 win10;; Microsoft Windows 10/2016/2019
-win11;; Microsoft Windows 11/2022
+win11;; Microsoft Windows 11/2022/2025
 l24;; Linux 2.4 Kernel
 l26;; Linux 2.6 - 6.X Kernel
 solaris;; Solaris/OpenSolaris/OpenIndiania kernel
@@ -596,7 +590,10 @@ EODESCR
     migrate_downtime => {
        optional => 1,
        type => 'number',
-       description => "Set maximum tolerated downtime (in seconds) for migrations.",
+       description => "Set maximum tolerated downtime (in seconds) for migrations. Should the"
+           ." migration not be able to converge in the very end, because too much newly dirtied"
+           ." RAM needs to be transferred, the limit will be increased automatically step-by-step"
+           ." until migration can converge.",
        minimum => 0,
        default => 0.1,
     },
@@ -1409,45 +1406,34 @@ sub print_drivedevice_full {
 
        my ($maxdev, $controller, $controller_prefix) = scsihw_infos($conf, $drive);
        my $unit = $drive->{index} % $maxdev;
-       my $devicetype = 'hd';
-       my $path = '';
-       if (drive_is_cdrom($drive)) {
-           $devicetype = 'cd';
-       } else {
-           if ($drive->{file} =~ m|^/|) {
-               $path = $drive->{file};
-               if (my $info = path_is_scsi($path)) {
-                   if ($info->{type} == 0 && $drive->{scsiblock}) {
-                       $devicetype = 'block';
-                   } elsif ($info->{type} == 1) { # tape
-                       $devicetype = 'generic';
-                   }
-               }
-           } else {
-                $path = PVE::Storage::path($storecfg, $drive->{file});
-           }
 
-           # for compatibility only, we prefer scsi-hd (#2408, #2355, #2380)
-           my $version = extract_version($machine_type, kvm_user_version());
-           if ($path =~ m/^iscsi\:\/\// &&
-              !min_version($version, 4, 1)) {
-               $devicetype = 'generic';
-           }
-       }
+       my $machine_version = extract_version($machine_type, kvm_user_version());
+       my $device_type = PVE::QemuServer::Drive::get_scsi_device_type(
+           $drive, $storecfg, $machine_version);
 
        if (!$conf->{scsihw} || $conf->{scsihw} =~ m/^lsi/ || $conf->{scsihw} eq 'pvscsi') {
-           $device = "scsi-$devicetype,bus=$controller_prefix$controller.0,scsi-id=$unit";
+           $device = "scsi-$device_type,bus=$controller_prefix$controller.0,scsi-id=$unit";
        } else {
-           $device = "scsi-$devicetype,bus=$controller_prefix$controller.0,channel=0,scsi-id=0"
+           $device = "scsi-$device_type,bus=$controller_prefix$controller.0,channel=0,scsi-id=0"
                .",lun=$drive->{index}";
        }
        $device .= ",drive=drive-$drive_id,id=$drive_id";
 
-       if ($drive->{ssd} && ($devicetype eq 'block' || $devicetype eq 'hd')) {
+       if ($drive->{ssd} && ($device_type eq 'block' || $device_type eq 'hd')) {
            $device .= ",rotation_rate=1";
        }
        $device .= ",wwn=$drive->{wwn}" if $drive->{wwn};
 
+       # only scsi-hd and scsi-cd support passing vendor and product information
+       if ($device_type eq 'hd' || $device_type eq 'cd') {
+           if (my $vendor = $drive->{vendor}) {
+               $device .= ",vendor=$vendor";
+           }
+           if (my $product = $drive->{product}) {
+               $device .= ",product=$product";
+           }
+       }
+
     } elsif ($drive->{interface} eq 'ide' || $drive->{interface} eq 'sata') {
        my $maxdev = ($drive->{interface} eq 'sata') ? $PVE::QemuServer::Drive::MAX_SATA_DISKS : 2;
        my $controller = int($drive->{index} / $maxdev);
@@ -1463,9 +1449,9 @@ sub print_drivedevice_full {
            $unit = 0;
        }
 
-       my $devicetype = ($drive->{media} && $drive->{media} eq 'cdrom') ? "cd" : "hd";
+       my $device_type = ($drive->{media} && $drive->{media} eq 'cdrom') ? "cd" : "hd";
 
-       $device = "ide-$devicetype";
+       $device = "ide-$device_type";
        if ($drive->{interface} eq 'ide') {
            $device .= ",bus=ide.$controller,unit=$unit";
        } else {
@@ -1473,7 +1459,7 @@ sub print_drivedevice_full {
        }
        $device .= ",drive=drive-$drive_id,id=$drive_id";
 
-       if ($devicetype eq 'hd') {
+       if ($device_type eq 'hd') {
            if (my $model = $drive->{model}) {
                $model = URI::Escape::uri_unescape($model);
                $device .= ",model=$model";
@@ -1547,7 +1533,7 @@ my sub drive_uses_cache_direct {
 }
 
 sub print_drive_commandline_full {
-    my ($storecfg, $vmid, $drive, $pbs_name, $io_uring) = @_;
+    my ($storecfg, $vmid, $drive, $live_restore_name, $io_uring) = @_;
 
     my $path;
     my $volid = $drive->{file};
@@ -1559,7 +1545,7 @@ sub print_drive_commandline_full {
 
     if (drive_is_cdrom($drive)) {
        $path = get_iso_path($storecfg, $vmid, $volid);
-        die "$drive_id: cannot back cdrom drive with PBS snapshot\n" if $pbs_name;
+        die "$drive_id: cannot back cdrom drive with a live restore image\n" if $live_restore_name;
     } else {
        if ($storeid) {
            $path = PVE::Storage::path($storecfg, $volid);
@@ -1610,7 +1596,7 @@ sub print_drive_commandline_full {
        }
     }
 
-    if ($pbs_name) {
+    if ($live_restore_name) {
        $format = "rbd" if $is_rbd;
        die "$drive_id: Proxmox Backup Server backed drive cannot auto-detect the format\n"
            if !$format;
@@ -1650,18 +1636,18 @@ sub print_drive_commandline_full {
 
        # note: 'detect-zeroes' works per blockdev and we want it to persist
        # after the alloc-track is removed, so put it on 'file' directly
-       my $dz_param = $pbs_name ? "file.detect-zeroes" : "detect-zeroes";
+       my $dz_param = $live_restore_name ? "file.detect-zeroes" : "detect-zeroes";
        $opts .= ",$dz_param=$detectzeroes" if $detectzeroes;
     }
 
-    if ($pbs_name) {
-       $opts .= ",backing=$pbs_name";
+    if ($live_restore_name) {
+       $opts .= ",backing=$live_restore_name";
        $opts .= ",auto-remove=on";
     }
 
-    # my $file_param = $pbs_name ? "file.file.filename" : "file";
+    # my $file_param = $live_restore_name ? "file.file.filename" : "file";
     my $file_param = "file";
-    if ($pbs_name) {
+    if ($live_restore_name) {
        # non-rbd drivers require the underlying file to be a seperate block
        # node, so add a second .file indirection
        $file_param .= ".file" if !$is_rbd;
@@ -1763,7 +1749,7 @@ sub print_netdev_full {
         if length($ifname) >= 16;
 
     my $vhostparam = '';
-    if (is_native($arch)) {
+    if (is_native_arch($arch)) {
        $vhostparam = ',vhost=on' if kernel_has_vhost_net() && $net->{model} eq 'virtio';
     }
 
@@ -1813,7 +1799,7 @@ sub print_vga_device {
        }
     }
 
-    die "no devicetype for $vga->{type}\n" if !$type;
+    die "no device-type for $vga->{type}\n" if !$type;
 
     my $memory = "";
     if ($vgamem_mb) {
@@ -2111,8 +2097,9 @@ sub qemu_created_version_fixups {
     # check if we need to apply some handling for VMs that always use the latest machine version but
     # had a machine version transition happen that affected HW such that, e.g., an OS config change
     # would be required (we do not want to pin machine version for non-windows OS type)
+    my $machine_conf = PVE::QemuServer::Machine::parse_machine($conf->{machine});
     if (
-       (!defined($conf->{machine}) || $conf->{machine} =~ m/^(?:pc|q35|virt)$/) # non-versioned machine
+       (!defined($machine_conf->{type}) || $machine_conf->{type} =~ m/^(?:pc|q35|virt)$/) # non-versioned machine
        && (!defined($meta->{'creation-qemu'}) || !min_version($meta->{'creation-qemu'}, 6, 1)) # created before 6.1
        && (!$forced_vers || min_version($forced_vers, 6, 1)) # handle snapshot-rollback/migrations
        && min_version($kvmver, 6, 1) # only need to apply the change since 6.1
@@ -2606,7 +2593,7 @@ sub check_local_resources {
     foreach my $k (keys %$conf) {
        if ($k =~ m/^usb/) {
            my $entry = parse_property_string('pve-qm-usb', $conf->{$k});
-           next if $entry->{host} =~ m/^spice$/i;
+           next if $entry->{host} && $entry->{host} =~ m/^spice$/i;
            if ($entry->{mapping}) {
                $add_missing_mapping->('usb', $k, $entry->{mapping});
                push @$mapped_res, $k;
@@ -3226,11 +3213,6 @@ sub vga_conf_has_spice {
     return $1 || 1;
 }
 
-sub is_native($) {
-    my ($arch) = @_;
-    return get_host_arch() eq $arch;
-}
-
 sub get_vm_arch {
     my ($conf) = @_;
     return $conf->{arch} // get_host_arch();
@@ -3273,7 +3255,8 @@ sub windows_get_pinned_machine_version {
 sub get_vm_machine {
     my ($conf, $forcemachine, $arch, $add_pve_version, $kvmversion) = @_;
 
-    my $machine = $forcemachine || $conf->{machine};
+    my $machine_conf = PVE::QemuServer::Machine::parse_machine($conf->{machine});
+    my $machine = $forcemachine || $machine_conf->{type};
 
     if (!$machine || $machine =~ m/^(?:pc|q35|virt)$/) {
        $kvmversion //= kvm_user_version();
@@ -3334,7 +3317,7 @@ my $Arch2Qemu = {
 };
 sub get_command_for_arch($) {
     my ($arch) = @_;
-    return '/usr/bin/kvm' if is_native($arch);
+    return '/usr/bin/kvm' if is_native_arch($arch);
 
     my $cmd = $Arch2Qemu->{$arch}
        or die "don't know how to emulate architecture '$arch'\n";
@@ -3512,7 +3495,7 @@ my sub print_ovmf_drive_commandlines {
 
 sub config_to_command {
     my ($storecfg, $vmid, $conf, $defaults, $forcemachine, $forcecpu,
-        $pbs_backing) = @_;
+        $live_restore_backing) = @_;
 
     my ($globalFlags, $machineFlags, $rtcFlags) = ([], [], []);
     my $devices = [];
@@ -3522,6 +3505,8 @@ sub config_to_command {
     my $kvm = $conf->{kvm};
     my $nodename = nodename();
 
+    my $machine_conf = PVE::QemuServer::Machine::parse_machine($conf->{machine});
+
     my $arch = get_vm_arch($conf);
     my $kvm_binary = get_command_for_arch($arch);
     my $kvmver = kvm_user_version($kvm_binary);
@@ -3535,7 +3520,7 @@ sub config_to_command {
 
     my $machine_type = get_vm_machine($conf, $forcemachine, $arch, $add_pve_version);
     my $machine_version = extract_version($machine_type, $kvmver);
-    $kvm //= 1 if is_native($arch);
+    $kvm //= 1 if is_native_arch($arch);
 
     $machine_version =~ m/(\d+)\.(\d+)/;
     my ($machine_major, $machine_minor) = ($1, $2);
@@ -3629,6 +3614,9 @@ sub config_to_command {
     }
 
     if ($conf->{bios} && $conf->{bios} eq 'ovmf') {
+       die "OVMF (UEFI) BIOS is not supported on 32-bit CPU types\n"
+           if !$forcecpu && get_cpu_bitness($conf->{cpu}, $arch) == 32;
+
        my ($code_drive_str, $var_drive_str) =
            print_ovmf_drive_commandlines($conf, $storecfg, $vmid, $arch, $q35, $version_guard);
        push $cmd->@*, '-drive', $code_drive_str;
@@ -3757,7 +3745,7 @@ sub config_to_command {
     if ($hotplug_features->{cpu} && min_version($machine_version, 2, 7)) {
        push @$cmd, '-smp', "1,sockets=$sockets,cores=$cores,maxcpus=$maxcpus";
         for (my $i = 2; $i <= $vcpus; $i++)  {
-           my $cpustr = print_cpu_device($conf,$i);
+           my $cpustr = print_cpu_device($conf, $arch, $i);
            push @$cmd, '-device', $cpustr;
        }
 
@@ -3997,15 +3985,15 @@ sub config_to_command {
            $ahcicontroller->{$controller}=1;
         }
 
-       my $pbs_conf = $pbs_backing->{$ds};
-       my $pbs_name = undef;
-       if ($pbs_conf) {
-           $pbs_name = "drive-$ds-pbs";
-           push @$devices, '-blockdev', print_pbs_blockdev($pbs_conf, $pbs_name);
+       my $live_restore = $live_restore_backing->{$ds};
+       my $live_blockdev_name = undef;
+       if ($live_restore) {
+           $live_blockdev_name = $live_restore->{name};
+           push @$devices, '-blockdev', $live_restore->{blockdev};
        }
 
        my $drive_cmd = print_drive_commandline_full(
-           $storecfg, $vmid, $drive, $pbs_name, min_version($kvmver, 6, 0));
+           $storecfg, $vmid, $drive, $live_blockdev_name, min_version($kvmver, 6, 0));
 
        # extra protection for templates, but SATA and IDE don't support it..
        $drive_cmd .= ',readonly=on' if drive_is_read_only($conf, $drive);
@@ -4095,6 +4083,17 @@ sub config_to_command {
     }
     push @$machineFlags, "type=${machine_type_min}";
 
+    PVE::QemuServer::Machine::assert_valid_machine_property($conf, $machine_conf);
+
+    if (my $viommu = $machine_conf->{viommu}) {
+       if ($viommu eq 'intel') {
+           unshift @$devices, '-device', 'intel-iommu,intremap=on,caching-mode=on';
+           push @$machineFlags, 'kernel-irqchip=split';
+       } elsif ($viommu eq 'virtio') {
+           push @$devices, '-device', 'virtio-iommu-pci';
+       }
+    }
+
     push @$cmd, @$devices;
     push @$cmd, '-rtc', join(',', @$rtcFlags) if scalar(@$rtcFlags);
     push @$cmd, '-machine', join(',', @$machineFlags) if scalar(@$machineFlags);
@@ -4599,9 +4598,10 @@ sub qemu_cpu_hotplug {
        if scalar(@{$currentrunningvcpus}) != $currentvcpus;
 
     if (PVE::QemuServer::Machine::machine_version($machine_type, 2, 7)) {
+       my $arch = get_vm_arch($conf);
 
        for (my $i = $currentvcpus+1; $i <= $vcpus; $i++) {
-           my $cpustr = print_cpu_device($conf, $i);
+           my $cpustr = print_cpu_device($conf, $arch, $i);
            qemu_deviceadd($vmid, $cpustr);
 
            my $retry = 0;
@@ -4691,24 +4691,29 @@ sub qemu_volume_snapshot {
 }
 
 sub qemu_volume_snapshot_delete {
-    my ($vmid, $deviceid, $storecfg, $volid, $snap) = @_;
+    my ($vmid, $storecfg, $volid, $snap) = @_;
 
     my $running = check_running($vmid);
+    my $attached_deviceid;
 
-    if($running) {
-
-       $running = undef;
+    if ($running) {
        my $conf = PVE::QemuConfig->load_config($vmid);
        PVE::QemuConfig->foreach_volume($conf, sub {
            my ($ds, $drive) = @_;
-           $running = 1 if $drive->{file} eq $volid;
+           $attached_deviceid = "drive-$ds" if $drive->{file} eq $volid;
        });
     }
 
-    if ($running && do_snapshots_with_qemu($storecfg, $volid, $deviceid)) {
-       mon_cmd($vmid, 'blockdev-snapshot-delete-internal-sync', device => $deviceid, name => $snap);
+    if ($attached_deviceid && do_snapshots_with_qemu($storecfg, $volid, $attached_deviceid)) {
+       mon_cmd(
+           $vmid,
+           'blockdev-snapshot-delete-internal-sync',
+           device => $attached_deviceid,
+           name => $snap,
+       );
     } else {
-       PVE::Storage::volume_snapshot_delete($storecfg, $volid, $snap, $running);
+       PVE::Storage::volume_snapshot_delete(
+           $storecfg, $volid, $snap, $attached_deviceid ? 1 : undef);
     }
 }
 
@@ -5207,19 +5212,21 @@ sub vmconfig_apply_pending {
            if (defined($conf->{$opt}) && is_valid_drivename($opt)) {
                vmconfig_register_unused_drive($storecfg, $vmid, $conf, parse_drive($opt, $conf->{$opt}))
            } elsif (defined($conf->{pending}->{$opt}) && $opt =~ m/^net\d+$/) {
-               if($have_sdn) {
-                    my $new_net = PVE::QemuServer::parse_net($conf->{pending}->{$opt});
-                   if ($conf->{$opt}){
-                       my $old_net = PVE::QemuServer::parse_net($conf->{$opt});
+               return if !$have_sdn; # return from eval if SDN is not available
 
-                       if ($old_net->{bridge} ne $new_net->{bridge} ||
-                           $old_net->{macaddr} ne $new_net->{macaddr}) {
-                           PVE::Network::SDN::Vnets::del_ips_from_mac($old_net->{bridge}, $old_net->{macaddr}, $conf->{name});
-                       }
-                  }
-                  #fixme: reuse ip if mac change && same bridge
-                  PVE::Network::SDN::Vnets::add_next_free_cidr($new_net->{bridge}, $conf->{name}, $new_net->{macaddr}, $vmid, undef, 1);
+               my $new_net = PVE::QemuServer::parse_net($conf->{pending}->{$opt});
+               if ($conf->{$opt}) {
+                   my $old_net = PVE::QemuServer::parse_net($conf->{$opt});
+
+                   if (defined($old_net->{bridge}) && defined($old_net->{macaddr}) && (
+                       safe_string_ne($old_net->{bridge}, $new_net->{bridge}) ||
+                       safe_string_ne($old_net->{macaddr}, $new_net->{macaddr})
+                   )) {
+                       PVE::Network::SDN::Vnets::del_ips_from_mac($old_net->{bridge}, $old_net->{macaddr}, $conf->{name});
+                   }
                }
+               #fixme: reuse ip if mac change && same bridge
+               PVE::Network::SDN::Vnets::add_next_free_cidr($new_net->{bridge}, $conf->{name}, $new_net->{macaddr}, $vmid, undef, 1);
            }
        };
        if (my $err = $@) {
@@ -5258,13 +5265,14 @@ sub vmconfig_update_net {
            safe_string_ne($oldnet->{macaddr}, $newnet->{macaddr}) ||
            safe_num_ne($oldnet->{queues}, $newnet->{queues}) ||
            safe_num_ne($oldnet->{mtu}, $newnet->{mtu}) ||
-           !($newnet->{bridge} && $oldnet->{bridge})) { # bridge/nat mode change
+           !($newnet->{bridge} && $oldnet->{bridge})
+       ) { # bridge/nat mode change
 
             # for non online change, we try to hot-unplug
            die "skip\n" if !$hotplug;
            vm_deviceunplug($vmid, $conf, $opt);
 
-           if($have_sdn) {
+           if ($have_sdn) {
                PVE::Network::SDN::Vnets::del_ips_from_mac($oldnet->{bridge}, $oldnet->{macaddr}, $conf->{name});
            }
 
@@ -5276,12 +5284,14 @@ sub vmconfig_update_net {
            if (safe_string_ne($oldnet->{bridge}, $newnet->{bridge}) ||
                safe_num_ne($oldnet->{tag}, $newnet->{tag}) ||
                safe_string_ne($oldnet->{trunks}, $newnet->{trunks}) ||
-               safe_num_ne($oldnet->{firewall}, $newnet->{firewall})) {
+               safe_num_ne($oldnet->{firewall}, $newnet->{firewall})
+           ) {
                PVE::Network::tap_unplug($iface);
 
                #set link_down in guest if bridge or vlan change to notify guest (dhcp renew for example)
                if (safe_string_ne($oldnet->{bridge}, $newnet->{bridge}) ||
-                   safe_num_ne($oldnet->{tag}, $newnet->{tag})) {
+                   safe_num_ne($oldnet->{tag}, $newnet->{tag})
+               ) {
                    qemu_set_link_status($vmid, $opt, 0);
                }
 
@@ -5300,7 +5310,8 @@ sub vmconfig_update_net {
 
                #set link_up in guest if bridge or vlan change to notify guest (dhcp renew for example)
                if (safe_string_ne($oldnet->{bridge}, $newnet->{bridge}) ||
-                   safe_num_ne($oldnet->{tag}, $newnet->{tag})) {
+                   safe_num_ne($oldnet->{tag}, $newnet->{tag})
+               ) {
                    qemu_set_link_status($vmid, $opt, 1);
                }
 
@@ -5380,8 +5391,10 @@ sub vmconfig_update_disk {
                    safe_string_ne($drive->{discard}, $old_drive->{discard}) ||
                    safe_string_ne($drive->{iothread}, $old_drive->{iothread}) ||
                    safe_string_ne($drive->{queues}, $old_drive->{queues}) ||
+                   safe_string_ne($drive->{product}, $old_drive->{product}) ||
                    safe_string_ne($drive->{cache}, $old_drive->{cache}) ||
                    safe_string_ne($drive->{ssd}, $old_drive->{ssd}) ||
+                   safe_string_ne($drive->{vendor}, $old_drive->{vendor}) ||
                    safe_string_ne($drive->{ro}, $old_drive->{ro})) {
                    die "skip\n";
                }
@@ -5612,12 +5625,10 @@ sub vm_start {
 #   timeout => in seconds
 #   paused => start VM in paused state (backup)
 #   resume => resume from hibernation
-#   pbs-backing => {
+#   live-restore-backing => {
 #      sata0 => {
-#         repository
-#         snapshot
-#         keyfile
-#         archive
+#          name => blockdev-name,
+#          blockdev => "arg to the -blockdev command instantiating device named 'name'",
 #      },
 #      virtio2 => ...
 #   }
@@ -5691,7 +5702,7 @@ sub vm_start_nolock {
     }
 
     my ($cmd, $vollist, $spice_port, $pci_devices) = config_to_command($storecfg, $vmid,
-       $conf, $defaults, $forcemachine, $forcecpu, $params->{'pbs-backing'});
+       $conf, $defaults, $forcemachine, $forcecpu, $params->{'live-restore-backing'});
 
     my $migration_ip;
     my $get_migration_ip = sub {
@@ -6137,12 +6148,20 @@ sub cleanup_pci_devices {
            my $dev_sysfs_dir = "/sys/bus/mdev/devices/$uuid";
 
            # some nvidia vgpu driver versions want to clean the mdevs up themselves, and error
-           # out when we do it first. so wait for 10 seconds and then try it
-           if ($d->{ids}->[0]->[0]->{vendor} =~ m/^(0x)?10de$/) {
-               sleep 10;
+           # out when we do it first. so wait for up to 10 seconds and then try it manually
+           if ($d->{ids}->[0]->[0]->{vendor} =~ m/^(0x)?10de$/ && -e $dev_sysfs_dir) {
+               my $count = 0;
+               while (-e $dev_sysfs_dir && $count < 10) {
+                   sleep 1;
+                   $count++;
+               }
+               print "waited $count seconds for mediated device driver finishing clean up\n";
            }
 
-           PVE::SysFSTools::file_write("$dev_sysfs_dir/remove", "1") if -e $dev_sysfs_dir;
+           if (-e $dev_sysfs_dir) {
+               print "actively clean up mediated device with UUID $uuid\n";
+               PVE::SysFSTools::file_write("$dev_sysfs_dir/remove", "1");
+           }
        }
     }
     PVE::QemuServer::PCI::remove_pci_reservation($vmid);
@@ -6387,7 +6406,8 @@ sub vm_suspend {
            if ($err) {
                # cleanup, but leave suspending lock, to indicate something went wrong
                eval {
-                   mon_cmd($vmid, "savevm-end");
+                   eval { mon_cmd($vmid, "savevm-end"); };
+                   warn $@ if $@;
                    PVE::Storage::deactivate_volumes($storecfg, [$vmstate]);
                    PVE::Storage::vdisk_free($storecfg, $vmstate);
                    delete $conf->@{qw(vmstate runningmachine runningcpu)};
@@ -7203,20 +7223,27 @@ sub pbs_live_restore {
     print "starting VM for live-restore\n";
     print "repository: '$opts->{repo}', snapshot: '$opts->{snapshot}'\n";
 
-    my $pbs_backing = {};
+    my $live_restore_backing = {};
     for my $ds (keys %$restored_disks) {
        $ds =~ m/^drive-(.*)$/;
        my $confname = $1;
-       $pbs_backing->{$confname} = {
+       my $pbs_conf = {};
+       $pbs_conf = {
            repository => $opts->{repo},
            snapshot => $opts->{snapshot},
            archive => "$ds.img.fidx",
        };
-       $pbs_backing->{$confname}->{keyfile} = $opts->{keyfile} if -e $opts->{keyfile};
-       $pbs_backing->{$confname}->{namespace} = $opts->{namespace} if defined($opts->{namespace});
+       $pbs_conf->{keyfile} = $opts->{keyfile} if -e $opts->{keyfile};
+       $pbs_conf->{namespace} = $opts->{namespace} if defined($opts->{namespace});
 
        my $drive = parse_drive($confname, $conf->{$confname});
        print "restoring '$ds' to '$drive->{file}'\n";
+
+       my $pbs_name = "drive-${confname}-pbs";
+       $live_restore_backing->{$confname} = {
+           name => $pbs_name,
+           blockdev => print_pbs_blockdev($pbs_conf, $pbs_name),
+       };
     }
 
     my $drives_streamed = 0;
@@ -7228,7 +7255,7 @@ sub pbs_live_restore {
 
        # start VM with backing chain pointing to PBS backup, environment vars for PBS driver
        # in QEMU (PBS_PASSWORD and PBS_FINGERPRINT) are already set by our caller
-       vm_start_nolock($storecfg, $vmid, $conf, {paused => 1, 'pbs-backing' => $pbs_backing}, {});
+       vm_start_nolock($storecfg, $vmid, $conf, {paused => 1, 'live-restore-backing' => $live_restore_backing}, {});
 
        my $qmeventd_fd = register_qmeventd_handle($vmid);
 
@@ -7268,6 +7295,93 @@ sub pbs_live_restore {
     }
 }
 
+# Inspired by pbs live-restore, this restores with the disks being available as files.
+# Theoretically this can also be used to quick-start a full-clone vm if the
+# disks are all available as files.
+#
+# The mapping should provide a path by config entry, such as
+# `{ scsi0 => { format => <qcow2|raw|...>, path => "/path/to/file", sata1 => ... } }`
+#
+# This is used when doing a `create` call with the `--live-import` parameter,
+# where the disks get an `import-from=` property. The non-live part is
+# therefore already handled in the `$create_disks()` call happening in the
+# `create` api call
+sub live_import_from_files {
+    my ($mapping, $vmid, $conf, $restore_options) = @_;
+
+    my $live_restore_backing = {};
+    for my $dev (keys %$mapping) {
+       die "disk not support for live-restoring: '$dev'\n"
+           if !is_valid_drivename($dev) || $dev =~ /^(?:efidisk|tpmstate)/;
+
+       die "mapping contains disk '$dev' which does not exist in the config\n"
+           if !exists($conf->{$dev});
+
+       my $info = $mapping->{$dev};
+       my ($format, $path) = $info->@{qw(format path)};
+       die "missing path for '$dev' mapping\n" if !$path;
+       die "missing format for '$dev' mapping\n" if !$format;
+       die "invalid format '$format' for '$dev' mapping\n"
+           if !grep { $format eq $_ } qw(raw qcow2 vmdk);
+
+       $live_restore_backing->{$dev} = {
+           name => "drive-$dev-restore",
+           blockdev => "driver=$format,node-name=drive-$dev-restore"
+           . ",read-only=on"
+           . ",file.driver=file,file.filename=$path"
+       };
+    };
+
+    my $storecfg = PVE::Storage::config();
+    eval {
+
+       # make sure HA doesn't interrupt our restore by stopping the VM
+       if (PVE::HA::Config::vm_is_ha_managed($vmid)) {
+           run_command(['ha-manager', 'set',  "vm:$vmid", '--state', 'started']);
+       }
+
+       vm_start_nolock($storecfg, $vmid, $conf, {paused => 1, 'live-restore-backing' => $live_restore_backing}, {});
+
+       # prevent shutdowns from qmeventd when the VM powers off from the inside
+       my $qmeventd_fd = register_qmeventd_handle($vmid);
+
+       # begin streaming, i.e. data copy from PBS to target disk for every vol,
+       # this will effectively collapse the backing image chain consisting of
+       # [target <- alloc-track -> PBS snapshot] to just [target] (alloc-track
+       # removes itself once all backing images vanish with 'auto-remove=on')
+       my $jobs = {};
+       for my $ds (sort keys %$live_restore_backing) {
+           my $job_id = "restore-$ds";
+           mon_cmd($vmid, 'block-stream',
+               'job-id' => $job_id,
+               device => "drive-$ds",
+           );
+           $jobs->{$job_id} = {};
+       }
+
+       mon_cmd($vmid, 'cont');
+       qemu_drive_mirror_monitor($vmid, undef, $jobs, 'auto', 0, 'stream');
+
+       print "restore-drive jobs finished successfully, removing all tracking block devices\n";
+
+       for my $ds (sort keys %$live_restore_backing) {
+           mon_cmd($vmid, 'blockdev-del', 'node-name' => "drive-$ds-restore");
+       }
+
+       close($qmeventd_fd);
+    };
+
+    my $err = $@;
+
+    if ($err) {
+       warn "An error occurred during live-restore: $err\n";
+       _do_vm_stop($storecfg, $vmid, 1, 1, 10, 0, 1);
+       die "live-restore failed\n";
+    }
+
+    PVE::QemuConfig->remove_lock($vmid, "import");
+}
+
 sub restore_vma_archive {
     my ($archive, $vmid, $user, $opts, $comp) = @_;
 
@@ -7772,7 +7886,11 @@ sub qemu_img_convert {
 sub qemu_img_format {
     my ($scfg, $volname) = @_;
 
-    if ($scfg->{path} && $volname =~ m/\.($PVE::QemuServer::Drive::QEMU_FORMAT_RE)$/) {
+    # FIXME: this entire function is kind of weird given that `parse_volname`
+    # also already gives us a format?
+    my $is_path_storage = $scfg->{path} || $scfg->{type} eq 'esxi';
+
+    if ($is_path_storage && $volname =~ m/\.($PVE::QemuServer::Drive::QEMU_FORMAT_RE)$/) {
        return $1;
     } else {
        return "raw";
@@ -8040,7 +8158,8 @@ sub clone_disk {
     my ($newvmid, $dst_drivename, $efisize) = $dest->@{qw(vmid drivename efisize)};
     my ($storage, $format) = $dest->@{qw(storage format)};
 
-    my $use_drive_mirror = $full && $running && $src_drivename && !$snapname;
+    my $unused = defined($src_drivename) && $src_drivename =~ /^unused/;
+    my $use_drive_mirror = $full && $running && $src_drivename && !$snapname && !$unused;
 
     if ($src_drivename && $dst_drivename && $src_drivename ne $dst_drivename) {
        die "cloning from/to EFI disk requires EFI disk\n"
@@ -8146,7 +8265,7 @@ no_data_clone:
     my $disk = dclone($drive);
     delete $disk->{format};
     $disk->{file} = $newvolid;
-    $disk->{size} = $size if defined($size);
+    $disk->{size} = $size if defined($size) && !$unused;
 
     return $disk;
 }