# Note: $pool is only needed when creating a VM, because pool permissions
# are automatically inherited if VM already exists inside a pool.
-my $create_disks = sub {
- my ($rpcenv, $authuser, $conf, $arch, $storecfg, $vmid, $pool, $settings, $default_storage) = @_;
+my sub create_disks : prototype($$$$$$$$$$) {
+ my (
+ $rpcenv,
+ $authuser,
+ $conf,
+ $arch,
+ $storecfg,
+ $vmid,
+ $pool,
+ $settings,
+ $default_storage,
+ $is_live_import,
+ ) = @_;
my $vollist = [];
my $res = {};
+ my $live_import_mapping = {};
+
my $code = sub {
my ($ds, $disk) = @_;
my ($storeid, $size) = ($2 || $default_storage, $3);
die "no storage ID specified (and no default storage)\n" if !$storeid;
+ $size = PVE::Tools::convert_size($size, 'gb' => 'kb'); # vdisk_alloc uses kb
+
+ my $live_import = $is_live_import && $ds ne 'efidisk0';
+ my $needs_creation = 1;
+
if (my $source = delete $disk->{'import-from'}) {
my $dst_volid;
+ $needs_creation = $live_import;
+
if (PVE::Storage::parse_volume_id($source, 1)) { # PVE-managed volume
- my $dest_info = {
- vmid => $vmid,
- drivename => $ds,
- storage => $storeid,
- format => $disk->{format},
- };
+ if ($live_import && $ds ne 'efidisk0') {
+ my $path = PVE::Storage::path($storecfg, $source)
+ or die "failed to get a path for '$source'\n";
+ $source = $path;
+ ($size, my $source_format) = PVE::Storage::file_size_info($source);
+ die "could not get file size of $source\n" if !$size;
+ $live_import_mapping->{$ds} = {
+ path => $source,
+ format => $source_format,
+ };
+ } else {
+ my $dest_info = {
+ vmid => $vmid,
+ drivename => $ds,
+ storage => $storeid,
+ format => $disk->{format},
+ };
- $dest_info->{efisize} = PVE::QemuServer::get_efivars_size($conf, $disk)
- if $ds eq 'efidisk0';
+ $dest_info->{efisize} = PVE::QemuServer::get_efivars_size($conf, $disk)
+ if $ds eq 'efidisk0';
- ($dst_volid, $size) = eval {
- $import_from_volid->($storecfg, $source, $dest_info, $vollist);
- };
- die "cannot import from '$source' - $@" if $@;
+ ($dst_volid, $size) = eval {
+ $import_from_volid->($storecfg, $source, $dest_info, $vollist);
+ };
+ die "cannot import from '$source' - $@" if $@;
+ }
} else {
$source = PVE::Storage::abs_filesystem_path($storecfg, $source, 1);
$size = PVE::Storage::file_size_info($source);
push @$vollist, $dst_volid;
}
- $disk->{file} = $dst_volid;
- $disk->{size} = $size;
- delete $disk->{format}; # no longer needed
- $res->{$ds} = PVE::QemuServer::print_drive($disk);
- } else {
+ if ($needs_creation) {
+ $size = PVE::Tools::convert_size($size, 'b' => 'kb'); # vdisk_alloc uses kb
+ } else {
+ $disk->{file} = $dst_volid;
+ $disk->{size} = $size;
+ delete $disk->{format}; # no longer needed
+ $res->{$ds} = PVE::QemuServer::print_drive($disk);
+ }
+ }
+
+ if ($needs_creation) {
my $defformat = PVE::Storage::storage_default_format($storecfg, $storeid);
my $fmt = $disk->{format} || $defformat;
- $size = PVE::Tools::convert_size($size, 'gb' => 'kb'); # vdisk_alloc uses kb
-
my $volid;
if ($ds eq 'efidisk0') {
my $smm = PVE::QemuServer::Machine::machine_type_is_q35($conf);
die $err;
}
- return ($vollist, $res);
+ # don't return empty import mappings
+ $live_import_mapping = undef if !%$live_import_mapping;
+
+ return ($vollist, $res, $live_import_mapping);
};
my $check_cpu_model_access = sub {
return $res;
};
-
__PACKAGE__->register_method({
name => 'create_vm',
path => '',
'live-restore' => {
optional => 1,
type => 'boolean',
- description => "Start the VM immediately from the backup and restore in background. PBS only.",
- requires => 'archive',
+ description => "Start the VM immediately while importing or restoring in the background.",
},
pool => {
optional => 1,
};
my $createfn = sub {
+ my $live_import_mapping = {};
+
# ensure no old replication state are exists
PVE::ReplicationState::delete_guest_states($vmid);
my $conf = $param;
my $arch = PVE::QemuServer::get_vm_arch($conf);
-
for my $opt (sort keys $param->%*) {
next if $opt !~ m/^scsi\d+$/;
assert_scsi_feature_compatibility($opt, $conf, $storecfg, $param->{$opt});
my $vollist = [];
eval {
- ($vollist, my $created_opts) = $create_disks->(
+ ($vollist, my $created_opts, $live_import_mapping) = create_disks(
$rpcenv,
$authuser,
$conf,
$pool,
$param,
$storage,
+ $live_restore,
);
$conf->{$_} = $created_opts->{$_} for keys $created_opts->%*;
}
}
- PVE::QemuConfig->write_config($vmid, $conf);
+ $conf->{lock} = 'import' if $live_import_mapping;
+ PVE::QemuConfig->write_config($vmid, $conf);
};
my $err = $@;
PVE::QemuConfig->lock_config_full($vmid, 1, $realcmd);
- if ($start_after_create) {
+ if ($start_after_create && !$live_restore) {
print "Execute autostart\n";
eval { PVE::API2::Qemu->vm_start({vmid => $vmid, node => $node}) };
warn $@ if $@;
+ return;
+ } else {
+ return $live_import_mapping;
}
};
} else {
$worker_name = 'qmcreate';
$code = sub {
- eval { $createfn->() };
+ # If a live import was requested the create function returns
+ # the mapping for the startup.
+ my $live_import_mapping = eval { $createfn->() };
if (my $err = $@) {
eval {
my $conffile = PVE::QemuConfig->config_file($vmid);
warn $@ if $@;
die $err;
}
+
+ if ($live_import_mapping) {
+ my $import_options = {
+ bwlimit => $bwlimit,
+ live => 1,
+ };
+
+ my $conf = PVE::QemuConfig->load_config($vmid);
+ PVE::QemuServer::live_import_from_files(
+ $live_import_mapping,
+ $vmid,
+ $conf,
+ $import_options,
+ );
+ }
};
}
assert_scsi_feature_compatibility($opt, $conf, $storecfg, $param->{$opt})
if $opt =~ m/^scsi\d+$/;
- my (undef, $created_opts) = $create_disks->(
+ my (undef, $created_opts) = create_disks(
$rpcenv,
$authuser,
$conf,
$vmid,
undef,
{$opt => $param->{$opt}},
+ undef,
+ undef,
);
$conf->{pending}->{$_} = $created_opts->{$_} for keys $created_opts->%*;
ostype => {
optional => 1,
type => 'string',
+ # NOTE: When extending, also consider extending `%guest_types` in `Import/ESXi.pm`.
enum => [qw(other wxp w2k w2k3 w2k8 wvista win7 win8 win10 win11 l24 l26 solaris)],
description => "Specify guest operating system.",
verbose_description => <<EODESC,
}
}
+# 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) = @_;
+
+ die "only live-restore is implemented for restirng from files\n"
+ if !$restore_options->{live};
+
+ 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}, {});
+
+ # prevent shutdowns from qmeventd when the VM powers off from the inside
+ 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 %$live_restore_backing) {
+ my $job_id = "restore-$ds";
+ mon_cmd($vmid, 'block-stream',
+ 'job-id' => $job_id,
+ device => "drive-$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\n";
+
+ for my $ds (sort keys %$live_restore_backing) {
+ mon_cmd($vmid, 'blockdev-del', 'node-name' => "drive-$ds-restore");
+ }
+
+ 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";
+ }
+
+ PVE::QemuConfig->remove_lock($vmid, "import");
+}
+
sub restore_vma_archive {
my ($archive, $vmid, $user, $opts, $comp) = @_;
sub qemu_img_format {
my ($scfg, $volname) = @_;
- if ($scfg->{path} && $volname =~ m/\.($PVE::QemuServer::Drive::QEMU_FORMAT_RE)$/) {
+ # FIXME: this entire function is kind of weird given that `parse_volname`
+ # also already gives us a format?
+ my $is_path_storage = $scfg->{path} || $scfg->{type} eq 'esxi';
+
+ if ($is_path_storage && $volname =~ m/\.($PVE::QemuServer::Drive::QEMU_FORMAT_RE)$/) {
return $1;
} else {
return "raw";