X-Git-Url: https://git.proxmox.com/?a=blobdiff_plain;f=PVE%2FStorage%2FPlugin.pm;h=080835f68109338c3daa6e8193dd49cbab2bec2b;hb=af50c2e67101c8242da5a9837387c6e6a13510b6;hp=6e321aeee6d5a0057947133dbb6fd3aeb710227a;hpb=3353698f4520b77b2d929eeae2b7f52dce142fc6;p=pve-storage.git diff --git a/PVE/Storage/Plugin.pm b/PVE/Storage/Plugin.pm index 6e321ae..080835f 100644 --- a/PVE/Storage/Plugin.pm +++ b/PVE/Storage/Plugin.pm @@ -19,6 +19,8 @@ use base qw(PVE::SectionConfig); use constant COMPRESSOR_RE => 'gz|lzo|zst'; +use constant NOTES_EXT => ".notes"; + our @COMMON_TAR_FLAGS = qw( --one-file-system -p --sparse --numeric-owner --acls @@ -35,7 +37,9 @@ our @SHARED_STORAGE = ( 'iscsidirect', 'glusterfs', 'zfs', - 'drbd'); + 'drbd', + 'pbs', +); our $MAX_VOLUMES_PER_GUEST = 1024; @@ -49,45 +53,55 @@ my %prune_option = ( format_description => 'N', ); -my $prune_backups_format = { - 'keep-last' => { - %prune_option, - description => 'Keep the last backups.', - }, - 'keep-hourly' => { - %prune_option, - description => 'Keep backups for the last different hours. If there is more' . - 'than one backup for a single hour, only the latest one is kept.' - }, - 'keep-daily' => { - %prune_option, - description => 'Keep backups for the last different days. If there is more' . - 'than one backup for a single day, only the latest one is kept.' - }, - 'keep-weekly' => { - %prune_option, - description => 'Keep backups for the last different weeks. If there is more' . - 'than one backup for a single week, only the latest one is kept.' - }, - 'keep-monthly' => { - %prune_option, - description => 'Keep backups for the last different months. If there is more' . - 'than one backup for a single month, only the latest one is kept.' - }, - 'keep-yearly' => { - %prune_option, - description => 'Keep backups for the last different years. If there is more' . - 'than one backup for a single year, only the latest one is kept.' - }, +our $prune_backups_format = { + 'keep-all' => { + type => 'boolean', + description => 'Keep all backups. Conflicts with the other options when true.', + optional => 1, + }, + 'keep-last' => { + %prune_option, + description => 'Keep the last backups.', + }, + 'keep-hourly' => { + %prune_option, + description => 'Keep backups for the last different hours. If there is more' . + 'than one backup for a single hour, only the latest one is kept.' + }, + 'keep-daily' => { + %prune_option, + description => 'Keep backups for the last different days. If there is more' . + 'than one backup for a single day, only the latest one is kept.' + }, + 'keep-weekly' => { + %prune_option, + description => 'Keep backups for the last different weeks. If there is more' . + 'than one backup for a single week, only the latest one is kept.' + }, + 'keep-monthly' => { + %prune_option, + description => 'Keep backups for the last different months. If there is more' . + 'than one backup for a single month, only the latest one is kept.' + }, + 'keep-yearly' => { + %prune_option, + description => 'Keep backups for the last different years. If there is more' . + 'than one backup for a single year, only the latest one is kept.' + }, }; PVE::JSONSchema::register_format('prune-backups', $prune_backups_format, \&validate_prune_backups); sub validate_prune_backups { - my ($keep) = @_; + my ($prune_backups) = @_; - die "at least one keep-option must be set and positive\n" - if !grep { $_ } values %{$keep}; + my $keep_all = delete $prune_backups->{'keep-all'}; + + if (!scalar(grep {$_ > 0} values %{$prune_backups})) { + $prune_backups = { 'keep-all' => 1 }; + } elsif ($keep_all) { + die "keep-all cannot be set together with other options.\n"; + } - return $keep; + return $prune_backups; } register_standard_option('prune-backups', { description => "The retention options with shorter intervals are processed first " . @@ -119,7 +133,8 @@ my $defaultData = { optional => 1, }, maxfiles => { - description => "Maximal number of backup files per VM. Use '0' for unlimted.", + description => "Deprecated: use 'prune-backups' instead. " . + "Maximal number of backup files per VM. Use '0' for unlimited.", type => 'integer', minimum => 0, optional => 1, @@ -330,6 +345,10 @@ sub decode_value { die "unable to combine 'none' with other content types\n"; } + if (scalar(keys $res->%*) == 0 && !$valid_content->{none}) { + die "storage does not support content type 'none'\n"; + } + return $res; } elsif ($key eq 'format') { my $valid_formats = $def->{format}->[0]; @@ -388,8 +407,15 @@ sub parse_config { type => 'dir', priority => 0, # force first entry path => '/var/lib/vz', - maxfiles => 0, - content => { images => 1, rootdir => 1, vztmpl => 1, iso => 1, snippets => 1}, + 'prune-backups' => 'keep-all=1', + content => { + backup => 1, + images => 1, + iso => 1, + rootdir => 1, + snippets => 1, + vztmpl => 1, + }, }; } @@ -419,12 +445,13 @@ sub parse_config { # Storage implementation # called during addition of storage (before the new storage config got written) -# die to abort additon if there are (grave) problems +# die to abort addition if there are (grave) problems # NOTE: runs in a storage config *locked* context sub on_add_hook { my ($class, $storeid, $scfg, %param) = @_; # do nothing by default + return undef; } # called during storage configuration update (before the updated storage config got written) @@ -434,6 +461,7 @@ sub on_update_hook { my ($class, $storeid, $scfg, %param) = @_; # do nothing by default + return undef; } # called during deletion of storage (before the new storage config got written) @@ -445,6 +473,7 @@ sub on_delete_hook { my ($class, $storeid, $scfg) = @_; # do nothing by default + return undef; } sub cluster_lock_storage { @@ -489,7 +518,7 @@ sub parse_volname { return ('images', $name, $vmid, undef, undef, $isBase, $format); } elsif ($volname =~ m!^iso/([^/]+$PVE::Storage::iso_extension_re)$!) { return ('iso', $1); - } elsif ($volname =~ m!^vztmpl/([^/]+\.tar\.[gx]z)$!) { + } elsif ($volname =~ m!^vztmpl/([^/]+$PVE::Storage::vztmpl_extension_re)$!) { return ('vztmpl', $1); } elsif ($volname =~ m!^rootdir/(\d+)$!) { return ('rootdir', $1, $1); @@ -524,7 +553,7 @@ sub get_subdir { my $path = $scfg->{path}; - die "storage definintion has no path\n" if !$path; + die "storage definition has no path\n" if !$path; my $subdir = $vtype_subdirs->{$vtype}; @@ -660,7 +689,7 @@ sub clone_image { my ($class, $scfg, $storeid, $volname, $vmid, $snap) = @_; # this only works for file based storage types - die "storage definintion has no path\n" if !$scfg->{path}; + die "storage definition has no path\n" if !$scfg->{path}; my ($vtype, $basename, $basevmid, undef, undef, $isBase, $format) = $class->parse_volname($volname); @@ -678,7 +707,7 @@ sub clone_image { mkpath $imagedir; - my $name = $class->find_free_diskname($imagedir, $scfg, $vmid, "qcow2", 1); + my $name = $class->find_free_diskname($storeid, $scfg, $vmid, "qcow2", 1); warn "clone $volname: $vtype, $name, $vmid to $name (base=../$basevmid/$basename)\n"; @@ -691,7 +720,7 @@ sub clone_image { local $CWD = $imagedir; my $cmd = ['/usr/bin/qemu-img', 'create', '-b', "../$basevmid/$basename", - '-f', 'qcow2', $path]; + '-F', $format, '-f', 'qcow2', $path]; run_command($cmd); }; @@ -710,7 +739,7 @@ sub alloc_image { mkpath $imagedir; - $name = $class->find_free_diskname($imagedir, $scfg, $vmid, $fmt, 1) if !$name; + $name = $class->find_free_diskname($storeid, $scfg, $vmid, $fmt, 1) if !$name; my (undef, $tmpfmt) = parse_name_dir($name); @@ -783,6 +812,12 @@ sub file_size_info { my $st = File::stat::stat($filename); + if (!defined($st)) { + my $extramsg = -l $filename ? ' - dangling symlink?' : ''; + warn "failed to stat '$filename'$extramsg\n"; + return undef; + } + if (S_ISDIR($st->mode)) { return wantarray ? (0, 'subvol', 0, undef, $st->ctime) : 1; } @@ -798,13 +833,34 @@ sub file_size_info { warn $@ if $@; my $info = eval { decode_json($json) }; - warn "could not parse qemu-img info command output for '$filename'\n" if $@; + if (my $err = $@) { + warn "could not parse qemu-img info command output for '$filename' - $err\n"; + return wantarray ? (undef, undef, undef, undef, $st->ctime) : undef; + } 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 + ($used) = ($used =~ /^(\d+)$/) or die "used '$used' not an integer\n"; # untaint + ($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 + } return wantarray ? ($size, $format, $used, $parent, $st->ctime) : $size; } +sub get_volume_notes { + my ($class, $scfg, $storeid, $volname, $timeout) = @_; + + die "volume notes are not supported for $class"; +} + +sub update_volume_notes { + my ($class, $scfg, $storeid, $volname, $notes, $timeout) = @_; + + die "volume notes are not supported for $class"; +} + sub volume_size_info { my ($class, $scfg, $storeid, $volname, $timeout) = @_; my $path = $class->filesystem_path($scfg, $volname); @@ -882,6 +938,10 @@ sub volume_snapshot_delete { return undef; } +sub volume_snapshot_needs_fsfreeze { + + return 0; +} sub storage_can_replicate { my ($class, $scfg, $storeid, $format) = @_; @@ -978,7 +1038,6 @@ my $get_subdir_files = sub { my $res = []; foreach my $fn (<$path/*>) { - my $st = File::stat::stat($fn); next if (!$st || S_ISDIR($st->mode)); @@ -991,15 +1050,19 @@ my $get_subdir_files = sub { $info = { volid => "$sid:iso/$1", format => 'iso' }; } elsif ($tt eq 'vztmpl') { - next if $fn !~ m!/([^/]+\.tar\.([gx]z))$!; + next if $fn !~ m!/([^/]+$PVE::Storage::vztmpl_extension_re)$!; $info = { volid => "$sid:vztmpl/$1", format => "t$2" }; } elsif ($tt eq 'backup') { - next if defined($vmid) && $fn !~ m/\S+-$vmid-\S+/; next if $fn !~ m!/([^/]+\.(tgz|(?:(?:tar|vma)(?:\.(${\COMPRESSOR_RE}))?)))$!; + my $original = $fn; my $format = $2; $fn = $1; + + # only match for VMID now, to avoid false positives (VMID in parent directory name) + next if defined($vmid) && $fn !~ m/\S+-$vmid-\S+/; + $info = { volid => "$sid:backup/$fn", format => $format }; my $archive_info = eval { PVE::Storage::archive_info($fn) } // {}; @@ -1010,6 +1073,12 @@ my $get_subdir_files = sub { $info->{vmid} = $vmid // $1; } + 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); + } + } elsif ($tt eq 'snippets') { $info = { @@ -1082,7 +1151,7 @@ sub status { my $path = $scfg->{path}; - die "storage definintion has no path\n" if !$path; + die "storage definition has no path\n" if !$path; my $timeout = 2; my $res = PVE::Tools::df($path, $timeout); @@ -1106,7 +1175,7 @@ sub activate_storage { my $path = $scfg->{path}; - die "storage definintion has no path\n" if !$path; + die "storage definition has no path\n" if !$path; # this path test may hang indefinitely on unresponsive mounts my $timeout = 2; @@ -1174,6 +1243,75 @@ sub check_connection { return 1; } +sub prune_backups { + my ($class, $scfg, $storeid, $keep, $vmid, $type, $dryrun, $logfunc) = @_; + + $logfunc //= sub { print "$_[1]\n" }; + + my $backups = $class->list_volumes($storeid, $scfg, $vmid, ['backup']); + + my $backup_groups = {}; + my $prune_list = []; + + foreach my $backup (@{$backups}) { + my $volid = $backup->{volid}; + my $archive_info = eval { PVE::Storage::archive_info($volid) } // {}; + my $backup_type = $archive_info->{type} // 'unknown'; + my $backup_vmid = $archive_info->{vmid} // $backup->{vmid}; + + next if defined($type) && $type ne $backup_type; + + my $prune_entry = { + ctime => $backup->{ctime}, + type => $backup_type, + volid => $volid, + }; + + $prune_entry->{vmid} = $backup_vmid if defined($backup_vmid); + + if ($archive_info->{is_std_name}) { + die "internal error - got no VMID\n" if !defined($backup_vmid); + die "internal error - got wrong VMID '$backup_vmid' != '$vmid'\n" + if defined($vmid) && $backup_vmid ne $vmid; + + $prune_entry->{ctime} = $archive_info->{ctime}; + my $group = "$backup_type/$backup_vmid"; + push @{$backup_groups->{$group}}, $prune_entry; + } else { + # ignore backups that don't use the standard naming scheme + $prune_entry->{mark} = 'protected'; + } + + push @{$prune_list}, $prune_entry; + } + + foreach my $backup_group (values %{$backup_groups}) { + PVE::Storage::prune_mark_backup_group($backup_group, $keep); + } + + my $failed; + if (!$dryrun) { + foreach my $prune_entry (@{$prune_list}) { + next if $prune_entry->{mark} ne 'remove'; + + my $volid = $prune_entry->{volid}; + $logfunc->('info', "removing backup '$volid'"); + eval { + my (undef, $volname) = parse_volume_id($volid); + my $archive_path = $class->filesystem_path($scfg, $volname); + PVE::Storage::archive_remove($archive_path); + }; + if (my $err = $@) { + $logfunc->('err', "error when removing backup '$volid' - $err\n"); + $failed = 1; + } + } + } + die "error pruning backups - check log\n" if $failed; + + return $prune_list; +} + # Import/Export interface: # Any path based storage is assumed to support 'raw' and 'tar' streams, so # the default implementations will return this if $scfg->{path} is set,