]> git.proxmox.com Git - pve-common.git/blame - src/PVE/PBSClient.pm
bump version to 8.2.1
[pve-common.git] / src / PVE / PBSClient.pm
CommitLineData
0904f388 1package PVE::PBSClient;
243568ca 2# utility functions for interaction with Proxmox Backup client CLI executable
0904f388
SI
3
4use strict;
5use warnings;
243568ca 6
0904f388 7use Fcntl qw(F_GETFD F_SETFD FD_CLOEXEC);
77e402f0 8use File::Temp qw(tempdir);
0904f388
SI
9use IO::File;
10use JSON;
77e402f0 11use POSIX qw(mkfifo strftime ENOENT);
0904f388 12
0904f388 13use PVE::JSONSchema qw(get_standard_option);
5640c3db
DC
14use 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# }
24sub 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}
0904f388
SI
43
44sub 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
243568ca
TL
52 my $self = bless {
53 scfg => $scfg,
54 storeid => $storeid,
55 secret_dir => $secret_dir
56 }, $class;
57 return $self;
0904f388
SI
58}
59
60my sub password_file_name {
61 my ($self) = @_;
62
63 return "$self->{secret_dir}/$self->{storeid}.pw";
64}
65
66sub set_password {
67 my ($self, $password) = @_;
68
69a3a585 69 my $pwfile = password_file_name($self);
0904f388
SI
70 mkdir $self->{secret_dir};
71
72 PVE::Tools::file_set_contents($pwfile, "$password\n", 0600);
73};
74
75sub delete_password {
76 my ($self) = @_;
77
69a3a585 78 my $pwfile = password_file_name($self);
0904f388 79
01d2fb78 80 unlink $pwfile or $! == ENOENT or die "deleting password file failed - $!\n";
0904f388
SI
81};
82
83sub get_password {
84 my ($self) = @_;
85
69a3a585 86 my $pwfile = password_file_name($self);
0904f388
SI
87
88 return PVE::Tools::file_read_firstline($pwfile);
89}
90
91sub encryption_key_file_name {
92 my ($self) = @_;
93
94 return "$self->{secret_dir}/$self->{storeid}.enc";
95};
96
97sub set_encryption_key {
98 my ($self, $key) = @_;
99
243568ca 100 my $encfile = $self->encryption_key_file_name();
0904f388
SI
101 mkdir $self->{secret_dir};
102
103 PVE::Tools::file_set_contents($encfile, "$key\n", 0600);
104};
105
106sub delete_encryption_key {
107 my ($self) = @_;
108
243568ca 109 my $encfile = $self->encryption_key_file_name();
0904f388
SI
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.
118my sub open_encryption_key {
119 my ($self) = @_;
120
243568ca 121 my $encryption_key_file = $self->encryption_key_file_name();
0904f388
SI
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
132my $USE_CRYPT_PARAMS = {
6b00e70c
TL
133 'proxmox-backup-client' => {
134 backup => 1,
135 restore => 1,
136 'upload-log' => 1,
137 },
138 'proxmox-file-restore' => {
139 list => 1,
140 extract => 1,
141 },
0904f388
SI
142};
143
144my sub do_raw_client_cmd {
145 my ($self, $client_cmd, $param, %opts) = @_;
146
76ddb876 147 my $client_bin = (delete $opts{binary}) || 'proxmox-backup-client';
6b00e70c 148 my $use_crypto = $USE_CRYPT_PARAMS->{$client_bin}->{$client_cmd} // 0;
0904f388 149
76ddb876
TL
150 my $client_exe = "/usr/bin/$client_bin";
151 die "executable not found '$client_exe'! $client_bin not installed?\n" if ! -x $client_exe;
0904f388
SI
152
153 my $scfg = $self->{scfg};
5640c3db 154 my $repo = get_repository($scfg);
0904f388
SI
155
156 my $userns_cmd = delete $opts{userns_cmd};
157
158 my $cmd = [];
159
160 push @$cmd, @$userns_cmd if defined($userns_cmd);
161
162 push @$cmd, $client_exe, $client_cmd;
163
164 # This must live in the top scope to not get closed before the `run_command`
165 my $keyfd;
166 if ($use_crypto) {
69a3a585 167 if (defined($keyfd = open_encryption_key($self))) {
0904f388
SI
168 my $flags = fcntl($keyfd, F_GETFD, 0)
169 // die "failed to get file descriptor flags: $!\n";
170 fcntl($keyfd, F_SETFD, $flags & ~FD_CLOEXEC)
171 or die "failed to remove FD_CLOEXEC from encryption key file descriptor\n";
172 push @$cmd, '--crypt-mode=encrypt', '--keyfd='.fileno($keyfd);
173 } else {
174 push @$cmd, '--crypt-mode=none';
175 }
176 }
177
178 push @$cmd, @$param if defined($param);
179
5640c3db 180 push @$cmd, "--repository", $repo;
4a09bdc3
WB
181 if (defined(my $ns = delete($opts{namespace}))) {
182 push @$cmd, '--ns', $ns;
183 }
0904f388 184
243568ca 185 local $ENV{PBS_PASSWORD} = $self->get_password();
0904f388
SI
186
187 local $ENV{PBS_FINGERPRINT} = $scfg->{fingerprint};
188
189 # no ascii-art on task logs
190 local $ENV{PROXMOX_OUTPUT_NO_BORDER} = 1;
191 local $ENV{PROXMOX_OUTPUT_NO_HEADER} = 1;
192
193 if (my $logfunc = $opts{logfunc}) {
194 $logfunc->("run: " . join(' ', @$cmd));
195 }
196
197 run_command($cmd, %opts);
198}
199
4a09bdc3 200my sub run_raw_client_cmd : prototype($$$%) {
0904f388 201 my ($self, $client_cmd, $param, %opts) = @_;
69a3a585 202 return do_raw_client_cmd($self, $client_cmd, $param, %opts);
0904f388
SI
203}
204
4a09bdc3
WB
205my sub run_client_cmd : prototype($$;$$$$) {
206 my ($self, $client_cmd, $param, $no_output, $binary, $namespace) = @_;
0904f388
SI
207
208 my $json_str = '';
209 my $outfunc = sub { $json_str .= "$_[0]\n" };
210
b15abdfe
SR
211 $binary //= 'proxmox-backup-client';
212
0904f388
SI
213 $param = [] if !defined($param);
214 $param = [ $param ] if !ref($param);
215
216 $param = [@$param, '--output-format=json'] if !$no_output;
217
69a3a585 218 do_raw_client_cmd(
b15abdfe
SR
219 $self,
220 $client_cmd,
221 $param,
222 outfunc => $outfunc,
223 errmsg => "$binary failed",
224 binary => $binary,
4a09bdc3 225 namespace => $namespace,
243568ca 226 );
0904f388
SI
227
228 return undef if $no_output;
229
230 my $res = decode_json($json_str);
231
232 return $res;
233}
234
235sub autogen_encryption_key {
236 my ($self) = @_;
243568ca 237 my $encfile = $self->encryption_key_file_name();
0cc6c7e0
TL
238 run_command(
239 ['proxmox-backup-client', 'key', 'create', '--kdf', 'none', $encfile],
240 errmsg => 'failed to create encryption key'
241 );
242 return file_get_contents($encfile);
0904f388
SI
243};
244
a95beed2
FE
245# TODO remove support for namespaced parameters. Needs Breaks for pmg-api and libpve-storage-perl.
246# Deprecated! The namespace should be passed in as part of the config in new().
139dc881
FE
247# Snapshot or group parameters can be either just a string and will then default to the namespace
248# that's part of the initial configuration in new(), or a tuple of `[namespace, snapshot]`.
249my sub split_namespaced_parameter : prototype($$) {
250 my ($self, $snapshot) = @_;
251 return ($self->{scfg}->{namespace}, $snapshot) if !ref($snapshot);
4a09bdc3
WB
252
253 (my $namespace, $snapshot) = @$snapshot;
254 return ($namespace, $snapshot);
255}
256
2113c7e8 257# lists all snapshots, optionally limited to a specific group
0904f388 258sub get_snapshots {
2113c7e8 259 my ($self, $group) = @_;
0904f388 260
4a09bdc3
WB
261 my $namespace;
262 if (defined($group)) {
139dc881
FE
263 ($namespace, $group) = split_namespaced_parameter($self, $group);
264 } else {
265 $namespace = $self->{scfg}->{namespace};
4a09bdc3
WB
266 }
267
0904f388 268 my $param = [];
2113c7e8 269 push @$param, $group if defined($group);
0904f388 270
4a09bdc3 271 return run_client_cmd($self, "snapshots", $param, undef, undef, $namespace);
0904f388
SI
272};
273
6674eb1e
TL
274# create a new PXAR backup of a FS directory tree - doesn't cross FS boundary
275# by default.
276sub backup_fs_tree {
89ed119d 277 my ($self, $root, $id, $pxarname, $cmd_opts) = @_;
0904f388 278
0904f388 279 die "backup-id not provided\n" if !defined($id);
6674eb1e 280 die "backup root dir not provided\n" if !defined($root);
0904f388 281 die "archive name not provided\n" if !defined($pxarname);
0904f388 282
ad6b3237
TL
283 my $param = [
284 "$pxarname.pxar:$root",
6674eb1e 285 '--backup-type', 'host',
ad6b3237
TL
286 '--backup-id', $id,
287 ];
0904f388 288
6674eb1e
TL
289 $cmd_opts //= {};
290
89ed119d 291 $cmd_opts->{namespace} = $self->{scfg}->{namespace} if defined($self->{scfg}->{namespace});
4a09bdc3 292
6674eb1e 293 return run_raw_client_cmd($self, 'backup', $param, %$cmd_opts);
0904f388
SI
294};
295
296sub restore_pxar {
8b88b2f6 297 my ($self, $snapshot, $pxarname, $target, $cmd_opts) = @_;
0904f388 298
0904f388 299 die "snapshot not provided\n" if !defined($snapshot);
0904f388 300 die "archive name not provided\n" if !defined($pxarname);
0904f388 301 die "restore-target not provided\n" if !defined($target);
0904f388 302
139dc881 303 (my $namespace, $snapshot) = split_namespaced_parameter($self, $snapshot);
4a09bdc3 304
ad6b3237
TL
305 my $param = [
306 "$snapshot",
307 "$pxarname.pxar",
308 "$target",
309 "--allow-existing-dirs", 0,
310 ];
8b88b2f6 311 $cmd_opts //= {};
0904f388 312
4a09bdc3
WB
313 $cmd_opts->{namespace} = $namespace;
314
69a3a585 315 return run_raw_client_cmd($self, 'restore', $param, %$cmd_opts);
0904f388
SI
316};
317
318sub forget_snapshot {
319 my ($self, $snapshot) = @_;
320
321 die "snapshot not provided\n" if !defined($snapshot);
322
139dc881 323 (my $namespace, $snapshot) = split_namespaced_parameter($self, $snapshot);
4a09bdc3 324
e1fa96e1 325 return run_client_cmd($self, 'forget', ["$snapshot"], 1, undef, $namespace)
0904f388
SI
326};
327
328sub prune_group {
329 my ($self, $opts, $prune_opts, $group) = @_;
330
331 die "group not provided\n" if !defined($group);
332
139dc881 333 (my $namespace, $group) = split_namespaced_parameter($self, $group);
4a09bdc3 334
0904f388
SI
335 # do nothing if no keep options specified for remote
336 return [] if scalar(keys %$prune_opts) == 0;
337
338 my $param = [];
339
340 push @$param, "--quiet";
341
342 if (defined($opts->{'dry-run'}) && $opts->{'dry-run'}) {
343 push @$param, "--dry-run", $opts->{'dry-run'};
344 }
345
346 foreach my $keep_opt (keys %$prune_opts) {
347 push @$param, "--$keep_opt", $prune_opts->{$keep_opt};
348 }
349 push @$param, "$group";
350
4a09bdc3 351 return run_client_cmd($self, 'prune', $param, undef, undef, $namespace);
0904f388
SI
352};
353
354sub status {
355 my ($self) = @_;
356
357 my $total = 0;
358 my $free = 0;
359 my $used = 0;
360 my $active = 0;
361
362 eval {
69a3a585 363 my $res = run_client_cmd($self, "status");
0904f388
SI
364
365 $active = 1;
366 $total = $res->{total};
367 $used = $res->{used};
368 $free = $res->{avail};
369 };
370 if (my $err = $@) {
371 warn $err;
372 }
373
374 return ($total, $free, $used, $active);
375};
376
67252649 377sub file_restore_list {
76e28e03 378 my ($self, $snapshot, $filepath, $base64, $extra_params) = @_;
4a09bdc3 379
139dc881 380 (my $namespace, $snapshot) = split_namespaced_parameter($self, $snapshot);
76e28e03
DC
381 my $cmd = [ $snapshot, $filepath, "--base64", $base64 ? 1 : 0];
382
383 if (my $timeout = $extra_params->{timeout}) {
384 push $cmd->@*, '--timeout', $timeout;
385 }
4a09bdc3 386
67252649
SR
387 return run_client_cmd(
388 $self,
389 "list",
76e28e03 390 $cmd,
67252649
SR
391 0,
392 "proxmox-file-restore",
4a09bdc3 393 $namespace,
67252649
SR
394 );
395}
396
77e402f0
SR
397# call sync from API, returns a fifo path for streaming data to clients,
398# pass it to file_restore_extract to start transfering data
399sub file_restore_extract_prepare {
400 my ($self) = @_;
401
402 my $tmpdir = tempdir();
403 mkfifo("$tmpdir/fifo", 0600)
404 or die "creating file download fifo '$tmpdir/fifo' failed: $!\n";
405
406 # allow reading data for proxy user
407 my $wwwid = getpwnam('www-data') ||
408 die "getpwnam failed";
409 chown $wwwid, -1, "$tmpdir"
410 or die "changing permission on fifo dir '$tmpdir' failed: $!\n";
411 chown $wwwid, -1, "$tmpdir/fifo"
412 or die "changing permission on fifo '$tmpdir/fifo' failed: $!\n";
413
414 return "$tmpdir/fifo";
415}
416
417# this blocks while data is transfered, call this from a background worker
418sub file_restore_extract {
b4b17fd9 419 my ($self, $output_file, $snapshot, $filepath, $base64, $tar) = @_;
77e402f0 420
139dc881 421 (my $namespace, $snapshot) = split_namespaced_parameter($self, $snapshot);
4a09bdc3 422
77e402f0
SR
423 my $ret = eval {
424 local $SIG{ALRM} = sub { die "got timeout\n" };
425 alarm(30);
426 sysopen(my $fh, "$output_file", O_WRONLY)
427 or die "open target '$output_file' for writing failed: $!\n";
428 alarm(0);
429
430 my $fn = fileno($fh);
431 my $errfunc = sub { print $_[0], "\n"; };
432
b4b17fd9
DC
433 my $cmd = [ $snapshot, $filepath, "-", "--base64", $base64 ? 1 : 0];
434 if ($tar) {
435 push @$cmd, '--format', 'tar', '--zstd', 1;
436 }
437
77e402f0
SR
438 return run_raw_client_cmd(
439 $self,
440 "extract",
b4b17fd9 441 $cmd,
77e402f0 442 binary => "proxmox-file-restore",
4a09bdc3 443 namespace => $namespace,
77e402f0
SR
444 errfunc => $errfunc,
445 output => ">&$fn",
446 );
447 };
448 my $err = $@;
449
450 unlink($output_file);
451 $output_file =~ s/fifo$//;
452 rmdir($output_file) if -d $output_file;
453
454 die "file restore task failed: $err" if $err;
455 return $ret;
456}
457
0904f388 4581;