support live-import for 'import-from' disk options on create

Signed-off-by: Wolfgang Bumiller <w.bumiller@proxmox.com>
This commit is contained in:
Wolfgang Bumiller 2024-02-14 09:29:58 +01:00 committed by Thomas Lamprecht
parent 5b8d01f575
commit eb06e48657
2 changed files with 189 additions and 32 deletions

View File

@ -316,13 +316,26 @@ my $import_from_volid = sub {
# Note: $pool is only needed when creating a VM, because pool permissions # Note: $pool is only needed when creating a VM, because pool permissions
# are automatically inherited if VM already exists inside a pool. # are automatically inherited if VM already exists inside a pool.
my $create_disks = sub { my sub create_disks : prototype($$$$$$$$$$) {
my ($rpcenv, $authuser, $conf, $arch, $storecfg, $vmid, $pool, $settings, $default_storage) = @_; my (
$rpcenv,
$authuser,
$conf,
$arch,
$storecfg,
$vmid,
$pool,
$settings,
$default_storage,
$is_live_import,
) = @_;
my $vollist = []; my $vollist = [];
my $res = {}; my $res = {};
my $live_import_mapping = {};
my $code = sub { my $code = sub {
my ($ds, $disk) = @_; my ($ds, $disk) = @_;
@ -368,24 +381,43 @@ my $create_disks = sub {
my ($storeid, $size) = ($2 || $default_storage, $3); my ($storeid, $size) = ($2 || $default_storage, $3);
die "no storage ID specified (and no default storage)\n" if !$storeid; 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'}) { if (my $source = delete $disk->{'import-from'}) {
my $dst_volid; my $dst_volid;
$needs_creation = $live_import;
if (PVE::Storage::parse_volume_id($source, 1)) { # PVE-managed volume if (PVE::Storage::parse_volume_id($source, 1)) { # PVE-managed volume
my $dest_info = { if ($live_import && $ds ne 'efidisk0') {
vmid => $vmid, my $path = PVE::Storage::path($storecfg, $source)
drivename => $ds, or die "failed to get a path for '$source'\n";
storage => $storeid, $source = $path;
format => $disk->{format}, ($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) $dest_info->{efisize} = PVE::QemuServer::get_efivars_size($conf, $disk)
if $ds eq 'efidisk0'; if $ds eq 'efidisk0';
($dst_volid, $size) = eval { ($dst_volid, $size) = eval {
$import_from_volid->($storecfg, $source, $dest_info, $vollist); $import_from_volid->($storecfg, $source, $dest_info, $vollist);
}; };
die "cannot import from '$source' - $@" if $@; die "cannot import from '$source' - $@" if $@;
}
} else { } else {
$source = PVE::Storage::abs_filesystem_path($storecfg, $source, 1); $source = PVE::Storage::abs_filesystem_path($storecfg, $source, 1);
$size = PVE::Storage::file_size_info($source); $size = PVE::Storage::file_size_info($source);
@ -404,16 +436,20 @@ my $create_disks = sub {
push @$vollist, $dst_volid; push @$vollist, $dst_volid;
} }
$disk->{file} = $dst_volid; if ($needs_creation) {
$disk->{size} = $size; $size = PVE::Tools::convert_size($size, 'b' => 'kb'); # vdisk_alloc uses kb
delete $disk->{format}; # no longer needed } else {
$res->{$ds} = PVE::QemuServer::print_drive($disk); $disk->{file} = $dst_volid;
} else { $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 $defformat = PVE::Storage::storage_default_format($storecfg, $storeid);
my $fmt = $disk->{format} || $defformat; my $fmt = $disk->{format} || $defformat;
$size = PVE::Tools::convert_size($size, 'gb' => 'kb'); # vdisk_alloc uses kb
my $volid; my $volid;
if ($ds eq 'efidisk0') { if ($ds eq 'efidisk0') {
my $smm = PVE::QemuServer::Machine::machine_type_is_q35($conf); my $smm = PVE::QemuServer::Machine::machine_type_is_q35($conf);
@ -474,7 +510,10 @@ my $create_disks = sub {
die $err; 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 { my $check_cpu_model_access = sub {
@ -794,7 +833,6 @@ my $parse_restore_archive = sub {
return $res; return $res;
}; };
__PACKAGE__->register_method({ __PACKAGE__->register_method({
name => 'create_vm', name => 'create_vm',
path => '', path => '',
@ -842,8 +880,7 @@ __PACKAGE__->register_method({
'live-restore' => { 'live-restore' => {
optional => 1, optional => 1,
type => 'boolean', type => 'boolean',
description => "Start the VM immediately from the backup and restore in background. PBS only.", description => "Start the VM immediately while importing or restoring in the background.",
requires => 'archive',
}, },
pool => { pool => {
optional => 1, optional => 1,
@ -1034,6 +1071,8 @@ __PACKAGE__->register_method({
}; };
my $createfn = sub { my $createfn = sub {
my $live_import_mapping = {};
# ensure no old replication state are exists # ensure no old replication state are exists
PVE::ReplicationState::delete_guest_states($vmid); PVE::ReplicationState::delete_guest_states($vmid);
@ -1041,7 +1080,6 @@ __PACKAGE__->register_method({
my $conf = $param; my $conf = $param;
my $arch = PVE::QemuServer::get_vm_arch($conf); my $arch = PVE::QemuServer::get_vm_arch($conf);
for my $opt (sort keys $param->%*) { for my $opt (sort keys $param->%*) {
next if $opt !~ m/^scsi\d+$/; next if $opt !~ m/^scsi\d+$/;
assert_scsi_feature_compatibility($opt, $conf, $storecfg, $param->{$opt}); assert_scsi_feature_compatibility($opt, $conf, $storecfg, $param->{$opt});
@ -1051,7 +1089,7 @@ __PACKAGE__->register_method({
my $vollist = []; my $vollist = [];
eval { eval {
($vollist, my $created_opts) = $create_disks->( ($vollist, my $created_opts, $live_import_mapping) = create_disks(
$rpcenv, $rpcenv,
$authuser, $authuser,
$conf, $conf,
@ -1061,6 +1099,7 @@ __PACKAGE__->register_method({
$pool, $pool,
$param, $param,
$storage, $storage,
$live_restore,
); );
$conf->{$_} = $created_opts->{$_} for keys $created_opts->%*; $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 = $@; my $err = $@;
@ -1109,10 +1149,13 @@ __PACKAGE__->register_method({
PVE::QemuConfig->lock_config_full($vmid, 1, $realcmd); PVE::QemuConfig->lock_config_full($vmid, 1, $realcmd);
if ($start_after_create) { if ($start_after_create && !$live_restore) {
print "Execute autostart\n"; print "Execute autostart\n";
eval { PVE::API2::Qemu->vm_start({vmid => $vmid, node => $node}) }; eval { PVE::API2::Qemu->vm_start({vmid => $vmid, node => $node}) };
warn $@ if $@; warn $@ if $@;
return;
} else {
return $live_import_mapping;
} }
}; };
@ -1137,7 +1180,9 @@ __PACKAGE__->register_method({
} else { } else {
$worker_name = 'qmcreate'; $worker_name = 'qmcreate';
$code = sub { $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 = $@) { if (my $err = $@) {
eval { eval {
my $conffile = PVE::QemuConfig->config_file($vmid); my $conffile = PVE::QemuConfig->config_file($vmid);
@ -1146,6 +1191,21 @@ __PACKAGE__->register_method({
warn $@ if $@; warn $@ if $@;
die $err; 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}) assert_scsi_feature_compatibility($opt, $conf, $storecfg, $param->{$opt})
if $opt =~ m/^scsi\d+$/; if $opt =~ m/^scsi\d+$/;
my (undef, $created_opts) = $create_disks->( my (undef, $created_opts) = create_disks(
$rpcenv, $rpcenv,
$authuser, $authuser,
$conf, $conf,
@ -1879,6 +1939,8 @@ my $update_vm_api = sub {
$vmid, $vmid,
undef, undef,
{$opt => $param->{$opt}}, {$opt => $param->{$opt}},
undef,
undef,
); );
$conf->{pending}->{$_} = $created_opts->{$_} for keys $created_opts->%*; $conf->{pending}->{$_} = $created_opts->{$_} for keys $created_opts->%*;

View File

@ -412,6 +412,7 @@ my $confdesc = {
ostype => { ostype => {
optional => 1, optional => 1,
type => 'string', 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)], enum => [qw(other wxp w2k w2k3 w2k8 wvista win7 win8 win10 win11 l24 l26 solaris)],
description => "Specify guest operating system.", description => "Specify guest operating system.",
verbose_description => <<EODESC, 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 { sub restore_vma_archive {
my ($archive, $vmid, $user, $opts, $comp) = @_; my ($archive, $vmid, $user, $opts, $comp) = @_;
@ -7787,7 +7878,11 @@ sub qemu_img_convert {
sub qemu_img_format { sub qemu_img_format {
my ($scfg, $volname) = @_; 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; return $1;
} else { } else {
return "raw"; return "raw";