diff --git a/PVE/API2/Qemu.pm b/PVE/API2/Qemu.pm index 40b6c302..6620f1d9 100644 --- a/PVE/API2/Qemu.pm +++ b/PVE/API2/Qemu.pm @@ -316,13 +316,26 @@ my $import_from_volid = sub { # 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) = @_; @@ -368,24 +381,43 @@ my $create_disks = sub { 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); @@ -404,16 +436,20 @@ my $create_disks = sub { 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); @@ -474,7 +510,10 @@ my $create_disks = sub { 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 { @@ -794,7 +833,6 @@ my $parse_restore_archive = sub { return $res; }; - __PACKAGE__->register_method({ name => 'create_vm', path => '', @@ -842,8 +880,7 @@ __PACKAGE__->register_method({ '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, @@ -1034,6 +1071,8 @@ __PACKAGE__->register_method({ }; my $createfn = sub { + my $live_import_mapping = {}; + # ensure no old replication state are exists PVE::ReplicationState::delete_guest_states($vmid); @@ -1041,7 +1080,6 @@ __PACKAGE__->register_method({ 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}); @@ -1051,7 +1089,7 @@ __PACKAGE__->register_method({ my $vollist = []; eval { - ($vollist, my $created_opts) = $create_disks->( + ($vollist, my $created_opts, $live_import_mapping) = create_disks( $rpcenv, $authuser, $conf, @@ -1061,6 +1099,7 @@ __PACKAGE__->register_method({ $pool, $param, $storage, + $live_restore, ); $conf->{$_} = $created_opts->{$_} for keys $created_opts->%*; @@ -1089,8 +1128,9 @@ __PACKAGE__->register_method({ } } - PVE::QemuConfig->write_config($vmid, $conf); + $conf->{lock} = 'import' if $live_import_mapping; + PVE::QemuConfig->write_config($vmid, $conf); }; my $err = $@; @@ -1109,10 +1149,13 @@ __PACKAGE__->register_method({ 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; } }; @@ -1137,7 +1180,9 @@ __PACKAGE__->register_method({ } 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); @@ -1146,6 +1191,21 @@ __PACKAGE__->register_method({ 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, + ); + } }; } @@ -1870,7 +1930,7 @@ my $update_vm_api = sub { 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, @@ -1879,6 +1939,8 @@ my $update_vm_api = sub { $vmid, undef, {$opt => $param->{$opt}}, + undef, + undef, ); $conf->{pending}->{$_} = $created_opts->{$_} for keys $created_opts->%*; diff --git a/PVE/QemuServer.pm b/PVE/QemuServer.pm index 6dff91c2..dc0f9c7a 100644 --- a/PVE/QemuServer.pm +++ b/PVE/QemuServer.pm @@ -412,6 +412,7 @@ my $confdesc = { 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 => < { format => , 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) = @_; @@ -7787,7 +7878,11 @@ sub qemu_img_convert { 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";