[pve-devel] [PATCH v6 qemu-server 2/5] disk reassign: add API endpoint

Aaron Lauterer a.lauterer at proxmox.com
Mon Apr 19 11:26:17 CEST 2021



On 4/15/21 1:52 PM, Fabian Ebner wrote:
> One nit and one comment inline.
> 
> Am 02.04.21 um 12:19 schrieb Aaron Lauterer:
>> The goal of this new API endpoint is to provide an easy way to move a
>> disk between VMs as this was only possible with manual intervention
>> until now. Either by renaming the VM disk or by manually adding the
>> disks volid to the config of the other VM.
>>
>> The latter can easily cause unexpected behavior such as disks attached
>> to VM B would be deleted if it used to be a disk of VM A. This happens
>> because PVE assumes that the VMID in the volname always matches the VM
>> the disk is attached to and thus, would remove any disk with VMID A
>> when VM A was deleted.
>>
>> The term `reassign` was chosen as it is not yet used
>> for VM disks.
>>
>> Signed-off-by: Aaron Lauterer <a.lauterer at proxmox.com>
>> ---
>> v5 -> v6:
>> * guard Replication snapshot cleanup
>>      additionally to the eval, that code is now only run if the volume is
>>      on a storage with the 'replicate' feature
>> * add permission check for target vmid
>> * changed regex to match unused keys better
>>
>> thx @Fabian for these suggestions/catching problems
>>
>> v4 -> v5:
>> * implemented suggestions from Fabian [1]
>>      * logging before action
>>      * improving description
>>      * improving error messages
>>      * using Replication::prepare to remove replication snapshots
>>      * check if disk is physical disk using /dev/...
>>
>> v3 -> v4: nothing
>>
>> v2 -> v3:
>> * reordered the locking as discussed with fabian [0] to
>> run checks
>>      fork worker
>>     lock source config
>>         lock target config
>>         run checks
>>         ...
>>
>> * added more checks
>>      * will not reassign to or from templates
>>      * will not reassign if VM has snapshots present
>> * cleanup if disk used to be replicated
>> * made task log slightly more verbose
>> * integrated general recommendations regarding code
>> * renamed `disk` to `drive_key`
>> * prepended some vars with `source_` for easier distinction
>>
>> v1 -> v2: print config key and volid info at the end of the job so it
>> shows up on the CLI and task log
>>
>> rfc -> v1:
>> * add support to reassign unused disks
>> * add support to provide a config digest for the target vm
>> * add additional check if disk key is present in config
>> * reorder checks a bit
>>
>> In order to support unused disk I had to extend
>> PVE::QemuServer::Drive::valid_drive_names for the API parameter
>> validation.
>>
>> Checks are ordered so that cheap tests are run at the first chance to
>> fail early.
>>
>> The check if both VMs are present on the node is a bit redundant because
>> locking the config files will fail if the VM is not present. But with
>> the additional check we can provide a useful error message to the user
>> instead of a "Configuration file xyz does not exist" error.
>>
>> [0] https://lists.proxmox.com/pipermail/pve-devel/2020-September/044930.html
>> [1] https://lists.proxmox.com/pipermail/pve-devel/2020-November/046030.html
>>
>>   PVE/API2/Qemu.pm        | 172 ++++++++++++++++++++++++++++++++++++++++
>>   PVE/QemuServer/Drive.pm |   4 +
>>   2 files changed, 176 insertions(+)
>>
>> diff --git a/PVE/API2/Qemu.pm b/PVE/API2/Qemu.pm
>> index e95ab13..9642b9b 100644
>> --- a/PVE/API2/Qemu.pm
>> +++ b/PVE/API2/Qemu.pm
>> @@ -35,6 +35,7 @@ use PVE::API2::Qemu::Agent;
>>   use PVE::VZDump::Plugin;
>>   use PVE::DataCenterConfig;
>>   use PVE::SSHInfo;
>> +use PVE::Replication;
>>   BEGIN {
>>       if (!$ENV{PVE_GENERATING_DOCS}) {
>> @@ -4377,4 +4378,175 @@ __PACKAGE__->register_method({
>>       return PVE::QemuServer::Cloudinit::dump_cloudinit_config($conf, $param->{vmid}, $param->{type});
>>       }});
>> +__PACKAGE__->register_method({
>> +    name => 'reassign_vm_disk',
>> +    path => '{vmid}/reassign_disk',
>> +    method => 'POST',
>> +    protected => 1,
>> +    proxyto => 'node',
>> +    description => "Reassign a disk to another VM",
>> +    permissions => {
>> +    description => "You need 'VM.Config.Disk' permissions on /vms/{vmid} and /vms/{target vmid}, and 'Datastore.Allocate' permissions on the storage.",
>> +    check => [ 'and',
>> +           ['perm', '/vms/{vmid}', [ 'VM.Config.Disk' ]],
>> +           ['perm', '/storage/{storage}', [ 'Datastore.Allocate' ]],
>> +        ],
>> +    },
>> +    parameters => {
>> +        additionalProperties => 0,
>> +    properties => {
>> +        node => get_standard_option('pve-node'),
>> +        vmid => get_standard_option('pve-vmid', { completion => \&PVE::QemuServer::complete_vmid }),
>> +        target_vmid => get_standard_option('pve-vmid', { completion => \&PVE::QemuServer::complete_vmid }),
>> +        drive_name => {
>> +            type => 'string',
>> +        description => "The config key of the disk to reassign (for example, ide0 or scsi1).",
>> +        enum => [PVE::QemuServer::Drive::valid_drive_names_with_unused()],
>> +        },
>> +        digest => {
>> +        type => 'string',
>> +        description => 'Prevent changes if current the configuration file of the source VM has a different SHA1 digest. This can be used to prevent concurrent modifications.',
>> +        maxLength => 40,
>> +        optional => 1,
>> +        },
>> +        target_digest => {
>> +        type => 'string',
>> +        description => 'Prevent changes if current the configuration file of the target VM has a different SHA1 digest. This can be used to prevent concurrent modifications.',
>> +        maxLength => 40,
>> +        optional => 1,
>> +        },
>> +    },
>> +    },
>> +    returns => {
>> +    type => 'string',
>> +    description => "the task ID.",
>> +    },
>> +    code => sub {
>> +    my ($param) = @_;
>> +
>> +    my $rpcenv = PVE::RPCEnvironment::get();
>> +    my $authuser = $rpcenv->get_user();
>> +
>> +    my $node = extract_param($param, 'node');
>> +    my $source_vmid = extract_param($param, 'vmid');
>> +    my $target_vmid = extract_param($param, 'target_vmid');
>> +    my $source_digest = extract_param($param, 'digest');
>> +    my $target_digest = extract_param($param, 'target_digest');
>> +    my $drive_name = extract_param($param, 'drive_name');
>> +
>> +    my $storecfg = PVE::Storage::config();
>> +    my $vmlist; > +    my $drive;
>> +    my $source_volid;
> Nit: $vmlist and $drive are only ever used within the load_and_check_configs closure, so they can be declared there
> 
>> +
>> +    $rpcenv->check_vm_perm($authuser, $target_vmid, undef, ['VM.Config.Disk'])
>> +        if $authuser ne 'root at pam';
>> +
>> +    die "Reassigning a disk to the same VM is not possible. Did you mean to move the disk?\n"
>> +        if $source_vmid eq $target_vmid;
>> +
>> +    my $load_and_check_configs = sub {
>> +        $vmlist = PVE::Cluster::get_vmlist()->{ids};
>> +        die "Both VMs need to be on the same node ($vmlist->{$source_vmid}->{node}) but target VM is on $vmlist->{$target_vmid}->{node}.\n"
>> +        if $vmlist->{$source_vmid}->{node} ne $vmlist->{$target_vmid}->{node};
>> +
>> +        my $source_conf = PVE::QemuConfig->load_config($source_vmid);
>> +        PVE::QemuConfig->check_lock($source_conf);
>> +        my $target_conf = PVE::QemuConfig->load_config($target_vmid);
>> +        PVE::QemuConfig->check_lock($target_conf);
>> +
>> +        die "Can't reassign disks from or to templates\n"
>> +        if ($source_conf->{template} || $target_conf->{template});
>> +
>> +        if ($source_digest) {
>> +        eval { PVE::Tools::assert_if_modified($source_digest, $source_conf->{digest}) };
>> +        if (my $err = $@) {
>> +            die "VM ${source_vmid}: ${err}";
>> +        }
>> +        }
>> +
>> +        if ($target_digest) {
>> +        eval { PVE::Tools::assert_if_modified($target_digest, $target_conf->{digest}) };
>> +        if (my $err = $@) {
>> +            die "VM ${target_vmid}: ${err}";
>> +        }
>> +        }
>> +
>> +        die "Disk '${drive_name}' does not exist\n"
>> +        if !defined($source_conf->{$drive_name});
>> +
>> +        $drive = PVE::QemuServer::parse_drive($drive_name, $source_conf->{$drive_name});
>> +        $source_volid = $drive->{file};
>> +        die "disk '${drive_name}' has no associated volume\n" if !$source_volid;
>> +        die "CD drive contents can't be reassigned\n" if PVE::QemuServer::drive_is_cdrom($drive, 1);
>> +        die "Can't reassign physical disk\n" if $drive->{file} =~ m|^/dev/|;
>> +        die "Can't reassign disk used by a snapshot\n"
>> +        if PVE::QemuServer::Drive::is_volume_in_use($storecfg, $source_conf, $drive_name, $source_volid);
>> +
>> +        die "Storage does not support the reassignment of this disk\n"
>> +        if !PVE::Storage::volume_has_feature($storecfg, 'reassign', $source_volid);
>> +
>> +        die "Cannot reassign disk while the source VM is running\n"
>> +        if PVE::QemuServer::check_running($source_vmid) && $drive_name !~ m/^unused\d+$/;
>> +
>> +        return ($source_conf, $target_conf);
>> +    };
>> +
>> +    my $logfunc = sub {
>> +        my ($msg) = @_;
>> +        print STDERR "$msg\n";
>> +    };
>> +
>> +    my $reassign_func = sub {
>> +        return PVE::QemuConfig->lock_config($source_vmid, sub {
>> +        return PVE::QemuConfig->lock_config($target_vmid, sub {
>> +            my ($source_conf, $target_conf) = &$load_and_check_configs();
>> +
>> +            PVE::Cluster::log_msg('info', $authuser, "reassign disk VM $source_vmid: reassign --disk ${drive_name} --target_vmid $target_vmid");
>> +
>> +            my $new_volid = PVE::Storage::reassign_volume(
>> +            $storecfg,
>> +            $source_volid,
>> +            $target_vmid,
>> +            );
>> +
>> +            delete $source_conf->{$drive_name};
>> +            print "removing disk '${drive_name}' from VM '${source_vmid}'\n";
>> +            PVE::QemuConfig->write_config($source_vmid, $source_conf);
>> +
>> +            # remove possible replication snapshots
>> +            if (PVE::Storage::volume_has_feature($storecfg, 'replicate', $source_volid)) {
>> +            eval {
>> +                PVE::Replication::prepare(
>> +                $storecfg,
>> +                [$new_volid],
>> +                undef,
>> +                undef,
> 
> To actually remove the replication snapshots, you need to use 1 for last_sync. undef defaults to 0 and does not remove the replication snapshots. 0 happens when a VM was stolen, but replication snapshots for stolen VMs are still valid!
> 
> The good news is that patch 4 isn't needed ;)

Thanks a lot for that hint :)

> 
>> +                undef,
>> +                $logfunc,
>> +                )
>> +            };
>> +            if (my $err = $@) {
>> +                print "Failed to remove replication snapshots on reassigned disk. Manual cleanup could be necessary.\n";
>> +            }
>> +            }
>> +
>> +            my $key;
>> +            eval { $key = PVE::QemuConfig->add_unused_volume($target_conf, $new_volid) };
>> +            if (my $err = $@) {
>> +            print "failed to add reassigned disk '${new_volid}' to VM '${target_vmid}'. Try to free an 'unused' disk slot and run 'qm rescan ${target_vmid}'.\n";
>> +            return 0;
>> +            }
>> +
>> +            print "adding disk to VM '${target_vmid}' as '${key}: ${new_volid}'\n";
>> +            PVE::QemuConfig->write_config($target_vmid, $target_conf);
>> +        });
>> +        });
>> +    };
>> +
>> +    &$load_and_check_configs();
>> +
>> +    return $rpcenv->fork_worker('qmreassign', $source_vmid, $authuser, $reassign_func);
>> +    }});
>> +
>>   1;
>> diff --git a/PVE/QemuServer/Drive.pm b/PVE/QemuServer/Drive.pm
>> index 01ea8d7..e938b9b 100644
>> --- a/PVE/QemuServer/Drive.pm
>> +++ b/PVE/QemuServer/Drive.pm
>> @@ -392,6 +392,10 @@ sub valid_drive_names {
>>               'efidisk0');
>>   }
>> +sub valid_drive_names_with_unused {
>> +    return (valid_drive_names(), map {"unused$_"} (0 .. ($MAX_UNUSED_DISKS -1)));
>> +}
>> +
>>   sub is_valid_drivename {
>>       my $dev = shift;
>>




More information about the pve-devel mailing list