From: Shannon Sterz <s.sterz@proxmox.com>
To: pve-devel@lists.proxmox.com
Subject: [PATCH manager 08/21] api: cluster: add endpoints for manage host backup jobs
Date: Fri, 28 Aug 2026 15:30:17 +0200 [thread overview]
Message-ID: <20260828133030.351140-9-s.sterz@proxmox.com> (raw)
In-Reply-To: <20260828133030.351140-1-s.sterz@proxmox.com>
includes the follow api endpoints:
* GET /cluster/jobs/host-backup: list configured host backup jobs
* POST /cluster/jobs/host-backup: create a new host backup job
* GET /cluster/jobs/host-backup/{id}: get configuration for job {id}
* PUT /cluster/jobs/host-backup/{id}: update host backup job {id}
* DELETE /cluster/jobs/host-backup/{id}: remove host backup job {id}
Signed-off-by: Shannon Sterz <s.sterz@proxmox.com>
---
PVE/API2/Cluster/HostBackup.pm | 355 +++++++++++++++++++++++++++++++++
PVE/API2/Cluster/Jobs.pm | 10 +-
PVE/API2/Cluster/Makefile | 1 +
3 files changed, 365 insertions(+), 1 deletion(-)
create mode 100644 PVE/API2/Cluster/HostBackup.pm
diff --git a/PVE/API2/Cluster/HostBackup.pm b/PVE/API2/Cluster/HostBackup.pm
new file mode 100644
index 000000000..97c3553d7
--- /dev/null
+++ b/PVE/API2/Cluster/HostBackup.pm
@@ -0,0 +1,355 @@
+package PVE::API2::Cluster::HostBackup;
+
+use v5.36;
+
+use UUID qw(uuid);
+
+use PVE::API2::HostBackup;
+use PVE::Cluster qw(cfs_lock_file cfs_read_file cfs_write_file);
+use PVE::Exception qw(raise_param_exc);
+use PVE::GuestHelpers;
+use PVE::JSONSchema qw(get_standard_option);
+use PVE::Jobs::HostBackup;
+use PVE::Jobs;
+use PVE::RPCEnvironment;
+use PVE::Storage::PBSPlugin;
+use PVE::Storage;
+use PVE::Tools qw(extract_param split_list);
+
+use Proxmox::RS::CalendarEvent;
+
+use base qw(PVE::RESTHandler);
+
+# Helper to override or add certain parameters.
+my sub host_backup_properties : prototype(%) ($prop) {
+ my $create_schema = PVE::Jobs::HostBackup->createSchema();
+
+ foreach my $opt (keys %$prop) {
+ $create_schema->{properties}->{$opt} = $prop->{$opt};
+ }
+
+ return $create_schema;
+}
+
+__PACKAGE__->register_method({
+ name => 'list_jobs',
+ path => '',
+ method => 'GET',
+ description => "List host backup jobs.",
+ permissions => {
+ check => ['perm', '/', ['Sys.Audit']],
+ },
+ parameters => {
+ additionalProperties => 0,
+ properties => {},
+ },
+ returns => {
+ type => 'array',
+ items => host_backup_properties({
+ id => get_standard_option('pve-backup-jobid'),
+ 'next-run' => {
+ description => "UNIX timestamp when this host backup job will be executed next",
+ type => 'integer',
+ optional => 1,
+ },
+ }),
+ },
+ code => sub($) {
+ my $jobs_data = cfs_read_file('jobs.cfg');
+ my $order = $jobs_data->{order};
+ my $jobs = $jobs_data->{ids};
+ my $result = [];
+
+ foreach my $jobid (sort { $order->{$a} <=> $order->{$b} } keys %$jobs) {
+ my $job = $jobs->{$jobid};
+ $job->{id} = $jobid;
+ next if $job->{type} ne 'host-backup';
+
+ if (my $schedule = $job->{schedule}) {
+ my $last_run = time();
+ my $calspec = Proxmox::RS::CalendarEvent->new($schedule);
+ my $next_run = $calspec->compute_next_event($last_run);
+ $job->{'next-run'} = $next_run if defined($next_run);
+ }
+
+ push @$result, $job;
+ }
+
+ return $result;
+ },
+});
+
+__PACKAGE__->register_method({
+ name => 'create_backup_job',
+ path => '',
+ method => 'POST',
+ protected => 1,
+ permissions => {
+ check => [
+ 'and',
+ ['perm', '/', ['Sys.Console']],
+ ['perm', '/storage/{storage}', ['Datastore.AllocateSpace']],
+ ],
+ description =>
+ "The user needs to have 'Datastore.AllocateSpace' permissions on the storage that the "
+ . "backups are intended to be saved and 'Sys.Console' on '/'. The 'hooks' and "
+ . "'additional-files' parameters are further restricted to 'root\@pam'.",
+ },
+ description => "Create a new host backup job.",
+ parameters => host_backup_properties({
+ 'id' => get_standard_option(
+ 'pve-backup-jobid',
+ {
+ description => 'The job ID, will be auto-generated.',
+ optional => 1,
+ },
+ ),
+ }),
+ returns => { type => 'null' },
+ code => sub($param) {
+ my $rpcenv = PVE::RPCEnvironment::get();
+ my $user = $rpcenv->get_user();
+ my $cfg = PVE::Storage::config();
+
+ if (defined($param->{hooks})) {
+ raise_param_exc({ hooks => "Only root may set the hooks option." })
+ if $user ne "root\@pam";
+
+ eval { PVE::GuestHelpers::check_hookscript($param->{hooks}, $cfg); };
+ raise_param_exc({ hooks => $@ }) if $@;
+ }
+
+ if (defined($param->{"additional-files"}) && $user ne "root\@pam") {
+ raise_param_exc({
+ "additional-files" =>
+ "Only root may set or remove the additional-files option.",
+ });
+ }
+
+ # check that storage exists and is a pbs storage
+ my $storeid = $param->{storage};
+ my $scfg = PVE::Storage::storage_config($cfg, $storeid);
+ die "currently only proxmox backup server is supported for host backups\n"
+ if $scfg->{type} ne PVE::Storage::PBSPlugin::type();
+
+ my $id = extract_param($param, 'id') // UUID::uuid();
+
+ cfs_lock_file(
+ 'jobs.cfg',
+ undef,
+ sub {
+ my $data = cfs_read_file('jobs.cfg');
+
+ die "Job '$id' already exists\n" if $data->{ids}->{$id};
+
+ my $opts = PVE::Jobs::HostBackup->check_config($id, $param, 1, 1);
+
+ $data->{ids}->{$id} = $opts;
+
+ PVE::Jobs::create_job($id, 'host-backup', $opts);
+ cfs_write_file('jobs.cfg', $data);
+ },
+ );
+
+ die "could not add host backup job: $@\n" if $@;
+ return;
+ },
+});
+
+__PACKAGE__->register_method({
+ name => 'read_job',
+ path => '{id}',
+ method => 'GET',
+ description => "Get the configuration of a host backup job.",
+ permissions => {
+ check => ['perm', '/', ['Sys.Audit']],
+ },
+ parameters => {
+ additionalProperties => 0,
+ properties => {
+ id => get_standard_option('pve-backup-jobid'),
+ },
+ },
+ returns => host_backup_properties({
+ id => get_standard_option('pve-backup-jobid'),
+ }),
+ code => sub($param) {
+ my $jobs_data = cfs_read_file('jobs.cfg');
+ my $id = extract_param($param, 'id');
+ my $job = $jobs_data->{ids}->{$id};
+
+ if ($job && $job->{type} eq 'host-backup') {
+ $job->{id} = $id;
+ return $job;
+ }
+
+ raise_param_exc({ id => "No such job '$id'" });
+ },
+});
+
+__PACKAGE__->register_method({
+ name => 'update_job',
+ path => '{id}',
+ method => 'PUT',
+ protected => 1,
+ description => "Update host backup job configuration.",
+ permissions => {
+ check => [
+ 'and',
+ ['perm', '/', ['Sys.Console']],
+ ['perm', '/storage/{storage}', ['Datastore.AllocateSpace']],
+ ],
+ description =>
+ "The user needs to have 'Datastore.AllocateSpace' permissions on the storage that the "
+ . "backups are intended to be saved and 'Sys.Console' on '/'. The 'hooks' and "
+ . "'additional-files' parameters are further restricted to 'root\@pam'.",
+ },
+ parameters => host_backup_properties({
+ id => get_standard_option('pve-backup-jobid'),
+ schedule => {
+ description =>
+ "Backup schedule. The format is a subset of `systemd` calendar events.",
+ type => 'string',
+ format => 'pve-calendar-event',
+ maxLength => 128,
+ optional => 1,
+ },
+ storage => get_standard_option(
+ 'pve-storage-id',
+ {
+ description =>
+ "The storage that will store the backups. Currently, only Proxmox Backup Server is "
+ . "supported.",
+ completion => \&PVE::API2::HostBackup::complete_proxmox_backup_storage,
+ optional => 1,
+ },
+ ),
+ delete => {
+ type => 'string',
+ format => 'pve-configid-list',
+ description => "A list of settings you want to delete.",
+ optional => 1,
+ },
+ }),
+ returns => { type => 'null' },
+ code => sub($param) {
+ my $cfg = PVE::Storage::config();
+ my $rpcenv = PVE::RPCEnvironment::get();
+ my $user = $rpcenv->get_user();
+ my $delete = extract_param($param, 'delete');
+ $delete = { map { $_ => 1 } PVE::Tools::split_list($delete) } if $delete;
+
+ if ($user ne "root\@pam") {
+ if ((defined($param->{hooks}) || (defined($delete) && defined($delete->{hooks})))) {
+ raise_param_exc({ hooks => "Only root may set or remove the hooks option." });
+ }
+
+ if (
+ defined($param->{"additional-files"})
+ || (defined($delete) && defined($delete->{"additional-files"}))
+ ) {
+ raise_param_exc({
+ "additional-files" =>
+ "Only root may set or remove the additional-files option.",
+ });
+ }
+ }
+
+ if (defined($param->{hooks})) {
+ eval { PVE::GuestHelpers::check_hookscript($param->{hooks}, $cfg); };
+ raise_param_exc({ hooks => $@ }) if $@;
+ }
+
+ # check that storage exists if it is being modified
+ if (my $storeid = $param->{storage}) {
+ # check that storage exists and is a pbs storage
+ my $scfg = PVE::Storage::storage_config($cfg, $storeid);
+ die "currently only proxmox backup server is supported for host backups\n"
+ if $scfg->{type} ne PVE::Storage::PBSPlugin::type();
+ }
+
+ cfs_lock_file(
+ 'jobs.cfg',
+ undef,
+ sub {
+ my $id = extract_param($param, 'id');
+ my $jobs_data = cfs_read_file('jobs.cfg');
+ my $job = $jobs_data->{ids}->{$id};
+
+ raise_param_exc({ id => "No host backup job with ID '$id' exists." })
+ if !$job || $job->{type} ne 'host-backup';
+
+ my $deletable = {
+ 'additional-files' => 1,
+ 'repeat-missed' => 1,
+ comment => 1,
+ hooks => 1,
+ };
+
+ if (defined($delete)) {
+ for my $prop (keys $delete->%*) {
+ raise_param_exc({ delete => "unknown option '$prop'" })
+ if !$deletable->{$prop};
+
+ delete $job->{$prop};
+ }
+ }
+
+ foreach my $prop (keys %$param) {
+ $job->{$prop} = $param->{$prop};
+ }
+
+ cfs_write_file('jobs.cfg', $jobs_data);
+ PVE::Jobs::detect_changed_runtime_props($id, 'host-backup', $job);
+ return;
+
+ },
+ );
+ die "$@" if $@;
+ },
+});
+
+__PACKAGE__->register_method({
+ name => 'delete_job',
+ path => '{id}',
+ method => 'DELETE',
+ description => "Delete host backup job.",
+ permissions => {
+ check => ['perm', '/', ['Sys.Console']],
+ },
+ protected => 1,
+ parameters => {
+ additionalProperties => 0,
+ properties => {
+ id => get_standard_option('pve-backup-jobid'),
+ },
+ },
+ returns => { type => 'null' },
+ code => sub($param) {
+ my $id = extract_param($param, 'id');
+
+ cfs_lock_file(
+ 'jobs.cfg',
+ undef,
+ sub {
+ my $jobs_data = cfs_read_file('jobs.cfg');
+
+ if (!defined($jobs_data->{ids}->{$id})) {
+ raise_param_exc({ id => "No such job '$id'" });
+ }
+
+ raise_param_exc({ id => "Not a host-backup job." })
+ if $jobs_data->{ids}->{$id}->{type} ne 'host-backup';
+
+ delete $jobs_data->{ids}->{$id};
+ PVE::Jobs::remove_job($id, 'host-backup');
+ cfs_write_file('jobs.cfg', $jobs_data);
+ },
+ );
+
+ die "$@" if $@;
+ return;
+ },
+});
+
+1;
diff --git a/PVE/API2/Cluster/Jobs.pm b/PVE/API2/Cluster/Jobs.pm
index e02eed9e0..c1e992848 100644
--- a/PVE/API2/Cluster/Jobs.pm
+++ b/PVE/API2/Cluster/Jobs.pm
@@ -6,6 +6,7 @@ use warnings;
use PVE::RESTHandler;
use PVE::CalendarEvent;
+use PVE::API2::Cluster::HostBackup;
use PVE::API2::Jobs::RealmSync;
use base qw(PVE::RESTHandler);
@@ -15,6 +16,11 @@ __PACKAGE__->register_method({
path => 'realm-sync',
});
+__PACKAGE__->register_method({
+ subclass => "PVE::API2::Cluster::HostBackup",
+ path => 'host-backup',
+});
+
__PACKAGE__->register_method({
name => 'index',
path => '',
@@ -41,7 +47,9 @@ __PACKAGE__->register_method({
},
code => sub {
return [
- { subdir => 'schedule-analyze' }, { subdir => 'realm-sync' },
+ { subdir => 'schedule-analyze' },
+ { subdir => 'realm-sync' },
+ { subdir => 'host-backup' },
];
},
});
diff --git a/PVE/API2/Cluster/Makefile b/PVE/API2/Cluster/Makefile
index d3a56830c..da42cf81a 100644
--- a/PVE/API2/Cluster/Makefile
+++ b/PVE/API2/Cluster/Makefile
@@ -10,6 +10,7 @@ PERLSOURCE= \
BackupInfo.pm \
BulkAction.pm \
Ceph.pm \
+ HostBackup.pm \
Jobs.pm \
Mapping.pm \
MetricServer.pm \
--
2.47.3
next prev parent reply other threads:[~2026-08-28 13:31 UTC|newest]
Thread overview: 22+ messages / expand[flat|nested] mbox.gz Atom feed top
2026-08-28 13:30 [RFC cluster/common/container/docs/installer/manager 00/21] add rudimentary host backup mechanism Shannon Sterz
2026-08-28 13:30 ` [PATCH cluster 01/21] pmxcfs: status: fix formatting of parameters in checked_mkdir() Shannon Sterz
2026-08-28 13:30 ` [PATCH cluster 02/21] pmxcfs: correctly log message when directory can't be created Shannon Sterz
2026-08-28 13:30 ` [PATCH cluster 03/21] pmxcfs: add live backup capability Shannon Sterz
2026-08-28 13:30 ` [PATCH cluster 04/21] pmxcfs: add ability to query backup progress Shannon Sterz
2026-08-28 13:30 ` [PATCH common 05/21] systemd: move parse_os_release() helper to PVE::Systemd Shannon Sterz
2026-08-28 13:30 ` [PATCH container 06/21] setup: use parse_os_release from PVE::Systemd Shannon Sterz
2026-08-28 13:30 ` [PATCH manager 07/21] jobs/api: add basic host backup job logic Shannon Sterz
2026-08-28 13:30 ` Shannon Sterz [this message]
2026-08-28 13:30 ` [PATCH manager 09/21] api: node: add endpoints for listing backups for a node Shannon Sterz
2026-08-28 13:30 ` [PATCH manager 10/21] api: host backup: include global, disk and network options for restore Shannon Sterz
2026-08-28 13:30 ` [PATCH manager 11/21] api: host backup: add warnings in case zfs snapdir is disabled Shannon Sterz
2026-08-28 13:30 ` [PATCH manager 12/21] ui: node: add panel to manage backups of a host Shannon Sterz
2026-08-28 13:30 ` [PATCH manager 13/21] ui: dc: add panel for managing host backup jobs Shannon Sterz
2026-08-28 13:30 ` [PATCH installer 14/21] bump proxmox-installer-types to 0.2 Shannon Sterz
2026-08-28 13:30 ` [PATCH installer 15/21] make tidy and clean up whitespace in unconfigured.sh Shannon Sterz
2026-08-28 13:30 ` [PATCH installer 16/21] installer-common: add option to verify TLS connections via callback Shannon Sterz
2026-08-28 13:30 ` [PATCH installer 17/21] low-level-installer: add support for restoring backups Shannon Sterz
2026-08-28 13:30 ` [PATCH installer 18/21] installer-common/tui-installer: implement restore tui Shannon Sterz
2026-08-28 13:30 ` [PATCH installer 19/21] unconfigured: add restore mode to unconfigured.sh Shannon Sterz
2026-08-28 13:30 ` [PATCH installer 20/21] tui-installer: unmount a potentially mounted backup on abort Shannon Sterz
2026-08-28 13:30 ` [PATCH docs 21/21] examples: add example hook script for host backup jobs Shannon Sterz
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=20260828133030.351140-9-s.sterz@proxmox.com \
--to=s.sterz@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