]> git.proxmox.com Git - qemu-server.git/commitdiff
support live-import for 'import-from' disk options on create
authorWolfgang Bumiller <w.bumiller@proxmox.com>
Wed, 14 Feb 2024 08:29:58 +0000 (09:29 +0100)
committerThomas Lamprecht <t.lamprecht@proxmox.com>
Wed, 13 Mar 2024 15:29:58 +0000 (16:29 +0100)
Signed-off-by: Wolfgang Bumiller <w.bumiller@proxmox.com>
PVE/API2/Qemu.pm
PVE/QemuServer.pm

index 40b6c3020982302b27d86f8d1ca462cad6315404..6620f1d9c17c1be4809d5ab2c6affc1d723f5906 100644 (file)
@@ -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->%*;
 
index 6dff91c267ad145d33984fe6868c2d300dd5b21e..dc0f9c7a33a8bd168a94c36a0c9801e3d9f211d2 100644 (file)
@@ -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 => <<EODESC,
@@ -7283,6 +7284,96 @@ sub pbs_live_restore {
     }
 }
 
+# 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) = @_;
 
@@ -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";