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 7A85960ECD for ; Mon, 19 Oct 2020 21:03:02 +0200 (CEST) Received: from firstgate.proxmox.com (localhost [127.0.0.1]) by firstgate.proxmox.com (Proxmox) with ESMTP id 71AA22F246 for ; Mon, 19 Oct 2020 21:02:32 +0200 (CEST) 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 749112F190 for ; Mon, 19 Oct 2020 21:02:25 +0200 (CEST) Received: from proxmox-new.maurer-it.com (localhost.localdomain [127.0.0.1]) by proxmox-new.maurer-it.com (Proxmox) with ESMTP id 4643F444A5 for ; Mon, 19 Oct 2020 21:02:25 +0200 (CEST) From: Stoiko Ivanov To: pmg-devel@lists.proxmox.com Date: Mon, 19 Oct 2020 21:02:03 +0200 Message-Id: <20201019190209.11495-7-s.ivanov@proxmox.com> X-Mailer: git-send-email 2.20.1 In-Reply-To: <20201019190209.11495-1-s.ivanov@proxmox.com> References: <20201019190209.11495-1-s.ivanov@proxmox.com> MIME-Version: 1.0 Content-Transfer-Encoding: 8bit X-SPAM-LEVEL: Spam detection results: 0 AWL -0.294 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_SBL 0.644 Contains an URL's NS IP listed in the Spamhaus SBL blocklist [backup.pm] URIBL_SBL_A 0.1 Contains URL's A record listed in the Spamhaus SBL blocklist [backup.pm] Subject: [pmg-devel] [RFC pmg-api 06/12] add helper module for handling PBS Integration 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, 19 Oct 2020 19:03:02 -0000 PBSTools.pm contains methods which eventually should be shared between PVE and PMG, for: * handling (sensitive) config-information (passwords, encryption keys) * running PBS operations Signed-off-by: Stoiko Ivanov --- src/Makefile | 1 + src/PMG/PBSTools.pm | 239 ++++++++++++++++++++++++++++++++++++++++++++ 2 files changed, 240 insertions(+) create mode 100644 src/PMG/PBSTools.pm diff --git a/src/Makefile b/src/Makefile index 05d9598..7f9726b 100644 --- a/src/Makefile +++ b/src/Makefile @@ -66,6 +66,7 @@ LIBSOURCES = \ PMG/SMTP.pm \ PMG/Unpack.pm \ PMG/Backup.pm \ + PMG/PBSTools.pm \ PMG/RuleCache.pm \ PMG/Statistic.pm \ PMG/UserConfig.pm \ diff --git a/src/PMG/PBSTools.pm b/src/PMG/PBSTools.pm new file mode 100644 index 0000000..e2a2b60 --- /dev/null +++ b/src/PMG/PBSTools.pm @@ -0,0 +1,239 @@ +package PMG::PBSTools; + +# utility functions to talk with Proxmox Backup Server + +use strict; +use warnings; +# FIXME: cleanup to only needed modules: +use Fcntl qw(F_GETFD F_SETFD FD_CLOEXEC); +use HTTP::Request; +use IO::File; +use JSON; +use LWP::UserAgent; +use POSIX qw(strftime ENOENT); + +use PVE::Tools qw(run_command file_set_contents file_get_contents file_read_firstline trim dir_glob_foreach); +use PVE::JSONSchema qw(get_standard_option); +use PVE::Systemd; + +# Helpers +my $secret_dir; + +sub set_secret_dir { + my ($dir) = @_; + $secret_dir = $dir; +} + +sub pbs_password_file_name { + my ($scfg, $storeid) = @_; + + return "${secret_dir}/${storeid}.pw"; +} + +sub pbs_set_password { + my ($scfg, $storeid, $password) = @_; + + my $pwfile = pbs_password_file_name($scfg, $storeid); + mkdir $secret_dir; + + PVE::Tools::file_set_contents($pwfile, "$password\n", 0600); +} + +sub pbs_delete_password { + my ($scfg, $storeid) = @_; + + my $pwfile = pbs_password_file_name($scfg, $storeid); + + unlink $pwfile; +} + +sub pbs_get_password { + my ($scfg, $storeid) = @_; + + my $pwfile = pbs_password_file_name($scfg, $storeid); + + return PVE::Tools::file_read_firstline($pwfile); +} + +sub pbs_encryption_key_file_name { + my ($scfg, $storeid) = @_; + + return "${secret_dir}/${storeid}.enc"; +} + +sub pbs_set_encryption_key { + my ($scfg, $storeid, $key) = @_; + + my $encfile = pbs_encryption_key_file_name($scfg, $storeid); + mkdir $secret_dir; + + PVE::Tools::file_set_contents($encfile, "$key\n", 0600); +} + +sub pbs_delete_encryption_key { + my ($scfg, $storeid) = @_; + + my $encfile = pbs_encryption_key_file_name($scfg, $storeid); + + if (!unlink $encfile) { + return if $! == ENOENT; + die "failed to delete encryption key! $!\n"; + } +} + +sub pbs_get_encryption_key { + my ($scfg, $storeid) = @_; + + my $encfile = pbs_encryption_key_file_name($scfg, $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 ($scfg, $storeid) = @_; + + my $encryption_key_file = pbs_encryption_key_file_name($scfg, $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) = @_; + + #FIXME: my $use_crypto = $USE_CRYPT_PARAMS->{$client_cmd}; + my $use_crypto = 0; + + 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($scfg, $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($scfg, $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); +} + +# FIXME: External perl code should NOT have access to this. +# +# There should be separate functions to +# - make backups +# - restore backups +# - restore files +# with a sane API +sub run_raw_client_cmd { + my ($scfg, $storeid, $client_cmd, $param, %opts) = @_; + return do_raw_client_cmd($scfg, $storeid, $client_cmd, $param, %opts); +} + +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; +} + +my $autogen_encryption_key = sub { + my ($scfg, $storeid) = @_; + my $encfile = pbs_encryption_key_file_name($storeid); + run_command(['proxmox-backup-client', 'key', 'create', '--kdf', 'none', $encfile]); +}; + +sub print_snapshot { + my ($storeid, $btype, $bid, $btime) = @_; + + my $time_str = strftime("%FT%TZ", gmtime($btime)); + my $volname = "backup/${btype}/${bid}/${time_str}"; + + return "${storeid}:${volname}"; +} + +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