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