api: support VM disk import

Extend qm importdisk functionality to the API.

Co-authored-by: Fabian Grünbichler <f.gruenbichler@proxmox.com>
Co-authored-by: Dominic Jäger <d.jaeger@proxmox.com>
Signed-off-by: Fabian Ebner <f.ebner@proxmox.com>
This commit is contained in:
Dominic Jäger 2022-03-17 12:31:05 +01:00 committed by Fabian Grünbichler
parent c1accf9db9
commit e6ac9fed7b
3 changed files with 205 additions and 21 deletions

View File

@ -21,8 +21,9 @@ use PVE::ReplicationConfig;
use PVE::GuestHelpers;
use PVE::QemuConfig;
use PVE::QemuServer;
use PVE::QemuServer::Drive;
use PVE::QemuServer::CPUConfig;
use PVE::QemuServer::Drive;
use PVE::QemuServer::ImportDisk;
use PVE::QemuServer::Monitor qw(mon_cmd);
use PVE::QemuServer::Machine;
use PVE::QemuMigrate;
@ -88,6 +89,28 @@ my $check_drive_param = sub {
my $drive = PVE::QemuServer::parse_drive($opt, $param->{$opt}, 1);
raise_param_exc({ $opt => "unable to parse drive options" }) if !$drive;
if ($drive->{'import-from'}) {
if ($drive->{file} !~ $NEW_DISK_RE || $3 != 0) {
raise_param_exc({
$opt => "'import-from' requires special syntax - ".
"use <storage ID>:0,import-from=<source>",
});
}
if ($opt eq 'efidisk0') {
for my $required (qw(efitype pre-enrolled-keys)) {
if (!defined($drive->{$required})) {
raise_param_exc({
$opt => "need to specify '$required' when using 'import-from'",
});
}
}
} elsif ($opt eq 'tpmstate0') {
raise_param_exc({ $opt => "need to specify 'version' when using 'import-from'" })
if !defined($drive->{version});
}
}
PVE::QemuServer::cleanup_drive_path($opt, $storecfg, $drive);
$extra_checks->($drive) if $extra_checks;
@ -128,6 +151,21 @@ my $check_storage_access = sub {
'images',
);
}
if (my $src_image = $drive->{'import-from'}) {
my $src_vmid;
if (PVE::Storage::parse_volume_id($src_image, 1)) { # PVE-managed volume
(my $vtype, undef, $src_vmid) = PVE::Storage::parse_volname($storecfg, $src_image);
raise_param_exc({ $ds => "$src_image has wrong type '$vtype' - not an image" })
if $vtype ne 'images';
}
if ($src_vmid) { # might be actively used by VM and will be copied via clone_disk()
$rpcenv->check($authuser, "/vms/${src_vmid}", ['VM.Clone']);
} else {
PVE::Storage::check_volume_access($rpcenv, $authuser, $storecfg, $vmid, $src_image);
}
}
});
$rpcenv->check($authuser, "/storage/$settings->{vmstatestorage}", ['Datastore.AllocateSpace'])
@ -186,6 +224,91 @@ my $check_storage_access_migrate = sub {
if !$scfg->{content}->{images};
};
my $import_from_volid = sub {
my ($storecfg, $src_volid, $dest_info, $vollist) = @_;
die "could not get size of $src_volid\n"
if !PVE::Storage::volume_size_info($storecfg, $src_volid, 10);
die "cannot import from cloudinit disk\n"
if PVE::QemuServer::Drive::drive_is_cloudinit({ file => $src_volid });
my $src_vmid = (PVE::Storage::parse_volname($storecfg, $src_volid))[2];
my $src_vm_state = sub {
my $exists = $src_vmid && PVE::Cluster::get_vmlist()->{ids}->{$src_vmid} ? 1 : 0;
my $runs = 0;
if ($exists) {
eval { PVE::QemuConfig::assert_config_exists_on_node($src_vmid); };
die "owner VM $src_vmid not on local node\n" if $@;
$runs = PVE::QemuServer::Helpers::vm_running_locally($src_vmid) || 0;
}
return ($exists, $runs);
};
my ($src_vm_exists, $running) = $src_vm_state->();
die "cannot import from '$src_volid' - full clone feature is not supported\n"
if !PVE::Storage::volume_has_feature($storecfg, 'copy', $src_volid, undef, $running);
my $clonefn = sub {
my ($src_vm_exists_now, $running_now) = $src_vm_state->();
die "owner VM $src_vmid changed state unexpectedly\n"
if $src_vm_exists_now != $src_vm_exists || $running_now != $running;
my $src_conf = $src_vm_exists_now ? PVE::QemuConfig->load_config($src_vmid) : {};
my $src_drive = { file => $src_volid };
my $src_drivename;
PVE::QemuConfig->foreach_volume($src_conf, sub {
my ($ds, $drive) = @_;
return if $src_drivename;
if ($drive->{file} eq $src_volid) {
$src_drive = $drive;
$src_drivename = $ds;
}
});
my $source_info = {
vmid => $src_vmid,
running => $running_now,
drivename => $src_drivename,
drive => $src_drive,
snapname => undef,
};
my ($src_storeid) = PVE::Storage::parse_volume_id($src_volid);
return PVE::QemuServer::clone_disk(
$storecfg,
$source_info,
$dest_info,
1,
$vollist,
undef,
undef,
$src_conf->{agent},
PVE::Storage::get_bandwidth_limit('clone', [$src_storeid, $dest_info->{storage}]),
);
};
my $cloned;
if ($running) {
$cloned = PVE::QemuConfig->lock_config_full($src_vmid, 30, $clonefn);
} elsif ($src_vmid) {
$cloned = PVE::QemuConfig->lock_config_shared($src_vmid, 30, $clonefn);
} else {
$cloned = $clonefn->();
}
return $cloned->@{qw(file size)};
};
# 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 {
@ -229,28 +352,71 @@ my $create_disks = sub {
} elsif ($volid =~ $NEW_DISK_RE) {
my ($storeid, $size) = ($2 || $default_storage, $3);
die "no storage ID specified (and no default storage)\n" if !$storeid;
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
if (my $source = delete $disk->{'import-from'}) {
my $dst_volid;
my $volid;
if ($ds eq 'efidisk0') {
my $smm = PVE::QemuServer::Machine::machine_type_is_q35($conf);
($volid, $size) = PVE::QemuServer::create_efidisk(
$storecfg, $storeid, $vmid, $fmt, $arch, $disk, $smm);
} elsif ($ds eq 'tpmstate0') {
# swtpm can only use raw volumes, and uses a fixed size
$size = PVE::Tools::convert_size(PVE::QemuServer::Drive::TPMSTATE_DISK_SIZE, 'b' => 'kb');
$volid = PVE::Storage::vdisk_alloc($storecfg, $storeid, $vmid, "raw", undef, $size);
if (PVE::Storage::parse_volume_id($source, 1)) { # PVE-managed volume
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';
($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);
die "could not get file size of $source\n" if !$size;
(undef, $dst_volid) = PVE::QemuServer::ImportDisk::do_import(
$source,
$vmid,
$storeid,
{
drive_name => $ds,
format => $disk->{format},
'skip-config-update' => 1,
},
);
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 {
$volid = PVE::Storage::vdisk_alloc($storecfg, $storeid, $vmid, $fmt, undef, $size);
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);
($volid, $size) = PVE::QemuServer::create_efidisk(
$storecfg, $storeid, $vmid, $fmt, $arch, $disk, $smm);
} elsif ($ds eq 'tpmstate0') {
# swtpm can only use raw volumes, and uses a fixed size
$size = PVE::Tools::convert_size(PVE::QemuServer::Drive::TPMSTATE_DISK_SIZE, 'b' => 'kb');
$volid = PVE::Storage::vdisk_alloc($storecfg, $storeid, $vmid, "raw", undef, $size);
} else {
$volid = PVE::Storage::vdisk_alloc($storecfg, $storeid, $vmid, $fmt, undef, $size);
}
push @$vollist, $volid;
$disk->{file} = $volid;
$disk->{size} = PVE::Tools::convert_size($size, 'kb' => 'b');
delete $disk->{format}; # no longer needed
$res->{$ds} = PVE::QemuServer::print_drive($disk);
}
push @$vollist, $volid;
$disk->{file} = $volid;
$disk->{size} = PVE::Tools::convert_size($size, 'kb' => 'b');
delete $disk->{format}; # no longer needed
$res->{$ds} = PVE::QemuServer::print_drive($disk);
} else {
PVE::Storage::check_volume_access(
$rpcenv,

View File

@ -409,8 +409,21 @@ my $alldrive_fmt = {
%efitype_fmt,
};
my %import_from_fmt = (
'import-from' => {
type => 'string',
format => 'pve-volume-id-or-absolute-path',
format_description => 'source volume',
description => "Create a new disk, importing from this source (volume ID or absolute ".
"path). When an absolute path is specified, it's up to you to ensure that the source ".
"is not actively used by another process during the import!",
optional => 1,
},
);
my $alldrive_fmt_with_alloc = {
%$alldrive_fmt,
%import_from_fmt,
};
my $unused_fmt = {
@ -440,6 +453,8 @@ my $desc_with_alloc = sub {
my $new_desc = dclone($desc);
$new_desc->{format}->{'import-from'} = $import_from_fmt{'import-from'};
my $extra_note = '';
if ($type eq 'efidisk') {
$extra_note = " Note that SIZE_IN_GiB is ignored here and that the default EFI vars are ".
@ -449,7 +464,8 @@ my $desc_with_alloc = sub {
}
$new_desc->{description} .= " Use the special syntax STORAGE_ID:SIZE_IN_GiB to allocate a new ".
"volume.${extra_note}";
"volume.${extra_note} Use STORAGE_ID:0 and the 'import-from' parameter to import from an ".
"existing volume.";
$with_alloc_desc_cache->{$type} = $new_desc;

View File

@ -11,6 +11,8 @@ use PVE::Tools qw(run_command extract_param);
# and creates by default a drive entry unused[n] pointing to the created volume
# $params->{drive_name} may be used to specify ide0, scsi1, etc ...
# $params->{format} may be used to specify qcow2, raw, etc ...
# $params->{skiplock} may be used to skip checking for a lock in the VM config
# $params->{'skip-config-update'} may be used to import the disk without updating the VM config
sub do_import {
my ($src_path, $vmid, $storage_id, $params) = @_;
@ -71,7 +73,7 @@ sub do_import {
PVE::Storage::activate_volumes($storecfg, [$dst_volid]);
PVE::QemuServer::qemu_img_convert($src_path, $dst_volid, $src_size, undef, $zeroinit);
PVE::Storage::deactivate_volumes($storecfg, [$dst_volid]);
PVE::QemuConfig->lock_config($vmid, $create_drive);
PVE::QemuConfig->lock_config($vmid, $create_drive) if !$params->{'skip-config-update'};
};
if (my $err = $@) {
eval { PVE::Storage::vdisk_free($storecfg, $dst_volid) };