From: Stoiko Ivanov <s.ivanov@proxmox.com>
To: pmg-devel@pve.proxmox.com
Subject: [pmg-devel] [PATCH pve-common 2/2] add helper module for handling PBS Integration
Date: Wed, 28 Oct 2020 19:54:18 +0100 [thread overview]
Message-ID: <20201028185432.23067-3-s.ivanov@proxmox.com> (raw)
In-Reply-To: <20201028185432.23067-1-s.ivanov@proxmox.com>
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
next prev parent reply other threads:[~2020-10-28 18:55 UTC|newest]
Thread overview: 26+ messages / expand[flat|nested] mbox.gz Atom feed top
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 ` Stoiko Ivanov [this message]
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
Reply instructions:
You may reply publicly to this message via plain-text email
using any one of the following methods:
* Save the following mbox file, import it into your mail client,
and reply-to-all from there: mbox
Avoid top-posting and favor interleaved quoting:
https://en.wikipedia.org/wiki/Posting_style#Interleaved_style
* Reply using the --to, --cc, and --in-reply-to
switches of git-send-email(1):
git send-email \
--in-reply-to=20201028185432.23067-3-s.ivanov@proxmox.com \
--to=s.ivanov@proxmox.com \
--cc=pmg-devel@pve.proxmox.com \
/path/to/YOUR_REPLY
https://kernel.org/pub/software/scm/git/docs/git-send-email.html
* If your mail client supports setting the In-Reply-To header
via mailto: links, try the mailto: link
Be sure your reply has a Subject: header at the top and a blank line
before the message body.
This is an external index of several public inboxes,
see mirroring instructions on how to clone and mirror
all data and code used by this external index.
Service provided by Proxmox Server Solutions GmbH | Privacy | Legal