From: "Fabian Grünbichler" <f.gruenbichler@proxmox.com>
To: "Dominic Jäger" <d.jaeger@proxmox.com>,
"Proxmox VE development discussion" <pve-devel@lists.proxmox.com>
Subject: Re: [pve-devel] [PATCH v4 qemu-server] Add API for disk & VM import
Date: Fri, 12 Feb 2021 10:03:50 +0100 [thread overview]
Message-ID: <1613119192.onm1x6fg5c.astroid@nora.none> (raw)
In-Reply-To: <20210211103223.GA34013@mala>
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.
next prev parent reply other threads:[~2021-02-12 9:04 UTC|newest]
Thread overview: 9+ messages / expand[flat|nested] mbox.gz Atom feed top
2021-02-05 10:04 Dominic Jäger
2021-02-05 10:04 ` [pve-devel] [PATCH v4 manager] gui: Add import wizard for disk & VM Dominic Jäger
2021-02-10 9:49 ` Fabian Grünbichler
2021-03-08 11:38 ` Dominic Jäger
2021-03-08 12:39 ` Fabian Grünbichler
2021-02-10 9:40 ` [pve-devel] [PATCH v4 qemu-server] Add API for disk & VM import Fabian Grünbichler
2021-02-11 10:32 ` Dominic Jäger
2021-02-12 9:03 ` Fabian Grünbichler [this message]
2021-02-12 11:13 ` Dominic Jäger
Reply instructions:
You may reply publicly to this message via plain-text email
using any one of the following methods:
* Save the following mbox file, import it into your mail client,
and reply-to-all from there: mbox
Avoid top-posting and favor interleaved quoting:
https://en.wikipedia.org/wiki/Posting_style#Interleaved_style
* Reply using the --to, --cc, and --in-reply-to
switches of git-send-email(1):
git send-email \
--in-reply-to=1613119192.onm1x6fg5c.astroid@nora.none \
--to=f.gruenbichler@proxmox.com \
--cc=d.jaeger@proxmox.com \
--cc=pve-devel@lists.proxmox.com \
/path/to/YOUR_REPLY
https://kernel.org/pub/software/scm/git/docs/git-send-email.html
* If your mail client supports setting the In-Reply-To header
via mailto: links, try the mailto: link
Be sure your reply has a Subject: header at the top and a blank line
before the message body.
This is an external index of several public inboxes,
see mirroring instructions on how to clone and mirror
all data and code used by this external index.