X-Git-Url: https://git.proxmox.com/?a=blobdiff_plain;f=PVE%2FStorage%2FPlugin.pm;h=8a41df10b94b7112bf566561409305505fc8e67c;hb=4470f0cbe94c8b7a6c480012e8fd20d124c9c9bb;hp=fab231642c366dabf51a8d90723fc0c865c69514;hpb=b19ae5b47e134eaaad05cc1d0828dbf960ce4d24;p=pve-storage.git diff --git a/PVE/Storage/Plugin.pm b/PVE/Storage/Plugin.pm index fab2316..8a41df1 100644 --- a/PVE/Storage/Plugin.pm +++ b/PVE/Storage/Plugin.pm @@ -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( @@ -41,6 +43,19 @@ our @SHARED_STORAGE = ( 'pbs', ); +our $QCOW2_PREALLOCATION = { + off => 1, + metadata => 1, + falloc => 1, + full => 1, +}; + +our $RAW_PREALLOCATION = { + off => 1, + falloc => 1, + full => 1, +}; + our $MAX_VOLUMES_PER_GUEST = 1024; cfs_register_file ('storage.cfg', @@ -142,6 +157,13 @@ 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', @@ -152,6 +174,13 @@ my $defaultData = { type => 'string', format => 'pve-storage-format', optional => 1, }, + preallocation => { + description => "Preallocation mode for raw and qcow2 images. " . + "Using 'metadata' on raw images results in preallocation=off.", + type => 'string', enum => ['off', 'metadata', 'falloc', 'full'], + default => 'metadata', + optional => 1, + }, }, }; @@ -444,6 +473,31 @@ sub parse_config { return $cfg; } +sub preallocation_cmd_option { + my ($scfg, $fmt) = @_; + + my $prealloc = $scfg->{preallocation}; + + if ($fmt eq 'qcow2') { + $prealloc = $prealloc // 'metadata'; + + die "preallocation mode '$prealloc' not supported by format '$fmt'\n" + if !$QCOW2_PREALLOCATION->{$prealloc}; + + return "preallocation=$prealloc"; + } elsif ($fmt eq 'raw') { + $prealloc = $prealloc // 'off'; + $prealloc = 'off' if $prealloc eq 'metadata'; + + die "preallocation mode '$prealloc' not supported by format '$fmt'\n" + if !$RAW_PREALLOCATION->{$prealloc}; + + return "preallocation=$prealloc"; + } + + return; +} + # Storage implementation # called during addition of storage (before the new storage config got written) @@ -518,13 +572,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); @@ -764,7 +818,8 @@ sub alloc_image { } else { my $cmd = ['/usr/bin/qemu-img', 'create']; - push @$cmd, '-o', 'preallocation=metadata' if $fmt eq 'qcow2'; + my $prealloc_opt = preallocation_cmd_option($scfg, $fmt); + push @$cmd, '-o', $prealloc_opt if defined($prealloc_opt); push @$cmd, '-f', $fmt, $path, "${size}K"; @@ -782,6 +837,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) { @@ -843,7 +901,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 @@ -851,18 +913,53 @@ sub file_size_info { return wantarray ? ($size, $format, $used, $parent, $st->ctime) : $size; } +# FIXME remove on the next APIAGE reset. +# Deprecated, use get_volume_attribute instead. sub get_volume_notes { my ($class, $scfg, $storeid, $volname, $timeout) = @_; die "volume notes are not supported for $class"; } +# FIXME remove on the next APIAGE reset. +# Deprecated, use update_volume_attribute instead. sub update_volume_notes { my ($class, $scfg, $storeid, $volname, $notes, $timeout) = @_; die "volume notes are not supported for $class"; } +# Returns undef if the attribute is not supported for the volume. +# 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) = @_; + + if ($attribute eq 'notes') { + my $notes = eval { $class->get_volume_notes($scfg, $storeid, $volname); }; + if (my $err = $@) { + return if $err =~ m/^volume notes are not supported/; + die $err; + } + return $notes; + } + + return; +} + +# Dies if the attribute is not supported for the volume. +sub update_volume_attribute { + my ($class, $scfg, $storeid, $volname, $attribute, $value) = @_; + + if ($attribute eq 'notes') { + $class->update_volume_notes($scfg, $storeid, $volname, $value); + } + + die "attribute '$attribute' is not supported for storage type '$scfg->{type}'\n"; +} + sub volume_size_info { my ($class, $scfg, $storeid, $volname, $timeout) = @_; my $path = $class->filesystem_path($scfg, $volname); @@ -902,8 +999,11 @@ sub volume_snapshot { return undef; } +# Asserts that a rollback to $snap on $volname is possible. +# If certain snapshots are preventing the rollback and $blockers is an array +# reference, the snapshot names can be pushed onto $blockers prior to dying. sub volume_rollback_is_possible { - my ($class, $scfg, $storeid, $volname, $snap) = @_; + my ($class, $scfg, $storeid, $volname, $snap, $blockers) = @_; return 1; } @@ -954,23 +1054,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){ @@ -1047,17 +1167,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; @@ -1070,6 +1190,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; @@ -1078,9 +1199,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 = { @@ -1098,6 +1220,8 @@ my $get_subdir_files = sub { return $res; }; +# If attributes are set on a volume, they should be included in the result. +# See get_volume_attribute for a list of possible attributes. sub list_volumes { my ($class, $storeid, $scfg, $vmid, $content_types) = @_; @@ -1163,13 +1287,14 @@ sub status { return ($res->{total}, $res->{avail}, $res->{used}, 1); } -sub volume_snapshot_list { +# Returns a hash with the snapshot names as keys and the following data: +# id - Unique id to distinguish different snapshots even if the have the same name. +# timestamp - Creation time of the snapshot (seconds since epoch). +# Returns an empty hash if the volume does not exist. +sub volume_snapshot_info { my ($class, $scfg, $storeid, $volname) = @_; - # implement in subclass - die "Volume_snapshot_list is not implemented for $class"; - - # return an empty array if dataset does not exist. + die "volume_snapshot_info is not implemented for $class"; } sub activate_storage { @@ -1281,9 +1406,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; } @@ -1485,4 +1612,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;