From: "Michael Köppl" <m.koeppl@proxmox.com>
To: pve-devel@lists.proxmox.com
Subject: [PATCH e2e-tests 20/38] tests: vm: verify snapshots, clones, templates
Date: Wed, 4 Nov 2026 20:03:27 +0100 [thread overview]
Message-ID: <20261104190345.2573464-21-m.koeppl@proxmox.com> (raw)
In-Reply-To: <20261104190345.2573464-1-m.koeppl@proxmox.com>
Take a snapshot of a running VM including its RAM and check that a
rollback restores the config from the time of the snapshot and
resumes the VM right away, and that deleting the snapshot removes
the saved RAM.
Clone the running VM and a template made from it, and check that
each clone gets its own disk and a new MAC address, SMBIOS UUID and
VM generation ID, so that the clones can be told apart. Check that the
template itself can no longer be started.
The VM has no OS, so the test does not check disk contents.
Signed-off-by: Michael Köppl <m.koeppl@proxmox.com>
---
tests/pve_vm_snapshot_clone.pl | 282 +++++++++++++++++++++++++++++++++
tests/testcases.ron | 9 ++
2 files changed, 291 insertions(+)
create mode 100755 tests/pve_vm_snapshot_clone.pl
diff --git a/tests/pve_vm_snapshot_clone.pl b/tests/pve_vm_snapshot_clone.pl
new file mode 100755
index 0000000..69eee60
--- /dev/null
+++ b/tests/pve_vm_snapshot_clone.pl
@@ -0,0 +1,282 @@
+#!/usr/bin/perl
+
+=head1 NAME
+
+C<pve_vm_snapshot_clone> - Test snapshots, clones and templates of a VM.
+
+=head1 DESCRIPTION
+
+This testcase creates a VM with a disk on local-lvm and a network device, starts
+it and takes a snapshot with RAM. After changing, adding and removing options and
+leaving a change pending, it rolls the snapshot back and verifies that the VM
+runs again right away (snapshot with RAM), that the config from the time of the
+snapshot is restored with a new VM generation ID, and that the pending change is
+discarded. Deleting the snapshot must also remove the saved VM state.
+
+Afterwards, it creates a full clone of the running VM and checks that the clone
+has its own disk and its own identity (i.e. MAC address, SMBIOS UUID, VM
+generation ID). Finally, it converts the VM to a template and creates a linked
+clone of the template, which must get its own identity as well and start.
+
+The testcase does not check disk contents, as the VM has no operating system.
+
+=cut
+
+use strict;
+use warnings;
+
+use Test::More;
+
+use lib '.';
+use Proxmox::Test::Helper qw(get_pve_instance);
+use Proxmox::Test::Testcase;
+
+my $pve = get_pve_instance('pve1');
+my $node = $pve->get_nodename();
+my $client = $pve->client();
+
+my $snapname = 'e2esnap';
+
+my $test_vm = {
+ vmid => $client->get('/cluster/nextid'),
+ name => 'vm-snapshot',
+ memory => 256,
+ scsihw => 'virtio-scsi-pci',
+ scsi0 => 'local-lvm:1',
+ net0 => 'virtio,bridge=vmbr0',
+ tags => 'e2e',
+};
+
+my $vmid = $test_vm->{vmid};
+my $full_clone = { vmid => undef, name => 'vm-clone-full' };
+my $linked_clone = { vmid => undef, name => 'vm-clone-linked' };
+my $vmstate;
+my $snapshot_conf;
+
+my $vm_config = sub {
+ my ($id) = @_;
+ return $client->get("/nodes/$node/qemu/$id/config", {});
+};
+
+# The config in effect, without pending changes applied.
+my $current_config = sub {
+ return $client->get("/nodes/$node/qemu/$vmid/config", { current => 1 });
+};
+
+my $pending_changes = sub {
+ my $pending = $client->get("/nodes/$node/qemu/$vmid/pending", {});
+ return {
+ map { $_->{key} => 1 }
+ grep { defined($_->{pending}) || $_->{delete} } $pending->@*
+ };
+};
+
+my $vm_identity = sub {
+ my ($conf) = @_;
+
+ my ($mac) = ($conf->{net0} // '') =~ m/^[^=,]+=([0-9A-Fa-f:]{17})\b/;
+ my ($uuid) = ($conf->{smbios1} // '') =~ m/\buuid=([0-9a-fA-F-]+)/;
+
+ return { mac => $mac, uuid => $uuid, vmgenid => $conf->{vmgenid} };
+};
+
+my $volume_exists = sub {
+ my ($volid, $owner) = @_;
+
+ my ($storage) = $volid =~ m/^([^:]+):/;
+ my $content = $client->get("/nodes/$node/storage/$storage/content", { vmid => $owner });
+ return grep { $_->{volid} eq $volid } $content->@*;
+};
+
+my $verify_clone = sub {
+ my ($clone, $source_conf) = @_;
+ my $id = $clone->{vmid};
+
+ my $conf = $vm_config->($id);
+ is($conf->{name}, $clone->{name}, "clone $id has its own name");
+ is($conf->{memory}, $source_conf->{memory}, "clone $id has the memory of the source");
+
+ my ($disk) = ($conf->{scsi0} // '') =~ m/^([^,]+)/;
+ my ($source_disk) = ($source_conf->{scsi0} // '') =~ m/^([^,]+)/;
+ ok(
+ defined($disk) && $disk ne $source_disk && $volume_exists->($disk, $id),
+ "clone $id has its own disk",
+ );
+
+ my $clone_identity = $vm_identity->($conf);
+ my $source_identity = $vm_identity->($source_conf);
+ for my $key (sort keys $clone_identity->%*) {
+ ok(defined($clone_identity->{$key}), "clone $id has a $key");
+ isnt($clone_identity->{$key}, $source_identity->{$key}, "clone $id has its own $key");
+ }
+
+ $pve->start_guest('qemu', $id);
+ $pve->stop_guest('qemu', $id);
+};
+
+Proxmox::Test::Testcase->new('pve_vm_snapshot_clone')->with([{}])->step(
+ 'create VM',
+ sub {
+ my $upid = $client->post("/nodes/$node/qemu", $test_vm);
+ $pve->task_ok($upid, "created VM $vmid") or die "creating VM $vmid failed\n";
+ },
+ sub {
+ $pve->destroy_guest('qemu', $vmid);
+ },
+)->step(
+ 'verify generated identity',
+ sub {
+ my $identity = $vm_identity->($vm_config->($vmid));
+
+ ok($identity->{mac}, 'a MAC address is generated for net0');
+ ok($identity->{uuid}, 'an SMBIOS UUID is generated');
+ ok($identity->{vmgenid}, 'a vmgenid is generated');
+ },
+)->step(
+ 'start VM',
+ sub {
+ $pve->start_guest('qemu', $vmid);
+ },
+)->step(
+ 'create snapshot including RAM',
+ sub {
+ my $upid = $client->post(
+ "/nodes/$node/qemu/$vmid/snapshot",
+ { snapname => $snapname, vmstate => 1 },
+ );
+ $pve->task_ok($upid, "created snapshot '$snapname' of the running VM")
+ or die "creating snapshot '$snapname' failed\n";
+
+ my ($snapshot) = grep { $_->{name} eq $snapname }
+ $client->get("/nodes/$node/qemu/$vmid/snapshot", {})->@*;
+ ok($snapshot, "snapshot '$snapname' is listed");
+ is($snapshot->{vmstate}, 1, "snapshot '$snapname' includes the RAM");
+
+ $vmstate =
+ $client->get("/nodes/$node/qemu/$vmid/snapshot/$snapname/config", {})->{vmstate};
+ ok(defined($vmstate) && $volume_exists->($vmstate, $vmid), 'RAM is saved to a volume');
+
+ $snapshot_conf = $current_config->();
+ },
+)->step(
+ 'change config after the snapshot',
+ sub {
+ # Changing the memory of a running VM without memory hotplug stays pending, the other
+ # changes apply right away.
+ $client->put(
+ "/nodes/$node/qemu/$vmid/config",
+ { name => 'vm-snapshot-changed', onboot => 1, delete => 'tags', memory => 512 },
+ );
+
+ my $conf = $current_config->();
+ is($conf->{name}, 'vm-snapshot-changed', 'name is changed');
+ is($conf->{onboot}, 1, 'onboot is added');
+ ok(!defined($conf->{tags}), 'tags are removed');
+ is_deeply($pending_changes->(), { memory => 1 }, 'memory change is pending');
+ },
+)->step(
+ 'rollback snapshot',
+ sub {
+ my ($since) = $pve->run_command('date +%s');
+ chomp($since);
+
+ my $upid = $client->post("/nodes/$node/qemu/$vmid/snapshot/$snapname/rollback", {});
+ $pve->task_ok($upid, "rolled back to snapshot '$snapname'")
+ or die "rolling back snapshot '$snapname' failed\n";
+
+ is(
+ $client->get("/nodes/$node/qemu/$vmid/status/current", {})->{status},
+ 'running',
+ 'VM runs again right after the rollback, as the snapshot includes its RAM',
+ );
+
+ my $conf = $current_config->();
+ isnt($conf->{vmgenid}, $snapshot_conf->{vmgenid}, 'rollback generates a new vmgenid');
+ my %expected = $snapshot_conf->%*;
+ # Rollback gives the VM a new generation ID, so this can never match.
+ delete $expected{$_} for qw(digest vmgenid);
+ delete $conf->{$_} for qw(digest vmgenid);
+ is_deeply(
+ $conf,
+ \%expected,
+ 'rollback restored the config from the time of the snapshot',
+ );
+ is_deeply($pending_changes->(), {}, 'rollback discarded the pending change');
+
+ $pve->wait_for_qemu_cleanup($vmid, $since);
+ },
+)->step(
+ 'delete snapshot',
+ sub {
+ my $upid = $client->delete("/nodes/$node/qemu/$vmid/snapshot/$snapname", {});
+ $pve->task_ok($upid, "deleted snapshot '$snapname'")
+ or die "deleting snapshot '$snapname' failed\n";
+
+ ok(
+ !(
+ grep { $_->{name} eq $snapname }
+ $client->get("/nodes/$node/qemu/$vmid/snapshot", {})->@*
+ ),
+ "snapshot '$snapname' is gone",
+ );
+ ok(!$volume_exists->($vmstate, $vmid), 'saved VM state is removed with the snapshot');
+ },
+)->step(
+ 'create full clone of the running VM',
+ sub {
+ $full_clone->{vmid} = $client->get('/cluster/nextid');
+ my $upid = $client->post(
+ "/nodes/$node/qemu/$vmid/clone",
+ { newid => $full_clone->{vmid}, name => $full_clone->{name}, full => 1 },
+ );
+ $pve->task_ok($upid, "created full clone $full_clone->{vmid}")
+ or die "cloning VM $vmid failed\n";
+ },
+ sub {
+ $pve->destroy_guest('qemu', $full_clone->{vmid}) if defined($full_clone->{vmid});
+ },
+)->step(
+ 'verify full clone',
+ sub {
+ $verify_clone->($full_clone, $vm_config->($vmid));
+ },
+)->step(
+ 'stop VM',
+ sub {
+ $pve->stop_guest('qemu', $vmid);
+ },
+)->step(
+ 'convert VM to template',
+ sub {
+ my $upid = $client->post("/nodes/$node/qemu/$vmid/template", {});
+ $pve->task_ok($upid, "converted VM $vmid to a template")
+ or die "converting VM $vmid to a template failed\n";
+
+ is($vm_config->($vmid)->{template}, 1, "VM $vmid is marked as template");
+
+ $upid = eval { $client->post("/nodes/$node/qemu/$vmid/status/start", {}) };
+ my $status = defined($upid) ? $pve->wait_for_task($upid, 1) : "API error: $@";
+ ok(!$pve->task_successful($status), 'starting the template is refused');
+ },
+)->step(
+ 'create linked clone',
+ sub {
+ $linked_clone->{vmid} = $client->get('/cluster/nextid');
+ my $upid = $client->post(
+ "/nodes/$node/qemu/$vmid/clone",
+ { newid => $linked_clone->{vmid}, name => $linked_clone->{name}, full => 0 },
+ );
+ $pve->task_ok($upid, "created linked clone $linked_clone->{vmid}")
+ or die "creating the linked clone failed\n";
+ },
+ sub {
+ $pve->destroy_guest('qemu', $linked_clone->{vmid}) if defined($linked_clone->{vmid});
+ },
+)->step(
+ 'verify linked clone',
+ sub {
+ $verify_clone->($linked_clone, $vm_config->($vmid));
+ },
+)->run();
+
+done_testing();
diff --git a/tests/testcases.ron b/tests/testcases.ron
index afbaa5b..e7f8917 100644
--- a/tests/testcases.ron
+++ b/tests/testcases.ron
@@ -187,6 +187,15 @@ TestcaseConfig(
],
tag: "extended",
),
+ Testcase(
+ name: "pve_vm_snapshot_clone",
+ run: "./tests/pve_vm_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
next prev parent reply other threads:[~2026-11-04 19:07 UTC|newest]
Thread overview: 47+ messages / expand[flat|nested] mbox.gz Atom feed top
2026-11-04 19:03 [PATCH e2e-tests/test-tools 00/38] tests: add tests for guests, Ceph, PBS and cluster workflows Michael Köppl
2026-11-04 19:03 ` [PATCH test-tools 01/38] ansible: rename snd_test bridge to tt_test as multi-purpose bridge Michael Köppl
2026-11-04 19:03 ` [PATCH e2e-tests 02/38] plans: rename sdn_test bridge to tt_test Michael Köppl
2026-11-04 19:03 ` [PATCH e2e-tests 03/38] debian: add missing jq dependency Michael Köppl
2026-11-04 19:03 ` [PATCH e2e-tests 04/38] test framework: move the default container template to the helper Michael Köppl
2026-11-04 19:03 ` [PATCH e2e-tests 05/38] lib: instance: add helper to check if an address answers ping Michael Köppl
2026-11-04 19:03 ` [PATCH e2e-tests 06/38] lib: pveinstance: look up disks by their SCSI serial Michael Köppl
2026-11-04 19:03 ` [PATCH e2e-tests 07/38] lib: pveinstance: add helper to find a task started by another task Michael Köppl
2026-11-04 19:03 ` [PATCH e2e-tests 08/38] lib: pveinstance: add helpers to start and stop guests Michael Köppl
2026-11-04 19:03 ` [PATCH e2e-tests 09/38] lib: pveinstance: add helper to run commands in containers Michael Köppl
2026-11-04 19:03 ` [PATCH e2e-tests 10/38] lib: pveinstance: add helper to wait for qmeventd's cleanup Michael Köppl
2026-11-04 19:03 ` [PATCH e2e-tests 11/38] lib: pveinstance: add helper to check if guest exists Michael Köppl
2026-11-04 19:03 ` [PATCH e2e-tests 12/38] lib: add helpers to use a PBS datastore as PVE storage Michael Köppl
2026-11-04 19:03 ` [PATCH e2e-tests 13/38] scripts: retry container template download, fail if it fails Michael Köppl
2026-11-04 19:03 ` [PATCH e2e-tests 14/38] tests: container: verify lifecycle and guest setup Michael Köppl
2026-11-04 19:03 ` [PATCH e2e-tests 15/38] tests: container: verify volume and bind mountpoints Michael Köppl
2026-11-04 19:03 ` [PATCH e2e-tests 16/38] tests: container: verify snapshots, clones, templates Michael Köppl
2026-11-04 19:03 ` [PATCH e2e-tests 17/38] tests: container: verify device passthrough Michael Köppl
2026-11-04 19:03 ` [PATCH e2e-tests 18/38] plans,scripts: build a minimal OCI image onto pve1 Michael Köppl
2026-11-04 19:03 ` [PATCH e2e-tests 19/38] tests: container: verify application containers from OCI images Michael Köppl
2026-11-04 19:03 ` Michael Köppl [this message]
2026-11-04 19:03 ` [PATCH e2e-tests 21/38] tests: vm: verify adding, resizing, and removing disks Michael Köppl
2026-11-04 19:03 ` [PATCH e2e-tests 22/38] plans,scripts: download a Debian cloud image onto pve1 Michael Köppl
2026-11-04 19:03 ` [PATCH e2e-tests 23/38] tests: vm: verify lifecycle operations and cloud-init guest setup Michael Köppl
2026-11-04 19:03 ` [PATCH e2e-tests 24/38] plans,scripts: install Ceph Tentacle on the cluster nodes Michael Köppl
2026-11-04 19:03 ` [PATCH e2e-tests 25/38] test framework: add Ceph test helpers Michael Köppl
2026-11-06 12:24 ` Kefu Chai
2026-11-06 13:04 ` Michael Köppl
2026-11-04 19:03 ` [PATCH e2e-tests 26/38] tests: ceph: verify setting up a hyper-converged cluster Michael Köppl
2026-11-06 12:50 ` Kefu Chai
2026-11-06 13:05 ` Michael Köppl
2026-11-04 19:03 ` [PATCH e2e-tests 27/38] tests: ceph: verify guests on RBD storage Michael Köppl
2026-11-06 13:01 ` Kefu Chai
2026-11-06 13:07 ` Michael Köppl
2026-11-04 19:03 ` [PATCH e2e-tests 28/38] tests: ceph: verify setting up, using and destroying a CephFS Michael Köppl
2026-11-04 19:03 ` [PATCH e2e-tests 29/38] tests: ceph: verify replacing an OSD Michael Köppl
2026-11-06 13:14 ` Kefu Chai
2026-11-04 19:03 ` [PATCH e2e-tests 30/38] tests: ceph: drop the outdated Ceph setup Michael Köppl
2026-11-04 19:03 ` [PATCH e2e-tests 31/38] tests: pbs: verify container backup and restore Michael Köppl
2026-11-04 19:03 ` [PATCH e2e-tests 32/38] tests: backup: verify VM backup and restore, locally and on PBS Michael Köppl
2026-11-04 19:03 ` [PATCH e2e-tests 33/38] tests: pbs: verify single file restore from a container backup Michael Köppl
2026-11-04 19:03 ` [PATCH e2e-tests 34/38] tests: storage: verify moving guest volumes between storages Michael Köppl
2026-11-04 19:03 ` [PATCH e2e-tests 35/38] tests: migration: verify container migration between nodes Michael Köppl
2026-11-04 19:03 ` [PATCH e2e-tests 36/38] tests: migration: verify VM migration with local disks Michael Köppl
2026-11-04 19:03 ` [PATCH e2e-tests 37/38] tests: ha: verify moving guests off a node for maintenance Michael Köppl
2026-11-04 19:03 ` [PATCH e2e-tests 38/38] tests: migration: verify migrating guests in bulk Michael Köppl
2026-11-06 13:25 ` [PATCH e2e-tests/test-tools 00/38] tests: add tests for guests, Ceph, PBS and cluster workflows Kefu Chai
Reply instructions:
You may reply publicly to this message via plain-text email
using any one of the following methods:
* Save the following mbox file, import it into your mail client,
and reply-to-all from there: mbox
Avoid top-posting and favor interleaved quoting:
https://en.wikipedia.org/wiki/Posting_style#Interleaved_style
* Reply using the --to, --cc, and --in-reply-to
switches of git-send-email(1):
git send-email \
--in-reply-to=20261104190345.2573464-21-m.koeppl@proxmox.com \
--to=m.koeppl@proxmox.com \
--cc=pve-devel@lists.proxmox.com \
/path/to/YOUR_REPLY
https://kernel.org/pub/software/scm/git/docs/git-send-email.html
* If your mail client supports setting the In-Reply-To header
via mailto: links, try the mailto: link
Be sure your reply has a Subject: header at the top and a blank line
before the message body.
This is a public inbox, see mirroring instructions
for how to clone and mirror all data and code used for this inbox