+ my $changes = update_disk_config($vmid, $conf, $vm_volids);
+
+ PVE::QemuConfig->write_config($vmid, $conf) if $changes && !$dryrun;
+ };
+
+ if (defined($vmid)) {
+ if ($nolock) {
+ &$updatefn($vmid);
+ } else {
+ PVE::QemuConfig->lock_config($vmid, $updatefn, $vmid);
+ }
+ } else {
+ my $vmlist = config_list();
+ foreach my $vmid (keys %$vmlist) {
+ if ($nolock) {
+ &$updatefn($vmid);
+ } else {
+ PVE::QemuConfig->lock_config($vmid, $updatefn, $vmid);
+ }
+ }
+ }
+}
+
+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 $fingerprint = $scfg->{fingerprint};
+ my $keyfile = PVE::Storage::PBSPlugin::pbs_encryption_key_file_name($storecfg, $storeid);
+
+ my $repo = PVE::PBSClient::get_repository($scfg);
+ my $namespace = $scfg->{namespace};
+
+ # This is only used for `pbs-restore` and the QEMU PBS driver (live-restore)
+ 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);
+ # 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 $new_conf_raw = '';
+
+ my $rpcenv = PVE::RPCEnvironment::get();
+ my $devinfo = {}; # info about drives included in backup
+ my $virtdev_hash = {}; # info about allocated drives
+
+ 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);
+
+ 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") ||
+ die "unable to read qemu-server.conf - $!\n";
+
+ $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
+
+ # this fails if storage is unavailable
+ my $volid = $d->{volid};
+ my $path = PVE::Storage::path($storecfg, $volid);
+
+ # for live-restore we only want to preload the efidisk and TPM state
+ next if $options->{live} && $virtdev ne 'efidisk0' && $virtdev ne 'tpmstate0';
+
+ my @ns_arg;
+ if (defined(my $ns = $scfg->{namespace})) {
+ @ns_arg = ('--ns', $ns);
+ }
+
+ my $pbs_restore_cmd = [
+ '/usr/bin/pbs-restore',
+ '--repository', $repo,
+ @ns_arg,
+ $pbs_backup_name,
+ "$d->{devname}.img.fidx",
+ $path,
+ '--verbose',
+ ];
+
+ push @$pbs_restore_cmd, '--format', $d->{format} if $d->{format};
+ push @$pbs_restore_cmd, '--keyfile', $keyfile if -e $keyfile;
+
+ 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 $cookie = { netcount => 0 };
+ while (defined(my $line = <$fh>)) {
+ $new_conf_raw .= restore_update_config_line(
+ $cookie,
+ $map,
+ $line,
+ $options->{unique},
+ );
+ }
+
+ $fh->close();
+ };
+ my $err = $@;
+
+ if ($err || !$options->{live}) {
+ $restore_deactivate_volumes->($storecfg, $virtdev_hash);
+ }
+
+ rmtree $tmpdir;
+
+ if ($err) {
+ $restore_destroy_volumes->($storecfg, $virtdev_hash);
+ die $err;
+ }
+
+ if ($options->{live}) {
+ # keep lock during live-restore
+ $new_conf_raw .= "\nlock: create";
+ }
+
+ my $new_conf = restore_merge_config($conffile, $new_conf_raw, $options->{override_conf});
+ check_restore_permissions($rpcenv, $user, $new_conf);
+ PVE::QemuConfig->write_config($vmid, $new_conf);
+
+ eval { rescan($vmid, 1); };
+ warn $@ if $@;
+
+ PVE::AccessControl::add_vm_to_pool($vmid, $options->{pool}) if $options->{pool};
+
+ if ($options->{live}) {
+ # enable interrupts
+ local $SIG{INT} =
+ local $SIG{TERM} =
+ local $SIG{QUIT} =
+ local $SIG{HUP} =
+ local $SIG{PIPE} = sub { die "got signal ($!) - abort\n"; };
+
+ my $conf = PVE::QemuConfig->load_config($vmid);
+ die "cannot do live-restore for template\n" if PVE::QemuConfig->is_template($conf);
+
+ # these special drives are already restored before start
+ delete $devinfo->{'drive-efidisk0'};
+ delete $devinfo->{'drive-tpmstate0-backup'};
+
+ my $pbs_opts = {
+ repo => $repo,
+ keyfile => $keyfile,
+ snapshot => $pbs_backup_name,
+ namespace => $namespace,
+ };
+ pbs_live_restore($vmid, $conf, $storecfg, $devinfo, $pbs_opts);
+
+ PVE::QemuConfig->remove_lock($vmid, "create");
+ }
+}
+
+sub pbs_live_restore {
+ my ($vmid, $conf, $storecfg, $restored_disks, $opts) = @_;
+
+ print "starting VM for live-restore\n";
+ print "repository: '$opts->{repo}', snapshot: '$opts->{snapshot}'\n";
+
+ my $live_restore_backing = {};
+ for my $ds (keys %$restored_disks) {
+ $ds =~ m/^drive-(.*)$/;
+ my $confname = $1;
+ my $pbs_conf = {};
+ $pbs_conf = {
+ repository => $opts->{repo},
+ snapshot => $opts->{snapshot},
+ archive => "$ds.img.fidx",
+ };
+ $pbs_conf->{keyfile} = $opts->{keyfile} if -e $opts->{keyfile};
+ $pbs_conf->{namespace} = $opts->{namespace} if defined($opts->{namespace});
+
+ my $drive = parse_drive($confname, $conf->{$confname});
+ print "restoring '$ds' to '$drive->{file}'\n";
+
+ my $pbs_name = "drive-${confname}-pbs";
+ $live_restore_backing->{$confname} = {
+ name => $pbs_name,
+ blockdev => print_pbs_blockdev($pbs_conf, $pbs_name),
+ };
+ }
+
+ my $drives_streamed = 0;
+ eval {
+ # make sure HA doesn't interrupt our restore by stopping the VM
+ if (PVE::HA::Config::vm_is_ha_managed($vmid)) {
+ run_command(['ha-manager', 'set', "vm:$vmid", '--state', 'started']);
+ }
+
+ # start VM with backing chain pointing to PBS backup, environment vars for PBS driver
+ # in QEMU (PBS_PASSWORD and PBS_FINGERPRINT) are already set by our caller
+ vm_start_nolock($storecfg, $vmid, $conf, {paused => 1, 'live-restore-backing' => $live_restore_backing}, {});
+
+ my $qmeventd_fd = register_qmeventd_handle($vmid);
+
+ # begin streaming, i.e. data copy from PBS to target disk for every vol,
+ # this will effectively collapse the backing image chain consisting of
+ # [target <- alloc-track -> PBS snapshot] to just [target] (alloc-track
+ # removes itself once all backing images vanish with 'auto-remove=on')
+ my $jobs = {};
+ for my $ds (sort keys %$restored_disks) {
+ my $job_id = "restore-$ds";
+ mon_cmd($vmid, 'block-stream',
+ 'job-id' => $job_id,
+ device => "$ds",
+ );
+ $jobs->{$job_id} = {};
+ }
+
+ mon_cmd($vmid, 'cont');
+ qemu_drive_mirror_monitor($vmid, undef, $jobs, 'auto', 0, 'stream');
+
+ print "restore-drive jobs finished successfully, removing all tracking block devices"
+ ." to disconnect from Proxmox Backup Server\n";
+
+ for my $ds (sort keys %$restored_disks) {
+ mon_cmd($vmid, 'blockdev-del', 'node-name' => "$ds-pbs");
+ }
+
+ close($qmeventd_fd);
+ };
+
+ my $err = $@;
+
+ if ($err) {
+ warn "An error occurred during live-restore: $err\n";
+ _do_vm_stop($storecfg, $vmid, 1, 1, 10, 0, 1);
+ die "live-restore failed\n";
+ }
+}
+
+# Inspired by pbs live-restore, this restores with the disks being available as files.
+# Theoretically this can also be used to quick-start a full-clone vm if the
+# disks are all available as files.
+#
+# The mapping should provide a path by config entry, such as
+# `{ scsi0 => { format => <qcow2|raw|...>, path => "/path/to/file", sata1 => ... } }`
+#
+# This is used when doing a `create` call with the `--live-import` parameter,
+# where the disks get an `import-from=` property. The non-live part is
+# therefore already handled in the `$create_disks()` call happening in the
+# `create` api call
+sub live_import_from_files {
+ my ($mapping, $vmid, $conf, $restore_options) = @_;
+
+ my $live_restore_backing = {};
+ for my $dev (keys %$mapping) {
+ die "disk not support for live-restoring: '$dev'\n"
+ if !is_valid_drivename($dev) || $dev =~ /^(?:efidisk|tpmstate)/;
+
+ die "mapping contains disk '$dev' which does not exist in the config\n"
+ if !exists($conf->{$dev});
+
+ my $info = $mapping->{$dev};
+ my ($format, $path) = $info->@{qw(format path)};
+ die "missing path for '$dev' mapping\n" if !$path;
+ die "missing format for '$dev' mapping\n" if !$format;
+ die "invalid format '$format' for '$dev' mapping\n"
+ if !grep { $format eq $_ } qw(raw qcow2 vmdk);
+
+ $live_restore_backing->{$dev} = {
+ name => "drive-$dev-restore",
+ blockdev => "driver=$format,node-name=drive-$dev-restore"
+ . ",read-only=on"
+ . ",file.driver=file,file.filename=$path"
+ };
+ };
+
+ my $storecfg = PVE::Storage::config();
+ eval {
+
+ # make sure HA doesn't interrupt our restore by stopping the VM
+ if (PVE::HA::Config::vm_is_ha_managed($vmid)) {
+ run_command(['ha-manager', 'set', "vm:$vmid", '--state', 'started']);
+ }
+
+ vm_start_nolock($storecfg, $vmid, $conf, {paused => 1, 'live-restore-backing' => $live_restore_backing}, {});