[pve-devel] [PATCH v4 qemu-server] Add API for disk & VM import

Fabian Grünbichler f.gruenbichler at proxmox.com
Fri Feb 12 10:03:50 CET 2021


On February 11, 2021 11:32 am, Dominic Jäger wrote:
> Thank you for looking so carefully!
> 
> On Wed, Feb 10, 2021 at 10:40:56AM +0100, Fabian Grünbichler wrote:
>> 
>> On February 5, 2021 11:04 am, Dominic Jäger wrote:
>> > Extend qm importdisk/importovf functionality to the API.
>> >  
>> > @@ -4325,4 +4324,378 @@ __PACKAGE__->register_method({
>> >  	return PVE::QemuServer::Cloudinit::dump_cloudinit_config($conf, $param->{vmid}, $param->{type});
>> >      }});
>> >  
>> > +# Raise exception if $format is not supported by $storageid
>> > +my $check_format_is_supported = sub {
>> > +    my ($format, $storageid) = @_;
>> > +
>> > +    return if !$format;
>> > +
>> > +    my $store_conf = PVE::Storage::config();
>> 
>> it probably makes sense to pass this in as parameter, all call sites 
>> probably have it already ;)
> 
> I just noticed that I have it in importdisk, but unused.  Does it make a
> relevant difference in terms of performance to call ::config() multiple times
> instead of passing it as parameter?

well, it should be the same and fairly cheap since it comes from a cache 
after the first request, but if you trigger a cfs_update() in-between 
you might get two different versions.

> 
>> 
>> > +    }
>> > +};
>> > +
>> > +# paths are returned as is
>> > +# volids are returned as paths
>> > +#
>> > +# Also checks if $original actually exists
>> > +my $convert_to_path = sub {
>> > +	my ($original) = @_;
>> > +	my $volid_as_path = eval { # Nonempty iff $original_source is a volid
>> > +	    PVE::Storage::path(PVE::Storage::config(), $original);
>> > +	};
>> > +	my $result = $volid_as_path || $original ;
>> > +	if (!-e $result) {
>> > +	    die "Could not import because source '$original' does not exist!";
>> > +	}
>> > +	return $result;
>> > +};
>> 
>> what does this do that PVE::Storage::abs_filesystem_path doesn't already 
>> do (except having 'import' in the error message ;))?
> 
> Haven't thought about that function in the last time... 
> I'll have a look at it.
> 
>> ah, further down below I see that -b might be missing for raw block 
>> devices.. maybe it makes sense to allow those in that helper as well? 
>> optionally? call-sites would need to be checked..
>  Would it make sense to not change helper for the moment, certainly not break
> other call-sites and refactor later in a separate patch?

optionally supporting block devices should not be able to break any 
existing call sites ;) but yeah, you can also replace your helper here 
with a fork of abs_filesystem_path that allows -b, that would allow 
simply switching it out if we want to handle that in pve-storage..

>> 
>> > +
>> > +# vmid ... target VM ID
>> > +# source ... absolute path of the source image (volid must be converted before)
>> 
>> that restriction is not actually needed, see below
>> 
>> > +# storage ... target storage for the disk image
>> > +# format ... target format for the disk image (optional)
>> > +#
>> > +# returns ... volid of the allocated disk image (e.g. local-lvm:vm-100-disk-2)
>> > +my $import_disk_image = sub {
>> > +    my ($param) = @_;
>> > +    my $vmid = $param->{vmid};
>> > +    my $requested_format = $param->{format};
>> > +    my $storage = $param->{storage};
>> > +    my $source = $param->{source};
>> > +
>> > +    my $vm_conf = PVE::QemuConfig->load_config($vmid);
>> 
>> could be passed in, the call sites already have it.. and it might allow 
>> to pull in some of the surrounding codes at the call sites that is 
>> similar (config updating, property string building) into this helper
> 
> It's actually unused. I'll remove it.
>> 
>> > +    my $store_conf = PVE::Storage::config();
>> 
>> could also be passed in here
> * see below
>> 
>> > +    if (!$source) {
>> > +	die "It is necessary to pass the source parameter";
>> > +    }
>> 
>> just a check for definedness
> Hmm but if someone does
> $import_disk_image->({vmid => 102, source => "", storage => "local-lvm"});
> then
>> 
>> > +    if ($source !~ m!^/!) {
>> > +	die "source must be an absolute path but is $source";
> will only say "... path but is  at /usr/share/..."?

no, because that check gets removed as well.. $source could be a volid 
at this point

> 
>> 
>> > +    if (!$storage) {
>> > +	die "It is necessary to pass the storage parameter";
>> > +    }
>> 
>> call PVE::Storage::storage_config() here to get both that and the check 
>> that the storage exists..
> Do you mean $store_conf->{ids}->{$storage} ?

no, I mean storage_config() ;)

it checks whether $storage is defined & non-empty, is an actually 
defined-in-storage.cfg storage, and returns that storage's config 
section.

> 
>> 
>> > +
>> > +__PACKAGE__->register_method ({
>> > +    name => 'importdisk',
>> > +    path => '{vmid}/importdisk',
>> > +    method => 'POST',
>> > +    protected => 1, # for worker upid file
>> 
>> huh? no - because we need elevated privileges to allocate disks on 
>> storages..
> Might be left over, I'll check.
>> 
>> > +    proxyto => 'node',
>> > +    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-lvm:vm-104-disk-0), an image on a PVE storage ".
>> > +		    "(local:104/toImport.raw) or (for root only) an absolute ".
>> 
>> how does the second work here? and the whole thing is root-only, so that 
>> qualifier at the end is redundant ;)
> 
> The description is bad... I guess local:104/toImport.raw is a volid, too?
> I could just image it to be useful here to allow special volids like
> --source local:99/somedisk.qcow2

it's a volid, but not a valid VM-owned volume that we can do ACL checks 
on.. so it's important to think about where to use `parse_volid` vs 
`parse_volname` (the latter gives you all the checks per storage type, 
and returns type of volume, associated VMID, etc.).

for this series/patch, we can accept a proper vdisk volid if the user 
has access, or any volid/path for root. we can't allow arbitrary volids 
for unprivileged users, as that would allow reading ANY file on the 
storage.

> 
>> 
>> > +	    format => {
>> > +		type => 'string',
>> > +		description => 'Target format.',
>> > +		enum => [ 'raw', 'qcow2', 'vmdk' ],
>> > +		optional => 1,
>> > +	    },
>> 
>> not directly related to this series, but this enum is copied all over 
>> the place and might be defined in one place as standard option 
>> (qm-new-disk-format ?)
> Makes sense to me. Only
>> (qm-new-disk-format ?)
> raw, qcow2 and vmdk are not only for new disks? Those are allowed formats for every directory storage, so maybe something like dir-disk-format?

as separate parameter they are only used when creating/importing/.. NEW 
disks. of course, they are also part of the schema as part of property 
strings. also dir-disk-format would be wrong - just because 
non-dir-storages only support raw(/subvol) doesn't make the parameter 
invalid for them ;)

>> 
>> > +	    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 $original_source = extract_param($param, 'source');
>> 
>> not sure why this gets this name, it's just passed once and not used 
>> afterwards..
> It was useful previously... Changed it now.
>> 
>> > +	my $digest = extract_param($param, 'digest');
>> > +	my $device_options = extract_param($param, 'device_options');
>> 
>> IMHO this one needs to be parsed - e.g., by adding a fake volume with 
>> the syntax used in importvm at the front and parsing it according to the 
>> disk schema..
>> 
>> both to catch bogus stuff early, and to make the handling below more 
>> robust w.r.t. future changes..
> 
> OK. The API currently allows any string. Would it be worth to change that then?

not sure whether that's trivially possible - the options are not uniform 
across bus / device types? you could have a 'everything optional 
superset of all disk device types' schema, that would be less strict 
than the final check but better than nothing? looking at 
PVE::QemuServer::Drive it almost exists in $alldrive_fmt, so cloning 
that and removing the file property could work?

>> > +	    if ($format_explicit && $format_device_option) {
>> > +		raise_param_exc({format => "Disk format may be specified only once!"});
>> 
>> format already specified in device_options?
> Also good for me.
> 
>> 
>> > +	    }
>> > +	}
>> > +	my $format = $format_explicit || $format_device_option;
>> 
>> since both of those are not needed after this point, this could just be 
>> returned / set by the above if, dropping the extra variables in the 
>> outer scope..
> Do you mean with a small function?
> my $get_format = sub {
> 	...
> 	return $explicit || $device_options
> }
> my $format = $get_format($device_options, extract_param($param, 'format'));

I think a function is overkill for a single use, just

my $format;
if (...) {
  // decide and set $format
}

>> 
>> > +	$check_format_is_supported->($format, $storeid);
>> > +
>> > +	my $locked = sub {
>> > +	    my $conf = PVE::QemuConfig->load_config($vmid);
>> > +	    PVE::Tools::assert_if_modified($conf->{digest}, $digest);
>> > +
>> > +	    if ($device && $conf->{$device}) {
>> > +		die "Could not import because device $device is already in ".
>> > +		"use in VM $vmid. Choose a different device!";
>> 
>> both of these checks might make sense outside of the worker already to 
>> give immediate feedback in the API response..
> I wanted to do this but didn't know what to do with the lock.
> If I check first and lock later then the config could already have changed?
> Or check twice?
> 1. outside the worker, because most times it is OK and gives a nice error and
> 2. inside the worker, to be really sure?
> 
> Or lock outside the worker? I'll have to try what is actually possible...

check before, compare digest after fork + lock + reload should handle any 
changes in-between?

then you only need to duplicate the digest check.

> 
>> > +	    }
>> 
>> > +
>> > +	my $msg = "There must be exactly as many devices specified as there " .
>> > +	    " are devices in the diskimage parameter.\n For example for " .
>> > +	    "--scsi0 local-lvm:0,discard=on --scsi1 local:0,cache=unsafe " .
>> > +	    "there must be --diskimages scsi0=/source/path,scsi1=/other/path";
>> > +	my $device_count = grep { $use_import->($_) } keys %$param;
>> 
>> why though? isn't it sufficient to say there must be a matching device for 
>> each diskimages entry and vice-versa?
> That's what I meant, but your formulation is better.

it also allows you to give concrete error messages if you do one-to-one 
mapping:

"scsi0 configured for disk import, but no matching import source given"
"virtio0 not configured for disk import, but import source '...' given"

> 
>> 
>> > +	}
>> > +
>> > +	my $worker = sub {
>> > +	    eval { PVE::QemuConfig->create_and_lock_config($vmid, 0, 'import') };
>> > +	    die "Unable to create config for VM import: $@" if $@;
>> 
>> should happen outside the worker (so that a VMID clash is detected 
>> early). inside the worker you just lock and load, then check that the 
>> 'import' lock is still there..
>> 
>> we could also filter out disk parameters, and call update_vm_api with 
>> the rest here (those should be fast, but potentially in the future could 
>> run into permission issues that would be nice to return early before 
>> doing a massive disk conversion that has to be undone afterwards..
>> 
>> > +
>> > +	    my @volids_of_imported = ();
>> > +	    eval { foreach my $opt (keys %$param) {
>> 
>> this could then just iterate over the disk parameters
> I thought about that, too. But I didn't take future permission issues into account. And then having a single loop and no filters seemed like the easier-to-read option to me.
> 
> I'll change it.
>> 
>> > +		next if ($opt eq 'start');
>> > +
>> > +		my $updated_value;
>> > +		if ($use_import->($opt)) {
>> > +		    # $opt is bus/device like ide0, scsi5
>> > +
>> > +		    my $device = PVE::QemuServer::parse_drive($opt, $param->{$opt});
>> 
>> $drive? $device is used in this very patch for something else ;)
> Right.
> 
> 
> "OK, will fix" to everything that I haven't mentioned. I certainly read it.




More information about the pve-devel mailing list