]> git.proxmox.com Git - qemu-server.git/blobdiff - PVE/QemuServer.pm
vm_start: split out NBD disk allocation
[qemu-server.git] / PVE / QemuServer.pm
index a5b000e0cdd72ac23779170687e76448fe162fdc..510a9958c302b51412fd615fea2e4b4f5b4829a5 100644 (file)
@@ -545,7 +545,7 @@ EODESCR
        optional => 1,
        description => "Emulated CPU type.",
        type => 'string',
-       format => $PVE::QemuServer::CPUConfig::cpu_fmt,
+       format => 'pve-vm-cpu-conf',
     },
     parent => get_standard_option('pve-snapshot-name', {
        optional => 1,
@@ -1091,10 +1091,6 @@ for my $key (keys %{$PVE::QemuServer::Drive::drivedesc_hash}) {
     $confdesc->{$key} = $PVE::QemuServer::Drive::drivedesc_hash->{$key};
 }
 
-for (my $i = 0; $i < $PVE::QemuServer::Drive::MAX_UNUSED_DISKS; $i++) {
-    $confdesc->{"unused$i"} = $PVE::QemuServer::Drive::unuseddesc;
-}
-
 for (my $i = 0; $i < $MAX_USB_DEVICES; $i++)  {
     $confdesc->{"usb$i"} = $usbdesc;
 }
@@ -3055,8 +3051,14 @@ sub config_to_command {
            $format = 'raw';
        }
 
+       my $size_str = "";
+
+       if ($format eq 'raw' && $version_guard->(4, 1, 2)) {
+           $size_str = ",size=" . (-s $ovmf_vars);
+       }
+
        push @$cmd, '-drive', "if=pflash,unit=0,format=raw,readonly,file=$ovmf_code";
-       push @$cmd, '-drive', "if=pflash,unit=1,format=$format,id=drive-efidisk0,file=$path";
+       push @$cmd, '-drive', "if=pflash,unit=1,format=$format,id=drive-efidisk0$size_str,file=$path";
     }
 
     # load q35 config
@@ -4707,355 +4709,400 @@ sub vmconfig_update_disk {
     vm_deviceplug($storecfg, $conf, $vmid, $opt, $drive, $arch, $machine_type);
 }
 
-sub vm_start {
-    my ($storecfg, $vmid, $statefile, $skiplock, $migratedfrom, $paused,
-       $forcemachine, $spice_ticket, $migration_network, $migration_type,
-       $targetstorage, $timeout, $nbd_protocol_version) = @_;
+# called in locked context by incoming migration
+sub vm_migrate_alloc_nbd_disks {
+    my ($storecfg, $vmid, $conf, $targetstorage, $replicated_volumes) = @_;
 
-    PVE::QemuConfig->lock_config($vmid, sub {
-       my $conf = PVE::QemuConfig->load_config($vmid, $migratedfrom);
+    my $local_volumes = {};
+    foreach_drive($conf, sub {
+       my ($ds, $drive) = @_;
 
-       die "you can't start a vm if it's a template\n" if PVE::QemuConfig->is_template($conf);
+       return if drive_is_cdrom($drive);
 
-       my $is_suspended = PVE::QemuConfig->has_lock($conf, 'suspended');
+       my $volid = $drive->{file};
 
-       PVE::QemuConfig->check_lock($conf)
-           if !($skiplock || $is_suspended);
+       return if !$volid;
 
-       die "VM $vmid already running\n" if check_running($vmid, undef, $migratedfrom);
+       my ($storeid, $volname) = PVE::Storage::parse_volume_id($volid);
 
-       # clean up leftover reboot request files
-       eval { clear_reboot_request($vmid); };
-       warn $@ if $@;
+       my $scfg = PVE::Storage::storage_config($storecfg, $storeid);
+       return if $scfg->{shared};
+       $local_volumes->{$ds} = [$volid, $storeid, $volname];
+    });
+
+    my $format = undef;
 
-       if (!$statefile && scalar(keys %{$conf->{pending}})) {
-           vmconfig_apply_pending($vmid, $conf, $storecfg);
-           $conf = PVE::QemuConfig->load_config($vmid); # update/reload
+    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";
+           next;
        }
+       my $drive = parse_drive($opt, $conf->{$opt});
 
-       PVE::QemuServer::Cloudinit::generate_cloudinitconfig($conf, $vmid);
+       # 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;
+           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);
+           $format = (grep {$fileFormat eq $_} @{$validFormats}) ? $fileFormat : $defFormat;
+       } else {
+           my $scfg = PVE::Storage::storage_config($storecfg, $storeid);
+           $format = qemu_img_format($scfg, $volname);
+       }
 
-       my $defaults = load_defaults();
+       my $newvolid = PVE::Storage::vdisk_alloc($storecfg, $storeid, $vmid, $format, undef, ($drive->{size}/1024));
+       my $newdrive = $drive;
+       $newdrive->{format} = $format;
+       $newdrive->{file} = $newvolid;
+       my $drivestr = print_drive($newdrive);
+       $nbd->{$opt} = $drivestr;
+       #pass drive to conf for command line
+       $conf->{$opt} = $drivestr;
+    }
 
-       # set environment variable useful inside network script
-       $ENV{PVE_MIGRATED_FROM} = $migratedfrom if $migratedfrom;
+    return $nbd;
+}
 
-       my $local_volumes = {};
+# see vm_start_nolock for parameters, additionally:
+# migrate_opts:
+#   targetstorage = storageid/'1' - target storage for disks migrated over NBD
+sub vm_start {
+    my ($storecfg, $vmid, $params, $migrate_opts) = @_;
 
-       if ($targetstorage) {
-           foreach_drive($conf, sub {
-               my ($ds, $drive) = @_;
+    PVE::QemuConfig->lock_config($vmid, sub {
+       my $conf = PVE::QemuConfig->load_config($vmid, $migrate_opts->{migratedfrom});
 
-               return if drive_is_cdrom($drive);
+       die "you can't start a vm if it's a template\n" if PVE::QemuConfig->is_template($conf);
 
-               my $volid = $drive->{file};
+       $params->{resume} = PVE::QemuConfig->has_lock($conf, 'suspended');
 
-               return if !$volid;
+       PVE::QemuConfig->check_lock($conf)
+           if !($params->{skiplock} || $params->{resume});
 
-               my ($storeid, $volname) = PVE::Storage::parse_volume_id($volid);
+       die "VM $vmid already running\n" if check_running($vmid, undef, $migrate_opts->{migratedfrom});
 
-               my $scfg = PVE::Storage::storage_config($storecfg, $storeid);
-               return if $scfg->{shared};
-               $local_volumes->{$ds} = [$volid, $storeid, $volname];
-           });
+       $migrate_opts->{nbd} = vm_migrate_alloc_nbd_disks($storecfg, $vmid, $conf, $migrate_opts->{targetstorage}, $migrate_opts->{replicated_volumes})
+           if $migrate_opts->{targetstorage};
 
-           my $format = undef;
+       vm_start_nolock($storecfg, $vmid, $conf, $params, $migrate_opts);
+    });
+}
 
-           foreach my $opt (sort keys %$local_volumes) {
 
-               my ($volid, $storeid, $volname) = @{$local_volumes->{$opt}};
-               my $drive = parse_drive($opt, $conf->{$opt});
+# params:
+#   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)
+#   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)
+#   migratedfrom => source node
+#   spice_ticket => used for spice migration, passed via tunnel/stdin
+#   network => CIDR of migration network
+#   type => secure/insecure - tunnel over encrypted connection or plain-text
+#   nbd_proto_version => int, 0 for TCP, 1 for UNIX
+#   replicated_volumes = which volids should be re-used with bitmaps for nbd migration
+sub vm_start_nolock {
+    my ($storecfg, $vmid, $conf, $params, $migrate_opts) = @_;
+
+    my $statefile = $params->{statefile};
+    my $resume = $params->{resume};
+
+    my $migratedfrom = $migrate_opts->{migratedfrom};
+    my $migration_type = $migrate_opts->{type};
+
+    # clean up leftover reboot request files
+    eval { clear_reboot_request($vmid); };
+    warn $@ if $@;
 
-               # 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;
-                   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);
-                   $format = (grep {$fileFormat eq $_} @{$validFormats}) ? $fileFormat : $defFormat;
-               } else {
-                   my $scfg = PVE::Storage::storage_config($storecfg, $storeid);
-                   $format = qemu_img_format($scfg, $volname);
-               }
+    if (!$statefile && scalar(keys %{$conf->{pending}})) {
+       vmconfig_apply_pending($vmid, $conf, $storecfg);
+       $conf = PVE::QemuConfig->load_config($vmid); # update/reload
+    }
 
-               my $newvolid = PVE::Storage::vdisk_alloc($storecfg, $storeid, $vmid, $format, undef, ($drive->{size}/1024));
-               my $newdrive = $drive;
-               $newdrive->{format} = $format;
-               $newdrive->{file} = $newvolid;
-               my $drivestr = print_drive($newdrive);
-               $local_volumes->{$opt} = $drivestr;
-               #pass drive to conf for command line
-               $conf->{$opt} = $drivestr;
-           }
-       }
+    PVE::QemuServer::Cloudinit::generate_cloudinitconfig($conf, $vmid);
 
-       PVE::GuestHelpers::exec_hookscript($conf, $vmid, 'pre-start', 1);
+    my $defaults = load_defaults();
 
-       if ($is_suspended) {
-           # enforce machine type on suspended vm to ensure HW compatibility
-           $forcemachine = $conf->{runningmachine};
-           print "Resuming suspended VM\n";
-       }
+    # set environment variable useful inside network script
+    $ENV{PVE_MIGRATED_FROM} = $migratedfrom if $migratedfrom;
 
-       my ($cmd, $vollist, $spice_port) = config_to_command($storecfg, $vmid, $conf, $defaults, $forcemachine);
+    PVE::GuestHelpers::exec_hookscript($conf, $vmid, 'pre-start', 1);
 
-       my $migration_ip;
-       my $get_migration_ip = sub {
-           my ($cidr, $nodename) = @_;
+    my $forcemachine = $params->{forcemachine};
+    if ($resume) {
+       # enforce machine type on suspended vm to ensure HW compatibility
+       $forcemachine = $conf->{runningmachine};
+       print "Resuming suspended VM\n";
+    }
 
-           return $migration_ip if defined($migration_ip);
+    my ($cmd, $vollist, $spice_port) = config_to_command($storecfg, $vmid, $conf, $defaults, $forcemachine);
 
-           if (!defined($cidr)) {
-               my $dc_conf = PVE::Cluster::cfs_read_file('datacenter.cfg');
-               $cidr = $dc_conf->{migration}->{network};
-           }
+    my $migration_ip;
+    my $get_migration_ip = sub {
+       my ($nodename) = @_;
 
-           if (defined($cidr)) {
-               my $ips = PVE::Network::get_local_ip_from_cidr($cidr);
+       return $migration_ip if defined($migration_ip);
 
-               die "could not get IP: no address configured on local " .
-                   "node for network '$cidr'\n" if scalar(@$ips) == 0;
+       my $cidr = $migrate_opts->{network};
 
-               die "could not get IP: multiple addresses configured on local " .
-                   "node for network '$cidr'\n" if scalar(@$ips) > 1;
+       if (!defined($cidr)) {
+           my $dc_conf = PVE::Cluster::cfs_read_file('datacenter.cfg');
+           $cidr = $dc_conf->{migration}->{network};
+       }
 
-               $migration_ip = @$ips[0];
-           }
+       if (defined($cidr)) {
+           my $ips = PVE::Network::get_local_ip_from_cidr($cidr);
 
-           $migration_ip = PVE::Cluster::remote_node_ip($nodename, 1)
-               if !defined($migration_ip);
+           die "could not get IP: no address configured on local " .
+               "node for network '$cidr'\n" if scalar(@$ips) == 0;
 
-           return $migration_ip;
-       };
+           die "could not get IP: multiple addresses configured on local " .
+               "node for network '$cidr'\n" if scalar(@$ips) > 1;
 
-       my $migrate_uri;
-       if ($statefile) {
-           if ($statefile eq 'tcp') {
-               my $localip = "localhost";
-               my $datacenterconf = PVE::Cluster::cfs_read_file('datacenter.cfg');
-               my $nodename = nodename();
+           $migration_ip = @$ips[0];
+       }
 
-               if (!defined($migration_type)) {
-                   if (defined($datacenterconf->{migration}->{type})) {
-                       $migration_type = $datacenterconf->{migration}->{type};
-                   } else {
-                       $migration_type = 'secure';
-                   }
-               }
+       $migration_ip = PVE::Cluster::remote_node_ip($nodename, 1)
+           if !defined($migration_ip);
+
+       return $migration_ip;
+    };
+
+    my $migrate_uri;
+    if ($statefile) {
+       if ($statefile eq 'tcp') {
+           my $localip = "localhost";
+           my $datacenterconf = PVE::Cluster::cfs_read_file('datacenter.cfg');
+           my $nodename = nodename();
 
-               if ($migration_type eq 'insecure') {
-                   $localip = $get_migration_ip->($migration_network, $nodename);
-                   $localip = "[$localip]" if Net::IP::ip_is_ipv6($localip);
+           if (!defined($migration_type)) {
+               if (defined($datacenterconf->{migration}->{type})) {
+                   $migration_type = $datacenterconf->{migration}->{type};
+               } else {
+                   $migration_type = 'secure';
                }
+           }
 
-               my $pfamily = PVE::Tools::get_host_address_family($nodename);
-               my $migrate_port = PVE::Tools::next_migrate_port($pfamily);
-               $migrate_uri = "tcp:${localip}:${migrate_port}";
-               push @$cmd, '-incoming', $migrate_uri;
-               push @$cmd, '-S';
+           if ($migration_type eq 'insecure') {
+               $localip = $get_migration_ip->($nodename);
+               $localip = "[$localip]" if Net::IP::ip_is_ipv6($localip);
+           }
 
-           } elsif ($statefile eq 'unix') {
-               # should be default for secure migrations as a ssh TCP forward
-               # tunnel is not deterministic reliable ready and fails regurarly
-               # to set up in time, so use UNIX socket forwards
-               my $socket_addr = "/run/qemu-server/$vmid.migrate";
-               unlink $socket_addr;
+           my $pfamily = PVE::Tools::get_host_address_family($nodename);
+           my $migrate_port = PVE::Tools::next_migrate_port($pfamily);
+           $migrate_uri = "tcp:${localip}:${migrate_port}";
+           push @$cmd, '-incoming', $migrate_uri;
+           push @$cmd, '-S';
 
-               $migrate_uri = "unix:$socket_addr";
+       } elsif ($statefile eq 'unix') {
+           # should be default for secure migrations as a ssh TCP forward
+           # tunnel is not deterministic reliable ready and fails regurarly
+           # to set up in time, so use UNIX socket forwards
+           my $socket_addr = "/run/qemu-server/$vmid.migrate";
+           unlink $socket_addr;
 
-               push @$cmd, '-incoming', $migrate_uri;
-               push @$cmd, '-S';
+           $migrate_uri = "unix:$socket_addr";
 
-           } elsif (-e $statefile) {
-               push @$cmd, '-loadstate', $statefile;
-           } else {
-               my $statepath = PVE::Storage::path($storecfg, $statefile);
-               push @$vollist, $statefile;
-               push @$cmd, '-loadstate', $statepath;
-           }
-       } elsif ($paused) {
+           push @$cmd, '-incoming', $migrate_uri;
            push @$cmd, '-S';
+
+       } elsif (-e $statefile) {
+           push @$cmd, '-loadstate', $statefile;
+       } else {
+           my $statepath = PVE::Storage::path($storecfg, $statefile);
+           push @$vollist, $statefile;
+           push @$cmd, '-loadstate', $statepath;
        }
+    } elsif ($params->{paused}) {
+       push @$cmd, '-S';
+    }
 
-       # host pci devices
-        for (my $i = 0; $i < $MAX_HOSTPCI_DEVICES; $i++)  {
-          my $d = parse_hostpci($conf->{"hostpci$i"});
-          next if !$d;
-         my $pcidevices = $d->{pciid};
-         foreach my $pcidevice (@$pcidevices) {
-               my $pciid = $pcidevice->{id};
+    # host pci devices
+    for (my $i = 0; $i < $MAX_HOSTPCI_DEVICES; $i++)  {
+      my $d = parse_hostpci($conf->{"hostpci$i"});
+      next if !$d;
+      my $pcidevices = $d->{pciid};
+      foreach my $pcidevice (@$pcidevices) {
+           my $pciid = $pcidevice->{id};
+
+           my $info = PVE::SysFSTools::pci_device_info("$pciid");
+           die "IOMMU not present\n" if !PVE::SysFSTools::check_iommu_support();
+           die "no pci device info for device '$pciid'\n" if !$info;
+
+           if ($d->{mdev}) {
+               my $uuid = PVE::SysFSTools::generate_mdev_uuid($vmid, $i);
+               PVE::SysFSTools::pci_create_mdev_device($pciid, $uuid, $d->{mdev});
+           } else {
+               die "can't unbind/bind pci group to vfio '$pciid'\n"
+                   if !PVE::SysFSTools::pci_dev_group_bind_to_vfio($pciid);
+               die "can't reset pci device '$pciid'\n"
+                   if $info->{has_fl_reset} and !PVE::SysFSTools::pci_dev_reset($info);
+           }
+      }
+    }
 
-               my $info = PVE::SysFSTools::pci_device_info("$pciid");
-               die "IOMMU not present\n" if !PVE::SysFSTools::check_iommu_support();
-               die "no pci device info for device '$pciid'\n" if !$info;
+    PVE::Storage::activate_volumes($storecfg, $vollist);
 
-               if ($d->{mdev}) {
-                   my $uuid = PVE::SysFSTools::generate_mdev_uuid($vmid, $i);
-                   PVE::SysFSTools::pci_create_mdev_device($pciid, $uuid, $d->{mdev});
-               } else {
-                   die "can't unbind/bind pci group to vfio '$pciid'\n"
-                       if !PVE::SysFSTools::pci_dev_group_bind_to_vfio($pciid);
-                   die "can't reset pci device '$pciid'\n"
-                       if $info->{has_fl_reset} and !PVE::SysFSTools::pci_dev_reset($info);
-               }
-         }
-        }
+    eval {
+       run_command(['/bin/systemctl', 'stop', "$vmid.scope"],
+           outfunc => sub {}, errfunc => sub {});
+    };
+    # Issues with the above 'stop' not being fully completed are extremely rare, a very low
+    # timeout should be more than enough here...
+    PVE::Systemd::wait_for_unit_removed("$vmid.scope", 5);
+
+    my $cpuunits = defined($conf->{cpuunits}) ? $conf->{cpuunits}
+                                             : $defaults->{cpuunits};
+
+    my $start_timeout = $params->{timeout} // config_aware_timeout($conf, $resume);
+    my %run_params = (
+       timeout => $statefile ? undef : $start_timeout,
+       umask => 0077,
+       noerr => 1,
+    );
 
-       PVE::Storage::activate_volumes($storecfg, $vollist);
+    # when migrating, prefix QEMU output so other side can pick up any
+    # errors that might occur and show the user
+    if ($migratedfrom) {
+       $run_params{quiet} = 1;
+       $run_params{logfunc} = sub { print "QEMU: $_[0]\n" };
+    }
 
-       eval {
-           run_command(['/bin/systemctl', 'stop', "$vmid.scope"],
-               outfunc => sub {}, errfunc => sub {});
-       };
-       # Issues with the above 'stop' not being fully completed are extremely rare, a very low
-       # timeout should be more than enough here...
-       PVE::Systemd::wait_for_unit_removed("$vmid.scope", 5);
+    my %properties = (
+       Slice => 'qemu.slice',
+       KillMode => 'none',
+       CPUShares => $cpuunits
+    );
 
-       my $cpuunits = defined($conf->{cpuunits}) ? $conf->{cpuunits}
-                                                 : $defaults->{cpuunits};
+    if (my $cpulimit = $conf->{cpulimit}) {
+       $properties{CPUQuota} = int($cpulimit * 100);
+    }
+    $properties{timeout} = 10 if $statefile; # setting up the scope shoul be quick
 
-       my $start_timeout = $timeout // config_aware_timeout($conf, $is_suspended);
-       my %run_params = (
-           timeout => $statefile ? undef : $start_timeout,
-           umask => 0077,
-           noerr => 1,
-       );
+    my $run_qemu = sub {
+       PVE::Tools::run_fork sub {
+           PVE::Systemd::enter_systemd_scope($vmid, "Proxmox VE VM $vmid", %properties);
 
-       # when migrating, prefix QEMU output so other side can pick up any
-       # errors that might occur and show the user
-       if ($migratedfrom) {
-           $run_params{quiet} = 1;
-           $run_params{logfunc} = sub { print "QEMU: $_[0]\n" };
-       }
+           my $exitcode = run_command($cmd, %run_params);
+           die "QEMU exited with code $exitcode\n" if $exitcode;
+       };
+    };
 
-       my %properties = (
-           Slice => 'qemu.slice',
-           KillMode => 'none',
-           CPUShares => $cpuunits
-       );
+    if ($conf->{hugepages}) {
 
-       if (my $cpulimit = $conf->{cpulimit}) {
-           $properties{CPUQuota} = int($cpulimit * 100);
-       }
-       $properties{timeout} = 10 if $statefile; # setting up the scope shoul be quick
+       my $code = sub {
+           my $hugepages_topology = PVE::QemuServer::Memory::hugepages_topology($conf);
+           my $hugepages_host_topology = PVE::QemuServer::Memory::hugepages_host_topology();
 
-       my $run_qemu = sub {
-           PVE::Tools::run_fork sub {
-               PVE::Systemd::enter_systemd_scope($vmid, "Proxmox VE VM $vmid", %properties);
+           PVE::QemuServer::Memory::hugepages_mount();
+           PVE::QemuServer::Memory::hugepages_allocate($hugepages_topology, $hugepages_host_topology);
 
-               my $exitcode = run_command($cmd, %run_params);
-               die "QEMU exited with code $exitcode\n" if $exitcode;
-           };
+           eval { $run_qemu->() };
+           if (my $err = $@) {
+               PVE::QemuServer::Memory::hugepages_reset($hugepages_host_topology);
+               die $err;
+           }
+
+           PVE::QemuServer::Memory::hugepages_pre_deallocate($hugepages_topology);
        };
+       eval { PVE::QemuServer::Memory::hugepages_update_locked($code); };
 
-       if ($conf->{hugepages}) {
+    } else {
+       eval { $run_qemu->() };
+    }
 
-           my $code = sub {
-               my $hugepages_topology = PVE::QemuServer::Memory::hugepages_topology($conf);
-               my $hugepages_host_topology = PVE::QemuServer::Memory::hugepages_host_topology();
+    if (my $err = $@) {
+       # deactivate volumes if start fails
+       eval { PVE::Storage::deactivate_volumes($storecfg, $vollist); };
+       die "start failed: $err";
+    }
 
-               PVE::QemuServer::Memory::hugepages_mount();
-               PVE::QemuServer::Memory::hugepages_allocate($hugepages_topology, $hugepages_host_topology);
+    print "migration listens on $migrate_uri\n" if $migrate_uri;
 
-               eval { $run_qemu->() };
-               if (my $err = $@) {
-                   PVE::QemuServer::Memory::hugepages_reset($hugepages_host_topology);
-                   die $err;
-               }
+    if ($statefile && $statefile ne 'tcp' && $statefile ne 'unix')  {
+       eval { mon_cmd($vmid, "cont"); };
+       warn $@ if $@;
+    }
 
-               PVE::QemuServer::Memory::hugepages_pre_deallocate($hugepages_topology);
-           };
-           eval { PVE::QemuServer::Memory::hugepages_update_locked($code); };
+    #start nbd server for storage migration
+    if (my $nbd = $migrate_opts->{nbd}) {
+       my $nbd_protocol_version = $migrate_opts->{nbd_proto_version} // 0;
 
+       my $migrate_storage_uri;
+       # nbd_protocol_version > 0 for unix socket support
+       if ($nbd_protocol_version > 0 && $migration_type eq 'secure') {
+           my $socket_path = "/run/qemu-server/$vmid\_nbd.migrate";
+           mon_cmd($vmid, "nbd-server-start", addr => { type => 'unix', data => { path => $socket_path } } );
+           $migrate_storage_uri = "nbd:unix:$socket_path";
        } else {
-           eval { $run_qemu->() };
-       }
+           my $nodename = nodename();
+           my $localip = $get_migration_ip->($nodename);
+           my $pfamily = PVE::Tools::get_host_address_family($nodename);
+           my $storage_migrate_port = PVE::Tools::next_migrate_port($pfamily);
 
-       if (my $err = $@) {
-           # deactivate volumes if start fails
-           eval { PVE::Storage::deactivate_volumes($storecfg, $vollist); };
-           die "start failed: $err";
+           mon_cmd($vmid, "nbd-server-start", addr => { type => 'inet', data => { host => "${localip}", port => "${storage_migrate_port}" } } );
+           $localip = "[$localip]" if Net::IP::ip_is_ipv6($localip);
+           $migrate_storage_uri = "nbd:${localip}:${storage_migrate_port}";
        }
 
-       print "migration listens on $migrate_uri\n" if $migrate_uri;
-
-       if ($statefile && $statefile ne 'tcp' && $statefile ne 'unix')  {
-           eval { mon_cmd($vmid, "cont"); };
-           warn $@ if $@;
+       foreach my $opt (sort keys %$nbd) {
+           my $drivestr = $nbd->{$opt};
+           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";
        }
+    }
 
-       #start nbd server for storage migration
-       if ($targetstorage) {
-           $nbd_protocol_version //= 0;
-
-           my $migrate_storage_uri;
-           # nbd_protocol_version > 0 for unix socket support
-           if ($nbd_protocol_version > 0 && $migration_type eq 'secure') {
-               my $socket_path = "/run/qemu-server/$vmid\_nbd.migrate";
-               mon_cmd($vmid, "nbd-server-start", addr => { type => 'unix', data => { path => $socket_path } } );
-               $migrate_storage_uri = "nbd:unix:$socket_path";
-           } else {
-               my $nodename = nodename();
-               my $localip = $get_migration_ip->($migration_network, $nodename);
-               my $pfamily = PVE::Tools::get_host_address_family($nodename);
-               my $storage_migrate_port = PVE::Tools::next_migrate_port($pfamily);
-
-               mon_cmd($vmid, "nbd-server-start", addr => { type => 'inet', data => { host => "${localip}", port => "${storage_migrate_port}" } } );
-               $localip = "[$localip]" if Net::IP::ip_is_ipv6($localip);
-               $migrate_storage_uri = "nbd:${localip}:${storage_migrate_port}";
-           }
+    if ($migratedfrom) {
+       eval {
+           set_migration_caps($vmid);
+       };
+       warn $@ if $@;
 
-           foreach my $opt (sort keys %$local_volumes) {
-               my $drivestr = $local_volumes->{$opt};
-               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";
+       if ($spice_port) {
+           print "spice listens on port $spice_port\n";
+           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");
            }
        }
 
-       if ($migratedfrom) {
-           eval {
-               set_migration_caps($vmid);
-           };
-           warn $@ if $@;
-
-           if ($spice_port) {
-               print "spice listens on port $spice_port\n";
-               if ($spice_ticket) {
-                   mon_cmd($vmid, "set_password", protocol => 'spice', password => $spice_ticket);
-                   mon_cmd($vmid, "expire_password", protocol => 'spice', time => "+30");
-               }
-           }
-
-       } else {
-           mon_cmd($vmid, "balloon", value => $conf->{balloon}*1024*1024)
-               if !$statefile && $conf->{balloon};
+    } else {
+       mon_cmd($vmid, "balloon", value => $conf->{balloon}*1024*1024)
+           if !$statefile && $conf->{balloon};
 
-           foreach my $opt (keys %$conf) {
-               next if $opt !~  m/^net\d+$/;
-               my $nicconf = parse_net($conf->{$opt});
-               qemu_set_link_status($vmid, $opt, 0) if $nicconf->{link_down};
-           }
+       foreach my $opt (keys %$conf) {
+           next if $opt !~  m/^net\d+$/;
+           my $nicconf = parse_net($conf->{$opt});
+           qemu_set_link_status($vmid, $opt, 0) if $nicconf->{link_down};
        }
+    }
 
-       mon_cmd($vmid, 'qom-set',
-                   path => "machine/peripheral/balloon0",
-                   property => "guest-stats-polling-interval",
-                   value => 2) if (!defined($conf->{balloon}) || $conf->{balloon});
+    mon_cmd($vmid, 'qom-set',
+               path => "machine/peripheral/balloon0",
+               property => "guest-stats-polling-interval",
+               value => 2) if (!defined($conf->{balloon}) || $conf->{balloon});
 
-       if ($is_suspended) {
-           print "Resumed VM, removing state\n";
-           if (my $vmstate = $conf->{vmstate}) {
-               PVE::Storage::deactivate_volumes($storecfg, [$vmstate]);
-               PVE::Storage::vdisk_free($storecfg, $vmstate);
-           }
-           delete $conf->@{qw(lock vmstate runningmachine)};
-           PVE::QemuConfig->write_config($vmid, $conf);
+    if ($resume) {
+       print "Resumed VM, removing state\n";
+       if (my $vmstate = $conf->{vmstate}) {
+           PVE::Storage::deactivate_volumes($storecfg, [$vmstate]);
+           PVE::Storage::vdisk_free($storecfg, $vmstate);
        }
+       delete $conf->@{qw(lock vmstate runningmachine)};
+       PVE::QemuConfig->write_config($vmid, $conf);
+    }
 
-       PVE::GuestHelpers::exec_hookscript($conf, $vmid, 'post-start');
-    });
+    PVE::GuestHelpers::exec_hookscript($conf, $vmid, 'post-start');
 }
 
 sub vm_commandline {
@@ -5750,35 +5797,37 @@ sub update_disk_config {
     my $referencedpath = {};
 
     # update size info
-    foreach my $opt (keys %$conf) {
-       if (is_valid_drivename($opt)) {
-           my $drive = parse_drive($opt, $conf->{$opt});
-           my $volid = $drive->{file};
-           next if !$volid;
-
-           # mark volid as "in-use" for next step
-           $referenced->{$volid} = 1;
-           if ($volid_hash->{$volid} &&
-               (my $path = $volid_hash->{$volid}->{path})) {
-               $referencedpath->{$path} = 1;
-           }
+    PVE::QemuConfig->foreach_volume($conf, sub {
+       my ($opt, $drive) = @_;
 
-           next if drive_is_cdrom($drive);
-           next if !$volid_hash->{$volid};
+       my $volid = $drive->{file};
+       return if !$volid;
 
-           my ($updated, $old_size, $new_size) = PVE::QemuServer::Drive::update_disksize($drive, $volid_hash);
-           if (defined($updated)) {
-               $changes = 1;
-               $conf->{$opt} = print_drive($updated);
-               print "$prefix size of disk '$volid' ($opt) updated from $old_size to $new_size\n";
-           }
+       # mark volid as "in-use" for next step
+       $referenced->{$volid} = 1;
+       if ($volid_hash->{$volid} &&
+           (my $path = $volid_hash->{$volid}->{path})) {
+           $referencedpath->{$path} = 1;
        }
-    }
+
+       return if drive_is_cdrom($drive);
+       return if !$volid_hash->{$volid};
+
+       my ($updated, $old_size, $new_size) = PVE::QemuServer::Drive::update_disksize($drive, $volid_hash);
+       if (defined($updated)) {
+           $changes = 1;
+           $conf->{$opt} = print_drive($updated);
+           print "$prefix size of disk '$volid' ($opt) updated from $old_size to $new_size\n";
+       }
+    });
 
     # remove 'unusedX' entry if volume is used
-    foreach my $opt (keys %$conf) {
-       next if $opt !~ m/^unused\d+$/;
-       my $volid = $conf->{$opt};
+    PVE::QemuConfig->foreach_unused_volume($conf, sub {
+       my ($opt, $drive) = @_;
+
+       my $volid = $drive->{file};
+       return if !$volid;
+
        my $path = $volid_hash->{$volid}->{path} if $volid_hash->{$volid};
        if ($referenced->{$volid} || ($path && $referencedpath->{$path})) {
            print "$prefix remove entry '$opt', its volume '$volid' is in use\n";
@@ -5788,7 +5837,7 @@ sub update_disk_config {
 
        $referenced->{$volid} = 1;
        $referencedpath->{$path} = 1 if $path;
-    }
+    });
 
     foreach my $volid (sort keys %$volid_hash) {
        next if $volid =~ m/vm-$vmid-state-/;
@@ -6533,7 +6582,7 @@ sub qemu_img_format {
 }
 
 sub qemu_drive_mirror {
-    my ($vmid, $drive, $dst_volid, $vmiddst, $is_zero_initialized, $jobs, $completion, $qga, $bwlimit) = @_;
+    my ($vmid, $drive, $dst_volid, $vmiddst, $is_zero_initialized, $jobs, $completion, $qga, $bwlimit, $src_bitmap) = @_;
 
     $jobs = {} if !$jobs;
 
@@ -6560,6 +6609,12 @@ sub qemu_drive_mirror {
     my $opts = { timeout => 10, device => "drive-$drive", mode => "existing", sync => "full", target => $qemu_target };
     $opts->{format} = $format if $format;
 
+    if (defined($src_bitmap)) {
+       $opts->{sync} = 'incremental';
+       $opts->{bitmap} = $src_bitmap;
+       print "drive mirror re-using dirty bitmap '$src_bitmap'\n";
+    }
+
     if (defined($bwlimit)) {
        $opts->{speed} = $bwlimit * 1024;
        print "drive mirror is starting for drive-$drive with bandwidth limit: ${bwlimit} KB/s\n";
@@ -6724,7 +6779,7 @@ sub qemu_blockjobs_cancel {
 
 sub clone_disk {
     my ($storecfg, $vmid, $running, $drivename, $drive, $snapname,
-       $newvmid, $storage, $format, $full, $newvollist, $jobs, $completion, $qga, $bwlimit) = @_;
+       $newvmid, $storage, $format, $full, $newvollist, $jobs, $completion, $qga, $bwlimit, $conf) = @_;
 
     my $newvolid;
 
@@ -6747,6 +6802,8 @@ sub clone_disk {
            $name .= ".$dst_format" if $dst_format ne 'raw';
            $snapname = undef;
            $size = PVE::QemuServer::Cloudinit::CLOUDINIT_DISK_SIZE;
+       } elsif ($drivename eq 'efidisk0') {
+           $size = get_efivars_size($conf);
        }
        $newvolid = PVE::Storage::vdisk_alloc($storecfg, $storeid, $newvmid, $dst_format, $name, ($size/1024));
        push @$newvollist, $newvolid;
@@ -6760,7 +6817,16 @@ sub clone_disk {
        my $sparseinit = PVE::Storage::volume_has_feature($storecfg, 'sparseinit', $newvolid);
        if (!$running || $snapname) {
            # TODO: handle bwlimits
-           qemu_img_convert($drive->{file}, $newvolid, $size, $snapname, $sparseinit);
+           if ($drivename eq 'efidisk0') {
+               # the relevant data on the efidisk may be smaller than the source
+               # e.g. on RBD/ZFS, so we use dd to copy only the amount
+               # that is given by the OVMF_VARS.fd
+               my $src_path = PVE::Storage::path($storecfg, $drive->{file});
+               my $dst_path = PVE::Storage::path($storecfg, $newvolid);
+               run_command(['qemu-img', 'dd', '-n', '-O', $dst_format, "bs=1", "count=$size", "if=$src_path", "of=$dst_path"]);
+           } else {
+               qemu_img_convert($drive->{file}, $newvolid, $size, $snapname, $sparseinit);
+           }
        } else {
 
            my $kvmver = get_running_qemu_version ($vmid);
@@ -6812,6 +6878,26 @@ sub qemu_use_old_bios_files {
     return ($use_old_bios_files, $machine_type);
 }
 
+sub get_efivars_size {
+    my ($conf) = @_;
+    my $arch = get_vm_arch($conf);
+    my (undef, $ovmf_vars) = get_ovmf_files($arch);
+    die "uefi vars image '$ovmf_vars' not found\n" if ! -f $ovmf_vars;
+    return -s $ovmf_vars;
+}
+
+sub update_efidisk_size {
+    my ($conf) = @_;
+
+    return if !defined($conf->{efidisk0});
+
+    my $disk = PVE::QemuServer::parse_drive('efidisk0', $conf->{efidisk0});
+    $disk->{size} = get_efivars_size($conf);
+    $conf->{efidisk0} = print_drive($disk);
+
+    return;
+}
+
 sub create_efidisk($$$$$) {
     my ($storecfg, $storeid, $vmid, $fmt, $arch) = @_;