[pmg-devel] [PATCH pve-common 2/2] add helper module for handling PBS Integration

Stoiko Ivanov s.ivanov at proxmox.com
Wed Oct 28 19:54:18 CET 2020


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 at 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 at 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





More information about the pmg-devel mailing list