]> git.proxmox.com Git - pve-storage.git/blob - PVE/Storage/PBSPlugin.pm
06ebfa76f6b6adec1dfe45731368a3d5fab86254
[pve-storage.git] / PVE / Storage / PBSPlugin.pm
1 package PVE::Storage::PBSPlugin;
2
3 # Plugin to access Proxmox Backup Server
4
5 use strict;
6 use warnings;
7
8 use Fcntl qw(F_GETFD F_SETFD FD_CLOEXEC);
9 use IO::File;
10 use JSON;
11 use MIME::Base64 qw(decode_base64);
12 use POSIX qw(mktime strftime ENOENT);
13 use POSIX::strptime;
14
15 use PVE::APIClient::LWP;
16 use PVE::JSONSchema qw(get_standard_option);
17 use PVE::Network;
18 use PVE::PBSClient;
19 use PVE::Storage::Plugin;
20 use PVE::Tools qw(run_command file_read_firstline trim dir_glob_regex dir_glob_foreach $IPV6RE);
21
22 use base qw(PVE::Storage::Plugin);
23
24 # Configuration
25
26 sub type {
27 return 'pbs';
28 }
29
30 sub plugindata {
31 return {
32 content => [ {backup => 1, none => 1}, { backup => 1 }],
33 };
34 }
35
36 sub properties {
37 return {
38 datastore => {
39 description => "Proxmox Backup Server datastore name.",
40 type => 'string',
41 },
42 # openssl s_client -connect <host>:8007 2>&1 |openssl x509 -fingerprint -sha256
43 fingerprint => get_standard_option('fingerprint-sha256'),
44 'encryption-key' => {
45 description => "Encryption key. Use 'autogen' to generate one automatically without passphrase.",
46 type => 'string',
47 },
48 'master-pubkey' => {
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.",
50 type => 'string',
51 },
52 port => {
53 description => "For non default port.",
54 type => 'integer',
55 minimum => 1,
56 maximum => 65535,
57 default => 8007,
58 },
59 };
60 }
61
62 sub options {
63 return {
64 server => { fixed => 1 },
65 datastore => { fixed => 1 },
66 port => { optional => 1 },
67 nodes => { optional => 1},
68 disable => { optional => 1},
69 content => { optional => 1},
70 username => { optional => 1 },
71 password => { optional => 1 },
72 'encryption-key' => { optional => 1 },
73 'master-pubkey' => { optional => 1 },
74 maxfiles => { optional => 1 },
75 'prune-backups' => { optional => 1 },
76 fingerprint => { optional => 1 },
77 };
78 }
79
80 # Helpers
81
82 sub pbs_password_file_name {
83 my ($scfg, $storeid) = @_;
84
85 return "/etc/pve/priv/storage/${storeid}.pw";
86 }
87
88 sub pbs_set_password {
89 my ($scfg, $storeid, $password) = @_;
90
91 my $pwfile = pbs_password_file_name($scfg, $storeid);
92 mkdir "/etc/pve/priv/storage";
93
94 PVE::Tools::file_set_contents($pwfile, "$password\n");
95 }
96
97 sub pbs_delete_password {
98 my ($scfg, $storeid) = @_;
99
100 my $pwfile = pbs_password_file_name($scfg, $storeid);
101
102 unlink $pwfile;
103 }
104
105 sub 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
113 sub pbs_encryption_key_file_name {
114 my ($scfg, $storeid) = @_;
115
116 return "/etc/pve/priv/storage/${storeid}.enc";
117 }
118
119 sub 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
128 sub pbs_delete_encryption_key {
129 my ($scfg, $storeid) = @_;
130
131 my $pwfile = pbs_encryption_key_file_name($scfg, $storeid);
132
133 if (!unlink $pwfile) {
134 return if $! == ENOENT;
135 die "failed to delete encryption key! $!\n";
136 }
137 delete $scfg->{'encryption-key'};
138 }
139
140 sub 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.
149 sub 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
163 sub pbs_master_pubkey_file_name {
164 my ($scfg, $storeid) = @_;
165
166 return "/etc/pve/priv/storage/${storeid}.master.pem";
167 }
168
169 sub 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
178 sub 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
190 sub 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.
199 sub 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
213 sub 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 }
221
222 # essentially the inverse of print_volid
223 sub 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
252 my $USE_CRYPT_PARAMS = {
253 backup => 1,
254 restore => 1,
255 'upload-log' => 1,
256 };
257
258 my $USE_MASTER_KEY = {
259 backup => 1,
260 };
261
262 my sub do_raw_client_cmd {
263 my ($scfg, $storeid, $client_cmd, $param, %opts) = @_;
264
265 my $use_crypto = $USE_CRYPT_PARAMS->{$client_cmd};
266 my $use_master = $USE_MASTER_KEY->{$client_cmd};
267
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
272 my $repo = PVE::PBSClient::get_repository($scfg);
273
274 my $userns_cmd = delete $opts{userns_cmd};
275
276 my $cmd = [];
277
278 push @$cmd, @$userns_cmd if defined($userns_cmd);
279
280 push @$cmd, $client_exe, $client_cmd;
281
282 # This must live in the top scope to not get closed before the `run_command`
283 my ($keyfd, $master_fd);
284 if ($use_crypto) {
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);
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 }
298 } else {
299 push @$cmd, '--crypt-mode=none';
300 }
301 }
302
303 push @$cmd, @$param if defined($param);
304
305 push @$cmd, "--repository", $repo;
306
307 local $ENV{PBS_PASSWORD} = pbs_get_password($scfg, $storeid);
308
309 local $ENV{PBS_FINGERPRINT} = $scfg->{fingerprint};
310
311 # no ascii-art on task logs
312 local $ENV{PROXMOX_OUTPUT_NO_BORDER} = 1;
313 local $ENV{PROXMOX_OUTPUT_NO_HEADER} = 1;
314
315 if (my $logfunc = $opts{logfunc}) {
316 $logfunc->("run: " . join(' ', @$cmd));
317 }
318
319 run_command($cmd, %opts);
320 }
321
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
329 sub run_raw_client_cmd {
330 my ($scfg, $storeid, $client_cmd, $param, %opts) = @_;
331 return do_raw_client_cmd($scfg, $storeid, $client_cmd, $param, %opts);
332 }
333
334 sub run_client_cmd {
335 my ($scfg, $storeid, $client_cmd, $param, $no_output) = @_;
336
337 my $json_str = '';
338 my $outfunc = sub { $json_str .= "$_[0]\n" };
339
340 $param = [] if !defined($param);
341 $param = [ $param ] if !ref($param);
342
343 $param = [@$param, '--output-format=json'] if !$no_output;
344
345 do_raw_client_cmd($scfg, $storeid, $client_cmd, $param,
346 outfunc => $outfunc, errmsg => 'proxmox-backup-client failed');
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
357 sub 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 = '';
363 my $outfunc = sub { $config .= "$_[0]\n" };
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
374 do_raw_client_cmd($scfg, $storeid, 'restore', [ $name, $config_name, '-' ],
375 outfunc => $outfunc, errmsg => 'proxmox-backup-client failed');
376
377 return $config;
378 }
379
380 sub 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;
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 };
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
437 my $mark = $backup->{keep} ? 'keep' : 'remove';
438 $mark = 'protected' if $backup->{protected};
439
440 push @{$prune_list}, {
441 ctime => $ctime,
442 mark => $mark,
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
459 my $autogen_encryption_key = sub {
460 my ($scfg, $storeid) = @_;
461 my $encfile = pbs_encryption_key_file_name($scfg, $storeid);
462 if (-f $encfile) {
463 rename $encfile, "$encfile.old";
464 }
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);
468 };
469
470 sub on_add_hook {
471 my ($class, $storeid, $scfg, %param) = @_;
472
473 my $res = {};
474
475 if (defined(my $password = $param{password})) {
476 pbs_set_password($scfg, $storeid, $password);
477 } else {
478 pbs_delete_password($scfg, $storeid);
479 }
480
481 if (defined(my $encryption_key = $param{'encryption-key'})) {
482 my $decoded_key;
483 if ($encryption_key eq 'autogen') {
484 $res->{'encryption-key'} = $autogen_encryption_key->($scfg, $storeid);
485 $decoded_key = decode_json($res->{'encryption-key'});
486 } else {
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 }
491 pbs_set_encryption_key($scfg, $storeid, $encryption_key);
492 $res->{'encryption-key'} = $encryption_key;
493 }
494 $scfg->{'encryption-key'} = $decoded_key->{fingerprint} || 1;
495 } else {
496 pbs_delete_encryption_key($scfg, $storeid);
497 }
498
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
510 return $res;
511 }
512
513 sub on_update_hook {
514 my ($class, $storeid, $scfg, %param) = @_;
515
516 my $res = {};
517
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 }
525
526 if (exists($param{'encryption-key'})) {
527 if (defined(my $encryption_key = delete($param{'encryption-key'}))) {
528 my $decoded_key;
529 if ($encryption_key eq 'autogen') {
530 $res->{'encryption-key'} = $autogen_encryption_key->($scfg, $storeid);
531 $decoded_key = decode_json($res->{'encryption-key'});
532 } else {
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 }
537 pbs_set_encryption_key($scfg, $storeid, $encryption_key);
538 $res->{'encryption-key'} = $encryption_key;
539 }
540 $scfg->{'encryption-key'} = $decoded_key->{fingerprint} || 1;
541 } else {
542 pbs_delete_encryption_key($scfg, $storeid);
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);
555 }
556 }
557
558 return $res;
559 }
560
561 sub on_delete_hook {
562 my ($class, $storeid, $scfg) = @_;
563
564 pbs_delete_password($scfg, $storeid);
565 pbs_delete_encryption_key($scfg, $storeid);
566 pbs_delete_master_pubkey($scfg, $storeid);
567
568 return;
569 }
570
571 sub 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
592 sub 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
600 my $repo = PVE::PBSClient::get_repository($scfg);
601
602 # artificial url - we currently do not use that anywhere
603 my $path = "pbs://$repo/$name";
604
605 return ($path, $vmid, $vtype);
606 }
607
608 sub create_base {
609 my ($class, $storeid, $scfg, $volname) = @_;
610
611 die "can't create base images in pbs storage\n";
612 }
613
614 sub clone_image {
615 my ($class, $scfg, $storeid, $volname, $vmid, $snap) = @_;
616
617 die "can't clone images in pbs storage\n";
618 }
619
620 sub alloc_image {
621 my ($class, $storeid, $scfg, $vmid, $fmt, $name, $size) = @_;
622
623 die "can't allocate space in pbs storage\n";
624 }
625
626 sub 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);
632
633 return;
634 }
635
636
637 sub list_images {
638 my ($class, $storeid, $scfg, $vmid, $vollist, $cache) = @_;
639
640 my $res = [];
641
642 return $res;
643 }
644
645 my 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';
658 $any ||= defined($crypt) && $crypt eq 'encrypt';
659 }
660 return $any && $all;
661 }
662
663 sub 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"};
675 my $epoch = $item->{"backup-time"};
676 my $size = $item->{size} // 1;
677
678 next if !($btype eq 'vm' || $btype eq 'ct');
679 next if $bid !~ m/^\d+$/;
680 next if defined($vmid) && $bid ne $vmid;
681
682 my $volid = print_volid($storeid, $btype, $bid, $epoch);
683
684 my $info = {
685 volid => $volid,
686 format => "pbs-$btype",
687 size => $size,
688 content => 'backup',
689 vmid => int($bid),
690 ctime => $epoch,
691 };
692
693 $info->{verification} = $item->{verification} if defined($item->{verification});
694 $info->{notes} = $item->{comment} if defined($item->{comment});
695 $info->{protected} = 1 if $item->{protected};
696 if (defined($item->{fingerprint})) {
697 $info->{encrypted} = $item->{fingerprint};
698 } elsif (snapshot_files_encrypted($item->{files})) {
699 $info->{encrypted} = '1';
700 }
701
702 push @$res, $info;
703 }
704
705 return $res;
706 }
707
708 sub 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};
722 $free = $res->{avail};
723 };
724 if (my $err = $@) {
725 warn $err;
726 }
727
728 return ($total, $free, $used, $active);
729 }
730
731 # TODO: use a client with native rust/proxmox-backup bindings to profit from
732 # API schema checks and types
733 my 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)) {
741 $params->{apitoken} = "PBSAPIToken=${tokenid}:${password}";
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
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 # }
769 sub 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
780 sub activate_storage {
781 my ($class, $storeid, $scfg, $cache) = @_;
782
783 my $password = pbs_get_password($scfg, $storeid);
784
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
796 die "$storeid: Cannot find datastore '$datastore', check permissions and existence!\n";
797 }
798
799 sub deactivate_storage {
800 my ($class, $storeid, $scfg, $cache) = @_;
801 return 1;
802 }
803
804 sub 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
812 sub 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
820 # FIXME remove on the next APIAGE reset.
821 # Deprecated, use get_volume_attribute instead.
822 sub 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
832 # FIXME remove on the next APIAGE reset.
833 # Deprecated, use update_volume_attribute instead.
834 sub 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
844 sub 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
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
866 return;
867 }
868
869 sub 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
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
884 $conn->put("/api2/json/admin/datastore/$datastore/$attribute", $param);
885 return;
886 }
887
888 die "attribute '$attribute' is not supported for storage type '$scfg->{type}'\n";
889 }
890
891 sub volume_size_info {
892 my ($class, $scfg, $storeid, $volname, $timeout) = @_;
893
894 my ($vtype, $name, undef, undef, undef, undef, $format) = $class->parse_volname($volname);
895
896 my $data = run_client_cmd($scfg, $storeid, "files", [ $name ]);
897
898 my $size = 0;
899 foreach my $info (@$data) {
900 if ($info->{size} && $info->{size} =~ /^(\d+)$/) { # untaints
901 $size += $1;
902 }
903 }
904
905 my $used = $size;
906
907 return wantarray ? ($size, $format, $used, undef) : $size;
908 }
909
910 sub volume_resize {
911 my ($class, $scfg, $storeid, $volname, $size, $running) = @_;
912 die "volume resize is not possible on pbs device";
913 }
914
915 sub volume_snapshot {
916 my ($class, $scfg, $storeid, $volname, $snap) = @_;
917 die "volume snapshot is not possible on pbs device";
918 }
919
920 sub volume_snapshot_rollback {
921 my ($class, $scfg, $storeid, $volname, $snap) = @_;
922 die "volume snapshot rollback is not possible on pbs device";
923 }
924
925 sub volume_snapshot_delete {
926 my ($class, $scfg, $storeid, $volname, $snap) = @_;
927 die "volume snapshot delete is not possible on pbs device";
928 }
929
930 sub volume_has_feature {
931 my ($class, $scfg, $feature, $storeid, $volname, $snapname, $running) = @_;
932
933 return undef;
934 }
935
936 1;