1 package PVE
::Storage
::PBSPlugin
;
3 # Plugin to access Proxmox Backup Server
8 use Fcntl
qw(F_GETFD F_SETFD FD_CLOEXEC);
11 use POSIX
qw(strftime ENOENT);
13 use PVE
::APIClient
::LWP
;
14 use PVE
::JSONSchema
qw(get_standard_option);
16 use PVE
::Storage
::Plugin
;
17 use PVE
::Tools
qw(run_command file_read_firstline trim dir_glob_regex dir_glob_foreach $IPV6RE);
19 use base
qw(PVE::Storage::Plugin);
29 content
=> [ {backup
=> 1, none
=> 1}, { backup
=> 1 }],
36 description
=> "Proxmox Backup Server datastore name.",
39 # openssl s_client -connect <host>:8007 2>&1 |openssl x509 -fingerprint -sha256
40 fingerprint
=> get_standard_option
('fingerprint-sha256'),
42 description
=> "Encryption key. Use 'autogen' to generate one automatically without passphrase.",
46 description
=> "For non default port.",
57 server
=> { fixed
=> 1 },
58 datastore
=> { fixed
=> 1 },
59 port
=> { optional
=> 1 },
60 nodes
=> { optional
=> 1},
61 disable
=> { optional
=> 1},
62 content
=> { optional
=> 1},
63 username
=> { optional
=> 1 },
64 password
=> { optional
=> 1 },
65 'encryption-key' => { optional
=> 1 },
66 maxfiles
=> { optional
=> 1 },
67 'prune-backups' => { optional
=> 1 },
68 fingerprint
=> { optional
=> 1 },
74 sub pbs_password_file_name
{
75 my ($scfg, $storeid) = @_;
77 return "/etc/pve/priv/storage/${storeid}.pw";
80 sub pbs_set_password
{
81 my ($scfg, $storeid, $password) = @_;
83 my $pwfile = pbs_password_file_name
($scfg, $storeid);
84 mkdir "/etc/pve/priv/storage";
86 PVE
::Tools
::file_set_contents
($pwfile, "$password\n");
89 sub pbs_delete_password
{
90 my ($scfg, $storeid) = @_;
92 my $pwfile = pbs_password_file_name
($scfg, $storeid);
97 sub pbs_get_password
{
98 my ($scfg, $storeid) = @_;
100 my $pwfile = pbs_password_file_name
($scfg, $storeid);
102 return PVE
::Tools
::file_read_firstline
($pwfile);
105 sub pbs_encryption_key_file_name
{
106 my ($scfg, $storeid) = @_;
108 return "/etc/pve/priv/storage/${storeid}.enc";
111 sub pbs_set_encryption_key
{
112 my ($scfg, $storeid, $key) = @_;
114 my $pwfile = pbs_encryption_key_file_name
($scfg, $storeid);
115 mkdir "/etc/pve/priv/storage";
117 PVE
::Tools
::file_set_contents
($pwfile, "$key\n");
120 sub pbs_delete_encryption_key
{
121 my ($scfg, $storeid) = @_;
123 my $pwfile = pbs_encryption_key_file_name
($scfg, $storeid);
125 if (!unlink $pwfile) {
126 return if $! == ENOENT
;
127 die "failed to delete encryption key! $!\n";
129 delete $scfg->{'encryption-key'};
132 sub pbs_get_encryption_key
{
133 my ($scfg, $storeid) = @_;
135 my $pwfile = pbs_encryption_key_file_name
($scfg, $storeid);
137 return PVE
::Tools
::file_get_contents
($pwfile);
140 # Returns a file handle if there is an encryption key, or `undef` if there is not. Dies on error.
141 sub pbs_open_encryption_key
{
142 my ($scfg, $storeid) = @_;
144 my $encryption_key_file = pbs_encryption_key_file_name
($scfg, $storeid);
147 if (!open($keyfd, '<', $encryption_key_file)) {
148 return undef if $! == ENOENT
;
149 die "failed to open encryption key: $encryption_key_file: $!\n";
156 my ($storeid, $btype, $bid, $btime) = @_;
158 my $time_str = strftime
("%FT%TZ", gmtime($btime));
159 my $volname = "backup/${btype}/${bid}/${time_str}";
161 return "${storeid}:${volname}";
164 my sub get_server_with_port
{
167 my $server = $scfg->{server
};
168 $server = "[$server]" if $server =~ /^$IPV6RE$/;
170 if (my $port = $scfg->{port
}) {
171 $server .= ":$port" if $port != 8007;
176 my $USE_CRYPT_PARAMS = {
182 my sub do_raw_client_cmd
{
183 my ($scfg, $storeid, $client_cmd, $param, %opts) = @_;
185 my $use_crypto = $USE_CRYPT_PARAMS->{$client_cmd};
187 my $client_exe = '/usr/bin/proxmox-backup-client';
188 die "executable not found '$client_exe'! Proxmox backup client not installed?\n"
191 my $server = get_server_with_port
($scfg);
192 my $datastore = $scfg->{datastore
};
193 my $username = $scfg->{username
} // 'root@pam';
195 my $userns_cmd = delete $opts{userns_cmd
};
199 push @$cmd, @$userns_cmd if defined($userns_cmd);
201 push @$cmd, $client_exe, $client_cmd;
203 # This must live in the top scope to not get closed before the `run_command`
206 if (defined($keyfd = pbs_open_encryption_key
($scfg, $storeid))) {
207 my $flags = fcntl($keyfd, F_GETFD
, 0)
208 // die "failed to get file descriptor flags: $!\n";
209 fcntl($keyfd, F_SETFD
, $flags & ~FD_CLOEXEC
)
210 or die "failed to remove FD_CLOEXEC from encryption key file descriptor\n";
211 push @$cmd, '--crypt-mode=encrypt', '--keyfd='.fileno($keyfd);
213 push @$cmd, '--crypt-mode=none';
217 push @$cmd, @$param if defined($param);
219 push @$cmd, "--repository", "$username\@$server:$datastore";
221 local $ENV{PBS_PASSWORD
} = pbs_get_password
($scfg, $storeid);
223 local $ENV{PBS_FINGERPRINT
} = $scfg->{fingerprint
};
225 # no ascii-art on task logs
226 local $ENV{PROXMOX_OUTPUT_NO_BORDER
} = 1;
227 local $ENV{PROXMOX_OUTPUT_NO_HEADER
} = 1;
229 if (my $logfunc = $opts{logfunc
}) {
230 $logfunc->("run: " . join(' ', @$cmd));
233 run_command
($cmd, %opts);
236 # FIXME: External perl code should NOT have access to this.
238 # There should be separate functions to
243 sub run_raw_client_cmd
{
244 my ($scfg, $storeid, $client_cmd, $param, %opts) = @_;
245 return do_raw_client_cmd
($scfg, $storeid, $client_cmd, $param, %opts);
249 my ($scfg, $storeid, $client_cmd, $param, $no_output) = @_;
252 my $outfunc = sub { $json_str .= "$_[0]\n" };
254 $param = [] if !defined($param);
255 $param = [ $param ] if !ref($param);
257 $param = [@$param, '--output-format=json'] if !$no_output;
259 do_raw_client_cmd
($scfg, $storeid, $client_cmd, $param,
260 outfunc
=> $outfunc, errmsg
=> 'proxmox-backup-client failed');
262 return undef if $no_output;
264 my $res = decode_json
($json_str);
269 # Storage implementation
271 sub extract_vzdump_config
{
272 my ($class, $scfg, $volname, $storeid) = @_;
274 my ($vtype, $name, $vmid, undef, undef, undef, $format) = $class->parse_volname($volname);
277 my $outfunc = sub { $config .= "$_[0]\n" };
280 if ($format eq 'pbs-vm') {
281 $config_name = 'qemu-server.conf';
282 } elsif ($format eq 'pbs-ct') {
283 $config_name = 'pct.conf';
285 die "unable to extract configuration for backup format '$format'\n";
288 do_raw_client_cmd
($scfg, $storeid, 'restore', [ $name, $config_name, '-' ],
289 outfunc
=> $outfunc, errmsg
=> 'proxmox-backup-client failed');
295 my ($class, $scfg, $storeid, $keep, $vmid, $type, $dryrun, $logfunc) = @_;
297 $logfunc //= sub { print "$_[1]\n" };
299 my $backups = $class->list_volumes($storeid, $scfg, $vmid, ['backup']);
301 $type = 'vm' if defined($type) && $type eq 'qemu';
302 $type = 'ct' if defined($type) && $type eq 'lxc';
304 my $backup_groups = {};
305 foreach my $backup (@{$backups}) {
306 (my $backup_type = $backup->{format
}) =~ s/^pbs-//;
308 next if defined($type) && $backup_type ne $type;
310 my $backup_group = "$backup_type/$backup->{vmid}";
311 $backup_groups->{$backup_group} = 1;
316 my $keep_all = delete $keep->{'keep-all'};
319 foreach my $opt (keys %{$keep}) {
320 next if $keep->{$opt} == 0;
321 push @param, "--$opt";
322 push @param, "$keep->{$opt}";
324 } else { # no need to pass anything to PBS
325 $keep = { 'keep-all' => 1 };
328 push @param, '--dry-run' if $dryrun;
333 foreach my $backup_group (keys %{$backup_groups}) {
334 $logfunc->('info', "running 'proxmox-backup-client prune' for '$backup_group'")
337 my $res = run_client_cmd
($scfg, $storeid, 'prune', [ $backup_group, @param ]);
339 foreach my $backup (@{$res}) {
340 die "result from proxmox-backup-client is not as expected\n"
341 if !defined($backup->{'backup-time'})
342 || !defined($backup->{'backup-type'})
343 || !defined($backup->{'backup-id'})
344 || !defined($backup->{'keep'});
346 my $ctime = $backup->{'backup-time'};
347 my $type = $backup->{'backup-type'};
348 my $vmid = $backup->{'backup-id'};
349 my $volid = print_volid
($storeid, $type, $vmid, $ctime);
351 push @{$prune_list}, {
353 mark
=> $backup->{keep
} ?
'keep' : 'remove',
354 type
=> $type eq 'vm' ?
'qemu' : 'lxc',
361 $logfunc->('err', "prune '$backup_group': $err\n");
365 die "error pruning backups - check log\n" if $failed;
370 my $autogen_encryption_key = sub {
371 my ($scfg, $storeid) = @_;
372 my $encfile = pbs_encryption_key_file_name
($scfg, $storeid);
374 rename $encfile, "$encfile.old";
376 my $cmd = ['proxmox-backup-client', 'key', 'create', '--kdf', 'none', $encfile];
377 run_command
($cmd, errmsg
=> 'failed to create encryption key');
378 return PVE
::Tools
::file_get_contents
($encfile);
382 my ($class, $storeid, $scfg, %param) = @_;
386 if (defined(my $password = $param{password
})) {
387 pbs_set_password
($scfg, $storeid, $password);
389 pbs_delete_password
($scfg, $storeid);
392 if (defined(my $encryption_key = $param{'encryption-key'})) {
394 if ($encryption_key eq 'autogen') {
395 $res->{'encryption-key'} = $autogen_encryption_key->($scfg, $storeid);
396 $decoded_key = decode_json
($res->{'encryption-key'});
398 $decoded_key = eval { decode_json
($encryption_key) };
399 if ($@ || !exists($decoded_key->{data
})) {
400 die "Value does not seems like a valid, JSON formatted encryption key!\n";
402 pbs_set_encryption_key
($scfg, $storeid, $encryption_key);
403 $res->{'encryption-key'} = $encryption_key;
405 $scfg->{'encryption-key'} = $decoded_key->{fingerprint
} || 1;
407 pbs_delete_encryption_key
($scfg, $storeid);
414 my ($class, $storeid, $scfg, %param) = @_;
418 if (exists($param{password
})) {
419 if (defined($param{password
})) {
420 pbs_set_password
($scfg, $storeid, $param{password
});
422 pbs_delete_password
($scfg, $storeid);
426 if (exists($param{'encryption-key'})) {
427 if (defined(my $encryption_key = delete($param{'encryption-key'}))) {
429 if ($encryption_key eq 'autogen') {
430 $res->{'encryption-key'} = $autogen_encryption_key->($scfg, $storeid);
431 $decoded_key = decode_json
($res->{'encryption-key'});
433 $decoded_key = eval { decode_json
($encryption_key) };
434 if ($@ || !exists($decoded_key->{data
})) {
435 die "Value does not seems like a valid, JSON formatted encryption key!\n";
437 pbs_set_encryption_key
($scfg, $storeid, $encryption_key);
438 $res->{'encryption-key'} = $encryption_key;
440 $scfg->{'encryption-key'} = $decoded_key->{fingerprint
} || 1;
442 pbs_delete_encryption_key
($scfg, $storeid);
450 my ($class, $storeid, $scfg) = @_;
452 pbs_delete_password
($scfg, $storeid);
453 pbs_delete_encryption_key
($scfg, $storeid);
459 my ($class, $volname) = @_;
461 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)$!) {
465 my $format = "pbs-$btype";
467 my $name = "$btype/$bid/$btime";
469 if ($bid =~ m/^\d+$/) {
470 return ('backup', $name, $bid, undef, undef, undef, $format);
472 return ('backup', $name, undef, undef, undef, undef, $format);
476 die "unable to parse PBS volume name '$volname'\n";
480 my ($class, $scfg, $volname, $storeid, $snapname) = @_;
482 die "volume snapshot is not possible on pbs storage"
483 if defined($snapname);
485 my ($vtype, $name, $vmid) = $class->parse_volname($volname);
487 my $server = get_server_with_port
($scfg);
488 my $datastore = $scfg->{datastore
};
489 my $username = $scfg->{username
} // 'root@pam';
491 # artifical url - we currently do not use that anywhere
492 my $path = "pbs://$username\@$server:$datastore/$name";
494 return ($path, $vmid, $vtype);
498 my ($class, $storeid, $scfg, $volname) = @_;
500 die "can't create base images in pbs storage\n";
504 my ($class, $scfg, $storeid, $volname, $vmid, $snap) = @_;
506 die "can't clone images in pbs storage\n";
510 my ($class, $storeid, $scfg, $vmid, $fmt, $name, $size) = @_;
512 die "can't allocate space in pbs storage\n";
516 my ($class, $storeid, $scfg, $volname, $isBase) = @_;
518 my ($vtype, $name, $vmid) = $class->parse_volname($volname);
520 run_client_cmd
($scfg, $storeid, "forget", [ $name ], 1);
525 my ($class, $storeid, $scfg, $vmid, $vollist, $cache) = @_;
532 my sub snapshot_files_encrypted
{
538 for my $file (@$files) {
539 my $fn = $file->{filename
};
540 next if $fn eq 'client.log.blob' || $fn eq 'index.json.blob';
542 my $crypt = $file->{'crypt-mode'};
544 $all = 0 if !$crypt || $crypt ne 'encrypt';
545 $any ||= $crypt eq 'encrypt';
551 my ($class, $storeid, $scfg, $vmid, $content_types) = @_;
555 return $res if !grep { $_ eq 'backup' } @$content_types;
557 my $data = run_client_cmd
($scfg, $storeid, "snapshots");
559 foreach my $item (@$data) {
560 my $btype = $item->{"backup-type"};
561 my $bid = $item->{"backup-id"};
562 my $epoch = $item->{"backup-time"};
563 my $size = $item->{size
} // 1;
565 next if !($btype eq 'vm' || $btype eq 'ct');
566 next if $bid !~ m/^\d+$/;
567 next if defined($vmid) && $bid ne $vmid;
569 my $volid = print_volid
($storeid, $btype, $bid, $epoch);
573 format
=> "pbs-$btype",
580 $info->{verification
} = $item->{verification
} if defined($item->{verification
});
581 $info->{notes
} = $item->{comment
} if defined($item->{comment
});
582 if (defined($item->{fingerprint
})) {
583 $info->{encrypted
} = $item->{fingerprint
};
584 } elsif (snapshot_files_encrypted
($item->{files
})) {
585 $info->{encrypted
} = '1';
595 my ($class, $storeid, $scfg, $cache) = @_;
603 my $res = run_client_cmd
($scfg, $storeid, "status");
606 $total = $res->{total
};
607 $used = $res->{used
};
608 $free = $res->{avail
};
614 return ($total, $free, $used, $active);
617 # TODO: use a client with native rust/proxmox-backup bindings to profit from
618 # API schema checks and types
619 my sub pbs_api_connect
{
620 my ($scfg, $password) = @_;
624 my $user = $scfg->{username
} // 'root@pam';
626 if (my $tokenid = PVE
::AccessControl
::pve_verify_tokenid
($user, 1)) {
627 $params->{apitoken
} = "PBSAPIToken=${tokenid}=${password}";
629 $params->{password
} = $password;
630 $params->{username
} = $user;
633 if (my $fp = $scfg->{fingerprint
}) {
634 $params->{cached_fingerprints
}->{uc($fp)} = 1;
637 my $conn = PVE
::APIClient
::LWP-
>new(
639 host
=> $scfg->{server
},
640 port
=> $scfg->{port
} // 8007,
641 timeout
=> 7, # cope with a 401 (3s api delay) and high latency
642 cookie_name
=> 'PBSAuthCookie',
648 # can also be used for not (yet) added storages, pass $scfg with
652 # port (optional default to 8007)
653 # fingerprint (optional for trusted certs)
655 sub scan_datastores
{
656 my ($scfg, $password) = @_;
658 my $conn = pbs_api_connect
($scfg, $password);
660 my $response = eval { $conn->get('/api2/json/admin/datastore', {}) };
661 die "error fetching datastores - $@" if $@;
666 sub activate_storage
{
667 my ($class, $storeid, $scfg, $cache) = @_;
669 # a 'status' client command is to expensive here
670 # TODO: use a dummy ping API call to ensure the PBS API daemon is available for real
671 my $server = $scfg->{server
};
672 my $port = $scfg->{port
} // 8007;
673 PVE
::Network
::tcp_ping
($server, $port, 2);
678 sub deactivate_storage
{
679 my ($class, $storeid, $scfg, $cache) = @_;
683 sub activate_volume
{
684 my ($class, $storeid, $scfg, $volname, $snapname, $cache) = @_;
686 die "volume snapshot is not possible on pbs device" if $snapname;
691 sub deactivate_volume
{
692 my ($class, $storeid, $scfg, $volname, $snapname, $cache) = @_;
694 die "volume snapshot is not possible on pbs device" if $snapname;
699 sub get_volume_notes
{
700 my ($class, $scfg, $storeid, $volname, $timeout) = @_;
702 my (undef, $name, undef, undef, undef, undef, $format) = $class->parse_volname($volname);
704 my $data = run_client_cmd
($scfg, $storeid, "snapshot", [ "notes", "show", $name ]);
706 return $data->{notes
};
709 sub update_volume_notes
{
710 my ($class, $scfg, $storeid, $volname, $notes, $timeout) = @_;
712 my (undef, $name, undef, undef, undef, undef, $format) = $class->parse_volname($volname);
714 run_client_cmd
($scfg, $storeid, "snapshot", [ "notes", "update", $name, $notes ], 1);
719 sub volume_size_info
{
720 my ($class, $scfg, $storeid, $volname, $timeout) = @_;
722 my ($vtype, $name, undef, undef, undef, undef, $format) = $class->parse_volname($volname);
724 my $data = run_client_cmd
($scfg, $storeid, "files", [ $name ]);
727 foreach my $info (@$data) {
728 $size += $info->{size
} if $info->{size
};
733 return wantarray ?
($size, $format, $used, undef) : $size;
737 my ($class, $scfg, $storeid, $volname, $size, $running) = @_;
738 die "volume resize is not possible on pbs device";
741 sub volume_snapshot
{
742 my ($class, $scfg, $storeid, $volname, $snap) = @_;
743 die "volume snapshot is not possible on pbs device";
746 sub volume_snapshot_rollback
{
747 my ($class, $scfg, $storeid, $volname, $snap) = @_;
748 die "volume snapshot rollback is not possible on pbs device";
751 sub volume_snapshot_delete
{
752 my ($class, $scfg, $storeid, $volname, $snap) = @_;
753 die "volume snapshot delete is not possible on pbs device";
756 sub volume_has_feature
{
757 my ($class, $scfg, $feature, $storeid, $volname, $snapname, $running) = @_;