public inbox for pve-devel@lists.proxmox.com
 help / color / mirror / Atom feed
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





  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
Service provided by Proxmox Server Solutions GmbH | Privacy | Legal