[pve-devel] [PATCH v5 qemu-server] Add API for import wizards
Dominic Jäger
d.jaeger at proxmox.com
Fri Mar 5 12:11:38 CET 2021
Extend qm importdisk/importovf functionality to the API.
Signed-off-by: Dominic Jäger <d.jaeger at proxmox.com>
---
v4->v5: Feedback by Fabian Grünbichler
Use more existing helpers, parse more, change signature of import helper
function, more detailed errors esp. for API, ...
Remaining todo: Some error cases still require handling & double check if I've
missed something
PVE/API2/Qemu.pm | 435 +++++++++++++++++++++++++++++++++++++++++-
PVE/QemuServer.pm | 16 +-
PVE/QemuServer/OVF.pm | 10 +-
3 files changed, 454 insertions(+), 7 deletions(-)
diff --git a/PVE/API2/Qemu.pm b/PVE/API2/Qemu.pm
index feb9ea8..2efbca9 100644
--- a/PVE/API2/Qemu.pm
+++ b/PVE/API2/Qemu.pm
@@ -45,7 +45,6 @@ BEGIN {
}
}
-use Data::Dumper; # fixme: remove
use base qw(PVE::RESTHandler);
@@ -4330,4 +4329,438 @@ __PACKAGE__->register_method({
return PVE::QemuServer::Cloudinit::dump_cloudinit_config($conf, $param->{vmid}, $param->{type});
}});
+# Raise exception if $format is not supported by $storeid
+my $check_format_is_supported = sub {
+ my ($format, $storeid, $storecfg) = @_;
+ die "You have to provide storage ID" if !$storeid;
+ die "You have to provide the storage configurration" if !$storecfg;
+
+ return if !$format;
+
+ my (undef, $valid_formats) = PVE::Storage::storage_default_format($storecfg, $storeid);
+ my $supported = grep { $_ eq $format } @$valid_formats;
+
+ die "$format is not supported on storage $storeid" if !$supported;
+};
+
+# storecfg ... PVE::Storage::config()
+# vmid ... target VM ID
+# vmconf ... target VM configuration
+# source ... source image (volid or absolute path)
+# target ... hash with
+# storeid => storage ID
+# format => disk format (optional)
+# options => string with device options (may or may not contain <storeid>:0)
+# device => device where the disk is attached (for example, scsi3) (optional)
+#
+# returns ... volid of the allocated disk image (e.g. local-lvm:vm-100-disk-2)
+my $import_disk_image = sub {
+ my ($param) = @_;
+ my $storecfg = $param->{storecfg};
+ my $vmid = $param->{vmid};
+ my $vmconf = $param->{vmconf};
+ my $target = $param->{target};
+ my $requested_format = $target->{format};
+ my $storeid = $target->{storeid};
+
+ die "Source parameter is undefined!" if !defined $param->{source};
+ my $source = PVE::Storage::abs_filesystem_path($storecfg, $param->{source}, 1);
+
+ eval { PVE::Storage::storage_config($storecfg, $storeid) };
+ die "Error while importing disk image $source: $@\n" if $@;
+
+ my $src_size = PVE::Storage::file_size_info($source);
+ # Previous abs_filesystem_path performs additional checks
+ die "Could not get file size of $source" if !defined($src_size);
+
+ $check_format_is_supported->($requested_format, $storeid, $storecfg);
+
+ my $dst_format = PVE::QemuServer::resolve_dst_disk_format(
+ $storecfg, $storeid, undef, $requested_format);
+ my $dst_volid = PVE::Storage::vdisk_alloc($storecfg, $storeid,
+ $vmid, $dst_format, undef, $src_size / 1024);
+
+ print "Importing disk image '$source'...\n";
+ eval {
+ local $SIG{INT} =
+ local $SIG{TERM} =
+ local $SIG{QUIT} =
+ local $SIG{HUP} =
+ local $SIG{PIPE} = sub { die "Interrupted by signal $!\n"; };
+
+ my $zeroinit = PVE::Storage::volume_has_feature($storecfg,
+ 'sparseinit', $dst_volid);
+
+ PVE::Storage::activate_volumes($storecfg, [$dst_volid]);
+ PVE::QemuServer::qemu_img_convert($source, $dst_volid,
+ $src_size, undef, $zeroinit);
+ PVE::Storage::deactivate_volumes($storecfg, [$dst_volid]);
+
+ };
+ if (my $err = $@) {
+ eval { PVE::Storage::vdisk_free($storecfg, $dst_volid) };
+ warn "Cleanup of $dst_volid failed: $@ \n" if $@;
+
+ die "Importing disk '$source' failed: $err\n" if $err;
+ }
+
+ my $drive = $dst_volid;
+ if ($target->{device}) {
+ # Attach to target device with options if they are specified
+ if (defined $target->{options}) {
+ # Options string with or without storeid is allowed
+ # => Avoid potential duplicate storeid for update
+ $target->{options} =~ s/$storeid:0,//;
+ $drive .= ",$target->{options}" ;
+ }
+ } else {
+ $target->{device} = PVE::QemuConfig->add_unused_volume($vmconf, $dst_volid);
+ }
+ print "Imported '$source' to $dst_volid\n";
+ $update_vm_api->(
+ {
+ node => $target->{node},
+ vmid => $vmid,
+ $target->{device} => $drive,
+ skiplock => 1,
+ },
+ 1,
+ );
+
+ return $dst_volid;
+};
+
+__PACKAGE__->register_method ({
+ name => 'importdisk',
+ path => '{vmid}/importdisk',
+ method => 'POST',
+ proxyto => 'node',
+ protected => 1,
+ description => "Import an external disk image into a VM. The image format ".
+ "has to be supported by qemu-img.",
+ parameters => {
+ additionalProperties => 0,
+ properties => {
+ node => get_standard_option('pve-node'),
+ vmid => get_standard_option('pve-vmid',
+ {completion => \&PVE::QemuServer::complete_vmid}),
+ source => {
+ description => "Disk image to import. Can be a volid ".
+ "(local:99/imageToImport.raw) or an absolute path on the server.",
+ type => 'string',
+ format => 'pve-volume-id-or-absolute-path',
+ },
+ device => {
+ type => 'string',
+ description => "Bus/Device type of the new disk (e.g. 'ide0', ".
+ "'scsi2'). Will add the image as unused disk if omitted.",
+ enum => [PVE::QemuServer::Drive::valid_drive_names()],
+ optional => 1,
+ },
+ device_options => {
+ type => 'string',
+ description => "Options to set for the new disk (e.g. 'discard=on,backup=0')",
+ optional => 1,
+ requires => 'device',
+ },
+ storage => get_standard_option('pve-storage-id', {
+ description => "The storage to which the image will be imported to.",
+ completion => \&PVE::QemuServer::complete_storage,
+ }),
+ format => {
+ type => 'string',
+ description => 'Target format.',
+ enum => [ 'raw', 'qcow2', 'vmdk' ],
+ optional => 1,
+ },
+ digest => get_standard_option('pve-config-digest'),
+ },
+ },
+ returns => { type => 'string'},
+ code => sub {
+ my ($param) = @_;
+ my $vmid = extract_param($param, 'vmid');
+ my $node = extract_param($param, 'node');
+ my $source = extract_param($param, 'source');
+ my $digest_param = extract_param($param, 'digest');
+ my $device_options = extract_param($param, 'device_options');
+ my $device = extract_param($param, 'device');
+ my $storeid = extract_param($param, 'storage');
+
+ my $rpcenv = PVE::RPCEnvironment::get();
+ my $authuser = $rpcenv->get_user();
+ my $storecfg = PVE::Storage::config();
+ PVE::Storage::storage_config($storecfg, $storeid); # check for errors
+
+ # Format can be set explicitly "--format vmdk"
+ # or as part of device options "--device_options discard=on,format=vmdk"
+ # or not at all, but not both together
+ my $device_options_format;
+ if ($device_options) {
+ # parse device_options string according to disk schema for
+ # validation and to make usage easier
+
+ # any existing storage ID is OK to get a valid (fake) string for parse_drive
+ my $valid_string = "$storeid:0,$device_options";
+
+ # member "file" is fake
+ my $drive_full = PVE::QemuServer::Drive::parse_drive($device, $valid_string);
+ $device_options_format = $drive_full->{format};
+ }
+
+ my $format = extract_param($param, 'format'); # may be undefined
+ if ($device_options) {
+ if ($format && $device_options_format) {
+ raise_param_exc({format => "Format already specified in device_options!"});
+ } else {
+ $format = $format || $device_options_format; # may still be undefined
+ }
+ }
+
+ $check_format_is_supported->($format, $storeid, $storecfg);
+
+ # provide a useful error (in the API response) before forking
+ my $no_lock_conf = PVE::QemuConfig->load_config($vmid);
+ PVE::QemuConfig->check_lock($no_lock_conf);
+ PVE::Tools::assert_if_modified($no_lock_conf->{digest}, $digest_param);
+ if ($device && $no_lock_conf->{$device}) {
+ die "Could not import because device $device is already in ".
+ "use in VM $vmid. Choose a different device!";
+ }
+
+ my $worker = sub {
+ my $conf;
+ PVE::QemuConfig->lock_config($vmid, sub {
+ $conf = PVE::QemuConfig->load_config($vmid);
+ PVE::QemuConfig->check_lock($conf);
+
+ # Our device-in-use check may be invalid if the new conf is different
+ PVE::Tools::assert_if_modified($conf->{digest}, $no_lock_conf->{digest});
+
+ PVE::QemuConfig->set_lock($vmid, 'import');
+ });
+
+ my $target = {
+ node => $node,
+ storeid => $storeid,
+ };
+ # Avoid keys with undef values
+ $target->{format} = $format if defined $format;
+ $target->{device} = $device if defined $device;
+ $target->{options} = $device_options if defined $device_options;
+ $import_disk_image->({
+ storecfg => $storecfg,
+ vmid => $vmid,
+ vmconf => $conf,
+ source => $source,
+ target => $target,
+ });
+
+ PVE::QemuConfig->remove_lock($vmid, 'import');
+ };
+ return $rpcenv->fork_worker('importdisk', $vmid, $authuser, $worker);
+ }});
+
+__PACKAGE__->register_method({
+ name => 'importvm',
+ path => '{vmid}/importvm',
+ method => 'POST',
+ description => "Import a VM from existing disk images.",
+ protected => 1,
+ proxyto => 'node',
+ parameters => {
+ additionalProperties => 0,
+ properties => PVE::QemuServer::json_config_properties(
+ {
+ node => get_standard_option('pve-node'),
+ vmid => get_standard_option('pve-vmid', { completion =>
+ \&PVE::Cluster::complete_next_vmid }),
+ diskimages => {
+ description => "Mapping of devices to disk images. For " .
+ "example, scsi0=/mnt/nfs/image1.vmdk,scsi1=/mnt/nfs/image2",
+ type => 'string',
+ },
+ start => {
+ optional => 1,
+ type => 'boolean',
+ default => 0,
+ description => "Start VM after it was imported successfully.",
+ },
+ }),
+ },
+ returns => {
+ type => 'string',
+ },
+ code => sub {
+ my ($param) = @_;
+ my $node = extract_param($param, 'node');
+ my $vmid = extract_param($param, 'vmid');
+ my $diskimages_string = extract_param($param, 'diskimages');
+
+ my $rpcenv = PVE::RPCEnvironment::get();
+ my $authuser = $rpcenv->get_user();
+ my $storecfg = PVE::Storage::config();
+
+ PVE::Cluster::check_cfs_quorum();
+
+ # Return true iff $opt is to be imported, that means it is a device
+ # like scsi2 and the special import syntax/zero is specified for it,
+ # for example local-lvm:0 but not local-lvm:5
+ my $is_import = sub {
+ my ($opt) = @_;
+ return 0 if $opt eq 'efidisk0';
+ if (PVE::QemuServer::Drive::is_valid_drivename($opt)) {
+ my $drive = PVE::QemuServer::parse_drive($opt, $param->{$opt});
+ return $drive->{file} =~ m/0$/;
+ }
+ return 0;
+ };
+
+ # bus/device like ide0, scsi5 where the imported disk images get attached
+ my $target_devices = [];
+ # List of VM parameters like memory, cpu type, but also disks that are newly created
+ my $vm_params = [];
+ foreach my $opt (keys %$param) {
+ next if ($opt eq 'start'); # does not belong in config
+ my $list = $is_import->($opt) ? $target_devices : $vm_params;
+ push @$list, $opt;
+ }
+
+ my $diskimages = {};
+ foreach (split ',', $diskimages_string) {
+ my ($device, $diskimage) = split('=', $_);
+ $diskimages->{$device} = $diskimage;
+ if (defined $param->{$device}) {
+ my $drive = PVE::QemuServer::parse_drive($device, $param->{$device});
+ $drive->{file} =~ m/(\d)$/;
+ if ($1 != 0) {
+ raise_param_exc({
+ $device => "Each entry of --diskimages must have a ".
+ "corresponding device with special import syntax " .
+ "(e.g. --scsi3 local-lvm:0). $device from " .
+ "--diskimages has a matching device but the size " .
+ "of that is $1 instead of 0!",
+ });
+ }
+ } else {
+ # It is possible to also create new empty disk images during
+ # import by adding something like scsi2=local:10, therefore
+ # vice-versa check is not required
+ raise_param_exc({
+ diskimages => "There must be a matching device for each " .
+ "--diskimages entry that should be imported, but " .
+ "there is no matching device for $device!\n" .
+ " For example, for --diskimages scsi0=/source/path,scsi1=/other/path " .
+ "there must be --scsi0 local-lvm:0,discard=on --scsi1 local:0,cache=unsafe",
+ });
+ }
+ # Dies if $diskimage cannot be found
+ PVE::Storage::abs_filesystem_path($storecfg, $diskimage, 1);
+ }
+ foreach my $device (@$target_devices) {
+ my $drive = PVE::QemuServer::parse_drive($device, $param->{$device});
+ if ($drive->{file} =~ m/0$/) {
+ if (!defined $diskimages->{$device}) {
+ raise_param_exc({
+ $device => "Each device with the special import " .
+ "syntax (the 0) must have a corresponding in " .
+ "--diskimages that specifies the source of the " .
+ "import, but there is no such entry for $device!",
+ });
+ }
+ }
+ }
+
+ eval { PVE::QemuConfig->create_and_lock_config($vmid, 0, 'import') };
+ die "Unable to create config for VM import: $@" if $@;
+
+ my $worker = sub {
+ my $get_conf = sub {
+ my ($vmid) = @_;
+ PVE::QemuConfig->lock_config($vmid, sub {
+ my $conf = PVE::QemuConfig->load_config($vmid);
+ if (PVE::QemuConfig->has_lock($conf, 'import')) {
+ return $conf;
+ } else {
+ die "import lock in VM $vmid config file missing!";
+ }
+ });
+ };
+
+ $get_conf->($vmid); # quick check for lock
+
+ my $short_update = {
+ node => $node,
+ vmid => $vmid,
+ skiplock => 1,
+ };
+ foreach ( @$vm_params ) {
+ $short_update->{$_} = $param->{$_};
+ }
+ $update_vm_api->($short_update, 1); # writes directly in config file
+
+ my $conf = $get_conf->($vmid);
+
+ # When all short updates were succesfull, then the long imports
+ my @imported_successfully = ();
+ eval { foreach my $device (@$target_devices) {
+ my $param_parsed = PVE::QemuServer::parse_drive($device, $param->{$device});
+ die "Parsing $param->{$device} failed" if !$param_parsed;
+
+ my $imported = $import_disk_image->({
+ storecfg => $storecfg,
+ vmid => $vmid,
+ vmconf => $conf,
+ source => $diskimages->{$device},
+ target => {
+ storeid => (split ':', $param_parsed->{file})[0],
+ format => $param_parsed->{format},
+ options => $param->{$device},
+ device => $device,
+ },
+ });
+ push @imported_successfully, $imported;
+ }};
+ my $err = $@;
+ if ($err) {
+ foreach my $volid (@imported_successfully) {
+ eval { PVE::Storage::vdisk_free($storecfg, $volid) };
+ warn $@ if $@;
+ }
+
+ eval {
+ my $conffile = PVE::QemuConfig->config_file($vmid);
+ unlink($conffile) or die "Failed to remove config file: $!";
+ };
+ warn $@ if $@;
+
+ die "Import aborted: $err";
+ } else {
+ $conf = $get_conf->($vmid); # import_disk_image changed config file directly
+ if (!$conf->{boot}) {
+ my $bootdevs = PVE::QemuServer::get_default_bootdevices($conf);
+ $update_vm_api->(
+ {
+ node => $node,
+ vmid => $vmid,
+ boot => PVE::QemuServer::print_bootorder($bootdevs),
+ skiplock => 1,
+ },
+ 1,
+ );
+ }
+
+ eval { PVE::QemuConfig->remove_lock($vmid, 'import') };
+ warn $@ if $@;
+
+ if ($param->{start}) {
+ PVE::QemuServer::vm_start($storecfg, $vmid);
+ }
+ }
+ };
+
+ return $rpcenv->fork_worker('importvm', $vmid, $authuser, $worker);
+ }});
+
+
1;
diff --git a/PVE/QemuServer.pm b/PVE/QemuServer.pm
index 4a433a5..3262a0c 100644
--- a/PVE/QemuServer.pm
+++ b/PVE/QemuServer.pm
@@ -300,7 +300,7 @@ my $confdesc = {
optional => 1,
type => 'string',
description => "Lock/unlock the VM.",
- enum => [qw(backup clone create migrate rollback snapshot snapshot-delete suspending suspended)],
+ enum => [qw(backup clone create migrate rollback snapshot snapshot-delete suspending suspended import)],
},
cpulimit => {
optional => 1,
@@ -998,6 +998,18 @@ sub verify_volume_id_or_qm_path {
return $volid;
}
+PVE::JSONSchema::register_format('pve-volume-id-or-absolute-path', \&verify_volume_id_or_absolute_path);
+sub verify_volume_id_or_absolute_path {
+ my ($volid, $noerr) = @_;
+
+ # Exactly these 2 are allowed in id_or_qm_path but should not be allowed here
+ if ($volid eq 'none' || $volid eq 'cdrom') {
+ return undef if $noerr;
+ die "Invalid format! Should be volume ID or absolute path.";
+ }
+ return verify_volume_id_or_qm_path($volid, $noerr);
+}
+
my $usb_fmt = {
host => {
default_key => 1,
@@ -6700,7 +6712,7 @@ sub qemu_img_convert {
$src_path = PVE::Storage::path($storecfg, $src_volid, $snapname);
$src_is_iscsi = ($src_path =~ m|^iscsi://|);
$cachemode = 'none' if $src_scfg->{type} eq 'zfspool';
- } elsif (-f $src_volid) {
+ } elsif (-f $src_volid || -b _) { # -b required to import from LVM images
$src_path = $src_volid;
if ($src_path =~ m/\.($PVE::QemuServer::Drive::QEMU_FORMAT_RE)$/) {
$src_format = $1;
diff --git a/PVE/QemuServer/OVF.pm b/PVE/QemuServer/OVF.pm
index c76c199..36b7fff 100644
--- a/PVE/QemuServer/OVF.pm
+++ b/PVE/QemuServer/OVF.pm
@@ -87,7 +87,7 @@ sub id_to_pve {
# returns two references, $qm which holds qm.conf style key/values, and \@disks
sub parse_ovf {
- my ($ovf, $debug) = @_;
+ my ($ovf, $debug, $ignore_size) = @_;
my $dom = XML::LibXML->load_xml(location => $ovf, no_blanks => 1);
@@ -220,9 +220,11 @@ ovf:Item[rasd:InstanceID='%s']/rasd:ResourceType", $controller_id);
die "error parsing $filepath, file seems not to exist at $backing_file_path\n";
}
- my $virtual_size;
- if ( !($virtual_size = PVE::Storage::file_size_info($backing_file_path)) ) {
- die "error parsing $backing_file_path, size seems to be $virtual_size\n";
+ my $virtual_size = 0;
+ if (!$ignore_size) { # Not possible if manifest is uploaded in web gui
+ if ( !($virtual_size = PVE::Storage::file_size_info($backing_file_path)) ) {
+ die "error parsing $backing_file_path: Could not get file size info: $@\n";
+ }
}
$pve_disk = {
--
2.20.1
More information about the pve-devel
mailing list