]> git.proxmox.com Git - qemu-server.git/blobdiff - PVE/QemuServer.pm
fix efidisks on storages with minimum sizes bigger than OVMF_VARS.fd
[qemu-server.git] / PVE / QemuServer.pm
index 27b9866551875f36cd278f7bbe55408dd96bf43c..611fb4509e00d207931ccf153458b333dcd0ff30 100644 (file)
@@ -29,7 +29,7 @@ use UUID;
 use PVE::Cluster qw(cfs_register_file cfs_read_file cfs_write_file cfs_lock_file);
 use PVE::DataCenterConfig;
 use PVE::Exception qw(raise raise_param_exc);
-use PVE::GuestHelpers;
+use PVE::GuestHelpers qw(safe_string_ne safe_num_ne safe_boolean_ne);
 use PVE::INotify;
 use PVE::JSONSchema qw(get_standard_option);
 use PVE::ProcFSTools;
@@ -44,12 +44,19 @@ 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::Machine;
 use PVE::QemuServer::Memory;
 use PVE::QemuServer::Monitor qw(mon_cmd);
 use PVE::QemuServer::PCI qw(print_pci_addr print_pcie_addr print_pcie_root_port);
 use PVE::QemuServer::USB qw(parse_usb_device);
 
+my $have_sdn;
+eval {
+    require PVE::Network::SDN::Zones;
+    $have_sdn = 1;
+};
+
 my $EDK2_FW_BASE = '/usr/share/pve-edk2-firmware/';
 my $OVMF = {
     x86_64 => [
@@ -64,8 +71,6 @@ my $OVMF = {
 
 my $cpuinfo = PVE::ProcFSTools::read_cpuinfo();
 
-my $QEMU_FORMAT_RE = qr/raw|cow|qcow|qcow2|qed|vmdk|cloop/;
-
 # Note about locking: we use flock on the config file protect
 # against concurent actions.
 # Aditionaly, we have a 'lock' setting in the config file. This
@@ -84,13 +89,6 @@ PVE::JSONSchema::register_standard_option('pve-qm-stateuri', {
     optional => 1,
 });
 
-PVE::JSONSchema::register_standard_option('pve-qm-image-format', {
-    type => 'string',
-    enum => [qw(raw cow qcow qed qcow2 vmdk cloop)],
-    description => "The drive's backing file's data format.",
-    optional => 1,
-});
-
 PVE::JSONSchema::register_standard_option('pve-qemu-machine', {
        description => "Specifies the Qemu machine type.",
        type => 'string',
@@ -219,6 +217,43 @@ my $spice_enhancements_fmt = {
     },
 };
 
+my $rng_fmt = {
+    source => {
+       type => 'string',
+       enum => ['/dev/urandom', '/dev/random', '/dev/hwrng'],
+       default_key => 1,
+       description => "The file on the host to gather entropy from. In most"
+                    . " cases /dev/urandom should be preferred over /dev/random"
+                    . " to avoid entropy-starvation issues on the host. Using"
+                    . " urandom does *not* decrease security in any meaningful"
+                    . " way, as it's still seeded from real entropy, and the"
+                    . " bytes provided will most likely be mixed with real"
+                    . " entropy on the guest as well. /dev/hwrng can be used"
+                    . " to pass through a hardware RNG from the host.",
+    },
+    max_bytes => {
+       type => 'integer',
+       description => "Maximum bytes of entropy injected into the guest every"
+                    . " 'period' milliseconds. Prefer a lower value when using"
+                    . " /dev/random as source. Use 0 to disable limiting"
+                    . " (potentially dangerous!).",
+       optional => 1,
+
+       # default is 1 KiB/s, provides enough entropy to the guest to avoid
+       # boot-starvation issues (e.g. systemd etc...) while allowing no chance
+       # of overwhelming the host, provided we're reading from /dev/urandom
+       default => 1024,
+    },
+    period => {
+       type => 'integer',
+       description => "Every 'period' milliseconds the entropy-injection quota"
+                    . " is reset, allowing the guest to retrieve another"
+                    . " 'max_bytes' of entropy.",
+       optional => 1,
+       default => 1000,
+    },
+};
+
 my $confdesc = {
     onboot => {
        optional => 1,
@@ -510,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,
@@ -607,6 +642,12 @@ EODESCR
        description => 'Tags of the VM. This is only meta information.',
        optional => 1,
     },
+    rng0 => {
+       type => 'string',
+       format => $rng_fmt,
+       description => "Configure a VirtIO-based Random Number Generator.",
+       optional => 1,
+    },
 };
 
 my $cicustom_fmt = {
@@ -696,13 +737,8 @@ while (my ($k, $v) = each %$confdesc) {
     PVE::JSONSchema::register_standard_option("pve-qm-$k", $v);
 }
 
-my $MAX_IDE_DISKS = 4;
-my $MAX_SCSI_DISKS = 31;
-my $MAX_VIRTIO_DISKS = 16;
-my $MAX_SATA_DISKS = 6;
 my $MAX_USB_DEVICES = 5;
 my $MAX_NETS = 32;
-my $MAX_UNUSED_DISKS = 256;
 my $MAX_HOSTPCI_DEVICES = 16;
 my $MAX_SERIAL_PORTS = 4;
 my $MAX_PARALLEL_PORTS = 3;
@@ -905,310 +941,6 @@ sub verify_volume_id_or_qm_path {
     return $volid;
 }
 
-my $drivename_hash;
-
-my %drivedesc_base = (
-    volume => { alias => 'file' },
-    file => {
-       type => 'string',
-       format => 'pve-volume-id-or-qm-path',
-       default_key => 1,
-       format_description => 'volume',
-       description => "The drive's backing volume.",
-    },
-    media => {
-       type => 'string',
-       enum => [qw(cdrom disk)],
-       description => "The drive's media type.",
-       default => 'disk',
-       optional => 1
-    },
-    cyls => {
-       type => 'integer',
-       description => "Force the drive's physical geometry to have a specific cylinder count.",
-       optional => 1
-    },
-    heads => {
-       type => 'integer',
-       description => "Force the drive's physical geometry to have a specific head count.",
-       optional => 1
-    },
-    secs => {
-       type => 'integer',
-       description => "Force the drive's physical geometry to have a specific sector count.",
-       optional => 1
-    },
-    trans => {
-       type => 'string',
-       enum => [qw(none lba auto)],
-       description => "Force disk geometry bios translation mode.",
-       optional => 1,
-    },
-    snapshot => {
-       type => 'boolean',
-       description => "Controls qemu's snapshot mode feature."
-           . " If activated, changes made to the disk are temporary and will"
-           . " be discarded when the VM is shutdown.",
-       optional => 1,
-    },
-    cache => {
-       type => 'string',
-       enum => [qw(none writethrough writeback unsafe directsync)],
-       description => "The drive's cache mode",
-       optional => 1,
-    },
-    format => get_standard_option('pve-qm-image-format'),
-    size => {
-       type => 'string',
-       format => 'disk-size',
-       format_description => 'DiskSize',
-       description => "Disk size. This is purely informational and has no effect.",
-       optional => 1,
-    },
-    backup => {
-       type => 'boolean',
-       description => "Whether the drive should be included when making backups.",
-       optional => 1,
-    },
-    replicate => {
-       type => 'boolean',
-       description => 'Whether the drive should considered for replication jobs.',
-       optional => 1,
-       default => 1,
-    },
-    rerror => {
-       type => 'string',
-       enum => [qw(ignore report stop)],
-       description => 'Read error action.',
-       optional => 1,
-    },
-    werror => {
-       type => 'string',
-       enum => [qw(enospc ignore report stop)],
-       description => 'Write error action.',
-       optional => 1,
-    },
-    aio => {
-       type => 'string',
-       enum => [qw(native threads)],
-       description => 'AIO type to use.',
-       optional => 1,
-    },
-    discard => {
-       type => 'string',
-       enum => [qw(ignore on)],
-       description => 'Controls whether to pass discard/trim requests to the underlying storage.',
-       optional => 1,
-    },
-    detect_zeroes => {
-       type => 'boolean',
-       description => 'Controls whether to detect and try to optimize writes of zeroes.',
-       optional => 1,
-    },
-    serial => {
-       type => 'string',
-       format => 'urlencoded',
-       format_description => 'serial',
-       maxLength => 20*3, # *3 since it's %xx url enoded
-       description => "The drive's reported serial number, url-encoded, up to 20 bytes long.",
-       optional => 1,
-    },
-    shared => {
-       type => 'boolean',
-       description => 'Mark this locally-managed volume as available on all nodes',
-       verbose_description => "Mark this locally-managed volume as available on all nodes.\n\nWARNING: This option does not share the volume automatically, it assumes it is shared already!",
-       optional => 1,
-       default => 0,
-    }
-);
-
-my %iothread_fmt = ( iothread => {
-       type => 'boolean',
-       description => "Whether to use iothreads for this drive",
-       optional => 1,
-});
-
-my %model_fmt = (
-    model => {
-       type => 'string',
-       format => 'urlencoded',
-       format_description => 'model',
-       maxLength => 40*3, # *3 since it's %xx url enoded
-       description => "The drive's reported model name, url-encoded, up to 40 bytes long.",
-       optional => 1,
-    },
-);
-
-my %queues_fmt = (
-    queues => {
-       type => 'integer',
-       description => "Number of queues.",
-       minimum => 2,
-       optional => 1
-    }
-);
-
-my %scsiblock_fmt = (
-    scsiblock => {
-       type => 'boolean',
-       description => "whether to use scsi-block for full passthrough of host block device\n\nWARNING: can lead to I/O errors in combination with low memory or high memory fragmentation on host",
-       optional => 1,
-       default => 0,
-    },
-);
-
-my %ssd_fmt = (
-    ssd => {
-       type => 'boolean',
-       description => "Whether to expose this drive as an SSD, rather than a rotational hard disk.",
-       optional => 1,
-    },
-);
-
-my %wwn_fmt = (
-    wwn => {
-       type => 'string',
-       pattern => qr/^(0x)[0-9a-fA-F]{16}/,
-       format_description => 'wwn',
-       description => "The drive's worldwide name, encoded as 16 bytes hex string, prefixed by '0x'.",
-       optional => 1,
-    },
-);
-
-my $add_throttle_desc = sub {
-    my ($key, $type, $what, $unit, $longunit, $minimum) = @_;
-    my $d = {
-       type => $type,
-       format_description => $unit,
-       description => "Maximum $what in $longunit.",
-       optional => 1,
-    };
-    $d->{minimum} = $minimum if defined($minimum);
-    $drivedesc_base{$key} = $d;
-};
-# throughput: (leaky bucket)
-$add_throttle_desc->('bps',     'integer', 'r/w speed',   'bps',  'bytes per second');
-$add_throttle_desc->('bps_rd',  'integer', 'read speed',  'bps',  'bytes per second');
-$add_throttle_desc->('bps_wr',  'integer', 'write speed', 'bps',  'bytes per second');
-$add_throttle_desc->('mbps',    'number',  'r/w speed',   'mbps', 'megabytes per second');
-$add_throttle_desc->('mbps_rd', 'number',  'read speed',  'mbps', 'megabytes per second');
-$add_throttle_desc->('mbps_wr', 'number',  'write speed', 'mbps', 'megabytes per second');
-$add_throttle_desc->('iops',    'integer', 'r/w I/O',     'iops', 'operations per second');
-$add_throttle_desc->('iops_rd', 'integer', 'read I/O',    'iops', 'operations per second');
-$add_throttle_desc->('iops_wr', 'integer', 'write I/O',   'iops', 'operations per second');
-
-# pools: (pool of IO before throttling starts taking effect)
-$add_throttle_desc->('mbps_max',    'number',  'unthrottled r/w pool',       'mbps', 'megabytes per second');
-$add_throttle_desc->('mbps_rd_max', 'number',  'unthrottled read pool',      'mbps', 'megabytes per second');
-$add_throttle_desc->('mbps_wr_max', 'number',  'unthrottled write pool',     'mbps', 'megabytes per second');
-$add_throttle_desc->('iops_max',    'integer', 'unthrottled r/w I/O pool',   'iops', 'operations per second');
-$add_throttle_desc->('iops_rd_max', 'integer', 'unthrottled read I/O pool',  'iops', 'operations per second');
-$add_throttle_desc->('iops_wr_max', 'integer', 'unthrottled write I/O pool', 'iops', 'operations per second');
-
-# burst lengths
-$add_throttle_desc->('bps_max_length',     'integer', 'length of I/O bursts',       'seconds', 'seconds', 1);
-$add_throttle_desc->('bps_rd_max_length',  'integer', 'length of read I/O bursts',  'seconds', 'seconds', 1);
-$add_throttle_desc->('bps_wr_max_length',  'integer', 'length of write I/O bursts', 'seconds', 'seconds', 1);
-$add_throttle_desc->('iops_max_length',    'integer', 'length of I/O bursts',       'seconds', 'seconds', 1);
-$add_throttle_desc->('iops_rd_max_length', 'integer', 'length of read I/O bursts',  'seconds', 'seconds', 1);
-$add_throttle_desc->('iops_wr_max_length', 'integer', 'length of write I/O bursts', 'seconds', 'seconds', 1);
-
-# legacy support
-$drivedesc_base{'bps_rd_length'} = { alias => 'bps_rd_max_length' };
-$drivedesc_base{'bps_wr_length'} = { alias => 'bps_wr_max_length' };
-$drivedesc_base{'iops_rd_length'} = { alias => 'iops_rd_max_length' };
-$drivedesc_base{'iops_wr_length'} = { alias => 'iops_wr_max_length' };
-
-my $ide_fmt = {
-    %drivedesc_base,
-    %model_fmt,
-    %ssd_fmt,
-    %wwn_fmt,
-};
-PVE::JSONSchema::register_format("pve-qm-ide", $ide_fmt);
-
-my $idedesc = {
-    optional => 1,
-    type => 'string', format => $ide_fmt,
-    description => "Use volume as IDE hard disk or CD-ROM (n is 0 to " .($MAX_IDE_DISKS -1) . ").",
-};
-PVE::JSONSchema::register_standard_option("pve-qm-ide", $idedesc);
-
-my $scsi_fmt = {
-    %drivedesc_base,
-    %iothread_fmt,
-    %queues_fmt,
-    %scsiblock_fmt,
-    %ssd_fmt,
-    %wwn_fmt,
-};
-my $scsidesc = {
-    optional => 1,
-    type => 'string', format => $scsi_fmt,
-    description => "Use volume as SCSI hard disk or CD-ROM (n is 0 to " . ($MAX_SCSI_DISKS - 1) . ").",
-};
-PVE::JSONSchema::register_standard_option("pve-qm-scsi", $scsidesc);
-
-my $sata_fmt = {
-    %drivedesc_base,
-    %ssd_fmt,
-    %wwn_fmt,
-};
-my $satadesc = {
-    optional => 1,
-    type => 'string', format => $sata_fmt,
-    description => "Use volume as SATA hard disk or CD-ROM (n is 0 to " . ($MAX_SATA_DISKS - 1). ").",
-};
-PVE::JSONSchema::register_standard_option("pve-qm-sata", $satadesc);
-
-my $virtio_fmt = {
-    %drivedesc_base,
-    %iothread_fmt,
-};
-my $virtiodesc = {
-    optional => 1,
-    type => 'string', format => $virtio_fmt,
-    description => "Use volume as VIRTIO hard disk (n is 0 to " . ($MAX_VIRTIO_DISKS - 1) . ").",
-};
-PVE::JSONSchema::register_standard_option("pve-qm-virtio", $virtiodesc);
-
-my $alldrive_fmt = {
-    %drivedesc_base,
-    %iothread_fmt,
-    %model_fmt,
-    %queues_fmt,
-    %scsiblock_fmt,
-    %ssd_fmt,
-    %wwn_fmt,
-};
-
-my $efidisk_fmt = {
-    volume => { alias => 'file' },
-    file => {
-       type => 'string',
-       format => 'pve-volume-id-or-qm-path',
-       default_key => 1,
-       format_description => 'volume',
-       description => "The drive's backing volume.",
-    },
-    format => get_standard_option('pve-qm-image-format'),
-    size => {
-       type => 'string',
-       format => 'disk-size',
-       format_description => 'DiskSize',
-       description => "Disk size. This is purely informational and has no effect.",
-       optional => 1,
-    },
-};
-
-my $efidisk_desc = {
-    optional => 1,
-    type => 'string', format => $efidisk_fmt,
-    description => "Configure a Disk for storing EFI vars",
-};
-
-PVE::JSONSchema::register_standard_option("pve-qm-efidisk", $efidisk_desc);
-
 my $usb_fmt = {
     host => {
        default_key => 1,
@@ -1355,43 +1087,14 @@ for (my $i = 0; $i < $MAX_HOSTPCI_DEVICES; $i++)  {
     $confdesc->{"hostpci$i"} = $hostpcidesc;
 }
 
-for (my $i = 0; $i < $MAX_IDE_DISKS; $i++)  {
-    $drivename_hash->{"ide$i"} = 1;
-    $confdesc->{"ide$i"} = $idedesc;
-}
-
-for (my $i = 0; $i < $MAX_SATA_DISKS; $i++)  {
-    $drivename_hash->{"sata$i"} = 1;
-    $confdesc->{"sata$i"} = $satadesc;
-}
-
-for (my $i = 0; $i < $MAX_SCSI_DISKS; $i++)  {
-    $drivename_hash->{"scsi$i"} = 1;
-    $confdesc->{"scsi$i"} = $scsidesc ;
-}
-
-for (my $i = 0; $i < $MAX_VIRTIO_DISKS; $i++)  {
-    $drivename_hash->{"virtio$i"} = 1;
-    $confdesc->{"virtio$i"} = $virtiodesc;
+for my $key (keys %{$PVE::QemuServer::Drive::drivedesc_hash}) {
+    $confdesc->{$key} = $PVE::QemuServer::Drive::drivedesc_hash->{$key};
 }
 
-$drivename_hash->{efidisk0} = 1;
-$confdesc->{efidisk0} = $efidisk_desc;
-
 for (my $i = 0; $i < $MAX_USB_DEVICES; $i++)  {
     $confdesc->{"usb$i"} = $usbdesc;
 }
 
-my $unuseddesc = {
-    optional => 1,
-    type => 'string', format => 'pve-volume-id',
-    description => "Reference to unused volumes. This is used internally, and should not be modified manually.",
-};
-
-for (my $i = 0; $i < $MAX_UNUSED_DISKS; $i++)  {
-    $confdesc->{"unused$i"} = $unuseddesc;
-}
-
 my $kvm_api_version = 0;
 
 sub kvm_version {
@@ -1440,21 +1143,6 @@ sub kernel_has_vhost_net {
     return -c '/dev/vhost-net';
 }
 
-sub valid_drive_names {
-    # order is important - used to autoselect boot disk
-    return ((map { "ide$_" } (0 .. ($MAX_IDE_DISKS - 1))),
-            (map { "scsi$_" } (0 .. ($MAX_SCSI_DISKS - 1))),
-            (map { "virtio$_" } (0 .. ($MAX_VIRTIO_DISKS - 1))),
-            (map { "sata$_" } (0 .. ($MAX_SATA_DISKS - 1))),
-            'efidisk0');
-}
-
-sub is_valid_drivename {
-    my $dev = shift;
-
-    return defined($drivename_hash->{$dev});
-}
-
 sub option_exists {
     my $key = shift;
     return defined($confdesc->{$key});
@@ -1571,95 +1259,6 @@ sub pve_verify_hotplug_features {
     die "unable to parse hotplug option\n";
 }
 
-# ideX = [volume=]volume-id[,media=d][,cyls=c,heads=h,secs=s[,trans=t]]
-#        [,snapshot=on|off][,cache=on|off][,format=f][,backup=yes|no]
-#        [,rerror=ignore|report|stop][,werror=enospc|ignore|report|stop]
-#        [,aio=native|threads][,discard=ignore|on][,detect_zeroes=on|off]
-#        [,iothread=on][,serial=serial][,model=model]
-
-sub parse_drive {
-    my ($key, $data) = @_;
-
-    my ($interface, $index);
-
-    if ($key =~ m/^([^\d]+)(\d+)$/) {
-       $interface = $1;
-       $index = $2;
-    } else {
-       return undef;
-    }
-
-    my $desc = $key =~ /^unused\d+$/ ? $alldrive_fmt
-                                     : $confdesc->{$key}->{format};
-    if (!$desc) {
-       warn "invalid drive key: $key\n";
-       return undef;
-    }
-    my $res = eval { PVE::JSONSchema::parse_property_string($desc, $data) };
-    return undef if !$res;
-    $res->{interface} = $interface;
-    $res->{index} = $index;
-
-    my $error = 0;
-    foreach my $opt (qw(bps bps_rd bps_wr)) {
-       if (my $bps = defined(delete $res->{$opt})) {
-           if (defined($res->{"m$opt"})) {
-               warn "both $opt and m$opt specified\n";
-               ++$error;
-               next;
-           }
-           $res->{"m$opt"} = sprintf("%.3f", $bps / (1024*1024.0));
-       }
-    }
-
-    # can't use the schema's 'requires' because of the mbps* => bps* "transforming aliases"
-    for my $requirement (
-       [mbps_max => 'mbps'],
-       [mbps_rd_max => 'mbps_rd'],
-       [mbps_wr_max => 'mbps_wr'],
-       [miops_max => 'miops'],
-       [miops_rd_max => 'miops_rd'],
-       [miops_wr_max => 'miops_wr'],
-       [bps_max_length => 'mbps_max'],
-       [bps_rd_max_length => 'mbps_rd_max'],
-       [bps_wr_max_length => 'mbps_wr_max'],
-       [iops_max_length => 'iops_max'],
-       [iops_rd_max_length => 'iops_rd_max'],
-       [iops_wr_max_length => 'iops_wr_max']) {
-       my ($option, $requires) = @$requirement;
-       if ($res->{$option} && !$res->{$requires}) {
-           warn "$option requires $requires\n";
-           ++$error;
-       }
-    }
-
-    return undef if $error;
-
-    return undef if $res->{mbps_rd} && $res->{mbps};
-    return undef if $res->{mbps_wr} && $res->{mbps};
-    return undef if $res->{iops_rd} && $res->{iops};
-    return undef if $res->{iops_wr} && $res->{iops};
-
-    if ($res->{media} && ($res->{media} eq 'cdrom')) {
-       return undef if $res->{snapshot} || $res->{trans} || $res->{format};
-       return undef if $res->{heads} || $res->{secs} || $res->{cyls};
-       return undef if $res->{interface} eq 'virtio';
-    }
-
-    if (my $size = $res->{size}) {
-       return undef if !defined($res->{size} = PVE::JSONSchema::parse_size($size));
-    }
-
-    return $res;
-}
-
-sub print_drive {
-    my ($drive) = @_;
-    my $data = { %$drive };
-    delete $data->{$_} for qw(index interface);
-    return PVE::JSONSchema::print_property_string($data, $alldrive_fmt);
-}
-
 sub scsi_inquiry {
     my($fh, $noerr) = @_;
 
@@ -1797,7 +1396,7 @@ sub print_drivedevice_full {
        $device .= ",wwn=$drive->{wwn}" if $drive->{wwn};
 
     } elsif ($drive->{interface} eq 'ide' || $drive->{interface} eq 'sata') {
-       my $maxdev = ($drive->{interface} eq 'sata') ? $MAX_SATA_DISKS : 2;
+       my $maxdev = ($drive->{interface} eq 'sata') ? $PVE::QemuServer::Drive::MAX_SATA_DISKS : 2;
        my $controller = int($drive->{index} / $maxdev);
        my $unit = $drive->{index} % $maxdev;
        my $devicetype = ($drive->{media} && $drive->{media} eq 'cdrom') ? "cd" : "hd";
@@ -1852,7 +1451,7 @@ sub get_initiator_name {
     return $initiator;
 }
 
-sub print_drive_full {
+sub print_drive_commandline_full {
     my ($storecfg, $vmid, $drive) = @_;
 
     my $path;
@@ -2082,20 +1681,6 @@ sub print_vga_device {
     return "$type,id=${vgaid}${memory}${max_outputs}${pciaddr}";
 }
 
-sub drive_is_cloudinit {
-    my ($drive) = @_;
-    return $drive->{file} =~ m@[:/]vm-\d+-cloudinit(?:\.$QEMU_FORMAT_RE)?$@;
-}
-
-sub drive_is_cdrom {
-    my ($drive, $exclude_cloudinit) = @_;
-
-    return 0 if $exclude_cloudinit && drive_is_cloudinit($drive);
-
-    return $drive && $drive->{media} && ($drive->{media} eq 'cdrom');
-
-}
-
 sub parse_number_sets {
     my ($set) = @_;
     my $res = [];
@@ -2305,17 +1890,6 @@ sub print_smbios1 {
 
 PVE::JSONSchema::register_format('pve-qm-smbios1', $smbios1_fmt);
 
-PVE::JSONSchema::register_format('pve-qm-bootdisk', \&verify_bootdisk);
-sub verify_bootdisk {
-    my ($value, $noerr) = @_;
-
-    return $value if is_valid_drivename($value);
-
-    return undef if $noerr;
-
-    die "invalid boot disk '$value'\n";
-}
-
 sub parse_watchdog {
     my ($value) = @_;
 
@@ -2348,6 +1922,16 @@ sub parse_vga {
     return $res;
 }
 
+sub parse_rng {
+    my ($value) = @_;
+
+    return undef if !$value;
+
+    my $res = eval { PVE::JSONSchema::parse_property_string($rng_fmt, $value) };
+    warn $@ if $@;
+    return $res;
+}
+
 PVE::JSONSchema::register_format('pve-qm-usb-device', \&verify_usb_device);
 sub verify_usb_device {
     my ($value, $noerr) = @_;
@@ -2840,26 +2424,6 @@ sub vzlist {
     return $vzlist;
 }
 
-sub disksize {
-    my ($storecfg, $conf) = @_;
-
-    my $bootdisk = $conf->{bootdisk};
-    return undef if !$bootdisk;
-    return undef if !is_valid_drivename($bootdisk);
-
-    return undef if !$conf->{$bootdisk};
-
-    my $drive = parse_drive($bootdisk, $conf->{$bootdisk});
-    return undef if !defined($drive);
-
-    return undef if drive_is_cdrom($drive);
-
-    my $volid = $drive->{file};
-    return undef if !$volid;
-
-    return $drive->{size};
-}
-
 our $vmstatus_return_properties = {
     vmid => get_standard_option('pve-vmid'),
     status => {
@@ -2947,7 +2511,7 @@ sub vmstatus {
        # fixme: better status?
        $d->{status} = $list->{$vmid}->{pid} ? 'running' : 'stopped';
 
-       my $size = disksize($storecfg, $conf);
+       my $size = PVE::QemuServer::Drive::bootdisk_size($storecfg, $conf);
        if (defined($size)) {
            $d->{disk} = 0; # no info available
            $d->{maxdisk} = $size;
@@ -3126,65 +2690,6 @@ sub vmstatus {
     return $res;
 }
 
-sub foreach_drive {
-    my ($conf, $func, @param) = @_;
-
-    foreach my $ds (valid_drive_names()) {
-       next if !defined($conf->{$ds});
-
-       my $drive = parse_drive($ds, $conf->{$ds});
-       next if !$drive;
-
-       &$func($ds, $drive, @param);
-    }
-}
-
-sub foreach_volid {
-    my ($conf, $func, @param) = @_;
-
-    my $volhash = {};
-
-    my $test_volid = sub {
-       my ($volid, $is_cdrom, $replicate, $shared, $snapname, $size) = @_;
-
-       return if !$volid;
-
-       $volhash->{$volid}->{cdrom} //= 1;
-       $volhash->{$volid}->{cdrom} = 0 if !$is_cdrom;
-
-       $volhash->{$volid}->{replicate} //= 0;
-       $volhash->{$volid}->{replicate} = 1 if $replicate;
-
-       $volhash->{$volid}->{shared} //= 0;
-       $volhash->{$volid}->{shared} = 1 if $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);
-       $volhash->{$volid}->{size} = $size if $size;
-    };
-
-    foreach_drive($conf, sub {
-       my ($ds, $drive) = @_;
-       $test_volid->($drive->{file}, drive_is_cdrom($drive), $drive->{replicate} // 1, $drive->{shared}, undef, $drive->{size});
-    });
-
-    foreach my $snapname (keys %{$conf->{snapshots}}) {
-       my $snap = $conf->{snapshots}->{$snapname};
-       $test_volid->($snap->{vmstate}, 0, 1, $snapname);
-       foreach_drive($snap, sub {
-           my ($ds, $drive) = @_;
-           $test_volid->($drive->{file}, drive_is_cdrom($drive), $drive->{replicate} // 1, $drive->{shared}, $snapname);
-        });
-    }
-
-    foreach my $volid (keys %$volhash) {
-       &$func($volid, $volhash->{$volid}, @param);
-    }
-}
-
 sub conf_has_serial {
     my ($conf) = @_;
 
@@ -3415,7 +2920,6 @@ sub config_to_command {
     my $devices = [];
     my $pciaddr = '';
     my $bridges = {};
-    my $vernum = 0; # unknown
     my $ostype = $conf->{ostype};
     my $winversion = windows_version($ostype);
     my $kvm = $conf->{kvm};
@@ -3425,6 +2929,11 @@ sub config_to_command {
     my $kvm_binary = get_command_for_arch($arch);
     my $kvmver = kvm_user_version($kvm_binary);
 
+    if (!$kvmver || $kvmver !~ m/^(\d+)\.(\d+)/ || $1 < 3) {
+       $kvmver //= "undefined";
+       die "Detected old QEMU binary ('$kvmver', at least 3.0 is required)\n";
+    }
+
     my $add_pve_version = min_version($kvmver, 4, 1);
 
     my $machine_type = get_vm_machine($conf, $forcemachine, $arch, $add_pve_version);
@@ -3449,6 +2958,8 @@ sub config_to_command {
     my $version_guard = sub {
        my ($major, $minor, $pve) = @_;
        return 0 if !min_version($machine_version, $major, $minor, $pve);
+       my $max_pve = PVE::QemuServer::Machine::get_pve_version("$major.$minor");
+       return 1 if min_version($machine_version, $major, $minor, $max_pve+1);
        $required_pve_version = $pve if $pve && $pve > $required_pve_version;
        return 1;
     };
@@ -3458,14 +2969,6 @@ sub config_to_command {
            if !defined kvm_version();
     }
 
-    if ($kvmver =~ m/^(\d+)\.(\d+)$/) {
-       $vernum = $1*1000000+$2*1000;
-    } elsif ($kvmver =~ m/^(\d+)\.(\d+)\.(\d+)$/) {
-       $vernum = $1*1000000+$2*1000+$3;
-    }
-
-    die "detected old qemu-kvm binary ($kvmver)\n" if $vernum < 15000;
-
     my $q35 = PVE::QemuServer::Machine::machine_type_is_q35($conf);
     my $hotplug_features = parse_hotplug_features(defined($conf->{hotplug}) ? $conf->{hotplug} : '1');
     my $use_old_bios_files = undef;
@@ -3527,7 +3030,7 @@ sub config_to_command {
        my $path;
        my $format;
        if (my $efidisk = $conf->{efidisk0}) {
-           my $d = PVE::JSONSchema::parse_property_string($efidisk_fmt, $efidisk);
+           my $d = parse_drive('efidisk0', $efidisk);
            my ($storeid, $volname) = PVE::Storage::parse_volume_id($d->{file}, 1);
            $format = $d->{format};
            if ($storeid) {
@@ -3548,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
@@ -3831,6 +3340,26 @@ sub config_to_command {
        }
     }
 
+    my $rng = parse_rng($conf->{rng0}) if $conf->{rng0};
+    if ($rng && &$version_guard(4, 1, 2)) {
+       my $max_bytes = $rng->{max_bytes} // $rng_fmt->{max_bytes}->{default};
+       my $period = $rng->{period} // $rng_fmt->{period}->{default};
+
+       my $limiter_str = "";
+       if ($max_bytes) {
+           $limiter_str = ",max-bytes=$max_bytes,period=$period";
+       }
+
+       # mostly relevant for /dev/hwrng, but doesn't hurt to check others too
+       die "cannot create VirtIO RNG device: source file '$rng->{source}' doesn't exist\n"
+           if ! -e $rng->{source};
+
+       my $rng_addr = print_pci_addr("rng0", $bridges, $arch, $machine_type);
+
+       push @$devices, '-object', "rng-random,filename=$rng->{source},id=rng0";
+       push @$devices, '-device', "virtio-rng-pci,rng=rng0$limiter_str$rng_addr";
+    }
+
     my $spice_port;
 
     if ($qxlnum) {
@@ -3955,13 +3484,13 @@ sub config_to_command {
         }
 
         if ($drive->{interface} eq 'sata') {
-           my $controller = int($drive->{index} / $MAX_SATA_DISKS);
+           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_full($storecfg, $vmid, $drive);
+       my $drive_cmd = print_drive_commandline_full($storecfg, $vmid, $drive);
        push @$devices, '-drive',$drive_cmd;
        push @$devices, '-device', print_drivedevice_full($storecfg, $conf, $vmid, $drive, $bridges, $arch, $machine_type);
     });
@@ -4343,7 +3872,7 @@ sub qemu_objectdel {
 sub qemu_driveadd {
     my ($storecfg, $vmid, $device) = @_;
 
-    my $drive = print_drive_full($storecfg, $vmid, $device);
+    my $drive = print_drive_commandline_full($storecfg, $vmid, $device);
     $drive =~ s/\\/\\\\/g;
     my $ret = PVE::QemuServer::Monitor::hmp_cmd($vmid, "drive_add auto \"$drive\"");
 
@@ -4424,8 +3953,8 @@ sub qemu_deletescsihw {
 
     my $devices_list = vm_devices_list($vmid);
     foreach my $opt (keys %{$devices_list}) {
-       if (PVE::QemuServer::is_valid_drivename($opt)) {
-           my $drive = PVE::QemuServer::parse_drive($opt, $conf->{$opt});
+       if (is_valid_drivename($opt)) {
+           my $drive = parse_drive($opt, $conf->{$opt});
            if($drive->{interface} eq 'scsi' && $drive->{index} < (($maxdev-1)*($controller+1))) {
                return 1;
            }
@@ -4660,6 +4189,9 @@ sub qemu_block_resize {
 
     return if !$running;
 
+    my $padding = (1024 - $size % 1024) % 1024;
+    $size = $size + $padding;
+
     mon_cmd($vmid, "block_resize", device => $deviceid, size => int($size));
 
 }
@@ -4934,7 +4466,7 @@ sub try_deallocate_drive {
 
            # check if the disk is really unused
            die "unable to delete '$volid' - volume is still in use (snapshot?)\n"
-               if is_volume_in_use($storecfg, $conf, $key, $volid);
+               if PVE::QemuServer::Drive::is_volume_in_use($storecfg, $conf, $key, $volid);
            PVE::Storage::vdisk_free($storecfg, $volid);
            return 1;
        } else {
@@ -5014,26 +4546,6 @@ sub vmconfig_apply_pending {
     PVE::QemuConfig->write_config($vmid, $conf);
 }
 
-my $safe_num_ne = sub {
-    my ($a, $b) = @_;
-
-    return 0 if !defined($a) && !defined($b);
-    return 1 if !defined($a);
-    return 1 if !defined($b);
-
-    return $a != $b;
-};
-
-my $safe_string_ne = sub {
-    my ($a, $b) = @_;
-
-    return 0 if !defined($a) && !defined($b);
-    return 1 if !defined($a);
-    return 1 if !defined($b);
-
-    return $a ne $b;
-};
-
 sub vmconfig_update_net {
     my ($storecfg, $conf, $hotplug, $vmid, $opt, $value, $arch, $machine_type) = @_;
 
@@ -5042,9 +4554,9 @@ sub vmconfig_update_net {
     if ($conf->{$opt}) {
        my $oldnet = parse_net($conf->{$opt});
 
-       if (&$safe_string_ne($oldnet->{model}, $newnet->{model}) ||
-           &$safe_string_ne($oldnet->{macaddr}, $newnet->{macaddr}) ||
-           &$safe_num_ne($oldnet->{queues}, $newnet->{queues}) ||
+       if (safe_string_ne($oldnet->{model}, $newnet->{model}) ||
+           safe_string_ne($oldnet->{macaddr}, $newnet->{macaddr}) ||
+           safe_num_ne($oldnet->{queues}, $newnet->{queues}) ||
            !($newnet->{bridge} && $oldnet->{bridge})) { # bridge/nat mode change
 
             # for non online change, we try to hot-unplug
@@ -5055,19 +4567,24 @@ sub vmconfig_update_net {
            die "internal error" if $opt !~ m/net(\d+)/;
            my $iface = "tap${vmid}i$1";
 
-           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})) {
+           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})) {
                PVE::Network::tap_unplug($iface);
-               PVE::Network::tap_plug($iface, $newnet->{bridge}, $newnet->{tag}, $newnet->{firewall}, $newnet->{trunks}, $newnet->{rate});
-           } elsif (&$safe_num_ne($oldnet->{rate}, $newnet->{rate})) {
+
+               if ($have_sdn) {
+                   PVE::Network::SDN::Zones::tap_plug($iface, $newnet->{bridge}, $newnet->{tag}, $newnet->{firewall}, $newnet->{trunks}, $newnet->{rate});
+               } else {
+                   PVE::Network::tap_plug($iface, $newnet->{bridge}, $newnet->{tag}, $newnet->{firewall}, $newnet->{trunks}, $newnet->{rate});
+               }
+           } elsif (safe_num_ne($oldnet->{rate}, $newnet->{rate})) {
                # Rate can be applied on its own but any change above needs to
                # include the rate in tap_plug since OVS resets everything.
                PVE::Network::tap_rate_limit($iface, $newnet->{rate});
            }
 
-           if (&$safe_string_ne($oldnet->{link_down}, $newnet->{link_down})) {
+           if (safe_string_ne($oldnet->{link_down}, $newnet->{link_down})) {
                qemu_set_link_status($vmid, $opt, !$newnet->{link_down});
            }
 
@@ -5109,33 +4626,33 @@ sub vmconfig_update_disk {
                    # update existing disk
 
                    # skip non hotpluggable value
-                   if (&$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->{cache}, $old_drive->{cache}) ||
-                       &$safe_string_ne($drive->{ssd}, $old_drive->{ssd})) {
+                   if (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->{cache}, $old_drive->{cache}) ||
+                       safe_string_ne($drive->{ssd}, $old_drive->{ssd})) {
                        die "skip\n";
                    }
 
                    # apply throttle
-                   if (&$safe_num_ne($drive->{mbps}, $old_drive->{mbps}) ||
-                       &$safe_num_ne($drive->{mbps_rd}, $old_drive->{mbps_rd}) ||
-                       &$safe_num_ne($drive->{mbps_wr}, $old_drive->{mbps_wr}) ||
-                       &$safe_num_ne($drive->{iops}, $old_drive->{iops}) ||
-                       &$safe_num_ne($drive->{iops_rd}, $old_drive->{iops_rd}) ||
-                       &$safe_num_ne($drive->{iops_wr}, $old_drive->{iops_wr}) ||
-                       &$safe_num_ne($drive->{mbps_max}, $old_drive->{mbps_max}) ||
-                       &$safe_num_ne($drive->{mbps_rd_max}, $old_drive->{mbps_rd_max}) ||
-                       &$safe_num_ne($drive->{mbps_wr_max}, $old_drive->{mbps_wr_max}) ||
-                       &$safe_num_ne($drive->{iops_max}, $old_drive->{iops_max}) ||
-                       &$safe_num_ne($drive->{iops_rd_max}, $old_drive->{iops_rd_max}) ||
-                       &$safe_num_ne($drive->{iops_wr_max}, $old_drive->{iops_wr_max}) ||
-                       &$safe_num_ne($drive->{bps_max_length}, $old_drive->{bps_max_length}) ||
-                       &$safe_num_ne($drive->{bps_rd_max_length}, $old_drive->{bps_rd_max_length}) ||
-                       &$safe_num_ne($drive->{bps_wr_max_length}, $old_drive->{bps_wr_max_length}) ||
-                       &$safe_num_ne($drive->{iops_max_length}, $old_drive->{iops_max_length}) ||
-                       &$safe_num_ne($drive->{iops_rd_max_length}, $old_drive->{iops_rd_max_length}) ||
-                       &$safe_num_ne($drive->{iops_wr_max_length}, $old_drive->{iops_wr_max_length})) {
+                   if (safe_num_ne($drive->{mbps}, $old_drive->{mbps}) ||
+                       safe_num_ne($drive->{mbps_rd}, $old_drive->{mbps_rd}) ||
+                       safe_num_ne($drive->{mbps_wr}, $old_drive->{mbps_wr}) ||
+                       safe_num_ne($drive->{iops}, $old_drive->{iops}) ||
+                       safe_num_ne($drive->{iops_rd}, $old_drive->{iops_rd}) ||
+                       safe_num_ne($drive->{iops_wr}, $old_drive->{iops_wr}) ||
+                       safe_num_ne($drive->{mbps_max}, $old_drive->{mbps_max}) ||
+                       safe_num_ne($drive->{mbps_rd_max}, $old_drive->{mbps_rd_max}) ||
+                       safe_num_ne($drive->{mbps_wr_max}, $old_drive->{mbps_wr_max}) ||
+                       safe_num_ne($drive->{iops_max}, $old_drive->{iops_max}) ||
+                       safe_num_ne($drive->{iops_rd_max}, $old_drive->{iops_rd_max}) ||
+                       safe_num_ne($drive->{iops_wr_max}, $old_drive->{iops_wr_max}) ||
+                       safe_num_ne($drive->{bps_max_length}, $old_drive->{bps_max_length}) ||
+                       safe_num_ne($drive->{bps_rd_max_length}, $old_drive->{bps_rd_max_length}) ||
+                       safe_num_ne($drive->{bps_wr_max_length}, $old_drive->{bps_wr_max_length}) ||
+                       safe_num_ne($drive->{iops_max_length}, $old_drive->{iops_max_length}) ||
+                       safe_num_ne($drive->{iops_rd_max_length}, $old_drive->{iops_rd_max_length}) ||
+                       safe_num_ne($drive->{iops_wr_max_length}, $old_drive->{iops_wr_max_length})) {
 
                        qemu_block_set_io_throttle($vmid,"drive-$opt",
                                                   ($drive->{mbps} || 0)*1024*1024,
@@ -5194,7 +4711,8 @@ sub vmconfig_update_disk {
 
 sub vm_start {
     my ($storecfg, $vmid, $statefile, $skiplock, $migratedfrom, $paused,
-       $forcemachine, $spice_ticket, $migration_network, $migration_type, $targetstorage, $timeout) = @_;
+       $forcemachine, $spice_ticket, $migration_network, $migration_type,
+       $targetstorage, $timeout, $nbd_protocol_version, $replicated_volumes) = @_;
 
     PVE::QemuConfig->lock_config($vmid, sub {
        my $conf = PVE::QemuConfig->load_config($vmid, $migratedfrom);
@@ -5248,6 +4766,12 @@ sub vm_start {
            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
+                   $local_volumes->{$opt} = $conf->{${opt}};
+                   print "re-using replicated volume: $opt - $volid\n";
+                   next;
+               }
                my $drive = parse_drive($opt, $conf->{$opt});
 
                # If a remote storage is specified and the format of the original
@@ -5472,20 +4996,29 @@ sub vm_start {
 
        #start nbd server for storage migration
        if ($targetstorage) {
-           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}" } } );
+           $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);
 
-           $localip = "[$localip]" if Net::IP::ip_is_ipv6($localip);
+               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}";
+           }
 
            foreach my $opt (sort keys %$local_volumes) {
                my $drivestr = $local_volumes->{$opt};
                mon_cmd($vmid, "nbd-server-add", device => "drive-$opt", writable => JSON::true );
-               my $migrate_storage_uri = "nbd:${localip}:${storage_migrate_port}:exportname=drive-$opt";
-               print "storage migration listens on $migrate_storage_uri volume:$drivestr\n";
+               print "storage migration listens on $migrate_storage_uri:exportname=drive-$opt volume:$drivestr\n";
            }
        }
 
@@ -5926,7 +5459,7 @@ sub tar_restore_cleanup {
     }
 }
 
-sub restore_archive {
+sub restore_file_archive {
     my ($archive, $vmid, $user, $opts) = @_;
 
     my $format = $opts->{format};
@@ -5960,7 +5493,144 @@ sub restore_archive {
     }
 }
 
-sub restore_update_config_line {
+# hepler to remove disks that will not be used after restore
+my $restore_cleanup_oldconf = sub {
+    my ($storecfg, $vmid, $oldconf, $virtdev_hash) = @_;
+
+    foreach_drive($oldconf, sub {
+       my ($ds, $drive) = @_;
+
+       return if drive_is_cdrom($drive, 1);
+
+       my $volid = $drive->{file};
+       return if !$volid || $volid =~ m|^/|;
+
+       my ($path, $owner) = PVE::Storage::path($storecfg, $volid);
+       return if !$path || !$owner || ($owner != $vmid);
+
+       # Note: only delete disk we want to restore
+       # other volumes will become unused
+       if ($virtdev_hash->{$ds}) {
+           eval { PVE::Storage::vdisk_free($storecfg, $volid); };
+           if (my $err = $@) {
+               warn $err;
+           }
+       }
+    });
+
+    # delete vmstate files, after the restore we have no snapshots anymore
+    foreach my $snapname (keys %{$oldconf->{snapshots}}) {
+       my $snap = $oldconf->{snapshots}->{$snapname};
+       if ($snap->{vmstate}) {
+           eval { PVE::Storage::vdisk_free($storecfg, $snap->{vmstate}); };
+           if (my $err = $@) {
+               warn $err;
+           }
+       }
+    }
+};
+
+# Helper to parse vzdump backup device hints
+#
+# $rpcenv: Environment, used to ckeck storage permissions
+# $user: User ID, to check storage permissions
+# $storecfg: Storage configuration
+# $fh: the file handle for reading the configuration
+# $devinfo: should contain device sizes for all backu-up'ed devices
+# $options: backup options (pool, default storage)
+#
+# Return: $virtdev_hash, updates $devinfo (add devname, virtdev, format, storeid)
+my $parse_backup_hints = sub {
+    my ($rpcenv, $user, $storecfg, $fh, $devinfo, $options) = @_;
+
+    my $virtdev_hash = {};
+
+    while (defined(my $line = <$fh>)) {
+       if ($line =~ m/^\#qmdump\#map:(\S+):(\S+):(\S*):(\S*):$/) {
+           my ($virtdev, $devname, $storeid, $format) = ($1, $2, $3, $4);
+           die "archive does not contain data for drive '$virtdev'\n"
+               if !$devinfo->{$devname};
+
+           if (defined($options->{storage})) {
+               $storeid = $options->{storage} || 'local';
+           } elsif (!$storeid) {
+               $storeid = 'local';
+           }
+           $format = 'raw' if !$format;
+           $devinfo->{$devname}->{devname} = $devname;
+           $devinfo->{$devname}->{virtdev} = $virtdev;
+           $devinfo->{$devname}->{format} = $format;
+           $devinfo->{$devname}->{storeid} = $storeid;
+
+           # check permission on storage
+           my $pool = $options->{pool}; # todo: do we need that?
+           if ($user ne 'root@pam') {
+               $rpcenv->check($user, "/storage/$storeid", ['Datastore.AllocateSpace']);
+           }
+
+           $virtdev_hash->{$virtdev} = $devinfo->{$devname};
+       } elsif ($line =~ m/^((?:ide|sata|scsi)\d+):\s*(.*)\s*$/) {
+           my $virtdev = $1;
+           my $drive = parse_drive($virtdev, $2);
+           if (drive_is_cloudinit($drive)) {
+               my ($storeid, $volname) = PVE::Storage::parse_volume_id($drive->{file});
+               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,
+                   size => PVE::QemuServer::Cloudinit::CLOUDINIT_DISK_SIZE,
+                   is_cloudinit => 1,
+               };
+           }
+       }
+    }
+
+    return $virtdev_hash;
+};
+
+# Helper to allocate and activate all volumes required for a restore
+#
+# $storecfg: Storage configuration
+# $virtdev_hash: as returned by parse_backup_hints()
+#
+# Returns: { $virtdev => $volid }
+my $restore_allocate_devices = sub {
+    my ($storecfg, $virtdev_hash, $vmid) = @_;
+
+    my $map = {};
+    foreach my $virtdev (sort keys %$virtdev_hash) {
+       my $d = $virtdev_hash->{$virtdev};
+       my $alloc_size = int(($d->{size} + 1024 - 1)/1024);
+       my $storeid = $d->{storeid};
+       my $scfg = PVE::Storage::storage_config($storecfg, $storeid);
+
+       # test if requested format is supported
+       my ($defFormat, $validFormats) = PVE::Storage::storage_default_format($storecfg, $storeid);
+       my $supported = grep { $_ eq $d->{format} } @$validFormats;
+       $d->{format} = $defFormat if !$supported;
+
+       my $name;
+       if ($d->{is_cloudinit}) {
+           $name = "vm-$vmid-cloudinit";
+           $name .= ".$d->{format}" if $d->{format} ne 'raw';
+       }
+
+       my $volid = PVE::Storage::vdisk_alloc($storecfg, $storeid, $vmid, $d->{format}, $name, $alloc_size);
+
+       print STDERR "new volume ID is '$volid'\n";
+       $d->{volid} = $volid;
+
+       PVE::Storage::activate_volumes($storecfg, [$volid]);
+
+       $map->{$virtdev} = $volid;
+    }
+
+    return $map;
+};
+
+my $restore_update_config_line = sub {
     my ($outfd, $cookie, $vmid, $map, $line, $unique) = @_;
 
     return if $line =~ m/^\#qmdump\#/;
@@ -6023,7 +5693,37 @@ sub restore_update_config_line {
     } else {
        print $outfd $line;
     }
-}
+};
+
+my $restore_deactivate_volumes = sub {
+    my ($storecfg, $devinfo) = @_;
+
+    my $vollist = [];
+    foreach my $devname (keys %$devinfo) {
+       my $volid = $devinfo->{$devname}->{volid};
+       push @$vollist, $volid if $volid;
+    }
+
+    PVE::Storage::deactivate_volumes($storecfg, $vollist);
+};
+
+my $restore_destroy_volumes = sub {
+    my ($storecfg, $devinfo) = @_;
+
+    foreach my $devname (keys %$devinfo) {
+       my $volid = $devinfo->{$devname}->{volid};
+       next if !$volid;
+       eval {
+           if ($volid =~ m|^/|) {
+               unlink $volid || die 'unlink failed\n';
+           } else {
+               PVE::Storage::vdisk_free($storecfg, $volid);
+           }
+           print STDERR "temporary volume '$volid' sucessfuly removed\n";
+       };
+       print STDERR "unable to cleanup '$volid' - $@" if $@;
+    }
+};
 
 sub scan_volids {
     my ($cfg, $vmid) = @_;
@@ -6042,68 +5742,6 @@ sub scan_volids {
     return $volid_hash;
 }
 
-sub is_volume_in_use {
-    my ($storecfg, $conf, $skip_drive, $volid) = @_;
-
-    my $path = PVE::Storage::path($storecfg, $volid);
-
-    my $scan_config = sub {
-       my ($cref, $snapname) = @_;
-
-       foreach my $key (keys %$cref) {
-           my $value = $cref->{$key};
-           if (is_valid_drivename($key)) {
-               next if $skip_drive && $key eq $skip_drive;
-               my $drive = parse_drive($key, $value);
-               next if !$drive || !$drive->{file} || drive_is_cdrom($drive);
-               return 1 if $volid eq $drive->{file};
-               if ($drive->{file} =~ m!^/!) {
-                   return 1 if $drive->{file} eq $path;
-               } else {
-                   my ($storeid, $volname) = PVE::Storage::parse_volume_id($drive->{file}, 1);
-                   next if !$storeid;
-                   my $scfg = PVE::Storage::storage_config($storecfg, $storeid, 1);
-                   next if !$scfg;
-                   return 1 if $path eq PVE::Storage::path($storecfg, $drive->{file}, $snapname);
-               }
-           }
-       }
-
-       return 0;
-    };
-
-    return 1 if &$scan_config($conf);
-
-    undef $skip_drive;
-
-    foreach my $snapname (keys %{$conf->{snapshots}}) {
-       return 1 if &$scan_config($conf->{snapshots}->{$snapname}, $snapname);
-    }
-
-    return 0;
-}
-
-sub update_disksize {
-    my ($drive, $volid_hash) = @_;
-
-    my $volid = $drive->{file};
-    return undef if !defined($volid);
-
-    my $oldsize = $drive->{size};
-    my $newsize = $volid_hash->{$volid}->{size};
-
-    if (defined($newsize) && defined($oldsize) && $newsize != $oldsize) {
-       $drive->{size} = $newsize;
-
-       my $old_fmt = PVE::JSONSchema::format_size($oldsize);
-       my $new_fmt = PVE::JSONSchema::format_size($newsize);
-
-       return wantarray ? ($drive, $old_fmt, $new_fmt) : $drive;
-    }
-
-    return undef;
-}
-
 sub update_disk_config {
     my ($vmid, $conf, $volid_hash) = @_;
 
@@ -6136,7 +5774,7 @@ sub update_disk_config {
            next if drive_is_cdrom($drive);
            next if !$volid_hash->{$volid};
 
-           my ($updated, $old_size, $new_size) = update_disksize($drive, $volid_hash);
+           my ($updated, $old_size, $new_size) = PVE::QemuServer::Drive::update_disksize($drive, $volid_hash);
            if (defined($updated)) {
                $changes = 1;
                $conf->{$opt} = print_drive($updated);
@@ -6225,6 +5863,174 @@ sub rescan {
     }
 }
 
+sub restore_proxmox_backup_archive {
+    my ($archive, $vmid, $user, $options) = @_;
+
+    my $storecfg = PVE::Storage::config();
+
+    my ($storeid, $volname) = PVE::Storage::parse_volume_id($archive);
+    my $scfg = PVE::Storage::storage_config($storecfg, $storeid);
+
+    my $server = $scfg->{server};
+    my $datastore = $scfg->{datastore};
+    my $username = $scfg->{username} // 'root@pam';
+    my $fingerprint = $scfg->{fingerprint};
+
+    my $repo = "$username\@$server:$datastore";
+    my $password = PVE::Storage::PBSPlugin::pbs_get_password($scfg, $storeid);
+    local $ENV{PBS_PASSWORD} = $password;
+    local $ENV{PBS_FINGERPRINT} = $fingerprint if defined($fingerprint);
+
+    my ($vtype, $pbs_backup_name, undef, undef, undef, undef, $format) =
+       PVE::Storage::parse_volname($storecfg, $archive);
+
+    die "got unexpected vtype '$vtype'\n" if $vtype ne 'backup';
+
+    die "got unexpected backup format '$format'\n" if $format ne 'pbs-vm';
+
+    my $tmpdir = "/var/tmp/vzdumptmp$$";
+    rmtree $tmpdir;
+    mkpath $tmpdir;
+
+    my $conffile = PVE::QemuConfig->config_file($vmid);
+    my $tmpfn = "$conffile.$$.tmp";
+     # disable interrupts (always do cleanups)
+    local $SIG{INT} =
+       local $SIG{TERM} =
+       local $SIG{QUIT} =
+       local $SIG{HUP} = sub { print STDERR "got interrupt - ignored\n"; };
+
+    # Note: $oldconf is undef if VM does not exists
+    my $cfs_path = PVE::QemuConfig->cfs_config_path($vmid);
+    my $oldconf = PVE::Cluster::cfs_read_file($cfs_path);
+
+    my $rpcenv = PVE::RPCEnvironment::get();
+    my $devinfo = {};
+
+    eval {
+       # enable interrupts
+       local $SIG{INT} =
+           local $SIG{TERM} =
+           local $SIG{QUIT} =
+           local $SIG{HUP} =
+           local $SIG{PIPE} = sub { die "interrupted by signal\n"; };
+
+       my $cfgfn = "$tmpdir/qemu-server.conf";
+       my $firewall_config_fn = "$tmpdir/fw.conf";
+       my $index_fn = "$tmpdir/index.json";
+
+       my $cmd = "restore";
+
+       my $param = [$pbs_backup_name, "index.json", $index_fn];
+       PVE::Storage::PBSPlugin::run_raw_client_cmd($scfg, $storeid, $cmd, $param);
+       my $index = PVE::Tools::file_get_contents($index_fn);
+       $index = decode_json($index);
+
+       # print Dumper($index);
+       foreach my $info (@{$index->{files}}) {
+           if ($info->{filename} =~ m/^(drive-\S+).img.fidx$/) {
+               my $devname = $1;
+               if ($info->{size} =~ m/^(\d+)$/) { # untaint size
+                   $devinfo->{$devname}->{size} = $1;
+               } else {
+                   die "unable to parse file size in 'index.json' - got '$info->{size}'\n";
+               }
+           }
+       }
+
+       my $is_qemu_server_backup = scalar(grep { $_->{filename} eq 'qemu-server.conf.blob' } @{$index->{files}});
+       if (!$is_qemu_server_backup) {
+           die "backup does not look like a qemu-server backup (missing 'qemu-server.conf' file)\n";
+       }
+       my $has_firewall_config = scalar(grep { $_->{filename} eq 'fw.conf.blob' } @{$index->{files}});
+
+       $param = [$pbs_backup_name, "qemu-server.conf", $cfgfn];
+       PVE::Storage::PBSPlugin::run_raw_client_cmd($scfg, $storeid, $cmd, $param);
+
+       if ($has_firewall_config) {
+           $param = [$pbs_backup_name, "fw.conf", $firewall_config_fn];
+           PVE::Storage::PBSPlugin::run_raw_client_cmd($scfg, $storeid, $cmd, $param);
+
+           my $pve_firewall_dir = '/etc/pve/firewall';
+           mkdir $pve_firewall_dir; # make sure the dir exists
+           PVE::Tools::file_copy($firewall_config_fn, "${pve_firewall_dir}/$vmid.fw");
+       }
+
+       my $fh = IO::File->new($cfgfn, "r") ||
+           "unable to read qemu-server.conf - $!\n";
+
+       my $virtdev_hash = $parse_backup_hints->($rpcenv, $user, $storecfg, $fh, $devinfo, $options);
+
+       # fixme: rate limit?
+
+       # create empty/temp config
+       PVE::Tools::file_set_contents($conffile, "memory: 128\nlock: create");
+
+       $restore_cleanup_oldconf->($storecfg, $vmid, $oldconf, $virtdev_hash) if $oldconf;
+
+       # allocate volumes
+       my $map = $restore_allocate_devices->($storecfg, $virtdev_hash, $vmid);
+
+       foreach my $virtdev (sort keys %$virtdev_hash) {
+           my $d = $virtdev_hash->{$virtdev};
+           next if $d->{is_cloudinit}; # no need to restore cloudinit
+
+           my $volid = $d->{volid};
+
+           my $path = PVE::Storage::path($storecfg, $volid);
+
+           my $pbs_restore_cmd = [
+               '/usr/bin/pbs-restore',
+               '--repository', $repo,
+               $pbs_backup_name,
+               "$d->{devname}.img.fidx",
+               $path,
+               '--verbose',
+               ];
+
+           if (PVE::Storage::volume_has_feature($storecfg, 'sparseinit', $volid)) {
+               push @$pbs_restore_cmd, '--skip-zero';
+           }
+
+           my $dbg_cmdstring = PVE::Tools::cmd2string($pbs_restore_cmd);
+           print "restore proxmox backup image: $dbg_cmdstring\n";
+           run_command($pbs_restore_cmd);
+       }
+
+       $fh->seek(0, 0) || die "seek failed - $!\n";
+
+       my $outfd = new IO::File ($tmpfn, "w") ||
+           die "unable to write config for VM $vmid\n";
+
+       my $cookie = { netcount => 0 };
+       while (defined(my $line = <$fh>)) {
+           $restore_update_config_line->($outfd, $cookie, $vmid, $map, $line, $options->{unique});
+       }
+
+       $fh->close();
+       $outfd->close();
+    };
+    my $err = $@;
+
+    $restore_deactivate_volumes->($storecfg, $devinfo);
+
+    rmtree $tmpdir;
+
+    if ($err) {
+       unlink $tmpfn;
+       $restore_destroy_volumes->($storecfg, $devinfo);
+       die $err;
+    }
+
+    rename($tmpfn, $conffile) ||
+       die "unable to commit configuration file '$conffile'\n";
+
+    PVE::Cluster::cfs_update(); # make sure we read new file
+
+    eval { rescan($vmid, 1); };
+    warn $@ if $@;
+}
+
 sub restore_vma_archive {
     my ($archive, $vmid, $user, $opts, $comp) = @_;
 
@@ -6309,8 +6115,6 @@ sub restore_vma_archive {
     my %storage_limits;
 
     my $print_devmap = sub {
-       my $virtdev_hash = {};
-
        my $cfgfn = "$tmpdir/qemu-server.conf";
 
        # we can read the config - that is already extracted
@@ -6324,51 +6128,7 @@ sub restore_vma_archive {
            PVE::Tools::file_copy($fwcfgfn, "${pve_firewall_dir}/$vmid.fw");
        }
 
-       while (defined(my $line = <$fh>)) {
-           if ($line =~ m/^\#qmdump\#map:(\S+):(\S+):(\S*):(\S*):$/) {
-               my ($virtdev, $devname, $storeid, $format) = ($1, $2, $3, $4);
-               die "archive does not contain data for drive '$virtdev'\n"
-                   if !$devinfo->{$devname};
-               if (defined($opts->{storage})) {
-                   $storeid = $opts->{storage} || 'local';
-               } elsif (!$storeid) {
-                   $storeid = 'local';
-               }
-               $format = 'raw' if !$format;
-               $devinfo->{$devname}->{devname} = $devname;
-               $devinfo->{$devname}->{virtdev} = $virtdev;
-               $devinfo->{$devname}->{format} = $format;
-               $devinfo->{$devname}->{storeid} = $storeid;
-
-               # check permission on storage
-               my $pool = $opts->{pool}; # todo: do we need that?
-               if ($user ne 'root@pam') {
-                   $rpcenv->check($user, "/storage/$storeid", ['Datastore.AllocateSpace']);
-               }
-
-               $storage_limits{$storeid} = $bwlimit;
-
-               $virtdev_hash->{$virtdev} = $devinfo->{$devname};
-           } elsif ($line =~ m/^((?:ide|sata|scsi)\d+):\s*(.*)\s*$/) {
-               my $virtdev = $1;
-               my $drive = parse_drive($virtdev, $2);
-               if (drive_is_cloudinit($drive)) {
-                   my ($storeid, $volname) = PVE::Storage::parse_volume_id($drive->{file});
-                   my $scfg = PVE::Storage::storage_config($cfg, $storeid);
-                   my $format = qemu_img_format($scfg, $volname); # has 'raw' fallback
-
-                   my $d = {
-                       format => $format,
-                       storeid => $opts->{storage} // $storeid,
-                       size => PVE::QemuServer::Cloudinit::CLOUDINIT_DISK_SIZE,
-                       file => $drive->{file}, # to make drive_is_cloudinit check possible
-                       name => "vm-$vmid-cloudinit",
-                       is_cloudinit => 1,
-                   };
-                   $virtdev_hash->{$virtdev} = $d;
-               }
-           }
-       }
+       my $virtdev_hash = $parse_backup_hints->($rpcenv, $user, $cfg, $fh, $devinfo, $opts);
 
        foreach my $key (keys %storage_limits) {
            my $limit = PVE::Storage::get_bandwidth_limit('restore', [$key], $bwlimit);
@@ -6385,81 +6145,35 @@ sub restore_vma_archive {
        # create empty/temp config
        if ($oldconf) {
            PVE::Tools::file_set_contents($conffile, "memory: 128\n");
-           foreach_drive($oldconf, sub {
-               my ($ds, $drive) = @_;
-
-               return if drive_is_cdrom($drive, 1);
-
-               my $volid = $drive->{file};
-               return if !$volid || $volid =~ m|^/|;
-
-               my ($path, $owner) = PVE::Storage::path($cfg, $volid);
-               return if !$path || !$owner || ($owner != $vmid);
-
-               # Note: only delete disk we want to restore
-               # other volumes will become unused
-               if ($virtdev_hash->{$ds}) {
-                   eval { PVE::Storage::vdisk_free($cfg, $volid); };
-                   if (my $err = $@) {
-                       warn $err;
-                   }
-               }
-           });
-
-           # delete vmstate files, after the restore we have no snapshots anymore
-           foreach my $snapname (keys %{$oldconf->{snapshots}}) {
-               my $snap = $oldconf->{snapshots}->{$snapname};
-               if ($snap->{vmstate}) {
-                   eval { PVE::Storage::vdisk_free($cfg, $snap->{vmstate}); };
-                   if (my $err = $@) {
-                       warn $err;
-                   }
-               }
-           }
+           $restore_cleanup_oldconf->($cfg, $vmid, $oldconf, $virtdev_hash);
        }
 
-       my $map = {};
+       # allocate volumes
+       my $map = $restore_allocate_devices->($cfg, $virtdev_hash, $vmid);
+
+       # print restore information to $fifofh
        foreach my $virtdev (sort keys %$virtdev_hash) {
            my $d = $virtdev_hash->{$virtdev};
-           my $alloc_size = int(($d->{size} + 1024 - 1)/1024);
+           next if $d->{is_cloudinit}; # no need to restore cloudinit
+
            my $storeid = $d->{storeid};
-           my $scfg = PVE::Storage::storage_config($cfg, $storeid);
+           my $volid = $d->{volid};
 
            my $map_opts = '';
            if (my $limit = $storage_limits{$storeid}) {
                $map_opts .= "throttling.bps=$limit:throttling.group=$storeid:";
            }
 
-           # test if requested format is supported
-           my ($defFormat, $validFormats) = PVE::Storage::storage_default_format($cfg, $storeid);
-           my $supported = grep { $_ eq $d->{format} } @$validFormats;
-           $d->{format} = $defFormat if !$supported;
-
-           my $name;
-           if ($d->{is_cloudinit}) {
-               $name = $d->{name};
-               $name .= ".$d->{format}" if $d->{format} ne 'raw';
-           }
-
-           my $volid = PVE::Storage::vdisk_alloc($cfg, $storeid, $vmid, $d->{format}, $name, $alloc_size);
-           print STDERR "new volume ID is '$volid'\n";
-           $d->{volid} = $volid;
-
-           PVE::Storage::activate_volumes($cfg, [$volid]);
-
            my $write_zeros = 1;
            if (PVE::Storage::volume_has_feature($cfg, 'sparseinit', $volid)) {
                $write_zeros = 0;
            }
 
-           if (!$d->{is_cloudinit}) {
-               my $path = PVE::Storage::path($cfg, $volid);
+           my $path = PVE::Storage::path($cfg, $volid);
 
-               print $fifofh "${map_opts}format=$d->{format}:${write_zeros}:$d->{devname}=$path\n";
+           print $fifofh "${map_opts}format=$d->{format}:${write_zeros}:$d->{devname}=$path\n";
 
-               print "map '$d->{devname}' to '$path' (write zeros = ${write_zeros})\n";
-           }
-           $map->{$virtdev} = $volid;
+           print "map '$d->{devname}' to '$path' (write zeros = ${write_zeros})\n";
        }
 
        $fh->seek(0, 0) || die "seek failed - $!\n";
@@ -6469,7 +6183,7 @@ sub restore_vma_archive {
 
        my $cookie = { netcount => 0 };
        while (defined(my $line = <$fh>)) {
-           restore_update_config_line($outfd, $cookie, $vmid, $map, $line, $opts->{unique});
+           $restore_update_config_line->($outfd, $cookie, $vmid, $map, $line, $opts->{unique});
        }
 
        $fh->close();
@@ -6516,38 +6230,17 @@ sub restore_vma_archive {
 
     alarm($oldtimeout) if $oldtimeout;
 
-    my $vollist = [];
-    foreach my $devname (keys %$devinfo) {
-       my $volid = $devinfo->{$devname}->{volid};
-       push @$vollist, $volid if $volid;
-    }
-
-    PVE::Storage::deactivate_volumes($cfg, $vollist);
+    $restore_deactivate_volumes->($cfg, $devinfo);
 
     unlink $mapfifo;
+    rmtree $tmpdir;
 
     if ($err) {
-       rmtree $tmpdir;
        unlink $tmpfn;
-
-       foreach my $devname (keys %$devinfo) {
-           my $volid = $devinfo->{$devname}->{volid};
-           next if !$volid;
-           eval {
-               if ($volid =~ m|^/|) {
-                   unlink $volid || die 'unlink failed\n';
-               } else {
-                   PVE::Storage::vdisk_free($cfg, $volid);
-               }
-               print STDERR "temporary volume '$volid' sucessfuly removed\n";
-           };
-           print STDERR "unable to cleanup '$volid' - $@" if $@;
-       }
+       $restore_destroy_volumes->($cfg, $devinfo);
        die $err;
     }
 
-    rmtree $tmpdir;
-
     rename($tmpfn, $conffile) ||
        die "unable to commit configuration file '$conffile'\n";
 
@@ -6644,7 +6337,7 @@ sub restore_tar_archive {
 
        my $cookie = { netcount => 0 };
        while (defined (my $line = <$srcfd>)) {
-           restore_update_config_line($outfd, $cookie, $vmid, $map, $line, $opts->{unique});
+           $restore_update_config_line->($outfd, $cookie, $vmid, $map, $line, $opts->{unique});
        }
 
        $srcfd->close();
@@ -6779,7 +6472,7 @@ sub qemu_img_convert {
        $cachemode = 'none' if $src_scfg->{type} eq 'zfspool';
     } elsif (-f $src_volid) {
        $src_path = $src_volid;
-       if ($src_path =~ m/\.($QEMU_FORMAT_RE)$/) {
+       if ($src_path =~ m/\.($PVE::QemuServer::Drive::QEMU_FORMAT_RE)$/) {
            $src_format = $1;
        }
     }
@@ -6840,7 +6533,7 @@ sub qemu_img_convert {
 sub qemu_img_format {
     my ($scfg, $volname) = @_;
 
-    if ($scfg->{path} && $volname =~ m/\.($QEMU_FORMAT_RE)$/) {
+    if ($scfg->{path} && $volname =~ m/\.($PVE::QemuServer::Drive::QEMU_FORMAT_RE)$/) {
        return $1;
     } else {
        return "raw";
@@ -6848,7 +6541,7 @@ sub qemu_img_format {
 }
 
 sub qemu_drive_mirror {
-    my ($vmid, $drive, $dst_volid, $vmiddst, $is_zero_initialized, $jobs, $skipcomplete, $qga, $bwlimit) = @_;
+    my ($vmid, $drive, $dst_volid, $vmiddst, $is_zero_initialized, $jobs, $completion, $qga, $bwlimit, $src_bitmap) = @_;
 
     $jobs = {} if !$jobs;
 
@@ -6875,6 +6568,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";
@@ -6890,11 +6589,17 @@ sub qemu_drive_mirror {
        die "mirroring error: $err\n";
     }
 
-    qemu_drive_mirror_monitor ($vmid, $vmiddst, $jobs, $skipcomplete, $qga);
+    qemu_drive_mirror_monitor ($vmid, $vmiddst, $jobs, $completion, $qga);
 }
 
+# $completion can be either
+# 'complete': wait until all jobs are ready, block-job-complete them (default)
+# 'cancel': wait until all jobs are ready, block-job-cancel them
+# 'skip': wait until all jobs are ready, return with block jobs in ready state
 sub qemu_drive_mirror_monitor {
-    my ($vmid, $vmiddst, $jobs, $skipcomplete, $qga) = @_;
+    my ($vmid, $vmiddst, $jobs, $completion, $qga) = @_;
+
+    $completion //= 'complete';
 
     eval {
        my $err_complete = 0;
@@ -6939,7 +6644,7 @@ sub qemu_drive_mirror_monitor {
 
            if ($readycounter == scalar(keys %$jobs)) {
                print "all mirroring jobs are ready \n";
-               last if $skipcomplete; #do the complete later
+               last if $completion eq 'skip'; #do the complete later
 
                if ($vmiddst && $vmiddst != $vmid) {
                    my $agent_running = $qga && qga_check_running($vmid);
@@ -6969,7 +6674,15 @@ sub qemu_drive_mirror_monitor {
                        # try to switch the disk if source and destination are on the same guest
                        print "$job: Completing block job...\n";
 
-                       eval { mon_cmd($vmid, "block-job-complete", device => $job) };
+                       my $op;
+                       if ($completion eq 'complete') {
+                           $op = 'block-job-complete';
+                       } elsif ($completion eq 'cancel') {
+                           $op = 'block-job-cancel';
+                       } else {
+                           die "invalid completion value: $completion\n";
+                       }
+                       eval { mon_cmd($vmid, $op, device => $job) };
                        if ($@ =~ m/cannot be completed/) {
                            print "$job: Block job cannot be completed, try again.\n";
                            $err_complete++;
@@ -7025,7 +6738,7 @@ sub qemu_blockjobs_cancel {
 
 sub clone_disk {
     my ($storecfg, $vmid, $running, $drivename, $drive, $snapname,
-       $newvmid, $storage, $format, $full, $newvollist, $jobs, $skipcomplete, $qga, $bwlimit) = @_;
+       $newvmid, $storage, $format, $full, $newvollist, $jobs, $completion, $qga, $bwlimit, $conf) = @_;
 
     my $newvolid;
 
@@ -7048,6 +6761,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;
@@ -7061,7 +6776,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);
@@ -7070,7 +6794,7 @@ sub clone_disk {
                    if $drive->{iothread};
            }
 
-           qemu_drive_mirror($vmid, $drivename, $newvolid, $newvmid, $sparseinit, $jobs, $skipcomplete, $qga, $bwlimit);
+           qemu_drive_mirror($vmid, $drivename, $newvolid, $newvmid, $sparseinit, $jobs, $completion, $qga, $bwlimit);
        }
     }
 
@@ -7113,6 +6837,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) = @_;
 
@@ -7200,19 +6944,6 @@ sub resolve_dst_disk_format {
        return $format;
 }
 
-sub resolve_first_disk {
-    my $conf = shift;
-    my @disks = PVE::QemuServer::valid_drive_names();
-    my $firstdisk;
-    foreach my $ds (reverse @disks) {
-       next if !$conf->{$ds};
-       my $disk = PVE::QemuServer::parse_drive($ds, $conf->{$ds});
-       next if PVE::QemuServer::drive_is_cdrom($disk);
-       $firstdisk = $ds;
-    }
-    return $firstdisk;
-}
-
 # NOTE: if this logic changes, please update docs & possibly gui logic
 sub find_vmstate_storage {
     my ($conf, $storecfg) = @_;