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
|
# 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->%*;
|
||||||
|
|
||||||
|
@ -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";
|
||||||
|
Loading…
Reference in New Issue
Block a user