Add the ability to move a backup, ISO, container template or snippet
between storages and nodes via an API method. Moving a VMA backup to a
Proxmox Backup Server requires the proxmox-vma-to-pbs package to be
installed. Currently only VMA backups can be moved to a Proxmox Backup
Server and moving backups from a Proxmox Backup Server is not yet
supported.

The method can be called from the PVE shell with `pvesm move-volume`:

```
pvesm move-volume <source volume> <target storage> [--target-node <node>] 
[--delete]
```

For example to move a VMA backup to a Proxmox Backup Server:

```
pvesm move-volume \
    local:backup/vzdump-qemu-100-2024_06_25-13_08_56.vma.zst pbs
```

Or move a container template to another node and delete the source:

```
pvesm move-volume \
    local:vztmpl/devuan-4.0-standard_4.0_amd64.tar.gz local \
    --target-node pvenode2 --delete
```

Or use curl to call the API method:

```
curl 
https://$APINODE:8006/api2/json/nodes/$SOURCENODE/storage/$SOURCESTORAGE/content/$SOURCEVOLUME
 \
    --insecure --cookie "$(<cookie)" -H "$(<csrftoken)" -X POST \
    --data-raw "target-storage=$TARGETSTORAGE&target-node=$TARGETNODE"
```

Signed-off-by: Filip Schauer <f.scha...@proxmox.com>
---
This patch depends on
[PATCH backup-qemu/vma-to-pbs 0/2] add support for notes and logs
https://lists.proxmox.com/pipermail/pbs-devel/2024-May/009445.html

As of the time of writing this the version of proxmox-vma-to-pbs
specified in debian/control does not exist.

Changes since v2:
* Specify permissions for move method
* Add success message to move method
* Limit the move method to non-vdisk volumes
* Check that source and target are not the same in the move method
* Remove the unnecessary movevolume method from pvesm and make the
  move-volume command call the move API method directly
* Fail when trying to move a protected volume with the delete option
  enabled, instead of ignoring the protection
* Change "not yet supported" to "not supported" in messages indicating
  unimplemented features
* Process auxiliary files first when moving a volume locally on a node
* Move a volume instead of copying it when trying to move a volume
  locally on a node with the delete option enabled.
* Use the more general `path` function instead of `filesystem_path` to
  get the path of a volume
* Loosen the required privileges to move an ISO or a container template,
  or when the delete option is not set.
* Move the volume_move sub from PVE::Storage to
  PVE::API2::Storage::Content since it is only used there.
* Explicitly check that storages are path-based in volume_move,
  except when moving a vma to a Proxmox Backup Server

Changes since v1:
* Rename pvesm command to move-volume
* Add a delete option to control whether the source volume should be
  kept
* Move the API method to the POST endpoint of
  /nodes/{node}/storage/{storage}/content/{volume}, replacing the
  experimental copy method that has not been used since its introduction
  in October 2011 883eeea6.
* Implement migrating volumes between nodes

 debian/control                  |   1 +
 src/PVE/API2/Storage/Content.pm | 245 +++++++++++++++++++++++++++-----
 src/PVE/CLI/pvesm.pm            |   2 +
 src/PVE/Storage.pm              |  74 ++++++----
 src/PVE/Storage/Plugin.pm       | 106 ++++++++++----
 5 files changed, 342 insertions(+), 86 deletions(-)

diff --git a/debian/control b/debian/control
index d7afa98..d45b1f6 100644
--- a/debian/control
+++ b/debian/control
@@ -42,6 +42,7 @@ Depends: ceph-common (>= 12.2~),
          nfs-common,
          proxmox-backup-client (>= 2.1.10~),
          proxmox-backup-file-restore,
+         proxmox-vma-to-pbs (>= 0.0.2),
          pve-cluster (>= 5.0-32),
          smartmontools,
          smbclient,
diff --git a/src/PVE/API2/Storage/Content.pm b/src/PVE/API2/Storage/Content.pm
index fe0ad4a..7d789d8 100644
--- a/src/PVE/API2/Storage/Content.pm
+++ b/src/PVE/API2/Storage/Content.pm
@@ -2,7 +2,9 @@ package PVE::API2::Storage::Content;
 
 use strict;
 use warnings;
-use Data::Dumper;
+
+use File::Basename;
+use File::Copy qw(copy move);
 
 use PVE::SafeSyslog;
 use PVE::Cluster;
@@ -483,15 +485,173 @@ __PACKAGE__->register_method ({
        return $upid;
     }});
 
+sub volume_move {
+    my ($cfg, $source_volid, $target_storeid, $delete) = @_;
+
+    my ($source_storeid, $source_volname) = 
PVE::Storage::parse_volume_id($source_volid, 0);
+
+    die "source and target storage cannot be the same\n" if ($source_storeid 
eq $target_storeid);
+
+    PVE::Storage::activate_storage($cfg, $source_storeid);
+    my $source_scfg = PVE::Storage::storage_config($cfg, $source_storeid);
+    my $source_plugin = PVE::Storage::Plugin->lookup($source_scfg->{type});
+    my ($vtype) = $source_plugin->parse_volname($source_volname);
+
+    die "source storage '$source_storeid' does not support volumes of type 
'$vtype'\n"
+       if !$source_scfg->{content}->{$vtype};
+
+    PVE::Storage::activate_storage($cfg, $target_storeid);
+    my $target_scfg = PVE::Storage::storage_config($cfg, $target_storeid);
+    die "target storage '$target_storeid' does not support volumes of type 
'$vtype'\n"
+       if !$target_scfg->{content}->{$vtype};
+
+    if ($vtype eq 'backup' || $vtype eq 'iso' || $vtype eq 'vztmpl' || $vtype 
eq 'snippets') {
+       my $target_plugin = PVE::Storage::Plugin->lookup($target_scfg->{type});
+
+       if ($vtype eq 'backup') {
+           die "moving a backup from a Proxmox Backup Server is not 
supported\n"
+               if $source_scfg->{type} eq 'pbs';
+
+           my $protected = $source_plugin->get_volume_attribute(
+               $source_scfg, $source_storeid, $source_volname, 'protected');
+
+           die "cannot delete protected backup\n"
+               if ($delete && $protected);
+       }
+
+       die "expected storage '$source_storeid' to be path based\n" if 
!$source_scfg->{path};
+       my $source_path = $source_plugin->path($source_scfg, $source_volname, 
$source_storeid);
+       die "$source_path does not exist" if (!-e $source_path);
+       my $source_dirname = dirname($source_path);
+
+       if ($vtype eq 'backup' && $target_scfg->{type} eq 'pbs') {
+           my $info = PVE::Storage::archive_info($source_path);
+           die "moving non-VMA backups to a Proxmox Backup Server is not 
supported\n"
+               if ($info->{format} ne 'vma');
+
+           my $repo = PVE::PBSClient::get_repository($target_scfg);
+           my $vmid = ($source_plugin->parse_volname($source_volname))[2];
+           my $fingerprint = $target_scfg->{fingerprint};
+           my $password = PVE::Storage::PBSPlugin::pbs_password_file_name(
+               $target_scfg, $target_storeid);
+           my $namespace = $target_scfg->{namespace};
+           my $keyfile = PVE::Storage::PBSPlugin::pbs_encryption_key_file_name(
+               $target_scfg, $target_storeid);
+           my $master_keyfile = 
PVE::Storage::PBSPlugin::pbs_master_pubkey_file_name(
+               $target_scfg, $target_storeid);
+
+           my $comp = $info->{compression};
+           my $backup_time = $info->{ctime};
+           my $log_file_path = "$source_dirname/$info->{logfilename}";
+           my $notes_file_path = "$source_dirname/$info->{notesfilename}";
+
+           my $vma_to_pbs_cmd = [
+               "vma-to-pbs",
+               "--repository", $repo,
+               "--vmid", $vmid,
+               "--fingerprint", $fingerprint,
+               "--password-file", $password,
+               "--backup-time", $backup_time,
+               "--compress",
+           ];
+
+           push @$vma_to_pbs_cmd, "--ns", $namespace if $namespace;
+           push @$vma_to_pbs_cmd, "--log-file", $log_file_path if -e 
$log_file_path;
+           push @$vma_to_pbs_cmd, "--notes-file", $notes_file_path if -e 
$notes_file_path;
+           push @$vma_to_pbs_cmd, "--encrypt", "--keyfile", $keyfile if -e 
$keyfile;
+           push @$vma_to_pbs_cmd, "--master-keyfile", $master_keyfile if -e 
$master_keyfile;
+
+           if ($comp) {
+               PVE::Storage::decompress_archive_into_pipe($source_path, 
$vma_to_pbs_cmd);
+           } else {
+               push @$vma_to_pbs_cmd, $source_path;
+               run_command($vma_to_pbs_cmd);
+           }
+
+           my $protected = $source_plugin->get_volume_attribute(
+               $source_scfg, $source_storeid, $source_volname, 'protected');
+
+           if ($protected) {
+               my $target_volid = PVE::Storage::PBSPlugin::print_volid(
+                   $target_storeid, 'vm', $vmid, $backup_time);
+               my (undef, $target_volname) = 
PVE::Storage::parse_volume_id($target_volid, 0);
+               $target_plugin->update_volume_attribute(
+                   $target_scfg, $target_storeid, $target_volname, 
'protected', 1);
+           }
+       } else {
+           die "expected storage '$target_storeid' to be path based\n" if 
!$target_scfg->{path};
+           my $target_path = $target_plugin->path($target_scfg, 
$source_volname, $target_storeid);
+
+           die "$target_path already exists" if (-e $target_path);
+
+           my @created_files = ();
+
+           eval {
+               if ($vtype eq 'backup') {
+                   my $target_dirname = dirname($target_path);
+                   my $info = PVE::Storage::archive_info($source_path);
+
+                   for my $type (qw(log notes)) {
+                       my $filename = $info->{"${type}filename"} or next;
+                       my $auxiliary_source_path = "$source_dirname/$filename";
+                       my $auxiliary_target_path = "$target_dirname/$filename";
+                       if (-e $auxiliary_source_path) {
+                           copy($auxiliary_source_path, $auxiliary_target_path)
+                               or die "copying backup $type file failed: $!";
+                           push(@created_files, $auxiliary_target_path);
+                       }
+                   }
+
+                   my $protected = $source_plugin->get_volume_attribute(
+                       $source_scfg, $source_storeid, $source_volname, 
'protected');
+
+                   if ($protected) {
+                       my $protection_target_path = 
PVE::Storage::protection_file_path(
+                            $target_path);
+                       $target_plugin->update_volume_attribute(
+                           $target_scfg, $target_storeid, $source_volname, 
'protected', 1);
+                       push(@created_files, $protection_target_path);
+                   }
+               }
+
+               if ($delete) {
+                   move($source_path, $target_path) or die "failed to move 
$vtype: $!";
+               } else {
+                   copy($source_path, $target_path) or die "failed to copy 
$vtype: $!";
+               }
+           };
+           if (my $err = $@) {
+               for my $created_file (@created_files) {
+                   eval { unlink($created_file) };
+                   warn $@ if $@;
+               }
+               die $err;
+           }
+       }
+
+        PVE::Storage::archive_remove($source_path) if $delete;
+    } elsif ($vtype eq 'images') {
+       die "use pct move-volume or qm disk move\n";
+    } elsif ($vtype eq 'rootdir') {
+       die "cannot move OpenVZ rootdir\n";
+    }
+
+    return;
+}
+
 __PACKAGE__->register_method ({
-    name => 'copy',
+    name => 'move',
     path => '{volume}',
     method => 'POST',
-    description => "Copy a volume. This is experimental code - do not use.",
+    description => "Move a volume.",
+    permissions => {
+       description => "You need the 'Datastore.Allocate' privilege on the 
storages.",
+       user => 'all',
+    },
     protected => 1,
     proxyto => 'node',
     parameters => {
-       additionalProperties => 0,
+       additionalProperties => 0,
        properties => {
            node => get_standard_option('pve-node'),
            storage => get_standard_option('pve-storage-id', { optional => 1}),
@@ -499,14 +659,21 @@ __PACKAGE__->register_method ({
                description => "Source volume identifier",
                type => 'string',
            },
-           target => {
-               description => "Target volume identifier",
+           'target-storage' => {
+               description => "Target storage",
                type => 'string',
            },
-           target_node => get_standard_option('pve-node',  {
+           'target-node' => get_standard_option('pve-node',  {
                description => "Target node. Default is local node.",
                optional => 1,
            }),
+           delete => {
+               type => 'boolean',
+               description => "Delete the original volume after a successful 
copy. " .
+                   "By default the original is kept.",
+               optional => 1,
+               default => 0,
+           },
        },
     },
     returns => {
@@ -515,43 +682,51 @@ __PACKAGE__->register_method ({
     code => sub {
        my ($param) = @_;
 
-       my $rpcenv = PVE::RPCEnvironment::get();
-
-       my $user = $rpcenv->get_user();
-
-       my $target_node = $param->{target_node} || PVE::INotify::nodename();
-       # pvesh examples
-       # cd /nodes/localhost/storage/local/content
-       # pve:/> create local:103/vm-103-disk-1.raw -target 
local:103/vm-103-disk-2.raw
-       # pve:/> create 103/vm-103-disk-1.raw -target 103/vm-103-disk-3.raw
-
        my $src_volid = &$real_volume_id($param->{storage}, $param->{volume});
-       my $dst_volid = &$real_volume_id($param->{storage}, $param->{target});
+       my $dst_storeid = $param->{'target-storage'};
+       my ($src_storeid, $volname) = PVE::Storage::parse_volume_id($src_volid);
+       my $src_node = PVE::INotify::nodename();
+       my $dst_node = $param->{'target-node'} || $src_node;
+       my $delete = $param->{delete};
 
-       print "DEBUG: COPY $src_volid TO $dst_volid\n";
+       die "source and target cannot be the same\n"
+           if ($src_node eq $dst_node && $src_storeid eq $dst_storeid);
 
        my $cfg = PVE::Storage::config();
+       my $rpcenv = PVE::RPCEnvironment::get();
+       my $user = $rpcenv->get_user();
 
-       # do all parameter checks first
-
-       # then do all short running task (to raise errors before we go to 
background)
-
-       # then start the worker task
-       my $worker = sub  {
-           my $upid = shift;
-
-           print "DEBUG: starting worker $upid\n";
+       if ($delete) {
+           $rpcenv->check($user, "/storage/$src_storeid", 
["Datastore.Allocate"]);
+       } else {
+           $rpcenv->check($user, "/storage/$src_storeid", ["Datastore.Audit"]);
+       }
 
-           my ($target_sid, $target_volname) = 
PVE::Storage::parse_volume_id($dst_volid);
-           #my $target_ip = PVE::Cluster::remote_node_ip($target_node);
+       my ($vtype) = PVE::Storage::parse_volname($cfg, $src_volid);
+       die "use pct move-volume or qm disk move" if $vtype eq 'images';
 
-           # you need to get this working (fails currently, because 
storage_migrate() uses
-           # ssh to connect to local host (which is not needed
-           my $sshinfo = PVE::SSHInfo::get_ssh_info($target_node);
-           PVE::Storage::storage_migrate($cfg, $src_volid, $sshinfo, 
$target_sid, {'target_volname' => $target_volname});
+       if ($vtype eq 'iso' || $vtype eq 'vztmpl') {
+           $rpcenv->check($user, "/storage/$dst_storeid", 
["Datastore.AllocateTemplate"]);
+       } else {
+           $rpcenv->check($user, "/storage/$dst_storeid", 
["Datastore.Allocate"]);
+       }
 
-           print "DEBUG: end worker $upid\n";
+       my $worker = sub  {
+           if ($src_node eq $dst_node) {
+               volume_move($cfg, $src_volid, $dst_storeid, $delete);
+           } else {
+               PVE::Storage::storage_check_enabled($cfg, $dst_storeid, 
$dst_node);
+               my $sshinfo = PVE::SSHInfo::get_ssh_info($dst_node);
+               PVE::Storage::storage_migrate(
+                   $cfg, $src_volid, $sshinfo, $dst_storeid, {'target_volname' 
=> $volname});
+               if ($delete) {
+                   my $src_path = PVE::Storage::abs_filesystem_path($cfg, 
$src_volid);
+                   PVE::Storage::archive_remove($src_path, 1);
+               }
+           }
 
+           print "Moved volume '$src_volid' on node '$src_node'"
+               ." to '$dst_storeid' on node '$dst_node'\n";
        };
 
        return $rpcenv->fork_worker('imgcopy', undef, $user, $worker);
diff --git a/src/PVE/CLI/pvesm.pm b/src/PVE/CLI/pvesm.pm
index 9b9676b..8031fc1 100755
--- a/src/PVE/CLI/pvesm.pm
+++ b/src/PVE/CLI/pvesm.pm
@@ -690,6 +690,8 @@ our $cmddef = {
        print "APIVER $res->{apiver}\n";
        print "APIAGE $res->{apiage}\n";
     }],
+    'move-volume' => [ "PVE::API2::Storage::Content", 'move', ['volume', 
'target-storage'],
+               { node => $nodename } ],
     'prune-backups' => [ __PACKAGE__, 'prunebackups', ['storage'], { node => 
$nodename }, sub {
        my $res = shift;
 
diff --git a/src/PVE/Storage.pm b/src/PVE/Storage.pm
index 739fd6d..18319ef 100755
--- a/src/PVE/Storage.pm
+++ b/src/PVE/Storage.pm
@@ -48,7 +48,15 @@ use constant APIVER => 10;
 # see 
https://www.gnu.org/software/libtool/manual/html_node/Libtool-versioning.html
 use constant APIAGE => 1;
 
-our $KNOWN_EXPORT_FORMATS = ['raw+size', 'tar+size', 'qcow2+size', 
'vmdk+size', 'zfs', 'btrfs'];
+our $KNOWN_EXPORT_FORMATS = [
+    'raw+size',
+    'tar+size',
+    'qcow2+size',
+    'vmdk+size',
+    'zfs',
+    'btrfs',
+    'backup+size'
+];
 
 # load standard plugins
 PVE::Storage::DirPlugin->register();
@@ -1680,6 +1688,45 @@ sub extract_vzdump_config_tar {
     return wantarray ? ($raw, $file) : $raw;
 }
 
+sub decompress_archive_into_pipe {
+    my ($archive, $cmd, $outfunc) = @_;
+
+    my $info = archive_info($archive);
+    die "archive is not compressed\n" if !$info->{compression};
+    my $decompressor = $info->{decompressor};
+    my $full_cmd = [ [@$decompressor, $archive], $cmd ];
+
+    # lzop/zcat exits with 1 when the pipe is closed early,
+    # 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/
+           || $output =~ m/zstd: error 70 : Write error.*Broken pipe/
+       ) {
+           $broken_pipe = 1;
+       } elsif (!defined ($errstring) && $output !~ m/^\s*$/) {
+           $errstring = "failed to decompress archive: $output\n";
+       }
+    };
+
+    my $rc = eval { run_command($full_cmd, outfunc => $outfunc, errfunc => 
$err, noerr => 1) };
+    my $rerr = $@;
+
+    $broken_pipe ||= $rc == 141; # broken pipe from cmd POV
+
+    if (!$errstring && !$broken_pipe && $rc != 0) {
+       die "$rerr\n" if $rerr;
+       die "archive decompression failed with exit code $rc\n";
+    }
+    die "$errstring\n" if $errstring;
+
+    return;
+}
+
 sub extract_vzdump_config_vma {
     my ($archive, $comp) = @_;
 
@@ -1691,30 +1738,7 @@ sub extract_vzdump_config_vma {
     my $decompressor = $info->{decompressor};
 
     if ($comp) {
-       my $cmd = [ [@$decompressor, $archive], ["vma", "config", "-"] ];
-
-       # lzop/zcat exits with 1 when the 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/ || $output =~ m/zstd: error 70 : Write error.*Broken 
pipe/) {
-               $broken_pipe = 1;
-           } elsif (!defined ($errstring) && $output !~ m/^\s*$/) {
-               $errstring = "Failed to extract config from VMA archive: 
$output\n";
-           }
-       };
-
-       my $rc = eval { run_command($cmd, outfunc => $out, errfunc => $err, 
noerr => 1) };
-       my $rerr = $@;
-
-       $broken_pipe ||= $rc == 141; # broken pipe from vma POV
-
-       if (!$errstring && !$broken_pipe && $rc != 0) {
-           die "$rerr\n" if $rerr;
-           die "config extraction failed with exit code $rc\n";
-       }
-       die "$errstring\n" if $errstring;
+       decompress_archive_into_pipe($archive, ["vma", "config", "-"], $out);
     } else {
        run_command(["vma", "config", $archive], outfunc => $out);
     }
diff --git a/src/PVE/Storage/Plugin.pm b/src/PVE/Storage/Plugin.pm
index 6444390..7308f33 100644
--- a/src/PVE/Storage/Plugin.pm
+++ b/src/PVE/Storage/Plugin.pm
@@ -1575,6 +1575,8 @@ sub prune_backups {
 #     unprivileged container. In other words, this is from the root user
 #     namespace's point of view with no uid-mapping in effect.
 #     As produced via `tar -C vm-100-disk-1.subvol -cpf TheOutputFile.dat .`
+#   backup+size: (backups only)
+#     A raw binary data stream prefixed with a protection flag and notes.
 
 # Plugins may reuse these helpers. Changes to the header format should be
 # reflected by changes to the function prototypes.
@@ -1623,6 +1625,16 @@ sub volume_export {
            run_command(['tar', @COMMON_TAR_FLAGS, '-cf', '-', '-C', $file, 
'.'],
                        output => '>&'.fileno($fh));
            return;
+       } elsif ($format eq 'backup+size') {
+           write_common_header($fh, $size);
+           my $protected = $class->get_volume_attribute(
+               $scfg, $storeid, $volname, 'protected') ? 1 : 0;
+           my $notes = $class->get_volume_attribute($scfg, $storeid, $volname, 
'notes') // "";
+           syswrite($fh, pack("C", $protected), 1);
+           syswrite($fh, pack("Q<", length($notes)), 8);
+           syswrite($fh, $notes, length($notes));
+           run_command(['dd', "if=$file", "bs=4k", "status=progress"], output 
=> '>&'.fileno($fh));
+           return;
        }
     }
     die $err_msg;
@@ -1635,6 +1647,10 @@ sub volume_export_formats {
            or return;
        my ($size, $format) = file_size_info($file);
 
+       my ($vtype) = $class->parse_volname($volname);
+
+       return ('backup+size') if $vtype eq 'backup';
+
        if ($with_snapshots) {
            return ($format.'+size') if ($format eq 'qcow2' || $format eq 
'vmdk');
            return ();
@@ -1650,7 +1666,7 @@ sub volume_import {
     my ($class, $scfg, $storeid, $fh, $volname, $format, $snapshot, 
$base_snapshot, $with_snapshots, $allow_rename) = @_;
 
     die "volume import format '$format' not available for $class\n"
-       if $format !~ /^(raw|tar|qcow2|vmdk)\+size$/;
+       if $format !~ /^(raw|tar|qcow2|vmdk|backup)\+size$/;
     my $data_format = $1;
 
     die "format $format cannot be imported without snapshots\n"
@@ -1661,16 +1677,21 @@ sub volume_import {
     my ($vtype, $name, $vmid, $basename, $basevmid, $isBase, $file_format) =
        $class->parse_volname($volname);
 
+    die "cannot import OpenVZ rootdir\n" if $vtype eq 'rootdir';
+
     # XXX: Should we bother with conversion routines at this level? This won't
     # happen without manual CLI usage, so for now we just error out...
-    die "cannot import format $format into a file of format $file_format\n"
-       if $data_format ne $file_format && !($data_format eq 'tar' && 
$file_format eq 'subvol');
+    if ($vtype eq 'images' && $data_format ne $file_format &&
+       !($data_format eq 'tar' && $file_format eq 'subvol')
+    ) {
+       die "cannot import format $format into a file of format $file_format\n";
+    }
 
     # Check for an existing file first since interrupting alloc_image doesn't
     # free it.
     my $file = $class->path($scfg, $volname, $storeid);
     if (-e $file) {
-       die "file '$file' already exists\n" if !$allow_rename;
+       die "file '$file' already exists\n" if !$allow_rename || $vtype ne 
'images';
        warn "file '$file' already exists - importing with a different name\n";
        $name = undef;
     }
@@ -1678,29 +1699,58 @@ sub volume_import {
     my ($size) = read_common_header($fh);
     $size = int($size/1024);
 
-    eval {
-       my $allocname = $class->alloc_image($storeid, $scfg, $vmid, 
$file_format, $name, $size);
-       my $oldname = $volname;
-       $volname = $allocname;
-       if (defined($name) && $allocname ne $oldname) {
-           die "internal error: unexpected allocated name: '$allocname' != 
'$oldname'\n";
+    if ($vtype eq 'images') {
+       eval {
+           my $allocname = $class->alloc_image($storeid, $scfg, $vmid, 
$file_format, $name, $size);
+           my $oldname = $volname;
+           $volname = $allocname;
+           if (defined($name) && $allocname ne $oldname) {
+               die "internal error: unexpected allocated name: '$allocname' != 
'$oldname'\n";
+           }
+           my $file = $class->path($scfg, $volname, $storeid)
+               or die "internal error: failed to get path to newly allocated 
volume $volname\n";
+           if ($data_format eq 'raw' || $data_format eq 'qcow2' || 
$data_format eq 'vmdk') {
+               run_command(['dd', "of=$file", 'conv=sparse', 'bs=64k'],
+                   input => '<&'.fileno($fh));
+           } elsif ($data_format eq 'tar') {
+               run_command(['tar', @COMMON_TAR_FLAGS, '-C', $file, '-xf', '-'],
+                   input => '<&'.fileno($fh));
+           } else {
+               die "volume import format '$format' not available for $class";
+           }
+       };
+       if (my $err = $@) {
+           eval { $class->free_image($storeid, $scfg, $volname, 0, 
$file_format) };
+           warn $@ if $@;
+           die $err;
        }
-       my $file = $class->path($scfg, $volname, $storeid)
-           or die "internal error: failed to get path to newly allocated 
volume $volname\n";
-       if ($data_format eq 'raw' || $data_format eq 'qcow2' || $data_format eq 
'vmdk') {
-           run_command(['dd', "of=$file", 'conv=sparse', 'bs=64k'],
-                       input => '<&'.fileno($fh));
-       } elsif ($data_format eq 'tar') {
-           run_command(['tar', @COMMON_TAR_FLAGS, '-C', $file, '-xf', '-'],
-                       input => '<&'.fileno($fh));
-       } else {
-           die "volume import format '$format' not available for $class";
+    } elsif ($vtype eq 'iso' || $vtype eq 'snippets' || $vtype eq 'vztmpl' || 
$vtype eq 'backup') {
+       my $protected;
+       my $notes;
+
+       if ($vtype eq 'backup') {
+           sysread($fh, $protected, 1);
+           $protected = unpack('C', $protected);
+           sysread($fh, my $notes_len, 8);
+           $notes_len = unpack('Q<', $notes_len);
+           sysread($fh, $notes, $notes_len);
+       }
+
+       eval {
+           run_command(['dd', "of=$file", 'bs=64k'], input => 
'<&'.fileno($fh));
+       };
+       if (my $err = $@) {
+           if (-e $file) {
+               eval { unlink($file) };
+               warn $@ if $@;
+           }
+           die $err;
+       }
+
+       if ($vtype eq 'backup') {
+           $class->update_volume_attribute($scfg, $storeid, $volname, 
'protected', $protected);
+           $class->update_volume_attribute($scfg, $storeid, $volname, 'notes', 
$notes);
        }
-    };
-    if (my $err = $@) {
-       eval { $class->free_image($storeid, $scfg, $volname, 0, $file_format) };
-       warn $@ if $@;
-       die $err;
     }
 
     return "$storeid:$volname";
@@ -1709,7 +1759,11 @@ sub volume_import {
 sub volume_import_formats {
     my ($class, $scfg, $storeid, $volname, $snapshot, $base_snapshot, 
$with_snapshots) = @_;
     if ($scfg->{path} && !defined($base_snapshot)) {
-       my $format = ($class->parse_volname($volname))[6];
+       my ($vtype, $format) = ($class->parse_volname($volname))[0, 6];
+
+       return ('backup+size') if $vtype eq 'backup';
+       return ('raw+size') if !defined($format);
+
        if ($with_snapshots) {
            return ($format.'+size') if ($format eq 'qcow2' || $format eq 
'vmdk');
            return ();
-- 
2.39.2



_______________________________________________
pve-devel mailing list
pve-devel@lists.proxmox.com
https://lists.proxmox.com/cgi-bin/mailman/listinfo/pve-devel

Reply via email to