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 113291FF0C0 for ; Wed, 04 Nov 2026 20:06:43 +0100 (CET) Received: from gate001.proxmox.com (localhost.localdomain [127.0.0.1]) by gate001.proxmox.com (Proxmox) with ESMTP id E586F217C8; Wed, 04 Nov 2026 20:05:23 +0100 (CET) From: =?UTF-8?q?Michael=20K=C3=B6ppl?= To: pve-devel@lists.proxmox.com Subject: [PATCH e2e-tests 16/38] tests: container: verify snapshots, clones, templates Date: Wed, 4 Nov 2026 20:03:23 +0100 Message-ID: <20261104190345.2573464-17-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: 1793819034781 X-SPAM-LEVEL: Spam detection results: 0 AWL 0.221 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: D7EBUECPW7JJYQDGU4P637S7TXHEJ4AY X-Message-ID-Hash: D7EBUECPW7JJYQDGU4P637S7TXHEJ4AY 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: Snapshot a running container and roll it back with 'start' set and create a full clone, which must keep the data but get its own hostname and machine ID. Also create a linked clone of a template. On LVM-thin, a linked clone does not depend on its origin, so the template can be destroyed while the clone exists. Check that the clone keeps working afterwards. Signed-off-by: Michael Köppl --- tests/pve_ct_snapshot_clone.pl | 267 +++++++++++++++++++++++++++++++++ tests/testcases.ron | 9 ++ 2 files changed, 276 insertions(+) create mode 100755 tests/pve_ct_snapshot_clone.pl diff --git a/tests/pve_ct_snapshot_clone.pl b/tests/pve_ct_snapshot_clone.pl new file mode 100755 index 0000000..e9119e8 --- /dev/null +++ b/tests/pve_ct_snapshot_clone.pl @@ -0,0 +1,267 @@ +#!/usr/bin/perl + +=head1 NAME + +C - Test snapshots, clones and templates of a container. + +=head1 DESCRIPTION + +This testcase creates an unprivileged container on local-lvm, writes a marker +file and takes a snapshot of the running container. After changing the marker, +it rolls the snapshot back and verifies that the original content is restored, +then deletes the snapshot. + +Afterwards, it creates a full clone and checks that the clone carries the data +but gets its own hostname and machine ID. Finally, it converts the container to +a template and creates a linked clone backed by the template's base volume. The +template can then be destroyed and the linked clone must keep its data. + +=cut + +use strict; +use warnings; + +use Test::More; + +use lib '.'; +use Proxmox::Test::Helper qw(CONTAINER_TEMPLATE get_pve_instance); +use Proxmox::Test::Testcase; +use Proxmox::Test::Util qw(poll_until); + +my $pve = get_pve_instance('pve1'); +my $node = $pve->get_nodename(); +my $client = $pve->client(); + +my $snapname = 'e2esnap'; + +my $test_container = { + vmid => $client->get('/cluster/nextid'), + node => $node, + ostemplate => CONTAINER_TEMPLATE, + rootfs => 'local-lvm:2', + storage => 'local-lvm', + memory => 256, + unprivileged => 1, + features => 'nesting=1', + hostname => 'ct-snapshot', +}; + +my $vmid = $test_container->{vmid}; +my $full_clone = { vmid => undef, hostname => 'ct-clone-full' }; +my $linked_clone = { vmid => undef, hostname => 'ct-clone-linked' }; +my $machine_id; + +my $snapshot_names = sub { + my $snapshots = $client->get("/nodes/$node/lxc/$vmid/snapshot", {}); + return map { $_->{name} } grep { $_->{name} ne 'current' } $snapshots->@*; +}; + +my $create_clone = sub { + my ($clone, $full) = @_; + + $clone->{vmid} = $client->get('/cluster/nextid'); + my $upid = $client->post( + "/nodes/$node/lxc/$vmid/clone", + { + newid => $clone->{vmid}, + hostname => $clone->{hostname}, + full => $full, + ($full ? (storage => 'local-lvm') : ()), + }, + ); + $pve->task_ok($upid, ($full ? 'full' : 'linked') . " clone $clone->{vmid} created") + or die "cloning container $vmid failed\n"; +}; + +Proxmox::Test::Testcase->new('pve_ct_snapshot_clone')->with([{}])->step( + 'create container', + sub { + my $upid = $client->post("/nodes/$node/lxc", $test_container); + $pve->task_ok($upid, "created container $vmid") + or die "creating container $vmid failed\n"; + }, + sub { + $pve->destroy_guest('lxc', $vmid); + }, +)->step( + 'start container and write marker', + sub { + $pve->start_guest('lxc', $vmid); + $pve->ct_exec($vmid, "sh -c 'echo before-snapshot > /root/marker'"); + + # Required later to check if the full clone has a different machine ID + $machine_id = $pve->ct_exec($vmid, 'cat /etc/machine-id'); + }, +)->step( + 'create snapshot of running container', + sub { + my $upid = $client->post("/nodes/$node/lxc/$vmid/snapshot", { snapname => $snapname }); + $pve->task_ok($upid, "created snapshot '$snapname'") + or die "creating snapshot '$snapname' failed\n"; + + ok((grep { $_ eq $snapname } $snapshot_names->()), "snapshot '$snapname' is listed"); + }, +)->step( + 'rollback snapshot', + sub { + $pve->ct_exec($vmid, "sh -c 'echo after-snapshot > /root/marker'"); + + # Rolling back stops the running container, 'start' brings it back afterwards in a separate + # task. + my $upid = + $client->post("/nodes/$node/lxc/$vmid/snapshot/$snapname/rollback", { start => 1 }); + $pve->task_ok($upid, "rolled back to snapshot '$snapname'") + or die "rolling back snapshot '$snapname' failed\n"; + + my $start_upid = $pve->find_spawned_task($upid, 'vzstart'); + $pve->task_ok($start_upid, "started container $vmid after the rollback") + or die "starting container $vmid after the rollback failed\n"; + + poll_until( + sub { + $client->get("/nodes/$node/lxc/$vmid/status/current", {})->{status} eq + 'running'; + }, + # A status query can take some time after teh start, so allow more than usual. + timeout => 120, + description => "container $vmid to run again after the rollback", + ); + is( + $pve->ct_exec($vmid, 'cat /root/marker'), + "before-snapshot\n", + 'rollback restored the content from the time of the snapshot', + ); + }, +)->step( + 'delete snapshot', + sub { + my $upid = $client->delete("/nodes/$node/lxc/$vmid/snapshot/$snapname", {}); + $pve->task_ok($upid, "deleted snapshot '$snapname'") + or die "deleting snapshot '$snapname' failed\n"; + + ok(!(grep { $_ eq $snapname } $snapshot_names->()), "snapshot '$snapname' is gone"); + }, +)->step( + 'stop container', + sub { + $pve->stop_guest('lxc', $vmid); + }, +)->step( + 'create full clone', + sub { + $create_clone->($full_clone, 1); + }, + sub { + $pve->destroy_guest('lxc', $full_clone->{vmid}) if defined($full_clone->{vmid}); + }, +)->step( + 'verify full clone', + sub { + my $id = $full_clone->{vmid}; + + my $conf = $client->get("/nodes/$node/lxc/$id/config", {}); + is($conf->{hostname}, $full_clone->{hostname}, 'full clone has its own hostname'); + like( + $conf->{rootfs} // '', + qr/^local-lvm:vm-$id-disk-\d+\b/, + 'full clone has own rootfs', + ); + + $pve->start_guest('lxc', $id); + is( + $pve->ct_exec($id, 'cat /root/marker'), + "before-snapshot\n", + 'full clone has the data', + ); + is( + $pve->ct_exec($id, 'cat /etc/hostname'), + "$full_clone->{hostname}\n", + 'hostname is set up inside the full clone', + ); + + # Cloning clears the machine ID, so the guest generates a new one on its first boot. + my $clone_machine_id = $pve->ct_exec($id, 'cat /etc/machine-id'); + like($clone_machine_id, qr/^[0-9a-f]{32}$/, 'full clone has a machine ID'); + isnt($clone_machine_id, $machine_id, 'full clone has a different machine ID'); + + $pve->stop_guest('lxc', $id); + }, +)->step( + 'convert container to template', + sub { + # Conversion runs in a worker task, but the API call does not return its task ID. + $client->post("/nodes/$node/lxc/$vmid/template", {}); + + my $conf; + eval { + poll_until( + sub { + $conf = $client->get("/nodes/$node/lxc/$vmid/config", {}); + return $conf->{template}; + }, + timeout => 60, + interval => 2, + description => "container $vmid to be converted to a template", + ); + }; + is($conf->{template}, 1, 'container is marked as template'); + like( + $conf->{rootfs} // '', + qr/^local-lvm:base-$vmid-disk-\d+\b/, + 'template rootfs was converted to a base volume', + ); + }, +)->step( + 'create linked clone', + sub { + $create_clone->($linked_clone, 0); + }, + sub { + $pve->destroy_guest('lxc', $linked_clone->{vmid}) if defined($linked_clone->{vmid}); + }, +)->step( + 'verify linked clone', + sub { + my $id = $linked_clone->{vmid}; + + my $conf = $client->get("/nodes/$node/lxc/$id/config", {}); + my ($volname) = ($conf->{rootfs} // '') =~ m/^local-lvm:([^,]+)/ + or die "linked clone $id has no rootfs on local-lvm\n"; + + my $vgname = $client->get('/storage/local-lvm', {})->{vgname}; + my ($origin) = $pve->run_command("lvs --noheadings -o origin $vgname/$volname"); + like( + $origin // '', + qr/^\s*base-$vmid-disk-\d+\s*$/, + 'linked clone rootfs is a thin snapshot of the template base volume', + ); + + $pve->start_guest('lxc', $id); + is( + $pve->ct_exec($id, 'cat /root/marker'), + "before-snapshot\n", + 'linked clone has the data', + ); + $pve->stop_guest('lxc', $id); + }, +)->step( + 'linked clone survives destroying the template', + sub { + # A snapshot on LVM-thin does not depend on its origin volume, so the template can be + # destroyed while linked clones of it exist and they must keep working. + my $upid = $client->delete("/nodes/$node/lxc/$vmid", {}); + $pve->task_ok($upid, "destroyed template $vmid") + or die "destroying the template failed\n"; + + my $id = $linked_clone->{vmid}; + $pve->start_guest('lxc', $id); + is( + $pve->ct_exec($id, 'cat /root/marker'), + "before-snapshot\n", + 'linked clone still has the data after the template is gone', + ); + $pve->stop_guest('lxc', $id); + }, +)->run(); + +done_testing(); diff --git a/tests/testcases.ron b/tests/testcases.ron index 79841ca..c0369e4 100644 --- a/tests/testcases.ron +++ b/tests/testcases.ron @@ -160,6 +160,15 @@ TestcaseConfig( ], tag: "extended", ), + Testcase( + name: "pve_ct_snapshot_clone", + run: "./tests/pve_ct_snapshot_clone.pl", + timeout: 600, + instances: [ + "pve1", + ], + tag: "extended", + ), Testcase( name: "pve_ha_resource_config", run: "./tests/pve_ha_resource_config.pl", -- 2.47.3