From mboxrd@z Thu Jan 1 00:00:00 1970 Return-Path: Received: from firstgate.proxmox.com (firstgate.proxmox.com [212.224.123.68]) (using TLSv1.3 with cipher TLS_AES_256_GCM_SHA384 (256/256 bits) key-exchange X25519 server-signature RSA-PSS (2048 bits)) (No client certificate requested) by lists.proxmox.com (Postfix) with ESMTPS id EF75269EEF for ; Mon, 16 Nov 2020 12:01:36 +0100 (CET) Received: from firstgate.proxmox.com (localhost [127.0.0.1]) by firstgate.proxmox.com (Proxmox) with ESMTP id 0394F2DB2C for ; Mon, 16 Nov 2020 12:01:36 +0100 (CET) Received: from proxmox-new.maurer-it.com (proxmox-new.maurer-it.com [212.186.127.180]) (using TLSv1.3 with cipher TLS_AES_256_GCM_SHA384 (256/256 bits) key-exchange X25519 server-signature RSA-PSS (2048 bits)) (No client certificate requested) by firstgate.proxmox.com (Proxmox) with ESMTPS id 28D712DAC2 for ; Mon, 16 Nov 2020 12:01:32 +0100 (CET) Received: from proxmox-new.maurer-it.com (localhost.localdomain [127.0.0.1]) by proxmox-new.maurer-it.com (Proxmox) with ESMTP id DCD9943716 for ; Mon, 16 Nov 2020 12:01:31 +0100 (CET) From: Stoiko Ivanov To: pmg-devel@lists.proxmox.com Date: Mon, 16 Nov 2020 12:01:08 +0100 Message-Id: <20201116110118.7483-2-s.ivanov@proxmox.com> X-Mailer: git-send-email 2.20.1 In-Reply-To: <20201116110118.7483-1-s.ivanov@proxmox.com> References: <20201116110118.7483-1-s.ivanov@proxmox.com> MIME-Version: 1.0 Content-Transfer-Encoding: 8bit X-SPAM-LEVEL: Spam detection results: 0 AWL 0.084 Adjusted score from AWL reputation of From: address KAM_DMARC_STATUS 0.01 Test Rule for DKIM or SPF Failure with Strict Alignment RCVD_IN_DNSWL_MED -2.3 Sender listed at https://www.dnswl.org/, medium trust SPF_HELO_NONE 0.001 SPF: HELO does not publish an SPF Record SPF_PASS -0.001 SPF: sender matches SPF record URIBL_BLOCKED 0.001 ADMINISTRATOR NOTICE: The query to URIBL was blocked. See http://wiki.apache.org/spamassassin/DnsBlocklists#dnsbl-block for more information. [network.pm, otp.pm, pty.pm, pbsclient.pm, restenvironment.pm, procfstools.pm, ldap.pm] Subject: [pmg-devel] [PATCH pve-common v3 1/1] add PBSClient module X-BeenThere: pmg-devel@lists.proxmox.com X-Mailman-Version: 2.1.29 Precedence: list List-Id: Proxmox Mail Gateway development discussion List-Unsubscribe: , List-Archive: List-Post: List-Help: List-Subscribe: , X-List-Received-Date: Mon, 16 Nov 2020 11:01:37 -0000 PBSClient.pm contains methods 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 --- src/Makefile | 1 + src/PVE/PBSClient.pm | 305 +++++++++++++++++++++++++++++++++++++++++++ 2 files changed, 306 insertions(+) create mode 100644 src/PVE/PBSClient.pm diff --git a/src/Makefile b/src/Makefile index b2a4ac6..098a648 100644 --- a/src/Makefile +++ b/src/Makefile @@ -21,6 +21,7 @@ LIB_SOURCES = \ LDAP.pm \ Network.pm \ OTP.pm \ + PBSClient.pm \ PTY.pm \ ProcFSTools.pm \ RESTEnvironment.pm \ diff --git a/src/PVE/PBSClient.pm b/src/PVE/PBSClient.pm new file mode 100644 index 0000000..1d9a9f4 --- /dev/null +++ b/src/PVE/PBSClient.pm @@ -0,0 +1,305 @@ +package PVE::PBSClient; + +# 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); + +sub new { + my ($class, $scfg, $storeid, $sdir) = @_; + + die "no section config provided\n" if ref($scfg) eq ''; + die "undefined store id\n" if !defined($storeid); + + my $secret_dir = $sdir // '/etc/pve/priv/storage'; + + my $self = bless { scfg => $scfg, storeid => $storeid, secret_dir => $secret_dir }, $class; +} + +my sub password_file_name { + my ($self) = @_; + + return "$self->{secret_dir}/$self->{storeid}.pw"; +} + +sub set_password { + my ($self, $password) = @_; + + my $pwfile = password_file_name($self); + mkdir $self->{secret_dir}; + + PVE::Tools::file_set_contents($pwfile, "$password\n", 0600); +}; + +sub delete_password { + my ($self) = @_; + + my $pwfile = password_file_name($self); + + unlink $pwfile; +}; + +sub get_password { + my ($self) = @_; + + my $pwfile = password_file_name($self); + + return PVE::Tools::file_read_firstline($pwfile); +} + +sub encryption_key_file_name { + my ($self) = @_; + + return "$self->{secret_dir}/$self->{storeid}.enc"; +}; + +sub set_encryption_key { + my ($self, $key) = @_; + + my $encfile = encryption_key_file_name($self); + mkdir $self->{secret_dir}; + + PVE::Tools::file_set_contents($encfile, "$key\n", 0600); +}; + +sub delete_encryption_key { + my ($self) = @_; + + my $encfile = encryption_key_file_name($self); + + if (!unlink $encfile) { + return if $! == ENOENT; + die "failed to delete encryption key! $!\n"; + } +}; + +# Returns a file handle if there is an encryption key, or `undef` if there is not. Dies on error. +my sub open_encryption_key { + my ($self) = @_; + + my $encryption_key_file = encryption_key_file_name($self); + + 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 ($self, $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 $scfg = $self->{scfg}; + 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 = open_encryption_key($self))) { + 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} = get_password($self); + + 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 ($self, $client_cmd, $param, %opts) = @_; + return do_raw_client_cmd($self, $client_cmd, $param, %opts); +} + +my sub run_client_cmd { + my ($self, $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($self, $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 ($self) = @_; + my $encfile = encryption_key_file_name($self); + run_command(['proxmox-backup-client', 'key', 'create', '--kdf', 'none', $encfile]); +}; + +sub get_snapshots { + my ($self, $opts) = @_; + + my $param = []; + if (defined($opts->{group})) { + push @$param, $opts->{group}; + } + + return run_client_cmd($self, "snapshots", $param); +}; + +sub backup_tree { + my ($self, $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($self, 'backup', $param, %$opts); +}; + +sub restore_pxar { + my ($self, $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($self, 'restore', $param, %$opts); +}; + +sub forget_snapshot { + my ($self, $snapshot) = @_; + + die "snapshot not provided\n" if !defined($snapshot); + + my $param = []; + + push @$param, "$snapshot"; + + return run_raw_client_cmd($self, 'forget', $param); +}; + +sub prune_group { + my ($self, $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($self, 'prune', $param); +}; + +sub status { + my ($self) = @_; + + my $total = 0; + my $free = 0; + my $used = 0; + my $active = 0; + + eval { + my $res = run_client_cmd($self, "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