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,
$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;
}
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;
};
$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
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);
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
#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";
}
}
}
};
-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\#/;
} 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) = @_;
}
}
+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) = @_;
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
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);
$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";
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();
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";
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();
}
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;
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";
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;
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);
# 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++;
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;
$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;
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);
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);
}
}
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) = @_;