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 6D21C1FF0DF for ; Fri, 28 Aug 2026 15:31:27 +0200 (CEST) Received: from gate001.proxmox.com (localhost.localdomain [127.0.0.1]) by gate001.proxmox.com (Proxmox) with ESMTP id 7345C214F5; Fri, 28 Aug 2026 15:31:11 +0200 (CEST) From: Shannon Sterz 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 Message-ID: <20260828133030.351140-9-s.sterz@proxmox.com> X-Mailer: git-send-email 2.47.3 In-Reply-To: <20260828133030.351140-1-s.sterz@proxmox.com> References: <20260828133030.351140-1-s.sterz@proxmox.com> MIME-Version: 1.0 Content-Transfer-Encoding: 8bit X-Bm-Milter-Handled: 55990f41-d878-4baa-be0a-ee34c49e34d2 X-Bm-Transport-Timestamp: 1787923824511 X-SPAM-LEVEL: Spam detection results: 0 AWL -0.249 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) KAM_MAILER 2 Automated Mailer Tag Left in Email POISEN_SPAM_PILL 0.1 Meta: its spam POISEN_SPAM_PILL_1 0.1 random spam to be learned in bayes POISEN_SPAM_PILL_3 0.1 random spam to be learned in bayes 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: AKVE5IGLNIZS5CB2HCJAQH6FNKUXQVEK X-Message-ID-Hash: AKVE5IGLNIZS5CB2HCJAQH6FNKUXQVEK X-MailFrom: s.sterz@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: 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 --- 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