1
0
mirror of https://github.com/pgbackrest/pgbackrest.git synced 2024-12-14 10:13:05 +02:00
pgbackrest/lib/pgBackRest/Stanza.pm

687 lines
26 KiB
Perl
Raw Normal View History

####################################################################################################################################
# STANZA MODULE
#
# Contains functions for adding, upgrading and removing a stanza.
####################################################################################################################################
package pgBackRest::Stanza;
use strict;
use warnings FATAL => qw(all);
use Carp qw(confess);
use English '-no_match_vars';
use Exporter qw(import);
our @EXPORT = qw();
use pgBackRest::Common::Exception;
use pgBackRest::Common::Log;
use pgBackRest::Config::Config;
use pgBackRest::Archive::Info;
use pgBackRest::Backup::Info;
use pgBackRest::Db;
use pgBackRest::DbVersion;
use pgBackRest::InfoCommon;
use pgBackRest::Protocol::Helper;
use pgBackRest::Protocol::Storage::Helper;
####################################################################################################################################
# Global variables
####################################################################################################################################
my $strHintForce = "\nHINT: use stanza-create --force to force the stanza data to be created.";
my $strInfoMissing = " information missing";
my $strStanzaCreateErrorMsg = "not empty";
my $strRepoEncryptedMsg = " and repo is encrypted and info file(s) are missing, --force cannot be used";
my $strHintStanzaCreate = "\nHINT: has a stanza-create been performed?";
####################################################################################################################################
# CONSTRUCTOR
####################################################################################################################################
sub new
{
my $class = shift; # Class name
# Create the class hash
my $self = {};
bless $self, $class;
# Assign function parameters, defaults, and log debug info
my $strOperation = logDebugParam(__PACKAGE__ . '->new');
# Initialize the database object
($self->{oDb}) = dbObjectGet();
$self->dbInfoGet();
# Return from function and log return values if any
return logDebugReturn
(
$strOperation,
{name => 'self', value => $self}
);
}
####################################################################################################################################
# Process Stanza Commands
####################################################################################################################################
sub process
{
my $self = shift;
# Assign function parameters, defaults, and log debug info
my ($strOperation) = logDebugParam(__PACKAGE__ . '->process');
my $iResult = 0;
# Process stanza create
if (cfgCommandTest(CFGCMD_STANZA_CREATE))
{
$iResult = $self->stanzaCreate();
}
# Process stanza upgrade
elsif (cfgCommandTest(CFGCMD_STANZA_UPGRADE))
{
$iResult = $self->stanzaUpgrade();
}
# Else error if any other command is found
else
{
confess &log(ASSERT, "stanza->process() called with invalid command: " . cfgCommandName(cfgCommandGet()));
}
# Return from function and log return values if any
return logDebugReturn
(
$strOperation,
{name => 'iResult', value => $iResult, trace => true}
);
}
####################################################################################################################################
# stanzaCreate
#
# Creates the required data for the stanza.
####################################################################################################################################
sub stanzaCreate
{
my $self = shift;
# Assign function parameters, defaults, and log debug info
my ($strOperation) = logDebugParam(__PACKAGE__ . '->stanzaCreate');
# Get the parent paths (create if not exist)
my $strParentPathArchive = $self->parentPathGet(STORAGE_REPO_ARCHIVE);
my $strParentPathBackup = $self->parentPathGet(STORAGE_REPO_BACKUP);
# Get a listing of files in the top-level repo directories, ignoring if any are missing
my @stryFileListArchive = storageRepo()->list($strParentPathArchive, {bIgnoreMissing => true});
my @stryFileListBackup = storageRepo()->list($strParentPathBackup, {bIgnoreMissing => true});
# If files exist force should be required since create must have already occurred and reissuing a create needs to be a consciuos
# effort to rewrite the files.
# If at least one directory is not empty, then check to see if the info files exist
if (@stryFileListArchive || @stryFileListBackup)
{
my $strBackupInfoFile = &FILE_BACKUP_INFO;
my $strArchiveInfoFile = &ARCHIVE_INFO_FILE;
# If .info exists, set to true. Do not include .info.copy
my $bBackupInfoFileExists = grep(/^$strBackupInfoFile$/i, @stryFileListBackup);
my $bArchiveInfoFileExists = grep(/^$strArchiveInfoFile$/i, @stryFileListArchive);
# Determine if a file exists other than the info files
my $strExistingFile = $self->existingFileName(STORAGE_REPO_BACKUP, $strParentPathBackup, &FILE_BACKUP_INFO);
if (!defined($strExistingFile))
{
$strExistingFile = $self->existingFileName(STORAGE_REPO_ARCHIVE, $strParentPathArchive, &ARCHIVE_INFO_FILE);
}
# If something other than the info files exist in the repo (maybe a backup is in progress) and the user is attempting to
# change the repo encryption in anyway, then error
if (defined($strExistingFile) && (!storageRepo()->encryptionValid(storageRepo()->encrypted($strExistingFile))))
{
confess &log(ERROR, 'files exist - the encryption type or passphrase cannot be changed', ERROR_PATH_NOT_EMPTY);
}
# If the .info file exists in one directory but is missing from the other directory then there is clearly a mismatch
# which requires force option so throw an error to indicate force is needed. If the repo is encrypted and something other
# than the info files exist, then error that force cannot be used (the info files cannot be reconstructed since we no longer
# have the passphrase that was in the info file to open the other files in the repo)
if (!$bArchiveInfoFileExists && $bBackupInfoFileExists)
{
$self->errorForce('archive' . $strInfoMissing, ERROR_FILE_MISSING, $strExistingFile, $bArchiveInfoFileExists,
$strParentPathArchive, $strParentPathBackup);
}
elsif (!$bBackupInfoFileExists && $bArchiveInfoFileExists)
{
$self->errorForce('backup' . $strInfoMissing, ERROR_FILE_MISSING, $strExistingFile, $bBackupInfoFileExists,
$strParentPathArchive, $strParentPathBackup);
}
# If we get here then either both exist or neither exist so if neither file exists and something else exists in the
# directories then need to use force option to recreate the missing info files - unless the repo is encrypted, then force
# cannot be used if other than the info files exist. If only the info files exist then force must be used to overwrite the
# files.
else
{
$self->errorForce(
(@stryFileListBackup ? 'backup directory ' : '') .
((@stryFileListBackup && @stryFileListArchive) ? 'and/or ' : '') .
(@stryFileListArchive ? 'archive directory ' : '') .
$strStanzaCreateErrorMsg, ERROR_PATH_NOT_EMPTY,
$strExistingFile, $bArchiveInfoFileExists, $strParentPathArchive, $strParentPathBackup);
}
}
# Instantiate the info objects. Throws an error and aborts if force not used and an error occurs during instantiation.
my $oArchiveInfo = $self->infoObject(STORAGE_REPO_ARCHIVE, $strParentPathArchive, {bRequired => false, bIgnoreMissing => true});
my $oBackupInfo = $self->infoObject(STORAGE_REPO_BACKUP, $strParentPathBackup, {bRequired => false, bIgnoreMissing => true});
# Create the archive info object
my ($iResult, $strResultMessage) = $self->infoFileCreate($oArchiveInfo);
if ($iResult == 0)
{
# Create the backup.info file
($iResult, $strResultMessage) = $self->infoFileCreate($oBackupInfo);
}
if ($iResult != 0)
{
&log(WARN, "unable to create stanza '" . cfgOption(CFGOPT_STANZA) . "'");
confess &log(ERROR, $strResultMessage, $iResult);
}
# Return from function and log return values if any
return logDebugReturn
(
$strOperation,
{name => 'iResult', value => $iResult, trace => true}
);
}
####################################################################################################################################
# stanzaUpgrade
#
# Updates stanza information to reflect new cluster information. Normally used for version upgrades, but could be used after a
# cluster has been dumped and restored to the same version.
####################################################################################################################################
sub stanzaUpgrade
{
my $self = shift;
# Assign function parameters, defaults, and log debug info
my ($strOperation) = logDebugParam(__PACKAGE__ . '->stanzaUpgrade');
# Get the archive info and backup info files; if either does not exist or cannot be opened an error will be thrown
my $oArchiveInfo = $self->infoObject(STORAGE_REPO_ARCHIVE, storageRepo()->pathGet(STORAGE_REPO_ARCHIVE));
my $oBackupInfo = $self->infoObject(STORAGE_REPO_BACKUP, storageRepo()->pathGet(STORAGE_REPO_BACKUP));
my $bBackupUpgraded = false;
my $bArchiveUpgraded = false;
# If the DB section does not match, then upgrade
if ($self->upgradeCheck($oBackupInfo, STORAGE_REPO_BACKUP, ERROR_BACKUP_MISMATCH))
{
# Reconstruct the file and save it
my ($bReconstruct, $strWarningMsgArchive) = $oBackupInfo->reconstruct(false, false, $self->{oDb}{strDbVersion},
$self->{oDb}{ullDbSysId}, $self->{oDb}{iControlVersion}, $self->{oDb}{iCatalogVersion});
$oBackupInfo->save();
$bBackupUpgraded = true;
}
if ($self->upgradeCheck($oArchiveInfo, STORAGE_REPO_ARCHIVE, ERROR_ARCHIVE_MISMATCH))
{
# Reconstruct the file and save it
my ($bReconstruct, $strWarningMsgArchive) = $oArchiveInfo->reconstruct($self->{oDb}{strDbVersion},
$self->{oDb}{ullDbSysId});
$oArchiveInfo->save();
$bArchiveUpgraded = true;
}
# If neither file needed upgrading then provide informational message that an upgrade was not necessary
if (!($bBackupUpgraded || $bArchiveUpgraded))
{
&log(INFO, "the stanza data is already up to date");
}
# Return from function and log return values if any
return logDebugReturn
(
$strOperation,
{name => 'iResult', value => 0, trace => true}
);
}
####################################################################################################################################
# parentPathGet
#
# Creates the parent path if it doesn't exist and returns the path.
####################################################################################################################################
sub parentPathGet
{
my $self = shift;
# Assign function parameters, defaults, and log debug info
my
(
$strOperation,
$strPathType,
) =
logDebugParam
(
__PACKAGE__ . '->parentPathGet', \@_,
{name => 'strPathType', trace => true},
);
my $strParentPath = storageRepo()->pathGet($strPathType);
# If the info path does not exist, create it
if (!storageRepo()->pathExists($strParentPath))
{
# Create the cluster repo path
storageRepo()->pathCreate($strPathType, {bIgnoreExists => true, bCreateParent => true});
}
# Return from function and log return values if any
return logDebugReturn
(
$strOperation,
{name => 'strParentPath', value => $strParentPath},
);
}
####################################################################################################################################
# existingFileName
#
# Get a name of a file in the specified storage
####################################################################################################################################
sub existingFileName
{
my $self = shift;
# Assign function parameters, defaults, and log debug info
my
(
$strOperation,
$strPathType,
$strParentPath,
$strExcludeFile,
) =
logDebugParam
(
__PACKAGE__ . '->existingFileName', \@_,
{name => 'strPathType'},
{name => 'strParentPath'},
{name => 'strExcludeFile'},
);
my $hFullList = storageRepo()->manifest(storageRepo()->pathGet($strPathType), {bIgnoreMissing => true});
my $strExistingFile = undef;
foreach my $strName (keys(%{$hFullList}))
{
if (($hFullList->{$strName}{type} eq 'f') &&
(substr($strName, 0, length($strExcludeFile)) ne $strExcludeFile))
{
$strExistingFile = $strParentPath . "/" . $strName;
last;
}
}
# Return from function and log return values if any
return logDebugReturn
(
$strOperation,
{name => 'strExistingFile', value => $strExistingFile},
);
}
####################################################################################################################################
# errorForce
#
# Creates the parent path if it doesn't exist and returns the path.
####################################################################################################################################
sub errorForce
{
my $self = shift;
# Assign function parameters, defaults, and log debug info
my
(
$strOperation,
$strMessage,
$iErrorCode,
$strFileName,
$bInfoFileExists,
$strParentPathArchive,
$strParentPathBackup,
) =
logDebugParam
(
__PACKAGE__ . '->errorForce', \@_,
{name => 'strMessage', trace => true},
{name => 'iErrorCode', trace => true},
{name => 'strFileName', required => false, trace => true},
{name => 'bInfoFileExists', trace => true},
{name => 'strParentPathArchive', trace => true},
{name => 'strParentPathBackup', trace => true},
);
my $bRepoEncrypted = false;
# Check if the file passed is encrypted (if one exists) else check the repo setting.
if (defined($strFileName))
{
$bRepoEncrypted = storageRepo()->encrypted($strFileName);
}
elsif (defined(storageRepo()->cipherType()))
{
$bRepoEncrypted = true;
}
# If the repo is encrypted and a file other than the info files exist yet the info file is missing then the --force option
# cannot be used
if ($bRepoEncrypted && defined($strFileName) && !$bInfoFileExists)
{
confess &log(ERROR, $strMessage . $strRepoEncryptedMsg, $iErrorCode);
}
elsif (!cfgOption(CFGOPT_FORCE))
{
# If info files exist, check to see if an upgrade is required
if ($bInfoFileExists && !defined($strFileName) && $iErrorCode == ERROR_PATH_NOT_EMPTY)
{
if ($self->upgradeCheck(new pgBackRest::Backup::Info($strParentPathBackup), STORAGE_REPO_BACKUP,
ERROR_BACKUP_MISMATCH) ||
$self->upgradeCheck(new pgBackRest::Archive::Info($strParentPathArchive), STORAGE_REPO_ARCHIVE,
ERROR_ARCHIVE_MISMATCH))
{
confess &log(ERROR, "backup info file or archive info file invalid\n" .
'HINT: use stanza-upgrade if the database has been upgraded or use --force', ERROR_FILE_INVALID);
}
}
# If get here just indicate force is required
confess &log(ERROR, $strMessage . $strHintForce, $iErrorCode);
}
# Return from function and log return values if any
return logDebugReturn($strOperation);
}
####################################################################################################################################
# infoObject
#
# Attempt to load an info object. Ignores missing files if directed. Throws an error and aborts if force not used and an error
# occurs during loading, else instatiates the object without loading it.
####################################################################################################################################
sub infoObject
{
my $self = shift;
# Assign function parameters, defaults, and log debug info
my
(
$strOperation,
$strPathType,
$strParentPath,
$bRequired,
$bIgnoreMissing,
) =
logDebugParam
(
__PACKAGE__ . '->infoObject', \@_,
{name => 'strPathType'},
{name => 'strParentPath'},
{name => 'bRequired', optional => true, default => true},
{name => 'bIgnoreMissing', optional => true, default => false},
);
my $iResult = 0;
my $strResultMessage;
my $oInfo;
# Turn off console logging to control when to display the error
logDisable();
# Instantiate the info object in an eval block to trap errors. If force is not used and an error occurs, throw the error
# along with a directive that force will need to be used to attempt to correct the issue
eval
{
# Ignore missing files if directed but if the info or info.copy file exists the exists flag will still be set and data will
# attempt to be loaded. If the info file is missing and the repo is encrypted, we can only get to this function if nothing
# else existed in the repo so a passphrase is generated to store in the file. If it exists and the repo is encrypted then
# the generated passphrase passed will not be used - the one from the info file will be read.
my $oParamRef =
{bIgnoreMissing => $bIgnoreMissing,
strCipherPassSub => defined(storageRepo()->cipherType()) ?
storageRepo()->cipherPassGen() : undef};
$oInfo = ($strPathType eq STORAGE_REPO_BACKUP ?
new pgBackRest::Backup::Info($strParentPath, false, $bRequired, $oParamRef) :
new pgBackRest::Archive::Info($strParentPath, $bRequired, $oParamRef));
# Reset the console logging
logEnable();
return true;
}
or do
{
# Reset console logging and capture error information
logEnable();
$iResult = exceptionCode($EVAL_ERROR);
$strResultMessage = exceptionMessage($EVAL_ERROR);
};
if ($iResult != 0)
{
# If force was not used, and the file is missing, then confess the error with hint to use force if the option is
# configurable (force is not configurable for stanza-upgrade so this will always confess errors on stanza-upgrade)
# else confess all other errors
if ((cfgOptionValid(CFGOPT_FORCE) && !cfgOption(CFGOPT_FORCE)) ||
(!cfgOptionValid(CFGOPT_FORCE)))
{
if ($iResult == ERROR_FILE_MISSING)
{
confess &log(ERROR, cfgOptionValid(CFGOPT_FORCE) ? $strResultMessage . $strHintForce :
$strResultMessage . $strHintStanzaCreate, $iResult);
}
else
{
confess &log(ERROR, $strResultMessage, $iResult);
}
}
# Else instatiate the object without loading it so we can reconstruct and overwrite the invalid files
else
{
# Confess unhandled exception
if (($iResult != ERROR_FILE_MISSING) && ($iResult != ERROR_CIPHER))
{
confess &log(ERROR, $strResultMessage, $iResult);
}
my $oParamRef =
{bLoad => false,
strCipherPassSub => defined(storageRepo()->cipherType()) ?
storageRepo()->cipherPassGen() : undef};
$oInfo = ($strPathType eq STORAGE_REPO_BACKUP ?
new pgBackRest::Backup::Info($strParentPath, false, false, $oParamRef) :
new pgBackRest::Archive::Info($strParentPath, false, $oParamRef));
}
}
# Return from function and log return values if any
return logDebugReturn
(
$strOperation,
{name => 'oInfo', value => $oInfo},
);
}
####################################################################################################################################
# infoFileCreate
#
# Creates the info file based on the data passed to the function
####################################################################################################################################
sub infoFileCreate
{
my $self = shift;
# Assign function parameters, defaults, and log debug info
my
(
$strOperation,
$oInfo,
) =
logDebugParam
(
__PACKAGE__ . '->infoFileCreate', \@_,
{name => 'oInfo', trace => true},
);
my $iResult = 0;
my $strResultMessage = undef;
my $strWarningMsgArchive = undef;
# Turn off console logging to control when to display the error
logDisable();
eval
{
# Reconstruct the file from the data in the directory if there is any else initialize the file
if (defined($oInfo->{strBackupClusterPath}))
{
$oInfo->reconstruct(false, false, $self->{oDb}{strDbVersion}, $self->{oDb}{ullDbSysId}, $self->{oDb}{iControlVersion},
$self->{oDb}{iCatalogVersion});
}
# If this is the archive.info reconstruction then catch any warnings
else
{
$strWarningMsgArchive = $oInfo->reconstruct($self->{oDb}{strDbVersion}, $self->{oDb}{ullDbSysId});
}
# Reset the console logging
logEnable();
return true;
}
or do
{
# Reset console logging and capture error information
logEnable();
$iResult = exceptionCode($EVAL_ERROR);
$strResultMessage = exceptionMessage($EVAL_ERROR);
};
# If we got here without error then save the reconstructed file
if ($iResult == 0)
{
$oInfo->save();
# Sync path
storageRepo()->pathSync(
defined($oInfo->{strArchiveClusterPath}) ? $oInfo->{strArchiveClusterPath} : $oInfo->{strBackupClusterPath});
}
# If a warning was issued, raise it
if (defined($strWarningMsgArchive))
{
&log(WARN, $strWarningMsgArchive);
}
# Return from function and log return values if any
return logDebugReturn
(
$strOperation,
{name => 'iResult', value => $iResult},
{name => 'strResultMessage', value => $strResultMessage},
);
}
####################################################################################################################################
# dbInfoGet
#
# Gets the database information and store it in $self
####################################################################################################################################
sub dbInfoGet
{
my $self = shift;
# Assign function parameters, defaults, and log debug info
my ($strOperation) = logDebugParam(__PACKAGE__ . '->dbInfoGet');
# Validate the database configuration. Do not require the database to be online before creating a stanza because the
# archive_command will attempt to push an achive before the archive.info file exists which will result in an error in the
# postgres logs.
if (cfgOption(CFGOPT_ONLINE))
{
# If the db-path in pgbackrest.conf does not match the pg_control then this will error alert the user to fix pgbackrest.conf
$self->{oDb}->configValidate();
}
($self->{oDb}{strDbVersion}, $self->{oDb}{iControlVersion}, $self->{oDb}{iCatalogVersion}, $self->{oDb}{ullDbSysId})
= $self->{oDb}->info();
# Return from function and log return values if any
return logDebugReturn($strOperation);
}
####################################################################################################################################
# upgradeCheck
#
# Checks the info file to see if an upgrade is necessary.
####################################################################################################################################
sub upgradeCheck
{
my $self = shift;
# Assign function parameters, defaults, and log debug info
my
(
$strOperation,
$oInfo,
$strPathType,
$iExpectedError,
) =
logDebugParam
(
__PACKAGE__ . '->upgradeCheck', \@_,
{name => 'oInfo'},
{name => 'strPathType'},
{name => 'iExpectedError'},
);
my $iResult = 0;
my $strResultMessage = undef;
# Turn off console logging to control when to display the error
logDisable();
eval
{
($strPathType eq STORAGE_REPO_BACKUP)
? $oInfo->check($self->{oDb}{strDbVersion}, $self->{oDb}{iControlVersion}, $self->{oDb}{iCatalogVersion},
$self->{oDb}{ullDbSysId}, true)
: $oInfo->check($self->{oDb}{strDbVersion}, $self->{oDb}{ullDbSysId}, true);
logEnable();
return true;
}
or do
{
logEnable();
# Confess unhandled errors
confess $EVAL_ERROR if (exceptionCode($EVAL_ERROR) != $iExpectedError);
# Capture the result which will be the expected error, meaning an upgrade is needed
$iResult = exceptionCode($EVAL_ERROR);
$strResultMessage = exceptionMessage($EVAL_ERROR);
};
# Return from function and log return values if any
return logDebugReturn
(
$strOperation,
{name => 'bResult', value => ($iResult == $iExpectedError ? true : false)},
);
}
1;