]> git.proxmox.com Git - pve-storage.git/blobdiff - PVE/Storage.pm
prune mark: correctly keep track of already included backups
[pve-storage.git] / PVE / Storage.pm
index edf9a2ecd6651baf89839d8d78d4c411f4fba59b..c1a21b43d96adfa7e587c87b2b3828fa57df1d85 100755 (executable)
@@ -8,6 +8,7 @@ use POSIX;
 use IO::Select;
 use IO::File;
 use IO::Socket::IP;
+use IPC::Open3;
 use File::Basename;
 use File::Path;
 use Cwd 'abs_path';
@@ -39,12 +40,12 @@ use PVE::Storage::ZFSPlugin;
 use PVE::Storage::DRBDPlugin;
 use PVE::Storage::PBSPlugin;
 
-# Storage API version. Icrement it on changes in storage API interface.
-use constant APIVER => 5;
+# Storage API version. Increment it on changes in storage API interface.
+use constant APIVER => 8;
 # 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 => 4;
+use constant APIAGE => 7;
 
 # load standard plugins
 PVE::Storage::DirPlugin->register();
@@ -128,6 +129,28 @@ sub lock_storage_config {
     }
 }
 
+# FIXME remove maxfiles for PVE 7.0
+my $convert_maxfiles_to_prune_backups = sub {
+    my ($scfg) = @_;
+
+    return if !$scfg;
+
+    my $maxfiles = delete $scfg->{maxfiles};
+
+    if (!defined($scfg->{'prune-backups'}) && defined($maxfiles)) {
+       my $prune_backups;
+       if ($maxfiles) {
+           $prune_backups = { 'keep-last' => $maxfiles };
+       } else { # maxfiles 0 means no limit
+           $prune_backups = { 'keep-all' => 1 };
+       }
+       $scfg->{'prune-backups'} = PVE::JSONSchema::print_property_string(
+           $prune_backups,
+           'prune-backups'
+       );
+    }
+};
+
 sub storage_config {
     my ($cfg, $storeid, $noerr) = @_;
 
@@ -137,6 +160,8 @@ sub storage_config {
 
     die "storage '$storeid' does not exist\n" if (!$noerr && !$scfg);
 
+    $convert_maxfiles_to_prune_backups->($scfg);
+
     return $scfg;
 }
 
@@ -192,6 +217,26 @@ sub file_size_info {
     return PVE::Storage::Plugin::file_size_info($filename, $timeout);
 }
 
+sub get_volume_notes {
+    my ($cfg, $volid, $timeout) = @_;
+
+    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);
+}
+
+sub update_volume_notes {
+    my ($cfg, $volid, $notes, $timeout) = @_;
+
+    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);
+}
+
 sub volume_size_info {
     my ($cfg, $volid, $timeout) = @_;
 
@@ -286,6 +331,18 @@ sub volume_snapshot_delete {
     }
 }
 
+# check if a filesystem on top of a volume needs to flush its journal for
+# consistency (see fsfreeze(8)) before a snapshot is taken - needed for
+# container mountpoints
+sub volume_snapshot_needs_fsfreeze {
+    my ($cfg, $volid) = @_;
+
+    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_needs_fsfreeze();
+}
+
 # check if a volume or snapshot supports a given feature
 # $feature - one of:
 #            clone - linked clone is possible
@@ -637,8 +694,11 @@ sub storage_migrate {
     my $ssh_base = PVE::SSHInfo::ssh_info_to_command_base($target_sshinfo);
     local $ENV{RSYNC_RSH} = PVE::Tools::cmd2string($ssh_base);
 
-    my @cstream = ([ '/usr/bin/cstream', '-t', $ratelimit_bps ])
-       if defined($ratelimit_bps);
+    my @cstream;
+    if (defined($ratelimit_bps)) {
+       @cstream = ([ '/usr/bin/cstream', '-t', $ratelimit_bps ]);
+       $logfunc->("using a bandwidth limit of $ratelimit_bps bps for transferring '$volid'") if $logfunc;
+    }
 
     my $migration_snapshot;
     if (!defined($snapshot)) {
@@ -693,17 +753,31 @@ sub storage_migrate {
     };
 
     volume_snapshot($cfg, $volid, $snapshot) if $migration_snapshot;
+
+    if (defined($snapshot)) {
+       activate_volumes($cfg, [$volid], $snapshot);
+    } else {
+       activate_volumes($cfg, [$volid]);
+    }
+
     eval {
        if ($insecure) {
-           open(my $info, '-|', @$recv)
+           my $input = IO::File->new();
+           my $info = IO::File->new();
+           open3($input, $info, $info, @{$recv})
                or die "receive command failed: $!\n";
+           close($input);
+
            my ($ip) = <$info> =~ /^($PVE::Tools::IPRE)$/ or die "no tunnel IP received\n";
            my ($port) = <$info> =~ /^(\d+)$/ or die "no tunnel port received\n";
            my $socket = IO::Socket::IP->new(PeerHost => $ip, PeerPort => $port, Type => SOCK_STREAM)
                or die "failed to connect to tunnel at $ip:$port\n";
            # we won't be reading from the socket
            shutdown($socket, 0);
-           run_command([$send, @cstream], output => '>&'.fileno($socket), errfunc => $logfunc);
+
+           eval { run_command([$send, @cstream], output => '>&'.fileno($socket), errfunc => $logfunc); };
+           my $send_error = $@;
+
            # don't close the connection entirely otherwise the receiving end
            # might not get all buffered data (and fails with 'connection reset by peer')
            shutdown($socket, 1);
@@ -719,6 +793,8 @@ sub storage_migrate {
                die "import failed: $!\n" if $!;
                die "import failed: exit code ".($?>>8)."\n";
            }
+
+           die $send_error if $send_error;
        } else {
            run_command([$send, @cstream, $recv], logfunc => $match_volid_and_log);
        }
@@ -882,6 +958,8 @@ sub vdisk_list {
        foreach my $sid (keys %$ids) {
            next if $storeid && $storeid ne $sid;
            next if !storage_check_enabled($cfg, $sid, undef, 1);
+           my $content = $ids->{$sid}->{content};
+           next if !($content->{rootdir} || $content->{images});
            push @$storage_list, $sid;
        }
     }
@@ -1377,7 +1455,8 @@ sub decompressor_info {
     die "ERROR: archive format not defined\n"
        if !defined($decompressor->{$format});
 
-    my $decomp = $decompressor->{$format}->{$comp} if $comp;
+    my $decomp;
+    $decomp = $decompressor->{$format}->{$comp} if $comp;
 
     my $info = {
        format => $format,
@@ -1540,6 +1619,104 @@ sub extract_vzdump_config {
     }
 }
 
+sub prune_backups {
+    my ($cfg, $storeid, $keep, $vmid, $type, $dryrun, $logfunc) = @_;
+
+    my $scfg = storage_config($cfg, $storeid);
+    die "storage '$storeid' does not support backups\n" if !$scfg->{content}->{backup};
+
+    if (!defined($keep)) {
+       die "no prune-backups options configured for storage '$storeid'\n"
+           if !defined($scfg->{'prune-backups'});
+       $keep = PVE::JSONSchema::parse_property_string('prune-backups', $scfg->{'prune-backups'});
+    }
+
+    my $plugin = PVE::Storage::Plugin->lookup($scfg->{type});
+    return $plugin->prune_backups($scfg, $storeid, $keep, $vmid, $type, $dryrun, $logfunc);
+}
+
+my $prune_mark = sub {
+    my ($prune_entries, $keep_count, $id_func) = @_;
+
+    return if !$keep_count;
+
+    my $already_included = {};
+    my $newly_included = {};
+
+    foreach my $prune_entry (@{$prune_entries}) {
+       my $mark = $prune_entry->{mark};
+       my $id = $id_func->($prune_entry->{ctime});
+       $already_included->{$id} = 1 if defined($mark) && $mark eq 'keep';
+    }
+
+    foreach my $prune_entry (@{$prune_entries}) {
+       my $mark = $prune_entry->{mark};
+       my $id = $id_func->($prune_entry->{ctime});
+
+       next if defined($mark) || $already_included->{$id};
+
+       if (!$newly_included->{$id}) {
+           last if scalar(keys %{$newly_included}) >= $keep_count;
+           $newly_included->{$id} = 1;
+           $prune_entry->{mark} = 'keep';
+       } else {
+           $prune_entry->{mark} = 'remove';
+       }
+    }
+};
+
+sub prune_mark_backup_group {
+    my ($backup_group, $keep) = @_;
+
+    my $keep_all = delete $keep->{'keep-all'};
+
+    if ($keep_all || !scalar(grep {$_ > 0} values %{$keep})) {
+       $keep = { 'keep-all' => 1 } if $keep_all;
+       foreach my $prune_entry (@{$backup_group}) {
+           $prune_entry->{mark} = 'keep';
+       }
+       return;
+    }
+
+    my $prune_list = [ sort { $b->{ctime} <=> $a->{ctime} } @{$backup_group} ];
+
+    $prune_mark->($prune_list, $keep->{'keep-last'}, sub {
+       my ($ctime) = @_;
+       return $ctime;
+    });
+    $prune_mark->($prune_list, $keep->{'keep-hourly'}, sub {
+       my ($ctime) = @_;
+       my (undef, undef, $hour, $day, $month, $year) = localtime($ctime);
+       return "$hour/$day/$month/$year";
+    });
+    $prune_mark->($prune_list, $keep->{'keep-daily'}, sub {
+       my ($ctime) = @_;
+       my (undef, undef, undef, $day, $month, $year) = localtime($ctime);
+       return "$day/$month/$year";
+    });
+    $prune_mark->($prune_list, $keep->{'keep-weekly'}, sub {
+       my ($ctime) = @_;
+       my ($sec, $min, $hour, $day, $month, $year) = localtime($ctime);
+       my $iso_week = int(strftime("%V", $sec, $min, $hour, $day, $month - 1, $year - 1900));
+       my $iso_week_year = int(strftime("%G", $sec, $min, $hour, $day, $month - 1, $year - 1900));
+       return "$iso_week/$iso_week_year";
+    });
+    $prune_mark->($prune_list, $keep->{'keep-monthly'}, sub {
+       my ($ctime) = @_;
+       my (undef, undef, undef, undef, $month, $year) = localtime($ctime);
+       return "$month/$year";
+    });
+    $prune_mark->($prune_list, $keep->{'keep-yearly'}, sub {
+       my ($ctime) = @_;
+       my $year = (localtime($ctime))[5];
+       return "$year";
+    });
+
+    foreach my $prune_entry (@{$prune_list}) {
+       $prune_entry->{mark} //= 'remove';
+    }
+}
+
 sub volume_export {
     my ($cfg, $fh, $volid, $format, $snapshot, $base_snapshot, $with_snapshots) = @_;