]> git.proxmox.com Git - pve-storage.git/blobdiff - PVE/Storage/Plugin.pm
cifs: small line bloat reduction
[pve-storage.git] / PVE / Storage / Plugin.pm
index 84de7855fcca68713593fa64c9805e868297a987..ca7a0d4ed4dcc8f50043caf1e726c0dd56e509b7 100644 (file)
@@ -3,6 +3,7 @@ package PVE::Storage::Plugin;
 use strict;
 use warnings;
 
+use Encode qw(decode);
 use Fcntl ':mode';
 use File::chdir;
 use File::Path;
@@ -19,6 +20,7 @@ use base qw(PVE::SectionConfig);
 
 use constant COMPRESSOR_RE => 'gz|lzo|zst';
 
+use constant LOG_EXT => ".log";
 use constant NOTES_EXT => ".notes";
 
 our @COMMON_TAR_FLAGS = qw(
@@ -155,11 +157,23 @@ my $defaultData = {
            optional => 1,
        },
        'prune-backups' => get_standard_option('prune-backups'),
+       'max-protected-backups' => {
+           description => "Maximal number of protected backups per guest. Use '-1' for unlimited.",
+           type => 'integer',
+           minimum => -1,
+           optional => 1,
+           default => "Unlimited for users with Datastore.Allocate privilege, 5 for other users",
+       },
        shared => {
            description => "Mark storage as shared.",
            type => 'boolean',
            optional => 1,
        },
+       subdir => {
+           description => "Subdir to mount.",
+           type => 'string', format => 'pve-storage-path',
+           optional => 1,
+       },
        'format' => {
            description => "Default image format.",
            type => 'string', format => 'pve-storage-format',
@@ -172,6 +186,11 @@ my $defaultData = {
            default => 'metadata',
            optional => 1,
        },
+       'content-dirs' => {
+           description => "Overrides for default content type directories.",
+           type => "string", format => "pve-dir-override-list",
+           optional => 1,
+       },
     },
 };
 
@@ -196,6 +215,12 @@ sub valid_content_types {
     return $def->{content}->[0];
 }
 
+sub dirs_hash_to_string {
+    my $hash = shift;
+
+    return join(',', map { "$_=$hash->{$_}" } sort keys %$hash);
+}
+
 sub default_format {
     my ($scfg) = @_;
 
@@ -326,6 +351,18 @@ sub parse_volume_id {
     die "unable to parse volume ID '$volid'\n";
 }
 
+PVE::JSONSchema::register_format('pve-dir-override', \&verify_dir_override);
+sub verify_dir_override {
+    my ($value, $noerr) = @_;
+
+    if($value =~ m/^([a-z]+)=\/.+$/ &&
+       verify_content($1, $noerr)) {
+       return $value;
+    }
+
+    return undef if $noerr;
+    die "invalid override '$value'\n";
+}
 
 sub private {
     return $defaultData;
@@ -396,6 +433,22 @@ sub decode_value {
        #    die "storage '$storeid' does not allow node restrictions\n";
        #}
 
+       return $res;
+    } elsif ($key eq 'content-dirs') {
+       my $valid_content = $def->{content}->[0];
+       my $res = {};
+
+       foreach my $dir (PVE::Tools::split_list($value)) {
+           my ($content, $path) = split(/=/, $dir, 2);
+
+           if (!$valid_content->{$content}) {
+               warn "storage does not support content type '$content'\n";
+               next;
+           }
+
+           $res->{$content} = $path;
+       }
+
        return $res;
     }
 
@@ -410,6 +463,9 @@ sub encode_value {
     } elsif ($key eq 'content') {
        my $res = content_hash_to_string($value) || 'none';
        return $res;
+    } elsif ($key eq 'content-dirs') {
+       my $res = dirs_hash_to_string($value);
+       return $res;
     }
 
     return $value;
@@ -563,13 +619,13 @@ sub parse_volname {
        my ($vmid, $name) = ($1, $2);
        my (undef, $format, $isBase) = parse_name_dir($name);
        return ('images', $name, $vmid, undef, undef, $isBase, $format);
-    } elsif ($volname =~ m!^iso/([^/]+$PVE::Storage::iso_extension_re)$!) {
+    } elsif ($volname =~ m!^iso/([^/]+$PVE::Storage::ISO_EXT_RE_0)$!) {
        return ('iso', $1);
-    } elsif ($volname =~ m!^vztmpl/([^/]+$PVE::Storage::vztmpl_extension_re)$!) {
+    } elsif ($volname =~ m!^vztmpl/([^/]+$PVE::Storage::VZTMPL_EXT_RE_1)$!) {
        return ('vztmpl', $1);
     } elsif ($volname =~ m!^rootdir/(\d+)$!) {
        return ('rootdir', $1, $1);
-    } elsif ($volname =~ m!^backup/([^/]+(?:\.(?:tgz|(?:(?:tar|vma)(?:\.(?:${\COMPRESSOR_RE}))?))))$!) {
+    } elsif ($volname =~ m!^backup/([^/]+$PVE::Storage::BACKUP_EXT_RE_2)$!) {
        my $fn = $1;
        if ($fn =~ m/^vzdump-(openvz|lxc|qemu)-(\d+)-.+/) {
            return ('backup', $fn, $2);
@@ -601,12 +657,11 @@ sub get_subdir {
     my $path = $scfg->{path};
 
     die "storage definition has no path\n" if !$path;
+    die "unknown vtype '$vtype'\n" if !exists($vtype_subdirs->{$vtype});
 
-    my $subdir = $vtype_subdirs->{$vtype};
+    my $subdir = $scfg->{"content-dirs"}->{$vtype} // "/".$vtype_subdirs->{$vtype};
 
-    die "unknown vtype '$vtype'\n" if !defined($subdir);
-
-    return "$path/$subdir";
+    return $path.$subdir;
 }
 
 sub filesystem_path {
@@ -828,6 +883,9 @@ sub alloc_image {
 sub free_image {
     my ($class, $storeid, $scfg, $volname, $isBase, $format) = @_;
 
+    die "cannot remove protected volume '$volname' on '$storeid'\n"
+       if $class->get_volume_attribute($scfg, $storeid, $volname, 'protected');
+
     my $path = $class->filesystem_path($scfg, $volname);
 
     if ($isBase) {
@@ -889,7 +947,11 @@ sub file_size_info {
     my ($size, $format, $used, $parent) = $info->@{qw(virtual-size format actual-size backing-filename)};
 
     ($size) = ($size =~ /^(\d+)$/) or die "size '$size' not an integer\n"; # untaint
+    # coerce back from string
+    $size = int($size);
     ($used) = ($used =~ /^(\d+)$/) or die "used '$used' not an integer\n"; # untaint
+    # coerce back from string
+    $used = int($used);
     ($format) = ($format =~ /^(\S+)$/) or die "format '$format' includes whitespace\n"; # untaint
     if (defined($parent)) {
        ($parent) = ($parent =~ /^(\S+)$/) or die "parent '$parent' includes whitespace\n"; # untaint
@@ -917,6 +979,7 @@ sub update_volume_notes {
 # Should die if there is an error fetching the attribute.
 # Possible attributes:
 # notes     - user-provided comments/notes.
+# protected - not to be removed by free_image, and for backups, ignored when pruning.
 sub get_volume_attribute {
     my ($class, $scfg, $storeid, $volname, $attribute) = @_;
 
@@ -1037,23 +1100,43 @@ sub volume_has_feature {
     my ($class, $scfg, $feature, $storeid, $volname, $snapname, $running, $opts) = @_;
 
     my $features = {
-       snapshot => { current => { qcow2 => 1}, snap => { qcow2 => 1} },
-       clone => { base => {qcow2 => 1, raw => 1, vmdk => 1} },
-       template => { current => {qcow2 => 1, raw => 1, vmdk => 1, subvol => 1} },
-       copy => { base => {qcow2 => 1, raw => 1, vmdk => 1},
-                 current => {qcow2 => 1, raw => 1, vmdk => 1},
-                 snap => {qcow2 => 1} },
-       sparseinit => { base => {qcow2 => 1, raw => 1, vmdk => 1},
-                       current => {qcow2 => 1, raw => 1, vmdk => 1} },
+       snapshot => {
+           current => { qcow2 => 1 },
+           snap => { qcow2 => 1 },
+       },
+       clone => {
+           base => { qcow2 => 1, raw => 1, vmdk => 1 },
+       },
+       template => {
+           current => { qcow2 => 1, raw => 1, vmdk => 1, subvol => 1 },
+       },
+       copy => {
+           base => { qcow2 => 1, raw => 1, vmdk => 1 },
+           current => { qcow2 => 1, raw => 1, vmdk => 1 },
+           snap => { qcow2 => 1 },
+       },
+       sparseinit => {
+           base => { qcow2 => 1, raw => 1, vmdk => 1 },
+           current => { qcow2 => 1, raw => 1, vmdk => 1 },
+       },
+       rename => {
+           current => {qcow2 => 1, raw => 1, vmdk => 1},
+       },
     };
 
-    # clone_image creates a qcow2 volume
-    return 0 if $feature eq 'clone' &&
-               defined($opts->{valid_target_formats}) &&
-               !(grep { $_ eq 'qcow2' } @{$opts->{valid_target_formats}});
+    if ($feature eq 'clone') {
+       if (
+           defined($opts->{valid_target_formats})
+           && !(grep { $_ eq 'qcow2' } @{$opts->{valid_target_formats}})
+       ) {
+           return 0; # clone_image creates a qcow2 volume
+       }
+    } elsif ($feature eq 'rename') {
+       return 0 if $class->can('api') && $class->api() < 10;
+    }
 
-    my ($vtype, $name, $vmid, $basename, $basevmid, $isBase, $format) =
-       $class->parse_volname($volname);
+
+    my ($vtype, $name, $vmid, $basename, $basevmid, $isBase, $format) = $class->parse_volname($volname);
 
     my $key = undef;
     if($snapname){
@@ -1130,17 +1213,17 @@ my $get_subdir_files = sub {
        my $info;
 
        if ($tt eq 'iso') {
-           next if $fn !~ m!/([^/]+$PVE::Storage::iso_extension_re)$!i;
+           next if $fn !~ m!/([^/]+$PVE::Storage::ISO_EXT_RE_0)$!i;
 
            $info = { volid => "$sid:iso/$1", format => 'iso' };
 
        } elsif ($tt eq 'vztmpl') {
-           next if $fn !~ m!/([^/]+$PVE::Storage::vztmpl_extension_re)$!;
+           next if $fn !~ m!/([^/]+$PVE::Storage::VZTMPL_EXT_RE_1)$!;
 
            $info = { volid => "$sid:vztmpl/$1", format => "t$2" };
 
        } elsif ($tt eq 'backup') {
-           next if $fn !~ m!/([^/]+\.(tgz|(?:(?:tar|vma)(?:\.(${\COMPRESSOR_RE}))?)))$!;
+           next if $fn !~ m!/([^/]+$PVE::Storage::BACKUP_EXT_RE_2)$!;
            my $original = $fn;
            my $format = $2;
            $fn = $1;
@@ -1153,6 +1236,7 @@ my $get_subdir_files = sub {
            my $archive_info = eval { PVE::Storage::archive_info($fn) } // {};
 
            $info->{ctime} = $archive_info->{ctime} if defined($archive_info->{ctime});
+           $info->{subtype} = $archive_info->{type} // 'unknown';
 
            if (defined($vmid) || $fn =~ m!\-([1-9][0-9]{2,8})\-[^/]+\.${format}$!) {
                $info->{vmid} = $vmid // $1;
@@ -1161,9 +1245,10 @@ my $get_subdir_files = sub {
            my $notes_fn = $original.NOTES_EXT;
            if (-f $notes_fn) {
                my $notes = PVE::Tools::file_read_firstline($notes_fn);
-               $info->{notes} = $notes if defined($notes);
+               $info->{notes} = eval { decode('UTF-8', $notes, 1) } // $notes if defined($notes);
            }
 
+           $info->{protected} = 1 if -e PVE::Storage::protection_file_path($original);
        } elsif ($tt eq 'snippets') {
 
            $info = {
@@ -1367,9 +1452,11 @@ sub prune_backups {
            push @{$backup_groups->{$group}}, $prune_entry;
        } else {
            # ignore backups that don't use the standard naming scheme
-           $prune_entry->{mark} = 'protected';
+           $prune_entry->{mark} = 'renamed';
        }
 
+       $prune_entry->{mark} = 'protected' if $backup->{protected};
+
        push @{$prune_list}, $prune_entry;
     }
 
@@ -1571,4 +1658,39 @@ sub volume_import_formats {
     return ();
 }
 
+sub rename_volume {
+    my ($class, $scfg, $storeid, $source_volname, $target_vmid, $target_volname) = @_;
+    die "not implemented in storage plugin '$class'\n" if $class->can('api') && $class->api() < 10;
+    die "no path found\n" if !$scfg->{path};
+
+    my (
+       undef,
+       $source_image,
+       $source_vmid,
+       $base_name,
+       $base_vmid,
+       undef,
+       $format
+    ) = $class->parse_volname($source_volname);
+
+    $target_volname = $class->find_free_diskname($storeid, $scfg, $target_vmid, $format, 1)
+       if !$target_volname;
+
+    my $basedir = $class->get_subdir($scfg, 'images');
+
+    mkpath "${basedir}/${target_vmid}";
+
+    my $old_path = "${basedir}/${source_vmid}/${source_image}";
+    my $new_path = "${basedir}/${target_vmid}/${target_volname}";
+
+    die "target volume '${target_volname}' already exists\n" if -e $new_path;
+
+    my $base = $base_name ? "${base_vmid}/${base_name}/" : '';
+
+    rename($old_path, $new_path) ||
+       die "rename '$old_path' to '$new_path' failed - $!\n";
+
+    return "${storeid}:${base}${target_vmid}/${target_volname}";
+}
+
 1;