1 package PVE
::Storage
::PBSPlugin
;
3 # Plugin to access Proxmox Backup Server
7 use Fcntl
qw(F_GETFD F_SETFD FD_CLOEXEC);
12 use POSIX
qw(strftime ENOENT);
14 use PVE
::Tools
qw(run_command file_read_firstline trim dir_glob_regex dir_glob_foreach);
15 use PVE
::Storage
::Plugin
;
16 use PVE
::JSONSchema
qw(get_standard_option);
18 use base
qw(PVE::Storage::Plugin);
28 content
=> [ {backup
=> 1, none
=> 1}, { backup
=> 1 }],
35 description
=> "Proxmox backup server datastore name.",
38 # openssl s_client -connect <host>:8007 2>&1 |openssl x509 -fingerprint -sha256
39 fingerprint
=> get_standard_option
('fingerprint-sha256'),
41 description
=> "Encryption key. Use 'autogen' to generate one automatically without passphrase.",
49 server
=> { fixed
=> 1 },
50 datastore
=> { fixed
=> 1 },
51 nodes
=> { optional
=> 1},
52 disable
=> { optional
=> 1},
53 content
=> { optional
=> 1},
54 username
=> { optional
=> 1 },
55 password
=> { optional
=> 1 },
56 'encryption-key' => { optional
=> 1 },
57 maxfiles
=> { optional
=> 1 },
58 'prune-backups' => { optional
=> 1 },
59 fingerprint
=> { optional
=> 1 },
65 sub pbs_password_file_name
{
66 my ($scfg, $storeid) = @_;
68 return "/etc/pve/priv/storage/${storeid}.pw";
71 sub pbs_set_password
{
72 my ($scfg, $storeid, $password) = @_;
74 my $pwfile = pbs_password_file_name
($scfg, $storeid);
75 mkdir "/etc/pve/priv/storage";
77 PVE
::Tools
::file_set_contents
($pwfile, "$password\n");
80 sub pbs_delete_password
{
81 my ($scfg, $storeid) = @_;
83 my $pwfile = pbs_password_file_name
($scfg, $storeid);
88 sub pbs_get_password
{
89 my ($scfg, $storeid) = @_;
91 my $pwfile = pbs_password_file_name
($scfg, $storeid);
93 return PVE
::Tools
::file_read_firstline
($pwfile);
96 sub pbs_encryption_key_file_name
{
97 my ($scfg, $storeid) = @_;
99 return "/etc/pve/priv/storage/${storeid}.enc";
102 sub pbs_set_encryption_key
{
103 my ($scfg, $storeid, $key) = @_;
105 my $pwfile = pbs_encryption_key_file_name
($scfg, $storeid);
106 mkdir "/etc/pve/priv/storage";
108 PVE
::Tools
::file_set_contents
($pwfile, "$key\n");
111 sub pbs_delete_encryption_key
{
112 my ($scfg, $storeid) = @_;
114 my $pwfile = pbs_encryption_key_file_name
($scfg, $storeid);
119 sub pbs_get_encryption_key
{
120 my ($scfg, $storeid) = @_;
122 my $pwfile = pbs_encryption_key_file_name
($scfg, $storeid);
124 return PVE
::Tools
::file_get_contents
($pwfile);
127 # Returns a file handle if there is an encryption key, or `undef` if there is not. Dies on error.
128 sub pbs_open_encryption_key
{
129 my ($scfg, $storeid) = @_;
131 my $encryption_key_file = pbs_encryption_key_file_name
($scfg, $storeid);
134 if (!open($keyfd, '<', $encryption_key_file)) {
135 return undef if $! == ENOENT
;
136 die "failed to open encryption key: $encryption_key_file: $!\n";
143 my ($storeid, $btype, $bid, $btime) = @_;
145 my $time_str = strftime
("%FT%TZ", gmtime($btime));
146 my $volname = "backup/${btype}/${bid}/${time_str}";
148 return "${storeid}:${volname}";
151 my $USE_CRYPT_PARAMS = {
157 my sub do_raw_client_cmd
{
158 my ($scfg, $storeid, $client_cmd, $param, %opts) = @_;
160 my $use_crypto = $USE_CRYPT_PARAMS->{$client_cmd};
162 my $client_exe = '/usr/bin/proxmox-backup-client';
163 die "executable not found '$client_exe'! Proxmox backup client not installed?\n"
166 my $server = $scfg->{server
};
167 my $datastore = $scfg->{datastore
};
168 my $username = $scfg->{username
} // 'root@pam';
170 my $userns_cmd = delete $opts{userns_cmd
};
174 push @$cmd, @$userns_cmd if defined($userns_cmd);
176 push @$cmd, $client_exe, $client_cmd;
178 # This must live in the top scope to not get closed before the `run_command`
181 if (defined($keyfd = pbs_open_encryption_key
($scfg, $storeid))) {
182 my $flags = fcntl($keyfd, F_GETFD
, 0)
183 // die "failed to get file descriptor flags: $!\n";
184 fcntl($keyfd, F_SETFD
, $flags & ~FD_CLOEXEC
)
185 or die "failed to remove FD_CLOEXEC from encryption key file descriptor\n";
186 push @$cmd, '--crypt-mode=encrypt', '--keyfd='.fileno($keyfd);
188 push @$cmd, '--crypt-mode=none';
192 push @$cmd, @$param if defined($param);
194 push @$cmd, "--repository", "$username\@$server:$datastore";
196 local $ENV{PBS_PASSWORD
} = pbs_get_password
($scfg, $storeid);
198 local $ENV{PBS_FINGERPRINT
} = $scfg->{fingerprint
};
200 # no ascii-art on task logs
201 local $ENV{PROXMOX_OUTPUT_NO_BORDER
} = 1;
202 local $ENV{PROXMOX_OUTPUT_NO_HEADER
} = 1;
204 if (my $logfunc = $opts{logfunc
}) {
205 $logfunc->("run: " . join(' ', @$cmd));
208 run_command
($cmd, %opts);
211 # FIXME: External perl code should NOT have access to this.
213 # There should be separate functions to
218 sub run_raw_client_cmd
{
219 my ($scfg, $storeid, $client_cmd, $param, %opts) = @_;
220 return do_raw_client_cmd
($scfg, $storeid, $client_cmd, $param, %opts);
224 my ($scfg, $storeid, $client_cmd, $param, $no_output) = @_;
227 my $outfunc = sub { $json_str .= "$_[0]\n" };
229 $param = [] if !defined($param);
230 $param = [ $param ] if !ref($param);
232 $param = [@$param, '--output-format=json'] if !$no_output;
234 do_raw_client_cmd
($scfg, $storeid, $client_cmd, $param,
235 outfunc
=> $outfunc, errmsg
=> 'proxmox-backup-client failed');
237 return undef if $no_output;
239 my $res = decode_json
($json_str);
244 # Storage implementation
246 sub extract_vzdump_config
{
247 my ($class, $scfg, $volname, $storeid) = @_;
249 my ($vtype, $name, $vmid, undef, undef, undef, $format) = $class->parse_volname($volname);
252 my $outfunc = sub { $config .= "$_[0]\n" };
255 if ($format eq 'pbs-vm') {
256 $config_name = 'qemu-server.conf';
257 } elsif ($format eq 'pbs-ct') {
258 $config_name = 'pct.conf';
260 die "unable to extract configuration for backup format '$format'\n";
263 do_raw_client_cmd
($scfg, $storeid, 'restore', [ $name, $config_name, '-' ],
264 outfunc
=> $outfunc, errmsg
=> 'proxmox-backup-client failed');
270 my ($class, $scfg, $storeid, $keep, $vmid, $type, $dryrun, $logfunc) = @_;
272 $logfunc //= sub { print "$_[1]\n" };
274 my $backups = $class->list_volumes($storeid, $scfg, $vmid, ['backup']);
276 $type = 'vm' if defined($type) && $type eq 'qemu';
277 $type = 'ct' if defined($type) && $type eq 'lxc';
279 my $backup_groups = {};
280 foreach my $backup (@{$backups}) {
281 (my $backup_type = $backup->{format
}) =~ s/^pbs-//;
283 next if defined($type) && $backup_type ne $type;
285 my $backup_group = "$backup_type/$backup->{vmid}";
286 $backup_groups->{$backup_group} = 1;
290 foreach my $opt (keys %{$keep}) {
291 push @param, "--$opt";
292 push @param, "$keep->{$opt}";
295 push @param, '--dry-run' if $dryrun;
300 foreach my $backup_group (keys %{$backup_groups}) {
301 $logfunc->('info', "running 'proxmox-backup-client prune' for '$backup_group'")
304 my $res = run_client_cmd
($scfg, $storeid, 'prune', [ $backup_group, @param ]);
306 foreach my $backup (@{$res}) {
307 die "result from proxmox-backup-client is not as expected\n"
308 if !defined($backup->{'backup-time'})
309 || !defined($backup->{'backup-type'})
310 || !defined($backup->{'backup-id'})
311 || !defined($backup->{'keep'});
313 my $ctime = $backup->{'backup-time'};
314 my $type = $backup->{'backup-type'};
315 my $vmid = $backup->{'backup-id'};
316 my $volid = print_volid
($storeid, $type, $vmid, $ctime);
318 push @{$prune_list}, {
320 mark
=> $backup->{keep
} ?
'keep' : 'remove',
321 type
=> $type eq 'vm' ?
'qemu' : 'lxc',
328 $logfunc->('err', "prune '$backup_group': $err\n");
332 die "error pruning backups - check log\n" if $failed;
337 my $autogen_encryption_key = sub {
338 my ($scfg, $storeid) = @_;
339 my $encfile = pbs_encryption_key_file_name
($scfg, $storeid);
340 run_command
(['proxmox-backup-client', 'key', 'create', '--kdf', 'none', $encfile]);
344 my ($class, $storeid, $scfg, %param) = @_;
346 if (defined(my $password = $param{password
})) {
347 pbs_set_password
($scfg, $storeid, $password);
349 pbs_delete_password
($scfg, $storeid);
352 if (defined(my $encryption_key = $param{'encryption-key'})) {
353 if ($encryption_key eq 'autogen') {
354 $autogen_encryption_key->($scfg, $storeid);
356 pbs_set_encryption_key
($scfg, $storeid, $encryption_key);
359 pbs_delete_encryption_key
($scfg, $storeid);
364 my ($class, $storeid, $scfg, %param) = @_;
366 if (exists($param{password
})) {
367 if (defined($param{password
})) {
368 pbs_set_password
($scfg, $storeid, $param{password
});
370 pbs_delete_password
($scfg, $storeid);
374 if (exists($param{'encryption-key'})) {
375 if (defined(my $encryption_key = delete($param{'encryption-key'}))) {
376 if ($encryption_key eq 'autogen') {
377 $autogen_encryption_key->($scfg, $storeid);
379 pbs_set_encryption_key
($scfg, $storeid, $encryption_key);
382 pbs_delete_encryption_key
($scfg, $storeid);
388 my ($class, $storeid, $scfg) = @_;
390 pbs_delete_password
($scfg, $storeid);
391 pbs_delete_encryption_key
($scfg, $storeid);
395 my ($class, $volname) = @_;
397 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)$!) {
401 my $format = "pbs-$btype";
403 my $name = "$btype/$bid/$btime";
405 if ($bid =~ m/^\d+$/) {
406 return ('backup', $name, $bid, undef, undef, undef, $format);
408 return ('backup', $name, undef, undef, undef, undef, $format);
412 die "unable to parse PBS volume name '$volname'\n";
416 my ($class, $scfg, $volname, $storeid, $snapname) = @_;
418 die "volume snapshot is not possible on pbs storage"
419 if defined($snapname);
421 my ($vtype, $name, $vmid) = $class->parse_volname($volname);
423 my $server = $scfg->{server
};
424 my $datastore = $scfg->{datastore
};
425 my $username = $scfg->{username
} // 'root@pam';
427 # artifical url - we currently do not use that anywhere
428 my $path = "pbs://$username\@$server:$datastore/$name";
430 return ($path, $vmid, $vtype);
434 my ($class, $storeid, $scfg, $volname) = @_;
436 die "can't create base images in pbs storage\n";
440 my ($class, $scfg, $storeid, $volname, $vmid, $snap) = @_;
442 die "can't clone images in pbs storage\n";
446 my ($class, $storeid, $scfg, $vmid, $fmt, $name, $size) = @_;
448 die "can't allocate space in pbs storage\n";
452 my ($class, $storeid, $scfg, $volname, $isBase) = @_;
454 my ($vtype, $name, $vmid) = $class->parse_volname($volname);
456 run_client_cmd
($scfg, $storeid, "forget", [ $name ], 1);
461 my ($class, $storeid, $scfg, $vmid, $vollist, $cache) = @_;
469 my ($class, $storeid, $scfg, $vmid, $content_types) = @_;
473 return $res if !grep { $_ eq 'backup' } @$content_types;
475 my $data = run_client_cmd
($scfg, $storeid, "snapshots");
477 foreach my $item (@$data) {
478 my $btype = $item->{"backup-type"};
479 my $bid = $item->{"backup-id"};
480 my $epoch = $item->{"backup-time"};
481 my $size = $item->{size
} // 1;
483 next if !($btype eq 'vm' || $btype eq 'ct');
484 next if $bid !~ m/^\d+$/;
485 next if defined($vmid) && $bid ne $vmid;
487 my $volid = print_volid
($storeid, $btype, $bid, $epoch);
491 format
=> "pbs-$btype",
505 my ($class, $storeid, $scfg, $cache) = @_;
513 my $res = run_client_cmd
($scfg, $storeid, "status");
516 $total = $res->{total
};
517 $used = $res->{used
};
518 $free = $res->{avail
};
524 return ($total, $free, $used, $active);
527 sub activate_storage
{
528 my ($class, $storeid, $scfg, $cache) = @_;
530 run_client_cmd
($scfg, $storeid, "status");
535 sub deactivate_storage
{
536 my ($class, $storeid, $scfg, $cache) = @_;
540 sub activate_volume
{
541 my ($class, $storeid, $scfg, $volname, $snapname, $cache) = @_;
543 die "volume snapshot is not possible on pbs device" if $snapname;
548 sub deactivate_volume
{
549 my ($class, $storeid, $scfg, $volname, $snapname, $cache) = @_;
551 die "volume snapshot is not possible on pbs device" if $snapname;
556 sub volume_size_info
{
557 my ($class, $scfg, $storeid, $volname, $timeout) = @_;
559 my ($vtype, $name, undef, undef, undef, undef, $format) = $class->parse_volname($volname);
561 my $data = run_client_cmd
($scfg, $storeid, "files", [ $name ]);
564 foreach my $info (@$data) {
565 $size += $info->{size
} if $info->{size
};
570 return wantarray ?
($size, $format, $used, undef) : $size;
574 my ($class, $scfg, $storeid, $volname, $size, $running) = @_;
575 die "volume resize is not possible on pbs device";
578 sub volume_snapshot
{
579 my ($class, $scfg, $storeid, $volname, $snap) = @_;
580 die "volume snapshot is not possible on pbs device";
583 sub volume_snapshot_rollback
{
584 my ($class, $scfg, $storeid, $volname, $snap) = @_;
585 die "volume snapshot rollback is not possible on pbs device";
588 sub volume_snapshot_delete
{
589 my ($class, $scfg, $storeid, $volname, $snap) = @_;
590 die "volume snapshot delete is not possible on pbs device";
593 sub volume_has_feature
{
594 my ($class, $scfg, $feature, $storeid, $volname, $snapname, $running) = @_;