mirror of
https://git.proxmox.com/git/qemu-server
synced 2025-07-05 21:13:22 +00:00
support live-import for 'import-from' disk options on create
Signed-off-by: Wolfgang Bumiller <w.bumiller@proxmox.com>
This commit is contained in:
parent
5b8d01f575
commit
eb06e48657
124
PVE/API2/Qemu.pm
124
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->%*;
|
||||
|
||||
|
@ -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";
|
||||
|
Loading…
Reference in New Issue
Block a user