1 package PVE
::PBSClient
;
2 # utility functions for interaction with Proxmox Backup client CLI executable
7 use Fcntl
qw(F_GETFD F_SETFD FD_CLOEXEC);
8 use File
::Temp
qw(tempdir);
11 use POSIX
qw(mkfifo strftime ENOENT);
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);
16 # returns a repository string suitable for proxmox-backup-client, pbs-restore, etc.
17 # $scfg must have the following structure:
21 # port (optional defaults to 8007)
22 # username (optional defaults to 'root@pam')
27 my $server = $scfg->{server
};
28 die "no server given\n" if !defined($server);
30 $server = "[$server]" if $server =~ /^$IPV6RE$/;
32 if (my $port = $scfg->{port
}) {
33 $server .= ":$port" if $port != 8007;
36 my $datastore = $scfg->{datastore
};
37 die "no datastore given\n" if !defined($datastore);
39 my $username = $scfg->{username
} // 'root@pam';
41 return "$username\@$server:$datastore";
45 my ($class, $scfg, $storeid, $sdir) = @_;
47 die "no section config provided\n" if ref($scfg) eq '';
48 die "undefined store id\n" if !defined($storeid);
50 my $secret_dir = $sdir // '/etc/pve/priv/storage';
55 secret_dir
=> $secret_dir
60 my sub password_file_name
{
63 return "$self->{secret_dir}/$self->{storeid}.pw";
67 my ($self, $password) = @_;
69 my $pwfile = password_file_name
($self);
70 mkdir $self->{secret_dir
};
72 PVE
::Tools
::file_set_contents
($pwfile, "$password\n", 0600);
78 my $pwfile = password_file_name
($self);
80 unlink $pwfile or $! == ENOENT
or die "deleting password file failed - $!\n";
86 my $pwfile = password_file_name
($self);
88 return PVE
::Tools
::file_read_firstline
($pwfile);
91 sub encryption_key_file_name
{
94 return "$self->{secret_dir}/$self->{storeid}.enc";
97 sub set_encryption_key
{
98 my ($self, $key) = @_;
100 my $encfile = $self->encryption_key_file_name();
101 mkdir $self->{secret_dir
};
103 PVE
::Tools
::file_set_contents
($encfile, "$key\n", 0600);
106 sub delete_encryption_key
{
109 my $encfile = $self->encryption_key_file_name();
111 if (!unlink $encfile) {
112 return if $! == ENOENT
;
113 die "failed to delete encryption key! $!\n";
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
{
121 my $encryption_key_file = $self->encryption_key_file_name();
124 if (!open($keyfd, '<', $encryption_key_file)) {
125 return undef if $! == ENOENT
;
126 die "failed to open encryption key: $encryption_key_file: $!\n";
132 my $USE_CRYPT_PARAMS = {
133 'proxmox-backup-client' => {
138 'proxmox-file-restore' => {
144 my sub do_raw_client_cmd
{
145 my ($self, $client_cmd, $param, %opts) = @_;
147 my $client_bin = (delete $opts{binary
}) || 'proxmox-backup-client';
148 my $use_crypto = $USE_CRYPT_PARAMS->{$client_bin}->{$client_cmd} // 0;
150 my $client_exe = "/usr/bin/$client_bin";
151 die "executable not found '$client_exe'! $client_bin not installed?\n" if ! -x
$client_exe;
153 my $scfg = $self->{scfg
};
154 my $repo = get_repository
($scfg);
156 my $userns_cmd = delete $opts{userns_cmd
};
160 push @$cmd, @$userns_cmd if defined($userns_cmd);
162 push @$cmd, $client_exe, $client_cmd;
164 # This must live in the top scope to not get closed before the `run_command`
167 if (defined($keyfd = open_encryption_key
($self))) {
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);
174 push @$cmd, '--crypt-mode=none';
178 push @$cmd, @$param if defined($param);
180 push @$cmd, "--repository", $repo;
181 if (defined(my $ns = delete($opts{namespace
}))) {
182 push @$cmd, '--ns', $ns;
185 local $ENV{PBS_PASSWORD
} = $self->get_password();
187 local $ENV{PBS_FINGERPRINT
} = $scfg->{fingerprint
};
189 # no ascii-art on task logs
190 local $ENV{PROXMOX_OUTPUT_NO_BORDER
} = 1;
191 local $ENV{PROXMOX_OUTPUT_NO_HEADER
} = 1;
193 if (my $logfunc = $opts{logfunc
}) {
194 $logfunc->("run: " . join(' ', @$cmd));
197 run_command
($cmd, %opts);
200 my sub run_raw_client_cmd
: prototype($$$%) {
201 my ($self, $client_cmd, $param, %opts) = @_;
202 return do_raw_client_cmd
($self, $client_cmd, $param, %opts);
205 my sub run_client_cmd
: prototype($$;$$$$) {
206 my ($self, $client_cmd, $param, $no_output, $binary, $namespace) = @_;
209 my $outfunc = sub { $json_str .= "$_[0]\n" };
211 $binary //= 'proxmox-backup-client';
213 $param = [] if !defined($param);
214 $param = [ $param ] if !ref($param);
216 $param = [@$param, '--output-format=json'] if !$no_output;
223 errmsg
=> "$binary failed",
225 namespace
=> $namespace,
228 return undef if $no_output;
230 my $res = decode_json
($json_str);
235 sub autogen_encryption_key
{
237 my $encfile = $self->encryption_key_file_name();
239 ['proxmox-backup-client', 'key', 'create', '--kdf', 'none', $encfile],
240 errmsg
=> 'failed to create encryption key'
242 return file_get_contents
($encfile);
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().
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]`.
249 my sub split_namespaced_parameter
: prototype($$) {
250 my ($self, $snapshot) = @_;
251 return ($self->{scfg
}->{namespace
}, $snapshot) if !ref($snapshot);
253 (my $namespace, $snapshot) = @$snapshot;
254 return ($namespace, $snapshot);
257 # lists all snapshots, optionally limited to a specific group
259 my ($self, $group) = @_;
262 if (defined($group)) {
263 ($namespace, $group) = split_namespaced_parameter
($self, $group);
265 $namespace = $self->{scfg
}->{namespace
};
269 push @$param, $group if defined($group);
271 return run_client_cmd
($self, "snapshots", $param, undef, undef, $namespace);
274 # create a new PXAR backup of a FS directory tree - doesn't cross FS boundary
277 my ($self, $root, $id, $pxarname, $cmd_opts) = @_;
279 die "backup-id not provided\n" if !defined($id);
280 die "backup root dir not provided\n" if !defined($root);
281 die "archive name not provided\n" if !defined($pxarname);
284 "$pxarname.pxar:$root",
285 '--backup-type', 'host',
291 $cmd_opts->{namespace
} = $self->{scfg
}->{namespace
} if defined($self->{scfg
}->{namespace
});
293 return run_raw_client_cmd
($self, 'backup', $param, %$cmd_opts);
297 my ($self, $snapshot, $pxarname, $target, $cmd_opts) = @_;
299 die "snapshot not provided\n" if !defined($snapshot);
300 die "archive name not provided\n" if !defined($pxarname);
301 die "restore-target not provided\n" if !defined($target);
303 (my $namespace, $snapshot) = split_namespaced_parameter
($self, $snapshot);
309 "--allow-existing-dirs", 0,
313 $cmd_opts->{namespace
} = $namespace;
315 return run_raw_client_cmd
($self, 'restore', $param, %$cmd_opts);
318 sub forget_snapshot
{
319 my ($self, $snapshot) = @_;
321 die "snapshot not provided\n" if !defined($snapshot);
323 (my $namespace, $snapshot) = split_namespaced_parameter
($self, $snapshot);
325 return run_client_cmd
($self, 'forget', ["$snapshot"], 1, undef, $namespace)
329 my ($self, $opts, $prune_opts, $group) = @_;
331 die "group not provided\n" if !defined($group);
333 (my $namespace, $group) = split_namespaced_parameter
($self, $group);
335 # do nothing if no keep options specified for remote
336 return [] if scalar(keys %$prune_opts) == 0;
340 push @$param, "--quiet";
342 if (defined($opts->{'dry-run'}) && $opts->{'dry-run'}) {
343 push @$param, "--dry-run", $opts->{'dry-run'};
346 foreach my $keep_opt (keys %$prune_opts) {
347 push @$param, "--$keep_opt", $prune_opts->{$keep_opt};
349 push @$param, "$group";
351 return run_client_cmd
($self, 'prune', $param, undef, undef, $namespace);
363 my $res = run_client_cmd
($self, "status");
366 $total = $res->{total
};
367 $used = $res->{used
};
368 $free = $res->{avail
};
374 return ($total, $free, $used, $active);
377 sub file_restore_list
{
378 my ($self, $snapshot, $filepath, $base64) = @_;
380 (my $namespace, $snapshot) = split_namespaced_parameter
($self, $snapshot);
382 return run_client_cmd
(
385 [ $snapshot, $filepath, "--base64", $base64 ?
1 : 0 ],
387 "proxmox-file-restore",
392 # call sync from API, returns a fifo path for streaming data to clients,
393 # pass it to file_restore_extract to start transfering data
394 sub file_restore_extract_prepare
{
397 my $tmpdir = tempdir
();
398 mkfifo
("$tmpdir/fifo", 0600)
399 or die "creating file download fifo '$tmpdir/fifo' failed: $!\n";
401 # allow reading data for proxy user
402 my $wwwid = getpwnam('www-data') ||
403 die "getpwnam failed";
404 chown $wwwid, -1, "$tmpdir"
405 or die "changing permission on fifo dir '$tmpdir' failed: $!\n";
406 chown $wwwid, -1, "$tmpdir/fifo"
407 or die "changing permission on fifo '$tmpdir/fifo' failed: $!\n";
409 return "$tmpdir/fifo";
412 # this blocks while data is transfered, call this from a background worker
413 sub file_restore_extract
{
414 my ($self, $output_file, $snapshot, $filepath, $base64) = @_;
416 (my $namespace, $snapshot) = split_namespaced_parameter
($self, $snapshot);
419 local $SIG{ALRM
} = sub { die "got timeout\n" };
421 sysopen(my $fh, "$output_file", O_WRONLY
)
422 or die "open target '$output_file' for writing failed: $!\n";
425 my $fn = fileno($fh);
426 my $errfunc = sub { print $_[0], "\n"; };
428 return run_raw_client_cmd
(
431 [ $snapshot, $filepath, "-", "--base64", $base64 ?
1 : 0 ],
432 binary
=> "proxmox-file-restore",
433 namespace
=> $namespace,
440 unlink($output_file);
441 $output_file =~ s/fifo$//;
442 rmdir($output_file) if -d
$output_file;
444 die "file restore task failed: $err" if $err;