]> git.proxmox.com Git - pve-storage.git/blame - PVE/Storage/PBSPlugin.pm
pbs: update attribute: cleaner error message if not supported
[pve-storage.git] / PVE / Storage / PBSPlugin.pm
CommitLineData
271fe394
DM
1package PVE::Storage::PBSPlugin;
2
3# Plugin to access Proxmox Backup Server
4
5use strict;
6use warnings;
4133e6e2 7
76bb5feb 8use Fcntl qw(F_GETFD F_SETFD FD_CLOEXEC);
76bb5feb 9use IO::File;
271fe394 10use JSON;
c56f7a71 11use MIME::Base64 qw(decode_base64);
bfba9626
FE
12use POSIX qw(mktime strftime ENOENT);
13use POSIX::strptime;
271fe394 14
2f9eb6dc 15use PVE::APIClient::LWP;
271fe394 16use PVE::JSONSchema qw(get_standard_option);
4133e6e2 17use PVE::Network;
53003cb5 18use PVE::PBSClient;
4133e6e2
TL
19use PVE::Storage::Plugin;
20use PVE::Tools qw(run_command file_read_firstline trim dir_glob_regex dir_glob_foreach $IPV6RE);
271fe394
DM
21
22use base qw(PVE::Storage::Plugin);
23
24# Configuration
25
26sub type {
27 return 'pbs';
28}
29
30sub plugindata {
31 return {
32 content => [ {backup => 1, none => 1}, { backup => 1 }],
33 };
34}
35
36sub properties {
37 return {
38 datastore => {
4133e6e2 39 description => "Proxmox Backup Server datastore name.",
271fe394
DM
40 type => 'string',
41 },
42 # openssl s_client -connect <host>:8007 2>&1 |openssl x509 -fingerprint -sha256
43 fingerprint => get_standard_option('fingerprint-sha256'),
ce2e2733 44 'encryption-key' => {
1aeb322b 45 description => "Encryption key. Use 'autogen' to generate one automatically without passphrase.",
76bb5feb
WB
46 type => 'string',
47 },
c56f7a71 48 'master-pubkey' => {
5b955999 49 description => "Base64-encoded, PEM-formatted public RSA key. Used to encrypt a copy of the encryption-key which will be added to each encrypted backup.",
c56f7a71
FG
50 type => 'string',
51 },
4133e6e2
TL
52 port => {
53 description => "For non default port.",
54 type => 'integer',
55 minimum => 1,
56 maximum => 65535,
57 default => 8007,
2f9eb6dc 58 },
271fe394
DM
59 };
60}
61
62sub options {
63 return {
64 server => { fixed => 1 },
65 datastore => { fixed => 1 },
4133e6e2 66 port => { optional => 1 },
271fe394
DM
67 nodes => { optional => 1},
68 disable => { optional => 1},
69 content => { optional => 1},
70 username => { optional => 1 },
ce2e2733
TL
71 password => { optional => 1 },
72 'encryption-key' => { optional => 1 },
c56f7a71 73 'master-pubkey' => { optional => 1 },
271fe394 74 maxfiles => { optional => 1 },
3353698f 75 'prune-backups' => { optional => 1 },
271fe394
DM
76 fingerprint => { optional => 1 },
77 };
78}
79
80# Helpers
81
82sub pbs_password_file_name {
83 my ($scfg, $storeid) = @_;
84
462537a2 85 return "/etc/pve/priv/storage/${storeid}.pw";
271fe394
DM
86}
87
88sub pbs_set_password {
89 my ($scfg, $storeid, $password) = @_;
90
91 my $pwfile = pbs_password_file_name($scfg, $storeid);
9e34813f 92 mkdir "/etc/pve/priv/storage";
271fe394
DM
93
94 PVE::Tools::file_set_contents($pwfile, "$password\n");
95}
96
97sub pbs_delete_password {
98 my ($scfg, $storeid) = @_;
99
100 my $pwfile = pbs_password_file_name($scfg, $storeid);
101
102 unlink $pwfile;
103}
104
105sub pbs_get_password {
106 my ($scfg, $storeid) = @_;
107
108 my $pwfile = pbs_password_file_name($scfg, $storeid);
109
110 return PVE::Tools::file_read_firstline($pwfile);
111}
112
76bb5feb
WB
113sub pbs_encryption_key_file_name {
114 my ($scfg, $storeid) = @_;
115
116 return "/etc/pve/priv/storage/${storeid}.enc";
117}
118
119sub pbs_set_encryption_key {
120 my ($scfg, $storeid, $key) = @_;
121
122 my $pwfile = pbs_encryption_key_file_name($scfg, $storeid);
123 mkdir "/etc/pve/priv/storage";
124
125 PVE::Tools::file_set_contents($pwfile, "$key\n");
126}
127
128sub pbs_delete_encryption_key {
129 my ($scfg, $storeid) = @_;
130
131 my $pwfile = pbs_encryption_key_file_name($scfg, $storeid);
132
4ef17e1f
TL
133 if (!unlink $pwfile) {
134 return if $! == ENOENT;
135 die "failed to delete encryption key! $!\n";
136 }
18cf6c9f 137 delete $scfg->{'encryption-key'};
76bb5feb
WB
138}
139
140sub pbs_get_encryption_key {
141 my ($scfg, $storeid) = @_;
142
143 my $pwfile = pbs_encryption_key_file_name($scfg, $storeid);
144
145 return PVE::Tools::file_get_contents($pwfile);
146}
147
148# Returns a file handle if there is an encryption key, or `undef` if there is not. Dies on error.
149sub pbs_open_encryption_key {
150 my ($scfg, $storeid) = @_;
151
152 my $encryption_key_file = pbs_encryption_key_file_name($scfg, $storeid);
153
154 my $keyfd;
155 if (!open($keyfd, '<', $encryption_key_file)) {
156 return undef if $! == ENOENT;
157 die "failed to open encryption key: $encryption_key_file: $!\n";
158 }
159
160 return $keyfd;
161}
162
c56f7a71
FG
163sub pbs_master_pubkey_file_name {
164 my ($scfg, $storeid) = @_;
165
166 return "/etc/pve/priv/storage/${storeid}.master.pem";
167}
168
169sub pbs_set_master_pubkey {
170 my ($scfg, $storeid, $key) = @_;
171
172 my $pwfile = pbs_master_pubkey_file_name($scfg, $storeid);
173 mkdir "/etc/pve/priv/storage";
174
175 PVE::Tools::file_set_contents($pwfile, "$key\n");
176}
177
178sub pbs_delete_master_pubkey {
179 my ($scfg, $storeid) = @_;
180
181 my $pwfile = pbs_master_pubkey_file_name($scfg, $storeid);
182
183 if (!unlink $pwfile) {
184 return if $! == ENOENT;
185 die "failed to delete master public key! $!\n";
186 }
187 delete $scfg->{'master-pubkey'};
188}
189
190sub pbs_get_master_pubkey {
191 my ($scfg, $storeid) = @_;
192
193 my $pwfile = pbs_master_pubkey_file_name($scfg, $storeid);
194
195 return PVE::Tools::file_get_contents($pwfile);
196}
197
198# Returns a file handle if there is a master key, or `undef` if there is not. Dies on error.
199sub pbs_open_master_pubkey {
200 my ($scfg, $storeid) = @_;
201
202 my $master_pubkey_file = pbs_master_pubkey_file_name($scfg, $storeid);
203
204 my $keyfd;
205 if (!open($keyfd, '<', $master_pubkey_file)) {
206 return undef if $! == ENOENT;
207 die "failed to open master public key: $master_pubkey_file: $!\n";
208 }
209
210 return $keyfd;
211}
212
8602fd56
FE
213sub print_volid {
214 my ($storeid, $btype, $bid, $btime) = @_;
215
216 my $time_str = strftime("%FT%TZ", gmtime($btime));
217 my $volname = "backup/${btype}/${bid}/${time_str}";
218
219 return "${storeid}:${volname}";
220}
271fe394 221
bfba9626
FE
222# essentially the inverse of print_volid
223sub api_param_from_volname {
224 my ($class, $volname) = @_;
225
226 my $name = ($class->parse_volname($volname))[1];
227
228 my ($btype, $bid, $timestr) = split('/', $name);
229
230 my @tm = (POSIX::strptime($timestr, "%FT%TZ"));
231 # expect sec, min, hour, mday, mon, year
232 die "error parsing time from '$volname'" if grep { !defined($_) } @tm[0..5];
233
234 my $btime;
235 {
236 local $ENV{TZ} = 'UTC'; # $timestr is UTC
237
238 # Fill in isdst to avoid undef warning. No daylight saving time for UTC.
239 $tm[8] //= 0;
240
241 my $since_epoch = mktime(@tm) or die "error converting time from '$volname'\n";
242 $btime = int($since_epoch);
243 }
244
245 return {
246 'backup-type' => $btype,
247 'backup-id' => $bid,
248 'backup-time' => $btime,
249 };
250}
251
02cc5e10
WB
252my $USE_CRYPT_PARAMS = {
253 backup => 1,
254 restore => 1,
255 'upload-log' => 1,
256};
257
c56f7a71
FG
258my $USE_MASTER_KEY = {
259 backup => 1,
260};
261
76bb5feb 262my sub do_raw_client_cmd {
02cc5e10
WB
263 my ($scfg, $storeid, $client_cmd, $param, %opts) = @_;
264
265 my $use_crypto = $USE_CRYPT_PARAMS->{$client_cmd};
c56f7a71 266 my $use_master = $USE_MASTER_KEY->{$client_cmd};
271fe394 267
1574a590
TL
268 my $client_exe = '/usr/bin/proxmox-backup-client';
269 die "executable not found '$client_exe'! Proxmox backup client not installed?\n"
270 if ! -x $client_exe;
271
53003cb5 272 my $repo = PVE::PBSClient::get_repository($scfg);
271fe394
DM
273
274 my $userns_cmd = delete $opts{userns_cmd};
275
276 my $cmd = [];
277
278 push @$cmd, @$userns_cmd if defined($userns_cmd);
279
1574a590 280 push @$cmd, $client_exe, $client_cmd;
271fe394 281
76bb5feb 282 # This must live in the top scope to not get closed before the `run_command`
c56f7a71 283 my ($keyfd, $master_fd);
02cc5e10 284 if ($use_crypto) {
76bb5feb
WB
285 if (defined($keyfd = pbs_open_encryption_key($scfg, $storeid))) {
286 my $flags = fcntl($keyfd, F_GETFD, 0)
287 // die "failed to get file descriptor flags: $!\n";
288 fcntl($keyfd, F_SETFD, $flags & ~FD_CLOEXEC)
289 or die "failed to remove FD_CLOEXEC from encryption key file descriptor\n";
290 push @$cmd, '--crypt-mode=encrypt', '--keyfd='.fileno($keyfd);
c56f7a71
FG
291 if ($use_master && defined($master_fd = pbs_open_master_pubkey($scfg, $storeid))) {
292 my $flags = fcntl($master_fd, F_GETFD, 0)
293 // die "failed to get file descriptor flags: $!\n";
294 fcntl($master_fd, F_SETFD, $flags & ~FD_CLOEXEC)
295 or die "failed to remove FD_CLOEXEC from master public key file descriptor\n";
296 push @$cmd, '--master-pubkey-fd='.fileno($master_fd);
297 }
76bb5feb
WB
298 } else {
299 push @$cmd, '--crypt-mode=none';
300 }
301 }
302
271fe394
DM
303 push @$cmd, @$param if defined($param);
304
53003cb5 305 push @$cmd, "--repository", $repo;
271fe394
DM
306
307 local $ENV{PBS_PASSWORD} = pbs_get_password($scfg, $storeid);
308
309 local $ENV{PBS_FINGERPRINT} = $scfg->{fingerprint};
310
8b4c2a7e
DM
311 # no ascii-art on task logs
312 local $ENV{PROXMOX_OUTPUT_NO_BORDER} = 1;
313 local $ENV{PROXMOX_OUTPUT_NO_HEADER} = 1;
314
271fe394 315 if (my $logfunc = $opts{logfunc}) {
e6d1edcb 316 $logfunc->("run: " . join(' ', @$cmd));
271fe394
DM
317 }
318
319 run_command($cmd, %opts);
320}
321
76bb5feb
WB
322# FIXME: External perl code should NOT have access to this.
323#
324# There should be separate functions to
325# - make backups
326# - restore backups
327# - restore files
328# with a sane API
02cc5e10 329sub run_raw_client_cmd {
76bb5feb 330 my ($scfg, $storeid, $client_cmd, $param, %opts) = @_;
02cc5e10 331 return do_raw_client_cmd($scfg, $storeid, $client_cmd, $param, %opts);
76bb5feb
WB
332}
333
271fe394
DM
334sub run_client_cmd {
335 my ($scfg, $storeid, $client_cmd, $param, $no_output) = @_;
336
337 my $json_str = '';
fee2ece3 338 my $outfunc = sub { $json_str .= "$_[0]\n" };
271fe394
DM
339
340 $param = [] if !defined($param);
341 $param = [ $param ] if !ref($param);
342
343 $param = [@$param, '--output-format=json'] if !$no_output;
344
02cc5e10 345 do_raw_client_cmd($scfg, $storeid, $client_cmd, $param,
76bb5feb 346 outfunc => $outfunc, errmsg => 'proxmox-backup-client failed');
271fe394
DM
347
348 return undef if $no_output;
349
350 my $res = decode_json($json_str);
351
352 return $res;
353}
354
355# Storage implementation
356
c855ac15
DM
357sub extract_vzdump_config {
358 my ($class, $scfg, $volname, $storeid) = @_;
359
360 my ($vtype, $name, $vmid, undef, undef, undef, $format) = $class->parse_volname($volname);
361
362 my $config = '';
fee2ece3 363 my $outfunc = sub { $config .= "$_[0]\n" };
c855ac15
DM
364
365 my $config_name;
366 if ($format eq 'pbs-vm') {
367 $config_name = 'qemu-server.conf';
368 } elsif ($format eq 'pbs-ct') {
369 $config_name = 'pct.conf';
370 } else {
371 die "unable to extract configuration for backup format '$format'\n";
372 }
373
02cc5e10 374 do_raw_client_cmd($scfg, $storeid, 'restore', [ $name, $config_name, '-' ],
76bb5feb 375 outfunc => $outfunc, errmsg => 'proxmox-backup-client failed');
c855ac15
DM
376
377 return $config;
378}
379
8f26b391
FE
380sub prune_backups {
381 my ($class, $scfg, $storeid, $keep, $vmid, $type, $dryrun, $logfunc) = @_;
382
383 $logfunc //= sub { print "$_[1]\n" };
384
385 my $backups = $class->list_volumes($storeid, $scfg, $vmid, ['backup']);
386
387 $type = 'vm' if defined($type) && $type eq 'qemu';
388 $type = 'ct' if defined($type) && $type eq 'lxc';
389
390 my $backup_groups = {};
391 foreach my $backup (@{$backups}) {
392 (my $backup_type = $backup->{format}) =~ s/^pbs-//;
393
394 next if defined($type) && $backup_type ne $type;
395
396 my $backup_group = "$backup_type/$backup->{vmid}";
397 $backup_groups->{$backup_group} = 1;
398 }
399
400 my @param;
1b87f013
FE
401
402 my $keep_all = delete $keep->{'keep-all'};
403
404 if (!$keep_all) {
405 foreach my $opt (keys %{$keep}) {
406 next if $keep->{$opt} == 0;
407 push @param, "--$opt";
408 push @param, "$keep->{$opt}";
409 }
410 } else { # no need to pass anything to PBS
411 $keep = { 'keep-all' => 1 };
8f26b391
FE
412 }
413
414 push @param, '--dry-run' if $dryrun;
415
416 my $prune_list = [];
417 my $failed;
418
419 foreach my $backup_group (keys %{$backup_groups}) {
420 $logfunc->('info', "running 'proxmox-backup-client prune' for '$backup_group'")
421 if !$dryrun;
422 eval {
423 my $res = run_client_cmd($scfg, $storeid, 'prune', [ $backup_group, @param ]);
424
425 foreach my $backup (@{$res}) {
426 die "result from proxmox-backup-client is not as expected\n"
427 if !defined($backup->{'backup-time'})
428 || !defined($backup->{'backup-type'})
429 || !defined($backup->{'backup-id'})
430 || !defined($backup->{'keep'});
431
432 my $ctime = $backup->{'backup-time'};
433 my $type = $backup->{'backup-type'};
434 my $vmid = $backup->{'backup-id'};
435 my $volid = print_volid($storeid, $type, $vmid, $ctime);
436
bfba9626
FE
437 my $mark = $backup->{keep} ? 'keep' : 'remove';
438 $mark = 'protected' if $backup->{protected};
439
8f26b391
FE
440 push @{$prune_list}, {
441 ctime => $ctime,
bfba9626 442 mark => $mark,
8f26b391
FE
443 type => $type eq 'vm' ? 'qemu' : 'lxc',
444 vmid => $vmid,
445 volid => $volid,
446 };
447 }
448 };
449 if (my $err = $@) {
450 $logfunc->('err', "prune '$backup_group': $err\n");
451 $failed = 1;
452 }
453 }
454 die "error pruning backups - check log\n" if $failed;
455
456 return $prune_list;
457}
458
1aeb322b
TL
459my $autogen_encryption_key = sub {
460 my ($scfg, $storeid) = @_;
461 my $encfile = pbs_encryption_key_file_name($scfg, $storeid);
478609d3
TL
462 if (-f $encfile) {
463 rename $encfile, "$encfile.old";
464 }
4558cb6e
TL
465 my $cmd = ['proxmox-backup-client', 'key', 'create', '--kdf', 'none', $encfile];
466 run_command($cmd, errmsg => 'failed to create encryption key');
467 return PVE::Tools::file_get_contents($encfile);
1aeb322b
TL
468};
469
271fe394
DM
470sub on_add_hook {
471 my ($class, $storeid, $scfg, %param) = @_;
472
0b6b98d1
TL
473 my $res = {};
474
76bb5feb
WB
475 if (defined(my $password = $param{password})) {
476 pbs_set_password($scfg, $storeid, $password);
b494636a
DM
477 } else {
478 pbs_delete_password($scfg, $storeid);
479 }
76bb5feb 480
ce2e2733 481 if (defined(my $encryption_key = $param{'encryption-key'})) {
d2c47b38 482 my $decoded_key;
1aeb322b 483 if ($encryption_key eq 'autogen') {
0b6b98d1 484 $res->{'encryption-key'} = $autogen_encryption_key->($scfg, $storeid);
3cc2eb73 485 $decoded_key = decode_json($res->{'encryption-key'});
1aeb322b 486 } else {
d2c47b38
TL
487 $decoded_key = eval { decode_json($encryption_key) };
488 if ($@ || !exists($decoded_key->{data})) {
489 die "Value does not seems like a valid, JSON formatted encryption key!\n";
490 }
1aeb322b 491 pbs_set_encryption_key($scfg, $storeid, $encryption_key);
0b6b98d1 492 $res->{'encryption-key'} = $encryption_key;
1aeb322b 493 }
3cc2eb73 494 $scfg->{'encryption-key'} = $decoded_key->{fingerprint} || 1;
76bb5feb
WB
495 } else {
496 pbs_delete_encryption_key($scfg, $storeid);
497 }
0b6b98d1 498
c56f7a71
FG
499 if (defined(my $master_key = delete $param{'master-pubkey'})) {
500 die "'master-pubkey' can only be used together with 'encryption-key'\n"
501 if !defined($scfg->{'encryption-key'});
502
503 my $decoded = decode_base64($master_key);
504 pbs_set_master_pubkey($scfg, $storeid, $decoded);
505 $scfg->{'master-pubkey'} = 1;
506 } else {
507 pbs_delete_master_pubkey($scfg, $storeid);
508 }
509
0b6b98d1 510 return $res;
b494636a
DM
511}
512
513sub on_update_hook {
514 my ($class, $storeid, $scfg, %param) = @_;
515
0b6b98d1
TL
516 my $res = {};
517
76bb5feb
WB
518 if (exists($param{password})) {
519 if (defined($param{password})) {
520 pbs_set_password($scfg, $storeid, $param{password});
521 } else {
522 pbs_delete_password($scfg, $storeid);
523 }
524 }
b494636a 525
ce2e2733
TL
526 if (exists($param{'encryption-key'})) {
527 if (defined(my $encryption_key = delete($param{'encryption-key'}))) {
d2c47b38 528 my $decoded_key;
1aeb322b 529 if ($encryption_key eq 'autogen') {
0b6b98d1 530 $res->{'encryption-key'} = $autogen_encryption_key->($scfg, $storeid);
3cc2eb73 531 $decoded_key = decode_json($res->{'encryption-key'});
1aeb322b 532 } else {
d2c47b38
TL
533 $decoded_key = eval { decode_json($encryption_key) };
534 if ($@ || !exists($decoded_key->{data})) {
535 die "Value does not seems like a valid, JSON formatted encryption key!\n";
536 }
1aeb322b 537 pbs_set_encryption_key($scfg, $storeid, $encryption_key);
0b6b98d1 538 $res->{'encryption-key'} = $encryption_key;
1aeb322b 539 }
3cc2eb73 540 $scfg->{'encryption-key'} = $decoded_key->{fingerprint} || 1;
76bb5feb
WB
541 } else {
542 pbs_delete_encryption_key($scfg, $storeid);
c56f7a71
FG
543 delete $scfg->{'encryption-key'};
544 }
545 }
546
547 if (exists($param{'master-pubkey'})) {
548 if (defined(my $master_key = delete($param{'master-pubkey'}))) {
549 my $decoded = decode_base64($master_key);
550
551 pbs_set_master_pubkey($scfg, $storeid, $decoded);
552 $scfg->{'master-pubkey'} = 1;
553 } else {
554 pbs_delete_master_pubkey($scfg, $storeid);
76bb5feb 555 }
271fe394 556 }
0b6b98d1
TL
557
558 return $res;
271fe394
DM
559}
560
561sub on_delete_hook {
562 my ($class, $storeid, $scfg) = @_;
563
564 pbs_delete_password($scfg, $storeid);
76bb5feb 565 pbs_delete_encryption_key($scfg, $storeid);
c56f7a71 566 pbs_delete_master_pubkey($scfg, $storeid);
f3ccd0ef
FE
567
568 return;
271fe394
DM
569}
570
571sub parse_volname {
572 my ($class, $volname) = @_;
573
574 if ($volname =~ m!^backup/([^\s_]+)/([^\s_]+)/([0-9]{4}-[0-9]{2}-[0-9]{2}T[0-9]{2}:[0-9]{2}:[0-9]{2}Z)$!) {
575 my $btype = $1;
576 my $bid = $2;
577 my $btime = $3;
578 my $format = "pbs-$btype";
579
580 my $name = "$btype/$bid/$btime";
581
582 if ($bid =~ m/^\d+$/) {
583 return ('backup', $name, $bid, undef, undef, undef, $format);
584 } else {
585 return ('backup', $name, undef, undef, undef, undef, $format);
586 }
587 }
588
589 die "unable to parse PBS volume name '$volname'\n";
590}
591
592sub path {
593 my ($class, $scfg, $volname, $storeid, $snapname) = @_;
594
595 die "volume snapshot is not possible on pbs storage"
596 if defined($snapname);
597
598 my ($vtype, $name, $vmid) = $class->parse_volname($volname);
599
53003cb5 600 my $repo = PVE::PBSClient::get_repository($scfg);
271fe394 601
ffc31266 602 # artificial url - we currently do not use that anywhere
53003cb5 603 my $path = "pbs://$repo/$name";
271fe394
DM
604
605 return ($path, $vmid, $vtype);
606}
607
608sub create_base {
609 my ($class, $storeid, $scfg, $volname) = @_;
610
611 die "can't create base images in pbs storage\n";
612}
613
614sub clone_image {
615 my ($class, $scfg, $storeid, $volname, $vmid, $snap) = @_;
616
617 die "can't clone images in pbs storage\n";
618}
619
620sub alloc_image {
621 my ($class, $storeid, $scfg, $vmid, $fmt, $name, $size) = @_;
622
623 die "can't allocate space in pbs storage\n";
624}
625
626sub free_image {
627 my ($class, $storeid, $scfg, $volname, $isBase) = @_;
628
629 my ($vtype, $name, $vmid) = $class->parse_volname($volname);
630
631 run_client_cmd($scfg, $storeid, "forget", [ $name ], 1);
7ae13a34
FE
632
633 return;
271fe394
DM
634}
635
636
637sub list_images {
638 my ($class, $storeid, $scfg, $vmid, $vollist, $cache) = @_;
639
640 my $res = [];
641
642 return $res;
643}
644
878fe017
TL
645my sub snapshot_files_encrypted {
646 my ($files) = @_;
647 return 0 if !$files;
648
649 my $any;
650 my $all = 1;
651 for my $file (@$files) {
652 my $fn = $file->{filename};
653 next if $fn eq 'client.log.blob' || $fn eq 'index.json.blob';
654
655 my $crypt = $file->{'crypt-mode'};
656
657 $all = 0 if !$crypt || $crypt ne 'encrypt';
dfa374d3 658 $any ||= defined($crypt) && $crypt eq 'encrypt';
878fe017
TL
659 }
660 return $any && $all;
661}
662
271fe394
DM
663sub list_volumes {
664 my ($class, $storeid, $scfg, $vmid, $content_types) = @_;
665
666 my $res = [];
667
668 return $res if !grep { $_ eq 'backup' } @$content_types;
669
670 my $data = run_client_cmd($scfg, $storeid, "snapshots");
671
672 foreach my $item (@$data) {
673 my $btype = $item->{"backup-type"};
674 my $bid = $item->{"backup-id"};
545e127e 675 my $epoch = $item->{"backup-time"};
271fe394
DM
676 my $size = $item->{size} // 1;
677
678 next if !($btype eq 'vm' || $btype eq 'ct');
679 next if $bid !~ m/^\d+$/;
ddf7fdaa 680 next if defined($vmid) && $bid ne $vmid;
271fe394 681
8602fd56 682 my $volid = print_volid($storeid, $btype, $bid, $epoch);
271fe394 683
545e127e 684 my $info = {
c05b1a8c
TL
685 volid => $volid,
686 format => "pbs-$btype",
687 size => $size,
688 content => 'backup',
689 vmid => int($bid),
690 ctime => $epoch,
545e127e 691 };
271fe394 692
9778e5c2 693 $info->{verification} = $item->{verification} if defined($item->{verification});
6fef456c 694 $info->{notes} = $item->{comment} if defined($item->{comment});
bfba9626 695 $info->{protected} = 1 if $item->{protected};
878fe017
TL
696 if (defined($item->{fingerprint})) {
697 $info->{encrypted} = $item->{fingerprint};
698 } elsif (snapshot_files_encrypted($item->{files})) {
699 $info->{encrypted} = '1';
700 }
9778e5c2 701
271fe394
DM
702 push @$res, $info;
703 }
704
705 return $res;
706}
707
708sub status {
709 my ($class, $storeid, $scfg, $cache) = @_;
710
711 my $total = 0;
712 my $free = 0;
713 my $used = 0;
714 my $active = 0;
715
716 eval {
717 my $res = run_client_cmd($scfg, $storeid, "status");
718
719 $active = 1;
720 $total = $res->{total};
721 $used = $res->{used};
f155c912
TL
722 $free = $res->{avail};
723 };
271fe394
DM
724 if (my $err = $@) {
725 warn $err;
726 }
727
728 return ($total, $free, $used, $active);
729}
730
2f9eb6dc
TL
731# TODO: use a client with native rust/proxmox-backup bindings to profit from
732# API schema checks and types
733my sub pbs_api_connect {
734 my ($scfg, $password) = @_;
735
736 my $params = {};
737
738 my $user = $scfg->{username} // 'root@pam';
739
740 if (my $tokenid = PVE::AccessControl::pve_verify_tokenid($user, 1)) {
ab90c3b1 741 $params->{apitoken} = "PBSAPIToken=${tokenid}:${password}";
2f9eb6dc
TL
742 } else {
743 $params->{password} = $password;
744 $params->{username} = $user;
745 }
746
747 if (my $fp = $scfg->{fingerprint}) {
748 $params->{cached_fingerprints}->{uc($fp)} = 1;
749 }
750
751 my $conn = PVE::APIClient::LWP->new(
752 %$params,
753 host => $scfg->{server},
754 port => $scfg->{port} // 8007,
755 timeout => 7, # cope with a 401 (3s api delay) and high latency
756 cookie_name => 'PBSAuthCookie',
757 );
758
759 return $conn;
760}
761
8b62ac6a
TL
762# can also be used for not (yet) added storages, pass $scfg with
763# {
764# server
765# user
766# port (optional default to 8007)
767# fingerprint (optional for trusted certs)
768# }
769sub scan_datastores {
770 my ($scfg, $password) = @_;
771
772 my $conn = pbs_api_connect($scfg, $password);
773
774 my $response = eval { $conn->get('/api2/json/admin/datastore', {}) };
775 die "error fetching datastores - $@" if $@;
776
777 return $response;
778}
779
271fe394
DM
780sub activate_storage {
781 my ($class, $storeid, $scfg, $cache) = @_;
bb0a0f96 782
2cd10f58 783 my $password = pbs_get_password($scfg, $storeid);
bb0a0f96 784
2cd10f58
TL
785 my $datastores = eval { scan_datastores($scfg, $password) };
786 die "$storeid: $@" if $@;
787
788 my $datastore = $scfg->{datastore};
789
790 for my $ds (@$datastores) {
791 if ($ds->{store} eq $datastore) {
792 return 1;
793 }
794 }
795
ffc31266 796 die "$storeid: Cannot find datastore '$datastore', check permissions and existence!\n";
271fe394
DM
797}
798
799sub deactivate_storage {
800 my ($class, $storeid, $scfg, $cache) = @_;
801 return 1;
802}
803
804sub activate_volume {
805 my ($class, $storeid, $scfg, $volname, $snapname, $cache) = @_;
806
807 die "volume snapshot is not possible on pbs device" if $snapname;
808
809 return 1;
810}
811
812sub deactivate_volume {
813 my ($class, $storeid, $scfg, $volname, $snapname, $cache) = @_;
814
815 die "volume snapshot is not possible on pbs device" if $snapname;
816
817 return 1;
818}
819
f1de8281
FE
820# FIXME remove on the next APIAGE reset.
821# Deprecated, use get_volume_attribute instead.
45e93e6d
DC
822sub get_volume_notes {
823 my ($class, $scfg, $storeid, $volname, $timeout) = @_;
824
825 my (undef, $name, undef, undef, undef, undef, $format) = $class->parse_volname($volname);
826
827 my $data = run_client_cmd($scfg, $storeid, "snapshot", [ "notes", "show", $name ]);
828
829 return $data->{notes};
830}
831
f1de8281
FE
832# FIXME remove on the next APIAGE reset.
833# Deprecated, use update_volume_attribute instead.
45e93e6d
DC
834sub update_volume_notes {
835 my ($class, $scfg, $storeid, $volname, $notes, $timeout) = @_;
836
837 my (undef, $name, undef, undef, undef, undef, $format) = $class->parse_volname($volname);
838
839 run_client_cmd($scfg, $storeid, "snapshot", [ "notes", "update", $name, $notes ], 1);
840
841 return undef;
842}
843
f1de8281
FE
844sub get_volume_attribute {
845 my ($class, $scfg, $storeid, $volname, $attribute) = @_;
846
847 if ($attribute eq 'notes') {
848 return $class->get_volume_notes($scfg, $storeid, $volname);
849 }
850
bfba9626
FE
851 if ($attribute eq 'protected') {
852 my $param = $class->api_param_from_volname($volname);
853
854 my $password = pbs_get_password($scfg, $storeid);
855 my $conn = pbs_api_connect($scfg, $password);
856 my $datastore = $scfg->{datastore};
857
858 my $res = eval { $conn->get("/api2/json/admin/datastore/$datastore/$attribute", $param); };
859 if (my $err = $@) {
860 return if $err->{code} == 404; # not supported
861 die $err;
862 }
863 return $res;
864 }
865
f1de8281
FE
866 return;
867}
868
869sub update_volume_attribute {
870 my ($class, $scfg, $storeid, $volname, $attribute, $value) = @_;
871
872 if ($attribute eq 'notes') {
873 return $class->update_volume_notes($scfg, $storeid, $volname, $value);
874 }
875
bfba9626
FE
876 if ($attribute eq 'protected') {
877 my $param = $class->api_param_from_volname($volname);
878 $param->{$attribute} = $value;
879
880 my $password = pbs_get_password($scfg, $storeid);
881 my $conn = pbs_api_connect($scfg, $password);
882 my $datastore = $scfg->{datastore};
883
904f06a8
FE
884 eval { $conn->put("/api2/json/admin/datastore/$datastore/$attribute", $param); };
885 if (my $err = $@) {
886 die "Server is not recent enough to support feature '$attribute'\n"
887 if $err->{code} == 404;
888 die $err;
889 }
bfba9626
FE
890 return;
891 }
892
f1de8281
FE
893 die "attribute '$attribute' is not supported for storage type '$scfg->{type}'\n";
894}
895
271fe394
DM
896sub volume_size_info {
897 my ($class, $scfg, $storeid, $volname, $timeout) = @_;
898
899 my ($vtype, $name, undef, undef, undef, undef, $format) = $class->parse_volname($volname);
900
901 my $data = run_client_cmd($scfg, $storeid, "files", [ $name ]);
902
903 my $size = 0;
904 foreach my $info (@$data) {
d4e00f2b 905 if ($info->{size} && $info->{size} =~ /^(\d+)$/) { # untaints
ac598d85
SI
906 $size += $1;
907 }
271fe394
DM
908 }
909
910 my $used = $size;
911
912 return wantarray ? ($size, $format, $used, undef) : $size;
913}
914
915sub volume_resize {
916 my ($class, $scfg, $storeid, $volname, $size, $running) = @_;
917 die "volume resize is not possible on pbs device";
918}
919
920sub volume_snapshot {
921 my ($class, $scfg, $storeid, $volname, $snap) = @_;
922 die "volume snapshot is not possible on pbs device";
923}
924
925sub volume_snapshot_rollback {
926 my ($class, $scfg, $storeid, $volname, $snap) = @_;
927 die "volume snapshot rollback is not possible on pbs device";
928}
929
930sub volume_snapshot_delete {
931 my ($class, $scfg, $storeid, $volname, $snap) = @_;
932 die "volume snapshot delete is not possible on pbs device";
933}
934
935sub volume_has_feature {
936 my ($class, $scfg, $feature, $storeid, $volname, $snapname, $running) = @_;
937
938 return undef;
939}
940
9411;