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 961AF1FF0C0 for ; Wed, 04 Nov 2026 20:08:09 +0100 (CET) Received: from gate001.proxmox.com (localhost.localdomain [127.0.0.1]) by gate001.proxmox.com (Proxmox) with ESMTP id 2553721967; Wed, 04 Nov 2026 20:05:28 +0100 (CET) From: =?UTF-8?q?Michael=20K=C3=B6ppl?= To: pve-devel@lists.proxmox.com Subject: [PATCH e2e-tests 33/38] tests: pbs: verify single file restore from a container backup Date: Wed, 4 Nov 2026 20:03:40 +0100 Message-ID: <20261104190345.2573464-34-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: 1793819036515 X-SPAM-LEVEL: Spam detection results: 0 AWL 0.222 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: IXTSYH55QE3GROC34IE3W4XUDRGT4RMY X-Message-ID-Hash: IXTSYH55QE3GROC34IE3W4XUDRGT4RMY 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: Back up a container with a marker file to PBS, browse the backup through the file restore API down to the file and download it. Signed-off-by: Michael Köppl --- Notes: The test accepts curl's exit code 56 for the download, which curl with OpenSSL 3 returns because pveproxy ends the streamed response without a TLS close_notify. This workaround is only meant to stay until http-server patch [0] is applied, or the issue is resolved in some other way. [0] https://lore.proxmox.com/pve-devel/20261103155223.1960754-1-m.koeppl@proxmox.com/ tests/pbs/test_pbs_file_restore.pl | 151 +++++++++++++++++++++++++++++ tests/testcases.ron | 10 ++ 2 files changed, 161 insertions(+) create mode 100755 tests/pbs/test_pbs_file_restore.pl diff --git a/tests/pbs/test_pbs_file_restore.pl b/tests/pbs/test_pbs_file_restore.pl new file mode 100755 index 0000000..1d9a20e --- /dev/null +++ b/tests/pbs/test_pbs_file_restore.pl @@ -0,0 +1,151 @@ +#!/usr/bin/perl + +=head1 NAME + +C - Restore single files from a container backup on PBS. + +=head1 DESCRIPTION + +This testcase creates a local datastore on PBS and adds it to PVE as a storage. +It creates a container, writes a file to it and backs it up to the PBS datastore. +It then browses the backup through the single file restore API down to the file, +which must be listed with its size. It then downloads the file, which must have +the content written before the backup. + +=cut + +use strict; +use warnings; + +use Test::More; + +use URI::Escape qw(uri_escape); + +use lib '.'; +use Proxmox::Test::Helper qw(CONTAINER_TEMPLATE get_pbs_instance get_pve_instance); +use Proxmox::Test::Testcase; + +my $pve = get_pve_instance('pve1'); +my $pbs = get_pbs_instance('pbs1'); +my $node = $pve->get_nodename(); +my $client = $pve->client(); + +my $datastore = 'e2e-file-restore'; +my $storage_id = 'e2e-pbs-file-restore'; + +my $ctid; +my $backup_volid; +my $marker_file = 'e2e-marker'; +my $marker = "e2e-file-restore-$$"; +my $marker_entry; + +# Entries below $filepath in the backup +my $list = sub { + my ($filepath) = @_; + return $client->get( + "/nodes/$node/storage/$storage_id/file-restore/list", + { volume => $backup_volid, filepath => $filepath }, + ); +}; + +my $find_entry = sub { + my ($entries, $match) = @_; + my ($entry) = grep { $_->{text} =~ $match } $entries->@*; + return $entry; +}; + +Proxmox::Test::Testcase->new('pbs_file_restore')->with([{}])->step( + 'create datastore on PBS', + sub { + $pbs->create_datastore($datastore); + }, + sub { + $pbs->remove_datastore($datastore); + }, +)->step( + 'add PBS as storage on PVE', + sub { + $pve->add_pbs_storage($storage_id, $pbs, $datastore); + }, + sub { + $client->delete("/storage/$storage_id"); + }, +)->step( + 'create container and write file', + 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-file-restore', + }, + ); + $pve->task_ok($upid, "created container $ctid") + or die "creating container $ctid failed\n"; + + $pve->start_guest('lxc', $ctid); + $pve->ct_exec($ctid, "sh -c 'echo $marker > /root/$marker_file'"); + }, + sub { + $pve->destroy_guest('lxc', $ctid) if defined($ctid); + }, +)->step( + 'back up container to PBS', + sub { + my $upid = $client->post( + "/nodes/$node/vzdump", + { vmid => $ctid, storage => $storage_id, mode => 'snapshot' }, + ); + $pve->task_ok($upid, 'backed up container to PBS') or die "backup failed\n"; + + my $content = + $client->get("/nodes/$node/storage/$storage_id/content", { content => 'backup' }); + my ($backup) = grep { ($_->{vmid} // 0) == $ctid } $content->@*; + $backup_volid = $backup->{volid}; + ok($backup_volid, 'found container backup on PBS storage') or die "backup not found\n"; + }, +)->step( + 'browse backup down to the file', + sub { + my $archive = $find_entry->($list->('/'), qr/^root\.pxar/); + ok($archive, 'backup contains the root file system archive') + or die "archive not found\n"; + + my $root_dir = $find_entry->($list->($archive->{filepath}), qr/^root$/); + ok($root_dir, "archive contains the directory '/root'") or die "'/root' not found\n"; + + $marker_entry = $find_entry->($list->($root_dir->{filepath}), qr/^\Q$marker_file\E$/); + ok($marker_entry, 'directory contains the file') or die "file not found\n"; + is($marker_entry->{size}, length("$marker\n"), 'file is listed with its size'); + }, +)->step( + 'download file', + sub { + # This doesn't work with the API client (expects JSON), to use ticket from the node for + # manual request. + my $ticket = $client->post( + '/access/ticket', + { username => 'root@pam', password => $pve->get_root_password() }, + )->{ticket}; + + my $query = join( + '&', + 'volume=' . uri_escape($backup_volid), + 'filepath=' . uri_escape($marker_entry->{filepath}), + ); + # FIXME: remove workaround once api server sends TLS close notify + my ($content) = + $pve->run_command("curl -sSfk -H 'Cookie: PVEAuthCookie=$ticket'" + . " 'https://localhost:8006/api2/json/nodes/$node/storage/$storage_id" + . "/file-restore/download?$query'; rc=\$?; [ \$rc -eq 0 ] || [ \$rc -eq 56 ]"); + is($content, "$marker\n", 'downloaded file has the content from the backup'); + }, +)->run(); + +done_testing(); diff --git a/tests/testcases.ron b/tests/testcases.ron index b107835..51ceb81 100644 --- a/tests/testcases.ron +++ b/tests/testcases.ron @@ -293,6 +293,16 @@ TestcaseConfig( ], tag: "pbs", ), + Testcase( + name: "pbs_file_restore", + run: "./tests/pbs/test_pbs_file_restore.pl", + timeout: 600, + instances: [ + "pve1", + "pbs1", + ], + tag: "pbs", + ), Testcase( name: "sdn_create_vlan_zone", run: "./tests/sdn/test_vlan_zone.pl", -- 2.47.3