]> git.proxmox.com Git - pve-storage.git/blobdiff - PVE/Storage.pm
add disk rename feature
[pve-storage.git] / PVE / Storage.pm
index 19f675d5bd2045ead97c23b56c35fb7bce73e57b..d64019fc62a9a4c5db20ae58176875645b8b03b8 100755 (executable)
@@ -37,15 +37,15 @@ use PVE::Storage::ISCSIDirectPlugin;
 use PVE::Storage::GlusterfsPlugin;
 use PVE::Storage::ZFSPoolPlugin;
 use PVE::Storage::ZFSPlugin;
-use PVE::Storage::DRBDPlugin;
 use PVE::Storage::PBSPlugin;
+use PVE::Storage::BTRFSPlugin;
 
 # Storage API version. Increment it on changes in storage API interface.
-use constant APIVER => 8;
+use constant APIVER => 10;
 # Age is the number of versions we're backward compatible with.
 # This is like having 'current=APIVER' and age='APIAGE' in libtool,
 # see https://www.gnu.org/software/libtool/manual/html_node/Libtool-versioning.html
-use constant APIAGE => 7;
+use constant APIAGE => 1;
 
 # load standard plugins
 PVE::Storage::DirPlugin->register();
@@ -60,8 +60,8 @@ PVE::Storage::ISCSIDirectPlugin->register();
 PVE::Storage::GlusterfsPlugin->register();
 PVE::Storage::ZFSPoolPlugin->register();
 PVE::Storage::ZFSPlugin->register();
-PVE::Storage::DRBDPlugin->register();
 PVE::Storage::PBSPlugin->register();
+PVE::Storage::BTRFSPlugin->register();
 
 # load third-party plugins
 if ( -d '/usr/share/perl5/PVE/Storage/Custom' ) {
@@ -75,10 +75,8 @@ if ( -d '/usr/share/perl5/PVE/Storage/Custom' ) {
            require $file;
 
            # Check perl interface:
-           die "not derived from PVE::Storage::Plugin\n"
-               if !$modname->isa('PVE::Storage::Plugin');
-           die "does not provide an api() method\n"
-               if !$modname->can('api');
+           die "not derived from PVE::Storage::Plugin\n" if !$modname->isa('PVE::Storage::Plugin');
+           die "does not provide an api() method\n" if !$modname->can('api');
            # Check storage API version and that file is really storage plugin.
            my $version = $modname->api();
            die "implements an API version newer than current ($version > " . APIVER . ")\n"
@@ -86,11 +84,11 @@ if ( -d '/usr/share/perl5/PVE/Storage/Custom' ) {
            my $min_version = (APIVER - APIAGE);
            die "API version too old, please update the plugin ($version < $min_version)\n"
                if $version < $min_version;
+           # all OK, do import and register (i.e., "use")
            import $file;
            $modname->register();
 
-           # If we got this far and the API version is not the same, make some
-           # noise:
+           # If we got this far and the API version is not the same, make some noise:
            warn "Plugin \"$modname\" is implementing an older storage API, an upgrade is recommended\n"
                if $version != APIVER;
        };
@@ -105,6 +103,8 @@ PVE::Storage::Plugin->init();
 
 our $iso_extension_re = qr/\.(?:iso|img)/i;
 
+our $vztmpl_extension_re = qr/\.tar\.(gz|xz|zst)/i;
+
 #  PVE::Storage utility functions
 
 sub config {
@@ -127,7 +127,7 @@ sub lock_storage_config {
     }
 }
 
-# FIXME remove maxfiles for PVE 7.0
+# FIXME remove maxfiles for PVE 8.0 or PVE 9.0
 my $convert_maxfiles_to_prune_backups = sub {
     my ($scfg) = @_;
 
@@ -194,7 +194,7 @@ sub storage_check_enabled {
 
 # storage_can_replicate:
 # return true if storage supports replication
-# (volumes alocated with vdisk_alloc() has replication feature)
+# (volumes allocated with vdisk_alloc() has replication feature)
 sub storage_can_replicate {
     my ($cfg, $storeid, $format) = @_;
 
@@ -215,24 +215,24 @@ sub file_size_info {
     return PVE::Storage::Plugin::file_size_info($filename, $timeout);
 }
 
-sub get_volume_notes {
-    my ($cfg, $volid, $timeout) = @_;
+sub get_volume_attribute {
+    my ($cfg, $volid, $attribute) = @_;
 
     my ($storeid, $volname) = parse_volume_id($volid);
     my $scfg = storage_config($cfg, $storeid);
     my $plugin = PVE::Storage::Plugin->lookup($scfg->{type});
 
-    return $plugin->get_volume_notes($scfg, $storeid, $volname, $timeout);
+    return $plugin->get_volume_attribute($scfg, $storeid, $volname, $attribute);
 }
 
-sub update_volume_notes {
-    my ($cfg, $volid, $notes, $timeout) = @_;
+sub update_volume_attribute {
+    my ($cfg, $volid, $attribute, $value) = @_;
 
     my ($storeid, $volname) = parse_volume_id($volid);
     my $scfg = storage_config($cfg, $storeid);
     my $plugin = PVE::Storage::Plugin->lookup($scfg->{type});
 
-    $plugin->update_volume_notes($scfg, $storeid, $volname, $notes, $timeout);
+    return $plugin->update_volume_attribute($scfg, $storeid, $volname, $attribute, $value);
 }
 
 sub volume_size_info {
@@ -269,13 +269,13 @@ sub volume_resize {
 }
 
 sub volume_rollback_is_possible {
-    my ($cfg, $volid, $snap) = @_;
+    my ($cfg, $volid, $snap, $blockers) = @_;
 
     my ($storeid, $volname) = parse_volume_id($volid, 1);
     if ($storeid) {
         my $scfg = storage_config($cfg, $storeid);
         my $plugin = PVE::Storage::Plugin->lookup($scfg->{type});
-        return $plugin->volume_rollback_is_possible($scfg, $storeid, $volname, $snap);
+        return $plugin->volume_rollback_is_possible($scfg, $storeid, $volname, $snap, $blockers);
     } elsif ($volid =~ m|^(/.+)$| && -e $volid) {
         die "snapshot rollback file/device '$volid' is not possible\n";
     } else {
@@ -349,6 +349,7 @@ sub volume_snapshot_needs_fsfreeze {
 #            snapshot - taking a snapshot is possible
 #            sparseinit - volume is sparsely initialized
 #            template - conversion to base image is possible
+#            rename - renaming volumes is possible
 # $snap - check if the feature is supported for a given snapshot
 # $running - if the guest owning the volume is running
 # $opts - hash with further options:
@@ -371,20 +372,13 @@ sub volume_has_feature {
     }
 }
 
-sub volume_snapshot_list {
+sub volume_snapshot_info {
     my ($cfg, $volid) = @_;
 
-    my ($storeid, $volname) = parse_volume_id($volid, 1);
-    if ($storeid) {
-       my $scfg = storage_config($cfg, $storeid);
-       my $plugin = PVE::Storage::Plugin->lookup($scfg->{type});
-       return $plugin->volume_snapshot_list($scfg, $storeid, $volname);
-    } elsif ($volid =~ m|^(/.+)$| && -e $volid) {
-       die "send file/device '$volid' is not possible\n";
-    } else {
-       die "unable to parse volume ID '$volid'\n";
-    }
-    # return an empty array if dataset does not exist.
+    my ($storeid, $volname) = parse_volume_id($volid);
+    my $scfg = storage_config($cfg, $storeid);
+    my $plugin = PVE::Storage::Plugin->lookup($scfg->{type});
+    return $plugin->volume_snapshot_info($scfg, $storeid, $volname);
 }
 
 sub get_image_dir {
@@ -577,7 +571,7 @@ sub path_to_volume_id {
        } elsif ($path =~ m!^$isodir/([^/]+$iso_extension_re)$!) {
            my $name = $1;
            return ('iso', "$sid:iso/$name");
-       } elsif ($path =~ m!^$tmpldir/([^/]+\.tar\.gz)$!) {
+       } elsif ($path =~ m!^$tmpldir/([^/]+$vztmpl_extension_re)$!) {
            my $name = $1;
            return ('vztmpl', "$sid:vztmpl/$name");
        } elsif ($path =~ m!^$privatedir/(\d+)$!) {
@@ -609,22 +603,22 @@ sub path {
 }
 
 sub abs_filesystem_path {
-    my ($cfg, $volid) = @_;
+    my ($cfg, $volid, $allow_blockdev) = @_;
 
     my $path;
     if (parse_volume_id ($volid, 1)) {
        activate_volumes($cfg, [ $volid ]);
        $path = PVE::Storage::path($cfg, $volid);
     } else {
-       if (-f $volid) {
+       if (-f $volid || ($allow_blockdev && -b $volid)) {
            my $abspath = abs_path($volid);
            if ($abspath && $abspath =~ m|^(/.+)$|) {
                $path = $1; # untaint any path
            }
        }
     }
-
-    die "can't find file '$volid'\n" if !($path && -f $path);
+    die "can't find file '$volid'\n"
+       if !($path && (-f $path || ($allow_blockdev && -b $path)));
 
     return $path;
 }
@@ -692,7 +686,7 @@ sub storage_migrate {
 
     my $migration_snapshot;
     if (!defined($snapshot)) {
-       if ($scfg->{type} eq 'zfspool') {
+       if ($scfg->{type} eq 'zfspool' || $scfg->{type} eq 'btrfs') {
            $migration_snapshot = 1;
            $snapshot = '__migration__';
        }
@@ -716,7 +710,8 @@ sub storage_migrate {
     my $send = ['pvesm', 'export', $volid, $format, '-', '-with-snapshots', $with_snapshots];
     my $recv = [@$ssh, '--', 'pvesm', 'import', $target_volid, $format, $import_fn, '-with-snapshots', $with_snapshots];
     if (defined($snapshot)) {
-       push @$send, '-snapshot', $snapshot
+       push @$send, '-snapshot', $snapshot;
+       push @$recv, '-snapshot', $snapshot;
     }
     if ($migration_snapshot) {
        push @$recv, '-delete-snapshot', $snapshot;
@@ -726,7 +721,7 @@ sub storage_migrate {
     if (defined($base_snapshot)) {
        # Check if the snapshot exists on the remote side:
        push @$send, '-base', $base_snapshot;
-       push @$recv, '-base', $base_snapshot;
+       push @$recv, '-base', $base_snapshot if $target_apiver >= 9;
     }
 
     my $new_volid;
@@ -932,13 +927,13 @@ sub vdisk_free {
 }
 
 sub vdisk_list {
-    my ($cfg, $storeid, $vmid, $vollist) = @_;
+    my ($cfg, $storeid, $vmid, $vollist, $ctype) = @_;
 
     my $ids = $cfg->{ids};
 
     storage_check_enabled($cfg, $storeid) if ($storeid);
 
-    my $res = {};
+    my $res = $storeid ? { $storeid => [] } : {};
 
     # prepare/activate/refresh all storages
 
@@ -955,6 +950,7 @@ sub vdisk_list {
            next if $storeid && $storeid ne $sid;
            next if !storage_check_enabled($cfg, $sid, undef, 1);
            my $content = $ids->{$sid}->{content};
+           next if defined($ctype) && !$content->{$ctype};
            next if !($content->{rootdir} || $content->{images});
            push @$storage_list, $sid;
        }
@@ -964,10 +960,8 @@ sub vdisk_list {
 
     activate_storage_list($cfg, $storage_list, $cache);
 
-    # FIXME PVE 7.0: only scan storages with the correct content types
-    foreach my $sid (keys %$ids) {
+    for my $sid ($storage_list->@*) {
        next if $storeid && $storeid ne $sid;
-       next if !storage_check_enabled($cfg, $sid, undef, 1);
 
        my $scfg = $ids->{$sid};
        my $plugin = PVE::Storage::Plugin->lookup($scfg->{type});
@@ -1463,6 +1457,12 @@ sub decompressor_info {
     return $info;
 }
 
+sub protection_file_path {
+    my ($path) = @_;
+
+    return "${path}.protected";
+}
+
 sub archive_info {
     my ($archive) = shift;
     my $info;
@@ -1494,6 +1494,9 @@ sub archive_info {
 sub archive_remove {
     my ($archive_path) = @_;
 
+    die "cannot remove protected archive '$archive_path'\n"
+       if -e protection_file_path($archive_path);
+
     my $dirname = dirname($archive_path);
     my $archive_info = eval { archive_info($archive_path) } // {};
     my $logfn = $archive_info->{logfilename};
@@ -1560,7 +1563,7 @@ sub extract_vzdump_config_vma {
        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/) {
+           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";
@@ -1627,6 +1630,8 @@ sub prune_backups {
        $keep = PVE::JSONSchema::parse_property_string('prune-backups', $scfg->{'prune-backups'});
     }
 
+    activate_storage($cfg, $storeid);
+
     my $plugin = PVE::Storage::Plugin->lookup($scfg->{type});
     return $plugin->prune_backups($scfg, $storeid, $keep, $vmid, $type, $dryrun, $logfunc);
 }
@@ -1664,11 +1669,12 @@ my $prune_mark = sub {
 sub prune_mark_backup_group {
     my ($backup_group, $keep) = @_;
 
-    my $keep_all = delete $keep->{'keep-all'};
+    my @positive_opts = grep { $_ ne 'keep-all' && $keep->{$_} > 0 } keys $keep->%*;
 
-    if ($keep_all || !scalar(grep {$_ > 0} values %{$keep})) {
-       $keep = { 'keep-all' => 1 } if $keep_all;
+    if ($keep->{'keep-all'} || scalar(@positive_opts) == 0) {
        foreach my $prune_entry (@{$backup_group}) {
+           # preserve additional information like 'protected'
+           next if $prune_entry->{mark} && $prune_entry->{mark} ne 'remove';
            $prune_entry->{mark} = 'keep';
        }
        return;
@@ -1713,7 +1719,7 @@ sub prune_mark_backup_group {
     }
 }
 
-sub volume_export {
+sub volume_export : prototype($$$$$$$) {
     my ($cfg, $fh, $volid, $format, $snapshot, $base_snapshot, $with_snapshots) = @_;
 
     my ($storeid, $volname) = parse_volume_id($volid, 1);
@@ -1724,18 +1730,27 @@ sub volume_export {
                                   $snapshot, $base_snapshot, $with_snapshots);
 }
 
-sub volume_import {
-    my ($cfg, $fh, $volid, $format, $base_snapshot, $with_snapshots, $allow_rename) = @_;
+sub volume_import : prototype($$$$$$$$) {
+    my ($cfg, $fh, $volid, $format, $snapshot, $base_snapshot, $with_snapshots, $allow_rename) = @_;
 
     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, $allow_rename) // $volid;
-}
-
-sub volume_export_formats {
+    return $plugin->volume_import(
+       $scfg,
+       $storeid,
+       $fh,
+       $volname,
+       $format,
+       $snapshot,
+       $base_snapshot,
+       $with_snapshots,
+       $allow_rename,
+    ) // $volid;
+}
+
+sub volume_export_formats : prototype($$$$$) {
     my ($cfg, $volid, $snapshot, $base_snapshot, $with_snapshots) = @_;
 
     my ($storeid, $volname) = parse_volume_id($volid, 1);
@@ -1747,21 +1762,27 @@ sub volume_export_formats {
                                           $with_snapshots);
 }
 
-sub volume_import_formats {
-    my ($cfg, $volid, $base_snapshot, $with_snapshots) = @_;
+sub volume_import_formats : prototype($$$$$) {
+    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_import_formats($scfg, $storeid, $volname,
-                                          $base_snapshot, $with_snapshots);
+    return $plugin->volume_import_formats(
+       $scfg,
+       $storeid,
+       $volname,
+       $snapshot,
+       $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_formats = volume_import_formats($cfg, $dst_volid, $snapshot, $base_snapshot, $with_snapshots);
     my %import_hash = map { $_ => 1 } @import_formats;
     my @common = grep { $import_hash{$_} } @export_formats;
     return @common;
@@ -1840,6 +1861,26 @@ sub complete_volume {
     return $res;
 }
 
+sub rename_volume {
+    my ($cfg, $source_volid, $target_vmid, $target_volname) = @_;
+
+    die "no source volid provided\n" if !$source_volid;
+    die "no target VMID or target volname provided\n" if !$target_vmid && !$target_volname;
+
+    my ($storeid, $source_volname) = parse_volume_id($source_volid);
+
+    activate_storage($cfg, $storeid);
+
+    my $scfg = storage_config($cfg, $storeid);
+    my $plugin = PVE::Storage::Plugin->lookup($scfg->{type});
+
+    $target_vmid = ($plugin->parse_volname($source_volname))[3] if !$target_vmid;
+
+    return $plugin->cluster_lock_storage($storeid, $scfg->{shared}, undef, sub {
+       return $plugin->rename_volume($scfg, $storeid, $source_volname, $target_vmid, $target_volname);
+    });
+}
+
 # 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
@@ -1849,7 +1890,7 @@ 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
+    # $operation limit and should update $override for every limit affecting
     # us.
     my $use_global_limits = 0;
     my $apply_limit = sub {
@@ -1928,4 +1969,16 @@ sub assert_sid_unused {
     return undef;
 }
 
+# removes leading/trailing spaces and (back)slashes completely
+# substitutes every non-ASCII-alphanumerical char with '_', except '_.-'
+sub normalize_content_filename {
+    my ($filename) = @_;
+
+    chomp $filename;
+    $filename =~ s/^.*[\/\\]//;
+    $filename =~ s/[^a-zA-Z0-9_.-]/_/g;
+
+    return $filename;
+}
+
 1;