+sub extract_vzdump_config_tar {
+ my ($archive, $conf_re) = @_;
+
+ die "ERROR: file '$archive' does not exist\n" if ! -f $archive;
+
+ my $pid = open(my $fh, '-|', 'tar', 'tf', $archive) ||
+ die "unable to open file '$archive'\n";
+
+ my $file;
+ while (defined($file = <$fh>)) {
+ if ($file =~ $conf_re) {
+ $file = $1; # untaint
+ last;
+ }
+ }
+
+ kill 15, $pid;
+ waitpid $pid, 0;
+ close $fh;
+
+ die "ERROR: archive contains no configuration file\n" if !$file;
+ chomp $file;
+
+ my $raw = '';
+ my $out = sub {
+ my $output = shift;
+ $raw .= "$output\n";
+ };
+
+ PVE::Tools::run_command(['tar', '-xpOf', $archive, $file, '--occurrence'], outfunc => $out);
+
+ return wantarray ? ($raw, $file) : $raw;
+}
+
+sub extract_vzdump_config_vma {
+ my ($archive, $comp) = @_;
+
+ my $cmd;
+ my $raw = '';
+ my $out = sub {
+ my $output = shift;
+ $raw .= "$output\n";
+ };
+
+
+ if ($comp) {
+ my $uncomp;
+ if ($comp eq 'gz') {
+ $uncomp = ["zcat", $archive];
+ } elsif ($comp eq 'lzo') {
+ $uncomp = ["lzop", "-d", "-c", $archive];
+ } else {
+ die "unknown compression method '$comp'\n";
+ }
+ $cmd = [$uncomp, ["vma", "config", "-"]];
+
+ # in some cases, lzop/zcat exits with 1 when its stdout pipe is
+ # closed early by vma, detect this and ignore the exit code later
+ my $broken_pipe;
+ my $errstring;
+ my $err = sub {
+ my $output = shift;
+ if ($output =~ m/lzop: Broken pipe: <stdout>/ || $output =~ m/gzip: stdout: Broken pipe/) {
+ $broken_pipe = 1;
+ } elsif (!defined ($errstring) && $output !~ m/^\s*$/) {
+ $errstring = "Failed to extract config from VMA archive: $output\n";
+ }
+ };
+
+ # in other cases, the pipeline will exit with exit code 141
+ # because of the broken pipe, handle / ignore this as well
+ my $rc;
+ eval {
+ $rc = PVE::Tools::run_command($cmd, outfunc => $out, errfunc => $err, noerr => 1);
+ };
+ my $rerr = $@;
+
+ # use exit code if no stderr output and not just broken pipe
+ if (!$errstring && !$broken_pipe && $rc != 0 && $rc != 141) {
+ die "$rerr\n" if $rerr;
+ die "config extraction failed with exit code $rc\n";
+ }
+ die "$errstring\n" if $errstring;
+ } else {
+ # simple case without compression and weird piping behaviour
+ PVE::Tools::run_command(["vma", "config", $archive], outfunc => $out);
+ }
+
+ return wantarray ? ($raw, undef) : $raw;
+}
+
+sub extract_vzdump_config {
+ my ($cfg, $volid) = @_;
+
+ my $archive = abs_filesystem_path($cfg, $volid);
+
+ if ($volid =~ /vzdump-(lxc|openvz)-\d+-(\d{4})_(\d{2})_(\d{2})-(\d{2})_(\d{2})_(\d{2})\.(tgz|(tar(\.(gz|lzo))?))$/) {
+ return extract_vzdump_config_tar($archive, qr!^(\./etc/vzdump/(pct|vps)\.conf)$!);
+ } elsif ($volid =~ /vzdump-qemu-\d+-(\d{4})_(\d{2})_(\d{2})-(\d{2})_(\d{2})_(\d{2})\.(tgz|((tar|vma)(\.(gz|lzo))?))$/) {
+ my $format;
+ my $comp;
+ if ($7 eq 'tgz') {
+ $format = 'tar';
+ $comp = 'gz';
+ } else {
+ $format = $9;
+ $comp = $11 if defined($11);
+ }
+
+ if ($format eq 'tar') {
+ return extract_vzdump_config_tar($archive, qr!\(\./qemu-server\.conf\)!);
+ } else {
+ return extract_vzdump_config_vma($archive, $comp);
+ }
+ } else {
+ die "cannot determine backup guest type for backup archive '$volid'\n";
+ }
+}
+
+sub volume_export {
+ my ($cfg, $fh, $volid, $format, $snapshot, $base_snapshot, $with_snapshots) = @_;
+
+ my ($storeid, $volname) = parse_volume_id($volid, 1);
+ die "cannot export volume '$volid'\n" if !$storeid;
+ my $scfg = storage_config($cfg, $storeid);
+ my $plugin = PVE::Storage::Plugin->lookup($scfg->{type});
+ return $plugin->volume_export($scfg, $storeid, $fh, $volname, $format,
+ $snapshot, $base_snapshot, $with_snapshots);
+}
+
+sub volume_import {
+ my ($cfg, $fh, $volid, $format, $base_snapshot, $with_snapshots) = @_;
+
+ my ($storeid, $volname) = parse_volume_id($volid, 1);
+ die "cannot import into volume '$volid'\n" if !$storeid;
+ my $scfg = storage_config($cfg, $storeid);
+ my $plugin = PVE::Storage::Plugin->lookup($scfg->{type});
+ return $plugin->volume_import($scfg, $storeid, $fh, $volname, $format,
+ $base_snapshot, $with_snapshots);
+}
+
+sub volume_export_formats {
+ my ($cfg, $volid, $snapshot, $base_snapshot, $with_snapshots) = @_;
+
+ my ($storeid, $volname) = parse_volume_id($volid, 1);
+ return if !$storeid;
+ my $scfg = storage_config($cfg, $storeid);
+ my $plugin = PVE::Storage::Plugin->lookup($scfg->{type});
+ return $plugin->volume_export_formats($scfg, $storeid, $volname,
+ $snapshot, $base_snapshot,
+ $with_snapshots);
+}
+
+sub volume_import_formats {
+ my ($cfg, $volid, $base_snapshot, $with_snapshots) = @_;
+
+ my ($storeid, $volname) = parse_volume_id($volid, 1);
+ return if !$storeid;
+ my $scfg = storage_config($cfg, $storeid);
+ my $plugin = PVE::Storage::Plugin->lookup($scfg->{type});
+ return $plugin->volume_import_formats($scfg, $storeid, $volname,
+ $base_snapshot, $with_snapshots);
+}
+
+sub volume_transfer_formats {
+ my ($cfg, $src_volid, $dst_volid, $snapshot, $base_snapshot, $with_snapshots) = @_;
+ my @export_formats = volume_export_formats($cfg, $src_volid, $snapshot, $base_snapshot, $with_snapshots);
+ my @import_formats = volume_import_formats($cfg, $dst_volid, $base_snapshot, $with_snapshots);
+ my %import_hash = map { $_ => 1 } @import_formats;
+ my @common = grep { $import_hash{$_} } @export_formats;
+ return @common;
+}
+
+# bash completion helper
+
+sub complete_storage {
+ my ($cmdname, $pname, $cvalue) = @_;
+
+ my $cfg = PVE::Storage::config();
+
+ return $cmdname eq 'add' ? [] : [ PVE::Storage::storage_ids($cfg) ];
+}
+
+sub complete_storage_enabled {
+ my ($cmdname, $pname, $cvalue) = @_;
+
+ my $res = [];
+
+ my $cfg = PVE::Storage::config();
+ foreach my $sid (keys %{$cfg->{ids}}) {
+ next if !storage_check_enabled($cfg, $sid, undef, 1);
+ push @$res, $sid;
+ }
+ return $res;
+}
+
+sub complete_content_type {
+ my ($cmdname, $pname, $cvalue) = @_;
+
+ return [qw(rootdir images vztmpl iso backup)];
+}
+
+sub complete_volume {
+ my ($cmdname, $pname, $cvalue) = @_;
+
+ my $cfg = config();
+
+ my $storage_list = complete_storage_enabled();
+
+ if ($cvalue =~ m/^([^:]+):/) {
+ $storage_list = [ $1 ];
+ } else {
+ if (scalar(@$storage_list) > 1) {
+ # only list storage IDs to avoid large listings
+ my $res = [];
+ foreach my $storeid (@$storage_list) {
+ # Hack: simply return 2 artificial values, so that
+ # completions does not finish
+ push @$res, "$storeid:volname", "$storeid:...";
+ }
+ return $res;
+ }
+ }
+
+ my $res = [];
+ foreach my $storeid (@$storage_list) {
+ my $vollist = PVE::Storage::volume_list($cfg, $storeid);
+
+ foreach my $item (@$vollist) {
+ push @$res, $item->{volid};
+ }
+ }
+
+ return $res;
+}
+
+# Various io-heavy operations require io/bandwidth limits which can be
+# configured on multiple levels: The global defaults in datacenter.cfg, and
+# per-storage overrides. When we want to do a restore from storage A to storage
+# B, we should take the smaller limit defined for storages A and B, and if no
+# such limit was specified, use the one from datacenter.cfg.
+sub get_bandwidth_limit {
+ my ($operation, $storage_list, $override) = @_;
+
+ # called for each limit (global, per-storage) with the 'default' and the
+ # $operation limit and should udpate $override for every limit affecting
+ # us.
+ my $use_global_limits = 0;
+ my $apply_limit = sub {
+ my ($bwlimit) = @_;
+ if (defined($bwlimit)) {
+ my $limits = PVE::JSONSchema::parse_property_string('bwlimit', $bwlimit);
+ my $limit = $limits->{$operation} // $limits->{default};
+ if (defined($limit)) {
+ if (!$override || $limit < $override) {
+ $override = $limit;
+ }
+ return;
+ }
+ }
+ # If there was no applicable limit, try to apply the global ones.
+ $use_global_limits = 1;
+ };
+
+ my ($rpcenv, $authuser);
+ if (defined($override)) {
+ $rpcenv = PVE::RPCEnvironment->get();
+ $authuser = $rpcenv->get_user();
+ }
+
+ # Apply per-storage limits - if there are storages involved.
+ if (@$storage_list) {
+ my $config = config();
+
+ # The Datastore.Allocate permission allows us to modify the per-storage
+ # limits, therefore it also allows us to override them.
+ # Since we have most likely multiple storages to check, do a quick check on
+ # the general '/storage' path to see if we can skip the checks entirely:
+ return $override if $rpcenv && $rpcenv->check($authuser, '/storage', ['Datastore.Allocate'], 1);
+
+ my %done;
+ foreach my $storage (@$storage_list) {
+ # Avoid duplicate checks:
+ next if $done{$storage};
+ $done{$storage} = 1;
+
+ # Otherwise we may still have individual /storage/$ID permissions:
+ if (!$rpcenv || !$rpcenv->check($authuser, "/storage/$storage", ['Datastore.Allocate'], 1)) {
+ # And if not: apply the limits.
+ my $storecfg = storage_config($config, $storage);
+ $apply_limit->($storecfg->{bwlimit});
+ }
+ }
+
+ # Storage limits take precedence over the datacenter defaults, so if
+ # a limit was applied:
+ return $override if !$use_global_limits;
+ }
+
+ # Sys.Modify on '/' means we can change datacenter.cfg which contains the
+ # global default limits.
+ if (!$rpcenv || !$rpcenv->check($authuser, '/', ['Sys.Modify'], 1)) {
+ # So if we cannot modify global limits, apply them to our currently
+ # requested override.
+ my $dc = cfs_read_file('datacenter.cfg');
+ $apply_limit->($dc->{bwlimit});
+ }
+
+ return $override;
+}
+
+# checks if the storage id is available and dies if not
+sub check_available {
+ my ($id) = @_;
+
+ my $cfg = config();
+ if (my $scfg = storage_config($cfg, $id, 1)) {
+ die "storage ID '$id' already defined\n";
+ }
+
+ return undef;
+}
+