]> 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 c40750aca0cad8242f167eb3034d543f424987e7..611fb4509e00d207931ccf153458b333dcd0ff30 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;
 }
@@ -2962,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;
     };
@@ -3053,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,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);
@@ -4761,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
@@ -4985,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";
            }
        }
 
@@ -5510,264 +5530,107 @@ my $restore_cleanup_oldconf = sub {
     }
 };
 
-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 $repo = "$username\@$server:$datastore";
-    my $password = PVE::Storage::PBSPlugin::pbs_get_password($scfg, $storeid);
-    local $ENV{PBS_PASSWORD} = $password;
-
-    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 = {};
+# 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) = @_;
 
-       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']);
-               }
+    my $virtdev_hash = {};
 
-               $virtdev_hash->{$virtdev} = $devinfo->{$devname};
-           } elsif ($line =~ m/^((?:ide|sata|scsi)\d+):\s*(.*)\s*$/) {
-               # fixme: cloudinit
-               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,
-                   };
-               }
+    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';
            }
-       }
-
-       # 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;
-
-       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';
+           $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']);
            }
 
-           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;
-
-           next if $d->{is_cloudinit}; # no need to restore cloudinit
-
-           PVE::Storage::activate_volumes($storecfg, [$volid]);
+           $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
 
-           my $path = PVE::Storage::path($storecfg, $volid);
-           if (PVE::Storage::volume_has_feature($storecfg, 'sparseinit', $volid)) {
-               #$path = "zeroinit:$path"; # fixme
+               $virtdev_hash->{$virtdev} = {
+                   format => $format,
+                   storeid => $options->{storage} // $storeid,
+                   size => PVE::QemuServer::Cloudinit::CLOUDINIT_DISK_SIZE,
+                   is_cloudinit => 1,
+               };
            }
-
-           my $pbs_restore_cmd = [
-               '/usr/bin/proxmox-backup-client',
-               'restore',
-               '--repository', $repo,
-               $pbs_backup_name,
-               "$d->{devname}.img",
-               '-',
-               '--verbose',
-               ];
-
-           my $import_cmd = [
-               '/usr/bin/qemu-img',
-               'dd', '-n', '-f', 'raw', '-O', $d->{format}, 'bs=64K',
-               'isize=0',
-               "osize=$d->{size}",
-               "of=$path",
-               ];
-
-           my $dbg_cmdstring = PVE::Tools::cmd2string($pbs_restore_cmd) . '|' . PVE::Tools::cmd2string($import_cmd);
-           print "restore proxmox backup image: $dbg_cmdstring\n";
-           run_command([$pbs_restore_cmd, $import_cmd]);
-
-           $map->{$virtdev} = $volid;
        }
+    }
 
-       $fh->seek(0, 0) || die "seek failed - $!\n";
+    return $virtdev_hash;
+};
 
-       my $outfd = new IO::File ($tmpfn, "w") ||
-           die "unable to write config for VM $vmid\n";
+# 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 $cookie = { netcount => 0 };
-       while (defined(my $line = <$fh>)) {
-           restore_update_config_line($outfd, $cookie, $vmid, $map, $line, $options->{unique});
+       my $name;
+       if ($d->{is_cloudinit}) {
+           $name = "vm-$vmid-cloudinit";
+           $name .= ".$d->{format}" if $d->{format} ne 'raw';
        }
 
-       $fh->close();
-       $outfd->close();
-    };
-    my $err = $@;
+       my $volid = PVE::Storage::vdisk_alloc($storecfg, $storeid, $vmid, $d->{format}, $name, $alloc_size);
 
-    my $vollist = [];
-    foreach my $devname (keys %$devinfo) {
-       my $volid = $devinfo->{$devname}->{volid};
-       push @$vollist, $volid if $volid;
-    }
+       print STDERR "new volume ID is '$volid'\n";
+       $d->{volid} = $volid;
 
-    PVE::Storage::deactivate_volumes($storecfg, $vollist);
-
-    if ($err) {
-       unlink $tmpfn;
-       rmtree $tmpdir;
+       PVE::Storage::activate_volumes($storecfg, [$volid]);
 
-       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 $@;
-       }
-       die $err;
+       $map->{$virtdev} = $volid;
     }
 
-    rmtree $tmpdir;
-
-    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 $@;
-}
+    return $map;
+};
 
-sub restore_update_config_line {
+my $restore_update_config_line = sub {
     my ($outfd, $cookie, $vmid, $map, $line, $unique) = @_;
 
     return if $line =~ m/^\#qmdump\#/;
@@ -5830,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) = @_;
@@ -5970,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) = @_;
 
@@ -6054,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
@@ -6069,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);
@@ -6133,48 +6148,32 @@ sub restore_vma_archive {
            $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";
@@ -6184,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();
@@ -6231,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";
 
@@ -6359,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();
@@ -6563,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;
 
@@ -6590,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";
@@ -6605,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;
@@ -6654,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);
@@ -6684,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++;
@@ -6740,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;
 
@@ -6763,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;
@@ -6776,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);
@@ -6785,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);
        }
     }
 
@@ -6828,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) = @_;