2015-08-29 20:20:46 +02:00
|
|
|
####################################################################################################################################
|
|
|
|
# EXPIRE MODULE
|
|
|
|
####################################################################################################################################
|
|
|
|
package BackRest::Expire;
|
|
|
|
|
|
|
|
use threads;
|
|
|
|
use strict;
|
|
|
|
use warnings FATAL => qw(all);
|
|
|
|
use Carp qw(confess);
|
|
|
|
|
|
|
|
use Exporter qw(import);
|
|
|
|
use File::Basename qw(dirname);
|
|
|
|
use File::Path qw(remove_tree);
|
|
|
|
use Scalar::Util qw(looks_like_number);
|
|
|
|
|
|
|
|
use lib dirname($0);
|
|
|
|
use BackRest::Common::Log;
|
|
|
|
use BackRest::BackupCommon;
|
2015-09-08 13:31:24 +02:00
|
|
|
use BackRest::Config::Config;
|
2015-08-29 20:20:46 +02:00
|
|
|
use BackRest::File;
|
|
|
|
use BackRest::Manifest;
|
|
|
|
|
|
|
|
####################################################################################################################################
|
|
|
|
# Operation constants
|
|
|
|
####################################################################################################################################
|
|
|
|
use constant OP_EXPIRE => 'Expire';
|
|
|
|
|
2015-10-08 17:43:56 +02:00
|
|
|
use constant OP_EXPIRE_DESTROY => OP_EXPIRE . '->DESTROY';
|
2015-08-29 20:20:46 +02:00
|
|
|
use constant OP_EXPIRE_NEW => OP_EXPIRE . '->new';
|
|
|
|
use constant OP_EXPIRE_PROCESS => OP_EXPIRE . '->process';
|
|
|
|
|
|
|
|
####################################################################################################################################
|
|
|
|
# new
|
|
|
|
####################################################################################################################################
|
|
|
|
sub new
|
|
|
|
{
|
|
|
|
my $class = shift;
|
|
|
|
|
|
|
|
# Create the class hash
|
|
|
|
my $self = {};
|
|
|
|
bless $self, $class;
|
|
|
|
|
|
|
|
# Assign function parameters, defaults, and log debug info
|
2015-10-08 17:43:56 +02:00
|
|
|
my ($strOperation) = logDebugParam(OP_EXPIRE_NEW);
|
|
|
|
|
|
|
|
# Initialize file object
|
|
|
|
$self->{oFile} = new BackRest::File
|
2015-08-29 20:20:46 +02:00
|
|
|
(
|
2015-10-08 17:43:56 +02:00
|
|
|
optionGet(OPTION_STANZA),
|
|
|
|
optionRemoteTypeTest(BACKUP) ? optionGet(OPTION_REPO_REMOTE_PATH) : optionGet(OPTION_REPO_PATH),
|
|
|
|
optionRemoteType(),
|
|
|
|
protocolGet()
|
|
|
|
);
|
2015-08-29 20:20:46 +02:00
|
|
|
|
|
|
|
# Return from function and log return values if any
|
|
|
|
return logDebugReturn
|
|
|
|
(
|
|
|
|
$strOperation,
|
|
|
|
{name => 'self', value => $self}
|
|
|
|
);
|
|
|
|
}
|
|
|
|
|
2015-10-08 17:43:56 +02:00
|
|
|
####################################################################################################################################
|
|
|
|
# DESTROY
|
|
|
|
####################################################################################################################################
|
|
|
|
sub DESTROY
|
|
|
|
{
|
|
|
|
my $self = shift;
|
|
|
|
|
|
|
|
# Assign function parameters, defaults, and log debug info
|
|
|
|
my
|
|
|
|
(
|
|
|
|
$strOperation
|
|
|
|
) =
|
|
|
|
logDebugParam
|
|
|
|
(
|
|
|
|
OP_EXPIRE_DESTROY
|
|
|
|
);
|
|
|
|
|
|
|
|
undef($self->{oFile});
|
|
|
|
|
|
|
|
# Return from function and log return values if any
|
|
|
|
return logDebugReturn
|
|
|
|
(
|
|
|
|
$strOperation
|
|
|
|
);
|
|
|
|
}
|
|
|
|
|
2015-08-29 20:20:46 +02:00
|
|
|
####################################################################################################################################
|
|
|
|
# process
|
|
|
|
#
|
|
|
|
# Removes expired backups and archive logs from the backup directory. Partial backups are not counted for expiration, so if full
|
|
|
|
# or differential retention is set to 2, there must be three complete backups before the oldest one can be deleted.
|
|
|
|
####################################################################################################################################
|
|
|
|
sub process
|
|
|
|
{
|
|
|
|
my $self = shift;
|
|
|
|
|
|
|
|
# Assign function parameters, defaults, and log debug info
|
|
|
|
my
|
|
|
|
(
|
|
|
|
$strOperation
|
|
|
|
) =
|
|
|
|
logDebugParam
|
|
|
|
(
|
|
|
|
OP_EXPIRE_PROCESS
|
|
|
|
);
|
|
|
|
|
|
|
|
my $strPath;
|
|
|
|
my @stryPath;
|
|
|
|
|
|
|
|
my $oFile = $self->{oFile};
|
|
|
|
my $strBackupClusterPath = $oFile->pathGet(PATH_BACKUP_CLUSTER);
|
|
|
|
my $iFullRetention = optionGet(OPTION_RETENTION_FULL, false);
|
|
|
|
my $iDifferentialRetention = optionGet(OPTION_RETENTION_DIFF, false);
|
|
|
|
my $strArchiveRetentionType = optionGet(OPTION_RETENTION_ARCHIVE_TYPE, false);
|
|
|
|
my $iArchiveRetention = optionGet(OPTION_RETENTION_ARCHIVE, false);
|
|
|
|
|
|
|
|
# Load or build backup.info
|
|
|
|
my $oBackupInfo = new BackRest::BackupInfo($oFile->pathGet(PATH_BACKUP_CLUSTER));
|
|
|
|
|
|
|
|
# Find all the expired full backups
|
|
|
|
if (defined($iFullRetention))
|
|
|
|
{
|
|
|
|
# Make sure iFullRetention is valid
|
|
|
|
if (!looks_like_number($iFullRetention) || $iFullRetention < 1)
|
|
|
|
{
|
2015-11-22 21:02:14 +02:00
|
|
|
confess &log(ERROR, 'retention-full must be a number >= 1');
|
2015-08-29 20:20:46 +02:00
|
|
|
}
|
|
|
|
|
2015-11-22 21:02:14 +02:00
|
|
|
@stryPath = $oFile->list(PATH_BACKUP_CLUSTER, undef, backupRegExpGet(true));
|
2015-08-29 20:20:46 +02:00
|
|
|
|
2015-11-22 21:02:14 +02:00
|
|
|
if (@stryPath > $iFullRetention)
|
2015-08-29 20:20:46 +02:00
|
|
|
{
|
|
|
|
# Delete all backups that depend on the full backup. Done in reverse order so that remaining backups will still
|
|
|
|
# be consistent if the process dies
|
2015-11-22 21:02:14 +02:00
|
|
|
for (my $iFullIdx = 0; $iFullIdx < @stryPath - $iFullRetention; $iFullIdx++)
|
2015-08-29 20:20:46 +02:00
|
|
|
{
|
2015-11-22 21:02:14 +02:00
|
|
|
my @stryRemoveList;
|
2015-08-29 20:20:46 +02:00
|
|
|
|
2015-11-22 21:02:14 +02:00
|
|
|
foreach $strPath ($oFile->list(PATH_BACKUP_CLUSTER, undef, '^' . $stryPath[$iFullIdx] . '.*'))
|
|
|
|
{
|
|
|
|
system("rm -rf ${strBackupClusterPath}/${strPath}") == 0
|
|
|
|
or confess &log(ERROR, "unable to delete backup ${strPath}");
|
|
|
|
|
|
|
|
$oBackupInfo->delete($strPath);
|
2015-08-29 20:20:46 +02:00
|
|
|
|
2015-11-22 21:02:14 +02:00
|
|
|
if ($strPath ne $stryPath[$iFullIdx])
|
|
|
|
{
|
|
|
|
push(@stryRemoveList, $strPath);
|
|
|
|
}
|
|
|
|
}
|
2015-08-29 20:20:46 +02:00
|
|
|
|
2015-11-22 21:02:14 +02:00
|
|
|
&log(INFO, 'remove expired full backup ' . (@stryRemoveList > 0 ? 'set: ' : '') . $stryPath[$iFullIdx] .
|
|
|
|
(@stryRemoveList > 0 ? ', ' . join(', ', @stryRemoveList) : ''));
|
|
|
|
}
|
2015-08-29 20:20:46 +02:00
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
# Find all the expired differential backups
|
|
|
|
if (defined($iDifferentialRetention))
|
|
|
|
{
|
|
|
|
# Make sure iDifferentialRetention is valid
|
|
|
|
if (!looks_like_number($iDifferentialRetention) || $iDifferentialRetention < 1)
|
|
|
|
{
|
2015-11-22 21:02:14 +02:00
|
|
|
confess &log(ERROR, 'retention-diff must be a number >= 1');
|
2015-08-29 20:20:46 +02:00
|
|
|
}
|
|
|
|
|
2015-11-22 21:02:14 +02:00
|
|
|
@stryPath = $oFile->list(PATH_BACKUP_CLUSTER, undef, backupRegExpGet(false, true));
|
2015-08-29 20:20:46 +02:00
|
|
|
|
2015-11-22 21:02:14 +02:00
|
|
|
if (@stryPath > $iDifferentialRetention)
|
2015-08-29 20:20:46 +02:00
|
|
|
{
|
2015-11-22 21:02:14 +02:00
|
|
|
# $strDiffRetain = $stryPath[@stryPath - $iDifferentialRetention];
|
2015-08-29 20:20:46 +02:00
|
|
|
|
2015-11-22 21:02:14 +02:00
|
|
|
# logDebugMisc($strOperation, 'differential expiration based on ' . $strDiffRetain);
|
|
|
|
|
|
|
|
for (my $iDiffIdx = 0; $iDiffIdx < @stryPath - $iDifferentialRetention; $iDiffIdx++)
|
2015-08-29 20:20:46 +02:00
|
|
|
{
|
2015-11-22 21:02:14 +02:00
|
|
|
# Get a list of all differential and incremental backups
|
|
|
|
my @stryRemoveList;
|
2015-08-29 20:20:46 +02:00
|
|
|
|
2015-11-22 21:02:14 +02:00
|
|
|
foreach $strPath ($oFile->list(PATH_BACKUP_CLUSTER, undef, backupRegExpGet(false, true, true)))
|
2015-08-29 20:20:46 +02:00
|
|
|
{
|
2015-11-22 21:02:14 +02:00
|
|
|
logDebugMisc($strOperation, "checking ${strPath} for differential expiration");
|
2015-08-29 20:20:46 +02:00
|
|
|
|
2015-11-22 21:02:14 +02:00
|
|
|
# Remove all differential and incremental backups before the oldest valid differential
|
|
|
|
if ($strPath lt $stryPath[$iDiffIdx + 1])
|
|
|
|
{
|
|
|
|
system("rm -rf ${strBackupClusterPath}/${strPath}") == 0
|
|
|
|
or confess &log(ERROR, "unable to delete backup ${strPath}");
|
|
|
|
$oBackupInfo->delete($strPath);
|
|
|
|
|
|
|
|
if ($strPath ne $stryPath[$iDiffIdx])
|
|
|
|
{
|
|
|
|
push(@stryRemoveList, $strPath);
|
|
|
|
}
|
|
|
|
# &log(INFO, "remove expired diff/incr backup ${strPath}");
|
|
|
|
}
|
2015-08-29 20:20:46 +02:00
|
|
|
}
|
2015-11-22 21:02:14 +02:00
|
|
|
|
|
|
|
&log(INFO, 'remove expired diff backup ' . (@stryRemoveList > 0 ? 'set: ' : '') . $stryPath[$iDiffIdx] .
|
|
|
|
(@stryRemoveList > 0 ? ', ' . join(', ', @stryRemoveList) : ''));
|
2015-08-29 20:20:46 +02:00
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2015-11-22 21:02:14 +02:00
|
|
|
# Default archive retention if not explicily set
|
|
|
|
if (defined($iFullRetention))
|
|
|
|
{
|
|
|
|
if (!defined($iArchiveRetention))
|
|
|
|
{
|
|
|
|
$iArchiveRetention = $iFullRetention;
|
|
|
|
}
|
|
|
|
|
|
|
|
if (!defined($strArchiveRetentionType))
|
|
|
|
{
|
|
|
|
$strArchiveRetentionType = BACKUP_TYPE_FULL;
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2015-08-29 20:20:46 +02:00
|
|
|
# If no archive retention type is set then exit
|
|
|
|
if (!defined($strArchiveRetentionType))
|
|
|
|
{
|
|
|
|
&log(INFO, 'archive retention type not set - archive logs will not be expired');
|
|
|
|
}
|
|
|
|
else
|
|
|
|
{
|
|
|
|
# Determine which backup type to use for archive retention (full, differential, incremental)
|
|
|
|
if ($strArchiveRetentionType eq BACKUP_TYPE_FULL)
|
|
|
|
{
|
|
|
|
@stryPath = $oFile->list(PATH_BACKUP_CLUSTER, undef, backupRegExpGet(true), 'reverse');
|
|
|
|
}
|
|
|
|
elsif ($strArchiveRetentionType eq BACKUP_TYPE_DIFF)
|
|
|
|
{
|
|
|
|
@stryPath = $oFile->list(PATH_BACKUP_CLUSTER, undef, backupRegExpGet(true, true), 'reverse');
|
|
|
|
}
|
|
|
|
elsif ($strArchiveRetentionType eq BACKUP_TYPE_INCR)
|
|
|
|
{
|
|
|
|
@stryPath = $oFile->list(PATH_BACKUP_CLUSTER, undef, backupRegExpGet(true, true, true), 'reverse');
|
|
|
|
}
|
|
|
|
else
|
|
|
|
{
|
|
|
|
confess &log(ERROR, "unknown archive_retention_type '$strArchiveRetentionType'");
|
|
|
|
}
|
|
|
|
|
|
|
|
# Make sure that iArchiveRetention is set and valid
|
|
|
|
if (!defined($iArchiveRetention))
|
|
|
|
{
|
|
|
|
confess &log(ERROR, 'archive_retention must be set if archive_retention_type is set');
|
|
|
|
}
|
|
|
|
|
|
|
|
if (!looks_like_number($iArchiveRetention) || $iArchiveRetention < 1)
|
|
|
|
{
|
|
|
|
confess &log(ERROR, 'archive_retention must be a number >= 1');
|
|
|
|
}
|
|
|
|
|
2015-11-22 21:02:14 +02:00
|
|
|
# if no backups were found then preserve current archive logs - too soon to expire them
|
2015-08-29 20:20:46 +02:00
|
|
|
my $iBackupTotal = scalar @stryPath;
|
|
|
|
|
|
|
|
if ($iBackupTotal > 0)
|
|
|
|
{
|
|
|
|
# See if enough backups exist for expiration to start
|
|
|
|
my $strArchiveRetentionBackup = $stryPath[$iArchiveRetention - 1];
|
|
|
|
|
|
|
|
if (!defined($strArchiveRetentionBackup))
|
|
|
|
{
|
|
|
|
if ($strArchiveRetentionType eq BACKUP_TYPE_FULL && scalar @stryPath > 0)
|
|
|
|
{
|
2015-11-22 21:02:14 +02:00
|
|
|
&log(INFO, "full backup total < ${iArchiveRetention} - using oldest full backup for archive retention");
|
2015-08-29 20:20:46 +02:00
|
|
|
$strArchiveRetentionBackup = $stryPath[scalar @stryPath - 1];
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
if (defined($strArchiveRetentionBackup))
|
|
|
|
{
|
2015-09-02 01:05:10 +02:00
|
|
|
my $strArchiveExpireStart;
|
|
|
|
my $strArchiveExpireStop;
|
|
|
|
|
2015-08-29 20:20:46 +02:00
|
|
|
# Get the archive logs that need to be kept. To be cautious we will keep all the archive logs starting from this
|
|
|
|
# backup even though they are also in the pg_xlog directory (since they have been copied more than once).
|
|
|
|
my $oManifest = new BackRest::Manifest($oFile->pathGet(PATH_BACKUP_CLUSTER) .
|
|
|
|
"/${strArchiveRetentionBackup}/backup.manifest");
|
|
|
|
|
2016-01-16 06:37:17 +02:00
|
|
|
if ($oManifest->test(MANIFEST_SECTION_BACKUP, MANIFEST_KEY_ARCHIVE_START))
|
2015-08-29 20:20:46 +02:00
|
|
|
{
|
2016-01-16 06:37:17 +02:00
|
|
|
my $strArchiveLast = $oManifest->get(MANIFEST_SECTION_BACKUP, MANIFEST_KEY_ARCHIVE_START);
|
2015-08-29 20:20:46 +02:00
|
|
|
|
2016-01-16 06:37:17 +02:00
|
|
|
&log(INFO, "archive retention from backup ${strArchiveRetentionBackup}, start = ${strArchiveLast}");
|
2015-08-29 20:20:46 +02:00
|
|
|
|
2016-01-16 06:37:17 +02:00
|
|
|
# Get archive info
|
|
|
|
my $oArchive = new BackRest::Archive();
|
|
|
|
my $strArchiveId = $oArchive->getCheck($oFile);
|
2015-08-29 20:20:46 +02:00
|
|
|
|
2016-01-16 06:37:17 +02:00
|
|
|
# Remove any archive directories or files that are out of date
|
|
|
|
foreach $strPath ($oFile->list(PATH_BACKUP_ARCHIVE, $strArchiveId, "^[0-F]{16}\$"))
|
2015-08-29 20:20:46 +02:00
|
|
|
{
|
2016-01-16 06:37:17 +02:00
|
|
|
logDebugMisc($strOperation, "found major WAL path: ${strPath}");
|
2015-08-29 20:20:46 +02:00
|
|
|
|
2016-01-16 06:37:17 +02:00
|
|
|
# If less than first 16 characters of current archive file, then remove the directory
|
|
|
|
if ($strPath lt substr($strArchiveLast, 0, 16))
|
|
|
|
{
|
|
|
|
my $strFullPath = $oFile->pathGet(PATH_BACKUP_ARCHIVE, $strArchiveId) . "/${strPath}";
|
2015-08-29 20:20:46 +02:00
|
|
|
|
2016-01-16 06:37:17 +02:00
|
|
|
remove_tree($strFullPath) > 0 or confess &log(ERROR, "unable to remove ${strFullPath}");
|
2015-09-02 01:05:10 +02:00
|
|
|
|
2016-01-16 06:37:17 +02:00
|
|
|
logDebugMisc($strOperation, "remove major WAL path: ${strFullPath}");
|
2015-09-02 01:05:10 +02:00
|
|
|
|
2016-01-16 06:37:17 +02:00
|
|
|
# Record expire start and stop location for info
|
|
|
|
if (!defined($strArchiveExpireStart))
|
|
|
|
{
|
|
|
|
$strArchiveExpireStart = $strPath;
|
|
|
|
}
|
2015-08-29 20:20:46 +02:00
|
|
|
|
2016-01-16 06:37:17 +02:00
|
|
|
$strArchiveExpireStop = $strPath;
|
|
|
|
}
|
|
|
|
# If equals the first 16 characters of the current archive file, then delete individual files instead
|
|
|
|
elsif ($strPath eq substr($strArchiveLast, 0, 16))
|
2015-08-29 20:20:46 +02:00
|
|
|
{
|
2016-01-16 06:37:17 +02:00
|
|
|
my $strSubPath;
|
|
|
|
|
|
|
|
# Look for archive files in the archive directory
|
|
|
|
foreach $strSubPath ($oFile->list(PATH_BACKUP_ARCHIVE, "${strArchiveId}/${strPath}", "^[0-F]{24}.*\$"))
|
2015-08-29 20:20:46 +02:00
|
|
|
{
|
2016-01-16 06:37:17 +02:00
|
|
|
# Delete if the first 24 characters less than the current archive file
|
|
|
|
if ($strSubPath lt substr($strArchiveLast, 0, 24))
|
|
|
|
{
|
|
|
|
unlink($oFile->pathGet(PATH_BACKUP_ARCHIVE, "${strArchiveId}/${strSubPath}"))
|
|
|
|
or confess &log(ERROR, 'unable to remove ' . $strSubPath);
|
2015-08-29 20:20:46 +02:00
|
|
|
|
2016-01-16 06:37:17 +02:00
|
|
|
logDebugMisc($strOperation, "remove expired WAL segment: ${strSubPath}");
|
2015-09-02 01:05:10 +02:00
|
|
|
|
2016-01-16 06:37:17 +02:00
|
|
|
# Record expire start and stop location for info
|
|
|
|
if (!defined($strArchiveExpireStart))
|
|
|
|
{
|
|
|
|
$strArchiveExpireStart = $strSubPath;
|
|
|
|
}
|
2015-09-02 01:05:10 +02:00
|
|
|
|
2016-01-16 06:37:17 +02:00
|
|
|
$strArchiveExpireStop = $strSubPath;
|
|
|
|
}
|
2015-08-29 20:20:46 +02:00
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
2015-09-02 01:05:10 +02:00
|
|
|
|
2016-01-16 06:37:17 +02:00
|
|
|
if (!defined($strArchiveExpireStart))
|
|
|
|
{
|
|
|
|
&log(INFO, 'no WAL segments to expire');
|
|
|
|
}
|
|
|
|
else
|
|
|
|
{
|
|
|
|
&log(INFO, 'expire WAL segments: start = ' . substr($strArchiveExpireStart, 0, 24) .
|
|
|
|
', stop = ' . substr($strArchiveExpireStop, 0, 24));
|
|
|
|
}
|
2015-09-02 01:05:10 +02:00
|
|
|
}
|
2015-08-29 20:20:46 +02:00
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
# Return from function and log return values if any
|
|
|
|
return logDebugReturn
|
|
|
|
(
|
|
|
|
$strOperation
|
|
|
|
);
|
|
|
|
}
|
|
|
|
|
|
|
|
1;
|