1 package PVE
::Storage
::PBSPlugin
;
3 # Plugin to access Proxmox Backup Server
8 use Fcntl
qw(F_GETFD F_SETFD FD_CLOEXEC);
11 use MIME
::Base64
qw(decode_base64);
12 use POSIX
qw(mktime strftime ENOENT);
15 use PVE
::APIClient
::LWP
;
16 use PVE
::JSONSchema
qw(get_standard_option);
19 use PVE
::Storage
::Plugin
;
20 use PVE
::Tools
qw(run_command file_read_firstline trim dir_glob_regex dir_glob_foreach $IPV6RE);
22 use base
qw(PVE::Storage::Plugin);
32 content
=> [ {backup
=> 1, none
=> 1}, { backup
=> 1 }],
39 description
=> "Proxmox Backup Server datastore name.",
42 # openssl s_client -connect <host>:8007 2>&1 |openssl x509 -fingerprint -sha256
43 fingerprint
=> get_standard_option
('fingerprint-sha256'),
45 description
=> "Encryption key. Use 'autogen' to generate one automatically without passphrase.",
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.",
53 description
=> "For non default port.",
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 },
82 sub pbs_password_file_name
{
83 my ($scfg, $storeid) = @_;
85 return "/etc/pve/priv/storage/${storeid}.pw";
88 sub pbs_set_password
{
89 my ($scfg, $storeid, $password) = @_;
91 my $pwfile = pbs_password_file_name
($scfg, $storeid);
92 mkdir "/etc/pve/priv/storage";
94 PVE
::Tools
::file_set_contents
($pwfile, "$password\n");
97 sub pbs_delete_password
{
98 my ($scfg, $storeid) = @_;
100 my $pwfile = pbs_password_file_name
($scfg, $storeid);
105 sub pbs_get_password
{
106 my ($scfg, $storeid) = @_;
108 my $pwfile = pbs_password_file_name
($scfg, $storeid);
110 return PVE
::Tools
::file_read_firstline
($pwfile);
113 sub pbs_encryption_key_file_name
{
114 my ($scfg, $storeid) = @_;
116 return "/etc/pve/priv/storage/${storeid}.enc";
119 sub pbs_set_encryption_key
{
120 my ($scfg, $storeid, $key) = @_;
122 my $pwfile = pbs_encryption_key_file_name
($scfg, $storeid);
123 mkdir "/etc/pve/priv/storage";
125 PVE
::Tools
::file_set_contents
($pwfile, "$key\n");
128 sub pbs_delete_encryption_key
{
129 my ($scfg, $storeid) = @_;
131 my $pwfile = pbs_encryption_key_file_name
($scfg, $storeid);
133 if (!unlink $pwfile) {
134 return if $! == ENOENT
;
135 die "failed to delete encryption key! $!\n";
137 delete $scfg->{'encryption-key'};
140 sub pbs_get_encryption_key
{
141 my ($scfg, $storeid) = @_;
143 my $pwfile = pbs_encryption_key_file_name
($scfg, $storeid);
145 return PVE
::Tools
::file_get_contents
($pwfile);
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) = @_;
152 my $encryption_key_file = pbs_encryption_key_file_name
($scfg, $storeid);
155 if (!open($keyfd, '<', $encryption_key_file)) {
156 return undef if $! == ENOENT
;
157 die "failed to open encryption key: $encryption_key_file: $!\n";
163 sub pbs_master_pubkey_file_name
{
164 my ($scfg, $storeid) = @_;
166 return "/etc/pve/priv/storage/${storeid}.master.pem";
169 sub pbs_set_master_pubkey
{
170 my ($scfg, $storeid, $key) = @_;
172 my $pwfile = pbs_master_pubkey_file_name
($scfg, $storeid);
173 mkdir "/etc/pve/priv/storage";
175 PVE
::Tools
::file_set_contents
($pwfile, "$key\n");
178 sub pbs_delete_master_pubkey
{
179 my ($scfg, $storeid) = @_;
181 my $pwfile = pbs_master_pubkey_file_name
($scfg, $storeid);
183 if (!unlink $pwfile) {
184 return if $! == ENOENT
;
185 die "failed to delete master public key! $!\n";
187 delete $scfg->{'master-pubkey'};
190 sub pbs_get_master_pubkey
{
191 my ($scfg, $storeid) = @_;
193 my $pwfile = pbs_master_pubkey_file_name
($scfg, $storeid);
195 return PVE
::Tools
::file_get_contents
($pwfile);
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) = @_;
202 my $master_pubkey_file = pbs_master_pubkey_file_name
($scfg, $storeid);
205 if (!open($keyfd, '<', $master_pubkey_file)) {
206 return undef if $! == ENOENT
;
207 die "failed to open master public key: $master_pubkey_file: $!\n";
214 my ($storeid, $btype, $bid, $btime) = @_;
216 my $time_str = strftime
("%FT%TZ", gmtime($btime));
217 my $volname = "backup/${btype}/${bid}/${time_str}";
219 return "${storeid}:${volname}";
222 # essentially the inverse of print_volid
223 sub api_param_from_volname
{
224 my ($class, $volname) = @_;
226 my $name = ($class->parse_volname($volname))[1];
228 my ($btype, $bid, $timestr) = split('/', $name);
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];
236 local $ENV{TZ
} = 'UTC'; # $timestr is UTC
238 # Fill in isdst to avoid undef warning. No daylight saving time for UTC.
241 my $since_epoch = mktime
(@tm) or die "error converting time from '$volname'\n";
242 $btime = int($since_epoch);
246 'backup-type' => $btype,
248 'backup-time' => $btime,
252 my $USE_CRYPT_PARAMS = {
258 my $USE_MASTER_KEY = {
262 my sub do_raw_client_cmd
{
263 my ($scfg, $storeid, $client_cmd, $param, %opts) = @_;
265 my $use_crypto = $USE_CRYPT_PARAMS->{$client_cmd};
266 my $use_master = $USE_MASTER_KEY->{$client_cmd};
268 my $client_exe = '/usr/bin/proxmox-backup-client';
269 die "executable not found '$client_exe'! Proxmox backup client not installed?\n"
272 my $repo = PVE
::PBSClient
::get_repository
($scfg);
274 my $userns_cmd = delete $opts{userns_cmd
};
278 push @$cmd, @$userns_cmd if defined($userns_cmd);
280 push @$cmd, $client_exe, $client_cmd;
282 # This must live in the top scope to not get closed before the `run_command`
283 my ($keyfd, $master_fd);
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);
299 push @$cmd, '--crypt-mode=none';
303 push @$cmd, @$param if defined($param);
305 push @$cmd, "--repository", $repo;
307 local $ENV{PBS_PASSWORD
} = pbs_get_password
($scfg, $storeid);
309 local $ENV{PBS_FINGERPRINT
} = $scfg->{fingerprint
};
311 # no ascii-art on task logs
312 local $ENV{PROXMOX_OUTPUT_NO_BORDER
} = 1;
313 local $ENV{PROXMOX_OUTPUT_NO_HEADER
} = 1;
315 if (my $logfunc = $opts{logfunc
}) {
316 $logfunc->("run: " . join(' ', @$cmd));
319 run_command
($cmd, %opts);
322 # FIXME: External perl code should NOT have access to this.
324 # There should be separate functions to
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);
335 my ($scfg, $storeid, $client_cmd, $param, $no_output) = @_;
338 my $outfunc = sub { $json_str .= "$_[0]\n" };
340 $param = [] if !defined($param);
341 $param = [ $param ] if !ref($param);
343 $param = [@$param, '--output-format=json'] if !$no_output;
345 do_raw_client_cmd
($scfg, $storeid, $client_cmd, $param,
346 outfunc
=> $outfunc, errmsg
=> 'proxmox-backup-client failed');
348 return undef if $no_output;
350 my $res = decode_json
($json_str);
355 # Storage implementation
357 sub extract_vzdump_config
{
358 my ($class, $scfg, $volname, $storeid) = @_;
360 my ($vtype, $name, $vmid, undef, undef, undef, $format) = $class->parse_volname($volname);
363 my $outfunc = sub { $config .= "$_[0]\n" };
366 if ($format eq 'pbs-vm') {
367 $config_name = 'qemu-server.conf';
368 } elsif ($format eq 'pbs-ct') {
369 $config_name = 'pct.conf';
371 die "unable to extract configuration for backup format '$format'\n";
374 do_raw_client_cmd
($scfg, $storeid, 'restore', [ $name, $config_name, '-' ],
375 outfunc
=> $outfunc, errmsg
=> 'proxmox-backup-client failed');
381 my ($class, $scfg, $storeid, $keep, $vmid, $type, $dryrun, $logfunc) = @_;
383 $logfunc //= sub { print "$_[1]\n" };
385 my $backups = $class->list_volumes($storeid, $scfg, $vmid, ['backup']);
387 $type = 'vm' if defined($type) && $type eq 'qemu';
388 $type = 'ct' if defined($type) && $type eq 'lxc';
390 my $backup_groups = {};
391 foreach my $backup (@{$backups}) {
392 (my $backup_type = $backup->{format
}) =~ s/^pbs-//;
394 next if defined($type) && $backup_type ne $type;
396 my $backup_group = "$backup_type/$backup->{vmid}";
397 $backup_groups->{$backup_group} = 1;
402 my $keep_all = delete $keep->{'keep-all'};
405 foreach my $opt (keys %{$keep}) {
406 next if $keep->{$opt} == 0;
407 push @param, "--$opt";
408 push @param, "$keep->{$opt}";
410 } else { # no need to pass anything to PBS
411 $keep = { 'keep-all' => 1 };
414 push @param, '--dry-run' if $dryrun;
419 foreach my $backup_group (keys %{$backup_groups}) {
420 $logfunc->('info', "running 'proxmox-backup-client prune' for '$backup_group'")
423 my $res = run_client_cmd
($scfg, $storeid, 'prune', [ $backup_group, @param ]);
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'});
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);
437 my $mark = $backup->{keep
} ?
'keep' : 'remove';
438 $mark = 'protected' if $backup->{protected
};
440 push @{$prune_list}, {
443 type
=> $type eq 'vm' ?
'qemu' : 'lxc',
450 $logfunc->('err', "prune '$backup_group': $err\n");
454 die "error pruning backups - check log\n" if $failed;
459 my $autogen_encryption_key = sub {
460 my ($scfg, $storeid) = @_;
461 my $encfile = pbs_encryption_key_file_name
($scfg, $storeid);
463 rename $encfile, "$encfile.old";
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);
471 my ($class, $storeid, $scfg, %param) = @_;
475 if (defined(my $password = $param{password
})) {
476 pbs_set_password
($scfg, $storeid, $password);
478 pbs_delete_password
($scfg, $storeid);
481 if (defined(my $encryption_key = $param{'encryption-key'})) {
483 if ($encryption_key eq 'autogen') {
484 $res->{'encryption-key'} = $autogen_encryption_key->($scfg, $storeid);
485 $decoded_key = decode_json
($res->{'encryption-key'});
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";
491 pbs_set_encryption_key
($scfg, $storeid, $encryption_key);
492 $res->{'encryption-key'} = $encryption_key;
494 $scfg->{'encryption-key'} = $decoded_key->{fingerprint
} || 1;
496 pbs_delete_encryption_key
($scfg, $storeid);
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'});
503 my $decoded = decode_base64
($master_key);
504 pbs_set_master_pubkey
($scfg, $storeid, $decoded);
505 $scfg->{'master-pubkey'} = 1;
507 pbs_delete_master_pubkey
($scfg, $storeid);
514 my ($class, $storeid, $scfg, %param) = @_;
518 if (exists($param{password
})) {
519 if (defined($param{password
})) {
520 pbs_set_password
($scfg, $storeid, $param{password
});
522 pbs_delete_password
($scfg, $storeid);
526 if (exists($param{'encryption-key'})) {
527 if (defined(my $encryption_key = delete($param{'encryption-key'}))) {
529 if ($encryption_key eq 'autogen') {
530 $res->{'encryption-key'} = $autogen_encryption_key->($scfg, $storeid);
531 $decoded_key = decode_json
($res->{'encryption-key'});
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";
537 pbs_set_encryption_key
($scfg, $storeid, $encryption_key);
538 $res->{'encryption-key'} = $encryption_key;
540 $scfg->{'encryption-key'} = $decoded_key->{fingerprint
} || 1;
542 pbs_delete_encryption_key
($scfg, $storeid);
543 delete $scfg->{'encryption-key'};
547 if (exists($param{'master-pubkey'})) {
548 if (defined(my $master_key = delete($param{'master-pubkey'}))) {
549 my $decoded = decode_base64
($master_key);
551 pbs_set_master_pubkey
($scfg, $storeid, $decoded);
552 $scfg->{'master-pubkey'} = 1;
554 pbs_delete_master_pubkey
($scfg, $storeid);
562 my ($class, $storeid, $scfg) = @_;
564 pbs_delete_password
($scfg, $storeid);
565 pbs_delete_encryption_key
($scfg, $storeid);
566 pbs_delete_master_pubkey
($scfg, $storeid);
572 my ($class, $volname) = @_;
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)$!) {
578 my $format = "pbs-$btype";
580 my $name = "$btype/$bid/$btime";
582 if ($bid =~ m/^\d+$/) {
583 return ('backup', $name, $bid, undef, undef, undef, $format);
585 return ('backup', $name, undef, undef, undef, undef, $format);
589 die "unable to parse PBS volume name '$volname'\n";
593 my ($class, $scfg, $volname, $storeid, $snapname) = @_;
595 die "volume snapshot is not possible on pbs storage"
596 if defined($snapname);
598 my ($vtype, $name, $vmid) = $class->parse_volname($volname);
600 my $repo = PVE
::PBSClient
::get_repository
($scfg);
602 # artificial url - we currently do not use that anywhere
603 my $path = "pbs://$repo/$name";
605 return ($path, $vmid, $vtype);
609 my ($class, $storeid, $scfg, $volname) = @_;
611 die "can't create base images in pbs storage\n";
615 my ($class, $scfg, $storeid, $volname, $vmid, $snap) = @_;
617 die "can't clone images in pbs storage\n";
621 my ($class, $storeid, $scfg, $vmid, $fmt, $name, $size) = @_;
623 die "can't allocate space in pbs storage\n";
627 my ($class, $storeid, $scfg, $volname, $isBase) = @_;
629 my ($vtype, $name, $vmid) = $class->parse_volname($volname);
631 run_client_cmd
($scfg, $storeid, "forget", [ $name ], 1);
638 my ($class, $storeid, $scfg, $vmid, $vollist, $cache) = @_;
645 my sub snapshot_files_encrypted
{
651 for my $file (@$files) {
652 my $fn = $file->{filename
};
653 next if $fn eq 'client.log.blob' || $fn eq 'index.json.blob';
655 my $crypt = $file->{'crypt-mode'};
657 $all = 0 if !$crypt || $crypt ne 'encrypt';
658 $any ||= defined($crypt) && $crypt eq 'encrypt';
664 my ($class, $storeid, $scfg, $vmid, $content_types) = @_;
668 return $res if !grep { $_ eq 'backup' } @$content_types;
670 my $data = run_client_cmd
($scfg, $storeid, "snapshots");
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;
678 next if !($btype eq 'vm' || $btype eq 'ct');
679 next if $bid !~ m/^\d+$/;
680 next if defined($vmid) && $bid ne $vmid;
682 my $volid = print_volid
($storeid, $btype, $bid, $epoch);
686 format
=> "pbs-$btype",
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';
709 my ($class, $storeid, $scfg, $cache) = @_;
717 my $res = run_client_cmd
($scfg, $storeid, "status");
720 $total = $res->{total
};
721 $used = $res->{used
};
722 $free = $res->{avail
};
728 return ($total, $free, $used, $active);
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) = @_;
738 my $user = $scfg->{username
} // 'root@pam';
740 if (my $tokenid = PVE
::AccessControl
::pve_verify_tokenid
($user, 1)) {
741 $params->{apitoken
} = "PBSAPIToken=${tokenid}:${password}";
743 $params->{password
} = $password;
744 $params->{username
} = $user;
747 if (my $fp = $scfg->{fingerprint
}) {
748 $params->{cached_fingerprints
}->{uc($fp)} = 1;
751 my $conn = PVE
::APIClient
::LWP-
>new(
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',
762 # can also be used for not (yet) added storages, pass $scfg with
766 # port (optional default to 8007)
767 # fingerprint (optional for trusted certs)
769 sub scan_datastores
{
770 my ($scfg, $password) = @_;
772 my $conn = pbs_api_connect
($scfg, $password);
774 my $response = eval { $conn->get('/api2/json/admin/datastore', {}) };
775 die "error fetching datastores - $@" if $@;
780 sub activate_storage
{
781 my ($class, $storeid, $scfg, $cache) = @_;
783 my $password = pbs_get_password
($scfg, $storeid);
785 my $datastores = eval { scan_datastores
($scfg, $password) };
786 die "$storeid: $@" if $@;
788 my $datastore = $scfg->{datastore
};
790 for my $ds (@$datastores) {
791 if ($ds->{store
} eq $datastore) {
796 die "$storeid: Cannot find datastore '$datastore', check permissions and existence!\n";
799 sub deactivate_storage
{
800 my ($class, $storeid, $scfg, $cache) = @_;
804 sub activate_volume
{
805 my ($class, $storeid, $scfg, $volname, $snapname, $cache) = @_;
807 die "volume snapshot is not possible on pbs device" if $snapname;
812 sub deactivate_volume
{
813 my ($class, $storeid, $scfg, $volname, $snapname, $cache) = @_;
815 die "volume snapshot is not possible on pbs device" if $snapname;
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) = @_;
825 my (undef, $name, undef, undef, undef, undef, $format) = $class->parse_volname($volname);
827 my $data = run_client_cmd
($scfg, $storeid, "snapshot", [ "notes", "show", $name ]);
829 return $data->{notes
};
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) = @_;
837 my (undef, $name, undef, undef, undef, undef, $format) = $class->parse_volname($volname);
839 run_client_cmd
($scfg, $storeid, "snapshot", [ "notes", "update", $name, $notes ], 1);
844 sub get_volume_attribute
{
845 my ($class, $scfg, $storeid, $volname, $attribute) = @_;
847 if ($attribute eq 'notes') {
848 return $class->get_volume_notes($scfg, $storeid, $volname);
851 if ($attribute eq 'protected') {
852 my $param = $class->api_param_from_volname($volname);
854 my $password = pbs_get_password
($scfg, $storeid);
855 my $conn = pbs_api_connect
($scfg, $password);
856 my $datastore = $scfg->{datastore
};
858 my $res = eval { $conn->get("/api2/json/admin/datastore/$datastore/$attribute", $param); };
860 return if $err->{code
} == 404; # not supported
869 sub update_volume_attribute
{
870 my ($class, $scfg, $storeid, $volname, $attribute, $value) = @_;
872 if ($attribute eq 'notes') {
873 return $class->update_volume_notes($scfg, $storeid, $volname, $value);
876 if ($attribute eq 'protected') {
877 my $param = $class->api_param_from_volname($volname);
878 $param->{$attribute} = $value;
880 my $password = pbs_get_password
($scfg, $storeid);
881 my $conn = pbs_api_connect
($scfg, $password);
882 my $datastore = $scfg->{datastore
};
884 eval { $conn->put("/api2/json/admin/datastore/$datastore/$attribute", $param); };
886 die "Server is not recent enough to support feature '$attribute'\n"
887 if $err->{code
} == 404;
893 die "attribute '$attribute' is not supported for storage type '$scfg->{type}'\n";
896 sub volume_size_info
{
897 my ($class, $scfg, $storeid, $volname, $timeout) = @_;
899 my ($vtype, $name, undef, undef, undef, undef, $format) = $class->parse_volname($volname);
901 my $data = run_client_cmd
($scfg, $storeid, "files", [ $name ]);
904 foreach my $info (@$data) {
905 if ($info->{size
} && $info->{size
} =~ /^(\d+)$/) { # untaints
912 return wantarray ?
($size, $format, $used, undef) : $size;
916 my ($class, $scfg, $storeid, $volname, $size, $running) = @_;
917 die "volume resize is not possible on pbs device";
920 sub volume_snapshot
{
921 my ($class, $scfg, $storeid, $volname, $snap) = @_;
922 die "volume snapshot is not possible on pbs device";
925 sub volume_snapshot_rollback
{
926 my ($class, $scfg, $storeid, $volname, $snap) = @_;
927 die "volume snapshot rollback is not possible on pbs device";
930 sub volume_snapshot_delete
{
931 my ($class, $scfg, $storeid, $volname, $snap) = @_;
932 die "volume snapshot delete is not possible on pbs device";
935 sub volume_has_feature
{
936 my ($class, $scfg, $feature, $storeid, $volname, $snapname, $running) = @_;