]> git.proxmox.com Git - qemu-server.git/blobdiff - PVE/QemuServer.pm
cleanup: get rid of unnecessary closures
[qemu-server.git] / PVE / QemuServer.pm
index 510a9958c302b51412fd615fea2e4b4f5b4829a5..efacc459de55ba1dfd0c25647b416a50843f669d 100644 (file)
@@ -44,7 +44,7 @@ use PVE::QemuConfig;
 use PVE::QemuServer::Helpers qw(min_version config_aware_timeout);
 use PVE::QemuServer::Cloudinit;
 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 parse_drive print_drive foreach_drive foreach_volid);
+use PVE::QemuServer::Drive qw(is_valid_drivename drive_is_cloudinit drive_is_cdrom parse_drive print_drive);
 use PVE::QemuServer::Machine;
 use PVE::QemuServer::Memory;
 use PVE::QemuServer::Monitor qw(mon_cmd);
@@ -97,6 +97,28 @@ PVE::JSONSchema::register_standard_option('pve-qemu-machine', {
        optional => 1,
 });
 
+
+sub map_storage {
+    my ($map, $source) = @_;
+
+    return $source if !defined($map);
+
+    return $map->{entries}->{$source}
+       if $map->{entries} && defined($map->{entries}->{$source});
+
+    return $map->{default} if $map->{default};
+
+    # identity (fallback)
+    return $source;
+}
+
+PVE::JSONSchema::register_standard_option('pve-targetstorage', {
+    description => "Mapping from source to target storages. Providing only a single storage ID maps all source storages to that storage. Providing the special value '1' will map each source storage to itself.",
+    type => 'string',
+    format => 'storagepair-list',
+    optional => 1,
+});
+
 #no warnings 'redefine';
 
 sub cgroups_write {
@@ -567,8 +589,15 @@ EODESCR
        optional => 1,
     }),
     runningmachine => get_standard_option('pve-qemu-machine', {
-       description => "Specifies the Qemu machine type of the running vm. This is used internally for snapshots.",
+       description => "Specifies the QEMU machine type of the running vm. This is used internally for snapshots.",
     }),
+    runningcpu => {
+       description => "Specifies the QEMU '-cpu' parameter of the running vm. This is used internally for snapshots.",
+       optional => 1,
+       type => 'string',
+       pattern => $PVE::QemuServer::CPUConfig::qemu_cmdline_cpu_re,
+       format_description => 'QEMU -cpu parameter'
+    },
     machine => get_standard_option('pve-qemu-machine'),
     arch => {
        description => "Virtual processor architecture. Defaults to the host.",
@@ -817,6 +846,7 @@ my $net_fmt = {
        type => 'string',
        description => $net_fmt_bridge_descr,
        format_description => 'bridge',
+       pattern => '[-_.\w\d]+',
        optional => 1,
     },
     queues => {
@@ -1948,7 +1978,8 @@ sub json_config_properties {
     my $prop = shift;
 
     foreach my $opt (keys %$confdesc) {
-       next if $opt eq 'parent' || $opt eq 'snaptime' || $opt eq 'vmstate' || $opt eq 'runningmachine';
+       next if $opt eq 'parent' || $opt eq 'snaptime' || $opt eq 'vmstate' ||
+           $opt eq 'runningmachine' || $opt eq 'runningcpu';
        $prop->{$opt} = $confdesc->{$opt};
     }
 
@@ -2007,7 +2038,7 @@ sub destroy_vm {
 
     if ($conf->{template}) {
        # check if any base image is still used by a linked clone
-       foreach_drive($conf, sub {
+       PVE::QemuConfig->foreach_volume($conf, sub {
                my ($ds, $drive) = @_;
                return if drive_is_cdrom($drive);
 
@@ -2021,7 +2052,7 @@ sub destroy_vm {
     }
 
     # only remove disks owned by this VM
-    foreach_drive($conf, sub {
+    PVE::QemuConfig->foreach_volume($conf, sub {
        my ($ds, $drive) = @_;
        return if drive_is_cdrom($drive, 1);
 
@@ -2310,7 +2341,7 @@ sub check_local_resources {
 sub check_storage_availability {
     my ($storecfg, $conf, $node) = @_;
 
-    foreach_drive($conf, sub {
+    PVE::QemuConfig->foreach_volume($conf, sub {
        my ($ds, $drive) = @_;
 
        my $volid = $drive->{file};
@@ -2333,7 +2364,7 @@ sub shared_nodes {
     my $nodehash = { map { $_ => 1 } @$nodelist };
     my $nodename = nodename();
 
-    foreach_drive($conf, sub {
+    PVE::QemuConfig->foreach_volume($conf, sub {
        my ($ds, $drive) = @_;
 
        my $volid = $drive->{file};
@@ -2365,7 +2396,7 @@ sub check_local_storage_availability {
     my $nodelist = PVE::Cluster::get_nodelist();
     my $nodehash = { map { $_ => {} } @$nodelist };
 
-    foreach_drive($conf, sub {
+    PVE::QemuConfig->foreach_volume($conf, sub {
        my ($ds, $drive) = @_;
 
        my $volid = $drive->{file};
@@ -2720,6 +2751,29 @@ sub conf_has_audio {
     };
 }
 
+sub audio_devs {
+    my ($audio, $audiopciaddr) = @_;
+
+    my $devs = [];
+
+    my $id = $audio->{dev_id};
+    my $audiodev = "audiodev=$audio->{backend_id}";
+
+    if ($audio->{dev} eq 'AC97') {
+       push @$devs, '-device', "AC97,id=${id}${audiopciaddr},$audiodev";
+    } elsif ($audio->{dev} =~ /intel\-hda$/) {
+       push @$devs, '-device', "$audio->{dev},id=${id}${audiopciaddr}";
+       push @$devs, '-device', "hda-micro,id=${id}-codec0,bus=${id}.0,cad=0,$audiodev";
+       push @$devs, '-device', "hda-duplex,id=${id}-codec1,bus=${id}.0,cad=1,$audiodev";
+    } else {
+       die "unkown audio device '$audio->{dev}', implement me!";
+    }
+
+    push @$devs, '-audiodev', "$audio->{backend},id=$audio->{backend_id}";
+
+    return $devs;
+}
+
 sub vga_conf_has_spice {
     my ($vga) = @_;
 
@@ -2911,7 +2965,7 @@ sub query_understood_cpu_flags {
 }
 
 sub config_to_command {
-    my ($storecfg, $vmid, $conf, $defaults, $forcemachine) = @_;
+    my ($storecfg, $vmid, $conf, $defaults, $forcemachine, $forcecpu) = @_;
 
     my $cmd = [];
     my $globalFlags = [];
@@ -2942,13 +2996,15 @@ sub config_to_command {
 
     $machine_version =~ m/(\d+)\.(\d+)/;
     my ($machine_major, $machine_minor) = ($1, $2);
-    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, $machine_major, $machine_minor);
 
-    if (!PVE::QemuServer::Machine::can_run_pve_machine_version($machine_version, $kvmver)) {
+    if ($kvmver =~ m/^\d+\.\d+\.(\d+)/ && $1 >= 90) {
+       warn "warning: Installed QEMU version ($kvmver) is a release candidate, ignoring version checks\n";
+    } elsif (!min_version($kvmver, $machine_major, $machine_minor)) {
+       die "Installed QEMU version '$kvmver' is too old to run machine type '$machine_type', please upgrade node '$nodename'\n"
+    } elsif (!PVE::QemuServer::Machine::can_run_pve_machine_version($machine_version, $kvmver)) {
        my $max_pve_version = PVE::QemuServer::Machine::get_pve_version($machine_version);
        die "Installed qemu-server (max feature level for $machine_major.$machine_minor is pve$max_pve_version)"
-         " is too old to run machine type '$machine_type', please upgrade node '$nodename'\n";
+           ." is too old to run machine type '$machine_type', please upgrade node '$nodename'\n";
     }
 
     # if a specific +pve version is required for a feature, use $version_guard
@@ -3023,14 +3079,12 @@ sub config_to_command {
        }
     }
 
-    my ($ovmf_code, $ovmf_vars) = get_ovmf_files($arch);
     if ($conf->{bios} && $conf->{bios} eq 'ovmf') {
-       die "uefi base image not found\n" if ! -f $ovmf_code;
+       my ($ovmf_code, $ovmf_vars) = get_ovmf_files($arch);
+       die "uefi base image '$ovmf_code' not found\n" if ! -f $ovmf_code;
 
-       my $path;
-       my $format;
-       if (my $efidisk = $conf->{efidisk0}) {
-           my $d = parse_drive('efidisk0', $efidisk);
+       my ($path, $format);
+       if (my $d = parse_drive('efidisk0', $conf->{efidisk0})) {
            my ($storeid, $volname) = PVE::Storage::parse_volume_id($d->{file}, 1);
            $format = $d->{format};
            if ($storeid) {
@@ -3219,22 +3273,10 @@ sub config_to_command {
        }
     }
 
-    if (my $audio = conf_has_audio($conf)) {
-
+    if (min_version($machine_version, 4, 0) && (my $audio = conf_has_audio($conf))) {
        my $audiopciaddr = print_pci_addr("audio0", $bridges, $arch, $machine_type);
-
-       my $id = $audio->{dev_id};
-       if ($audio->{dev} eq 'AC97') {
-           push @$devices, '-device', "AC97,id=${id}${audiopciaddr}";
-       } elsif ($audio->{dev} =~ /intel\-hda$/) {
-           push @$devices, '-device', "$audio->{dev},id=${id}${audiopciaddr}";
-           push @$devices, '-device', "hda-micro,id=${id}-codec0,bus=${id}.0,cad=0";
-           push @$devices, '-device', "hda-duplex,id=${id}-codec1,bus=${id}.0,cad=1";
-       } else {
-           die "unkown audio device '$audio->{dev}', implement me!";
-       }
-
-       push @$devices, '-audiodev', "$audio->{backend},id=$audio->{backend_id}";
+       my $audio_devs = audio_devs($audio, $audiopciaddr);
+       push @$devices, @$audio_devs;
     }
 
     my $sockets = 1;
@@ -3292,7 +3334,6 @@ sub config_to_command {
 
     # time drift fix
     my $tdf = defined($conf->{tdf}) ? $conf->{tdf} : $defaults->{tdf};
-
     my $useLocaltime = $conf->{localtime};
 
     if ($winversion >= 5) { # windows
@@ -3311,13 +3352,17 @@ sub config_to_command {
 
     push @$rtcFlags, 'driftfix=slew' if $tdf;
 
-    if (($conf->{startdate}) && ($conf->{startdate} ne 'now')) {
+    if ($conf->{startdate} && $conf->{startdate} ne 'now') {
        push @$rtcFlags, "base=$conf->{startdate}";
     } elsif ($useLocaltime) {
        push @$rtcFlags, 'base=localtime';
     }
 
-    push @$cmd, get_cpu_options($conf, $arch, $kvm, $kvm_off, $machine_version, $winversion, $gpu_passthrough);
+    if ($forcecpu) {
+       push @$cmd, '-cpu', $forcecpu;
+    } else {
+       push @$cmd, get_cpu_options($conf, $arch, $kvm, $kvm_off, $machine_version, $winversion, $gpu_passthrough);
+    }
 
     PVE::QemuServer::Memory::config($conf, $vmid, $sockets, $cores, $defaults, $hotplug_features, $cmd);
 
@@ -3365,7 +3410,7 @@ sub config_to_command {
     if ($qxlnum) {
        if ($qxlnum > 1) {
            if ($winversion){
-               for(my $i = 1; $i < $qxlnum; $i++){
+               for (my $i = 1; $i < $qxlnum; $i++){
                    push @$devices, '-device', print_vga_device($conf, $vga, $arch, $machine_version, $machine_type, $i, $qxlnum, $bridges);
                }
            } else {
@@ -3428,7 +3473,7 @@ sub config_to_command {
        push @$devices, '-iscsi', "initiator-name=$initiator";
     }
 
-    foreach_drive($conf, sub {
+    PVE::QemuConfig->foreach_volume($conf, sub {
        my ($ds, $drive) = @_;
 
        if (PVE::Storage::parse_volume_id($drive->{file}, 1)) {
@@ -3452,11 +3497,11 @@ sub config_to_command {
            }
        }
 
-       if($drive->{interface} eq 'virtio'){
+       if ($drive->{interface} eq 'virtio'){
            push @$cmd, '-object', "iothread,id=iothread-$ds" if $drive->{iothread};
        }
 
-        if ($drive->{interface} eq 'scsi') {
+       if ($drive->{interface} eq 'scsi') {
 
            my ($maxdev, $controller, $controller_prefix) = scsihw_infos($conf, $drive);
 
@@ -3481,13 +3526,13 @@ sub config_to_command {
 
            push @$devices, '-device', "$scsihw_type,id=$controller_prefix$controller$pciaddr$iothread$queues" if !$scsicontroller->{$controller};
            $scsicontroller->{$controller}=1;
-        }
+       }
 
         if ($drive->{interface} eq 'sata') {
-           my $controller = int($drive->{index} / $PVE::QemuServer::Drive::MAX_SATA_DISKS);
-           $pciaddr = print_pci_addr("ahci$controller", $bridges, $arch, $machine_type);
-           push @$devices, '-device', "ahci,id=ahci$controller,multifunction=on$pciaddr" if !$ahcicontroller->{$controller};
-           $ahcicontroller->{$controller}=1;
+           my $controller = int($drive->{index} / $PVE::QemuServer::Drive::MAX_SATA_DISKS);
+           $pciaddr = print_pci_addr("ahci$controller", $bridges, $arch, $machine_type);
+           push @$devices, '-device', "ahci,id=ahci$controller,multifunction=on$pciaddr" if !$ahcicontroller->{$controller};
+           $ahcicontroller->{$controller}=1;
         }
 
        my $drive_cmd = print_drive_commandline_full($storecfg, $vmid, $drive);
@@ -3496,22 +3541,22 @@ sub config_to_command {
     });
 
     for (my $i = 0; $i < $MAX_NETS; $i++) {
-         next if !$conf->{"net$i"};
-         my $d = parse_net($conf->{"net$i"});
-         next if !$d;
+        next if !$conf->{"net$i"};
+        my $d = parse_net($conf->{"net$i"});
+        next if !$d;
 
-         $use_virtio = 1 if $d->{model} eq 'virtio';
+        $use_virtio = 1 if $d->{model} eq 'virtio';
 
-         if ($bootindex_hash->{n}) {
-            $d->{bootindex} = $bootindex_hash->{n};
-            $bootindex_hash->{n} += 1;
-         }
+        if ($bootindex_hash->{n}) {
+           $d->{bootindex} = $bootindex_hash->{n};
+           $bootindex_hash->{n} += 1;
+        }
 
-         my $netdevfull = print_netdev_full($vmid, $conf, $arch, $d, "net$i");
-         push @$devices, '-netdev', $netdevfull;
+        my $netdevfull = print_netdev_full($vmid, $conf, $arch, $d, "net$i");
+        push @$devices, '-netdev', $netdevfull;
 
-         my $netdevicefull = print_netdevice_full($vmid, $conf, $d, "net$i", $bridges, $use_old_bios_files, $arch, $machine_type);
-         push @$devices, '-device', $netdevicefull;
+        my $netdevicefull = print_netdevice_full($vmid, $conf, $d, "net$i", $bridges, $use_old_bios_files, $arch, $machine_type);
+        push @$devices, '-device', $netdevicefull;
     }
 
     if ($conf->{ivshmem}) {
@@ -3569,12 +3614,9 @@ sub config_to_command {
     push @$machineFlags, "type=${machine_type_min}";
 
     push @$cmd, @$devices;
-    push @$cmd, '-rtc', join(',', @$rtcFlags)
-       if scalar(@$rtcFlags);
-    push @$cmd, '-machine', join(',', @$machineFlags)
-       if scalar(@$machineFlags);
-    push @$cmd, '-global', join(',', @$globalFlags)
-       if scalar(@$globalFlags);
+    push @$cmd, '-rtc', join(',', @$rtcFlags) if scalar(@$rtcFlags);
+    push @$cmd, '-machine', join(',', @$machineFlags) if scalar(@$machineFlags);
+    push @$cmd, '-global', join(',', @$globalFlags) if scalar(@$globalFlags);
 
     if (my $vmstate = $conf->{vmstate}) {
        my $statepath = PVE::Storage::path($storecfg, $vmstate);
@@ -4217,7 +4259,7 @@ sub qemu_volume_snapshot_delete {
 
        $running = undef;
        my $conf = PVE::QemuConfig->load_config($vmid);
-       foreach_drive($conf, sub {
+       PVE::QemuConfig->foreach_volume($conf, sub {
            my ($ds, $drive) = @_;
            $running = 1 if $drive->{file} eq $volid;
        });
@@ -4255,6 +4297,59 @@ sub set_migration_caps {
     mon_cmd($vmid, "migrate-set-capabilities", capabilities => $cap_ref);
 }
 
+sub foreach_volid {
+    my ($conf, $func, @param) = @_;
+
+    my $volhash = {};
+
+    my $test_volid = sub {
+       my ($key, $drive, $snapname) = @_;
+
+       my $volid = $drive->{file};
+       return if !$volid;
+
+       $volhash->{$volid}->{cdrom} //= 1;
+       $volhash->{$volid}->{cdrom} = 0 if !drive_is_cdrom($drive);
+
+       my $replicate = $drive->{replicate} // 1;
+       $volhash->{$volid}->{replicate} //= 0;
+       $volhash->{$volid}->{replicate} = 1 if $replicate;
+
+       $volhash->{$volid}->{shared} //= 0;
+       $volhash->{$volid}->{shared} = 1 if $drive->{shared};
+
+       $volhash->{$volid}->{referenced_in_config} //= 0;
+       $volhash->{$volid}->{referenced_in_config} = 1 if !defined($snapname);
+
+       $volhash->{$volid}->{referenced_in_snapshot}->{$snapname} = 1
+           if defined($snapname);
+
+       my $size = $drive->{size};
+       $volhash->{$volid}->{size} //= $size if $size;
+
+       $volhash->{$volid}->{is_vmstate} //= 0;
+       $volhash->{$volid}->{is_vmstate} = 1 if $key eq 'vmstate';
+
+       $volhash->{$volid}->{is_unused} //= 0;
+       $volhash->{$volid}->{is_unused} = 1 if $key =~ /^unused\d+$/;
+    };
+
+    my $include_opts = {
+       extra_keys => ['vmstate'],
+       include_unused => 1,
+    };
+
+    PVE::QemuConfig->foreach_volume_full($conf, $include_opts, $test_volid);
+    foreach my $snapname (keys %{$conf->{snapshots}}) {
+       my $snap = $conf->{snapshots}->{$snapname};
+       PVE::QemuConfig->foreach_volume_full($snap, $include_opts, $test_volid, $snapname);
+    }
+
+    foreach my $volid (keys %$volhash) {
+       &$func($volid, $volhash->{$volid}, @param);
+    }
+}
+
 my $fast_plug_option = {
     'lock' => 1,
     'name' => 1,
@@ -4710,11 +4805,11 @@ sub vmconfig_update_disk {
 }
 
 # called in locked context by incoming migration
-sub vm_migrate_alloc_nbd_disks {
-    my ($storecfg, $vmid, $conf, $targetstorage, $replicated_volumes) = @_;
+sub vm_migrate_get_nbd_disks {
+    my ($storecfg, $conf, $replicated_volumes) = @_;
 
     my $local_volumes = {};
-    foreach_drive($conf, sub {
+    PVE::QemuConfig->foreach_volume($conf, sub {
        my ($ds, $drive) = @_;
 
        return if drive_is_cdrom($drive);
@@ -4727,27 +4822,36 @@ sub vm_migrate_alloc_nbd_disks {
 
        my $scfg = PVE::Storage::storage_config($storecfg, $storeid);
        return if $scfg->{shared};
-       $local_volumes->{$ds} = [$volid, $storeid, $volname];
+
+       # replicated disks re-use existing state via bitmap
+       my $use_existing = $replicated_volumes->{$volid} ? 1 : 0;
+       $local_volumes->{$ds} = [$volid, $storeid, $volname, $drive, $use_existing];
     });
+    return $local_volumes;
+}
+
+# called in locked context by incoming migration
+sub vm_migrate_alloc_nbd_disks {
+    my ($storecfg, $vmid, $source_volumes, $storagemap) = @_;
 
     my $format = undef;
 
     my $nbd = {};
-    foreach my $opt (sort keys %$local_volumes) {
-       my ($volid, $storeid, $volname) = @{$local_volumes->{$opt}};
-       if ($replicated_volumes->{$volid}) {
-           # re-use existing, replicated volume with bitmap on source side
-           $nbd->{$opt} = $conf->{${opt}};
-           print "re-using replicated volume: $opt - $volid\n";
+    foreach my $opt (sort keys %$source_volumes) {
+       my ($volid, $storeid, $volname, $drive, $use_existing) = @{$source_volumes->{$opt}};
+
+       if ($use_existing) {
+           $nbd->{$opt}->{drivestr} = print_drive($drive);
+           $nbd->{$opt}->{volid} = $volid;
+           $nbd->{$opt}->{replicated} = 1;
            next;
        }
-       my $drive = parse_drive($opt, $conf->{$opt});
 
        # If a remote storage is specified and the format of the original
        # volume is not available there, fall back to the default format.
        # Otherwise use the same format as the original.
-       if ($targetstorage && $targetstorage ne "1") {
-           $storeid = $targetstorage;
+       if (!$storagemap->{identity}) {
+           $storeid = map_storage($storagemap, $storeid);
            my ($defFormat, $validFormats) = PVE::Storage::storage_default_format($storecfg, $storeid);
            my $scfg = PVE::Storage::storage_config($storecfg, $storeid);
            my $fileFormat = qemu_img_format($scfg, $volname);
@@ -4762,9 +4866,8 @@ sub vm_migrate_alloc_nbd_disks {
        $newdrive->{format} = $format;
        $newdrive->{file} = $newvolid;
        my $drivestr = print_drive($newdrive);
-       $nbd->{$opt} = $drivestr;
-       #pass drive to conf for command line
-       $conf->{$opt} = $drivestr;
+       $nbd->{$opt}->{drivestr} = $drivestr;
+       $nbd->{$opt}->{volid} = $newvolid;
     }
 
     return $nbd;
@@ -4772,11 +4875,11 @@ sub vm_migrate_alloc_nbd_disks {
 
 # see vm_start_nolock for parameters, additionally:
 # migrate_opts:
-#   targetstorage = storageid/'1' - target storage for disks migrated over NBD
+#   storagemap = parsed storage map for allocating NBD disks
 sub vm_start {
     my ($storecfg, $vmid, $params, $migrate_opts) = @_;
 
-    PVE::QemuConfig->lock_config($vmid, sub {
+    return PVE::QemuConfig->lock_config($vmid, sub {
        my $conf = PVE::QemuConfig->load_config($vmid, $migrate_opts->{migratedfrom});
 
        die "you can't start a vm if it's a template\n" if PVE::QemuConfig->is_template($conf);
@@ -4788,10 +4891,17 @@ sub vm_start {
 
        die "VM $vmid already running\n" if check_running($vmid, undef, $migrate_opts->{migratedfrom});
 
-       $migrate_opts->{nbd} = vm_migrate_alloc_nbd_disks($storecfg, $vmid, $conf, $migrate_opts->{targetstorage}, $migrate_opts->{replicated_volumes})
-           if $migrate_opts->{targetstorage};
+       if (my $storagemap = $migrate_opts->{storagemap}) {
+           my $replicated = $migrate_opts->{replicated_volumes};
+           my $disks = vm_migrate_get_nbd_disks($storecfg, $conf, $replicated);
+           $migrate_opts->{nbd} = vm_migrate_alloc_nbd_disks($storecfg, $vmid, $disks, $storagemap);
+
+           foreach my $opt (keys %{$migrate_opts->{nbd}}) {
+               $conf->{$opt} = $migrate_opts->{nbd}->{$opt}->{drivestr};
+           }
+       }
 
-       vm_start_nolock($storecfg, $vmid, $conf, $params, $migrate_opts);
+       return vm_start_nolock($storecfg, $vmid, $conf, $params, $migrate_opts);
     });
 }
 
@@ -4800,11 +4910,12 @@ sub vm_start {
 #   statefile => 'tcp', 'unix' for migration or path/volid for RAM state
 #   skiplock => 0/1, skip checking for config lock
 #   forcemachine => to force Qemu machine (rollback/migration)
+#   forcecpu => a QEMU '-cpu' argument string to override get_cpu_options
 #   timeout => in seconds
 #   paused => start VM in paused state (backup)
 #   resume => resume from hibernation
 # migrate_opts:
-#   nbd => newly allocated volumes for NBD exports (vm_migrate_alloc_nbd_disks)
+#   nbd => volumes for NBD exports (vm_migrate_alloc_nbd_disks)
 #   migratedfrom => source node
 #   spice_ticket => used for spice migration, passed via tunnel/stdin
 #   network => CIDR of migration network
@@ -4820,6 +4931,8 @@ sub vm_start_nolock {
     my $migratedfrom = $migrate_opts->{migratedfrom};
     my $migration_type = $migrate_opts->{type};
 
+    my $res = {};
+
     # clean up leftover reboot request files
     eval { clear_reboot_request($vmid); };
     warn $@ if $@;
@@ -4839,13 +4952,16 @@ sub vm_start_nolock {
     PVE::GuestHelpers::exec_hookscript($conf, $vmid, 'pre-start', 1);
 
     my $forcemachine = $params->{forcemachine};
+    my $forcecpu = $params->{forcecpu};
     if ($resume) {
-       # enforce machine type on suspended vm to ensure HW compatibility
+       # enforce machine and CPU type on suspended vm to ensure HW compatibility
        $forcemachine = $conf->{runningmachine};
+       $forcecpu = $conf->{runningcpu};
        print "Resuming suspended VM\n";
     }
 
-    my ($cmd, $vollist, $spice_port) = config_to_command($storecfg, $vmid, $conf, $defaults, $forcemachine);
+    my ($cmd, $vollist, $spice_port) =
+       config_to_command($storecfg, $vmid, $conf, $defaults, $forcemachine, $forcecpu);
 
     my $migration_ip;
     my $get_migration_ip = sub {
@@ -5028,6 +5144,7 @@ sub vm_start_nolock {
     }
 
     print "migration listens on $migrate_uri\n" if $migrate_uri;
+    $res->{migrate_uri} = $migrate_uri;
 
     if ($statefile && $statefile ne 'tcp' && $statefile ne 'unix')  {
        eval { mon_cmd($vmid, "cont"); };
@@ -5055,10 +5172,19 @@ sub vm_start_nolock {
            $migrate_storage_uri = "nbd:${localip}:${storage_migrate_port}";
        }
 
+       $res->{migrate_storage_uri} = $migrate_storage_uri;
+
        foreach my $opt (sort keys %$nbd) {
-           my $drivestr = $nbd->{$opt};
+           my $drivestr = $nbd->{$opt}->{drivestr};
+           my $volid = $nbd->{$opt}->{volid};
            mon_cmd($vmid, "nbd-server-add", device => "drive-$opt", writable => JSON::true );
-           print "storage migration listens on $migrate_storage_uri:exportname=drive-$opt volume:$drivestr\n";
+           my $nbd_uri = "$migrate_storage_uri:exportname=drive-$opt";
+           print "storage migration listens on $nbd_uri volume:$drivestr\n";
+           print "re-using replicated volume: $opt - $volid\n"
+               if $nbd->{$opt}->{replicated};
+
+           $res->{drives}->{$opt} = $nbd->{$opt};
+           $res->{drives}->{$opt}->{nbd_uri} = $nbd_uri;
        }
     }
 
@@ -5070,6 +5196,7 @@ sub vm_start_nolock {
 
        if ($spice_port) {
            print "spice listens on port $spice_port\n";
+           $res->{spice_port} = $spice_port;
            if ($migrate_opts->{spice_ticket}) {
                mon_cmd($vmid, "set_password", protocol => 'spice', password => $migrate_opts->{spice_ticket});
                mon_cmd($vmid, "expire_password", protocol => 'spice', time => "+30");
@@ -5098,11 +5225,13 @@ sub vm_start_nolock {
            PVE::Storage::deactivate_volumes($storecfg, [$vmstate]);
            PVE::Storage::vdisk_free($storecfg, $vmstate);
        }
-       delete $conf->@{qw(lock vmstate runningmachine)};
+       delete $conf->@{qw(lock vmstate runningmachine runningcpu)};
        PVE::QemuConfig->write_config($vmid, $conf);
     }
 
     PVE::GuestHelpers::exec_hookscript($conf, $vmid, 'post-start');
+
+    return $res;
 }
 
 sub vm_commandline {
@@ -5110,13 +5239,15 @@ sub vm_commandline {
 
     my $conf = PVE::QemuConfig->load_config($vmid);
     my $forcemachine;
+    my $forcecpu;
 
     if ($snapname) {
        my $snapshot = $conf->{snapshots}->{$snapname};
        die "snapshot '$snapname' does not exist\n" if !defined($snapshot);
 
-       # check for a 'runningmachine' in snapshot
-       $forcemachine = $snapshot->{runningmachine} if $snapshot->{runningmachine};
+       # check for machine or CPU overrides in snapshot
+       $forcemachine = $snapshot->{runningmachine};
+       $forcecpu = $snapshot->{runningcpu};
 
        $snapshot->{digest} = $conf->{digest}; # keep file digest for API
 
@@ -5125,7 +5256,8 @@ sub vm_commandline {
 
     my $defaults = load_defaults();
 
-    my $cmd = config_to_command($storecfg, $vmid, $conf, $defaults, $forcemachine);
+    my $cmd = config_to_command($storecfg, $vmid, $conf, $defaults,
+       $forcemachine, $forcecpu);
 
     return PVE::Tools::cmd2string($cmd);
 }
@@ -5399,7 +5531,7 @@ sub vm_suspend {
                    mon_cmd($vmid, "savevm-end");
                    PVE::Storage::deactivate_volumes($storecfg, [$vmstate]);
                    PVE::Storage::vdisk_free($storecfg, $vmstate);
-                   delete $conf->@{qw(vmstate runningmachine)};
+                   delete $conf->@{qw(vmstate runningmachine runningcpu)};
                    PVE::QemuConfig->write_config($vmid, $conf);
                };
                warn $@ if $@;
@@ -5536,7 +5668,7 @@ sub restore_file_archive {
 my $restore_cleanup_oldconf = sub {
     my ($storecfg, $vmid, $oldconf, $virtdev_hash) = @_;
 
-    foreach_drive($oldconf, sub {
+    PVE::QemuConfig->foreach_volume($oldconf, sub {
        my ($ds, $drive) = @_;
 
        return if drive_is_cdrom($drive, 1);
@@ -5613,12 +5745,13 @@ my $parse_backup_hints = sub {
            my $drive = parse_drive($virtdev, $2);
            if (drive_is_cloudinit($drive)) {
                my ($storeid, $volname) = PVE::Storage::parse_volume_id($drive->{file});
+               $storeid = $options->{storage} if defined ($options->{storage});
                my $scfg = PVE::Storage::storage_config($storecfg, $storeid);
                my $format = qemu_img_format($scfg, $volname); # has 'raw' fallback
 
                $virtdev_hash->{$virtdev} = {
                    format => $format,
-                   storeid => $options->{storage} // $storeid,
+                   storeid => $storeid,
                    size => PVE::QemuServer::Cloudinit::CLOUDINIT_DISK_SIZE,
                    is_cloudinit => 1,
                };
@@ -6406,7 +6539,7 @@ sub foreach_storage_used_by_vm {
 
     my $sidhash = {};
 
-    foreach_drive($conf, sub {
+    PVE::QemuConfig->foreach_volume($conf, sub {
        my ($ds, $drive) = @_;
        return if drive_is_cdrom($drive);
 
@@ -6457,7 +6590,7 @@ sub template_create {
 
     my $storecfg = PVE::Storage::config();
 
-    foreach_drive($conf, sub {
+    PVE::QemuConfig->foreach_volume($conf, sub {
        my ($ds, $drive) = @_;
 
        return if drive_is_cdrom($drive);