#################################################################################################################################### # ARCHIVE INFO MODULE # # The archive.info file is created when archiving begins. It is located under the stanza directory. The file contains information # regarding the stanza database version, database WAL segment system id and other information to ensure that archiving is being # performed on the proper database. #################################################################################################################################### package pgBackRest::Archive::Info; use parent 'pgBackRest::Common::Ini'; use strict; use warnings FATAL => qw(all); use Carp qw(confess); use English '-no_match_vars'; use Exporter qw(import); our @EXPORT = qw(); use File::Basename qw(dirname basename); use pgBackRest::Archive::Common; use pgBackRest::Common::Exception; use pgBackRest::Config::Config; use pgBackRest::Common::Ini; use pgBackRest::Common::Log; use pgBackRest::DbVersion; use pgBackRest::InfoCommon; use pgBackRest::Manifest; use pgBackRest::Protocol::Storage::Helper; use pgBackRest::Storage::Base; use pgBackRest::Storage::Filter::Gzip; use pgBackRest::Storage::Helper; #################################################################################################################################### # File/path constants #################################################################################################################################### use constant ARCHIVE_INFO_FILE => 'archive.info'; push @EXPORT, qw(ARCHIVE_INFO_FILE); #################################################################################################################################### # Archive info constants #################################################################################################################################### use constant INFO_ARCHIVE_SECTION_DB => INFO_BACKUP_SECTION_DB; push @EXPORT, qw(INFO_ARCHIVE_SECTION_DB); use constant INFO_ARCHIVE_SECTION_DB_HISTORY => INFO_BACKUP_SECTION_DB_HISTORY; push @EXPORT, qw(INFO_ARCHIVE_SECTION_DB_HISTORY); use constant INFO_ARCHIVE_KEY_DB_VERSION => MANIFEST_KEY_DB_VERSION; push @EXPORT, qw(INFO_ARCHIVE_KEY_DB_VERSION); use constant INFO_ARCHIVE_KEY_DB_ID => MANIFEST_KEY_DB_ID; push @EXPORT, qw(INFO_ARCHIVE_KEY_DB_ID); use constant INFO_ARCHIVE_KEY_DB_SYSTEM_ID => MANIFEST_KEY_SYSTEM_ID; push @EXPORT, qw(INFO_ARCHIVE_KEY_DB_SYSTEM_ID); #################################################################################################################################### # Global variables #################################################################################################################################### my $strArchiveInfoMissingMsg = ARCHIVE_INFO_FILE . " does not exist but is required to push/get WAL segments\n" . "HINT: is archive_command configured in postgresql.conf?\n" . "HINT: has a stanza-create been performed?\n" . "HINT: use --no-archive-check to disable archive checks during backup if you have an alternate archiving scheme."; #################################################################################################################################### # CONSTRUCTOR #################################################################################################################################### sub new { my $class = shift; # Class name # Assign function parameters, defaults, and log debug info my ( $strOperation, $strArchiveClusterPath, # Archive cluster path $bRequired, # Is archive info required? $bLoad, # Should the file attempt to be loaded? $bIgnoreMissing, # Don't error on missing files $strCipherPassSub, # Passphrase to encrypt the subsequent archive files if repo is encrypted ) = logDebugParam ( __PACKAGE__ . '->new', \@_, {name => 'strArchiveClusterPath'}, {name => 'bRequired', default => true}, {name => 'bLoad', optional => true, default => true}, {name => 'bIgnoreMissing', optional => true, default => false}, {name => 'strCipherPassSub', optional => true}, ); # Build the archive info path/file name my $strArchiveInfoFile = "${strArchiveClusterPath}/" . ARCHIVE_INFO_FILE; my $self = {}; my $iResult = 0; my $strResultMessage; # Init object and store variables eval { $self = $class->SUPER::new($strArchiveInfoFile, {bLoad => $bLoad, bIgnoreMissing => $bIgnoreMissing, oStorage => storageRepo(), strCipherPass => storageRepo()->cipherPassUser(), strCipherPassSub => $strCipherPassSub}); return true; } or do { # Capture error information $iResult = exceptionCode($EVAL_ERROR); $strResultMessage = exceptionMessage($EVAL_ERROR); }; if ($iResult != 0) { # If the file does not exist but is required to exist, then error # The archive info is only allowed not to exist when running a stanza-create on a new install if ($iResult == ERROR_FILE_MISSING) { if ($bRequired) { confess &log(ERROR, $strArchiveInfoMissingMsg, ERROR_FILE_MISSING); } } elsif ($iResult == ERROR_CRYPTO && $strResultMessage =~ "^unable to flush") { confess &log(ERROR, "unable to parse '$strArchiveInfoFile'\nHINT: Is or was the repo encrypted?", $iResult); } else { confess $EVAL_ERROR; } } $self->{strArchiveClusterPath} = $strArchiveClusterPath; # Return from function and log return values if any return logDebugReturn ( $strOperation, {name => 'self', value => $self} ); } #################################################################################################################################### # check # # Check archive info file and make sure it is compatible with the current version of the database for the stanza. If the file does # not exist an error will occur. #################################################################################################################################### sub check { my $self = shift; # Assign function parameters, defaults, and log debug info my ( $strOperation, $strDbVersion, $ullDbSysId, $bRequired, ) = logDebugParam ( __PACKAGE__ . '->check', \@_, {name => 'strDbVersion'}, {name => 'ullDbSysId'}, {name => 'bRequired', default => true}, ); # ??? remove bRequired after stanza-upgrade if ($bRequired) { # Confirm the info file exists with the DB section $self->confirmExists(); } my $strError = undef; if (!$self->test(INFO_ARCHIVE_SECTION_DB, INFO_ARCHIVE_KEY_DB_VERSION, undef, $strDbVersion)) { $strError = "WAL segment version ${strDbVersion} does not match archive version " . $self->get(INFO_ARCHIVE_SECTION_DB, INFO_ARCHIVE_KEY_DB_VERSION); } if (!$self->test(INFO_ARCHIVE_SECTION_DB, INFO_ARCHIVE_KEY_DB_SYSTEM_ID, undef, $ullDbSysId)) { $strError = (defined($strError) ? ($strError . "\n") : "") . "WAL segment system-id ${ullDbSysId} does not match archive system-id " . $self->get(INFO_ARCHIVE_SECTION_DB, INFO_ARCHIVE_KEY_DB_SYSTEM_ID); } if (defined($strError)) { confess &log(ERROR, "${strError}\nHINT: are you archiving to the correct stanza?", ERROR_ARCHIVE_MISMATCH); } # Return from function and log return values if any return logDebugReturn ( $strOperation, {name => 'strArchiveId', value => $self->archiveId()} ); } #################################################################################################################################### # archiveId # # Get the archive id which is a combination of the DB version and the db-id setting (e.g. 9.4-1) #################################################################################################################################### sub archiveId { my $self = shift; # Assign function parameters, defaults, and log debug info my ( $strOperation, $strDbVersion, $ullDbSysId, ) = logDebugParam ( __PACKAGE__ . '->archiveId', \@_, {name => 'strDbVersion', optional => true}, {name => 'ullDbSysId', optional => true}, ); my $strArchiveId = undef; # If neither optional version and system-id are passed then set the archive id to the current one if (!defined($strDbVersion) && !defined($ullDbSysId)) { $strArchiveId = $self->get(INFO_ARCHIVE_SECTION_DB, INFO_ARCHIVE_KEY_DB_VERSION) . "-" . $self->get(INFO_ARCHIVE_SECTION_DB, INFO_ARCHIVE_KEY_DB_ID); } # If both the optional version and system-id are passed elsif (defined($strDbVersion) && defined($ullDbSysId)) { # Get the newest archiveId for the version/system-id passed $strArchiveId = ($self->archiveIdList($strDbVersion, $ullDbSysId))[0]; } # Return from function and log return values if any return logDebugReturn ( $strOperation, {name => 'strArchiveId', value => $strArchiveId} ); } #################################################################################################################################### # archiveIdList # # Get a sorted list of the archive ids for the db-version and db-system-id passed. #################################################################################################################################### sub archiveIdList { my $self = shift; # Assign function parameters, defaults, and log debug info my ( $strOperation, $strDbVersion, $ullDbSysId, ) = logDebugParam ( __PACKAGE__ . '->archiveIdList', \@_, {name => 'strDbVersion'}, {name => 'ullDbSysId'}, ); my @stryArchiveId; # Get the version and system-id for all known databases my $hDbList = $self->dbHistoryList(); foreach my $iDbHistoryId (sort {$a <=> $b} keys %$hDbList) { # If the version and system-id match then construct the archive id so that the constructed array has the newest match first if (($hDbList->{$iDbHistoryId}{&INFO_DB_VERSION} eq $strDbVersion) && ($hDbList->{$iDbHistoryId}{&INFO_SYSTEM_ID} eq $ullDbSysId)) { unshift(@stryArchiveId, $strDbVersion . "-" . $iDbHistoryId); } } # If the archive id has still not been found, then error if (@stryArchiveId == 0) { confess &log( ERROR, "unable to retrieve the archive id for database version '$strDbVersion' and system-id '$ullDbSysId'", ERROR_ARCHIVE_MISMATCH); } # Return from function and log return values if any return logDebugReturn ( $strOperation, {name => 'stryArchiveId', value => \@stryArchiveId} ); } #################################################################################################################################### # create # # Creates the archive.info file. WARNING - this function should only be called from stanza-create or tests. #################################################################################################################################### sub create { my $self = shift; # Assign function parameters, defaults, and log debug info my ( $strOperation, $strDbVersion, $ullDbSysId, $bSave, ) = logDebugParam ( __PACKAGE__ . '->create', \@_, {name => 'strDbVersion'}, {name => 'ullDbSysId'}, {name => 'bSave', default => true}, ); # Fill db section and db history section $self->dbSectionSet($strDbVersion, $ullDbSysId, $self->dbHistoryIdGet(false)); if ($bSave) { $self->save(); } # Return from function and log return values if any return logDebugReturn($strOperation); } #################################################################################################################################### # reconstruct # # Reconstruct the info file from the existing directory and files #################################################################################################################################### sub reconstruct { my $self = shift; # Assign function parameters, defaults, and log debug info my ( $strOperation, $strCurrentDbVersion, $ullCurrentDbSysId, ) = logDebugParam ( __PACKAGE__ . '->reconstruct', \@_, {name => 'strCurrentDbVersion'}, {name => 'ullCurrentDbSysId'}, ); my $strInvalidFileStructure = undef; my @stryArchiveId = storageRepo()->list( $self->{strArchiveClusterPath}, {strExpression => REGEX_ARCHIVE_DIR_DB_VERSION, bIgnoreMissing => true}); my %hDbHistoryVersion; # Get the db-version and db-id (history id) from the upper level directory names, e.g. 9.4-1 foreach my $strArchiveId (@stryArchiveId) { my ($strDbVersion, $iDbHistoryId) = split("-", $strArchiveId); $hDbHistoryVersion{$iDbHistoryId} = $strDbVersion; } # Loop through the DBs in the order they were created as indicated by the db-id so that the last one is set in the db section foreach my $iDbHistoryId (sort {$a <=> $b} keys %hDbHistoryVersion) { my $strDbVersion = $hDbHistoryVersion{$iDbHistoryId}; my $strVersionDir = $strDbVersion . "-" . $iDbHistoryId; # Get the name of the first archive directory my $strArchiveDir = (storageRepo()->list( $self->{strArchiveClusterPath} . "/${strVersionDir}", {strExpression => REGEX_ARCHIVE_DIR_WAL, bIgnoreMissing => true}))[0]; # Continue if any file structure or missing files info if (!defined($strArchiveDir)) { $strInvalidFileStructure = "found empty directory " . $self->{strArchiveClusterPath} . "/${strVersionDir}"; next; } # ??? Should probably make a function in ArchiveCommon my $strArchiveFile = (storageRepo()->list( $self->{strArchiveClusterPath} . "/${strVersionDir}/${strArchiveDir}", {strExpression => "^[0-F]{24}(\\.partial){0,1}(-[0-f]+){0,1}(\\." . COMPRESS_EXT . "){0,1}\$", bIgnoreMissing => true}))[0]; # Continue if any file structure or missing files info if (!defined($strArchiveFile)) { $strInvalidFileStructure = "found empty directory " . $self->{strArchiveClusterPath} . "/${strVersionDir}/${strArchiveDir}"; next; } # Get the full path for the file my $strArchiveFilePath = $self->{strArchiveClusterPath}."/${strVersionDir}/${strArchiveDir}/${strArchiveFile}"; # Get the db-system-id from the WAL file depending on the version of postgres my $iSysIdOffset = $strDbVersion >= PG_VERSION_93 ? PG_WAL_SYSTEM_ID_OFFSET_GTE_93 : PG_WAL_SYSTEM_ID_OFFSET_LT_93; # Read first 8k of WAL segment my $tBlock; # Error if the file encryption setting is not valid for the repo if (!storageRepo()->encryptionValid(storageRepo()->encrypted($strArchiveFilePath))) { confess &log(ERROR, "encryption incompatible for '$strArchiveFilePath'" . "\nHINT: Is or was the repo encrypted?", ERROR_CRYPTO); } # If the file is encrypted, then the passprase from the info file is required, else getEncryptionKeySub returns undefined my $oFileIo = storageRepo()->openRead( $strArchiveFilePath, {rhyFilter => $strArchiveFile =~ ('\.' . COMPRESS_EXT . '$') ? [{strClass => STORAGE_FILTER_GZIP, rxyParam => [{strCompressType => STORAGE_DECOMPRESS}]}] : undef, strCipherPass => $self->cipherPassSub()}); $oFileIo->read(\$tBlock, 512, true); $oFileIo->close(); # Get the required data from the file that was pulled into scalar $tBlock my ($iMagic, $iFlag, $junk, $ullDbSysId) = unpack('SSa' . $iSysIdOffset . 'Q', $tBlock); if (!defined($ullDbSysId)) { confess &log(ERROR, "unable to read database system identifier", ERROR_FILE_READ); } # Fill db section and db history section $self->dbSectionSet($strDbVersion, $ullDbSysId, $iDbHistoryId); } # If the DB section does not exist, then there were no valid directories to read from so create the DB and History sections. if (!$self->test(INFO_ARCHIVE_SECTION_DB)) { $self->create($strCurrentDbVersion, $ullCurrentDbSysId, false); } # Else if it does exist but does not match the current DB, then update the DB section else { # Turn off console logging to control when to display the error logDisable(); eval { $self->check($strCurrentDbVersion, $ullCurrentDbSysId, false); logEnable(); return true; } or do { # Reset the console logging logEnable(); # Confess unhandled errors confess $EVAL_ERROR if (exceptionCode($EVAL_ERROR) != ERROR_ARCHIVE_MISMATCH); # Update the DB section if it does not match the current database $self->dbSectionSet($strCurrentDbVersion, $ullCurrentDbSysId, $self->dbHistoryIdGet(false)+1); }; } # Return from function and log return values if any return logDebugReturn ( $strOperation, {name => 'strInvalidFileStructure', value => $strInvalidFileStructure} ); } #################################################################################################################################### # dbHistoryIdGet # # Get the db history ID #################################################################################################################################### sub dbHistoryIdGet { my $self = shift; # Assign function parameters, defaults, and log debug info my ( $strOperation, $bFileRequired, ) = logDebugParam ( __PACKAGE__ . '->dbHistoryIdGet', \@_, {name => 'bFileRequired', default => true}, ); # Confirm the info file exists if it is required if ($bFileRequired) { $self->confirmExists(); } # If the DB section does not exist, initialize the history to one, else return the latest ID my $iDbHistoryId = (!$self->test(INFO_ARCHIVE_SECTION_DB)) ? 1 : $self->numericGet(INFO_ARCHIVE_SECTION_DB, INFO_ARCHIVE_KEY_DB_ID); # Return from function and log return values if any return logDebugReturn ( $strOperation, {name => 'iDbHistoryId', value => $iDbHistoryId} ); } #################################################################################################################################### # dbHistoryList # # Get the data from the db history section. #################################################################################################################################### sub dbHistoryList { my $self = shift; my ( $strOperation, ) = logDebugParam ( __PACKAGE__ . '->dbHistoryList', ); my %hDbHash; foreach my $iHistoryId ($self->keys(INFO_ARCHIVE_SECTION_DB_HISTORY)) { $hDbHash{$iHistoryId}{&INFO_DB_VERSION} = $self->get(INFO_ARCHIVE_SECTION_DB_HISTORY, $iHistoryId, INFO_ARCHIVE_KEY_DB_VERSION); $hDbHash{$iHistoryId}{&INFO_SYSTEM_ID} = $self->get(INFO_ARCHIVE_SECTION_DB_HISTORY, $iHistoryId, INFO_ARCHIVE_KEY_DB_ID); } # Return from function and log return values if any return logDebugReturn ( $strOperation, {name => 'hDbHash', value => \%hDbHash} ); } #################################################################################################################################### # dbSectionSet # # Set the db and db:history sections. #################################################################################################################################### sub dbSectionSet { my $self = shift; # Assign function parameters, defaults, and log debug info my ( $strOperation, $strDbVersion, $ullDbSysId, $iDbHistoryId, ) = logDebugParam ( __PACKAGE__ . '->dbSectionSet', \@_, {name => 'strDbVersion', trace => true}, {name => 'ullDbSysId', trace => true}, {name => 'iDbHistoryId', trace => true} ); # Fill db section $self->numericSet(INFO_ARCHIVE_SECTION_DB, INFO_ARCHIVE_KEY_DB_SYSTEM_ID, undef, $ullDbSysId); $self->set(INFO_ARCHIVE_SECTION_DB, INFO_ARCHIVE_KEY_DB_VERSION, undef, $strDbVersion); $self->numericSet(INFO_ARCHIVE_SECTION_DB, INFO_ARCHIVE_KEY_DB_ID, undef, $iDbHistoryId); # Fill db history $self->numericSet(INFO_ARCHIVE_SECTION_DB_HISTORY, $iDbHistoryId, INFO_ARCHIVE_KEY_DB_ID, $ullDbSysId); $self->set(INFO_ARCHIVE_SECTION_DB_HISTORY, $iDbHistoryId, INFO_ARCHIVE_KEY_DB_VERSION, $strDbVersion); # Return from function and log return values if any return logDebugReturn($strOperation); } #################################################################################################################################### # confirmExists # # Ensure that the archive.info file and the db section exist. #################################################################################################################################### sub confirmExists { my $self = shift; # Confirm the file exists and the DB section is filled out if (!$self->test(INFO_ARCHIVE_SECTION_DB) || !$self->{bExists}) { confess &log(ERROR, $strArchiveInfoMissingMsg, ERROR_FILE_MISSING); } } 1;