]> git.proxmox.com Git - pve-common.git/blob - src/PVE/PBSClient.pm
PBSClient: use crypt params for file 'list' and 'extract'
[pve-common.git] / src / PVE / PBSClient.pm
1 package PVE::PBSClient;
2 # utility functions for interaction with Proxmox Backup client CLI executable
3
4 use strict;
5 use warnings;
6
7 use Fcntl qw(F_GETFD F_SETFD FD_CLOEXEC);
8 use File::Temp qw(tempdir);
9 use IO::File;
10 use JSON;
11 use POSIX qw(mkfifo strftime ENOENT);
12
13 use PVE::JSONSchema qw(get_standard_option);
14 use PVE::Tools qw(run_command file_set_contents file_get_contents file_read_firstline $IPV6RE);
15
16 # returns a repository string suitable for proxmox-backup-client, pbs-restore, etc.
17 # $scfg must have the following structure:
18 # {
19 # datastore
20 # server
21 # port (optional defaults to 8007)
22 # username (optional defaults to 'root@pam')
23 # }
24 sub get_repository {
25 my ($scfg) = @_;
26
27 my $server = $scfg->{server};
28 die "no server given\n" if !defined($server);
29
30 $server = "[$server]" if $server =~ /^$IPV6RE$/;
31
32 if (my $port = $scfg->{port}) {
33 $server .= ":$port" if $port != 8007;
34 }
35
36 my $datastore = $scfg->{datastore};
37 die "no datastore given\n" if !defined($datastore);
38
39 my $username = $scfg->{username} // 'root@pam';
40
41 return "$username\@$server:$datastore";
42 }
43
44 sub new {
45 my ($class, $scfg, $storeid, $sdir) = @_;
46
47 die "no section config provided\n" if ref($scfg) eq '';
48 die "undefined store id\n" if !defined($storeid);
49
50 my $secret_dir = $sdir // '/etc/pve/priv/storage';
51
52 my $self = bless {
53 scfg => $scfg,
54 storeid => $storeid,
55 secret_dir => $secret_dir
56 }, $class;
57 return $self;
58 }
59
60 my sub password_file_name {
61 my ($self) = @_;
62
63 return "$self->{secret_dir}/$self->{storeid}.pw";
64 }
65
66 sub set_password {
67 my ($self, $password) = @_;
68
69 my $pwfile = password_file_name($self);
70 mkdir $self->{secret_dir};
71
72 PVE::Tools::file_set_contents($pwfile, "$password\n", 0600);
73 };
74
75 sub delete_password {
76 my ($self) = @_;
77
78 my $pwfile = password_file_name($self);
79
80 unlink $pwfile or die "deleting password file failed - $!\n";
81 };
82
83 sub get_password {
84 my ($self) = @_;
85
86 my $pwfile = password_file_name($self);
87
88 return PVE::Tools::file_read_firstline($pwfile);
89 }
90
91 sub encryption_key_file_name {
92 my ($self) = @_;
93
94 return "$self->{secret_dir}/$self->{storeid}.enc";
95 };
96
97 sub set_encryption_key {
98 my ($self, $key) = @_;
99
100 my $encfile = $self->encryption_key_file_name();
101 mkdir $self->{secret_dir};
102
103 PVE::Tools::file_set_contents($encfile, "$key\n", 0600);
104 };
105
106 sub delete_encryption_key {
107 my ($self) = @_;
108
109 my $encfile = $self->encryption_key_file_name();
110
111 if (!unlink $encfile) {
112 return if $! == ENOENT;
113 die "failed to delete encryption key! $!\n";
114 }
115 };
116
117 # Returns a file handle if there is an encryption key, or `undef` if there is not. Dies on error.
118 my sub open_encryption_key {
119 my ($self) = @_;
120
121 my $encryption_key_file = $self->encryption_key_file_name();
122
123 my $keyfd;
124 if (!open($keyfd, '<', $encryption_key_file)) {
125 return undef if $! == ENOENT;
126 die "failed to open encryption key: $encryption_key_file: $!\n";
127 }
128
129 return $keyfd;
130 }
131
132 my $USE_CRYPT_PARAMS = {
133 backup => 1,
134 restore => 1,
135 'upload-log' => 1,
136 list => 1,
137 extract => 1,
138 };
139
140 my sub do_raw_client_cmd {
141 my ($self, $client_cmd, $param, %opts) = @_;
142
143 my $use_crypto = $USE_CRYPT_PARAMS->{$client_cmd};
144
145 my $client_exe = (delete $opts{binary}) || 'proxmox-backup-client';
146 $client_exe = "/usr/bin/$client_exe";
147 die "executable not found '$client_exe'! proxmox-backup-client or proxmox-backup-file-restore not installed?\n"
148 if ! -x $client_exe;
149
150 my $scfg = $self->{scfg};
151 my $repo = get_repository($scfg);
152
153 my $userns_cmd = delete $opts{userns_cmd};
154
155 my $cmd = [];
156
157 push @$cmd, @$userns_cmd if defined($userns_cmd);
158
159 push @$cmd, $client_exe, $client_cmd;
160
161 # This must live in the top scope to not get closed before the `run_command`
162 my $keyfd;
163 if ($use_crypto) {
164 if (defined($keyfd = open_encryption_key($self))) {
165 my $flags = fcntl($keyfd, F_GETFD, 0)
166 // die "failed to get file descriptor flags: $!\n";
167 fcntl($keyfd, F_SETFD, $flags & ~FD_CLOEXEC)
168 or die "failed to remove FD_CLOEXEC from encryption key file descriptor\n";
169 push @$cmd, '--crypt-mode=encrypt', '--keyfd='.fileno($keyfd);
170 } else {
171 push @$cmd, '--crypt-mode=none';
172 }
173 }
174
175 push @$cmd, @$param if defined($param);
176
177 push @$cmd, "--repository", $repo;
178
179 local $ENV{PBS_PASSWORD} = $self->get_password();
180
181 local $ENV{PBS_FINGERPRINT} = $scfg->{fingerprint};
182
183 # no ascii-art on task logs
184 local $ENV{PROXMOX_OUTPUT_NO_BORDER} = 1;
185 local $ENV{PROXMOX_OUTPUT_NO_HEADER} = 1;
186
187 if (my $logfunc = $opts{logfunc}) {
188 $logfunc->("run: " . join(' ', @$cmd));
189 }
190
191 run_command($cmd, %opts);
192 }
193
194 my sub run_raw_client_cmd {
195 my ($self, $client_cmd, $param, %opts) = @_;
196 return do_raw_client_cmd($self, $client_cmd, $param, %opts);
197 }
198
199 my sub run_client_cmd {
200 my ($self, $client_cmd, $param, $no_output, $binary) = @_;
201
202 my $json_str = '';
203 my $outfunc = sub { $json_str .= "$_[0]\n" };
204
205 $binary //= 'proxmox-backup-client';
206
207 $param = [] if !defined($param);
208 $param = [ $param ] if !ref($param);
209
210 $param = [@$param, '--output-format=json'] if !$no_output;
211
212 do_raw_client_cmd(
213 $self,
214 $client_cmd,
215 $param,
216 outfunc => $outfunc,
217 errmsg => "$binary failed",
218 binary => $binary,
219 );
220
221 return undef if $no_output;
222
223 my $res = decode_json($json_str);
224
225 return $res;
226 }
227
228 sub autogen_encryption_key {
229 my ($self) = @_;
230 my $encfile = $self->encryption_key_file_name();
231 run_command(
232 ['proxmox-backup-client', 'key', 'create', '--kdf', 'none', $encfile],
233 errmsg => 'failed to create encryption key'
234 );
235 return file_get_contents($encfile);
236 };
237
238 # lists all snapshots, optionally limited to a specific group
239 sub get_snapshots {
240 my ($self, $group) = @_;
241
242 my $param = [];
243 push @$param, $group if defined($group);
244
245 return run_client_cmd($self, "snapshots", $param);
246 };
247
248 # create a new PXAR backup of a FS directory tree - doesn't cross FS boundary
249 # by default.
250 sub backup_fs_tree {
251 my ($self, $root, $id, $pxarname, $cmd_opts) = @_;
252
253 die "backup-id not provided\n" if !defined($id);
254 die "backup root dir not provided\n" if !defined($root);
255 die "archive name not provided\n" if !defined($pxarname);
256
257 my $param = [
258 "$pxarname.pxar:$root",
259 '--backup-type', 'host',
260 '--backup-id', $id,
261 ];
262
263 $cmd_opts //= {};
264
265 return run_raw_client_cmd($self, 'backup', $param, %$cmd_opts);
266 };
267
268 sub restore_pxar {
269 my ($self, $snapshot, $pxarname, $target, $cmd_opts) = @_;
270
271 die "snapshot not provided\n" if !defined($snapshot);
272 die "archive name not provided\n" if !defined($pxarname);
273 die "restore-target not provided\n" if !defined($target);
274
275 my $param = [
276 "$snapshot",
277 "$pxarname.pxar",
278 "$target",
279 "--allow-existing-dirs", 0,
280 ];
281 $cmd_opts //= {};
282
283 return run_raw_client_cmd($self, 'restore', $param, %$cmd_opts);
284 };
285
286 sub forget_snapshot {
287 my ($self, $snapshot) = @_;
288
289 die "snapshot not provided\n" if !defined($snapshot);
290
291 return run_raw_client_cmd($self, 'forget', ["$snapshot"]);
292 };
293
294 sub prune_group {
295 my ($self, $opts, $prune_opts, $group) = @_;
296
297 die "group not provided\n" if !defined($group);
298
299 # do nothing if no keep options specified for remote
300 return [] if scalar(keys %$prune_opts) == 0;
301
302 my $param = [];
303
304 push @$param, "--quiet";
305
306 if (defined($opts->{'dry-run'}) && $opts->{'dry-run'}) {
307 push @$param, "--dry-run", $opts->{'dry-run'};
308 }
309
310 foreach my $keep_opt (keys %$prune_opts) {
311 push @$param, "--$keep_opt", $prune_opts->{$keep_opt};
312 }
313 push @$param, "$group";
314
315 return run_client_cmd($self, 'prune', $param);
316 };
317
318 sub status {
319 my ($self) = @_;
320
321 my $total = 0;
322 my $free = 0;
323 my $used = 0;
324 my $active = 0;
325
326 eval {
327 my $res = run_client_cmd($self, "status");
328
329 $active = 1;
330 $total = $res->{total};
331 $used = $res->{used};
332 $free = $res->{avail};
333 };
334 if (my $err = $@) {
335 warn $err;
336 }
337
338 return ($total, $free, $used, $active);
339 };
340
341 sub file_restore_list {
342 my ($self, $snapshot, $filepath, $base64) = @_;
343 return run_client_cmd(
344 $self,
345 "list",
346 [ $snapshot, $filepath, "--base64", $base64 ? 1 : 0 ],
347 0,
348 "proxmox-file-restore",
349 );
350 }
351
352 # call sync from API, returns a fifo path for streaming data to clients,
353 # pass it to file_restore_extract to start transfering data
354 sub file_restore_extract_prepare {
355 my ($self) = @_;
356
357 my $tmpdir = tempdir();
358 mkfifo("$tmpdir/fifo", 0600)
359 or die "creating file download fifo '$tmpdir/fifo' failed: $!\n";
360
361 # allow reading data for proxy user
362 my $wwwid = getpwnam('www-data') ||
363 die "getpwnam failed";
364 chown $wwwid, -1, "$tmpdir"
365 or die "changing permission on fifo dir '$tmpdir' failed: $!\n";
366 chown $wwwid, -1, "$tmpdir/fifo"
367 or die "changing permission on fifo '$tmpdir/fifo' failed: $!\n";
368
369 return "$tmpdir/fifo";
370 }
371
372 # this blocks while data is transfered, call this from a background worker
373 sub file_restore_extract {
374 my ($self, $output_file, $snapshot, $filepath, $base64) = @_;
375
376 my $ret = eval {
377 local $SIG{ALRM} = sub { die "got timeout\n" };
378 alarm(30);
379 sysopen(my $fh, "$output_file", O_WRONLY)
380 or die "open target '$output_file' for writing failed: $!\n";
381 alarm(0);
382
383 my $fn = fileno($fh);
384 my $errfunc = sub { print $_[0], "\n"; };
385
386 return run_raw_client_cmd(
387 $self,
388 "extract",
389 [ $snapshot, $filepath, "-", "--base64", $base64 ? 1 : 0 ],
390 binary => "proxmox-file-restore",
391 errfunc => $errfunc,
392 output => ">&$fn",
393 );
394 };
395 my $err = $@;
396
397 unlink($output_file);
398 $output_file =~ s/fifo$//;
399 rmdir($output_file) if -d $output_file;
400
401 die "file restore task failed: $err" if $err;
402 return $ret;
403 }
404
405 1;