needs a rebase ;) On September 15, 2020 1:33 pm, Dominic Jäger wrote: > Required to create a GUI for importdisk. > > Add parameters that enable directly attaching the disk to a bus/device with > all > known disk options. This avoids intermediate steps as unused disk. > > We allow different places as source > * Regular VM images on PVE storages (Normal users + root) > * Other disk images on PVE storages (Normal users + root) (they have already > been displayed in the FileSelector before) > * Any path (root only) > > Using any path for normal users would be security risk. But if you have to > move a disk image to a PVE storage you only are not too many steps > * rename image according to PVE schema > * qm rescan > * double click in GUI to attach > away from making the whole importdisk obsolete. > > Enabling arbitrary paths for root additionally makes it unnecessary to move > the > disk image or create an appropriate storage. That means no knowledge about PVE > storage content naming schemes ("why do I have to move it into a images/<vmid> > subfolder of a directory based storage?") is required. Importing could then be > comprised of only two steps: > 1. mount external drive (hopefully most PVE admins can figure this out) > 2. Click through GUI window and insert > /mount/externalDrive/exportedFromEsxi.vmdk > > Uploading disk images to avoid the PVE storage naming knowledge can still be > added in the future. However, such a function might not be ideal for big > images > because > * Upload via browser might fail easily? > * Potentially copying huge images from server to local to server? > > So having the absolute path as an option between renaming everything manually > and just uploading it in GUI without CLI knowledge looks like a useful > addition > to me. > > This patch combines the main part of the previous qm importdisk and do_import > into a helper $convert_and_attach in PVE/API2/Qemu.pm to avoid race conditions > and potentially duplicating code from update_vm_api into do_import. > Furthermore, the only other place where it was invoked was importovf, which > now > also uses the helper. importovf will be moved to PVE/API2/Qemu.pm, too, so > placing the helper somewhere else does not look useful to me. > > Signed-off-by: Dominic Jäger <d.jae...@proxmox.com> > --- > v3->v4: > * More detailed permissions > * Solve race conditions and code reuse questions by completely moving > do_import > to PVE/API2/Qemu.pm; lock the whole procedure > * As I found both discussed approaches > - Image selector (Thomas) > - Paths (Dominik) > useful I included in both. Hope I didn't misunderstand any of you. > > > PVE/API2/Qemu.pm | 184 ++++++++++++++++++++++++++++++++++- > PVE/CLI/qm.pm | 70 ++----------- > PVE/QemuServer.pm | 2 +- > PVE/QemuServer/Drive.pm | 13 +++ > PVE/QemuServer/ImportDisk.pm | 85 ---------------- > PVE/QemuServer/Makefile | 1 - > 6 files changed, 205 insertions(+), 150 deletions(-) > delete mode 100755 PVE/QemuServer/ImportDisk.pm > > diff --git a/PVE/API2/Qemu.pm b/PVE/API2/Qemu.pm > index 8da616a..9aa85b5 100644 > --- a/PVE/API2/Qemu.pm > +++ b/PVE/API2/Qemu.pm > @@ -45,8 +45,6 @@ BEGIN { > } > } > > -use Data::Dumper; # fixme: remove > - > use base qw(PVE::RESTHandler); > > my $opt_force_description = "Force physical removal. Without this, we simple > remove the disk from the config file and create an additional configuration > entry called 'unused[n]', which contains the volume ID. Unlink of unused[n] > always cause physical removal."; > @@ -4265,4 +4263,186 @@ __PACKAGE__->register_method({ > return PVE::QemuServer::Cloudinit::dump_cloudinit_config($conf, > $param->{vmid}, $param->{type}); > }}); > > +# TODO Make locally scoped when importovf is moved from qm to API / this > package
maybe we could have a description of $param here? doing that often helps to show whether this is a good choice ;) e.g., in this case $param->{original_source} is never used.. I would suggest moving the config loading outside and passing the configs to this. importovf will probably hold a lock and load the configs once, so no need to put this inside this shared method. > +our $convert_and_attach_disk = sub { > + my ($param) = @_; > + my $vm_conf = PVE::QemuConfig->load_config($param->{vmid}); > + my $store_conf = PVE::Storage::config(); > + PVE::QemuConfig->check_lock($vm_conf) if !$param->{skiplock}; > + if ($param->{device} && $vm_conf->{$param->{device}}) { > + die "Could not import because device $param->{device} is already in ". > + "use in VM $param->{vmid}. Choose a different device!\n"; > + } > + if ($param->{digest} && $param->{digest} ne $vm_conf->{digest}) { > + die "VM $param->{vmid} config checksum missmatch (file change by other > user?)\n"; > + } use assert_if_modified, but also move this to the API call where the config is locked+loaded.. > + > + my $msg = $param->{device} ? "to $param->{device} on" : 'as unused disk > to'; > + print "Importing disk '$param->{source_as_path}' $msg VM > $param->{vmid}...\n"; this sounds strange, maybe Dylan can contribute a proper English phrasing for this ;) > + > + my $src_size = PVE::Storage::file_size_info($param->{source_as_path}); needs check for undef > + my $dst_format = PVE::QemuServer::resolve_dst_disk_format( > + $store_conf, $param->{storage}, undef, $param->{format}); isn't this a bit strange? if I request to import as qcow2, but my storage does not support it, I probably want to get an error and not silently be converted to the default format of the storage? > + my $dst_volid = PVE::Storage::vdisk_alloc($store_conf, $param->{storage}, > + $param->{vmid}, $dst_format, undef, $src_size / 1024); > + > + 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($store_conf, > + 'sparseinit', $dst_volid); > + > + PVE::Storage::activate_volumes($store_conf, [$dst_volid]); > + PVE::QemuServer::qemu_img_convert($param->{source_as_path}, $dst_volid, > + $src_size, undef, $zeroinit); > + PVE::Storage::deactivate_volumes($store_conf, [$dst_volid]); > + > + if ($param->{device}) { > + my $device_param = $dst_volid; > + $device_param .= ",$param->{device_options}" if > $param->{device_options}; > + $update_vm_api->({ > + vmid => $param->{vmid}, > + $param->{device} => $device_param, > + skiplock => $param->{skiplock} || 0, # Avoid uninitialized > values > + }, 1); > + } else { > + $param->{device} = PVE::QemuConfig->add_unused_volume($vm_conf, > $dst_volid); > + PVE::QemuConfig->write_config($param->{vmid}, $vm_conf); > + } > + }; > + if (my $err = $@) { > + eval { PVE::Storage::vdisk_free($store_conf, $dst_volid) }; > + warn "Cleanup of $dst_volid failed: $@ \n" if $@; > + > + die "Importing disk '$param->{source_as_path}' failed: $err\n" if $err; > + } > + > + return $dst_volid; > +}; > + > +__PACKAGE__->register_method ({ > + name => 'importdisk', > + path => '{vmid}/importdisk', > + method => 'POST', > + protected => 1, # for worker upid file > + proxyto => 'node', > + description => "Import an external disk image into a VM. The image > format ". > + "has to be supported by qemu-img.", > + permissions => { > + check => [ 'and', > + [ 'perm', '/storage/{storage}', ['Datastore.Audit']], > + [ 'perm', '/storage/{storage}', ['Datastore.Allocate']], > + [ 'perm', '/storage/{storage}', ['Datastore.AllocateTemplate']], > + [ 'perm', '/storage/{storage}', ['Datastore.AllocateSpace']], > + [ 'perm', '/vms/{vmid}', ['VM.Allocate']], > + [ 'perm', '/vms/{vmid}', ['VM.Config.Disk']], I doubt this is actually what you wanted here (all those permissions combined don't really make sense). Let's see whether we can disentangle it a bit: - we are modifying a VM config (adding a new disk entry or a new unused entry) - this is should require the same permissions as modifying that part of the config, which is probably VM.Config.Disk and Datastore.AllocateSpace - we are allocating a new vdisk volume on a storage (this should require Datastore.Allocatespace) we also potentially read from a storage (which requires a separate check) or from an arbitrary path (which requires yet another check). > + ], > + }, > + 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 ". > + "path on the server.", > + type => 'string', we have a format for something similar (volid or path) already, maybe we can extend it? > + }, > + 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', > + format => 'drive_options', > + description => "Options to set for the new disk ". > + "(e.g. 'discard=on,backup=0')", > + optional => 1, > + }, > + 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'), > + skiplock => get_standard_option('skiplock'), > + }, > + }, > + returns => { type => 'string'}, > + code => sub { > + my ($param) = @_; > + my $rpcenv = PVE::RPCEnvironment::get(); > + my $authuser = $rpcenv->get_user(); > + > + my $vmid = extract_param($param, 'vmid'); > + my $original_source = extract_param($param, 'source'); > + my $digest = extract_param($param, 'digest'); > + my $device_options = extract_param($param, 'device_options'); > + my $device = extract_param($param, 'device'); > + # importovf holds a lock itself which would make automatically updating > + # VM configs fail > + my $skiplock = extract_param($param, 'skiplock'); > + my $storecfg = PVE::Storage::config(); > + > + if ($skiplock && $authuser ne 'root@pam') { > + raise_perm_exc("Only root may use skiplock."); > + } > + if ($original_source eq "") { > + die "Could not import because source parameter is an empty > string!\n"; > + } should be handled by schema, not manually. > + if ($device && !PVE::QemuServer::is_valid_drivename($device)) { > + die "Invalid device name: $device!"; > + } this should not be possible, since it's an enum and already checked by the API > + if ($device_options && !$device) { > + die "Cannot use --device_options without specifying --device!" > + } this can be encoded in the schema (with 'requires') > + eval { > + PVE::Storage::check_volume_access($rpcenv, $authuser, $storecfg, > + $vmid, $original_source) > + }; so the STORAGE:PATH example from the description is also limited to root@pam > + raise_perm_exc($@) if $@; > + > + # A path is required for $convert_and_attach_disk > + my $volid_as_path = eval { # Nonempty iff $original_source is a volid > + PVE::Storage::path($storecfg, $original_source); > + }; > + my $source_as_path = $volid_as_path || $original_source ; > + if (!-e $source_as_path) { > + die "Could not import because source '$original_source' does not > exist!\n"; > + } > + > + my $worker = sub { > + my $dst_volid = PVE::QemuConfig->lock_config($vmid, > $convert_and_attach_disk, > + { > + vmid => $vmid, > + original_source => $original_source, > + device => $device, > + device_options => $device_options, > + storage => extract_param($param, 'storage'), > + source_as_path => $source_as_path, > + format => extract_param($param, 'format'), > + skiplock => $skiplock, > + }); > + print "Successfully imported disk '$original_source ' as ". > + "$device: $dst_volid\n"; $device is potentially undef here > + }; > + > + return $rpcenv->fork_worker('importdisk', $vmid, $authuser, $worker); > + }}); > + > 1; > diff --git a/PVE/CLI/qm.pm b/PVE/CLI/qm.pm > index 282fa86..f71b3d6 100755 > --- a/PVE/CLI/qm.pm > +++ b/PVE/CLI/qm.pm > @@ -31,7 +31,6 @@ use PVE::QemuConfig; > use PVE::QemuServer::Drive; > use PVE::QemuServer::Helpers; > use PVE::QemuServer::Agent qw(agent_available); > -use PVE::QemuServer::ImportDisk; > use PVE::QemuServer::Monitor qw(mon_cmd); > use PVE::QemuServer::OVF; > use PVE::QemuServer; > @@ -445,61 +444,6 @@ __PACKAGE__->register_method ({ > return undef; > }}); > > -__PACKAGE__->register_method ({ > - name => 'importdisk', > - path => 'importdisk', > - method => 'POST', > - description => "Import an external disk image as an unused disk in a VM. > The > - image format has to be supported by qemu-img(1).", > - parameters => { > - additionalProperties => 0, > - properties => { > - vmid => get_standard_option('pve-vmid', {completion => > \&PVE::QemuServer::complete_vmid}), > - source => { > - description => 'Path to the disk image to import', > - type => 'string', > - optional => 0, > - }, > - storage => get_standard_option('pve-storage-id', { > - description => 'Target storage ID', > - completion => \&PVE::QemuServer::complete_storage, > - optional => 0, > - }), > - format => { > - type => 'string', > - description => 'Target format', > - enum => [ 'raw', 'qcow2', 'vmdk' ], > - optional => 1, > - }, > - }, > - }, > - returns => { type => 'null'}, > - code => sub { > - my ($param) = @_; > - > - my $vmid = extract_param($param, 'vmid'); > - my $source = extract_param($param, 'source'); > - my $storeid = extract_param($param, 'storage'); > - my $format = extract_param($param, 'format'); > - > - my $vm_conf = PVE::QemuConfig->load_config($vmid); > - PVE::QemuConfig->check_lock($vm_conf); > - die "$source: non-existent or non-regular file\n" if (! -f $source); > - > - my $storecfg = PVE::Storage::config(); > - PVE::Storage::storage_check_enabled($storecfg, $storeid); > - > - my $target_storage_config = PVE::Storage::storage_config($storecfg, > $storeid); > - die "storage $storeid does not support vm images\n" > - if !$target_storage_config->{content}->{images}; > - > - print "importing disk '$source' to VM $vmid ...\n"; > - my ($drive_id, $volid) = > PVE::QemuServer::ImportDisk::do_import($source, $vmid, $storeid, { format => > $format }); > - print "Successfully imported disk as '$drive_id:$volid'\n"; > - > - return undef; > - }}); > - > __PACKAGE__->register_method ({ > name => 'terminal', > path => 'terminal', > @@ -640,17 +584,21 @@ __PACKAGE__->register_method ({ > $conf->{cores} = $parsed->{qm}->{cores} if > defined($parsed->{qm}->{cores}); > > eval { > - # order matters, as do_import() will load_config() internally > + # order matters, as $convert_and_attach_disk will load_config() > internally > $conf->{vmgenid} = PVE::QemuServer::generate_uuid(); > $conf->{smbios1} = PVE::QemuServer::generate_smbios1_uuid(); > PVE::QemuConfig->write_config($vmid, $conf); > > foreach my $disk (@{ $parsed->{disks} }) { > my ($file, $drive) = ($disk->{backing_file}, > $disk->{disk_address}); > - PVE::QemuServer::ImportDisk::do_import($file, $vmid, $storeid, { > - drive_name => $drive, > + $PVE::API2::Qemu::convert_and_attach_disk->({ > + node => $nodename, > + vmid => $vmid, > + source_as_path => $file, > + storage => $storeid, > + device => $drive, > format => $format, > - skiplock => 1, > + skiplock => 1, # Required to update VM configs > }); > } > > @@ -984,7 +932,7 @@ our $cmddef = { > > terminal => [ __PACKAGE__, 'terminal', ['vmid']], > > - importdisk => [ __PACKAGE__, 'importdisk', ['vmid', 'source', > 'storage']], > + importdisk => [ "PVE::API2::Qemu", 'importdisk', ['vmid', 'source', > 'storage'], { node => $nodename }], > > importovf => [ __PACKAGE__, 'importovf', ['vmid', 'manifest', > 'storage']], > > diff --git a/PVE/QemuServer.pm b/PVE/QemuServer.pm > index 2747c66..715c507 100644 > --- a/PVE/QemuServer.pm > +++ b/PVE/QemuServer.pm > @@ -6613,7 +6613,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 for LVM images for example this is not really related to this patch, right? > $src_path = $src_volid; > if ($src_path =~ m/\.($PVE::QemuServer::Drive::QEMU_FORMAT_RE)$/) { > $src_format = $1; > diff --git a/PVE/QemuServer/Drive.pm b/PVE/QemuServer/Drive.pm > index 91c33f8..af52035 100644 > --- a/PVE/QemuServer/Drive.pm > +++ b/PVE/QemuServer/Drive.pm > @@ -308,6 +308,19 @@ my $alldrive_fmt = { > %wwn_fmt, > }; > > +my %optional_file_drivedesc_base = %drivedesc_base; > +$optional_file_drivedesc_base{file}{optional} = 1; > +my $drive_options_fmt = { > + %optional_file_drivedesc_base, > + %iothread_fmt, > + %model_fmt, > + %queues_fmt, > + %scsiblock_fmt, > + %ssd_fmt, > + %wwn_fmt, > +}; > +PVE::JSONSchema::register_format('drive_options', $drive_options_fmt); > + > my $efidisk_fmt = { > volume => { alias => 'file' }, > file => { > diff --git a/PVE/QemuServer/ImportDisk.pm b/PVE/QemuServer/ImportDisk.pm > deleted file mode 100755 > index 51ad52e..0000000 > --- a/PVE/QemuServer/ImportDisk.pm > +++ /dev/null > @@ -1,85 +0,0 @@ > -package PVE::QemuServer::ImportDisk; > - > -use strict; > -use warnings; > - > -use PVE::Storage; > -use PVE::QemuServer; > -use PVE::Tools qw(run_command extract_param); > - > -# imports an external disk image to an existing VM > -# 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 ... > -sub do_import { > - my ($src_path, $vmid, $storage_id, $params) = @_; > - > - my $drive_name = extract_param($params, 'drive_name'); > - my $format = extract_param($params, 'format'); > - if ($drive_name && !(PVE::QemuServer::is_valid_drivename($drive_name))) { > - die "invalid drive name: $drive_name\n"; > - } > - > - # get the needed size from source disk > - my $src_size = PVE::Storage::file_size_info($src_path); > - > - # get target format, target image's path, and whether it's possible to > sparseinit > - my $storecfg = PVE::Storage::config(); > - my $dst_format = PVE::QemuServer::resolve_dst_disk_format($storecfg, > $storage_id, undef, $format); > - > - my $dst_volid = PVE::Storage::vdisk_alloc($storecfg, $storage_id, $vmid, > $dst_format, undef, $src_size / 1024); > - > - my $zeroinit = PVE::Storage::volume_has_feature($storecfg, 'sparseinit', > $dst_volid); > - > - my $create_drive = sub { > - my $vm_conf = PVE::QemuConfig->load_config($vmid); > - if (!$params->{skiplock}) { > - PVE::QemuConfig->check_lock($vm_conf); > - } > - > - if ($drive_name) { > - # should never happen as setting $drive_name is not exposed to > public interface > - die "cowardly refusing to overwrite existing entry: $drive_name\n" > if $vm_conf->{$drive_name}; > - > - my $modified = {}; # record what $option we modify > - $modified->{$drive_name} = 1; > - $vm_conf->{pending}->{$drive_name} = $dst_volid; > - PVE::QemuConfig->write_config($vmid, $vm_conf); > - > - my $running = PVE::QemuServer::check_running($vmid); > - if ($running) { > - my $errors = {}; > - PVE::QemuServer::vmconfig_hotplug_pending($vmid, $vm_conf, > $storecfg, $modified, $errors); > - warn "hotplugging imported disk '$_' failed: $errors->{$_}\n" > for keys %$errors; > - } else { > - PVE::QemuServer::vmconfig_apply_pending($vmid, $vm_conf, > $storecfg); > - } > - } else { > - $drive_name = PVE::QemuConfig->add_unused_volume($vm_conf, > $dst_volid); > - PVE::QemuConfig->write_config($vmid, $vm_conf); > - } > - }; > - > - eval { > - # trap interrupts so we have a chance to clean up > - local $SIG{INT} = > - local $SIG{TERM} = > - local $SIG{QUIT} = > - local $SIG{HUP} = > - local $SIG{PIPE} = sub { die "interrupted by signal $!\n"; }; > - > - 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); > - }; > - if (my $err = $@) { > - eval { PVE::Storage::vdisk_free($storecfg, $dst_volid) }; > - warn "cleanup of $dst_volid failed: $@\n" if $@; > - die $err; > - } > - > - return ($drive_name, $dst_volid); > -} > - > -1; > diff --git a/PVE/QemuServer/Makefile b/PVE/QemuServer/Makefile > index fd8cfbb..ecdab56 100644 > --- a/PVE/QemuServer/Makefile > +++ b/PVE/QemuServer/Makefile > @@ -1,7 +1,6 @@ > SOURCES=PCI.pm \ > USB.pm \ > Memory.pm \ > - ImportDisk.pm \ > OVF.pm \ > Cloudinit.pm \ > Agent.pm \ > -- > 2.20.1 > > > _______________________________________________ > pve-devel mailing list > pve-devel@lists.proxmox.com > https://lists.proxmox.com/cgi-bin/mailman/listinfo/pve-devel > _______________________________________________ pve-devel mailing list pve-devel@lists.proxmox.com https://lists.proxmox.com/cgi-bin/mailman/listinfo/pve-devel