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 an external index of several public inboxes,
see mirroring instructions on how to clone and mirror
all data and code used by this external index.