1####################################################################################################################################
2# ARCHIVE INFO MODULE
3#
4# The archive.info file is created when archiving begins. It is located under the stanza directory. The file contains information
5# regarding the stanza database version, database WAL segment system id and other information to ensure that archiving is being
6# performed on the proper database.
7####################################################################################################################################
8package pgBackRestTest::Env::ArchiveInfo;
9use parent 'pgBackRestDoc::Common::Ini';
10
11use strict;
12use warnings FATAL => qw(all);
13use Carp qw(confess);
14use English '-no_match_vars';
15
16use Exporter qw(import);
17    our @EXPORT = qw();
18use File::Basename qw(dirname basename);
19
20use pgBackRestDoc::Common::Exception;
21use pgBackRestDoc::Common::Ini;
22use pgBackRestDoc::Common::Log;
23
24use pgBackRestTest::Common::DbVersion;
25use pgBackRestTest::Common::StorageBase;
26use pgBackRestTest::Common::StorageRepo;
27use pgBackRestTest::Env::InfoCommon;
28use pgBackRestTest::Env::Manifest;
29
30####################################################################################################################################
31# File/path constants
32####################################################################################################################################
33use constant ARCHIVE_INFO_FILE                                      => 'archive.info';
34    push @EXPORT, qw(ARCHIVE_INFO_FILE);
35
36####################################################################################################################################
37# RegEx constants
38####################################################################################################################################
39use constant REGEX_ARCHIVE_DIR_DB_VERSION                           => '^[0-9]+(\.[0-9]+)*-[0-9]+$';
40    push @EXPORT, qw(REGEX_ARCHIVE_DIR_DB_VERSION);
41use constant REGEX_ARCHIVE_DIR_WAL                                  => '^[0-F]{16}$';
42    push @EXPORT, qw(REGEX_ARCHIVE_DIR_WAL);
43
44####################################################################################################################################
45# WAL segment size
46####################################################################################################################################
47use constant PG_WAL_SEGMENT_SIZE                                    => 16777216;
48    push @EXPORT, qw(PG_WAL_SEGMENT_SIZE);
49
50####################################################################################################################################
51# Archive info constants
52####################################################################################################################################
53use constant INFO_ARCHIVE_SECTION_DB                                => INFO_BACKUP_SECTION_DB;
54    push @EXPORT, qw(INFO_ARCHIVE_SECTION_DB);
55use constant INFO_ARCHIVE_SECTION_DB_HISTORY                        => INFO_BACKUP_SECTION_DB_HISTORY;
56    push @EXPORT, qw(INFO_ARCHIVE_SECTION_DB_HISTORY);
57
58use constant INFO_ARCHIVE_KEY_DB_VERSION                            => MANIFEST_KEY_DB_VERSION;
59    push @EXPORT, qw(INFO_ARCHIVE_KEY_DB_VERSION);
60use constant INFO_ARCHIVE_KEY_DB_ID                                 => MANIFEST_KEY_DB_ID;
61    push @EXPORT, qw(INFO_ARCHIVE_KEY_DB_ID);
62use constant INFO_ARCHIVE_KEY_DB_SYSTEM_ID                          => MANIFEST_KEY_SYSTEM_ID;
63    push @EXPORT, qw(INFO_ARCHIVE_KEY_DB_SYSTEM_ID);
64
65####################################################################################################################################
66# Global variables
67####################################################################################################################################
68my $strArchiveInfoMissingMsg =
69    ARCHIVE_INFO_FILE . " does not exist but is required to push/get WAL segments\n" .
70    "HINT: is archive_command configured in postgresql.conf?\n" .
71    "HINT: has a stanza-create been performed?\n" .
72    "HINT: use --no-archive-check to disable archive checks during backup if you have an alternate archiving scheme.";
73
74####################################################################################################################################
75# CONSTRUCTOR
76####################################################################################################################################
77sub new
78{
79    my $class = shift;                              # Class name
80
81    # Assign function parameters, defaults, and log debug info
82    my
83    (
84        $strOperation,
85        $strArchiveClusterPath,                     # Archive cluster path
86        $bRequired,                                 # Is archive info required?
87        $bLoad,                                     # Should the file attempt to be loaded?
88        $bIgnoreMissing,                            # Don't error on missing files
89        $strCipherPassSub,                          # Passphrase to encrypt the subsequent archive files if repo is encrypted
90    ) =
91        logDebugParam
92        (
93            __PACKAGE__ . '->new', \@_,
94            {name => 'strArchiveClusterPath'},
95            {name => 'bRequired', default => true},
96            {name => 'bLoad', optional => true, default => true},
97            {name => 'bIgnoreMissing', optional => true, default => false},
98            {name => 'strCipherPassSub', optional => true},
99        );
100
101    # Build the archive info path/file name
102    my $strArchiveInfoFile = "${strArchiveClusterPath}/" . ARCHIVE_INFO_FILE;
103    my $self = {};
104    my $iResult = 0;
105    my $strResultMessage;
106
107    # Init object and store variables
108    eval
109    {
110        $self = $class->SUPER::new(
111            storageRepo(), $strArchiveInfoFile,
112                {bLoad => $bLoad, bIgnoreMissing => $bIgnoreMissing, strCipherPassSub => $strCipherPassSub});
113        return true;
114    }
115    or do
116    {
117        # Capture error information
118        $iResult = exceptionCode($EVAL_ERROR);
119        $strResultMessage = exceptionMessage($EVAL_ERROR);
120    };
121
122    if ($iResult != 0)
123    {
124        # If the file does not exist but is required to exist, then error
125        # The archive info is only allowed not to exist when running a stanza-create on a new install
126        if ($iResult == ERROR_FILE_MISSING)
127        {
128            if ($bRequired)
129            {
130                confess &log(ERROR, $strArchiveInfoMissingMsg, ERROR_FILE_MISSING);
131            }
132        }
133        elsif ($iResult == ERROR_CRYPTO && $strResultMessage =~ "^unable to flush")
134        {
135            confess &log(ERROR, "unable to parse '$strArchiveInfoFile'\nHINT: is or was the repo encrypted?", $iResult);
136        }
137        else
138        {
139            confess $EVAL_ERROR;
140        }
141    }
142
143    $self->{strArchiveClusterPath} = $strArchiveClusterPath;
144
145    # Return from function and log return values if any
146    return logDebugReturn
147    (
148        $strOperation,
149        {name => 'self', value => $self}
150    );
151}
152
153####################################################################################################################################
154# check
155#
156# Check archive info file and make sure it is compatible with the current version of the database for the stanza. If the file does
157# not exist an error will occur.
158####################################################################################################################################
159sub check
160{
161    my $self = shift;
162
163    # Assign function parameters, defaults, and log debug info
164    my
165    (
166        $strOperation,
167        $strDbVersion,
168        $ullDbSysId,
169        $bRequired,
170    ) =
171        logDebugParam
172        (
173            __PACKAGE__ . '->check', \@_,
174            {name => 'strDbVersion'},
175            {name => 'ullDbSysId'},
176            {name => 'bRequired', default => true},
177        );
178
179    # ??? remove bRequired after stanza-upgrade
180    if ($bRequired)
181    {
182        # Confirm the info file exists with the DB section
183        $self->confirmExists();
184    }
185
186    my $strError = undef;
187
188    if (!$self->test(INFO_ARCHIVE_SECTION_DB, INFO_ARCHIVE_KEY_DB_VERSION, undef, $strDbVersion))
189    {
190        $strError = "WAL segment version ${strDbVersion} does not match archive version " .
191                    $self->get(INFO_ARCHIVE_SECTION_DB, INFO_ARCHIVE_KEY_DB_VERSION);
192    }
193
194    if (!$self->test(INFO_ARCHIVE_SECTION_DB, INFO_ARCHIVE_KEY_DB_SYSTEM_ID, undef, $ullDbSysId))
195    {
196        $strError = (defined($strError) ? ($strError . "\n") : "") .
197                    "WAL segment system-id ${ullDbSysId} does not match archive system-id " .
198                    $self->get(INFO_ARCHIVE_SECTION_DB, INFO_ARCHIVE_KEY_DB_SYSTEM_ID);
199    }
200
201    if (defined($strError))
202    {
203        confess &log(ERROR, "${strError}\nHINT: are you archiving to the correct stanza?", ERROR_ARCHIVE_MISMATCH);
204    }
205
206    # Return from function and log return values if any
207    return logDebugReturn
208    (
209        $strOperation,
210        {name => 'strArchiveId', value => $self->archiveId()}
211    );
212}
213
214####################################################################################################################################
215# archiveId
216#
217# Get the archive id which is a combination of the DB version and the db-id setting (e.g. 9.4-1)
218####################################################################################################################################
219sub archiveId
220{
221    my $self = shift;
222
223    # Assign function parameters, defaults, and log debug info
224    my
225    (
226        $strOperation,
227        $strDbVersion,
228        $ullDbSysId,
229    ) = logDebugParam
230        (
231            __PACKAGE__ . '->archiveId', \@_,
232            {name => 'strDbVersion', optional => true},
233            {name => 'ullDbSysId', optional => true},
234        );
235
236    my $strArchiveId = undef;
237
238    # If neither optional version and system-id are passed then set the archive id to the current one
239    if (!defined($strDbVersion) && !defined($ullDbSysId))
240    {
241        $strArchiveId = $self->get(INFO_ARCHIVE_SECTION_DB, INFO_ARCHIVE_KEY_DB_VERSION) . "-" .
242            $self->get(INFO_ARCHIVE_SECTION_DB, INFO_ARCHIVE_KEY_DB_ID);
243    }
244    # If both the optional version and system-id are passed
245    elsif (defined($strDbVersion) && defined($ullDbSysId))
246    {
247        # Get the newest archiveId for the version/system-id passed
248        $strArchiveId = ($self->archiveIdList($strDbVersion, $ullDbSysId))[0];
249    }
250
251    # Return from function and log return values if any
252    return logDebugReturn
253    (
254        $strOperation,
255        {name => 'strArchiveId', value => $strArchiveId}
256    );
257}
258
259####################################################################################################################################
260# archiveIdList
261#
262# Get a sorted list of the archive ids for the db-version and db-system-id passed.
263####################################################################################################################################
264sub archiveIdList
265{
266    my $self = shift;
267
268    # Assign function parameters, defaults, and log debug info
269    my
270    (
271        $strOperation,
272        $strDbVersion,
273        $ullDbSysId,
274    ) = logDebugParam
275        (
276            __PACKAGE__ . '->archiveIdList', \@_,
277            {name => 'strDbVersion'},
278            {name => 'ullDbSysId'},
279        );
280
281    my @stryArchiveId;
282
283    # Get the version and system-id for all known databases
284    my $hDbList = $self->dbHistoryList();
285
286    foreach my $iDbHistoryId (sort  {$a <=> $b} keys %$hDbList)
287    {
288        # If the version and system-id match then construct the archive id so that the constructed array has the newest match first
289        if (($hDbList->{$iDbHistoryId}{&INFO_DB_VERSION} eq $strDbVersion) &&
290            ($hDbList->{$iDbHistoryId}{&INFO_SYSTEM_ID} eq $ullDbSysId))
291        {
292            unshift(@stryArchiveId, $strDbVersion . "-" . $iDbHistoryId);
293        }
294    }
295
296    # If the archive id has still not been found, then error
297    if (@stryArchiveId == 0)
298    {
299        confess &log(
300            ERROR, "unable to retrieve the archive id for database version '$strDbVersion' and system-id '$ullDbSysId'",
301            ERROR_ARCHIVE_MISMATCH);
302    }
303
304    # Return from function and log return values if any
305    return logDebugReturn
306    (
307        $strOperation,
308        {name => 'stryArchiveId', value => \@stryArchiveId}
309    );
310}
311
312####################################################################################################################################
313# create
314#
315# Creates the archive.info file. WARNING - this function should only be called from stanza-create or tests.
316####################################################################################################################################
317sub create
318{
319    my $self = shift;
320
321    # Assign function parameters, defaults, and log debug info
322    my
323    (
324        $strOperation,
325        $strDbVersion,
326        $ullDbSysId,
327        $bSave,
328    ) =
329        logDebugParam
330        (
331            __PACKAGE__ . '->create', \@_,
332            {name => 'strDbVersion'},
333            {name => 'ullDbSysId'},
334            {name => 'bSave', default => true},
335        );
336
337    # Fill db section and db history section
338    $self->dbSectionSet($strDbVersion, $ullDbSysId, $self->dbHistoryIdGet(false));
339
340    if ($bSave)
341    {
342        $self->save();
343    }
344
345    # Return from function and log return values if any
346    return logDebugReturn($strOperation);
347}
348
349####################################################################################################################################
350# dbHistoryIdGet
351#
352# Get the db history ID
353####################################################################################################################################
354sub dbHistoryIdGet
355{
356    my $self = shift;
357
358    # Assign function parameters, defaults, and log debug info
359    my
360    (
361        $strOperation,
362        $bFileRequired,
363    ) =
364        logDebugParam
365    (
366        __PACKAGE__ . '->dbHistoryIdGet', \@_,
367        {name => 'bFileRequired', default => true},
368    );
369
370    # Confirm the info file exists if it is required
371    if ($bFileRequired)
372    {
373        $self->confirmExists();
374    }
375
376    # If the DB section does not exist, initialize the history to one, else return the latest ID
377    my $iDbHistoryId = (!$self->test(INFO_ARCHIVE_SECTION_DB))
378                        ? 1 : $self->numericGet(INFO_ARCHIVE_SECTION_DB, INFO_ARCHIVE_KEY_DB_ID);
379
380    # Return from function and log return values if any
381    return logDebugReturn
382    (
383        $strOperation,
384        {name => 'iDbHistoryId', value => $iDbHistoryId}
385    );
386}
387
388####################################################################################################################################
389# dbHistoryList
390#
391# Get the data from the db history section.
392####################################################################################################################################
393sub dbHistoryList
394{
395    my $self = shift;
396    my
397    (
398        $strOperation,
399    ) = logDebugParam
400        (
401            __PACKAGE__ . '->dbHistoryList',
402        );
403
404    my %hDbHash;
405
406    foreach my $iHistoryId ($self->keys(INFO_ARCHIVE_SECTION_DB_HISTORY))
407    {
408        $hDbHash{$iHistoryId}{&INFO_DB_VERSION} =
409            $self->get(INFO_ARCHIVE_SECTION_DB_HISTORY, $iHistoryId, INFO_ARCHIVE_KEY_DB_VERSION);
410        $hDbHash{$iHistoryId}{&INFO_SYSTEM_ID} =
411            $self->get(INFO_ARCHIVE_SECTION_DB_HISTORY, $iHistoryId, INFO_ARCHIVE_KEY_DB_ID);
412    }
413
414    # Return from function and log return values if any
415    return logDebugReturn
416    (
417        $strOperation,
418        {name => 'hDbHash', value => \%hDbHash}
419    );
420}
421
422####################################################################################################################################
423# dbSectionSet
424#
425# Set the db and db:history sections.
426####################################################################################################################################
427sub dbSectionSet
428{
429    my $self = shift;
430
431    # Assign function parameters, defaults, and log debug info
432    my
433    (
434        $strOperation,
435        $strDbVersion,
436        $ullDbSysId,
437        $iDbHistoryId,
438    ) =
439        logDebugParam
440        (
441            __PACKAGE__ . '->dbSectionSet', \@_,
442            {name => 'strDbVersion', trace => true},
443            {name => 'ullDbSysId', trace => true},
444            {name => 'iDbHistoryId', trace => true}
445        );
446
447    # Fill db section
448    $self->numericSet(INFO_ARCHIVE_SECTION_DB, INFO_ARCHIVE_KEY_DB_SYSTEM_ID, undef, $ullDbSysId);
449    # Force the version to a string since newer versions of JSON::PP lose track of the fact that it is one
450    $self->set(INFO_ARCHIVE_SECTION_DB, INFO_ARCHIVE_KEY_DB_VERSION, undef, $strDbVersion . '');
451    $self->numericSet(INFO_ARCHIVE_SECTION_DB, INFO_ARCHIVE_KEY_DB_ID, undef, $iDbHistoryId);
452
453    # Fill db history
454    $self->numericSet(INFO_ARCHIVE_SECTION_DB_HISTORY, $iDbHistoryId, INFO_ARCHIVE_KEY_DB_ID, $ullDbSysId);
455    # Force the version to a string since newer versions of JSON::PP lose track of the fact that it is one
456    $self->set(INFO_ARCHIVE_SECTION_DB_HISTORY, $iDbHistoryId, INFO_ARCHIVE_KEY_DB_VERSION, $strDbVersion . '');
457
458    # Return from function and log return values if any
459    return logDebugReturn($strOperation);
460}
461
462####################################################################################################################################
463# confirmExists
464#
465# Ensure that the archive.info file and the db section exist.
466####################################################################################################################################
467sub confirmExists
468{
469    my $self = shift;
470
471    # Confirm the file exists and the DB section is filled out
472    if (!$self->test(INFO_ARCHIVE_SECTION_DB) || !$self->{bExists})
473    {
474        confess &log(ERROR, $strArchiveInfoMissingMsg, ERROR_FILE_MISSING);
475    }
476}
477
4781;
479