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