[pve-devel] [PATCH v2 qemu-server 5/9] api: move-disk: add move to other VM
Fabian Ebner
f.ebner at proxmox.com
Wed Sep 1 11:48:47 CEST 2021
Am 13.08.21 um 17:35 schrieb Aaron Lauterer:
>
>
> On 8/13/21 9:41 AM, Fabian Ebner wrote:
>> Am 06.08.21 um 15:46 schrieb Aaron Lauterer:
>>> The goal of this is to expand the move-disk API endpoint to make it
>>> possible to move a disk to another VM. Previously this was only possible
>>> with manual intervertion either by renaming the VM disk or by manually
>>> adding the disks volid to the config of the other VM.
>>>
>>> Signed-off-by: Aaron Lauterer <a.lauterer at proxmox.com>
>>> ---
>>> v1 -> v2:
>>> * make --target-disk optional and use source disk key as fallback
>>> * use parse_volname instead of custom regex
>>> * adapt to find_free_volname
>>> * smaller (style) fixes
>>>
>>> rfc -> v1:
>>> * add check if target guest is replicated and fail if the moved volume
>>> does not support it
>>> * check if source volume has a format suffix and pass it to
>>> 'find_free_disk'
>>> * fixed some style nits
>>>
>>> old dedicated api endpoint -> rfc:
>>> There are some big changes here. The old [0] dedicated API endpoint is
>>> gone and most of its code is now part of move_disk. Error messages have
>>> been changed accordingly and sometimes enahnced by adding disk keys and
>>> VMIDs where appropriate.
>>>
>>> Since a move to other guests should be possible for unused disks, we
>>> need to check before doing a move to storage to make sure to not
>>> handle unused disks.
>>>
>>> PVE/API2/Qemu.pm | 238 ++++++++++++++++++++++++++++++++++++++++++++++-
>>> PVE/CLI/qm.pm | 2 +-
>>> 2 files changed, 234 insertions(+), 6 deletions(-)
>>>
>>> diff --git a/PVE/API2/Qemu.pm b/PVE/API2/Qemu.pm
>>> index ef0d877..30e222a 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}) {
>>> @@ -3274,9 +3275,11 @@ __PACKAGE__->register_method({
>>> method => 'POST',
>>> protected => 1,
>>> proxyto => 'node',
>>> - description => "Move volume to different storage.",
>>> + description => "Move volume to different storage or to a
>>> different VM.",
>>> permissions => {
>>> - description => "You need 'VM.Config.Disk' permissions on
>>> /vms/{vmid}, and 'Datastore.AllocateSpace' permissions on the storage.",
>>> + description => "You need 'VM.Config.Disk' permissions on
>>> /vms/{vmid}, " .
>>> + "and 'Datastore.AllocateSpace' permissions on the storage.
>>> To move ".
>>> + "a disk to another VM, you need the permissions on the
>>> target VM as well.",
>>> check => [ 'and',
>>> ['perm', '/vms/{vmid}', [ 'VM.Config.Disk' ]],
>>> ['perm', '/storage/{storage}', [
>>> 'Datastore.AllocateSpace' ]],
>>> @@ -3287,14 +3290,19 @@ __PACKAGE__->register_method({
>>> 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,
>>> + optional => 1,
>>> + }),
>>> disk => {
>>> type => 'string',
>>> description => "The disk you want to move.",
>>> - enum => [PVE::QemuServer::Drive::valid_drive_names()],
>>> + enum =>
>>> [PVE::QemuServer::Drive::valid_drive_names_with_unused()],
>>> },
>>> storage => get_standard_option('pve-storage-id', {
>>> description => "Target storage.",
>>> completion => \&PVE::QemuServer::complete_storage,
>>> + optional => 1,
>>> }),
>>> 'format' => {
>>> type => 'string',
>>> @@ -3321,6 +3329,20 @@ __PACKAGE__->register_method({
>>> minimum => '0',
>>> default => 'move limit from datacenter or storage config',
>>> },
>>> + 'target-disk' => {
>>> + type => 'string',
>>> + description => "The config key the disk will be moved to on
>>> the target VM " .
>>> + "(for example, ide0 or scsi1).",
>>> + enum =>
>>> [PVE::QemuServer::Drive::valid_drive_names_with_unused()],
>>> + optional => 1,
>>
>> The default could be mentioned here.
>
> Good point.
>
>>
>>> + },
>>> + 'target-digest' => {
>>> + type => 'string',
>>> + description => 'Prevent changes if current configuration
>>> file of the target VM has " .
>>> + "a different SHA1 digest. This can be used to prevent
>>> concurrent modifications.',
>>> + maxLength => 40,
>>> + optional => 1,
>>> + },
>>> },
>>> },
>>> returns => {
>>> @@ -3335,14 +3357,22 @@ __PACKAGE__->register_method({
>>> my $node = extract_param($param, 'node');
>>> my $vmid = extract_param($param, 'vmid');
>>> + my $target_vmid = extract_param($param, 'target-vmid');
>>> my $digest = extract_param($param, 'digest');
>>> + my $target_digest = extract_param($param, 'target-digest');
>>> my $disk = extract_param($param, 'disk');
>>> + my $target_disk = extract_param($param, 'target-disk') // $disk;
>>> my $storeid = extract_param($param, 'storage');
>>> my $format = extract_param($param, 'format');
>>> + die "either set storage or target-vmid, but not both\n"
>>> + if $storeid && $target_vmid;
>>> +
>>> +
>>> my $storecfg = PVE::Storage::config();
>>> + my $source_volid;
>>> - my $updatefn = sub {
>>> + my $move_updatefn = sub {
>>> my $conf = PVE::QemuConfig->load_config($vmid);
>>> PVE::QemuConfig->check_lock($conf);
>>> @@ -3452,7 +3482,205 @@ __PACKAGE__->register_method({
>>> return $rpcenv->fork_worker('qmmove', $vmid, $authuser,
>>> $realcmd);
>>> };
>>> - return PVE::QemuConfig->lock_config($vmid, $updatefn);
>>> + my $load_and_check_reassign_configs = sub {
>>> + my $vmlist = PVE::Cluster::get_vmlist()->{ids};
>>> +
>>> + if ($vmlist->{$vmid}->{node} ne
>>> $vmlist->{$target_vmid}->{node}) {
>>> + die "Both VMs need to be on the same node
>>> $vmlist->{$vmid}->{node}) ".
>>> + "but target VM is on $vmlist->{$target_vmid}->{node}.\n";
>>> + }
>>> +
>>> + my $source_conf = PVE::QemuConfig->load_config($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 move disks from or to template VMs\n"
>>> + if ($source_conf->{template} || $target_conf->{template});
>>> +
>>> + if ($digest) {
>>> + eval { PVE::Tools::assert_if_modified($digest,
>>> $source_conf->{digest}) };
>>> + if (my $err = $@) {
>>> + die "VM ${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 '${disk}' for VM '$vmid' does not exist\n"
>>> + if !defined($source_conf->{$disk});
>>> +
>>> + die "Target disk key '${target_disk}' is already in use for
>>> VM '$target_vmid'\n"
>>> + if exists($target_conf->{$target_disk});
>>> +
>>> + my $drive = PVE::QemuServer::parse_drive(
>>> + $disk,
>>> + $source_conf->{$disk},
>>> + );
>>> + $source_volid = $drive->{file};
>>> +
>>> + die "disk '${disk}' has no associated volume\n"
>>> + if !$source_volid;
>>> + die "CD drive contents can't be moved to another VM\n"
>>> + if PVE::QemuServer::drive_is_cdrom($drive, 1);
>>> + die "Can't move physical disk to another VM\n" if
>>> $source_volid =~ m|^/dev/|;
>>> + if (PVE::QemuServer::Drive::is_volume_in_use(
>>> + $storecfg,
>>> + $source_conf,
>>> + $disk,
>>> + $source_volid,
>>> + )) {
>>> + die "Can't move disk used by a snapshot to another VM\n"
>>> + }
>>
>> This looks weird to me style-wise. Also missing semicolon after die.
>
> Yeah, no matter how, it either looks weird or will be a few characters
> over the 100 limit...
> For the sake of readability I think I'll opt for the slightly too long
> post if variant.
>
The style guide doesn't have an explicit example of a long if with a
function call, but the two examples for long if conditions (at the end
of the "Wrapping post-if section") [0] look different.
To match one of those, you could either:
1. move the '{' to its own line.
2. use
if (
function call
) {
But maybe the current style is also acceptable?
[0]: https://pve.proxmox.com/wiki/Perl_Style_Guide#Wrapping_Post-If
>>
>>> +
>>> + if (!PVE::Storage::volume_has_feature(
>>> + $storecfg,
>>> + 'rename',
>>> + $source_volid,
>>> + )) {
>>> + die "Storage does not support moving of this disk to another
>>> VM\n"
>>> + }
>>
>> Same here, but this time the if-condition could fit on one line within
>> the 100 character limit ;) Again, missing semicolon.
>
> You are right, switchting this to post if
>
Could also be a normal if with the condition on one line, but both are fine.
>>
>>> +
>>> + die "Cannot move disk to another VM while the source VM is
>>> running\n"
>>> + if PVE::QemuServer::check_running($vmid) && $disk !~
>>> m/^unused\d+$/;
>>> +
>>> + if ($target_disk !~ m/^unused\d+$/ && $target_disk =~
>>> m/^([^\d]+)\d+$/) {
>>> + my $interface = $1;
>>
>> Nit: Isn't the interface already present in the result from parse_drive?
>
> The previous parse_drive is done on the source disk. Here we are
> checking against the target disk which can use a different config key /
> interface. Also we cannot use parse_drive for the target disk using the
> source_conf for the data as it will just return undef in case the
> parse_property_string fails, which is exactly what we are trying to set
> up here so that we can check if the target disk key supports all config
> options, and if not, present them to the user, so they have an idea why
> it does not work.
>
Right, there are potentially two different interfaces. Please just
ignore my wrong suggestion.
>>
>>> + my $desc =
>>> PVE::JSONSchema::get_standard_option("pve-qm-${interface}");
>>> + eval {
>>> + PVE::JSONSchema::parse_property_string(
>>> + $desc->{format},
>>> + $source_conf->{$disk},
>>> + )
>>> + };
>>> + if (my $err = $@) {
>>> + die "Cannot move disk to another VM: ${err}";
>>> + }
>>> + }
>>> +
>>> + my $repl_conf = PVE::ReplicationConfig->new();
>>> + my $is_replicated =
>>> $repl_conf->check_for_existing_jobs($target_vmid, 1);
>>> + my ($storeid, undef) =
>>> PVE::Storage::parse_volume_id($source_volid);
>>> + my $format = (PVE::Storage::parse_volname($storecfg,
>>> $source_volid))[6];
>>> + if ($is_replicated &&
>>> !PVE::Storage::storage_can_replicate($storecfg, $storeid, $format)) {
>>> + die "Cannot move disk to a replicated VM. Storage does not
>>> support replication!\n";
>>> + }
>>> +
>>> + return ($source_conf, $target_conf);
>>> + };
>>> +
>>> + my $logfunc = sub {
>>> + my ($msg) = @_;
>>> + print STDERR "$msg\n";
>>> + };
>>> +
>>> + my $disk_reassignfn = sub {
>>> + return PVE::QemuConfig->lock_config($vmid, sub {
>>> + return PVE::QemuConfig->lock_config($target_vmid, sub {
>>> + my ($source_conf, $target_conf) =
>>> &$load_and_check_reassign_configs();
>>> +
>>> + my $drive_param = PVE::QemuServer::parse_drive(
>>> + $target_disk,
>>> + $source_conf->{$disk},
>>> + );
>>> +
>>> + print "moving disk '$disk' from VM '$vmid' to
>>> '$target_vmid'\n";
>>> + my ($storeid, $source_volname) =
>>> PVE::Storage::parse_volume_id($source_volid);
>>> +
>>> + my (
>>> + undef,
>>> + undef,
>>> + undef,
>>> + undef,
>>> + undef,
>>> + undef,
>>> + $fmt
>>> + ) = PVE::Storage::parse_volname($storecfg, $source_volid);
>>
>> Nit: using
>> my $fmt = (PVE::Storage::parse_volname($storecfg,
>> $source_volid))[6];
>> like above is shorter.
>
> thx!
>
>>
>>> +
>>> + my $target_volname = PVE::Storage::find_free_volname(
>>> + $storecfg,
>>> + $storeid,
>>> + $target_vmid,
>>> + $fmt
>>> + );
>>> +
>>> + my $new_volid = PVE::Storage::rename_volume(
>>> + $storecfg,
>>> + $source_volid,
>>> + $target_volname,
>>> + );
>>> +
>>> + $drive_param->{file} = $new_volid;
>>> +
>>> + delete $source_conf->{$disk};
>>> + print "removing disk '${disk}' from VM '${vmid}' config\n";
>>> + PVE::QemuConfig->write_config($vmid, $source_conf);
>>> +
>>> + my $drive_string =
>>> PVE::QemuServer::print_drive($drive_param);
>>> + &$update_vm_api(
>>> + {
>>> + node => $node,
>>> + vmid => $target_vmid,
>>> + digest => $target_digest,
>>> + $target_disk => $drive_string,
>>> + },
>>> + 1,
>>> + );
>>> +
>>> + # remove possible replication snapshots
>>> + if (PVE::Storage::volume_has_feature(
>>> + $storecfg,
>>> + 'replicate',
>>> + $source_volid),
>>> + ) {
>>> + eval {
>>> + PVE::Replication::prepare(
>>> + $storecfg,
>>> + [$new_volid],
>>> + undef,
>>> + 1,
>>> + undef,
>>> + $logfunc,
>>> + )
>>> + };
>>> + if (my $err = $@) {
>>> + print "Failed to remove replication snapshots on
>>> moved disk " .
>>> + "'$target_disk'. Manual cleanup could be necessary.\n";
>>> + }
>>> + }
>>> + });
>>> + });
>>> + };
>>> +
>>> + if ($target_vmid) {
>>> + $rpcenv->check_vm_perm($authuser, $target_vmid, undef,
>>> ['VM.Config.Disk'])
>>> + if $authuser ne 'root at pam';
>>> +
>>> + die "Moving a disk to the same VM is not possible. Did you
>>> mean to ".
>>> + "move the disk to a different storage?\n"
>>> + if $vmid eq $target_vmid;
>>> +
>>> + &$load_and_check_reassign_configs();
>>> + return $rpcenv->fork_worker(
>>> + 'qmmove',
>>> + "${vmid}-${disk}>${target_vmid}-${target_disk}",
>>> + $authuser,
>>> + $disk_reassignfn
>>> + );
>>> + } elsif ($storeid) {
>>> + die "cannot move disk '$disk', only configured disks can be
>>> moved to another storage\n"
>>> + if $disk =~ m/^unused\d+$/;
>>> + return PVE::QemuConfig->lock_config($vmid, $move_updatefn);
>>> + } else {
>>> + die "Provide either a 'storage' to move the disk to a
>>> different " .
>>> + "storage or 'target-vmid' and 'target-disk' to move the disk
>>> " .
>>> + "to another VM\n";
>>> + }
>>> }});
>>> my $check_vm_disks_local = sub {
>>> diff --git a/PVE/CLI/qm.pm b/PVE/CLI/qm.pm
>>> index ef99b6d..a92d301 100755
>>> --- a/PVE/CLI/qm.pm
>>> +++ b/PVE/CLI/qm.pm
>>> @@ -910,7 +910,7 @@ our $cmddef = {
>>> resize => [ "PVE::API2::Qemu", 'resize_vm', ['vmid', 'disk',
>>> 'size'], { node => $nodename } ],
>>> - 'move-disk' => [ "PVE::API2::Qemu", 'move_vm_disk', ['vmid',
>>> 'disk', 'storage'], { node => $nodename }, $upid_exit ],
>>> + 'move-disk' => [ "PVE::API2::Qemu", 'move_vm_disk', ['vmid',
>>> 'disk', 'storage', 'target-vmid', 'target-disk'], { node => $nodename
>>> }, $upid_exit ],
>>> move_disk => { alias => 'move-disk' },
>>> unlink => [ "PVE::API2::Qemu", 'unlink', ['vmid'], { node =>
>>> $nodename } ],
>>>
More information about the pve-devel
mailing list