]> git.proxmox.com Git - pve-storage.git/blob - PVE/Storage/PBSPlugin.pm
pbs: allow setting up a master key
[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(strftime ENOENT);
13
14 use PVE::APIClient::LWP;
15 use PVE::JSONSchema qw(get_standard_option);
16 use PVE::Network;
17 use PVE::PBSClient;
18 use PVE::Storage::Plugin;
19 use PVE::Tools qw(run_command file_read_firstline trim dir_glob_regex dir_glob_foreach $IPV6RE);
20
21 use base qw(PVE::Storage::Plugin);
22
23 # Configuration
24
25 sub type {
26 return 'pbs';
27 }
28
29 sub plugindata {
30 return {
31 content => [ {backup => 1, none => 1}, { backup => 1 }],
32 };
33 }
34
35 sub properties {
36 return {
37 datastore => {
38 description => "Proxmox Backup Server datastore name.",
39 type => 'string',
40 },
41 # openssl s_client -connect <host>:8007 2>&1 |openssl x509 -fingerprint -sha256
42 fingerprint => get_standard_option('fingerprint-sha256'),
43 'encryption-key' => {
44 description => "Encryption key. Use 'autogen' to generate one automatically without passphrase.",
45 type => 'string',
46 },
47 'master-pubkey' => {
48 description => "Base64-encoded, PEM-formatted public RSA key. Used tp encrypt a copy of the encryption-key which will be added to each encrypted backup.",
49 type => 'string',
50 },
51 port => {
52 description => "For non default port.",
53 type => 'integer',
54 minimum => 1,
55 maximum => 65535,
56 default => 8007,
57 },
58 };
59 }
60
61 sub options {
62 return {
63 server => { fixed => 1 },
64 datastore => { fixed => 1 },
65 port => { optional => 1 },
66 nodes => { optional => 1},
67 disable => { optional => 1},
68 content => { optional => 1},
69 username => { optional => 1 },
70 password => { optional => 1 },
71 'encryption-key' => { optional => 1 },
72 'master-pubkey' => { optional => 1 },
73 maxfiles => { optional => 1 },
74 'prune-backups' => { optional => 1 },
75 fingerprint => { optional => 1 },
76 };
77 }
78
79 # Helpers
80
81 sub pbs_password_file_name {
82 my ($scfg, $storeid) = @_;
83
84 return "/etc/pve/priv/storage/${storeid}.pw";
85 }
86
87 sub pbs_set_password {
88 my ($scfg, $storeid, $password) = @_;
89
90 my $pwfile = pbs_password_file_name($scfg, $storeid);
91 mkdir "/etc/pve/priv/storage";
92
93 PVE::Tools::file_set_contents($pwfile, "$password\n");
94 }
95
96 sub pbs_delete_password {
97 my ($scfg, $storeid) = @_;
98
99 my $pwfile = pbs_password_file_name($scfg, $storeid);
100
101 unlink $pwfile;
102 }
103
104 sub 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
112 sub pbs_encryption_key_file_name {
113 my ($scfg, $storeid) = @_;
114
115 return "/etc/pve/priv/storage/${storeid}.enc";
116 }
117
118 sub 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
127 sub pbs_delete_encryption_key {
128 my ($scfg, $storeid) = @_;
129
130 my $pwfile = pbs_encryption_key_file_name($scfg, $storeid);
131
132 if (!unlink $pwfile) {
133 return if $! == ENOENT;
134 die "failed to delete encryption key! $!\n";
135 }
136 delete $scfg->{'encryption-key'};
137 }
138
139 sub 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.
148 sub 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
162 sub pbs_master_pubkey_file_name {
163 my ($scfg, $storeid) = @_;
164
165 return "/etc/pve/priv/storage/${storeid}.master.pem";
166 }
167
168 sub 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
177 sub 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
189 sub 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.
198 sub 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
212 sub 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 }
220
221 my $USE_CRYPT_PARAMS = {
222 backup => 1,
223 restore => 1,
224 'upload-log' => 1,
225 };
226
227 my $USE_MASTER_KEY = {
228 backup => 1,
229 };
230
231 my sub do_raw_client_cmd {
232 my ($scfg, $storeid, $client_cmd, $param, %opts) = @_;
233
234 my $use_crypto = $USE_CRYPT_PARAMS->{$client_cmd};
235 my $use_master = $USE_MASTER_KEY->{$client_cmd};
236
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
241 my $repo = PVE::PBSClient::get_repository($scfg);
242
243 my $userns_cmd = delete $opts{userns_cmd};
244
245 my $cmd = [];
246
247 push @$cmd, @$userns_cmd if defined($userns_cmd);
248
249 push @$cmd, $client_exe, $client_cmd;
250
251 # This must live in the top scope to not get closed before the `run_command`
252 my ($keyfd, $master_fd);
253 if ($use_crypto) {
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);
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 }
267 } else {
268 push @$cmd, '--crypt-mode=none';
269 }
270 }
271
272 push @$cmd, @$param if defined($param);
273
274 push @$cmd, "--repository", $repo;
275
276 local $ENV{PBS_PASSWORD} = pbs_get_password($scfg, $storeid);
277
278 local $ENV{PBS_FINGERPRINT} = $scfg->{fingerprint};
279
280 # no ascii-art on task logs
281 local $ENV{PROXMOX_OUTPUT_NO_BORDER} = 1;
282 local $ENV{PROXMOX_OUTPUT_NO_HEADER} = 1;
283
284 if (my $logfunc = $opts{logfunc}) {
285 $logfunc->("run: " . join(' ', @$cmd));
286 }
287
288 run_command($cmd, %opts);
289 }
290
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
298 sub run_raw_client_cmd {
299 my ($scfg, $storeid, $client_cmd, $param, %opts) = @_;
300 return do_raw_client_cmd($scfg, $storeid, $client_cmd, $param, %opts);
301 }
302
303 sub run_client_cmd {
304 my ($scfg, $storeid, $client_cmd, $param, $no_output) = @_;
305
306 my $json_str = '';
307 my $outfunc = sub { $json_str .= "$_[0]\n" };
308
309 $param = [] if !defined($param);
310 $param = [ $param ] if !ref($param);
311
312 $param = [@$param, '--output-format=json'] if !$no_output;
313
314 do_raw_client_cmd($scfg, $storeid, $client_cmd, $param,
315 outfunc => $outfunc, errmsg => 'proxmox-backup-client failed');
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
326 sub 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 = '';
332 my $outfunc = sub { $config .= "$_[0]\n" };
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
343 do_raw_client_cmd($scfg, $storeid, 'restore', [ $name, $config_name, '-' ],
344 outfunc => $outfunc, errmsg => 'proxmox-backup-client failed');
345
346 return $config;
347 }
348
349 sub 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;
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 };
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
425 my $autogen_encryption_key = sub {
426 my ($scfg, $storeid) = @_;
427 my $encfile = pbs_encryption_key_file_name($scfg, $storeid);
428 if (-f $encfile) {
429 rename $encfile, "$encfile.old";
430 }
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);
434 };
435
436 sub on_add_hook {
437 my ($class, $storeid, $scfg, %param) = @_;
438
439 my $res = {};
440
441 if (defined(my $password = $param{password})) {
442 pbs_set_password($scfg, $storeid, $password);
443 } else {
444 pbs_delete_password($scfg, $storeid);
445 }
446
447 if (defined(my $encryption_key = $param{'encryption-key'})) {
448 my $decoded_key;
449 if ($encryption_key eq 'autogen') {
450 $res->{'encryption-key'} = $autogen_encryption_key->($scfg, $storeid);
451 $decoded_key = decode_json($res->{'encryption-key'});
452 } else {
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 }
457 pbs_set_encryption_key($scfg, $storeid, $encryption_key);
458 $res->{'encryption-key'} = $encryption_key;
459 }
460 $scfg->{'encryption-key'} = $decoded_key->{fingerprint} || 1;
461 } else {
462 pbs_delete_encryption_key($scfg, $storeid);
463 }
464
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
476 return $res;
477 }
478
479 sub on_update_hook {
480 my ($class, $storeid, $scfg, %param) = @_;
481
482 my $res = {};
483
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 }
491
492 if (exists($param{'encryption-key'})) {
493 if (defined(my $encryption_key = delete($param{'encryption-key'}))) {
494 my $decoded_key;
495 if ($encryption_key eq 'autogen') {
496 $res->{'encryption-key'} = $autogen_encryption_key->($scfg, $storeid);
497 $decoded_key = decode_json($res->{'encryption-key'});
498 } else {
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 }
503 pbs_set_encryption_key($scfg, $storeid, $encryption_key);
504 $res->{'encryption-key'} = $encryption_key;
505 }
506 $scfg->{'encryption-key'} = $decoded_key->{fingerprint} || 1;
507 } else {
508 pbs_delete_encryption_key($scfg, $storeid);
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);
521 }
522 }
523
524 return $res;
525 }
526
527 sub on_delete_hook {
528 my ($class, $storeid, $scfg) = @_;
529
530 pbs_delete_password($scfg, $storeid);
531 pbs_delete_encryption_key($scfg, $storeid);
532 pbs_delete_master_pubkey($scfg, $storeid);
533
534 return;
535 }
536
537 sub 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
558 sub 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
566 my $repo = PVE::PBSClient::get_repository($scfg);
567
568 # artifical url - we currently do not use that anywhere
569 my $path = "pbs://$repo/$name";
570
571 return ($path, $vmid, $vtype);
572 }
573
574 sub create_base {
575 my ($class, $storeid, $scfg, $volname) = @_;
576
577 die "can't create base images in pbs storage\n";
578 }
579
580 sub clone_image {
581 my ($class, $scfg, $storeid, $volname, $vmid, $snap) = @_;
582
583 die "can't clone images in pbs storage\n";
584 }
585
586 sub alloc_image {
587 my ($class, $storeid, $scfg, $vmid, $fmt, $name, $size) = @_;
588
589 die "can't allocate space in pbs storage\n";
590 }
591
592 sub 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);
598 }
599
600
601 sub list_images {
602 my ($class, $storeid, $scfg, $vmid, $vollist, $cache) = @_;
603
604 my $res = [];
605
606 return $res;
607 }
608
609 my sub snapshot_files_encrypted {
610 my ($files) = @_;
611 return 0 if !$files;
612
613 my $any;
614 my $all = 1;
615 for my $file (@$files) {
616 my $fn = $file->{filename};
617 next if $fn eq 'client.log.blob' || $fn eq 'index.json.blob';
618
619 my $crypt = $file->{'crypt-mode'};
620
621 $all = 0 if !$crypt || $crypt ne 'encrypt';
622 $any ||= defined($crypt) && $crypt eq 'encrypt';
623 }
624 return $any && $all;
625 }
626
627 sub list_volumes {
628 my ($class, $storeid, $scfg, $vmid, $content_types) = @_;
629
630 my $res = [];
631
632 return $res if !grep { $_ eq 'backup' } @$content_types;
633
634 my $data = run_client_cmd($scfg, $storeid, "snapshots");
635
636 foreach my $item (@$data) {
637 my $btype = $item->{"backup-type"};
638 my $bid = $item->{"backup-id"};
639 my $epoch = $item->{"backup-time"};
640 my $size = $item->{size} // 1;
641
642 next if !($btype eq 'vm' || $btype eq 'ct');
643 next if $bid !~ m/^\d+$/;
644 next if defined($vmid) && $bid ne $vmid;
645
646 my $volid = print_volid($storeid, $btype, $bid, $epoch);
647
648 my $info = {
649 volid => $volid,
650 format => "pbs-$btype",
651 size => $size,
652 content => 'backup',
653 vmid => int($bid),
654 ctime => $epoch,
655 };
656
657 $info->{verification} = $item->{verification} if defined($item->{verification});
658 $info->{notes} = $item->{comment} if defined($item->{comment});
659 if (defined($item->{fingerprint})) {
660 $info->{encrypted} = $item->{fingerprint};
661 } elsif (snapshot_files_encrypted($item->{files})) {
662 $info->{encrypted} = '1';
663 }
664
665 push @$res, $info;
666 }
667
668 return $res;
669 }
670
671 sub status {
672 my ($class, $storeid, $scfg, $cache) = @_;
673
674 my $total = 0;
675 my $free = 0;
676 my $used = 0;
677 my $active = 0;
678
679 eval {
680 my $res = run_client_cmd($scfg, $storeid, "status");
681
682 $active = 1;
683 $total = $res->{total};
684 $used = $res->{used};
685 $free = $res->{avail};
686 };
687 if (my $err = $@) {
688 warn $err;
689 }
690
691 return ($total, $free, $used, $active);
692 }
693
694 # TODO: use a client with native rust/proxmox-backup bindings to profit from
695 # API schema checks and types
696 my sub pbs_api_connect {
697 my ($scfg, $password) = @_;
698
699 my $params = {};
700
701 my $user = $scfg->{username} // 'root@pam';
702
703 if (my $tokenid = PVE::AccessControl::pve_verify_tokenid($user, 1)) {
704 $params->{apitoken} = "PBSAPIToken=${tokenid}:${password}";
705 } else {
706 $params->{password} = $password;
707 $params->{username} = $user;
708 }
709
710 if (my $fp = $scfg->{fingerprint}) {
711 $params->{cached_fingerprints}->{uc($fp)} = 1;
712 }
713
714 my $conn = PVE::APIClient::LWP->new(
715 %$params,
716 host => $scfg->{server},
717 port => $scfg->{port} // 8007,
718 timeout => 7, # cope with a 401 (3s api delay) and high latency
719 cookie_name => 'PBSAuthCookie',
720 );
721
722 return $conn;
723 }
724
725 # can also be used for not (yet) added storages, pass $scfg with
726 # {
727 # server
728 # user
729 # port (optional default to 8007)
730 # fingerprint (optional for trusted certs)
731 # }
732 sub scan_datastores {
733 my ($scfg, $password) = @_;
734
735 my $conn = pbs_api_connect($scfg, $password);
736
737 my $response = eval { $conn->get('/api2/json/admin/datastore', {}) };
738 die "error fetching datastores - $@" if $@;
739
740 return $response;
741 }
742
743 sub activate_storage {
744 my ($class, $storeid, $scfg, $cache) = @_;
745
746 my $password = pbs_get_password($scfg, $storeid);
747
748 my $datastores = eval { scan_datastores($scfg, $password) };
749 die "$storeid: $@" if $@;
750
751 my $datastore = $scfg->{datastore};
752
753 for my $ds (@$datastores) {
754 if ($ds->{store} eq $datastore) {
755 return 1;
756 }
757 }
758
759 die "$storeid: Cannot find datastore '$datastore', check permissions and existance!\n";
760 }
761
762 sub deactivate_storage {
763 my ($class, $storeid, $scfg, $cache) = @_;
764 return 1;
765 }
766
767 sub activate_volume {
768 my ($class, $storeid, $scfg, $volname, $snapname, $cache) = @_;
769
770 die "volume snapshot is not possible on pbs device" if $snapname;
771
772 return 1;
773 }
774
775 sub deactivate_volume {
776 my ($class, $storeid, $scfg, $volname, $snapname, $cache) = @_;
777
778 die "volume snapshot is not possible on pbs device" if $snapname;
779
780 return 1;
781 }
782
783 sub get_volume_notes {
784 my ($class, $scfg, $storeid, $volname, $timeout) = @_;
785
786 my (undef, $name, undef, undef, undef, undef, $format) = $class->parse_volname($volname);
787
788 my $data = run_client_cmd($scfg, $storeid, "snapshot", [ "notes", "show", $name ]);
789
790 return $data->{notes};
791 }
792
793 sub update_volume_notes {
794 my ($class, $scfg, $storeid, $volname, $notes, $timeout) = @_;
795
796 my (undef, $name, undef, undef, undef, undef, $format) = $class->parse_volname($volname);
797
798 run_client_cmd($scfg, $storeid, "snapshot", [ "notes", "update", $name, $notes ], 1);
799
800 return undef;
801 }
802
803 sub volume_size_info {
804 my ($class, $scfg, $storeid, $volname, $timeout) = @_;
805
806 my ($vtype, $name, undef, undef, undef, undef, $format) = $class->parse_volname($volname);
807
808 my $data = run_client_cmd($scfg, $storeid, "files", [ $name ]);
809
810 my $size = 0;
811 foreach my $info (@$data) {
812 $size += $info->{size} if $info->{size};
813 }
814
815 my $used = $size;
816
817 return wantarray ? ($size, $format, $used, undef) : $size;
818 }
819
820 sub volume_resize {
821 my ($class, $scfg, $storeid, $volname, $size, $running) = @_;
822 die "volume resize is not possible on pbs device";
823 }
824
825 sub volume_snapshot {
826 my ($class, $scfg, $storeid, $volname, $snap) = @_;
827 die "volume snapshot is not possible on pbs device";
828 }
829
830 sub volume_snapshot_rollback {
831 my ($class, $scfg, $storeid, $volname, $snap) = @_;
832 die "volume snapshot rollback is not possible on pbs device";
833 }
834
835 sub volume_snapshot_delete {
836 my ($class, $scfg, $storeid, $volname, $snap) = @_;
837 die "volume snapshot delete is not possible on pbs device";
838 }
839
840 sub volume_has_feature {
841 my ($class, $scfg, $feature, $storeid, $volname, $snapname, $running) = @_;
842
843 return undef;
844 }
845
846 1;