]> git.proxmox.com Git - pve-common.git/blame - src/PVE/PBSClient.pm
PBSClient: adapt error message to include full package names
[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
SI
7use Fcntl qw(F_GETFD F_SETFD FD_CLOEXEC);
8use IO::File;
9use JSON;
10use POSIX qw(strftime ENOENT);
11
0904f388 12use PVE::JSONSchema qw(get_standard_option);
5640c3db
DC
13use PVE::Tools qw(run_command file_set_contents file_get_contents file_read_firstline $IPV6RE);
14
15# returns a repository string suitable for proxmox-backup-client, pbs-restore, etc.
16# $scfg must have the following structure:
17# {
18# datastore
19# server
20# port (optional defaults to 8007)
21# username (optional defaults to 'root@pam')
22# }
23sub get_repository {
24 my ($scfg) = @_;
25
26 my $server = $scfg->{server};
27 die "no server given\n" if !defined($server);
28
29 $server = "[$server]" if $server =~ /^$IPV6RE$/;
30
31 if (my $port = $scfg->{port}) {
32 $server .= ":$port" if $port != 8007;
33 }
34
35 my $datastore = $scfg->{datastore};
36 die "no datastore given\n" if !defined($datastore);
37
38 my $username = $scfg->{username} // 'root@pam';
39
40 return "$username\@$server:$datastore";
41}
0904f388
SI
42
43sub new {
44 my ($class, $scfg, $storeid, $sdir) = @_;
45
46 die "no section config provided\n" if ref($scfg) eq '';
47 die "undefined store id\n" if !defined($storeid);
48
49 my $secret_dir = $sdir // '/etc/pve/priv/storage';
50
243568ca
TL
51 my $self = bless {
52 scfg => $scfg,
53 storeid => $storeid,
54 secret_dir => $secret_dir
55 }, $class;
56 return $self;
0904f388
SI
57}
58
59my sub password_file_name {
60 my ($self) = @_;
61
62 return "$self->{secret_dir}/$self->{storeid}.pw";
63}
64
65sub set_password {
66 my ($self, $password) = @_;
67
69a3a585 68 my $pwfile = password_file_name($self);
0904f388
SI
69 mkdir $self->{secret_dir};
70
71 PVE::Tools::file_set_contents($pwfile, "$password\n", 0600);
72};
73
74sub delete_password {
75 my ($self) = @_;
76
69a3a585 77 my $pwfile = password_file_name($self);
0904f388 78
69a3a585 79 unlink $pwfile or die "deleting password file failed - $!\n";
0904f388
SI
80};
81
82sub get_password {
83 my ($self) = @_;
84
69a3a585 85 my $pwfile = password_file_name($self);
0904f388
SI
86
87 return PVE::Tools::file_read_firstline($pwfile);
88}
89
90sub encryption_key_file_name {
91 my ($self) = @_;
92
93 return "$self->{secret_dir}/$self->{storeid}.enc";
94};
95
96sub set_encryption_key {
97 my ($self, $key) = @_;
98
243568ca 99 my $encfile = $self->encryption_key_file_name();
0904f388
SI
100 mkdir $self->{secret_dir};
101
102 PVE::Tools::file_set_contents($encfile, "$key\n", 0600);
103};
104
105sub delete_encryption_key {
106 my ($self) = @_;
107
243568ca 108 my $encfile = $self->encryption_key_file_name();
0904f388
SI
109
110 if (!unlink $encfile) {
111 return if $! == ENOENT;
112 die "failed to delete encryption key! $!\n";
113 }
114};
115
116# Returns a file handle if there is an encryption key, or `undef` if there is not. Dies on error.
117my sub open_encryption_key {
118 my ($self) = @_;
119
243568ca 120 my $encryption_key_file = $self->encryption_key_file_name();
0904f388
SI
121
122 my $keyfd;
123 if (!open($keyfd, '<', $encryption_key_file)) {
124 return undef if $! == ENOENT;
125 die "failed to open encryption key: $encryption_key_file: $!\n";
126 }
127
128 return $keyfd;
129}
130
131my $USE_CRYPT_PARAMS = {
132 backup => 1,
133 restore => 1,
134 'upload-log' => 1,
135};
136
137my sub do_raw_client_cmd {
138 my ($self, $client_cmd, $param, %opts) = @_;
139
140 my $use_crypto = $USE_CRYPT_PARAMS->{$client_cmd};
141
b15abdfe
SR
142 my $client_exe = (delete $opts{binary}) || 'proxmox-backup-client';
143 $client_exe = "/usr/bin/$client_exe";
9f727e55 144 die "executable not found '$client_exe'! proxmox-backup-client or proxmox-backup-file-restore not installed?\n"
0904f388
SI
145 if ! -x $client_exe;
146
147 my $scfg = $self->{scfg};
5640c3db 148 my $repo = get_repository($scfg);
0904f388
SI
149
150 my $userns_cmd = delete $opts{userns_cmd};
151
152 my $cmd = [];
153
154 push @$cmd, @$userns_cmd if defined($userns_cmd);
155
156 push @$cmd, $client_exe, $client_cmd;
157
158 # This must live in the top scope to not get closed before the `run_command`
159 my $keyfd;
160 if ($use_crypto) {
69a3a585 161 if (defined($keyfd = open_encryption_key($self))) {
0904f388
SI
162 my $flags = fcntl($keyfd, F_GETFD, 0)
163 // die "failed to get file descriptor flags: $!\n";
164 fcntl($keyfd, F_SETFD, $flags & ~FD_CLOEXEC)
165 or die "failed to remove FD_CLOEXEC from encryption key file descriptor\n";
166 push @$cmd, '--crypt-mode=encrypt', '--keyfd='.fileno($keyfd);
167 } else {
168 push @$cmd, '--crypt-mode=none';
169 }
170 }
171
172 push @$cmd, @$param if defined($param);
173
5640c3db 174 push @$cmd, "--repository", $repo;
0904f388 175
243568ca 176 local $ENV{PBS_PASSWORD} = $self->get_password();
0904f388
SI
177
178 local $ENV{PBS_FINGERPRINT} = $scfg->{fingerprint};
179
180 # no ascii-art on task logs
181 local $ENV{PROXMOX_OUTPUT_NO_BORDER} = 1;
182 local $ENV{PROXMOX_OUTPUT_NO_HEADER} = 1;
183
184 if (my $logfunc = $opts{logfunc}) {
185 $logfunc->("run: " . join(' ', @$cmd));
186 }
187
188 run_command($cmd, %opts);
189}
190
191my sub run_raw_client_cmd {
192 my ($self, $client_cmd, $param, %opts) = @_;
69a3a585 193 return do_raw_client_cmd($self, $client_cmd, $param, %opts);
0904f388
SI
194}
195
196my sub run_client_cmd {
b15abdfe 197 my ($self, $client_cmd, $param, $no_output, $binary) = @_;
0904f388
SI
198
199 my $json_str = '';
200 my $outfunc = sub { $json_str .= "$_[0]\n" };
201
b15abdfe
SR
202 $binary //= 'proxmox-backup-client';
203
0904f388
SI
204 $param = [] if !defined($param);
205 $param = [ $param ] if !ref($param);
206
207 $param = [@$param, '--output-format=json'] if !$no_output;
208
69a3a585 209 do_raw_client_cmd(
b15abdfe
SR
210 $self,
211 $client_cmd,
212 $param,
213 outfunc => $outfunc,
214 errmsg => "$binary failed",
215 binary => $binary,
243568ca 216 );
0904f388
SI
217
218 return undef if $no_output;
219
220 my $res = decode_json($json_str);
221
222 return $res;
223}
224
225sub autogen_encryption_key {
226 my ($self) = @_;
243568ca 227 my $encfile = $self->encryption_key_file_name();
0cc6c7e0
TL
228 run_command(
229 ['proxmox-backup-client', 'key', 'create', '--kdf', 'none', $encfile],
230 errmsg => 'failed to create encryption key'
231 );
232 return file_get_contents($encfile);
0904f388
SI
233};
234
2113c7e8 235# lists all snapshots, optionally limited to a specific group
0904f388 236sub get_snapshots {
2113c7e8 237 my ($self, $group) = @_;
0904f388
SI
238
239 my $param = [];
2113c7e8 240 push @$param, $group if defined($group);
0904f388 241
69a3a585 242 return run_client_cmd($self, "snapshots", $param);
0904f388
SI
243};
244
6674eb1e
TL
245# create a new PXAR backup of a FS directory tree - doesn't cross FS boundary
246# by default.
247sub backup_fs_tree {
248 my ($self, $root, $id, $pxarname, $cmd_opts) = @_;
0904f388 249
0904f388 250 die "backup-id not provided\n" if !defined($id);
6674eb1e 251 die "backup root dir not provided\n" if !defined($root);
0904f388 252 die "archive name not provided\n" if !defined($pxarname);
0904f388 253
ad6b3237
TL
254 my $param = [
255 "$pxarname.pxar:$root",
6674eb1e 256 '--backup-type', 'host',
ad6b3237
TL
257 '--backup-id', $id,
258 ];
0904f388 259
6674eb1e
TL
260 $cmd_opts //= {};
261
262 return run_raw_client_cmd($self, 'backup', $param, %$cmd_opts);
0904f388
SI
263};
264
265sub restore_pxar {
8b88b2f6 266 my ($self, $snapshot, $pxarname, $target, $cmd_opts) = @_;
0904f388 267
0904f388 268 die "snapshot not provided\n" if !defined($snapshot);
0904f388 269 die "archive name not provided\n" if !defined($pxarname);
0904f388 270 die "restore-target not provided\n" if !defined($target);
0904f388 271
ad6b3237
TL
272 my $param = [
273 "$snapshot",
274 "$pxarname.pxar",
275 "$target",
276 "--allow-existing-dirs", 0,
277 ];
8b88b2f6 278 $cmd_opts //= {};
0904f388 279
69a3a585 280 return run_raw_client_cmd($self, 'restore', $param, %$cmd_opts);
0904f388
SI
281};
282
283sub forget_snapshot {
284 my ($self, $snapshot) = @_;
285
286 die "snapshot not provided\n" if !defined($snapshot);
287
69a3a585 288 return run_raw_client_cmd($self, 'forget', ["$snapshot"]);
0904f388
SI
289};
290
291sub prune_group {
292 my ($self, $opts, $prune_opts, $group) = @_;
293
294 die "group not provided\n" if !defined($group);
295
296 # do nothing if no keep options specified for remote
297 return [] if scalar(keys %$prune_opts) == 0;
298
299 my $param = [];
300
301 push @$param, "--quiet";
302
303 if (defined($opts->{'dry-run'}) && $opts->{'dry-run'}) {
304 push @$param, "--dry-run", $opts->{'dry-run'};
305 }
306
307 foreach my $keep_opt (keys %$prune_opts) {
308 push @$param, "--$keep_opt", $prune_opts->{$keep_opt};
309 }
310 push @$param, "$group";
311
69a3a585 312 return run_client_cmd($self, 'prune', $param);
0904f388
SI
313};
314
315sub status {
316 my ($self) = @_;
317
318 my $total = 0;
319 my $free = 0;
320 my $used = 0;
321 my $active = 0;
322
323 eval {
69a3a585 324 my $res = run_client_cmd($self, "status");
0904f388
SI
325
326 $active = 1;
327 $total = $res->{total};
328 $used = $res->{used};
329 $free = $res->{avail};
330 };
331 if (my $err = $@) {
332 warn $err;
333 }
334
335 return ($total, $free, $used, $active);
336};
337
3381;