From mboxrd@z Thu Jan 1 00:00:00 1970 Return-Path: Received: from gate001.proxmox.com (gate001.proxmox.com [IPv6:2a0f:8001:1:32::40]) by lore.proxmox.com (Postfix) with ESMTPS id E52911FF0C0 for ; Wed, 04 Nov 2026 20:09:01 +0100 (CET) Received: from gate001.proxmox.com (localhost.localdomain [127.0.0.1]) by gate001.proxmox.com (Proxmox) with ESMTP id BF02A21A4F; Wed, 04 Nov 2026 20:05:30 +0100 (CET) From: =?UTF-8?q?Michael=20K=C3=B6ppl?= To: pve-devel@lists.proxmox.com Subject: [PATCH e2e-tests 34/38] tests: storage: verify moving guest volumes between storages Date: Wed, 4 Nov 2026 20:03:41 +0100 Message-ID: <20261104190345.2573464-35-m.koeppl@proxmox.com> X-Mailer: git-send-email 2.47.3 In-Reply-To: <20261104190345.2573464-1-m.koeppl@proxmox.com> References: <20261104190345.2573464-1-m.koeppl@proxmox.com> MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit X-Bm-Milter-Handled: 55990f41-d878-4baa-be0a-ee34c49e34d2 X-Bm-Transport-Timestamp: 1793819036657 X-SPAM-LEVEL: Spam detection results: 0 AWL 0.217 Adjusted score from AWL reputation of From: address DMARC_MISSING 0.1 Missing DMARC policy KAM_DMARC_STATUS 0.01 Test Rule for DKIM or SPF Failure with Strict Alignment (newer systems) RCVD_IN_DNSWL_MED -2.3 Sender listed at https://www.dnswl.org/, medium trust SPF_HELO_NONE 0.001 SPF: HELO does not publish an SPF Record SPF_PASS -0.001 SPF: sender matches SPF record Message-ID-Hash: KZLX2KFWE6WTAQA3IVUTCI3JJUJZ6TXM X-Message-ID-Hash: KZLX2KFWE6WTAQA3IVUTCI3JJUJZ6TXM X-MailFrom: m.koeppl@proxmox.com X-Mailman-Rule-Misses: dmarc-mitigation; no-senders; approved; loop; banned-address; emergency; member-moderation; nonmember-moderation; administrivia; implicit-dest; max-recipients; max-size; news-moderation; no-subject; digests; suspicious-header X-Mailman-Version: 3.3.10 Precedence: list List-Id: Proxmox VE development discussion List-Help: List-Owner: List-Post: List-Subscribe: List-Unsubscribe: Move the disk of a running VM from a directory storage to local-lvm and delete the source, then move it back as qcow2 while keeping the source, which must be kept as an unused volume. The disk content is compared against a copy taken before the moves. Also move the root file system of a stopped container, which must start from the moved volume with its data intact, and check that moving a volume of a running container is refused. Signed-off-by: Michael Köppl --- tests/pve_move_disks.pl | 262 ++++++++++++++++++++++++++++++++++++++++ tests/testcases.ron | 9 ++ 2 files changed, 271 insertions(+) create mode 100755 tests/pve_move_disks.pl diff --git a/tests/pve_move_disks.pl b/tests/pve_move_disks.pl new file mode 100755 index 0000000..59dd780 --- /dev/null +++ b/tests/pve_move_disks.pl @@ -0,0 +1,262 @@ +#!/usr/bin/perl + +=head1 NAME + +C - Test moving guest volumes between storages. + +=head1 DESCRIPTION + +This testcase creates a VM with a disk on a temporary directory storage, writes +random data to the disk and keeps a copy of it. While the VM is running, it moves +the disk to local-lvm and deletes the source, then moves it back to the directory +storage in qcow2 format and keeps the source, which must then be kept as an +unused volume. Afterwards, the moved disk must still have the original content. + +It also creates a container on local-lvm and writes a file to it. Moving the +volume of the running container must be refused. Once the container is stopped, +its root file system is moved to the directory storage, and the container must +start from the moved volume with the file intact. + +=cut + +use strict; +use warnings; + +use Test::More; + +use lib '.'; +use Proxmox::Test::Helper qw(CONTAINER_TEMPLATE get_pve_instance); +use Proxmox::Test::Testcase; + +my $pve = get_pve_instance('pve1'); +my $node = $pve->get_nodename(); +my $client = $pve->client(); + +my $dir_storage = 'e2e-move'; +my $dir_storage_path = '/var/lib/e2e-move-store'; + +my $vmid; +my $ctid; +my $reference_dir; +my $marker = 'e2e-move-volume'; + +my $config = sub { + my ($type, $id) = @_; + return $client->get("/nodes/$node/$type/$id/config", {}); +}; + +my $volid_of = sub { + my ($value) = @_; + my ($volid) = ($value // '') =~ m/^([^,]+)/; + return $volid; +}; + +my $volume = sub { + my ($storage, $id, $volid) = @_; + my $content = $client->get("/nodes/$node/storage/$storage/content", { vmid => $id }); + my ($entry) = grep { $_->{volid} eq $volid } $content->@*; + return $entry; +}; + +my $move = sub { + my ($path, $params, $name) = @_; + my $upid = $client->post($path, $params); + $pve->task_ok($upid, $name) or die "$name failed\n"; +}; + +Proxmox::Test::Testcase->new('pve_move_disks')->with([{}])->step( + 'create directory storage', + sub { + $pve->run_command("mkdir -p $dir_storage_path"); + $client->post( + '/storage', + { + storage => $dir_storage, + type => 'dir', + path => $dir_storage_path, + content => 'images,rootdir', + }, + ); + }, + sub { + eval { $client->delete("/storage/$dir_storage") }; + $pve->run_command("rm -rf $dir_storage_path"); + }, +)->step( + 'create VM', + sub { + $vmid = $client->get('/cluster/nextid'); + my $upid = $client->post( + "/nodes/$node/qemu", + { + vmid => $vmid, + name => 'vm-move-disk', + memory => 256, + scsihw => 'virtio-scsi-pci', + scsi0 => "$dir_storage:1,format=raw", + }, + ); + $pve->task_ok($upid, "created VM $vmid") or die "creating VM $vmid failed\n"; + }, + sub { + $pve->destroy_guest('qemu', $vmid) if defined($vmid); + }, +)->step( + 'write data to the disk and keep a copy', + sub { + my $volid = $volid_of->($config->('qemu', $vmid)->{scsi0}); + my ($path) = $pve->run_command("pvesm path $volid"); + chomp($path); + + $pve->run_command("dd if=/dev/urandom of=$path bs=1M count=8 seek=16 conv=notrunc"); + + ($reference_dir) = $pve->run_command('mktemp -d'); + chomp($reference_dir); + $pve->run_command("cp --sparse=always $path $reference_dir/disk.raw"); + }, + sub { + $pve->run_command("rm -rf $reference_dir") if defined($reference_dir); + }, +)->step( + 'start VM', + sub { + $pve->start_guest('qemu', $vmid); + }, +)->step( + 'move disk of the running VM and delete the source', + sub { + my $source = $volid_of->($config->('qemu', $vmid)->{scsi0}); + + $move->( + "/nodes/$node/qemu/$vmid/move_disk", + { disk => 'scsi0', storage => 'local-lvm', delete => 1 }, + 'moved scsi0 to local-lvm', + ); + + my $conf = $config->('qemu', $vmid); + like($conf->{scsi0} // '', qr/^local-lvm:/, 'scsi0 is on local-lvm'); + ok(!$volume->($dir_storage, $vmid, $source), 'source volume is deleted'); + ok(!(grep { m/^unused\d+$/ } keys $conf->%*), 'no unused volume is left'); + is( + $client->get("/nodes/$node/qemu/$vmid/status/current", {})->{status}, + 'running', + 'VM keeps running during the move', + ); + }, +)->step( + 'move disk of the running VM to qcow2 and keep the source', + sub { + my $source = $volid_of->($config->('qemu', $vmid)->{scsi0}); + + $move->( + "/nodes/$node/qemu/$vmid/move_disk", + { disk => 'scsi0', storage => $dir_storage, format => 'qcow2' }, + "moved scsi0 to $dir_storage as qcow2", + ); + + my $conf = $config->('qemu', $vmid); + my $target = $volid_of->($conf->{scsi0}); + like($target // '', qr/^\Q$dir_storage\E:/, "scsi0 is on $dir_storage"); + is( + ($volume->($dir_storage, $vmid, $target) // {})->{format}, + 'qcow2', + 'moved volume has the qcow2 format', + ); + + my ($unused) = grep { m/^unused\d+$/ && $conf->{$_} eq $source } keys $conf->%*; + ok(defined($unused), 'source volume is kept as an unused volume'); + ok($volume->('local-lvm', $vmid, $source), 'source volume still exists'); + }, +)->step( + 'stop VM', + sub { + $pve->stop_guest('qemu', $vmid); + }, +)->step( + 'verify disk content', + sub { + my $volid = $volid_of->($config->('qemu', $vmid)->{scsi0}); + my ($path) = $pve->run_command("pvesm path $volid"); + chomp($path); + + ok( + eval { + $pve->run_command( + "qemu-img compare -f raw -F qcow2 $reference_dir/disk.raw $path"); + 1; + }, + 'moved disk has the original content', + ) or diag($@); + }, +)->step( + 'create container', + sub { + $ctid = $client->get('/cluster/nextid'); + my $upid = $client->post( + "/nodes/$node/lxc", + { + vmid => $ctid, + ostemplate => CONTAINER_TEMPLATE, + rootfs => 'local-lvm:2', + memory => 256, + unprivileged => 1, + features => 'nesting=1', + hostname => 'ct-move-volume', + }, + ); + $pve->task_ok($upid, "created container $ctid") + or die "creating container $ctid failed\n"; + }, + sub { + $pve->destroy_guest('lxc', $ctid) if defined($ctid); + }, +)->step( + 'write file in the running container', + sub { + $pve->start_guest('lxc', $ctid); + $pve->ct_exec($ctid, "sh -c 'echo $marker > /root/marker'"); + + eval { + $client->post( + "/nodes/$node/lxc/$ctid/move_volume", + { volume => 'rootfs', storage => $dir_storage }, + ); + }; + ok($@, 'moving the root file system of the running container fails'); + like( + $config->('lxc', $ctid)->{rootfs} // '', + qr/^local-lvm:/, + 'root file system is still on local-lvm', + ); + + $pve->stop_guest('lxc', $ctid); + }, +)->step( + 'move root file system of the stopped container', + sub { + my $source = $volid_of->($config->('lxc', $ctid)->{rootfs}); + + $move->( + "/nodes/$node/lxc/$ctid/move_volume", + { volume => 'rootfs', storage => $dir_storage, delete => 1 }, + "moved rootfs to $dir_storage", + ); + + like( + $config->('lxc', $ctid)->{rootfs} // '', + qr/^\Q$dir_storage\E:/, + "rootfs is on $dir_storage", + ); + ok(!$volume->('local-lvm', $ctid, $source), 'source volume is deleted'); + + $pve->start_guest('lxc', $ctid); + is( + $pve->ct_exec($ctid, 'cat /root/marker'), + "$marker\n", + 'file is intact after the move', + ); + $pve->stop_guest('lxc', $ctid); + }, +)->run(); + +done_testing(); diff --git a/tests/testcases.ron b/tests/testcases.ron index 51ceb81..f664c62 100644 --- a/tests/testcases.ron +++ b/tests/testcases.ron @@ -214,6 +214,15 @@ TestcaseConfig( ], tag: "extended", ), + Testcase( + name: "pve_move_disks", + run: "./tests/pve_move_disks.pl", + timeout: 600, + instances: [ + "pve1", + ], + tag: "extended", + ), Testcase( name: "pve_ha_resource_config", run: "./tests/pve_ha_resource_config.pl", -- 2.47.3