public inbox for pmg-devel@lists.proxmox.com
 help / color / mirror / Atom feed
* [pmg-devel] [PATCH pve-common/api/gui] add initial PBS integration
@ 2020-10-28 18:54 Stoiko Ivanov
  2020-10-28 18:54 ` [pmg-devel] [PATCH pve-common 1/2] Systemd: add helpers for parsing unit files Stoiko Ivanov
                   ` (16 more replies)
  0 siblings, 17 replies; 26+ messages in thread
From: Stoiko Ivanov @ 2020-10-28 18:54 UTC (permalink / raw)
  To: pmg-devel

changes RFC->v1:
* moved the potentially reusable parts to pve-common (PBSTools.pm, and 2
  functions in Systemd.pm)
* added GUI support (mostly adapted from the LDAPConfig) - huge thanks to
  Dominik for his patience and help!
* added support for encrypted backups
* added support for pruning backups
* added a systemd-timer, which runs a daily prune based on the configured
  settings

differences from the PBS storage-plugin config in PVE:
* the keep-options are kept as separate options instead of using a property
  string for all (like they are in the datastore config in PBS)


cover-letter from the RFC:
This series is a minimal proof-of-concept for integrating PBS into PMG.

The code needs quite a bit of cleanup, and more testing, however I'll send it
as RFC, for an initial sanity-check - to see if I missed some needed
functionality.

It reuses quite a bit of code from pve-storage (PBSPlugin, and the systemd
unit manipulation from the PVE::API2::Disks::Directory) and I'd like to
refactor the common parts into pve-common.

The backup consists of the same files, that end up in a locally generated
PMG Backup (instead of creating a tar.gz we send the data to a PBS instance).

What works:
* creating and manipulating 'remotes' (PBS instances where we back up to)
* creating, restoring and forgetting backups to those remotes
* restoring a backup from another PMG installation from a configured remote
* creating schedules for backups (systemd-timer units) for periodic backups
via CLI (pmgbackup, pmgsh)

What's missing:
* quite a bit of refactoring (e.g. move the systemd-timer helpers and most of
  PBSTools to pve-common (and reuse it in the PBS Storage plugin)
  * encryption support
  * pruneing support
  * testing
  * GUI
  * Documentation

  The first patch is a small cosmetic cleanup, and independent of the series.

  A sample session (which I used for testing:
  ```
  pmgbackup remote add pbs1 --datastore local --server 192.0.2.11 --user root@pam --password xxx --fingerprint  ff:ff:...
  pmgbackup remote list
  pmgbackup pbsjob run pbs1
  pmgbackup pbsjob restore pbs1 -althost pmg-live -config 1 -database 1 -statistic 1
  pmgbackup pbsjob run pbs1
  pmgbackup pbsjob list_backups pbs1
  pmgbackup pbsjob create pbs1 --schedule 'minutely' --delay '0s'
  systemctl list-timers --all
  ```


pve-common:
Stoiko Ivanov (2):
  Systemd: add helpers for parsing unit files
  add helper module for handling PBS Integration

 src/Makefile        |   1 +
 src/PVE/PBSTools.pm | 309 ++++++++++++++++++++++++++++++++++++++++++++
 src/PVE/Systemd.pm  |  72 +++++++++++
 3 files changed, 382 insertions(+)
 create mode 100644 src/PVE/PBSTools.pm

pmg-api:
Stoiko Ivanov (11):
  drop left-over commented out code
  Backup: split backup creation and creating tar
  Restore: optionally restore from directory
  Backup: push restore options to PMG::Backup
  debian: add dependency on proxmox-backup-client
  add initial SectionConfig for pbs
  Add API2 module for PBS configuration
  Add API2 module for per-node backups to PBS
  pbs-integration: add CLI calls to pmgbackup
  add scheduled backup to PBS remotes
  add daily timer for pruning configured remotes

 debian/control                |   1 +
 debian/dirs                   |   1 +
 debian/pmg-pbsbackup@.service |   6 +
 debian/pmg-pbsprune.service   |   6 +
 debian/pmg-pbsprune.timer     |  10 +
 debian/rules                  |   4 +-
 src/Makefile                  |   8 +-
 src/PMG/API2/Backup.pm        |  21 +-
 src/PMG/API2/Config.pm        |   7 +
 src/PMG/API2/Nodes.pm         |   7 +
 src/PMG/API2/PBS/Job.pm       | 544 ++++++++++++++++++++++++++++++++++
 src/PMG/API2/PBS/Remote.pm    | 248 ++++++++++++++++
 src/PMG/Backup.pm             |  86 +++++-
 src/PMG/CLI/pmgbackup.pm      | 102 +++++++
 src/PMG/PBSConfig.pm          | 209 +++++++++++++
 src/PMG/PBSSchedule.pm        | 104 +++++++
 16 files changed, 1327 insertions(+), 37 deletions(-)
 create mode 100644 debian/pmg-pbsbackup@.service
 create mode 100644 debian/pmg-pbsprune.service
 create mode 100644 debian/pmg-pbsprune.timer
 create mode 100644 src/PMG/API2/PBS/Job.pm
 create mode 100644 src/PMG/API2/PBS/Remote.pm
 create mode 100644 src/PMG/PBSConfig.pm
 create mode 100644 src/PMG/PBSSchedule.pm

pmg-gui:
Stoiko Ivanov (3):
  Make Backup/Restore panel a menuentry
  refactor RestoreWindow for PBS
  add PBSConfig tab to Backup menu

 js/BackupConfiguration.js |  23 ++
 js/BackupRestore.js       |  68 ++--
 js/Makefile               |   2 +
 js/NavigationTree.js      |   6 +
 js/PBSConfig.js           | 680 ++++++++++++++++++++++++++++++++++++++
 js/SystemConfiguration.js |   4 -
 6 files changed, 750 insertions(+), 33 deletions(-)
 create mode 100644 js/BackupConfiguration.js
 create mode 100644 js/PBSConfig.js

-- 
2.20.1





^ permalink raw reply	[flat|nested] 26+ messages in thread

* [pmg-devel] [PATCH pve-common 1/2] Systemd: add helpers for parsing unit files
  2020-10-28 18:54 [pmg-devel] [PATCH pve-common/api/gui] add initial PBS integration Stoiko Ivanov
@ 2020-10-28 18:54 ` Stoiko Ivanov
  2020-10-30  8:26   ` [pmg-devel] applied: " Dietmar Maurer
  2020-11-10  8:34   ` [pmg-devel] " Thomas Lamprecht
  2020-10-28 18:54 ` [pmg-devel] [PATCH pve-common 2/2] add helper module for handling PBS Integration Stoiko Ivanov
                   ` (15 subsequent siblings)
  16 siblings, 2 replies; 26+ messages in thread
From: Stoiko Ivanov @ 2020-10-28 18:54 UTC (permalink / raw)
  To: pmg-devel

taken from pve-storage/PVE/API2/Disks/Directory.pm (and made available as
public sub)

Signed-off-by: Stoiko Ivanov <s.ivanov@proxmox.com>
---
refactoring pve-storage to use this (and drop the now duplicated code) will
be done separately

 src/PVE/Systemd.pm | 72 ++++++++++++++++++++++++++++++++++++++++++++++
 1 file changed, 72 insertions(+)

diff --git a/src/PVE/Systemd.pm b/src/PVE/Systemd.pm
index 85b35a3..bcba5eb 100644
--- a/src/PVE/Systemd.pm
+++ b/src/PVE/Systemd.pm
@@ -7,6 +7,8 @@ use Net::DBus qw(dbus_uint32 dbus_uint64);
 use Net::DBus::Callback;
 use Net::DBus::Reactor;
 
+use PVE::Tools qw(file_set_contents file_get_contents trim);
+
 sub escape_unit {
     my ($val, $is_path) = @_;
 
@@ -163,4 +165,74 @@ sub wait_for_unit_removed($;$) {
     }, $timeout);
 }
 
+sub read_ini {
+    my ($filename) = @_;
+
+    my $content = file_get_contents($filename);
+    my @lines = split /\n/, $content;
+
+    my $result = {};
+    my $section;
+
+    foreach my $line (@lines) {
+	$line = trim($line);
+	if ($line =~ m/^\[([^\]]+)\]/) {
+	    $section = $1;
+	    if (!defined($result->{$section})) {
+		$result->{$section} = {};
+	    }
+	} elsif ($line =~ m/^(.*?)=(.*)$/) {
+	    my ($key, $val) = ($1, $2);
+	    if (!$section) {
+		warn "key value pair found without section, skipping\n";
+		next;
+	    }
+
+	    if ($result->{$section}->{$key}) {
+		# make duplicate properties to arrays to keep the order
+		my $prop = $result->{$section}->{$key};
+		if (ref($prop) eq 'ARRAY') {
+		    push @$prop, $val;
+		} else {
+		    $result->{$section}->{$key} = [$prop, $val];
+		}
+	    } else {
+		$result->{$section}->{$key} = $val;
+	    }
+	}
+	# ignore everything else
+    }
+
+    return $result;
+};
+
+sub write_ini {
+    my ($ini, $filename) = @_;
+
+    my $content = "";
+
+    foreach my $sname (sort keys %$ini) {
+	my $section = $ini->{$sname};
+
+	$content .= "[$sname]\n";
+
+	foreach my $pname (sort keys %$section) {
+	    my $prop = $section->{$pname};
+
+	    if (!ref($prop)) {
+		$content .= "$pname=$prop\n";
+	    } elsif (ref($prop) eq 'ARRAY') {
+		foreach my $val (@$prop) {
+		    $content .= "$pname=$val\n";
+		}
+	    } else {
+		die "invalid property '$pname'\n";
+	    }
+	}
+	$content .= "\n";
+    }
+
+    file_set_contents($filename, $content);
+};
+
 1;
-- 
2.20.1





^ permalink raw reply	[flat|nested] 26+ messages in thread

* [pmg-devel] [PATCH pve-common 2/2] add helper module for handling PBS Integration
  2020-10-28 18:54 [pmg-devel] [PATCH pve-common/api/gui] add initial PBS integration Stoiko Ivanov
  2020-10-28 18:54 ` [pmg-devel] [PATCH pve-common 1/2] Systemd: add helpers for parsing unit files Stoiko Ivanov
@ 2020-10-28 18:54 ` Stoiko Ivanov
  2020-10-28 18:54 ` [pmg-devel] [PATCH pmg-api 01/11] drop left-over commented out code Stoiko Ivanov
                   ` (14 subsequent siblings)
  16 siblings, 0 replies; 26+ messages in thread
From: Stoiko Ivanov @ 2020-10-28 18:54 UTC (permalink / raw)
  To: pmg-devel

PBSTools.pm contains methods which eventually should be shared between
PVE and PMG, for:
* handling (sensitive) config-information (passwords, encryption keys)
* creating/restoring/forgetting/listing backups

code is mostly based on the current PBSPlugin in pve-storage

Signed-off-by: Stoiko Ivanov <s.ivanov@proxmox.com>
---
 src/Makefile        |   1 +
 src/PVE/PBSTools.pm | 309 ++++++++++++++++++++++++++++++++++++++++++++
 2 files changed, 310 insertions(+)
 create mode 100644 src/PVE/PBSTools.pm

diff --git a/src/Makefile b/src/Makefile
index 1987d0e..9084903 100644
--- a/src/Makefile
+++ b/src/Makefile
@@ -20,6 +20,7 @@ LIB_SOURCES = \
 	LDAP.pm \
 	Network.pm \
 	OTP.pm \
+	PBSTools.pm \
 	PTY.pm \
 	ProcFSTools.pm \
 	RESTEnvironment.pm \
diff --git a/src/PVE/PBSTools.pm b/src/PVE/PBSTools.pm
new file mode 100644
index 0000000..77cc30b
--- /dev/null
+++ b/src/PVE/PBSTools.pm
@@ -0,0 +1,309 @@
+package PVE::PBSTools;
+
+# utility functions for interaction with Proxmox Backup Server
+
+use strict;
+use warnings;
+use Fcntl qw(F_GETFD F_SETFD FD_CLOEXEC);
+use IO::File;
+use JSON;
+use POSIX qw(strftime ENOENT);
+
+use PVE::Tools qw(run_command file_set_contents file_get_contents file_read_firstline);
+use PVE::JSONSchema qw(get_standard_option);
+
+# Helpers
+my $secret_dir;
+
+sub set_secret_dir {
+    my ($dir) = @_;
+    $secret_dir = $dir;
+}
+
+sub pbs_password_file_name {
+    my ($storeid) = @_;
+
+    return "${secret_dir}/${storeid}.pw";
+}
+
+sub pbs_set_password {
+    my ($storeid, $password) = @_;
+
+    my $pwfile = pbs_password_file_name($storeid);
+    mkdir $secret_dir;
+
+    PVE::Tools::file_set_contents($pwfile, "$password\n", 0600);
+}
+
+sub pbs_delete_password {
+    my ($storeid) = @_;
+
+    my $pwfile = pbs_password_file_name($storeid);
+
+    unlink $pwfile;
+}
+
+sub pbs_get_password {
+    my ($storeid) = @_;
+
+    my $pwfile = pbs_password_file_name($storeid);
+
+    return PVE::Tools::file_read_firstline($pwfile);
+}
+
+sub pbs_encryption_key_file_name {
+    my ($storeid) = @_;
+
+    return "${secret_dir}/${storeid}.enc";
+}
+
+sub pbs_set_encryption_key {
+    my ($storeid, $key) = @_;
+
+    my $encfile = pbs_encryption_key_file_name($storeid);
+    mkdir $secret_dir;
+
+    PVE::Tools::file_set_contents($encfile, "$key\n", 0600);
+}
+
+sub pbs_delete_encryption_key {
+    my ($storeid) = @_;
+
+    my $encfile = pbs_encryption_key_file_name($storeid);
+
+    if (!unlink $encfile) {
+	return if $! == ENOENT;
+	die "failed to delete encryption key! $!\n";
+    }
+}
+
+sub pbs_get_encryption_key {
+    my ($storeid) = @_;
+
+    my $encfile = pbs_encryption_key_file_name($storeid);
+
+    return PVE::Tools::file_get_contents($encfile);
+}
+
+# Returns a file handle if there is an encryption key, or `undef` if there is not. Dies on error.
+sub pbs_open_encryption_key {
+    my ($storeid) = @_;
+
+    my $encryption_key_file = pbs_encryption_key_file_name($storeid);
+
+    my $keyfd;
+    if (!open($keyfd, '<', $encryption_key_file)) {
+	return undef if $! == ENOENT;
+	die "failed to open encryption key: $encryption_key_file: $!\n";
+    }
+
+    return $keyfd;
+}
+
+my $USE_CRYPT_PARAMS = {
+    backup => 1,
+    restore => 1,
+    'upload-log' => 1,
+};
+
+my sub do_raw_client_cmd {
+    my ($scfg, $storeid, $client_cmd, $param, %opts) = @_;
+
+    my $use_crypto = $USE_CRYPT_PARAMS->{$client_cmd};
+
+    my $client_exe = '/usr/bin/proxmox-backup-client';
+    die "executable not found '$client_exe'! Proxmox backup client not installed?\n"
+	if ! -x $client_exe;
+
+    my $server = $scfg->{server};
+    my $datastore = $scfg->{datastore};
+    my $username = $scfg->{username} // 'root@pam';
+
+    my $userns_cmd = delete $opts{userns_cmd};
+
+    my $cmd = [];
+
+    push @$cmd, @$userns_cmd if defined($userns_cmd);
+
+    push @$cmd, $client_exe, $client_cmd;
+
+    # This must live in the top scope to not get closed before the `run_command`
+    my $keyfd;
+    if ($use_crypto) {
+	if (defined($keyfd = pbs_open_encryption_key($storeid))) {
+	    my $flags = fcntl($keyfd, F_GETFD, 0)
+		// die "failed to get file descriptor flags: $!\n";
+	    fcntl($keyfd, F_SETFD, $flags & ~FD_CLOEXEC)
+		or die "failed to remove FD_CLOEXEC from encryption key file descriptor\n";
+	    push @$cmd, '--crypt-mode=encrypt', '--keyfd='.fileno($keyfd);
+	} else {
+	    push @$cmd, '--crypt-mode=none';
+	}
+    }
+
+    push @$cmd, @$param if defined($param);
+
+    push @$cmd, "--repository", "$username\@$server:$datastore";
+
+    local $ENV{PBS_PASSWORD} = pbs_get_password($storeid);
+
+    local $ENV{PBS_FINGERPRINT} = $scfg->{fingerprint};
+
+    # no ascii-art on task logs
+    local $ENV{PROXMOX_OUTPUT_NO_BORDER} = 1;
+    local $ENV{PROXMOX_OUTPUT_NO_HEADER} = 1;
+
+    if (my $logfunc = $opts{logfunc}) {
+	$logfunc->("run: " . join(' ', @$cmd));
+    }
+
+    run_command($cmd, %opts);
+}
+
+my sub run_raw_client_cmd {
+    my ($scfg, $storeid, $client_cmd, $param, %opts) = @_;
+    return do_raw_client_cmd($scfg, $storeid, $client_cmd, $param, %opts);
+}
+
+my sub run_client_cmd {
+    my ($scfg, $storeid, $client_cmd, $param, $no_output) = @_;
+
+    my $json_str = '';
+    my $outfunc = sub { $json_str .= "$_[0]\n" };
+
+    $param = [] if !defined($param);
+    $param = [ $param ] if !ref($param);
+
+    $param = [@$param, '--output-format=json'] if !$no_output;
+
+    do_raw_client_cmd($scfg, $storeid, $client_cmd, $param,
+		      outfunc => $outfunc, errmsg => 'proxmox-backup-client failed');
+
+    return undef if $no_output;
+
+    my $res = decode_json($json_str);
+
+    return $res;
+}
+
+sub autogen_encryption_key {
+    my ($storeid) = @_;
+    my $encfile = pbs_encryption_key_file_name($storeid);
+    run_command(['proxmox-backup-client', 'key', 'create', '--kdf', 'none', $encfile]);
+};
+
+sub get_snapshots {
+    my ($scfg, $remote, $opts) = @_;
+
+    my $param = [];
+    if (defined($opts->{group})) {
+	push @$param, $opts->{group};
+    }
+
+    return run_client_cmd($scfg, $remote, "snapshots", $param);
+}
+
+sub backup_tree {
+    my ($scfg, $remote, $opts) = @_;
+
+    my $type = delete $opts->{type};
+    die "backup-type not provided\n" if !defined($type);
+    my $id = delete $opts->{id};
+    die "backup-id not provided\n" if !defined($id);
+    my $root = delete $opts->{root};
+    die "root dir not provided\n" if !defined($root);
+    my $pxarname = delete $opts->{pxarname};
+    die "archive name not provided\n" if !defined($pxarname);
+    my $time = delete $opts->{time};
+
+    my $param = [];
+
+    push @$param, "$pxarname.pxar:$root";
+    push @$param, '--backup-type', $type;
+    push @$param, '--backup-id', $id;
+    push @$param, '--backup-time', $time if defined($time);
+
+    return run_raw_client_cmd($scfg, $remote, 'backup', $param, %$opts);
+}
+
+sub restore_pxar {
+    my ($scfg, $remote, $opts) = @_;
+
+    my $snapshot = delete $opts->{snapshot};
+    die "snapshot not provided\n" if !defined($snapshot);
+    my $pxarname = delete $opts->{pxarname};
+    die "archive name not provided\n" if !defined($pxarname);
+    my $target = delete $opts->{target};
+    die "restore-target not provided\n" if !defined($target);
+    #my $time = delete $opts->{time};
+
+    my $param = [];
+
+    push @$param, "$snapshot";
+    push @$param, "$pxarname.pxar";
+    push @$param, "$target";
+    push @$param, "--allow-existing-dirs", 0;
+
+    return run_raw_client_cmd($scfg, $remote, 'restore', $param, %$opts);
+}
+
+sub forget_snapshot {
+    my ($scfg, $remote, $snapshot) = @_;
+
+    die "snapshot not provided\n" if !defined($snapshot);
+
+    my $param = [];
+
+    push @$param, "$snapshot";
+
+    return run_raw_client_cmd($scfg, $remote, 'forget', $param);
+}
+
+sub prune_group {
+    my ($scfg, $remote, $opts, $prune_opts, $group) = @_;
+
+    die "group not provided\n" if !defined($group);
+
+    # do nothing if no keep options specified for remote
+    return [] if scalar(keys %$prune_opts) == 0;
+
+    my $param = [];
+
+    push @$param, "--quiet";
+
+    if (defined($opts->{'dry-run'}) && $opts->{'dry-run'}) {
+	push @$param, "--dry-run", $opts->{'dry-run'};
+    }
+
+    foreach my $keep_opt (keys %$prune_opts) {
+	push @$param, "--$keep_opt", $prune_opts->{$keep_opt};
+    }
+    push @$param, "$group";
+
+    return run_client_cmd($scfg, $remote, 'prune', $param);
+}
+
+sub status {
+    my ($class, $storeid, $scfg) = @_;
+
+    my $total = 0;
+    my $free = 0;
+    my $used = 0;
+    my $active = 0;
+
+    eval {
+	my $res = run_client_cmd($scfg, $storeid, "status");
+
+	$active = 1;
+	$total = $res->{total};
+	$used = $res->{used};
+	$free = $res->{avail};
+    };
+    if (my $err = $@) {
+	warn $err;
+    }
+
+    return ($total, $free, $used, $active);
+}
+
+1;
-- 
2.20.1





^ permalink raw reply	[flat|nested] 26+ messages in thread

* [pmg-devel] [PATCH pmg-api 01/11] drop left-over commented out code
  2020-10-28 18:54 [pmg-devel] [PATCH pve-common/api/gui] add initial PBS integration Stoiko Ivanov
  2020-10-28 18:54 ` [pmg-devel] [PATCH pve-common 1/2] Systemd: add helpers for parsing unit files Stoiko Ivanov
  2020-10-28 18:54 ` [pmg-devel] [PATCH pve-common 2/2] add helper module for handling PBS Integration Stoiko Ivanov
@ 2020-10-28 18:54 ` Stoiko Ivanov
  2020-10-30  5:58   ` [pmg-devel] applied: " Dietmar Maurer
  2020-10-28 18:54 ` [pmg-devel] [PATCH pmg-api 02/11] Backup: split backup creation and creating tar Stoiko Ivanov
                   ` (13 subsequent siblings)
  16 siblings, 1 reply; 26+ messages in thread
From: Stoiko Ivanov @ 2020-10-28 18:54 UTC (permalink / raw)
  To: pmg-devel

Signed-off-by: Stoiko Ivanov <s.ivanov@proxmox.com>
---
 src/PMG/Backup.pm | 2 --
 1 file changed, 2 deletions(-)

diff --git a/src/PMG/Backup.pm b/src/PMG/Backup.pm
index 11eff5f..025bac2 100644
--- a/src/PMG/Backup.pm
+++ b/src/PMG/Backup.pm
@@ -186,8 +186,6 @@ sub pmg_backup {
 
 	push @$extra_cfgs, $sa_custom_config_fn;
 
-	#push @$extra_cfgs, '/etc/postfix/tls_policy';
-
 	my $extradb = $include_statistics ? $statfn : '';
 
 	my $extra = join(' ', @$extra_cfgs);
-- 
2.20.1





^ permalink raw reply	[flat|nested] 26+ messages in thread

* [pmg-devel] [PATCH pmg-api 02/11] Backup: split backup creation and creating tar
  2020-10-28 18:54 [pmg-devel] [PATCH pve-common/api/gui] add initial PBS integration Stoiko Ivanov
                   ` (2 preceding siblings ...)
  2020-10-28 18:54 ` [pmg-devel] [PATCH pmg-api 01/11] drop left-over commented out code Stoiko Ivanov
@ 2020-10-28 18:54 ` Stoiko Ivanov
  2020-10-30  6:20   ` [pmg-devel] applied: " Dietmar Maurer
  2020-10-28 18:54 ` [pmg-devel] [PATCH pmg-api 03/11] Restore: optionally restore from directory Stoiko Ivanov
                   ` (12 subsequent siblings)
  16 siblings, 1 reply; 26+ messages in thread
From: Stoiko Ivanov @ 2020-10-28 18:54 UTC (permalink / raw)
  To: pmg-devel

In preparation for integrating PMG with PBS split the current creation of
a PMG backup into 2 methods:
* create all files in a backup in a target directory
* create a tarball from a backup in a temporary directory

use the changed method in the backup API call.

Signed-off-by: Stoiko Ivanov <s.ivanov@proxmox.com>
---
 src/PMG/API2/Backup.pm |  2 +-
 src/PMG/Backup.pm      | 41 ++++++++++++++++++++++++++++++++---------
 2 files changed, 33 insertions(+), 10 deletions(-)

diff --git a/src/PMG/API2/Backup.pm b/src/PMG/API2/Backup.pm
index 0bfcfc9..08c06b5 100644
--- a/src/PMG/API2/Backup.pm
+++ b/src/PMG/API2/Backup.pm
@@ -131,7 +131,7 @@ __PACKAGE__->register_method ({
 
 	    print "starting backup to: $filename\n";
 
-	    PMG::Backup::pmg_backup($filename, $param->{statistic});
+	    PMG::Backup::pmg_backup_pack($filename, $param->{statistic});
 
 	    print "backup finished\n";
 
diff --git a/src/PMG/Backup.pm b/src/PMG/Backup.pm
index 025bac2..40c41f3 100644
--- a/src/PMG/Backup.pm
+++ b/src/PMG/Backup.pm
@@ -133,10 +133,11 @@ sub dumpstatdb {
 }
 
 sub pmg_backup {
-    my ($filename, $include_statistics) = @_;
+    my ($dirname, $include_statistics) = @_;
+
+    die "No backupdir provided!\n" if !defined($dirname);
 
     my $time = time;
-    my $dirname = "/tmp/proxbackup_$$.$time";
     my $dbfn = "Proxmox_ruledb.sql";
     my $statfn = "Proxmox_statdb.sql";
     my $tarfn = "config_backup.tar";
@@ -145,12 +146,7 @@ sub pmg_backup {
 
     eval {
 
-	my $targetdir = dirname($filename);
-	mkdir $targetdir; # try to create target dir
-	-d $targetdir ||
-	    die "unable to access target directory '$targetdir'\n";
-
-	# create a temporary directory
+	# create backup directory
 	mkdir $dirname;
 
 	# dump the database first
@@ -197,7 +193,34 @@ sub pmg_backup {
 	system("cd $dirname; md5sum $tarfn $dbfn $extradb $verfn> $sigfn") == 0 ||
 	    die "unable to create backup signature: ERROR";
 
-	system("rm -f $filename; tar czf $filename -C $dirname $verfn $sigfn $dbfn $extradb $tarfn") == 0 ||
+    };
+    my $err = $@;
+
+    if ($err) {
+	die $err;
+    }
+}
+
+sub pmg_backup_pack {
+    my ($filename, $include_statistics) = @_;
+
+    my $time = time;
+    my $dirname = "/tmp/proxbackup_$$.$time";
+
+    eval {
+
+	my $targetdir = dirname($filename);
+	mkdir $targetdir; # try to create target dir
+	-d $targetdir ||
+	    die "unable to access target directory '$targetdir'\n";
+
+	rmtree $dirname;
+	# create backup directory
+	mkdir $dirname;
+
+	pmg_backup($dirname, $include_statistics);
+
+	system("rm -f $filename; tar czf $filename --strip-components=1 -C $dirname .") == 0 ||
 	    die "unable to create backup archive: ERROR";
     };
     my $err = $@;
-- 
2.20.1





^ permalink raw reply	[flat|nested] 26+ messages in thread

* [pmg-devel] [PATCH pmg-api 03/11] Restore: optionally restore from directory
  2020-10-28 18:54 [pmg-devel] [PATCH pve-common/api/gui] add initial PBS integration Stoiko Ivanov
                   ` (3 preceding siblings ...)
  2020-10-28 18:54 ` [pmg-devel] [PATCH pmg-api 02/11] Backup: split backup creation and creating tar Stoiko Ivanov
@ 2020-10-28 18:54 ` Stoiko Ivanov
  2020-10-30  6:26   ` [pmg-devel] applied: " Dietmar Maurer
  2020-10-28 18:54 ` [pmg-devel] [PATCH pmg-api 04/11] Backup: push restore options to PMG::Backup Stoiko Ivanov
                   ` (11 subsequent siblings)
  16 siblings, 1 reply; 26+ messages in thread
From: Stoiko Ivanov @ 2020-10-28 18:54 UTC (permalink / raw)
  To: pmg-devel

In preparation for integrating PMG with PBS decide based on the type of the
provided filename, whether or not to untar:
* if it's a directory skip untarring (PBS)
* if it's a filename untar (local backup)

Signed-off-by: Stoiko Ivanov <s.ivanov@proxmox.com>
---
 src/PMG/Backup.pm | 19 +++++++++++++++----
 1 file changed, 15 insertions(+), 4 deletions(-)

diff --git a/src/PMG/Backup.pm b/src/PMG/Backup.pm
index 40c41f3..aec7905 100644
--- a/src/PMG/Backup.pm
+++ b/src/PMG/Backup.pm
@@ -245,12 +245,23 @@ sub pmg_restore {
     my $tarfn = "config_backup.tar";
     my $sigfn = "proxmox_backup_v1.md5";
 
+    my $untar = 1;
+
+    # directory indicates that the files were restored from a PBS remote
+    if ( -d $filename ) {
+	$dirname = $filename;
+	$untar = 0;
+    }
+
     eval {
-	# create a temporary directory
-	mkdir $dirname;
 
-	system("cd $dirname; tar xzf $filename >/dev/null 2>&1") == 0 ||
-	    die "unable to extract backup archive: ERROR";
+	if ($untar) {
+	    # create a temporary directory
+	    mkdir $dirname;
+
+	    system("cd $dirname; tar xzf $filename >/dev/null 2>&1") == 0 ||
+		die "unable to extract backup archive: ERROR";
+	}
 
 	system("cd $dirname; md5sum -c $sigfn") == 0 ||
 	    die "proxmox backup signature check failed: ERROR";
-- 
2.20.1





^ permalink raw reply	[flat|nested] 26+ messages in thread

* [pmg-devel] [PATCH pmg-api 04/11] Backup: push restore options to PMG::Backup
  2020-10-28 18:54 [pmg-devel] [PATCH pve-common/api/gui] add initial PBS integration Stoiko Ivanov
                   ` (4 preceding siblings ...)
  2020-10-28 18:54 ` [pmg-devel] [PATCH pmg-api 03/11] Restore: optionally restore from directory Stoiko Ivanov
@ 2020-10-28 18:54 ` Stoiko Ivanov
  2020-10-30  6:32   ` [pmg-devel] applied: " Dietmar Maurer
  2020-10-28 18:54 ` [pmg-devel] [PATCH pmg-api 05/11] debian: add dependency on proxmox-backup-client Stoiko Ivanov
                   ` (10 subsequent siblings)
  16 siblings, 1 reply; 26+ messages in thread
From: Stoiko Ivanov @ 2020-10-28 18:54 UTC (permalink / raw)
  To: pmg-devel

that way they can be reused for the PBS restore API call

Signed-off-by: Stoiko Ivanov <s.ivanov@proxmox.com>
---
 src/PMG/API2/Backup.pm | 19 +------------------
 src/PMG/Backup.pm      | 24 ++++++++++++++++++++++++
 2 files changed, 25 insertions(+), 18 deletions(-)

diff --git a/src/PMG/API2/Backup.pm b/src/PMG/API2/Backup.pm
index 08c06b5..4ea28d1 100644
--- a/src/PMG/API2/Backup.pm
+++ b/src/PMG/API2/Backup.pm
@@ -202,26 +202,9 @@ __PACKAGE__->register_method ({
     parameters => {
 	additionalProperties => 0,
 	properties => {
+	    PMG::Backup::get_restore_options(),
 	    node => get_standard_option('pve-node'),
 	    filename => $backup_filename_property,
-	    config => {
-		description => "Restore system configuration.",
-		type => 'boolean',
-		optional => 1,
-		default => 0,
-	    },
-	    database => {
-		description => "Restore the rule database. This is the default.",
-		type => 'boolean',
-		optional => 1,
-		default => 1,
-	    },
-	    statistic => {
-		description => "Restore statistic databases. Only considered when you restore the 'database'.",
-		type => 'boolean',
-		optional => 1,
-		default => 0,
-	    },
 	},
     },
     returns => { type => "string" },
diff --git a/src/PMG/Backup.pm b/src/PMG/Backup.pm
index aec7905..5e11684 100644
--- a/src/PMG/Backup.pm
+++ b/src/PMG/Backup.pm
@@ -6,6 +6,7 @@ use Data::Dumper;
 use File::Basename;
 use File::Path;
 
+use PVE::JSONSchema qw(get_standard_option);
 use PVE::Tools;
 
 use PMG::pmgcfg;
@@ -14,6 +15,29 @@ use PMG::Utils qw(postgres_admin_cmd);
 
 my $sa_custom_config_fn = "/etc/mail/spamassassin/custom.cf";
 
+sub get_restore_options {
+    return (
+	node => get_standard_option('pve-node'),
+	config => {
+	    description => "Restore system configuration.",
+	    type => 'boolean',
+	    optional => 1,
+	    default => 0,
+	},
+	database => {
+	    description => "Restore the rule database. This is the default.",
+	    type => 'boolean',
+	    optional => 1,
+	    default => 1,
+	},
+	statistic => {
+	    description => "Restore statistic databases. Only considered when you restore the 'database'.",
+	    type => 'boolean',
+	    optional => 1,
+	    default => 0,
+	});
+}
+
 sub dump_table {
     my ($dbh, $table, $ofh, $seq, $seqcol) = @_;
 
-- 
2.20.1





^ permalink raw reply	[flat|nested] 26+ messages in thread

* [pmg-devel] [PATCH pmg-api 05/11] debian: add dependency on proxmox-backup-client
  2020-10-28 18:54 [pmg-devel] [PATCH pve-common/api/gui] add initial PBS integration Stoiko Ivanov
                   ` (5 preceding siblings ...)
  2020-10-28 18:54 ` [pmg-devel] [PATCH pmg-api 04/11] Backup: push restore options to PMG::Backup Stoiko Ivanov
@ 2020-10-28 18:54 ` Stoiko Ivanov
  2020-10-30  6:33   ` [pmg-devel] applied: " Dietmar Maurer
  2020-10-28 18:54 ` [pmg-devel] [PATCH pmg-api 06/11] add initial SectionConfig for pbs Stoiko Ivanov
                   ` (9 subsequent siblings)
  16 siblings, 1 reply; 26+ messages in thread
From: Stoiko Ivanov @ 2020-10-28 18:54 UTC (permalink / raw)
  To: pmg-devel

Signed-off-by: Stoiko Ivanov <s.ivanov@proxmox.com>
---
 debian/control | 1 +
 1 file changed, 1 insertion(+)

diff --git a/debian/control b/debian/control
index af10bc0..2460411 100644
--- a/debian/control
+++ b/debian/control
@@ -76,6 +76,7 @@ Depends: apt,
          pmg-log-tracker,
          postfix (>= 2.5.5),
          postgresql-11,
+         proxmox-backup-client,,
          proxmox-mini-journalreader,
          proxmox-spamassassin,
          pve-xtermjs (>= 1.0-1),
-- 
2.20.1





^ permalink raw reply	[flat|nested] 26+ messages in thread

* [pmg-devel] [PATCH pmg-api 06/11] add initial SectionConfig for pbs
  2020-10-28 18:54 [pmg-devel] [PATCH pve-common/api/gui] add initial PBS integration Stoiko Ivanov
                   ` (6 preceding siblings ...)
  2020-10-28 18:54 ` [pmg-devel] [PATCH pmg-api 05/11] debian: add dependency on proxmox-backup-client Stoiko Ivanov
@ 2020-10-28 18:54 ` Stoiko Ivanov
  2020-10-30  6:38   ` Dietmar Maurer
  2020-10-28 18:54 ` [pmg-devel] [PATCH pmg-api 07/11] Add API2 module for PBS configuration Stoiko Ivanov
                   ` (8 subsequent siblings)
  16 siblings, 1 reply; 26+ messages in thread
From: Stoiko Ivanov @ 2020-10-28 18:54 UTC (permalink / raw)
  To: pmg-devel

add a SectionConfig definition to hold information about PBS-remotes used
for backing up PMG.

Mostly adapted from the PBSPlugin.pm in pve-storage.

Signed-off-by: Stoiko Ivanov <s.ivanov@proxmox.com>
---
This commit needs a versioned dependency on pve-common

 debian/dirs          |   1 +
 src/Makefile         |   1 +
 src/PMG/PBSConfig.pm | 209 +++++++++++++++++++++++++++++++++++++++++++
 3 files changed, 211 insertions(+)
 create mode 100644 src/PMG/PBSConfig.pm

diff --git a/debian/dirs b/debian/dirs
index f7ac2e7..f138bb4 100644
--- a/debian/dirs
+++ b/debian/dirs
@@ -1,4 +1,5 @@
 /etc/pmg
 /etc/pmg/dkim
+/etc/pmg/pbs
 /var/lib/pmg
 /var/lib/pmg/backup
diff --git a/src/Makefile b/src/Makefile
index 05d9598..daa9d46 100644
--- a/src/Makefile
+++ b/src/Makefile
@@ -66,6 +66,7 @@ LIBSOURCES =				\
 	PMG/SMTP.pm			\
 	PMG/Unpack.pm			\
 	PMG/Backup.pm			\
+	PMG/PBSConfig.pm		\
 	PMG/RuleCache.pm		\
 	PMG/Statistic.pm		\
 	PMG/UserConfig.pm		\
diff --git a/src/PMG/PBSConfig.pm b/src/PMG/PBSConfig.pm
new file mode 100644
index 0000000..768f36c
--- /dev/null
+++ b/src/PMG/PBSConfig.pm
@@ -0,0 +1,209 @@
+package PMG::PBSConfig;
+
+# section config implementation for PBS integration in PMG
+
+use strict;
+use warnings;
+
+use PVE::Tools qw(extract_param);
+use PVE::SectionConfig;
+use PVE::JSONSchema qw(get_standard_option);
+use PVE::PBSTools;
+
+use base qw(PVE::SectionConfig);
+
+my $inotify_file_id = 'pmg-pbs.conf';
+my $secret_dir = '/etc/pmg/pbs';
+my $config_filename = "${secret_dir}/pbs.conf";
+
+
+my %prune_option = (
+    optional => 1,
+    type => 'integer', minimum => '0',
+    format_description => 'N',
+);
+
+my %prune_properties = (
+    'keep-last' => {
+	%prune_option,
+	description => 'Keep the last <N> backups.',
+    },
+    'keep-hourly' => {
+	%prune_option,
+	description => 'Keep backups for the last <N> different hours. If there is more' .
+		       'than one backup for a single hour, only the latest one is kept.'
+    },
+    'keep-daily' => {
+	%prune_option,
+	description => 'Keep backups for the last <N> different days. If there is more' .
+		       'than one backup for a single day, only the latest one is kept.'
+    },
+    'keep-weekly' => {
+	%prune_option,
+	description => 'Keep backups for the last <N> different weeks. If there is more' .
+		       'than one backup for a single week, only the latest one is kept.'
+    },
+    'keep-monthly' => {
+	%prune_option,
+	description => 'Keep backups for the last <N> different months. If there is more' .
+		       'than one backup for a single month, only the latest one is kept.'
+    },
+    'keep-yearly' => {
+	%prune_option,
+	description => 'Keep backups for the last <N> different years. If there is more' .
+		       'than one backup for a single year, only the latest one is kept.'
+    },
+);
+
+my $defaultData = {
+    propertyList => {
+	type => { description => "Section type." },
+	remote => {
+	    description => "Proxmox Backup Server ID.",
+	    type => 'string', format => 'pve-configid',
+	},
+    },
+};
+
+sub properties {
+    return {
+	datastore => {
+	    description => "Proxmox backup server datastore name.",
+	    type => 'string',
+	},
+	server => {
+	    description => "Proxmox backup server address.",
+	    type => 'string', format => 'address',
+	    maxLength => 256,
+	},
+	disable => {
+	    description => "Flag to disable/deactivate the entry.",
+	    type => 'boolean',
+	    optional => 1,
+	},
+	password => {
+	    description => "Password for the user on the Proxmox backup server.",
+	    type => 'string',
+	    optional => 1,
+	},
+	username => get_standard_option('pmg-email-address', {
+	    description => "Username on the Proxmox backup server"
+	}),
+	fingerprint => get_standard_option('fingerprint-sha256'),
+	'encryption-key' => {
+	    description => "Encryption key. Use 'autogen' to generate one automatically without passphrase.",
+	    type => 'string',
+	    optional => 1,
+	},
+	%prune_properties,
+    };
+}
+
+sub options {
+    return {
+	server => {},
+	datastore => {},
+	disable => { optional => 1 },
+	username => { optional => 1 },
+	password => { optional => 1 },
+	'encryption-key' => { optional => 1 },
+	fingerprint => { optional => 1 },
+	'keep-last' => { optional => 1 },
+	'keep-hourly' =>  { optional => 1 },
+	'keep-daily' => { optional => 1 },
+	'keep-weekly' => { optional => 1 },
+	'keep-monthly' => { optional => 1 },
+	'keep-yearly' => { optional => 1 },
+    };
+}
+
+sub type {
+    return 'pbs';
+}
+
+sub private {
+    return $defaultData;
+}
+
+sub prune_options {
+    my ($self, $remote) = @_;
+
+    my $remote_cfg = $self->{ids}->{$remote};
+
+    my $res = {};
+
+    foreach my $keep_opt (keys %prune_properties) {
+
+	if (defined($remote_cfg->{$keep_opt})) {
+	    $res->{$keep_opt} = $remote_cfg->{$keep_opt};
+	}
+    }
+    return $res;
+}
+
+sub parse_config {
+    my ($class, $filename, $raw) = @_;
+
+    my $cfg = $class->SUPER::parse_config($filename, $raw);
+
+    PVE::PBSTools::set_secret_dir($secret_dir);
+
+    return $cfg;
+}
+
+sub new {
+    my ($type) = @_;
+
+    my $class = ref($type) || $type;
+
+    my $cfg = PVE::INotify::read_file($inotify_file_id);
+
+    return bless $cfg, $class;
+}
+
+sub write {
+    my ($self) = @_;
+
+    PVE::INotify::write_file($inotify_file_id, $self);
+}
+
+my $lockfile = "/var/lock/pmgpbsconfig.lck";
+
+sub lock_config {
+    my ($code, $errmsg) = @_;
+
+    my $p = PVE::Tools::lock_file($lockfile, undef, $code);
+    if (my $err = $@) {
+	$errmsg ? die "$errmsg: $err" : die $err;
+    }
+}
+
+
+__PACKAGE__->register();
+__PACKAGE__->init();
+
+sub read_pmg_pbs_conf {
+    my ($filename, $fh) = @_;
+
+    local $/ = undef; # slurp mode
+
+    my $raw = defined($fh) ? <$fh> : '';
+
+    return __PACKAGE__->parse_config($filename, $raw);
+}
+
+sub write_pmg_pbs_conf {
+    my ($filename, $fh, $cfg) = @_;
+
+    my $raw = __PACKAGE__->write_config($filename, $cfg);
+
+    PVE::Tools::safe_print($filename, $fh, $raw);
+}
+
+PVE::INotify::register_file($inotify_file_id, $config_filename,
+			    \&read_pmg_pbs_conf,
+			    \&write_pmg_pbs_conf,
+			    undef,
+			    always_call_parser => 1);
+
+1;
-- 
2.20.1





^ permalink raw reply	[flat|nested] 26+ messages in thread

* [pmg-devel] [PATCH pmg-api 07/11] Add API2 module for PBS configuration
  2020-10-28 18:54 [pmg-devel] [PATCH pve-common/api/gui] add initial PBS integration Stoiko Ivanov
                   ` (7 preceding siblings ...)
  2020-10-28 18:54 ` [pmg-devel] [PATCH pmg-api 06/11] add initial SectionConfig for pbs Stoiko Ivanov
@ 2020-10-28 18:54 ` Stoiko Ivanov
  2020-10-28 18:54 ` [pmg-devel] [PATCH pmg-api 08/11] Add API2 module for per-node backups to PBS Stoiko Ivanov
                   ` (7 subsequent siblings)
  16 siblings, 0 replies; 26+ messages in thread
From: Stoiko Ivanov @ 2020-10-28 18:54 UTC (permalink / raw)
  To: pmg-devel

The module provides the API methods for creating/updating/listing/deleting
PBS remotes

Signed-off-by: Stoiko Ivanov <s.ivanov@proxmox.com>
---
 src/Makefile               |   1 +
 src/PMG/API2/Config.pm     |   7 ++
 src/PMG/API2/PBS/Remote.pm | 248 +++++++++++++++++++++++++++++++++++++
 3 files changed, 256 insertions(+)
 create mode 100644 src/PMG/API2/PBS/Remote.pm

diff --git a/src/Makefile b/src/Makefile
index daa9d46..5add6af 100644
--- a/src/Makefile
+++ b/src/Makefile
@@ -137,6 +137,7 @@ LIBSOURCES =				\
 	PMG/API2/Statistics.pm		\
 	PMG/API2/MailTracker.pm		\
 	PMG/API2/Backup.pm		\
+	PMG/API2/PBS/Remote.pm		\
 	PMG/API2/Nodes.pm		\
 	PMG/API2/Postfix.pm		\
 	PMG/API2/Quarantine.pm		\
diff --git a/src/PMG/API2/Config.pm b/src/PMG/API2/Config.pm
index d4a9679..e11eb3f 100644
--- a/src/PMG/API2/Config.pm
+++ b/src/PMG/API2/Config.pm
@@ -25,6 +25,7 @@ use PMG::API2::Fetchmail;
 use PMG::API2::DestinationTLSPolicy;
 use PMG::API2::DKIMSign;
 use PMG::API2::SACustom;
+use PMG::API2::PBS::Remote;
 
 use base qw(PVE::RESTHandler);
 
@@ -93,6 +94,11 @@ __PACKAGE__->register_method({
     path => 'customscores',
 });
 
+__PACKAGE__->register_method ({
+    subclass => "PMG::API2::PBS::Remote",
+    path => 'pbs',
+});
+
 __PACKAGE__->register_method ({
     name => 'index', 
     path => '',
@@ -131,6 +137,7 @@ __PACKAGE__->register_method ({
 	push @$res, { section => 'regextest' };
 	push @$res, { section => 'tlspolicy' };
 	push @$res, { section => 'dkim' };
+	push @$res, { section => 'pbs' };
 
 	return $res;
     }});
diff --git a/src/PMG/API2/PBS/Remote.pm b/src/PMG/API2/PBS/Remote.pm
new file mode 100644
index 0000000..1c1d93f
--- /dev/null
+++ b/src/PMG/API2/PBS/Remote.pm
@@ -0,0 +1,248 @@
+package PMG::API2::PBS::Remote;
+
+use strict;
+use warnings;
+
+use PVE::SafeSyslog;
+use PVE::Tools qw(extract_param);
+use PVE::JSONSchema qw(get_standard_option);
+use PVE::RESTHandler;
+use PVE::PBSTools;
+
+use PMG::PBSConfig;
+
+use base qw(PVE::RESTHandler);
+
+__PACKAGE__->register_method ({
+    name => 'list',
+    path => '',
+    method => 'GET',
+    description => "List all configured Proxmox Backup Server instances.",
+    permissions => { check => [ 'admin', 'audit' ] },
+    proxyto => 'master',
+    protected => 1,
+    parameters => {
+	additionalProperties => 0,
+	properties => {}
+    },
+    returns => {
+        type => "array",
+        items => PMG::PBSConfig->createSchema(1),
+        links => [ { rel => 'child', href => "{remote}" } ],
+    },
+    code => sub {
+	my ($param) = @_;
+
+	my $res = [];
+
+	my $conf = PMG::PBSConfig->new();
+
+	if (defined($conf)) {
+	    foreach my $remote (keys %{$conf->{ids}}) {
+		my $d = $conf->{ids}->{$remote};
+		my $entry = {
+		    remote => $remote,
+		    server => $d->{server},
+		    datastore => $d->{datastore},
+		    username => $d->{username},
+		    disable => $d->{disable},
+		    'encryption-key' => $d->{'encryption-key'},
+		};
+		push @$res, $entry;
+	    }
+	}
+
+	return $res;
+    }});
+
+__PACKAGE__->register_method ({
+    name => 'create',
+    path => '',
+    method => 'POST',
+    description => "Add Proxmox Backup Server instance.",
+    permissions => { check => [ 'admin' ] },
+    proxyto => 'master',
+    protected => 1,
+    parameters => PMG::PBSConfig->createSchema(1),
+    returns => { type => 'null' } ,
+    code => sub {
+	my ($param) = @_;
+
+	my $code = sub {
+
+	    my $conf = PMG::PBSConfig->new();
+	    $conf->{ids} //= {};
+	    my $ids = $conf->{ids};
+
+	    my $remote = extract_param($param, 'remote');
+	    die "PBS remote '$remote' already exists\n"
+		if $ids->{$remote};
+
+	    my $remotecfg = PMG::PBSConfig->check_config($remote, $param, 1);
+
+	    my $password = extract_param($remotecfg, 'password');
+	    PVE::PBSTools::pbs_set_password($remote, $password) if defined($password);
+
+	    if (defined(my $encryption_key = extract_param($remotecfg, 'encryption-key'))) {
+		if ($encryption_key eq 'autogen') {
+		    PVE::PBSTools::autogen_encryption_key($remote);
+		} else {
+		    PVE::PBSTools::pbs_set_encryption_key($remote, $encryption_key);
+		}
+		$remotecfg->{'encryption-key'} = 1;
+	    }
+
+	    $ids->{$remote} = $remotecfg;
+	    $conf->write();
+	};
+
+	PMG::PBSConfig::lock_config($code, "add PBS remote failed");
+
+	return undef;
+    }});
+
+__PACKAGE__->register_method ({
+    name => 'read_config',
+    path => '{remote}',
+    method => 'GET',
+    description => "Get PBS remote configuration.",
+    proxyto => 'master',
+    permissions => { check => [ 'admin', 'audit' ] },
+    parameters => {
+	additionalProperties => 1,
+	properties => {
+	    remote => {
+		description => "Proxmox Backup Server ID.",
+		type => 'string', format => 'pve-configid',
+	    },
+	},
+    },
+    returns => {},
+    code => sub {
+	my ($param) = @_;
+
+	my $conf = PMG::PBSConfig->new();
+
+	my $remote = $param->{remote};
+
+	my $data = $conf->{ids}->{$remote};
+	die "PBS remote '$remote' does not exist\n" if !$data;
+
+	delete $data->{type};
+
+	$data->{digest} = $conf->{digest};
+	$data->{remote} = $remote;
+
+	return $data;
+    }});
+
+__PACKAGE__->register_method ({
+    name => 'update_config',
+    path => '{remote}',
+    method => 'PUT',
+    description => "Update PBS remote settings.",
+    permissions => { check => [ 'admin' ] },
+    protected => 1,
+    proxyto => 'master',
+    parameters => PMG::PBSConfig->updateSchema(),
+    returns => { type => 'null' },
+    code => sub {
+	my ($param) = @_;
+
+	my $code = sub {
+
+	    my $conf = PMG::PBSConfig->new();
+	    my $ids = $conf->{ids};
+
+	    my $digest = extract_param($param, 'digest');
+	    PVE::SectionConfig::assert_if_modified($conf, $digest);
+
+	    my $remote = extract_param($param, 'remote');
+
+	    die "PBS remote '$remote' does not exist\n"
+		if !$ids->{$remote};
+
+	    my $delete_str = extract_param($param, 'delete');
+	    die "no options specified\n"
+		if !$delete_str && !scalar(keys %$param);
+
+	    foreach my $opt (PVE::Tools::split_list($delete_str)) {
+		if ($opt eq 'password') {
+		    PVE::PBSTools::pbs_delete_password($remote);
+		} elsif ($opt eq 'encryption-key') {
+		    PVE::PBSTools::pbs_delete_encryption_key($remote);
+		}
+
+		delete $ids->{$remote}->{$opt};
+	    }
+
+	    if (defined(my $password = extract_param($param, 'password'))) {
+		PVE::PBSTools::pbs_set_password($remote, $password);
+	    }
+	    if (defined(my $encryption_key = extract_param($param, 'encryption-key'))) {
+		if ($encryption_key eq 'autogen') {
+		    PVE::PBSTools::autogen_encryption_key($remote);
+		} else {
+		    PVE::PBSTools::pbs_set_encryption_key($remote, $encryption_key);
+		}
+		$param->{'encryption-key'} = 1;
+	    }
+
+	    my $remoteconfig = PMG::PBSConfig->check_config($remote, $param, 0, 1);
+
+	    foreach my $p (keys %$remoteconfig) {
+		$ids->{$remote}->{$p} = $remoteconfig->{$p};
+	    }
+
+	    $conf->write();
+	};
+
+	PMG::PBSConfig::lock_config($code, "update PBS remote failed");
+
+	return undef;
+    }});
+
+__PACKAGE__->register_method ({
+    name => 'delete',
+    path => '{remote}',
+    method => 'DELETE',
+    description => "Delete an PBS remote",
+    permissions => { check => [ 'admin' ] },
+    protected => 1,
+    proxyto => 'master',
+    parameters => {
+	additionalProperties => 0,
+	properties => {
+	    remote => {
+		description => "Profile ID.",
+		type => 'string', format => 'pve-configid',
+	    },
+	}
+    },
+    returns => { type => 'null' },
+    code => sub {
+	my ($param) = @_;
+
+	my $code = sub {
+
+	    my $conf = PMG::PBSConfig->new();
+	    my $ids = $conf->{ids};
+
+	    my $remote = $param->{remote};
+
+	    die "PBS remote '$remote' does not exist\n"
+		if !$ids->{$remote};
+
+	    PVE::PBSTools::pbs_delete_password($remote);
+	    PVE::PBSTools::pbs_delete_encryption_key($remote);
+	    delete $ids->{$remote};
+
+	    $conf->write();
+	};
+
+	PMG::PBSConfig::lock_config($code, "delete PBS remote failed");
+
+	return undef;
+    }});
+
+1;
-- 
2.20.1





^ permalink raw reply	[flat|nested] 26+ messages in thread

* [pmg-devel] [PATCH pmg-api 08/11] Add API2 module for per-node backups to PBS
  2020-10-28 18:54 [pmg-devel] [PATCH pve-common/api/gui] add initial PBS integration Stoiko Ivanov
                   ` (8 preceding siblings ...)
  2020-10-28 18:54 ` [pmg-devel] [PATCH pmg-api 07/11] Add API2 module for PBS configuration Stoiko Ivanov
@ 2020-10-28 18:54 ` Stoiko Ivanov
  2020-10-28 18:54 ` [pmg-devel] [PATCH pmg-api 09/11] pbs-integration: add CLI calls to pmgbackup Stoiko Ivanov
                   ` (6 subsequent siblings)
  16 siblings, 0 replies; 26+ messages in thread
From: Stoiko Ivanov @ 2020-10-28 18:54 UTC (permalink / raw)
  To: pmg-devel

The module adds API2 methods for:

* creating/restoring/listing/forgetting backups on a configured PBS remote

Signed-off-by: Stoiko Ivanov <s.ivanov@proxmox.com>
---
 src/Makefile            |   1 +
 src/PMG/API2/Nodes.pm   |   7 +
 src/PMG/API2/PBS/Job.pm | 413 ++++++++++++++++++++++++++++++++++++++++
 3 files changed, 421 insertions(+)
 create mode 100644 src/PMG/API2/PBS/Job.pm

diff --git a/src/Makefile b/src/Makefile
index 5add6af..fb42f21 100644
--- a/src/Makefile
+++ b/src/Makefile
@@ -137,6 +137,7 @@ LIBSOURCES =				\
 	PMG/API2/Statistics.pm		\
 	PMG/API2/MailTracker.pm		\
 	PMG/API2/Backup.pm		\
+	PMG/API2/PBS/Job.pm		\
 	PMG/API2/PBS/Remote.pm		\
 	PMG/API2/Nodes.pm		\
 	PMG/API2/Postfix.pm		\
diff --git a/src/PMG/API2/Nodes.pm b/src/PMG/API2/Nodes.pm
index 96aa146..259f8f3 100644
--- a/src/PMG/API2/Nodes.pm
+++ b/src/PMG/API2/Nodes.pm
@@ -26,6 +26,7 @@ use PMG::API2::SpamAssassin;
 use PMG::API2::Postfix;
 use PMG::API2::MailTracker;
 use PMG::API2::Backup;
+use PMG::API2::PBS::Job;
 
 use base qw(PVE::RESTHandler);
 
@@ -79,6 +80,11 @@ __PACKAGE__->register_method ({
     path => 'backup',
 });
 
+__PACKAGE__->register_method ({
+    subclass => "PMG::API2::PBS::Job",
+    path => 'pbs',
+});
+
 __PACKAGE__->register_method ({
     name => 'index',
     path => '',
@@ -105,6 +111,7 @@ __PACKAGE__->register_method ({
 	my $result = [
 	    { name => 'apt' },
 	    { name => 'backup' },
+	    { name => 'pbs' },
 	    { name => 'clamav' },
 	    { name => 'spamassassin' },
 	    { name => 'postfix' },
diff --git a/src/PMG/API2/PBS/Job.pm b/src/PMG/API2/PBS/Job.pm
new file mode 100644
index 0000000..80a1f29
--- /dev/null
+++ b/src/PMG/API2/PBS/Job.pm
@@ -0,0 +1,413 @@
+package PMG::API2::PBS::Job;
+
+use strict;
+use warnings;
+
+use POSIX qw(strftime);
+
+use PVE::JSONSchema qw(get_standard_option);
+use PVE::RESTHandler;
+use PVE::SafeSyslog;
+use PVE::Tools qw(extract_param);
+use PVE::PBSTools;
+
+use PMG::RESTEnvironment;
+use PMG::Backup;
+use PMG::PBSConfig;
+
+use base qw(PVE::RESTHandler);
+
+__PACKAGE__->register_method ({
+    name => 'list',
+    path => '',
+    method => 'GET',
+    description => "List all configured Proxmox Backup Server jobs.",
+    permissions => { check => [ 'admin', 'audit' ] },
+    proxyto => 'node',
+    protected => 1,
+    parameters => {
+	additionalProperties => 0,
+	properties => {
+	    node => get_standard_option('pve-node'),
+	},
+    },
+    returns => {
+        type => "array",
+        items => PMG::PBSConfig->createSchema(1),
+        links => [ { rel => 'child', href => "{remote}" } ],
+    },
+    code => sub {
+	my ($param) = @_;
+
+	my $res = [];
+
+	my $conf = PMG::PBSConfig->new();
+	if (defined($conf)) {
+	    foreach my $remote (keys %{$conf->{ids}}) {
+		my $d = $conf->{ids}->{$remote};
+		my $entry = {
+		    remote => $remote,
+		    server => $d->{server},
+		    datastore => $d->{datastore},
+		};
+		push @$res, $entry;
+	    }
+	}
+
+	return $res;
+    }});
+
+__PACKAGE__->register_method ({
+    name => 'remote_index',
+    path => '{remote}',
+    method => 'GET',
+    description => "Backup Job index.",
+    parameters => {
+	additionalProperties => 0,
+	properties => {
+	    node => get_standard_option('pve-node'),
+	    remote => {
+		description => "Proxmox Backup Server ID.",
+		type => 'string', format => 'pve-configid',
+	    },
+	},
+    },
+    returns => {
+	type => 'array',
+	items => {
+	    type => "object",
+	    properties => { section => { type => 'string'} },
+	},
+	links => [ { rel => 'child', href => "{section}" } ],
+    },
+    code => sub {
+	my ($param) = @_;
+
+	my $result = [
+	    { section => 'snapshots' },
+	    { section => 'backup' },
+	    { section => 'restore' },
+	    { section => 'timer' },
+	    { section => 'prune' },
+	];
+	return $result;
+}});
+
+__PACKAGE__->register_method ({
+    name => 'get_snapshots',
+    path => '{remote}/snapshots',
+    method => 'GET',
+    description => "Get snapshots stored on remote.",
+    proxyto => 'node',
+    protected => 1,
+    permissions => { check => [ 'admin', 'audit' ] },
+    parameters => {
+	additionalProperties => 0,
+	properties => {
+	    node => get_standard_option('pve-node'),
+	    remote => {
+		description => "Proxmox Backup Server ID.",
+		type => 'string', format => 'pve-configid',
+	    },
+	},
+    },
+    returns => {
+	type => 'array',
+	items => {
+	    type => "object",
+	    properties => {
+		time => { type => 'string'},
+		ctime => { type => 'string'},
+		size => { type => 'integer'},
+		encrypted => { type => 'boolean'},
+	    },
+	},
+	links => [ { rel => 'child', href => "{time}" } ],
+    },
+    code => sub {
+	my ($param) = @_;
+
+	my $remote = $param->{remote};
+	my $node = $param->{node};
+
+	my $conf = PMG::PBSConfig->new();
+
+	my $remote_config = $conf->{ids}->{$remote};
+	die "PBS remote '$remote' does not exist\n" if !$remote_config;
+
+	return [] if $remote_config->{disable};
+
+	my $snap_param = {
+	    group => "host/$node",
+	};
+	my $snapshots = PVE::PBSTools::get_snapshots($remote_config, $remote, $snap_param);
+	my $res = [];
+	foreach my $item (@$snapshots) {
+	    my $btype = $item->{"backup-type"};
+	    my $bid = $item->{"backup-id"};
+	    my $epoch = $item->{"backup-time"};
+	    my $size = $item->{size} // 1;
+
+	    my @pxar = grep { $_->{filename} eq 'pmgbackup.pxar.didx' } @{$item->{files}};
+	    die "unexpected number of pmgbackup archives in snapshot\n" if (scalar(@pxar) != 1);
+
+	    my $encrypted = ($pxar[0]->{'crypt-mode'} eq 'encrypt') ? 1 : 0;
+
+
+	    next if !($btype eq 'host');
+	    next if !($bid eq $node);
+
+	    my $time = strftime("%FT%TZ", gmtime($epoch));
+
+	    my $info = {
+		time => $time,
+		ctime => $epoch,
+		size => $size,
+		encrypted => $encrypted,
+	    };
+
+	    push @$res, $info;
+	}
+
+	return $res;
+    }});
+
+__PACKAGE__->register_method ({
+    name => 'forget_snapshot',
+    path => '{remote}/snapshots/{time}',
+    method => 'DELETE',
+    description => "Forget a snapshot",
+    proxyto => 'node',
+    protected => 1,
+    permissions => { check => [ 'admin', 'audit' ] },
+    parameters => {
+	additionalProperties => 0,
+	properties => {
+	    node => get_standard_option('pve-node'),
+	    remote => {
+		description => "Proxmox Backup Server ID.",
+		type => 'string', format => 'pve-configid',
+	    },
+	    time => {
+		description => "Backup time in RFC 3399 format",
+		type => 'string',
+	    },
+	},
+    },
+    returns => {type => 'null' },
+    code => sub {
+	my ($param) = @_;
+
+	my $remote = $param->{remote};
+	my $node = $param->{node};
+	my $time = $param->{time};
+
+	my $snapshot = "host/$node/$time";
+
+	my $conf = PMG::PBSConfig->new();
+
+	my $remote_config = $conf->{ids}->{$remote};
+	die "PBS remote '$remote' does not exist\n" if !$remote_config;
+	die "PBS remote '$remote' is disabled\n" if $remote_config->{disable};
+
+	eval {
+	    PVE::PBSTools::forget_snapshot($remote_config, $remote, $snapshot);
+	};
+	die "Forgetting backup failed: $@" if $@;
+
+	return;
+
+    }});
+
+__PACKAGE__->register_method ({
+    name => 'run_backup',
+    path => '{remote}/backup',
+    method => 'POST',
+    description => "run backup",
+    proxyto => 'node',
+    protected => 1,
+    permissions => { check => [ 'admin', 'audit' ] },
+    parameters => {
+	additionalProperties => 0,
+	properties => {
+	    node => get_standard_option('pve-node'),
+	    remote => {
+		description => "Proxmox Backup Server ID.",
+		type => 'string', format => 'pve-configid',
+	    },
+	},
+    },
+    returns => { type => "string" },
+    code => sub {
+	my ($param) = @_;
+
+	my $rpcenv = PMG::RESTEnvironment->get();
+	my $authuser = $rpcenv->get_user();
+
+	my $remote = $param->{remote};
+	my $node = $param->{node};
+
+	my $conf = PMG::PBSConfig->new();
+
+	my $remote_config = $conf->{ids}->{$remote};
+	die "PBS remote '$remote' does not exist\n" if !$remote_config;
+	die "PBS remote '$remote' is disabled\n" if $remote_config->{disable};
+
+	my $backup_dir = "/var/lib/pmg/backup/current";
+
+	my $worker = sub {
+	    my $upid = shift;
+
+	    print "starting update of current backup state\n";
+
+	    PMG::Backup::pmg_backup($backup_dir, $param->{statistic});
+	    my $pbs_opts = {
+		type => 'host',
+		id => $node,
+		pxarname => 'pmgbackup',
+		root => $backup_dir,
+	    };
+
+	    PVE::PBSTools::backup_tree($remote_config, $remote, $pbs_opts);
+
+	    print "backup finished\n";
+
+	    return;
+	};
+
+	return $rpcenv->fork_worker('pbs_backup', undef, $authuser, $worker);
+
+    }});
+
+__PACKAGE__->register_method ({
+    name => 'restore',
+    path => '{remote}/restore',
+    method => 'POST',
+    description => "Restore the system configuration.",
+    permissions => { check => [ 'admin' ] },
+    proxyto => 'node',
+    protected => 1,
+    parameters => {
+	additionalProperties => 0,
+	properties => {
+	    PMG::Backup::get_restore_options(),
+	    remote => {
+		description => "Proxmox Backup Server ID.",
+		type => 'string', format => 'pve-configid',
+	    },
+	    'backup-time' => {description=> "backup-time to restore",
+		optional => 1, type => 'string'
+	    },
+	    'backup-id' => {description => "backup-id (hostname) of backup snapshot",
+		optional => 1, type => 'string'
+	    },
+	},
+    },
+    returns => { type => "string" },
+    code => sub {
+	my ($param) = @_;
+
+	my $rpcenv = PMG::RESTEnvironment->get();
+	my $authuser = $rpcenv->get_user();
+
+	my $remote = $param->{remote};
+	my $backup_id = $param->{'backup-id'} // $param->{node};
+	my $snapshot = "host/$backup_id";
+	$snapshot .= "/$param->{'backup-time'}" if defined($param->{'backup-time'});
+
+	my $conf = PMG::PBSConfig->new();
+
+	my $remote_config = $conf->{ids}->{$remote};
+	die "PBS remote '$remote' does not exist\n" if !$remote_config;
+	die "PBS remote '$remote' is disabled\n" if $remote_config->{disable};
+
+	my $time = time;
+	my $dirname = "/tmp/proxrestore_$$.$time";
+
+	$param->{database} //= 1;
+
+	die "nothing selected - please select what you want to restore (config or database?)\n"
+	    if !($param->{database} || $param->{config});
+
+	my $pbs_opts = {
+	    pxarname => 'pmgbackup',
+	    target => $dirname,
+	    snapshot => $snapshot,
+	};
+
+	my $worker = sub {
+	    my $upid = shift;
+
+	    print "starting restore of $snapshot from $remote\n";
+
+	    PVE::PBSTools::restore_pxar($remote_config, $remote, $pbs_opts);
+	    print "starting restore of PMG config\n";
+	    PMG::Backup::pmg_restore($dirname, $param->{database},
+		 $param->{config}, $param->{statistic});
+	    print "restore finished\n";
+
+	    return;
+	};
+
+	return $rpcenv->fork_worker('pbs_restore', undef, $authuser, $worker);
+    }});
+
+__PACKAGE__->register_method ({
+    name => 'run_prune',
+    path => '{remote}/prune',
+    method => 'POST',
+    description => "Prune the remote based on the configured options",
+    proxyto => 'node',
+    protected => 1,
+    permissions => { check => [ 'admin', 'audit' ] },
+    parameters => {
+	additionalProperties => 0,
+	properties => {
+	    node => get_standard_option('pve-node'),
+	    remote => {
+		description => "Proxmox Backup Server ID.",
+		type => 'string', format => 'pve-configid',
+	    },
+	},
+    },
+    returns => { type => "string" },
+    code => sub {
+	my ($param) = @_;
+
+	my $rpcenv = PMG::RESTEnvironment->get();
+	my $authuser = $rpcenv->get_user();
+
+	my $remote = $param->{remote};
+	my $node = $param->{node};
+
+	my $conf = PMG::PBSConfig->new();
+
+	my $remote_config = $conf->{ids}->{$remote};
+	die "PBS remote '$remote' does not exist\n" if !$remote_config;
+	die "PBS remote '$remote' is disabled\n" if $remote_config->{disable};
+
+	my $group = "host/$node";
+
+	my $worker = sub {
+	    my $upid = shift;
+
+	    print "starting prune of current backup state\n";
+
+	    my $prune_opts = $conf->prune_options($remote);
+	    my $res = PVE::PBSTools::prune_group($remote_config, $remote, $prune_opts, $group);
+
+	    foreach my $pruned (@$res){
+		my $time = strftime("%FT%TZ", gmtime($pruned->{'backup-time'}));
+		my $snap = $pruned->{'backup-type'} . '/' . $pruned->{'backup-id'} . '/' .  $time;
+		print "pruned snapshot: $snap\n";
+	    }
+
+	    print "prune finished\n";
+
+	    return;
+	};
+
+	return $rpcenv->fork_worker('pbs_backup', undef, $authuser, $worker);
+
+    }});
+1;
-- 
2.20.1





^ permalink raw reply	[flat|nested] 26+ messages in thread

* [pmg-devel] [PATCH pmg-api 09/11] pbs-integration: add CLI calls to pmgbackup
  2020-10-28 18:54 [pmg-devel] [PATCH pve-common/api/gui] add initial PBS integration Stoiko Ivanov
                   ` (9 preceding siblings ...)
  2020-10-28 18:54 ` [pmg-devel] [PATCH pmg-api 08/11] Add API2 module for per-node backups to PBS Stoiko Ivanov
@ 2020-10-28 18:54 ` Stoiko Ivanov
  2020-10-28 18:54 ` [pmg-devel] [PATCH pmg-api 10/11] add scheduled backup to PBS remotes Stoiko Ivanov
                   ` (5 subsequent siblings)
  16 siblings, 0 replies; 26+ messages in thread
From: Stoiko Ivanov @ 2020-10-28 18:54 UTC (permalink / raw)
  To: pmg-devel

add two new categories for commands to pmgbackup:
* pmgbackup remote - for managing PBS instances' configuration, cluster-wide
* pmgbackup pbsjob - for managing backups, restores, pruning

Signed-off-by: Stoiko Ivanov <s.ivanov@proxmox.com>
---
 src/PMG/CLI/pmgbackup.pm | 96 ++++++++++++++++++++++++++++++++++++++++
 1 file changed, 96 insertions(+)

diff --git a/src/PMG/CLI/pmgbackup.pm b/src/PMG/CLI/pmgbackup.pm
index 69224e5..2a85b76 100644
--- a/src/PMG/CLI/pmgbackup.pm
+++ b/src/PMG/CLI/pmgbackup.pm
@@ -3,14 +3,20 @@ package PMG::CLI::pmgbackup;
 use strict;
 use warnings;
 use Data::Dumper;
+use POSIX qw(strftime);
 
 use PVE::Tools;
 use PVE::SafeSyslog;
 use PVE::INotify;
 use PVE::CLIHandler;
+use PVE::CLIFormatter;
+use PVE::JSONSchema qw(get_standard_option);
+use PVE::PBSTools;
 
 use PMG::RESTEnvironment;
 use PMG::API2::Backup;
+use PMG::API2::PBS::Remote;
+use PMG::API2::PBS::Job;
 
 use base qw(PVE::CLIHandler);
 
@@ -28,10 +34,100 @@ my $format_backup_list = sub {
     }
 };
 
+__PACKAGE__->register_method ({
+    name => 'prune',
+    path => 'prune',
+    method => 'GET',
+    description => "Prune all remotes",
+    protected => 1,
+    permissions => { check => [ 'admin', 'audit' ] },
+    parameters => {
+	additionalProperties => 0,
+	properties => {
+	    node => get_standard_option('pve-node'),
+	    remote => {
+		description => "only prune the provided Proxmox Backup Server.",
+		type => 'string', format => 'pve-configid',
+		optional => 1,
+	    },
+	    'dry-run' => { type => 'boolean', optional => 1, default => 0},
+	    'verbose' => { type => 'boolean', optional => 1, default => 0},
+	},
+    },
+    returns => { type => "string" },
+    code => sub {
+	my ($param) = @_;
+
+	my $remote = $param->{remote};
+	my $node = $param->{node};
+
+	my $verbose = defined($param->{verbose}) && $param->{verbose};
+
+	my $group = "host/$node";
+
+	my $conf = PMG::PBSConfig->new();
+
+	my $pbs_opts = {};
+	if (defined($param->{'dry-run'}) && $param->{'dry-run'}) {
+	    $pbs_opts->{'dry-run'} = 1;
+	}
+
+	my $run_prune = sub {
+	    my ($remote_config, $remote) = @_;
+
+	    print "running prune for $remote\n" if $verbose;
+
+	    my $prune_opts = $conf->prune_options($remote);
+	    my $res = PVE::PBSTools::prune_group($remote_config, $remote, $pbs_opts, $prune_opts, $group);
+
+	    if ($verbose) {
+		foreach my $pruned (@$res){
+		    my $time = strftime("%FT%TZ", gmtime($pruned->{'backup-time'}));
+		    my $snap = $pruned->{'backup-type'} . '/' . $pruned->{'backup-id'} . '/' .  $time;
+		    print "pruned snapshot: $snap\n";
+		}
+
+	    }
+	    print "finished pruning $remote\n" if $verbose;
+	};
+
+	if (defined($remote)) {
+	    my $remote_config = $conf->{ids}->{$remote};
+	    die "PBS remote '$remote' does not exist\n" if !$remote_config;
+	    die "PBS remote '$remote' is disabled\n" if $remote_config->{disable};
+	    $run_prune->($remote_config, $remote);
+	} else {
+	    foreach my $remote (sort keys %{$conf->{ids}}){
+		my $cfg = $conf->{ids}->{$remote};
+		next if $cfg->{disable};
+		$run_prune->($cfg, $remote);
+	    }
+	}
+    }});
+
 our $cmddef = {
     backup => [ 'PMG::API2::Backup', 'backup', undef, { node => $nodename } ],
     restore => [ 'PMG::API2::Backup', 'restore', undef, { node => $nodename } ],
     list => [ 'PMG::API2::Backup', 'list', undef, { node => $nodename }, $format_backup_list ],
+    remote => {
+	list => ['PMG::API2::PBS::Remote', 'list', undef, undef,  sub {
+	    my ($data, $schema, $options) = @_;
+	    PVE::CLIFormatter::print_api_result($data, $schema, ['remote', 'server', 'datastore', 'username' ], $options);
+	}, $PVE::RESTHandler::standard_output_options ],
+	add => ['PMG::API2::PBS::Remote', 'create', ['remote'] ],
+	remove => ['PMG::API2::PBS::Remote', 'delete', ['remote'] ],
+	set => ['PMG::API2::PBS::Remote', 'update_config', ['remote'] ],
+    },
+    pbsjob => {
+	list_backups => ['PMG::API2::PBS::Job', 'get_snapshots', ['remote'] , { node => $nodename },  sub {
+	    my ($data, $schema, $options) = @_;
+	    PVE::CLIFormatter::print_api_result($data, $schema, ['time', 'size'], $options);
+	}, $PVE::RESTHandler::standard_output_options ],
+	forget => ['PMG::API2::PBS::Job', 'forget_snapshot', ['remote', 'time'], { node => $nodename} ],
+	run => ['PMG::API2::PBS::Job', 'run_backup', ['remote'], { node => $nodename} ],
+	restore => ['PMG::API2::PBS::Job', 'restore', ['remote'], { node => $nodename} ],
+	prune => [__PACKAGE__, 'prune', undef , { node => $nodename } ],
+    },
 };
 
 1;
-- 
2.20.1





^ permalink raw reply	[flat|nested] 26+ messages in thread

* [pmg-devel] [PATCH pmg-api 10/11] add scheduled backup to PBS remotes
  2020-10-28 18:54 [pmg-devel] [PATCH pve-common/api/gui] add initial PBS integration Stoiko Ivanov
                   ` (10 preceding siblings ...)
  2020-10-28 18:54 ` [pmg-devel] [PATCH pmg-api 09/11] pbs-integration: add CLI calls to pmgbackup Stoiko Ivanov
@ 2020-10-28 18:54 ` Stoiko Ivanov
  2020-10-28 18:54 ` [pmg-devel] [PATCH pmg-api 11/11] add daily timer for pruning configured remotes Stoiko Ivanov
                   ` (4 subsequent siblings)
  16 siblings, 0 replies; 26+ messages in thread
From: Stoiko Ivanov @ 2020-10-28 18:54 UTC (permalink / raw)
  To: pmg-devel

PMG::PBSSchedule contains methods for creating/deleting systemd-timer units,
which will run a backup to a configured PBS remote.

Signed-off-by: Stoiko Ivanov <s.ivanov@proxmox.com>
---
 debian/pmg-pbsbackup@.service |   6 ++
 debian/rules                  |   1 +
 src/Makefile                  |   3 +-
 src/PMG/API2/PBS/Job.pm       | 131 ++++++++++++++++++++++++++++++++++
 src/PMG/CLI/pmgbackup.pm      |   6 ++
 src/PMG/PBSSchedule.pm        | 104 +++++++++++++++++++++++++++
 6 files changed, 250 insertions(+), 1 deletion(-)
 create mode 100644 debian/pmg-pbsbackup@.service
 create mode 100644 src/PMG/PBSSchedule.pm

diff --git a/debian/pmg-pbsbackup@.service b/debian/pmg-pbsbackup@.service
new file mode 100644
index 0000000..37aa23b
--- /dev/null
+++ b/debian/pmg-pbsbackup@.service
@@ -0,0 +1,6 @@
+[Unit]
+Description=Backup to PBS remote %I
+
+[Service]
+Type=oneshot
+ExecStart=/usr/bin/pmgbackup pbsjob run %I
diff --git a/debian/rules b/debian/rules
index bab4d98..5a2cf7a 100755
--- a/debian/rules
+++ b/debian/rules
@@ -20,6 +20,7 @@ override_dh_installinit:
 	dh_systemd_enable --name=pmgspamreport pmgspamreport.service
 	dh_systemd_enable --name=pmgreport pmgreport.service
 	dh_systemd_enable --name=pmgsync pmgsync.service
+	dh_systemd_enable --no-enable --name=pmg-pbsbackup@ pmg-pbsbackup@.service
 
 override_dh_systemd_start:
 	dh_systemd_start pmg-hourly.timer pmg-daily.timer pmgspamreport.timer pmgreport.timer
diff --git a/src/Makefile b/src/Makefile
index fb42f21..9d5c335 100644
--- a/src/Makefile
+++ b/src/Makefile
@@ -15,7 +15,7 @@ CRONSCRIPTS = pmg-hourly pmg-daily
 
 CLI_CLASSES = $(addprefix PMG/CLI/, $(addsuffix .pm, ${CLITOOLS}))
 SERVICE_CLASSES = $(addprefix PMG/Service/, $(addsuffix .pm, ${SERVICES}))
-SERVICE_UNITS = $(addprefix debian/, $(addsuffix .service, ${SERVICES}))
+SERVICE_UNITS = $(addprefix debian/, $(addsuffix .service, ${SERVICES} pmg-pbsbackup@))
 TIMER_UNITS = $(addprefix debian/, $(addsuffix .timer, ${CRONSCRIPTS} pmgspamreport pmgreport))
 
 CLI_BINARIES = $(addprefix bin/, ${CLITOOLS} ${CLISCRIPTS} ${CRONSCRIPTS})
@@ -67,6 +67,7 @@ LIBSOURCES =				\
 	PMG/Unpack.pm			\
 	PMG/Backup.pm			\
 	PMG/PBSConfig.pm		\
+	PMG/PBSSchedule.pm		\
 	PMG/RuleCache.pm		\
 	PMG/Statistic.pm		\
 	PMG/UserConfig.pm		\
diff --git a/src/PMG/API2/PBS/Job.pm b/src/PMG/API2/PBS/Job.pm
index 80a1f29..088873e 100644
--- a/src/PMG/API2/PBS/Job.pm
+++ b/src/PMG/API2/PBS/Job.pm
@@ -14,6 +14,7 @@ use PVE::PBSTools;
 use PMG::RESTEnvironment;
 use PMG::Backup;
 use PMG::PBSConfig;
+use PMG::PBSSchedule;
 
 use base qw(PVE::RESTHandler);
 
@@ -410,4 +411,134 @@ __PACKAGE__->register_method ({
 	return $rpcenv->fork_worker('pbs_backup', undef, $authuser, $worker);
 
     }});
+
+__PACKAGE__->register_method ({
+    name => 'create_timer',
+    path => '{remote}/timer',
+    method => 'POST',
+    description => "Create backup schedule",
+    proxyto => 'node',
+    protected => 1,
+    permissions => { check => [ 'admin', 'audit' ] },
+    parameters => {
+	additionalProperties => 0,
+	properties => {
+	    node => get_standard_option('pve-node'),
+	    remote => {
+		description => "Proxmox Backup Server ID.",
+		type => 'string', format => 'pve-configid',
+	    },
+	    schedule => {
+		description => "Schedule for the backup (OnCalendar setting of the systemd.timer)",
+		type => 'string', pattern => '[0-9a-zA-Z*.:,\-/ ]+',
+		default => 'daily', optional => 1,
+	    },
+	    delay => {
+		description => "Randomized delay to add to the starttime (RandomizedDelaySec setting of the systemd.timer)",
+		type => 'string', pattern => '[0-9a-zA-Z. ]+',
+		default => 'daily', optional => 1,
+	    },
+	},
+    },
+    returns => { type => 'null' },
+    code => sub {
+	my ($param) = @_;
+
+	my $remote = $param->{remote};
+	my $schedule = $param->{schedule} // 'daily';
+	my $delay = $param->{delay} // '5min';
+
+	my $conf = PMG::PBSConfig->new();
+
+	my $remote_config = $conf->{ids}->{$remote};
+	die "PBS remote '$remote' does not exist\n" if !$remote_config;
+	die "PBS remote '$remote' is disabled\n" if $remote_config->{disable};
+
+	PMG::PBSSchedule::create_schedule($remote, $schedule, $delay);
+
+    }});
+
+__PACKAGE__->register_method ({
+    name => 'delete_timer',
+    path => '{remote}/timer',
+    method => 'DELETE',
+    description => "Delete backup schedule",
+    proxyto => 'node',
+    protected => 1,
+    permissions => { check => [ 'admin', 'audit' ] },
+    parameters => {
+	additionalProperties => 0,
+	properties => {
+	    node => get_standard_option('pve-node'),
+	    remote => {
+		description => "Proxmox Backup Server ID.",
+		type => 'string', format => 'pve-configid',
+	    },
+	},
+    },
+    returns => { type => 'null' },
+    code => sub {
+	my ($param) = @_;
+
+	my $remote = $param->{remote};
+
+	PMG::PBSSchedule::delete_schedule($remote);
+
+    }});
+
+__PACKAGE__->register_method ({
+    name => 'list_timer',
+    path => '{remote}/timer',
+    method => 'GET',
+    description => "Get timer specification",
+    proxyto => 'node',
+    protected => 1,
+    permissions => { check => [ 'admin', 'audit' ] },
+    parameters => {
+	additionalProperties => 0,
+	properties => {
+	    node => get_standard_option('pve-node'),
+	    remote => {
+		description => "Proxmox Backup Server ID.",
+		type => 'string', format => 'pve-configid',
+	    },
+	},
+    },
+    returns => { type => 'object', properties => {
+	    remote => {
+		description => "Proxmox Backup Server ID.",
+		type => 'string', format => 'pve-configid',
+		optional => 1,
+	    },
+	    schedule => {
+		description => "Schedule for the backup (OnCalendar setting of the systemd.timer)",
+		type => 'string', pattern => '[0-9a-zA-Z*.:,\-/ ]+',
+		default => 'daily', optional => 1,
+	    },
+	    delay => {
+		description => "Randomized delay to add to the starttime (RandomizedDelaySec setting of the systemd.timer)",
+		type => 'string', pattern => '[0-9a-zA-Z. ]+',
+		default => 'daily', optional => 1,
+	    },
+	    unitfile => {
+		description => "unit file for the systemd.timer unit",
+		type => 'string', optional => 1,
+	    },
+	}},
+    code => sub {
+	my ($param) = @_;
+
+	my $remote = $param->{remote};
+
+	my $schedules = PMG::PBSSchedule::get_schedules();
+	my @data = grep {$_->{remote} eq $remote} @$schedules;
+
+	my $res = {};
+	if (scalar(@data) == 1) {
+	    $res = $data[0];
+	}
+
+	return $res
+    }});
+
 1;
diff --git a/src/PMG/CLI/pmgbackup.pm b/src/PMG/CLI/pmgbackup.pm
index 2a85b76..0f1c6a9 100644
--- a/src/PMG/CLI/pmgbackup.pm
+++ b/src/PMG/CLI/pmgbackup.pm
@@ -127,6 +127,12 @@ our $cmddef = {
 	run => ['PMG::API2::PBS::Job', 'run_backup', ['remote'], { node => $nodename} ],
 	restore => ['PMG::API2::PBS::Job', 'restore', ['remote'], { node => $nodename} ],
 	prune => [__PACKAGE__, 'prune', undef , { node => $nodename } ],
+	create => ['PMG::API2::PBS::Job', 'create_timer', ['remote'], { node => $nodename }],
+	delete => ['PMG::API2::PBS::Job', 'delete_timer', ['remote'], { node => $nodename }],
+	schedule => ['PMG::API2::PBS::Job', 'list_timer', ['remote'], { node => $nodename },  sub {
+	    my ($data, $schema, $options) = @_;
+	    PVE::CLIFormatter::print_api_result($data, $schema, ['remote', 'schedule', 'delay'], $options);
+	}, $PVE::RESTHandler::standard_output_options ],
     },
 };
 
diff --git a/src/PMG/PBSSchedule.pm b/src/PMG/PBSSchedule.pm
new file mode 100644
index 0000000..6663f55
--- /dev/null
+++ b/src/PMG/PBSSchedule.pm
@@ -0,0 +1,104 @@
+package PMG::PBSSchedule;
+
+use strict;
+use warnings;
+
+use PVE::Tools qw(run_command file_set_contents file_get_contents trim dir_glob_foreach);
+use PVE::Systemd;
+
+# systemd timer
+sub get_schedules {
+    my ($param) = @_;
+
+    my $result = [];
+
+    my $systemd_dir = '/etc/systemd/system';
+
+    dir_glob_foreach($systemd_dir, '^pmg-pbsbackup@.+\.timer$', sub {
+	my ($filename) = @_;
+	my $remote;
+	if ($filename =~ /^pmg-pbsbackup\@(.+)\.timer$/) {
+	    $remote = PVE::Systemd::unescape_unit($1);
+	} else {
+	    die 'Unrecognized timer name!\n';
+	}
+
+	my $unitfile = "$systemd_dir/$filename";
+	my $unit = PVE::Systemd::read_ini($unitfile);
+
+	push @$result, {
+	    unitfile => $unitfile,
+	    remote => $remote,
+	    schedule => $unit->{'Timer'}->{'OnCalendar'},
+	    delay => $unit->{'Timer'}->{'RandomizedDelaySec'},
+	};
+    });
+
+    return $result;
+
+}
+
+sub create_schedule {
+    my ($remote, $schedule, $delay) = @_;
+
+    my $unit_name = 'pmg-pbsbackup@' . PVE::Systemd::escape_unit($remote);
+    #my $service_unit = $unit_name . '.service';
+    my $timer_unit = $unit_name . '.timer';
+    my $timer_unit_path = "/etc/systemd/system/$timer_unit";
+
+    # create systemd timer
+    run_command(['systemd-analyze', 'calendar', $schedule], errmsg => "Invalid schedule specification", outfunc => sub {});
+    run_command(['systemd-analyze', 'timespan', $delay], errmsg => "Invalid delay specification", outfunc => sub {});
+    my $timer = {
+	'Unit' => {
+	    'Description' => "Timer for PBS Backup to remote $remote",
+	},
+	'Timer' => {
+	    'OnCalendar' => $schedule,
+	    'RandomizedDelaySec' => $delay,
+	},
+	'Install' => {
+	    'WantedBy' => 'timers.target',
+	},
+    };
+
+    eval {
+	PVE::Systemd::write_ini($timer, $timer_unit_path);
+	run_command(['systemctl', 'daemon-reload']);
+	run_command(['systemctl', 'enable', $timer_unit]);
+	run_command(['systemctl', 'start', $timer_unit]);
+
+    };
+    if (my $err = $@) {
+	die "Creating backup schedule for $remote failed: $err\n";
+    }
+
+    return;
+}
+
+sub delete_schedule {
+    my ($remote) = @_;
+
+    my $schedules = get_schedules();
+
+    die "Schedule for $remote not found!\n" if !grep {$_->{remote} eq $remote} @$schedules;
+
+    my $unit_name = 'pmg-pbsbackup@' . PVE::Systemd::escape_unit($remote);
+    my $service_unit = $unit_name . '.service';
+    my $timer_unit = $unit_name . '.timer';
+    my $timer_unit_path = "/etc/systemd/system/$timer_unit";
+
+    eval {
+	run_command(['systemctl', 'disable', $timer_unit]);
+	unlink($timer_unit_path) || die "delete '$timer_unit_path' failed - $!\n";
+	run_command(['systemctl', 'daemon-reload']);
+
+    };
+    if (my $err = $@) {
+	die "Removing backup schedule for $remote failed: $err\n";
+    }
+
+    return;
+}
+
+1;
-- 
2.20.1





^ permalink raw reply	[flat|nested] 26+ messages in thread

* [pmg-devel] [PATCH pmg-api 11/11] add daily timer for pruning configured remotes
  2020-10-28 18:54 [pmg-devel] [PATCH pve-common/api/gui] add initial PBS integration Stoiko Ivanov
                   ` (11 preceding siblings ...)
  2020-10-28 18:54 ` [pmg-devel] [PATCH pmg-api 10/11] add scheduled backup to PBS remotes Stoiko Ivanov
@ 2020-10-28 18:54 ` Stoiko Ivanov
  2020-10-28 18:54 ` [pmg-devel] [PATCH pmg-gui 1/3] Make Backup/Restore panel a menuentry Stoiko Ivanov
                   ` (3 subsequent siblings)
  16 siblings, 0 replies; 26+ messages in thread
From: Stoiko Ivanov @ 2020-10-28 18:54 UTC (permalink / raw)
  To: pmg-devel

The service runs `pmgbackup pbsjob prune` , which iterates through all
configured remotes and runs prune with the set keep settings.

Running once daily seems like a sensible tradeoff (prune is not so expensive)

adding the dedicated systemd.timer, instead of simply hooking it into
pmg-daily, should make it easier for users who want to override the settings.

Signed-off-by: Stoiko Ivanov <s.ivanov@proxmox.com>
---
 debian/pmg-pbsprune.service |  6 ++++++
 debian/pmg-pbsprune.timer   | 10 ++++++++++
 debian/rules                |  3 ++-
 src/Makefile                |  4 ++--
 4 files changed, 20 insertions(+), 3 deletions(-)
 create mode 100644 debian/pmg-pbsprune.service
 create mode 100644 debian/pmg-pbsprune.timer

diff --git a/debian/pmg-pbsprune.service b/debian/pmg-pbsprune.service
new file mode 100644
index 0000000..b7fd633
--- /dev/null
+++ b/debian/pmg-pbsprune.service
@@ -0,0 +1,6 @@
+[Unit]
+Description=Prune all configured Proxmox Backup Server Remotes
+
+[Service]
+Type=oneshot
+ExecStart=/usr/bin/pmgbackup pbsjob prune
diff --git a/debian/pmg-pbsprune.timer b/debian/pmg-pbsprune.timer
new file mode 100644
index 0000000..52c7e06
--- /dev/null
+++ b/debian/pmg-pbsprune.timer
@@ -0,0 +1,10 @@
+[Unit]
+Description=Prune all configured Proxmox Backup Server Remotes
+
+[Timer]
+OnCalendar=04:00
+RandomizedDelaySec=1hours
+Persistent=true
+
+[Install]
+WantedBy=timers.target
diff --git a/debian/rules b/debian/rules
index 5a2cf7a..227b761 100755
--- a/debian/rules
+++ b/debian/rules
@@ -21,9 +21,10 @@ override_dh_installinit:
 	dh_systemd_enable --name=pmgreport pmgreport.service
 	dh_systemd_enable --name=pmgsync pmgsync.service
 	dh_systemd_enable --no-enable --name=pmg-pbsbackup@ pmg-pbsbackup@.service
+	dh_systemd_enable --name=pmg-pbsprune pmg-pbsprune.service
 
 override_dh_systemd_start:
-	dh_systemd_start pmg-hourly.timer pmg-daily.timer pmgspamreport.timer pmgreport.timer
+	dh_systemd_start pmg-hourly.timer pmg-daily.timer pmgspamreport.timer pmgreport.timer pmg-pbsprune.timer
 	dh_systemd_start --no-restart-on-upgrade --no-start pmgnetcommit.service pmgbanner.service pmgsync.service
 	dh_systemd_start pmg-smtp-filter.service pmgpolicy.service pmgtunnel.service pmgmirror.service
 	# we handle pmgproxy/pmgdaemon manually (use reload instead of restart to keep vnc connection active)
diff --git a/src/Makefile b/src/Makefile
index 9d5c335..85e91ce 100644
--- a/src/Makefile
+++ b/src/Makefile
@@ -15,8 +15,8 @@ CRONSCRIPTS = pmg-hourly pmg-daily
 
 CLI_CLASSES = $(addprefix PMG/CLI/, $(addsuffix .pm, ${CLITOOLS}))
 SERVICE_CLASSES = $(addprefix PMG/Service/, $(addsuffix .pm, ${SERVICES}))
-SERVICE_UNITS = $(addprefix debian/, $(addsuffix .service, ${SERVICES} pmg-pbsbackup@))
-TIMER_UNITS = $(addprefix debian/, $(addsuffix .timer, ${CRONSCRIPTS} pmgspamreport pmgreport))
+SERVICE_UNITS = $(addprefix debian/, $(addsuffix .service, ${SERVICES} pmg-pbsbackup@ pmg-pbsprune))
+TIMER_UNITS = $(addprefix debian/, $(addsuffix .timer, ${CRONSCRIPTS} pmgspamreport pmgreport pmg-pbsprune))
 
 CLI_BINARIES = $(addprefix bin/, ${CLITOOLS} ${CLISCRIPTS} ${CRONSCRIPTS})
 CLI_MANS = $(addsuffix .1, ${CLITOOLS}) pmgsh.1 pmg-system-report.1
-- 
2.20.1





^ permalink raw reply	[flat|nested] 26+ messages in thread

* [pmg-devel] [PATCH pmg-gui 1/3] Make Backup/Restore panel a menuentry
  2020-10-28 18:54 [pmg-devel] [PATCH pve-common/api/gui] add initial PBS integration Stoiko Ivanov
                   ` (12 preceding siblings ...)
  2020-10-28 18:54 ` [pmg-devel] [PATCH pmg-api 11/11] add daily timer for pruning configured remotes Stoiko Ivanov
@ 2020-10-28 18:54 ` Stoiko Ivanov
  2020-10-28 18:54 ` [pmg-devel] [PATCH pmg-gui 2/3] refactor RestoreWindow for PBS Stoiko Ivanov
                   ` (2 subsequent siblings)
  16 siblings, 0 replies; 26+ messages in thread
From: Stoiko Ivanov @ 2020-10-28 18:54 UTC (permalink / raw)
  To: pmg-devel

Move it away from the tab list in the Configuration entry to a submenu in
preparation for adding PBS integration

Signed-off-by: Stoiko Ivanov <s.ivanov@proxmox.com>
---
 js/BackupConfiguration.js | 18 ++++++++++++++++++
 js/Makefile               |  1 +
 js/NavigationTree.js      |  6 ++++++
 js/SystemConfiguration.js |  4 ----
 4 files changed, 25 insertions(+), 4 deletions(-)
 create mode 100644 js/BackupConfiguration.js

diff --git a/js/BackupConfiguration.js b/js/BackupConfiguration.js
new file mode 100644
index 0000000..35b50a4
--- /dev/null
+++ b/js/BackupConfiguration.js
@@ -0,0 +1,18 @@
+Ext.define('PMG.BackupConfiguration', {
+    extend: 'Ext.tab.Panel',
+    alias: 'widget.pmgBackupConfiguration',
+
+    title: gettext('Backup'),
+
+    border: false,
+    defaults: { border: false },
+
+    items: [
+	{
+	    itemId: 'local',
+	    title: gettext('Local Backup/Restore'),
+	    xtype: 'pmgBackupRestore',
+	},
+   ],
+});
+
diff --git a/js/Makefile b/js/Makefile
index badf7ab..a40f11f 100644
--- a/js/Makefile
+++ b/js/Makefile
@@ -35,6 +35,7 @@ JSSRC=							\
 	RuleConfiguration.js				\
 	SystemOptions.js				\
 	Subscription.js					\
+	BackupConfiguration.js				\
 	BackupRestore.js				\
 	SystemConfiguration.js				\
 	MailProxyRelaying.js				\
diff --git a/js/NavigationTree.js b/js/NavigationTree.js
index 0ea0d2f..ac01fd6 100644
--- a/js/NavigationTree.js
+++ b/js/NavigationTree.js
@@ -86,6 +86,12 @@ Ext.define('PMG.store.NavigationStore', {
 			path: 'pmgSubscription',
 			leaf: true,
 		    },
+		    {
+			text: gettext('Backup/Restore'),
+			iconCls: 'fa fa-floppy-o',
+			path: 'pmgBackupConfiguration',
+			leaf: true,
+		    },
 		],
 	    },
 	    {
diff --git a/js/SystemConfiguration.js b/js/SystemConfiguration.js
index 37cb3e4..51b558a 100644
--- a/js/SystemConfiguration.js
+++ b/js/SystemConfiguration.js
@@ -49,10 +49,6 @@ Ext.define('PMG.SystemConfiguration', {
             title: gettext('Options'),
 	    xtype: 'pmgSystemOptions',
 	},
-	{
-	    itemId: 'backup',
-	    xtype: 'pmgBackupRestore',
-	},
     ],
 
     initComponent: function() {
-- 
2.20.1





^ permalink raw reply	[flat|nested] 26+ messages in thread

* [pmg-devel] [PATCH pmg-gui 2/3] refactor RestoreWindow for PBS
  2020-10-28 18:54 [pmg-devel] [PATCH pve-common/api/gui] add initial PBS integration Stoiko Ivanov
                   ` (13 preceding siblings ...)
  2020-10-28 18:54 ` [pmg-devel] [PATCH pmg-gui 1/3] Make Backup/Restore panel a menuentry Stoiko Ivanov
@ 2020-10-28 18:54 ` Stoiko Ivanov
  2020-10-28 18:54 ` [pmg-devel] [PATCH pmg-gui 3/3] add PBSConfig tab to Backup menu Stoiko Ivanov
  2020-10-30  5:47 ` [pmg-devel] [PATCH pve-common/api/gui] add initial PBS integration Dietmar Maurer
  16 siblings, 0 replies; 26+ messages in thread
From: Stoiko Ivanov @ 2020-10-28 18:54 UTC (permalink / raw)
  To: pmg-devel

by moving the item definition to initComponent, and changing the check
for a provided filename, we can reuse the window for restores from PBS

Signed-off-by: Stoiko Ivanov <s.ivanov@proxmox.com>
---
 js/BackupRestore.js | 59 +++++++++++++++++++++++----------------------
 1 file changed, 30 insertions(+), 29 deletions(-)

diff --git a/js/BackupRestore.js b/js/BackupRestore.js
index 6c97230..996f128 100644
--- a/js/BackupRestore.js
+++ b/js/BackupRestore.js
@@ -26,40 +26,41 @@ Ext.define('PMG.RestoreWindow', {
     fieldDefaults: {
 	labelWidth: 150,
     },
-    items: [
-	{
-	    xtype: 'proxmoxcheckbox',
-	    name: 'config',
-	    fieldLabel: gettext('System Configuration'),
-	},
-	{
-	    xtype: 'proxmoxcheckbox',
-	    name: 'database',
-	    value: 1,
-	    uncheckedValue: 0,
-	    fieldLabel: gettext('Rule Database'),
-	    listeners: {
-		change: function(cb, value) {
-		    var me = this;
-		    me.up().down('field[name=statistic]').setDisabled(!value);
-		},
-	    },
-	},
-	{
-	    xtype: 'proxmoxcheckbox',
-	    name: 'statistic',
-	    fieldLabel: gettext('Statistic'),
-	},
-    ],
 
     initComponent: function() {
 	var me = this;
 
-	if (!me.filename) {
-	    throw "no filename given";
-	}
+	me.items = [
+	    {
+		xtype: 'proxmoxcheckbox',
+		name: 'config',
+		fieldLabel: gettext('System Configuration'),
+	    },
+	    {
+		xtype: 'proxmoxcheckbox',
+		name: 'database',
+		value: 1,
+		uncheckedValue: 0,
+		fieldLabel: gettext('Rule Database'),
+		listeners: {
+		    change: function(cb, value) {
+			me.up().down('field[name=statistic]').setDisabled(!value);
+		    },
+		},
+	    },
+	    {
+		xtype: 'proxmoxcheckbox',
+		name: 'statistic',
+		fieldLabel: gettext('Statistic'),
+	    },
+	];
 
-	me.url = "/nodes/" + Proxmox.NodeName + "/backup/" + encodeURIComponent(me.filename);
+	let initurl = "/nodes/" + Proxmox.NodeName;
+	if (me.filename) {
+	    me.url = initurl + "/backup/" + encodeURIComponent(me.filename);
+	} else {
+	    throw "neither filename nor snapshot given";
+	}
 
 	me.callParent();
     },
-- 
2.20.1





^ permalink raw reply	[flat|nested] 26+ messages in thread

* [pmg-devel] [PATCH pmg-gui 3/3] add PBSConfig tab to Backup menu
  2020-10-28 18:54 [pmg-devel] [PATCH pve-common/api/gui] add initial PBS integration Stoiko Ivanov
                   ` (14 preceding siblings ...)
  2020-10-28 18:54 ` [pmg-devel] [PATCH pmg-gui 2/3] refactor RestoreWindow for PBS Stoiko Ivanov
@ 2020-10-28 18:54 ` Stoiko Ivanov
  2020-10-30  5:47 ` [pmg-devel] [PATCH pve-common/api/gui] add initial PBS integration Dietmar Maurer
  16 siblings, 0 replies; 26+ messages in thread
From: Stoiko Ivanov @ 2020-10-28 18:54 UTC (permalink / raw)
  To: pmg-devel

The PBSConfig panel enables creation/editing/deletion of PBS instances.
Each instance can lists its snapshots and each snapshot can be restored

Inspired by the LDAPConfig panel and PBSEdit from pve-manager.

Signed-off-by: Stoiko Ivanov <s.ivanov@proxmox.com>
---
 js/BackupConfiguration.js |   5 +
 js/BackupRestore.js       |   9 +
 js/Makefile               |   1 +
 js/PBSConfig.js           | 680 ++++++++++++++++++++++++++++++++++++++
 4 files changed, 695 insertions(+)
 create mode 100644 js/PBSConfig.js

diff --git a/js/BackupConfiguration.js b/js/BackupConfiguration.js
index 35b50a4..e21771f 100644
--- a/js/BackupConfiguration.js
+++ b/js/BackupConfiguration.js
@@ -13,6 +13,11 @@ Ext.define('PMG.BackupConfiguration', {
 	    title: gettext('Local Backup/Restore'),
 	    xtype: 'pmgBackupRestore',
 	},
+	{
+	    itemId: 'proxmoxbackupserver',
+	    title: 'Proxmox Backup Server',
+	    xtype: 'pmgPBSConfig',
+	},
    ],
 });
 
diff --git a/js/BackupRestore.js b/js/BackupRestore.js
index 996f128..9981d42 100644
--- a/js/BackupRestore.js
+++ b/js/BackupRestore.js
@@ -58,6 +58,15 @@ Ext.define('PMG.RestoreWindow', {
 	let initurl = "/nodes/" + Proxmox.NodeName;
 	if (me.filename) {
 	    me.url = initurl + "/backup/" + encodeURIComponent(me.filename);
+	} else if (me.backup_time) {
+	    me.items.push(
+		{
+		    xtype: 'hiddenfield',
+		    name: 'backup-time',
+		    value: me.backup_time,
+		},
+	    );
+	    me.url = initurl + "/pbs/" + me.name + '/restore';
 	} else {
 	    throw "neither filename nor snapshot given";
 	}
diff --git a/js/Makefile b/js/Makefile
index a40f11f..bc14487 100644
--- a/js/Makefile
+++ b/js/Makefile
@@ -37,6 +37,7 @@ JSSRC=							\
 	Subscription.js					\
 	BackupConfiguration.js				\
 	BackupRestore.js				\
+	PBSConfig.js					\
 	SystemConfiguration.js				\
 	MailProxyRelaying.js				\
 	MailProxyPorts.js				\
diff --git a/js/PBSConfig.js b/js/PBSConfig.js
new file mode 100644
index 0000000..abec9d7
--- /dev/null
+++ b/js/PBSConfig.js
@@ -0,0 +1,680 @@
+Ext.define('Proxmox.form.PBSEncryptionCheckbox', {
+    extend: 'Ext.form.field.Checkbox',
+    xtype: 'pbsEncryptionCheckbox',
+
+    inputValue: true,
+
+    viewModel: {
+	data: {
+	    value: null,
+	    originalValue: null,
+	},
+	formulas: {
+	    blabel: (get) => {
+		let v = get('value');
+		let original = get('originalValue');
+		if (!get('isCreate') && original) {
+		    if (!v) {
+			return gettext('Warning: Existing encryption key will be deleted!');
+		    }
+		    return gettext('Active');
+		} else {
+		    return gettext('Auto-generate a client encryption key, saved privately in /etc/pmg');
+		}
+	    },
+	},
+    },
+
+    bind: {
+	value: '{value}',
+	boxLabel: '{blabel}',
+    },
+    resetOriginalValue: function() {
+	let me = this;
+	let vm = me.getViewModel();
+	vm.set('originalValue', me.value);
+
+	me.callParent(arguments);
+    },
+
+    getSubmitData: function() {
+	let me = this;
+	let val = me.getSubmitValue();
+	if (!me.isCreate) {
+	    if (val === null) {
+	       return { 'delete': 'encryption-key' };
+	    } else if (val && !!val !== !!me.originalValue) {
+	       return { 'encryption-key': 'autogen' };
+	    }
+	} else if (val) {
+	   return { 'encryption-key': 'autogen' };
+	}
+	return null;
+    },
+
+    initComponent: function() {
+	let me = this;
+	me.callParent();
+
+	let vm = me.getViewModel();
+	vm.set('isCreate', me.isCreate);
+    },
+});
+
+Ext.define('PMG.PBSInputPanel', {
+    extend: 'Ext.tab.Panel',
+    xtype: 'pmgPBSInputPanel',
+
+    bodyPadding: 10,
+    remoteId: undefined,
+
+    initComponent: function() {
+	let me = this;
+
+	me.items = [
+	    {
+		title: gettext('Backup Server'),
+		xtype: 'inputpanel',
+		reference: 'remoteeditpanel',
+		onGetValues: function(values) {
+		    let me = this;
+
+		    values.disable = values.enable ? 0 : 1;
+		    delete values.enable;
+
+		    return values;
+		},
+
+		column1: [
+		    {
+			xtype: me.isCreate ? 'textfield' : 'displayfield',
+			name: 'remote',
+			value: me.isCreate ? null : undefined,
+			fieldLabel: gettext('ID'),
+			allowBlank: false,
+		    },
+		    {
+			xtype: 'proxmoxtextfield',
+			name: 'server',
+			value: me.isCreate ? null : undefined,
+			vtype: 'DnsOrIp',
+			fieldLabel: gettext('Server'),
+			allowBlank: false,
+		    },
+		    {
+			xtype: 'proxmoxtextfield',
+			name: 'datastore',
+			value: me.isCreate ? null : undefined,
+			fieldLabel: 'Datastore',
+			allowBlank: false,
+		    },
+		],
+		column2: [
+		    {
+			xtype: 'proxmoxtextfield',
+			name: 'username',
+			value: me.isCreate ? null : undefined,
+			emptyText: gettext('Example') + ': admin@pbs',
+			fieldLabel: gettext('Username'),
+			regex: /\S+@\w+/,
+			regexText: gettext('Example') + ': admin@pbs',
+			allowBlank: false,
+		    },
+		    {
+			xtype: 'proxmoxtextfield',
+			inputType: 'password',
+			name: 'password',
+			value: me.isCreate ? null : undefined,
+			emptyText: me.isCreate ? gettext('None') : '********',
+			fieldLabel: gettext('Password'),
+			allowBlank: true,
+		    },
+		    {
+			xtype: 'proxmoxcheckbox',
+			name: 'enable',
+			checked: true,
+			uncheckedValue: 0,
+			fieldLabel: gettext('Enable'),
+		    },
+		],
+		columnB: [
+		    {
+			xtype: 'proxmoxtextfield',
+			name: 'fingerprint',
+			value: me.isCreate ? null : undefined,
+			fieldLabel: gettext('Fingerprint'),
+			emptyText: gettext('Server certificate SHA-256 fingerprint, required for self-signed certificates'),
+			regex: /[A-Fa-f0-9]{2}(:[A-Fa-f0-9]{2}){31}/,
+			regexText: gettext('Example') + ': AB:CD:EF:...',
+			allowBlank: true,
+		    },
+		    {
+			xtype: 'pbsEncryptionCheckbox',
+			name: 'encryption-key',
+			isCreate: me.isCreate,
+			fieldLabel: gettext('Encryption Key'),
+		    },
+		    {
+			xtype: 'displayfield',
+			userCls: 'pmx-hint',
+			value: `Proxmox Backup Server is currently in beta.`,
+		    },
+		],
+	    },
+	    {
+		title: gettext('Prune Options'),
+		xtype: 'inputpanel',
+		reference: 'prunepanel',
+		column1: [
+		    {
+			xtype: 'proxmoxintegerfield',
+			fieldLabel: gettext('Keep Last'),
+			name: 'keep-last',
+			cbind: {
+			    deleteEmpty: '{!isCreate}',
+			},
+			minValue: 1,
+			allowBlank: true,
+		    },
+		    {
+			xtype: 'proxmoxintegerfield',
+			fieldLabel: gettext('Keep Daily'),
+			name: 'keep-daily',
+			cbind: {
+			    deleteEmpty: '{!isCreate}',
+			},
+			minValue: 1,
+			allowBlank: true,
+		    },
+		    {
+			xtype: 'proxmoxintegerfield',
+			fieldLabel: gettext('Keep Monthly'),
+			name: 'keep-monthly',
+			cbind: {
+			    deleteEmpty: '{!isCreate}',
+			},
+			minValue: 1,
+			allowBlank: true,
+		    },
+		],
+		column2: [
+		    {
+			xtype: 'proxmoxintegerfield',
+			fieldLabel: gettext('Keep Hourly'),
+			name: 'keep-hourly',
+			cbind: {
+			    deleteEmpty: '{!isCreate}',
+			},
+			minValue: 1,
+			allowBlank: true,
+		    },
+		    {
+			xtype: 'proxmoxintegerfield',
+			fieldLabel: gettext('Keep Weekly'),
+			name: 'keep-weekly',
+			cbind: {
+			    deleteEmpty: '{!isCreate}',
+			},
+			minValue: 1,
+			allowBlank: true,
+		    },
+		    {
+			xtype: 'proxmoxintegerfield',
+			fieldLabel: gettext('Keep Yearly'),
+			name: 'keep-yearly',
+			cbind: {
+			    deleteEmpty: '{!isCreate}',
+			},
+			minValue: 1,
+			allowBlank: true,
+		    },
+		],
+	    },
+	];
+
+	me.callParent();
+    },
+
+});
+
+Ext.define('PMG.PBSEdit', {
+    extend: 'Proxmox.window.Edit',
+    xtype: 'pmgPBSEdit',
+
+    subject: 'Proxmox Backup Server',
+    isAdd: true,
+
+    bodyPadding: 0,
+
+    initComponent: function() {
+	let me = this;
+
+	me.isCreate = !me.remoteId;
+
+	if (me.isCreate) {
+            me.url = '/api2/extjs/config/pbs';
+            me.method = 'POST';
+	} else {
+            me.url = '/api2/extjs/config/pbs/' + me.remoteId;
+            me.method = 'PUT';
+	}
+
+	let ipanel = Ext.create('PMG.PBSInputPanel', {
+	    isCreate: me.isCreate,
+	    remoteId: me.remoteId,
+	});
+
+	me.items = [ipanel];
+
+	me.fieldDefaults = {
+	    labelWidth: 150,
+	};
+
+	me.callParent();
+
+	if (!me.isCreate) {
+	    me.load({
+		success: function(response, options) {
+		    let values = response.result.data;
+
+		    values.enable = values.disable ? 0 : 1;
+		    me.down('inputpanel[reference=remoteeditpanel]').setValues(values);
+		    me.down('inputpanel[reference=prunepanel]').setValues(values);
+		},
+	    });
+	}
+    },
+});
+
+Ext.define('PMG.PBSScheduleEdit', {
+    extend: 'Proxmox.window.Edit',
+    xtype: 'pmgPBSScheduleEdit',
+
+    isAdd: true,
+    method: 'POST',
+    subject: gettext('Scheduled Backup'),
+    autoLoad: true,
+    items: [
+	{
+	    xtype: 'proxmoxKVComboBox',
+	    name: 'schedule',
+	    fieldLabel: gettext('Schedule'),
+	    comboItems: [
+		['daily', 'daily'],
+		['hourly', 'hourly'],
+		['weekly', 'weekly'],
+		['monthly', 'monthly'],
+	    ],
+	    editable: true,
+	    emptyText: 'Systemd Calender Event',
+	},
+	{
+	    xtype: 'proxmoxKVComboBox',
+	    name: 'delay',
+	    fieldLabel: gettext('Random Delay'),
+	    comboItems: [
+		['0s', 'no delay'],
+		['15 minutes', '15 Minutes'],
+		['6 hours', '6 hours'],
+	    ],
+	    editable: true,
+	    emptyText: 'Systemd TimeSpan',
+	},
+    ],
+    initComponent: function() {
+	let me = this;
+
+	me.url = '/nodes/' + Proxmox.NodeName + '/pbs/' + me.remote + '/timer';
+	me.callParent();
+    },
+});
+
+Ext.define('PMG.PBSConfig', {
+    extend: 'Ext.panel.Panel',
+    xtype: 'pmgPBSConfig',
+
+    controller: {
+	xclass: 'Ext.app.ViewController',
+
+	callRestore: function(grid, record) {
+	    let name = this.getViewModel().get('name');
+	    Ext.create('PMG.RestoreWindow', {
+		name: name,
+		backup_time: record.data.time,
+	    }).show();
+	},
+
+	restoreSnapshot: function(button) {
+	    let me = this;
+	    let view = me.lookup('pbsremotegrid');
+	    let record = view.getSelection()[0];
+	    me.callRestore(view, record);
+	},
+
+	runBackup: function(button) {
+	    let me = this;
+	    let view = me.lookup('pbsremotegrid');
+	    let name = me.getViewModel().get('name');
+	    Proxmox.Utils.API2Request({
+		url: "/nodes/" + Proxmox.NodeName + "/pbs/" + name + "/backup",
+		method: 'POST',
+		waitMsgTarget: view,
+		failure: function(response, opts) {
+		    Ext.Msg.alert(gettext('Error'), response.htmlStatus);
+		},
+		success: function(response, opts) {
+		    let upid = response.result.data;
+
+		    let win = Ext.create('Proxmox.window.TaskViewer', {
+			upid: upid,
+		    });
+		    win.show();
+		    me.mon(win, 'close', function() { view.getStore().load(); });
+		},
+	    });
+	},
+
+	reload: function(grid) {
+	    let me = this;
+	    let selection = grid.getSelection();
+	    me.showInfo(grid, selection);
+	},
+
+	showInfo: function(grid, selected) {
+	    let me = this;
+	    let viewModel = me.getViewModel();
+	    if (selected[0]) {
+		let name = selected[0].data.remote;
+		viewModel.set('selected', true);
+		viewModel.set('name', name);
+
+		// set grid stores and load them
+		let remstore = me.lookup('pbsremotegrid').getStore();
+		remstore.getProxy().setUrl('/api2/json/nodes/' + Proxmox.NodeName + '/pbs/' + name + '/snapshots');
+		remstore.load();
+	    } else {
+		viewModel.set('selected', false);
+	    }
+	},
+	reloadSnapshots: function() {
+	    let me = this;
+	    let grid = me.lookup('grid');
+	    let selection = grid.getSelection();
+	    me.showInfo(grid, selection);
+	},
+	init: function(view) {
+	    let me = this;
+	    me.lookup('grid').relayEvents(view, ['activate']);
+	    let pbsremotegrid = me.lookup('pbsremotegrid');
+
+	    Proxmox.Utils.monStoreErrors(pbsremotegrid, pbsremotegrid.getStore(), true);
+	},
+
+	control: {
+	    'grid[reference=grid]': {
+		selectionchange: 'showInfo',
+		load: 'reload',
+	    },
+	    'grid[reference=pbsremotegrid]': {
+		itemdblclick: 'restoreSnapshot',
+	    },
+	},
+    },
+
+    viewModel: {
+	data: {
+	    name: '',
+	    selected: false,
+	},
+    },
+
+    layout: 'border',
+
+    items: [
+	{
+	    region: 'center',
+	    reference: 'grid',
+	    xtype: 'pmgPBSConfigGrid',
+	    border: false,
+	},
+	{
+	    xtype: 'grid',
+	    region: 'south',
+	    reference: 'pbsremotegrid',
+	    hidden: true,
+	    height: '70%',
+	    border: false,
+	    split: true,
+	    emptyText: gettext('No backups on remote'),
+	    tbar: [
+		{
+		    xtype: 'proxmoxButton',
+		    text: gettext('Backup'),
+		    handler: 'runBackup',
+		    selModel: false,
+		},
+		{
+		    xtype: 'proxmoxButton',
+		    text: gettext('Restore'),
+		    handler: 'restoreSnapshot',
+		    disabled: true,
+		},
+		{
+		    xtype: 'proxmoxStdRemoveButton',
+		    text: gettext('Forget Snapshot'),
+		    disabled: true,
+		    getUrl: function(rec) {
+			let me = this;
+			let remote = me.lookupViewModel().get('name');
+			return '/nodes/' + Proxmox.NodeName + '/pbs/' + remote +'/snapshots/'+ rec.data.time;
+		    },
+		    confirmMsg: function(rec) {
+			let me = this;
+			let time = rec.data.time;
+			return Ext.String.format(gettext('Are you sure you want to forget snapshot {0}'), `'${time}'`);
+		    },
+		    callback: 'reloadSnapshots',
+		},
+	    ],
+	    store: {
+		fields: ['time', 'size', 'ctime', 'encrypted'],
+		proxy: { type: 'proxmox' },
+		sorters: [
+		    {
+			property: 'time',
+			direction: 'DESC',
+		    },
+		],
+	    },
+	    bind: {
+		title: Ext.String.format(gettext("Backup snapshots on '{0}'"), '{name}'),
+		hidden: '{!selected}',
+	    },
+	    columns: [
+		{
+		    text: 'Time',
+		    dataIndex: 'time',
+		    flex: 1,
+		},
+		{
+		    text: 'Size',
+		    dataIndex: 'size',
+		    flex: 1,
+		},
+		{
+		    text: 'Encrypted',
+		    dataIndex: 'encrypted',
+		    renderer: Proxmox.Utils.format_boolean,
+		    flex: 1,
+		},
+	    ],
+	},
+    ],
+
+});
+
+Ext.define('pmg-pbs-config', {
+    extend: 'Ext.data.Model',
+    fields: ['remote', 'server', 'datastore', 'username', 'disabled'],
+    proxy: {
+	type: 'proxmox',
+	url: '/api2/json/config/pbs',
+    },
+    idProperty: 'remote',
+});
+
+Ext.define('PMG.PBSConfigGrid', {
+    extend: 'Ext.grid.GridPanel',
+    xtype: 'pmgPBSConfigGrid',
+
+    controller: {
+	xclass: 'Ext.app.ViewController',
+
+	run_editor: function() {
+	    let me = this;
+	    let view = me.getView();
+	    let rec = view.getSelection()[0];
+	    if (!rec) {
+		return;
+	    }
+
+	    let win = Ext.createWidget('pmgPBSEdit', {
+		remoteId: rec.data.remote,
+	    });
+	    win.on('destroy', me.reload, me);
+	    win.load();
+	    win.show();
+	},
+
+	newRemote: function() {
+	    let me = this;
+	    let win = Ext.createWidget('pmgPBSEdit', {});
+	    win.on('destroy', me.reload, me);
+	    win.show();
+	},
+
+
+	reload: function() {
+	    let me = this;
+	    let view = me.getView();
+	    view.getStore().load();
+	    view.fireEvent('load', view);
+	},
+
+	createSchedule: function() {
+	    let me = this;
+	    let view = me.getView();
+	    let rec = view.getSelection()[0];
+	    let remotename = rec.data.remote;
+	    let win = Ext.createWidget('pmgPBSScheduleEdit', {
+		remote: remotename,
+	    });
+	    win.on('destroy', me.reload, me);
+	    win.show();
+	},
+
+	init: function(view) {
+	    let me = this;
+	    Proxmox.Utils.monStoreErrors(view, view.getStore(), true);
+	},
+
+    },
+
+    store: {
+	model: 'pmg-pbs-config',
+	sorters: [{
+	    property: 'remote',
+	    order: 'DESC',
+	}],
+    },
+
+    tbar: [
+	{
+	    xtype: 'proxmoxButton',
+	    text: gettext('Edit'),
+	    disabled: true,
+	    handler: 'run_editor',
+	},
+	{
+	    text: gettext('Create'),
+	    handler: 'newRemote',
+	},
+	{
+	    xtype: 'proxmoxStdRemoveButton',
+	    baseurl: '/config/pbs',
+	    callback: 'reload',
+	},
+	{
+	    xtype: 'proxmoxButton',
+	    text: gettext('Schedule'),
+	    enableFn: function(rec) {
+		return !rec.data.disable;
+	    },
+	    disabled: true,
+	    handler: 'createSchedule',
+	},
+	{
+	    xtype: 'proxmoxStdRemoveButton',
+	    baseurl: '/nodes/' + Proxmox.NodeName + '/pbs/',
+	    callback: 'reload',
+	    text: gettext('Remove Schedule'),
+	    confirmMsg: function(rec) {
+		let me = this;
+		let name = rec.getId();
+		return Ext.String.format(gettext('Are you sure you want to remove the schedule for {0}'), `'${name}'`);
+	    },
+	    getUrl: function(rec) {
+		let me = this;
+		return me.baseurl + '/' + rec.getId() + '/timer';
+	    },
+	},
+    ],
+
+    listeners: {
+	itemdblclick: 'run_editor',
+	activate: 'reload',
+    },
+
+    columns: [
+	{
+	    header: gettext('Backup Server name'),
+	    sortable: true,
+	    dataIndex: 'remote',
+	    flex: 2,
+	},
+	{
+	    header: gettext('Server'),
+	    sortable: true,
+	    dataIndex: 'server',
+	    flex: 2,
+	},
+	{
+	    header: gettext('Datastore'),
+	    sortable: true,
+	    dataIndex: 'datastore',
+	    flex: 1,
+	},
+	{
+	    header: gettext('User ID'),
+	    sortable: true,
+	    dataIndex: 'username',
+	    flex: 1,
+	},
+	{
+	    header: gettext('Encryption'),
+	    width: 80,
+	    sortable: true,
+	    dataIndex: 'encryption-key',
+	    renderer: Proxmox.Utils.format_boolean,
+	},
+	{
+	    header: gettext('Enabled'),
+	    width: 80,
+	    sortable: true,
+	    dataIndex: 'disable',
+	    renderer: Proxmox.Utils.format_neg_boolean,
+	},
+    ],
+
+});
+
-- 
2.20.1





^ permalink raw reply	[flat|nested] 26+ messages in thread

* Re: [pmg-devel] [PATCH pve-common/api/gui] add initial PBS integration
  2020-10-28 18:54 [pmg-devel] [PATCH pve-common/api/gui] add initial PBS integration Stoiko Ivanov
                   ` (15 preceding siblings ...)
  2020-10-28 18:54 ` [pmg-devel] [PATCH pmg-gui 3/3] add PBSConfig tab to Backup menu Stoiko Ivanov
@ 2020-10-30  5:47 ` Dietmar Maurer
  16 siblings, 0 replies; 26+ messages in thread
From: Dietmar Maurer @ 2020-10-30  5:47 UTC (permalink / raw)
  To: Stoiko Ivanov, pmg-devel

> changes RFC->v1:
> * moved the potentially reusable parts to pve-common (PBSTools.pm, and 2
>   functions in Systemd.pm)

I would wait with that until we have a fully functional encryption 
implementation in PVE.

IMHO, we do nor need encryption for PMG now.

> * added GUI support (mostly adapted from the LDAPConfig) - huge thanks to
>   Dominik for his patience and help!
> * added support for encrypted backups

I would delay this (implement it on PVE first)

> * added support for pruning backups

We can do this after backup - no need for systemd timers.

> * added a systemd-timer, which runs a daily prune based on the configured
>   settings

see above




^ permalink raw reply	[flat|nested] 26+ messages in thread

* [pmg-devel] applied: [PATCH pmg-api 01/11] drop left-over commented out code
  2020-10-28 18:54 ` [pmg-devel] [PATCH pmg-api 01/11] drop left-over commented out code Stoiko Ivanov
@ 2020-10-30  5:58   ` Dietmar Maurer
  0 siblings, 0 replies; 26+ messages in thread
From: Dietmar Maurer @ 2020-10-30  5:58 UTC (permalink / raw)
  To: Stoiko Ivanov, pmg-devel





^ permalink raw reply	[flat|nested] 26+ messages in thread

* [pmg-devel] applied: [PATCH pmg-api 02/11] Backup: split backup creation and creating tar
  2020-10-28 18:54 ` [pmg-devel] [PATCH pmg-api 02/11] Backup: split backup creation and creating tar Stoiko Ivanov
@ 2020-10-30  6:20   ` Dietmar Maurer
  0 siblings, 0 replies; 26+ messages in thread
From: Dietmar Maurer @ 2020-10-30  6:20 UTC (permalink / raw)
  To: Stoiko Ivanov, pmg-devel

applied - see comment inline

> On 10/28/2020 7:54 PM Stoiko Ivanov <s.ivanov@proxmox.com> wrote:
> 
>  
> In preparation for integrating PMG with PBS split the current creation of
> a PMG backup into 2 methods:
> * create all files in a backup in a target directory
> * create a tarball from a backup in a temporary directory
> 
> use the changed method in the backup API call.
> 
> Signed-off-by: Stoiko Ivanov <s.ivanov@proxmox.com>
> ---
>  src/PMG/API2/Backup.pm |  2 +-
>  src/PMG/Backup.pm      | 41 ++++++++++++++++++++++++++++++++---------
>  2 files changed, 33 insertions(+), 10 deletions(-)
> 
> diff --git a/src/PMG/API2/Backup.pm b/src/PMG/API2/Backup.pm
> index 0bfcfc9..08c06b5 100644
> --- a/src/PMG/API2/Backup.pm
> +++ b/src/PMG/API2/Backup.pm
> @@ -131,7 +131,7 @@ __PACKAGE__->register_method ({
>  
>  	    print "starting backup to: $filename\n";
>  
> -	    PMG::Backup::pmg_backup($filename, $param->{statistic});
> +	    PMG::Backup::pmg_backup_pack($filename, $param->{statistic});
>  
>  	    print "backup finished\n";
>  
> diff --git a/src/PMG/Backup.pm b/src/PMG/Backup.pm
> index 025bac2..40c41f3 100644
> --- a/src/PMG/Backup.pm
> +++ b/src/PMG/Backup.pm
> @@ -133,10 +133,11 @@ sub dumpstatdb {
>  }
>  
>  sub pmg_backup {
> -    my ($filename, $include_statistics) = @_;
> +    my ($dirname, $include_statistics) = @_;
> +
> +    die "No backupdir provided!\n" if !defined($dirname);
>  
>      my $time = time;
> -    my $dirname = "/tmp/proxbackup_$$.$time";
>      my $dbfn = "Proxmox_ruledb.sql";
>      my $statfn = "Proxmox_statdb.sql";
>      my $tarfn = "config_backup.tar";
> @@ -145,12 +146,7 @@ sub pmg_backup {
>  
>      eval {
>  
> -	my $targetdir = dirname($filename);
> -	mkdir $targetdir; # try to create target dir
> -	-d $targetdir ||
> -	    die "unable to access target directory '$targetdir'\n";
> -
> -	# create a temporary directory
> +	# create backup directory
>  	mkdir $dirname;

seems we do this twice now

>  
>  	# dump the database first
> @@ -197,7 +193,34 @@ sub pmg_backup {
>  	system("cd $dirname; md5sum $tarfn $dbfn $extradb $verfn> $sigfn") == 0 ||
>  	    die "unable to create backup signature: ERROR";
>  
> -	system("rm -f $filename; tar czf $filename -C $dirname $verfn $sigfn $dbfn $extradb $tarfn") == 0 ||
> +    };
> +    my $err = $@;
> +
> +    if ($err) {
> +	die $err;
> +    }
> +}
> +
> +sub pmg_backup_pack {
> +    my ($filename, $include_statistics) = @_;
> +
> +    my $time = time;
> +    my $dirname = "/tmp/proxbackup_$$.$time";
> +
> +    eval {
> +
> +	my $targetdir = dirname($filename);
> +	mkdir $targetdir; # try to create target dir
> +	-d $targetdir ||
> +	    die "unable to access target directory '$targetdir'\n";
> +
> +	rmtree $dirname;
> +	# create backup directory
> +	mkdir $dirname;
> +
> +	pmg_backup($dirname, $include_statistics);
> +
> +	system("rm -f $filename; tar czf $filename --strip-components=1 -C $dirname .") == 0 ||
>  	    die "unable to create backup archive: ERROR";
>      };
>      my $err = $@;
> -- 
> 2.20.1
> 
> 
> 
> _______________________________________________
> pmg-devel mailing list
> pmg-devel@lists.proxmox.com
> https://lists.proxmox.com/cgi-bin/mailman/listinfo/pmg-devel




^ permalink raw reply	[flat|nested] 26+ messages in thread

* [pmg-devel] applied: [PATCH pmg-api 03/11] Restore: optionally restore from directory
  2020-10-28 18:54 ` [pmg-devel] [PATCH pmg-api 03/11] Restore: optionally restore from directory Stoiko Ivanov
@ 2020-10-30  6:26   ` Dietmar Maurer
  0 siblings, 0 replies; 26+ messages in thread
From: Dietmar Maurer @ 2020-10-30  6:26 UTC (permalink / raw)
  To: Stoiko Ivanov, pmg-devel

applied




^ permalink raw reply	[flat|nested] 26+ messages in thread

* [pmg-devel] applied: [PATCH pmg-api 04/11] Backup: push restore options to PMG::Backup
  2020-10-28 18:54 ` [pmg-devel] [PATCH pmg-api 04/11] Backup: push restore options to PMG::Backup Stoiko Ivanov
@ 2020-10-30  6:32   ` Dietmar Maurer
  0 siblings, 0 replies; 26+ messages in thread
From: Dietmar Maurer @ 2020-10-30  6:32 UTC (permalink / raw)
  To: Stoiko Ivanov, pmg-devel

applied




^ permalink raw reply	[flat|nested] 26+ messages in thread

* [pmg-devel] applied: [PATCH pmg-api 05/11] debian: add dependency on proxmox-backup-client
  2020-10-28 18:54 ` [pmg-devel] [PATCH pmg-api 05/11] debian: add dependency on proxmox-backup-client Stoiko Ivanov
@ 2020-10-30  6:33   ` Dietmar Maurer
  0 siblings, 0 replies; 26+ messages in thread
From: Dietmar Maurer @ 2020-10-30  6:33 UTC (permalink / raw)
  To: Stoiko Ivanov, pmg-devel

applied




^ permalink raw reply	[flat|nested] 26+ messages in thread

* Re: [pmg-devel] [PATCH pmg-api 06/11] add initial SectionConfig for pbs
  2020-10-28 18:54 ` [pmg-devel] [PATCH pmg-api 06/11] add initial SectionConfig for pbs Stoiko Ivanov
@ 2020-10-30  6:38   ` Dietmar Maurer
  0 siblings, 0 replies; 26+ messages in thread
From: Dietmar Maurer @ 2020-10-30  6:38 UTC (permalink / raw)
  To: Stoiko Ivanov, pmg-devel

> This commit needs a versioned dependency on pve-common

As menitioned before, I would like to have a fully functional encryption
implementation in PVE before we implement it here.




^ permalink raw reply	[flat|nested] 26+ messages in thread

* [pmg-devel] applied: [PATCH pve-common 1/2] Systemd: add helpers for parsing unit files
  2020-10-28 18:54 ` [pmg-devel] [PATCH pve-common 1/2] Systemd: add helpers for parsing unit files Stoiko Ivanov
@ 2020-10-30  8:26   ` Dietmar Maurer
  2020-11-10  8:34   ` [pmg-devel] " Thomas Lamprecht
  1 sibling, 0 replies; 26+ messages in thread
From: Dietmar Maurer @ 2020-10-30  8:26 UTC (permalink / raw)
  To: Stoiko Ivanov, pmg-devel

applied




^ permalink raw reply	[flat|nested] 26+ messages in thread

* Re: [pmg-devel] [PATCH pve-common 1/2] Systemd: add helpers for parsing unit files
  2020-10-28 18:54 ` [pmg-devel] [PATCH pve-common 1/2] Systemd: add helpers for parsing unit files Stoiko Ivanov
  2020-10-30  8:26   ` [pmg-devel] applied: " Dietmar Maurer
@ 2020-11-10  8:34   ` Thomas Lamprecht
  1 sibling, 0 replies; 26+ messages in thread
From: Thomas Lamprecht @ 2020-11-10  8:34 UTC (permalink / raw)
  To: Stoiko Ivanov, pmg-devel

On 28.10.20 19:54, Stoiko Ivanov wrote:
> taken from pve-storage/PVE/API2/Disks/Directory.pm (and made available as
> public sub)

we could now replace those usages over there? Not urgent, just as reminder. 

> 
> Signed-off-by: Stoiko Ivanov <s.ivanov@proxmox.com>
> ---
> refactoring pve-storage to use this (and drop the now duplicated code) will
> be done separately
> 
>  src/PVE/Systemd.pm | 72 ++++++++++++++++++++++++++++++++++++++++++++++
>  1 file changed, 72 insertions(+)
> 
> diff --git a/src/PVE/Systemd.pm b/src/PVE/Systemd.pm
> index 85b35a3..bcba5eb 100644
> --- a/src/PVE/Systemd.pm
> +++ b/src/PVE/Systemd.pm
> @@ -7,6 +7,8 @@ use Net::DBus qw(dbus_uint32 dbus_uint64);
>  use Net::DBus::Callback;
>  use Net::DBus::Reactor;
>  
> +use PVE::Tools qw(file_set_contents file_get_contents trim);
> +
>  sub escape_unit {
>      my ($val, $is_path) = @_;
>  
> @@ -163,4 +165,74 @@ sub wait_for_unit_removed($;$) {
>      }, $timeout);
>  }
>  
> +sub read_ini {
> +    my ($filename) = @_;
> +
> +    my $content = file_get_contents($filename);
> +    my @lines = split /\n/, $content;
> +
> +    my $result = {};
> +    my $section;
> +
> +    foreach my $line (@lines) {
> +	$line = trim($line);
> +	if ($line =~ m/^\[([^\]]+)\]/) {
> +	    $section = $1;
> +	    if (!defined($result->{$section})) {
> +		$result->{$section} = {};
> +	    }
> +	} elsif ($line =~ m/^(.*?)=(.*)$/) {
> +	    my ($key, $val) = ($1, $2);
> +	    if (!$section) {
> +		warn "key value pair found without section, skipping\n";
> +		next;
> +	    }
> +
> +	    if ($result->{$section}->{$key}) {
> +		# make duplicate properties to arrays to keep the order
> +		my $prop = $result->{$section}->{$key};
> +		if (ref($prop) eq 'ARRAY') {
> +		    push @$prop, $val;
> +		} else {
> +		    $result->{$section}->{$key} = [$prop, $val];
> +		}
> +	    } else {
> +		$result->{$section}->{$key} = $val;
> +	    }
> +	}
> +	# ignore everything else
> +    }
> +
> +    return $result;
> +};
> +
> +sub write_ini {
> +    my ($ini, $filename) = @_;
> +
> +    my $content = "";
> +
> +    foreach my $sname (sort keys %$ini) {
> +	my $section = $ini->{$sname};
> +
> +	$content .= "[$sname]\n";
> +
> +	foreach my $pname (sort keys %$section) {
> +	    my $prop = $section->{$pname};
> +
> +	    if (!ref($prop)) {
> +		$content .= "$pname=$prop\n";
> +	    } elsif (ref($prop) eq 'ARRAY') {
> +		foreach my $val (@$prop) {
> +		    $content .= "$pname=$val\n";
> +		}
> +	    } else {
> +		die "invalid property '$pname'\n";
> +	    }
> +	}
> +	$content .= "\n";
> +    }
> +
> +    file_set_contents($filename, $content);
> +};
> +
>  1;
> 





^ permalink raw reply	[flat|nested] 26+ messages in thread

end of thread, other threads:[~2020-11-10  8:35 UTC | newest]

Thread overview: 26+ messages (download: mbox.gz / follow: Atom feed)
-- links below jump to the message on this page --
2020-10-28 18:54 [pmg-devel] [PATCH pve-common/api/gui] add initial PBS integration Stoiko Ivanov
2020-10-28 18:54 ` [pmg-devel] [PATCH pve-common 1/2] Systemd: add helpers for parsing unit files Stoiko Ivanov
2020-10-30  8:26   ` [pmg-devel] applied: " Dietmar Maurer
2020-11-10  8:34   ` [pmg-devel] " Thomas Lamprecht
2020-10-28 18:54 ` [pmg-devel] [PATCH pve-common 2/2] add helper module for handling PBS Integration Stoiko Ivanov
2020-10-28 18:54 ` [pmg-devel] [PATCH pmg-api 01/11] drop left-over commented out code Stoiko Ivanov
2020-10-30  5:58   ` [pmg-devel] applied: " Dietmar Maurer
2020-10-28 18:54 ` [pmg-devel] [PATCH pmg-api 02/11] Backup: split backup creation and creating tar Stoiko Ivanov
2020-10-30  6:20   ` [pmg-devel] applied: " Dietmar Maurer
2020-10-28 18:54 ` [pmg-devel] [PATCH pmg-api 03/11] Restore: optionally restore from directory Stoiko Ivanov
2020-10-30  6:26   ` [pmg-devel] applied: " Dietmar Maurer
2020-10-28 18:54 ` [pmg-devel] [PATCH pmg-api 04/11] Backup: push restore options to PMG::Backup Stoiko Ivanov
2020-10-30  6:32   ` [pmg-devel] applied: " Dietmar Maurer
2020-10-28 18:54 ` [pmg-devel] [PATCH pmg-api 05/11] debian: add dependency on proxmox-backup-client Stoiko Ivanov
2020-10-30  6:33   ` [pmg-devel] applied: " Dietmar Maurer
2020-10-28 18:54 ` [pmg-devel] [PATCH pmg-api 06/11] add initial SectionConfig for pbs Stoiko Ivanov
2020-10-30  6:38   ` Dietmar Maurer
2020-10-28 18:54 ` [pmg-devel] [PATCH pmg-api 07/11] Add API2 module for PBS configuration Stoiko Ivanov
2020-10-28 18:54 ` [pmg-devel] [PATCH pmg-api 08/11] Add API2 module for per-node backups to PBS Stoiko Ivanov
2020-10-28 18:54 ` [pmg-devel] [PATCH pmg-api 09/11] pbs-integration: add CLI calls to pmgbackup Stoiko Ivanov
2020-10-28 18:54 ` [pmg-devel] [PATCH pmg-api 10/11] add scheduled backup to PBS remotes Stoiko Ivanov
2020-10-28 18:54 ` [pmg-devel] [PATCH pmg-api 11/11] add daily timer for pruning configured remotes Stoiko Ivanov
2020-10-28 18:54 ` [pmg-devel] [PATCH pmg-gui 1/3] Make Backup/Restore panel a menuentry Stoiko Ivanov
2020-10-28 18:54 ` [pmg-devel] [PATCH pmg-gui 2/3] refactor RestoreWindow for PBS Stoiko Ivanov
2020-10-28 18:54 ` [pmg-devel] [PATCH pmg-gui 3/3] add PBSConfig tab to Backup menu Stoiko Ivanov
2020-10-30  5:47 ` [pmg-devel] [PATCH pve-common/api/gui] add initial PBS integration Dietmar Maurer

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