From 205525b60780587366b9786750535a55977f0221 Mon Sep 17 00:00:00 2001 From: Cynthia Shang Date: Thu, 13 Dec 2018 16:22:34 -0500 Subject: [PATCH] Migrate local info command to C. The info command will only be executed in C if the repository is local, i.e. not located on a remote repository host. S3 is considered "local" in this case. This is a direct migration from Perl to integrate as seamlessly with the remaining Perl code as possible. It should not be possible to determine if the C version is running unless debug-level logging is enabled. Contributed by Cynthia Shang. --- doc/xml/release.xml | 8 + src/Makefile | 3 + src/command/archive/common.c | 2 + src/command/archive/common.h | 6 + src/command/info/info.c | 643 ++++++++++++++++++++++ src/command/info/info.h | 12 + test/define.yaml | 7 + test/src/module/command/infoTest.c | 853 +++++++++++++++++++++++++++++ 8 files changed, 1534 insertions(+) create mode 100644 src/command/info/info.c create mode 100644 src/command/info/info.h create mode 100644 test/src/module/command/infoTest.c diff --git a/doc/xml/release.xml b/doc/xml/release.xml index d10894426..87067244c 100644 --- a/doc/xml/release.xml +++ b/doc/xml/release.xml @@ -63,6 +63,14 @@

Enable S3 storage and encryption for archive-get command in C.

+ + + + + +

Migrate local info command to C.

+
+

Add S3 storage driver.

diff --git a/src/Makefile b/src/Makefile index 19c5d4e98..aa9d41954 100644 --- a/src/Makefile +++ b/src/Makefile @@ -183,6 +183,9 @@ command/control/control.o: command/control/control.c command/control/control.h c command/help/help.o: command/help/help.c common/assert.h common/debug.h common/error.auto.h common/error.h common/io/handle.h common/lock.h common/log.h common/logLevel.h common/memContext.h common/stackTrace.h common/time.h common/type/buffer.h common/type/convert.h common/type/keyValue.h common/type/string.h common/type/stringList.h common/type/variant.h common/type/variantList.h config/config.auto.h config/config.h config/define.auto.h config/define.h version.h $(CC) $(CFLAGS) -c command/help/help.c -o command/help/help.o +command/info/info.o: command/info/info.c command/archive/common.h command/info/info.h common/debug.h common/error.auto.h common/error.h common/ini.h common/io/filter/filter.h common/io/filter/group.h common/io/handle.h common/io/read.h common/io/write.h common/lock.h common/log.h common/logLevel.h common/memContext.h common/stackTrace.h common/time.h common/type/buffer.h common/type/convert.h common/type/json.h common/type/keyValue.h common/type/string.h common/type/stringList.h common/type/variant.h common/type/variantList.h config/config.auto.h config/config.h config/define.auto.h config/define.h crypto/crypto.h crypto/hash.h info/info.h info/infoArchive.h info/infoBackup.h info/infoPg.h perl/exec.h postgres/interface.h storage/fileRead.h storage/fileWrite.h storage/helper.h storage/info.h storage/storage.h + $(CC) $(CFLAGS) -c command/info/info.c -o command/info/info.o + common/debug.o: common/debug.c common/debug.h common/logLevel.h common/stackTrace.h common/type/convert.h $(CC) $(CFLAGS) -c common/debug.c -o common/debug.o diff --git a/src/command/archive/common.c b/src/command/archive/common.c index 3c859d1d4..ca85c5ae6 100644 --- a/src/command/archive/common.c +++ b/src/command/archive/common.c @@ -21,6 +21,8 @@ WAL segment constants ***********************************************************************************************************************************/ STRING_EXTERN(WAL_SEGMENT_REGEXP_STR, WAL_SEGMENT_REGEXP); STRING_EXTERN(WAL_SEGMENT_PARTIAL_REGEXP_STR, WAL_SEGMENT_PARTIAL_REGEXP); +STRING_EXTERN(WAL_SEGMENT_DIR_REGEXP_STR, WAL_SEGMENT_DIR_REGEXP); +STRING_EXTERN(WAL_SEGMENT_FILE_REGEXP_STR, WAL_SEGMENT_FILE_REGEXP); /*********************************************************************************************************************************** Check for ok/error status files in the spool in/out directory diff --git a/src/command/archive/common.h b/src/command/archive/common.h index eaba41a02..efce1dcea 100644 --- a/src/command/archive/common.h +++ b/src/command/archive/common.h @@ -40,6 +40,12 @@ WAL segment constants // Defines the size of standard WAL segment name -- hopefully this won't change #define WAL_SEGMENT_NAME_SIZE ((unsigned int)24) +// WAL segment directory/file +#define WAL_SEGMENT_DIR_REGEXP "^[0-F]{16}$" + STRING_DECLARE(WAL_SEGMENT_DIR_REGEXP_STR); +#define WAL_SEGMENT_FILE_REGEXP "^[0-F]{24}-[0-f]{40}(\\.gz){0,1}$" + STRING_DECLARE(WAL_SEGMENT_FILE_REGEXP_STR); + /*********************************************************************************************************************************** Functions ***********************************************************************************************************************************/ diff --git a/src/command/info/info.c b/src/command/info/info.c new file mode 100644 index 000000000..81b7359e6 --- /dev/null +++ b/src/command/info/info.c @@ -0,0 +1,643 @@ +/*********************************************************************************************************************************** +Info Command +***********************************************************************************************************************************/ +#include +#include +#include +#include + +#include "command/archive/common.h" +#include "command/info/info.h" +#include "common/debug.h" +#include "common/io/handle.h" +#include "common/log.h" +#include "common/memContext.h" +#include "common/type/json.h" +#include "config/config.h" +#include "info/info.h" +#include "info/infoArchive.h" +#include "info/infoBackup.h" +#include "info/infoPg.h" +#include "perl/exec.h" +#include "postgres/interface.h" +#include "storage/helper.h" + +/*********************************************************************************************************************************** +Constants +***********************************************************************************************************************************/ +STRING_STATIC(CFGOPTVAL_INFO_OUTPUT_TEXT_STR, "text"); + +// Naming convention: _KEY__STR. If the key exists in multiple sections, then _ is omitted. +STRING_STATIC(ARCHIVE_KEY_MIN_STR, "min"); +STRING_STATIC(ARCHIVE_KEY_MAX_STR, "max"); +STRING_STATIC(BACKREST_KEY_FORMAT_STR, "format"); +STRING_STATIC(BACKREST_KEY_VERSION_STR, "version"); +STRING_STATIC(BACKUP_KEY_BACKREST_STR, "backrest"); +STRING_STATIC(BACKUP_KEY_INFO_STR, "info"); +STRING_STATIC(BACKUP_KEY_LABEL_STR, "label"); +STRING_STATIC(BACKUP_KEY_PRIOR_STR, "prior"); +STRING_STATIC(BACKUP_KEY_REFERENCE_STR, "reference"); +STRING_STATIC(BACKUP_KEY_TIMESTAMP_STR, "timestamp"); +STRING_STATIC(BACKUP_KEY_TYPE_STR, "type"); +STRING_STATIC(DB_KEY_ID_STR, "id"); +STRING_STATIC(DB_KEY_SYSTEM_ID_STR, "system-id"); +STRING_STATIC(DB_KEY_VERSION_STR, "version"); +STRING_STATIC(INFO_KEY_REPOSITORY_STR, "repository"); +STRING_STATIC(KEY_ARCHIVE_STR, "archive"); +STRING_STATIC(KEY_DATABASE_STR, "database"); +STRING_STATIC(KEY_DELTA_STR, "delta"); +STRING_STATIC(KEY_SIZE_STR, "size"); +STRING_STATIC(KEY_START_STR, "start"); +STRING_STATIC(KEY_STOP_STR, "stop"); +STRING_STATIC(STANZA_KEY_BACKUP_STR, "backup"); +STRING_STATIC(STANZA_KEY_CIPHER_STR, "cipher"); +STRING_STATIC(STANZA_KEY_NAME_STR, "name"); +STRING_STATIC(STANZA_KEY_STATUS_STR, "status"); +STRING_STATIC(STANZA_KEY_DB_STR, "db"); +STRING_STATIC(STATUS_KEY_CODE_STR, "code"); +STRING_STATIC(STATUS_KEY_MESSAGE_STR, "message"); + +STRING_STATIC(INFO_STANZA_STATUS_OK, "ok"); +STRING_STATIC(INFO_STANZA_STATUS_ERROR, "error"); +#define INFO_STANZA_STATUS_CODE_OK 0 +STRING_STATIC(INFO_STANZA_STATUS_MESSAGE_OK_STR, "ok"); +#define INFO_STANZA_STATUS_CODE_MISSING_STANZA_PATH 1 +STRING_STATIC(INFO_STANZA_STATUS_MESSAGE_MISSING_STANZA_PATH_STR, "missing stanza path"); +#define INFO_STANZA_STATUS_CODE_NO_BACKUP 2 +STRING_STATIC(INFO_STANZA_STATUS_MESSAGE_NO_BACKUP_STR, "no valid backups"); +#define INFO_STANZA_STATUS_CODE_MISSING_STANZA_DATA 3 +STRING_STATIC(INFO_STANZA_STATUS_MESSAGE_MISSING_STANZA_DATA_STR, "missing stanza data"); + +/*********************************************************************************************************************************** +Set error status code and message for the stanza to the code and message passed. +***********************************************************************************************************************************/ +static void +stanzaStatus(const int code, const String *message, Variant *stanzaInfo) +{ + FUNCTION_TEST_BEGIN(); + FUNCTION_TEST_PARAM(INT, code); + FUNCTION_TEST_PARAM(STRING, message); + FUNCTION_TEST_PARAM(VARIANT, stanzaInfo); + + FUNCTION_TEST_ASSERT(code >= 0 && code <= 3); + FUNCTION_TEST_ASSERT(message != NULL); + FUNCTION_TEST_ASSERT(stanzaInfo != NULL); + FUNCTION_TEST_END(); + + Variant *stanzaStatus = varNewStr(STANZA_KEY_STATUS_STR); + KeyValue *statusKv = kvPutKv(varKv(stanzaInfo), stanzaStatus); + + kvAdd(statusKv, varNewStr(STATUS_KEY_CODE_STR), varNewInt(code)); + kvAdd(statusKv, varNewStr(STATUS_KEY_MESSAGE_STR), varNewStr(message)); + + FUNCTION_TEST_RESULT_VOID(); +} + +/*********************************************************************************************************************************** +Set the data for the archive section of the stanza for the database info from the backup.info file. +***********************************************************************************************************************************/ +void +archiveDbList(const String *stanza, const InfoPgData *pgData, VariantList *archiveSection, const InfoArchive *info, bool currentDb) +{ + FUNCTION_TEST_BEGIN(); + FUNCTION_TEST_PARAM(STRING, stanza); + FUNCTION_TEST_PARAM(INFO_PG_DATAP, pgData); + FUNCTION_TEST_PARAM(VARIANT, archiveSection); + FUNCTION_TEST_PARAM(BOOL, currentDb); + + FUNCTION_TEST_ASSERT(stanza != NULL); + FUNCTION_TEST_ASSERT(pgData != NULL); + FUNCTION_TEST_ASSERT(archiveSection != NULL); + FUNCTION_TEST_END(); + + // With multiple DB versions, the backup.info history-id may not be the same as archive.info history-id, so the + // archive path must be built by retrieving the archive id given the db version and system id of the backup.info file. + // If there is no match, an error will be thrown. + const String *archiveId = infoArchiveIdHistoryMatch(info, pgData->id, pgData->version, pgData->systemId); + + String *archivePath = strNewFmt("%s/%s/%s", STORAGE_REPO_ARCHIVE, strPtr(stanza), strPtr(archiveId)); + String *archiveStart = NULL; + String *archiveStop = NULL; + Variant *archiveInfo = varNewKv(); + + // Get a list of WAL directories in the archive repo from oldest to newest, if any exist + StringList *walDir = storageListP(storageRepo(), archivePath, .expression = WAL_SEGMENT_DIR_REGEXP_STR); + + if (walDir != NULL) + { + unsigned int sizeWalDir = strLstSize(walDir); + + if (sizeWalDir > 1) + walDir = strLstSort(walDir, sortOrderAsc); + + // Not every WAL dir has WAL files so check each + for (unsigned int idx = 0; idx < sizeWalDir; idx++) + { + // Get a list of all WAL in this WAL dir + StringList *list = storageListP( + storageRepo(), strNewFmt("%s/%s", strPtr(archivePath), strPtr(strLstGet(walDir, idx))), + .expression = WAL_SEGMENT_FILE_REGEXP_STR); + + // If wal segments are found, get the oldest one as the archive start + if (strLstSize(list) > 0) + { + // Sort the list from oldest to newest to get the oldest starting WAL archived for this DB + list = strLstSort(list, sortOrderAsc); + archiveStart = strSubN(strLstGet(list, 0), 0, 24); + break; + } + } + + // Iterate through the directory list in the reverse so processing newest first. Cast comparison to an int for readability. + for (unsigned int idx = sizeWalDir - 1; (int)idx > 0; idx--) + { + // Get a list of all WAL in this WAL dir + StringList *list = storageListP( + storageRepo(), strNewFmt("%s/%s", strPtr(archivePath), strPtr(strLstGet(walDir, idx))), + .expression = WAL_SEGMENT_FILE_REGEXP_STR); + + // If wal segments are found, get the newest one as the archive stop + if (strLstSize(list) > 0) + { + // Sort the list from newest to oldest to get the newest ending WAL archived for this DB + list = strLstSort(list, sortOrderDesc); + archiveStop = strSubN(strLstGet(list, 0), 0, 24); + break; + } + } + } + + // If there is an archive or the database is the current database then store it + if (currentDb || archiveStart != NULL) + { + // Add empty database section to archiveInfo and then fill in database id from the backup.info + KeyValue *databaseInfo = kvPutKv(varKv(archiveInfo), varNewStr(KEY_DATABASE_STR)); + + kvAdd(databaseInfo, varNewStr(DB_KEY_ID_STR), varNewUInt64(pgData->id)); + + kvPut(varKv(archiveInfo), varNewStr(DB_KEY_ID_STR), varNewStr(archiveId)); + kvPut( + varKv(archiveInfo), varNewStr(ARCHIVE_KEY_MIN_STR), (archiveStart != NULL ? varNewStr(archiveStart) : (Variant *)NULL)); + kvPut(varKv(archiveInfo), varNewStr(ARCHIVE_KEY_MAX_STR), (archiveStop != NULL ? varNewStr(archiveStop) : (Variant *)NULL)); + + varLstAdd(archiveSection, archiveInfo); + } + + FUNCTION_TEST_RESULT_VOID(); +} + +/*********************************************************************************************************************************** +For each current backup in the backup.info file of the stanza, set the data for the backup section. +***********************************************************************************************************************************/ +void +backupList(const String *stanza, VariantList *backupSection, InfoBackup *info) +{ + FUNCTION_TEST_BEGIN(); + FUNCTION_TEST_PARAM(STRING, stanza); + FUNCTION_TEST_PARAM(VARIANT, backupSection); + FUNCTION_TEST_PARAM(INFO_BACKUP, info); + + FUNCTION_TEST_ASSERT(stanza != NULL); + FUNCTION_TEST_ASSERT(backupSection != NULL); + FUNCTION_TEST_ASSERT(info != NULL); + FUNCTION_TEST_END(); + + // For each current backup, get the label and corresponding data and build the backup section + for (unsigned int keyIdx = 0; keyIdx < infoBackupDataTotal(info); keyIdx++) + { + // Get the backup data + InfoBackupData backupData = infoBackupData(info, keyIdx); + + Variant *backupInfo = varNewKv(); + + // main keys + kvPut(varKv(backupInfo), varNewStr(BACKUP_KEY_LABEL_STR), varNewStr(backupData.backupLabel)); + kvPut(varKv(backupInfo), varNewStr(BACKUP_KEY_TYPE_STR), varNewStr(backupData.backupType)); + kvPut( + varKv(backupInfo), varNewStr(BACKUP_KEY_PRIOR_STR), + (backupData.backupPrior != NULL ? varNewStr(backupData.backupPrior) : NULL)); + kvPut( + varKv(backupInfo), varNewStr(BACKUP_KEY_REFERENCE_STR), + (backupData.backupReference != NULL ? varNewVarLst(varLstNewStrLst(backupData.backupReference)) : NULL)); + + // archive section + KeyValue *archiveInfo = kvPutKv(varKv(backupInfo), varNewStr(KEY_ARCHIVE_STR)); + + kvAdd( + archiveInfo, varNewStr(KEY_START_STR), + (backupData.backupArchiveStart != NULL ? varNewStr(backupData.backupArchiveStart) : NULL)); + kvAdd( + archiveInfo, varNewStr(KEY_STOP_STR), + (backupData.backupArchiveStop != NULL ? varNewStr(backupData.backupArchiveStop) : NULL)); + + // backrest section + KeyValue *backrestInfo = kvPutKv(varKv(backupInfo), varNewStr(BACKUP_KEY_BACKREST_STR)); + + kvAdd(backrestInfo, varNewStr(BACKREST_KEY_FORMAT_STR), varNewUInt64(backupData.backrestFormat)); + kvAdd(backrestInfo, varNewStr(BACKREST_KEY_VERSION_STR), varNewStr(backupData.backrestVersion)); + + // database section + KeyValue *dbInfo = kvPutKv(varKv(backupInfo), varNewStr(KEY_DATABASE_STR)); + + kvAdd(dbInfo, varNewStr(DB_KEY_ID_STR), varNewUInt64(backupData.backupPgId)); + + // info section + KeyValue *infoInfo = kvPutKv(varKv(backupInfo), varNewStr(BACKUP_KEY_INFO_STR)); + + kvAdd(infoInfo, varNewStr(KEY_SIZE_STR), varNewUInt64(backupData.backupInfoSize)); + kvAdd(infoInfo, varNewStr(KEY_DELTA_STR), varNewUInt64(backupData.backupInfoSizeDelta)); + + // info:repository section + KeyValue *repoInfo = kvPutKv(infoInfo, varNewStr(INFO_KEY_REPOSITORY_STR)); + + kvAdd(repoInfo, varNewStr(KEY_SIZE_STR), varNewUInt64(backupData.backupInfoRepoSize)); + kvAdd(repoInfo, varNewStr(KEY_DELTA_STR), varNewUInt64(backupData.backupInfoRepoSizeDelta)); + + // timestamp section + KeyValue *timeInfo = kvPutKv(varKv(backupInfo), varNewStr(BACKUP_KEY_TIMESTAMP_STR)); + + kvAdd(timeInfo, varNewStr(KEY_START_STR), varNewUInt64(backupData.backupTimestampStart)); + kvAdd(timeInfo, varNewStr(KEY_STOP_STR), varNewUInt64(backupData.backupTimestampStop)); + + varLstAdd(backupSection, backupInfo); + } + + + FUNCTION_TEST_RESULT_VOID(); +} + +/*********************************************************************************************************************************** +Set the stanza data for each stanza found in the repo. +***********************************************************************************************************************************/ +static VariantList * +stanzaInfoList(const String *stanza, StringList *stanzaList) +{ + FUNCTION_TEST_BEGIN(); + FUNCTION_TEST_PARAM(STRING, stanza); + FUNCTION_TEST_PARAM(STRING_LIST, stanzaList); + + FUNCTION_TEST_ASSERT(stanzaList != NULL); + FUNCTION_TEST_END(); + + VariantList *result = varLstNew(); + bool stanzaFound = false; + + // Sort the list + stanzaList = strLstSort(stanzaList, sortOrderAsc); + + for (unsigned int idx = 0; idx < strLstSize(stanzaList); idx++) + { + String *stanzaListName = strLstGet(stanzaList, idx); + + // If a specific stanza has been requested and this is not it, then continue to the next in the list else indicate found + if (stanza != NULL) + { + if (!strEq(stanza, stanzaListName)) + continue; + else + stanzaFound = true; + } + + // Create the stanzaInfo and section variables + Variant *stanzaInfo = varNewKv(); + VariantList *dbSection = varLstNew(); + VariantList *backupSection = varLstNew(); + VariantList *archiveSection = varLstNew(); + InfoBackup *info = NULL; + + // Catch certain errors + TRY_BEGIN() + { + // Attempt to load the backup info file + info = infoBackupNew( + storageRepo(), strNewFmt("%s/%s/%s", STORAGE_REPO_BACKUP, strPtr(stanzaListName), INFO_BACKUP_FILE), false, + cipherType(cfgOptionStr(cfgOptRepoCipherType)), cfgOptionStr(cfgOptRepoCipherPass)); + } + CATCH(FileMissingError) + { + // If there is no backup.info then set the status to indicate missing + stanzaStatus( + INFO_STANZA_STATUS_CODE_MISSING_STANZA_DATA, INFO_STANZA_STATUS_MESSAGE_MISSING_STANZA_DATA_STR, stanzaInfo); + } + CATCH(CryptoError) + { + // If a reason for the error is due to a an ecryption error, add a hint + THROW_FMT( + CryptoError, + "%s\n" + "HINT: use option --stanza if encryption settings are different for the stanza than the global settings", + errorMessage()); + } + TRY_END(); + + // Set the stanza name and cipher + kvPut(varKv(stanzaInfo), varNewStr(STANZA_KEY_NAME_STR), varNewStr(stanzaListName)); + kvPut(varKv(stanzaInfo), varNewStr(STANZA_KEY_CIPHER_STR), varNewStr(cfgOptionStr(cfgOptRepoCipherType))); + + // If the backup.info file exists, get the database history information (newest to oldest) and corresponding archive + if (info != NULL) + { + for (unsigned int pgIdx = 0; pgIdx < infoPgDataTotal(infoBackupPg(info)); pgIdx++) + { + InfoPgData pgData = infoPgData(infoBackupPg(info), pgIdx); + Variant *pgInfo = varNewKv(); + + kvPut(varKv(pgInfo), varNewStr(DB_KEY_ID_STR), varNewUInt64(pgData.id)); + kvPut(varKv(pgInfo), varNewStr(DB_KEY_SYSTEM_ID_STR), varNewUInt64(pgData.systemId)); + kvPut(varKv(pgInfo), varNewStr(DB_KEY_VERSION_STR), varNewStr(pgVersionToStr(pgData.version))); + + varLstAdd(dbSection, pgInfo); + + // Get the archive info for the DB from the archive.info file + InfoArchive *info = infoArchiveNew( + storageRepo(), strNewFmt("%s/%s/%s", STORAGE_REPO_ARCHIVE, strPtr(stanzaListName), INFO_ARCHIVE_FILE), false, + cipherType(cfgOptionStr(cfgOptRepoCipherType)), cfgOptionStr(cfgOptRepoCipherPass)); + archiveDbList(stanzaListName, &pgData, archiveSection, info, (pgIdx == 0 ? true : false)); + } + + // Get data for all existing backups for this stanza + backupList(stanzaListName, backupSection, info); + } + + // Add the database history, backup and archive sections to the stanza info + kvPut(varKv(stanzaInfo), varNewStr(STANZA_KEY_DB_STR), varNewVarLst(dbSection)); + kvPut(varKv(stanzaInfo), varNewStr(STANZA_KEY_BACKUP_STR), varNewVarLst(backupSection)); + kvPut(varKv(stanzaInfo), varNewStr(KEY_ARCHIVE_STR), varNewVarLst(archiveSection)); + + // If a status has not already been set and there are no backups then set status to no backup + if (kvGet(varKv(stanzaInfo), varNewStr(STANZA_KEY_STATUS_STR)) == NULL && + varLstSize(kvGetList(varKv(stanzaInfo), varNewStr(STANZA_KEY_BACKUP_STR))) == 0) + { + stanzaStatus(INFO_STANZA_STATUS_CODE_NO_BACKUP, INFO_STANZA_STATUS_MESSAGE_NO_BACKUP_STR, stanzaInfo); + } + + // If a status has still not been set then set it to OK + if (kvGet(varKv(stanzaInfo), varNewStr(STANZA_KEY_STATUS_STR)) == NULL) + stanzaStatus(INFO_STANZA_STATUS_CODE_OK, INFO_STANZA_STATUS_MESSAGE_OK_STR, stanzaInfo); + + varLstAdd(result, stanzaInfo); + } + + // If looking for a specific stanza and it was not found, set minimum info and the status + if (stanza != NULL && !stanzaFound) + { + Variant *stanzaInfo = varNewKv(); + + kvPut(varKv(stanzaInfo), varNewStr(STANZA_KEY_NAME_STR), varNewStr(stanza)); + + kvPut(varKv(stanzaInfo), varNewStr(STANZA_KEY_DB_STR), varNewVarLst(varLstNew())); + kvPut(varKv(stanzaInfo), varNewStr(STANZA_KEY_BACKUP_STR), varNewVarLst(varLstNew())); + + stanzaStatus(INFO_STANZA_STATUS_CODE_MISSING_STANZA_PATH, INFO_STANZA_STATUS_MESSAGE_MISSING_STANZA_PATH_STR, stanzaInfo); + varLstAdd(result, stanzaInfo); + } + + FUNCTION_TEST_RESULT(VARIANT_LIST, result); +} + +/*********************************************************************************************************************************** +Format the text output for each database of the stanza. +***********************************************************************************************************************************/ +static void +formatTextDb(const KeyValue *stanzaInfo, String *resultStr) +{ + FUNCTION_TEST_BEGIN(); + FUNCTION_TEST_PARAM(KEY_VALUE, stanzaInfo); + FUNCTION_TEST_PARAM(STRING, resultStr); + + FUNCTION_TEST_ASSERT(stanzaInfo != NULL); + FUNCTION_TEST_END(); + + VariantList *dbSection = kvGetList(stanzaInfo, varNewStr(STANZA_KEY_DB_STR)); + VariantList *archiveSection = kvGetList(stanzaInfo, varNewStr(KEY_ARCHIVE_STR)); + VariantList *backupSection = kvGetList(stanzaInfo, varNewStr(STANZA_KEY_BACKUP_STR)); + + // For each database find the corresponding archive and backup info + for (unsigned int dbIdx = 0; dbIdx < varLstSize(dbSection); dbIdx++) + { + KeyValue *pgInfo = varKv(varLstGet(dbSection, dbIdx)); + uint64_t dbId = varUInt64(kvGet(pgInfo, varNewStr(DB_KEY_ID_STR))); + + // List is ordered so 0 is always the current DB index + if (dbIdx == 0) + strCat(resultStr, "\n db (current)"); + + // Get the min/max archive information for the database + String *archiveResult = strNew(""); + + for (unsigned int archiveIdx = 0; archiveIdx < varLstSize(archiveSection); archiveIdx++) + { + KeyValue *archiveInfo = varKv(varLstGet(archiveSection, archiveIdx)); + KeyValue *archiveDbInfo = varKv(kvGet(archiveInfo, varNewStr(KEY_DATABASE_STR))); + uint64_t archiveDbId = varUInt64(kvGet(archiveDbInfo, varNewStr(DB_KEY_ID_STR))); + + if (archiveDbId == dbId) + { + strCatFmt( + archiveResult, "\n wal archive min/max (%s): ", + strPtr(varStr(kvGet(archiveInfo, varNewStr(DB_KEY_ID_STR))))); + + // Get the archive min/max if there are any archives for the database + if (kvGet(archiveInfo, varNewStr(ARCHIVE_KEY_MIN_STR)) != NULL) + { + strCatFmt( + archiveResult, "%s/%s\n", strPtr(varStr(kvGet(archiveInfo, varNewStr(ARCHIVE_KEY_MIN_STR)))), + strPtr(varStr(kvGet(archiveInfo, varNewStr(ARCHIVE_KEY_MAX_STR))))); + } + else + strCat(archiveResult, "none present\n"); + } + } + + // Get the information for each current backup + String *backupResult = strNew(""); + + for (unsigned int backupIdx = 0; backupIdx < varLstSize(backupSection); backupIdx++) + { + KeyValue *backupInfo = varKv(varLstGet(backupSection, backupIdx)); + KeyValue *backupDbInfo = varKv(kvGet(backupInfo, varNewStr(KEY_DATABASE_STR))); + uint64_t backupDbId = varUInt64(kvGet(backupDbInfo, varNewStr(DB_KEY_ID_STR))); + + if (backupDbId == dbId) + { + strCatFmt( + backupResult, "\n %s backup: %s\n", strPtr(varStr(kvGet(backupInfo, varNewStr(BACKUP_KEY_TYPE_STR)))), + strPtr(varStr(kvGet(backupInfo, varNewStr(BACKUP_KEY_LABEL_STR))))); + + KeyValue *timestampInfo = varKv(kvGet(backupInfo, varNewStr(BACKUP_KEY_TIMESTAMP_STR))); + + // Get and format the backup start/stop time + static char timeBufferStart[20]; + static char timeBufferStop[20]; + time_t timeStart = (time_t) varUInt64(kvGet(timestampInfo, varNewStr(KEY_START_STR))); + time_t timeStop = (time_t) varUInt64(kvGet(timestampInfo, varNewStr(KEY_STOP_STR))); + + strftime(timeBufferStart, 20, "%Y-%m-%d %H:%M:%S", localtime(&timeStart)); + strftime(timeBufferStop, 20, "%Y-%m-%d %H:%M:%S", localtime(&timeStop)); + + strCatFmt( + backupResult, " timestamp start/stop: %s / %s\n", timeBufferStart, timeBufferStop); + strCat(backupResult, " wal start/stop: "); + + KeyValue *archiveDBackupInfo = varKv(kvGet(backupInfo, varNewStr(KEY_ARCHIVE_STR))); + + if (kvGet(archiveDBackupInfo, varNewStr(KEY_START_STR)) != NULL && + kvGet(archiveDBackupInfo, varNewStr(KEY_STOP_STR)) != NULL) + { + strCatFmt(backupResult, "%s / %s\n", strPtr(varStr(kvGet(archiveDBackupInfo, varNewStr(KEY_START_STR)))), + strPtr(varStr(kvGet(archiveDBackupInfo, varNewStr(KEY_STOP_STR))))); + } + else + strCat(backupResult, "n/a\n"); + + KeyValue *info = varKv(kvGet(backupInfo, varNewStr(BACKUP_KEY_INFO_STR))); + + strCatFmt( + backupResult, " database size: %s, backup size: %s\n", + strPtr(strSizeFormat(varUInt64Force(kvGet(info, varNewStr(KEY_SIZE_STR))))), + strPtr(strSizeFormat(varUInt64Force(kvGet(info, varNewStr(KEY_DELTA_STR)))))); + + KeyValue *repoInfo = varKv(kvGet(info, varNewStr(INFO_KEY_REPOSITORY_STR))); + + strCatFmt( + backupResult, " repository size: %s, repository backup size: %s\n", + strPtr(strSizeFormat(varUInt64Force(kvGet(repoInfo, varNewStr(KEY_SIZE_STR))))), + strPtr(strSizeFormat(varUInt64Force(kvGet(repoInfo, varNewStr(KEY_DELTA_STR)))))); + + if (kvGet(backupInfo, varNewStr(BACKUP_KEY_REFERENCE_STR)) != NULL) + { + StringList *referenceList = strLstNewVarLst(varVarLst(kvGet(backupInfo, varNewStr(BACKUP_KEY_REFERENCE_STR)))); + strCatFmt(backupResult, " backup reference list: %s\n", strPtr(strLstJoin(referenceList, ", "))); + } + } + } + + // If there is data to display, then display it. + if (strSize(archiveResult) > 0 || strSize(backupResult) > 0) + { + if (dbIdx != 0) + strCat(resultStr, "\n db (prior)"); + + if (strSize(archiveResult) > 0) + strCat(resultStr, strPtr(archiveResult)); + + if (strSize(backupResult) > 0) + strCat(resultStr, strPtr(backupResult)); + } + } + FUNCTION_TEST_RESULT_VOID(); +} + +/*********************************************************************************************************************************** +Render the information for the stanza based on the command parameters. +***********************************************************************************************************************************/ +static String * +infoRender(void) +{ + FUNCTION_DEBUG_VOID(logLevelDebug); + + String *result = NULL; + + MEM_CONTEXT_TEMP_BEGIN() + { + // Get stanza if specified + const String *stanza = cfgOptionTest(cfgOptStanza) ? cfgOptionStr(cfgOptStanza) : NULL; + + // Get a list of stanzas in the backup directory. + StringList *stanzaList = storageListP(storageRepo(), strNew(STORAGE_REPO_BACKUP), .errorOnMissing = true); + VariantList *infoList = varLstNew(); + String *resultStr = strNew(""); + + // If the backup storage exists, then search for and process any stanzas + if (strLstSize(stanzaList) > 0) + infoList = stanzaInfoList(stanza, stanzaList); + + // Format text output + if (strEq(cfgOptionStr(cfgOptOutput), CFGOPTVAL_INFO_OUTPUT_TEXT_STR)) + { + // Process any stanza directories + if (varLstSize(infoList) > 0) + { + for (unsigned int stanzaIdx = 0; stanzaIdx < varLstSize(infoList); stanzaIdx++) + { + KeyValue *stanzaInfo = varKv(varLstGet(infoList, stanzaIdx)); + + // Add a carriage return between stanzas + if (stanzaIdx > 0) + strCatFmt(resultStr, "\n"); + + // Stanza name and status + strCatFmt( + resultStr, "stanza: %s\n status: ", strPtr(varStr(kvGet(stanzaInfo, varNewStr(STANZA_KEY_NAME_STR))))); + + // If an error has occurred, provide the information that is available and move onto next stanza + KeyValue *stanzaStatus = varKv(kvGet(stanzaInfo, varNewStr(STANZA_KEY_STATUS_STR))); + int statusCode = varInt(kvGet(stanzaStatus, varNewStr(STATUS_KEY_CODE_STR))); + + if (statusCode != INFO_STANZA_STATUS_CODE_OK) + { + strCatFmt( + resultStr, "%s (%s)\n", strPtr(INFO_STANZA_STATUS_ERROR), + strPtr(varStr(kvGet(stanzaStatus, varNewStr(STATUS_KEY_MESSAGE_STR))))); + + if (statusCode == INFO_STANZA_STATUS_CODE_MISSING_STANZA_DATA || + statusCode == INFO_STANZA_STATUS_CODE_NO_BACKUP) + { + strCatFmt( + resultStr, " cipher: %s\n", strPtr(varStr(kvGet(stanzaInfo, varNewStr(STANZA_KEY_CIPHER_STR))))); + + // If there is a backup.info file but no backups, then process the archive info + if (statusCode == INFO_STANZA_STATUS_CODE_NO_BACKUP) + formatTextDb(stanzaInfo, resultStr); + } + + continue; + } + else + strCatFmt(resultStr, "%s\n", strPtr(INFO_STANZA_STATUS_OK)); + + // Cipher + strCatFmt(resultStr, " cipher: %s\n", + strPtr(varStr(kvGet(stanzaInfo, varNewStr(STANZA_KEY_CIPHER_STR))))); + + formatTextDb(stanzaInfo, resultStr); + } + } + else + resultStr = strNewFmt("No stanzas exist in %s\n", strPtr(storagePathNP(storageRepo(), NULL))); + } + // Format json output + else + resultStr = varToJson(varNewVarLst(infoList), 4); + + memContextSwitch(MEM_CONTEXT_OLD()); + result = strDup(resultStr); + memContextSwitch(MEM_CONTEXT_TEMP()); + } + MEM_CONTEXT_TEMP_END(); + + FUNCTION_DEBUG_RESULT(STRING, result); +} + +/*********************************************************************************************************************************** +Render info and output to stdout +***********************************************************************************************************************************/ +void +cmdInfo(void) +{ + FUNCTION_DEBUG_VOID(logLevelDebug); + + MEM_CONTEXT_TEMP_BEGIN() + { + if (!cfgOptionTest(cfgOptRepoHost)) // {uncovered - Perl code is covered in integration tests} + { + ioHandleWriteOneStr(STDOUT_FILENO, infoRender()); + } + // Else do it in Perl + else + perlExec(); // {+uncovered} + } + MEM_CONTEXT_TEMP_END(); + + FUNCTION_DEBUG_RESULT_VOID(); +} diff --git a/src/command/info/info.h b/src/command/info/info.h new file mode 100644 index 000000000..0ab728a59 --- /dev/null +++ b/src/command/info/info.h @@ -0,0 +1,12 @@ +/*********************************************************************************************************************************** +Info Command +***********************************************************************************************************************************/ +#ifndef COMMAND_INFO_INFO_H +#define COMMAND_INFO_INFO_H + +/*********************************************************************************************************************************** +Functions +***********************************************************************************************************************************/ +void cmdInfo(void); + +#endif diff --git a/test/define.yaml b/test/define.yaml index ca7e2d5a6..7e3e7eccb 100644 --- a/test/define.yaml +++ b/test/define.yaml @@ -612,6 +612,13 @@ unit: coverage: command/control/control: full + # ---------------------------------------------------------------------------------------------------------------------------- + - name: info + total: 3 + + coverage: + command/info/info: full + # ******************************************************************************************************************************** - name: archive diff --git a/test/src/module/command/infoTest.c b/test/src/module/command/infoTest.c new file mode 100644 index 000000000..a9f96a0ec --- /dev/null +++ b/test/src/module/command/infoTest.c @@ -0,0 +1,853 @@ +/*********************************************************************************************************************************** +Test Info Command +***********************************************************************************************************************************/ +#include "storage/driver/posix/storage.h" + +#include "common/harnessConfig.h" + +/*********************************************************************************************************************************** +Test Run +***********************************************************************************************************************************/ +void +testRun(void) +{ + FUNCTION_HARNESS_VOID(); + + // Create the repo directories + String *repoPath = strNewFmt("%s/repo", testPath()); + String *archivePath = strNewFmt("%s/%s", strPtr(repoPath), "archive"); + String *backupPath = strNewFmt("%s/%s", strPtr(repoPath), "backup"); + String *archiveStanza1Path = strNewFmt("%s/stanza1", strPtr(archivePath)); + String *backupStanza1Path = strNewFmt("%s/stanza1", strPtr(backupPath)); + + // ***************************************************************************************************************************** + if (testBegin("infoRender()")) + { + StringList *argList = strLstNew(); + strLstAddZ(argList, "pgbackrest"); + strLstAdd(argList, strNewFmt("--repo-path=%s", strPtr(repoPath))); + strLstAddZ(argList, "info"); + StringList *argListText = strLstDup(argList); + + strLstAddZ(argList, "--output=json"); + harnessCfgLoad(strLstSize(argList), strLstPtr(argList)); + + // No repo path + //-------------------------------------------------------------------------------------------------------------------------- + TEST_ERROR_FMT( + infoRender(), PathOpenError, + "unable to open path '%s' for read: [2] No such file or directory", strPtr(backupPath)); + + storagePathCreateNP(storageLocalWrite(), archivePath); + storagePathCreateNP(storageLocalWrite(), backupPath); + + // No stanzas have been created + //-------------------------------------------------------------------------------------------------------------------------- + TEST_RESULT_STR(strPtr(infoRender()), "[]\n", "json - repo but no stanzas"); + + harnessCfgLoad(strLstSize(argListText), strLstPtr(argListText)); + TEST_RESULT_STR(strPtr(infoRender()), + strPtr(strNewFmt("No stanzas exist in %s\n", strPtr(storagePathNP(storageRepo(), NULL)))), "text - no stanzas"); + + // Empty stanza + //-------------------------------------------------------------------------------------------------------------------------- + TEST_RESULT_VOID(storagePathCreateNP(storageLocalWrite(), backupStanza1Path), "backup stanza1 directory"); + TEST_RESULT_VOID(storagePathCreateNP(storageLocalWrite(), archiveStanza1Path), "archive stanza1 directory"); + TEST_RESULT_STR(strPtr(infoRender()), + "stanza: stanza1\n" + " status: error (missing stanza data)\n" + " cipher: none\n", "text - missing stanza data"); + + harnessCfgLoad(strLstSize(argList), strLstPtr(argList)); + TEST_RESULT_STR(strPtr(infoRender()), + "[\n" + " {\n" + " \"archive\" : [],\n" + " \"backup\" : [],\n" + " \"cipher\" : \"none\",\n" + " \"db\" : [],\n" + " \"name\" : \"stanza1\",\n" + " \"status\" : {\n" + " \"code\" : 3,\n" + " \"message\" : \"missing stanza data\"\n" + " }\n" + " }\n" + "]\n", "json - missing stanza data"); + + // backup.info file exists, but archive.info does not + //-------------------------------------------------------------------------------------------------------------------------- + String *content = strNew + ( + "[backrest]\n" + "backrest-checksum=\"51774ffab293c5cfb07511d7d2e101e92416f4ed\"\n" + "backrest-format=5\n" + "backrest-version=\"2.04\"\n" + "\n" + "[db]\n" + "db-catalog-version=201409291\n" + "db-control-version=942\n" + "db-id=2\n" + "db-system-id=6569239123849665679\n" + "db-version=\"9.4\"\n" + "\n" + "[db:history]\n" + "1={\"db-catalog-version\":201306121,\"db-control-version\":937,\"db-system-id\":6569239123849665666," + "\"db-version\":\"9.3\"}\n" + "2={\"db-catalog-version\":201409291,\"db-control-version\":942,\"db-system-id\":6569239123849665679," + "\"db-version\":\"9.4\"}\n" + ); + + TEST_RESULT_VOID( + storagePutNP(storageNewWriteNP(storageLocalWrite(), strNewFmt("%s/backup.info", strPtr(backupStanza1Path))), + bufNewStr(content)), "put backup info to file"); + + TEST_ERROR_FMT(infoRender(), FileMissingError, + "unable to load info file '%s/archive.info' or '%s/archive.info.copy':\n" + "FileMissingError: unable to open '%s/archive.info' for read: [2] No such file or directory\n" + "FileMissingError: unable to open '%s/archive.info.copy' for read: [2] No such file or directory\n" + "HINT: archive.info cannot be opened but is required to push/get WAL segments.\n" + "HINT: is archive_command configured correctly 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.", + strPtr(archiveStanza1Path), strPtr(archiveStanza1Path), strPtr(archiveStanza1Path), strPtr(archiveStanza1Path)); + + // backup.info/archive.info files exist, mismatched db ids, no backup:current section so no valid backups + // Only the current db information from the db:history will be processed. + //-------------------------------------------------------------------------------------------------------------------------- + content = strNew + ( + "[backrest]\n" + "backrest-checksum=\"0da11608456bae64c42cc1dc8df4ae79b953d597\"\n" + "backrest-format=5\n" + "backrest-version=\"2.04\"\n" + "\n" + "[db]\n" + "db-id=1\n" + "db-system-id=6569239123849665679\n" + "db-version=\"9.4\"\n" + "\n" + "[db:history]\n" + "1={\"db-id\":6569239123849665679,\"db-version\":\"9.4\"}\n" + "2={\"db-id\":6569239123849665666,\"db-version\":\"9.3\"}\n" + "3={\"db-id\":6569239123849665679,\"db-version\":\"9.4\"}\n" + ); + + TEST_RESULT_VOID( + storagePutNP(storageNewWriteNP(storageLocalWrite(), strNewFmt("%s/archive.info", strPtr(archiveStanza1Path))), + bufNewStr(content)), "put archive info to file"); + + // archive section will cross reference backup db-id 2 to archive db-id 3 but db section will only use the db-ids from + // backup.info + TEST_RESULT_STR(strPtr(infoRender()), + "[\n" + " {\n" + " \"archive\" : [\n" + " {\n" + " \"database\" : {\n" + " \"id\" : 2\n" + " },\n" + " \"id\" : \"9.4-3\",\n" + " \"max\" : null,\n" + " \"min\" : null\n" + " }\n" + " ],\n" + " \"backup\" : [],\n" + " \"cipher\" : \"none\",\n" + " \"db\" : [\n" + " {\n" + " \"id\" : 2,\n" + " \"system-id\" : 6569239123849665679,\n" + " \"version\" : \"9.4\"\n" + " },\n" + " {\n" + " \"id\" : 1,\n" + " \"system-id\" : 6569239123849665666,\n" + " \"version\" : \"9.3\"\n" + " }\n" + " ],\n" + " \"name\" : \"stanza1\",\n" + " \"status\" : {\n" + " \"code\" : 2,\n" + " \"message\" : \"no valid backups\"\n" + " }\n" + " }\n" + "]\n", "json - single stanza, no valid backups"); + + harnessCfgLoad(strLstSize(argListText), strLstPtr(argListText)); + TEST_RESULT_STR(strPtr(infoRender()), + "stanza: stanza1\n" + " status: error (no valid backups)\n" + " cipher: none\n" + "\n" + " db (current)\n" + " wal archive min/max (9.4-3): none present\n", + "text - single stanza, no valid backups"); + + // Coverage for stanzaStatus branches + //-------------------------------------------------------------------------------------------------------------------------- + String *archiveDb1_1 = strNewFmt("%s/9.4-1/0000000100000000", strPtr(archiveStanza1Path)); + TEST_RESULT_VOID(storagePathCreateNP(storageLocalWrite(), archiveDb1_1), "create db1 archive WAL1 directory"); + TEST_RESULT_INT(system( + strPtr(strNewFmt("touch %s", strPtr(strNewFmt("%s/000000010000000000000002-ac61b8f1ec7b1e6c3eaee9345214595eb7daa9a1.gz", + strPtr(archiveDb1_1)))))), 0, "touch WAL1 file"); + TEST_RESULT_INT(system( + strPtr(strNewFmt("touch %s", strPtr(strNewFmt("%s/000000010000000000000003-37dff2b7552a9d66e4bae1a762488a6885e7082c.gz", + strPtr(archiveDb1_1)))))), 0, "touch WAL1 file"); + + String *archiveDb1_2 = strNewFmt("%s/9.4-1/0000000200000000", strPtr(archiveStanza1Path)); + TEST_RESULT_VOID(storagePathCreateNP(storageLocalWrite(), archiveDb1_2), "create db1 archive WAL2 directory"); + TEST_RESULT_INT(system( + strPtr(strNewFmt("touch %s", strPtr(strNewFmt("%s/000000020000000000000003-37dff2b7552a9d66e4bae1a762488a6885e7082c.gz", + strPtr(archiveDb1_2)))))), 0, "touch WAL2 file"); + + String *archiveDb1_3 = strNewFmt("%s/9.4-1/0000000300000000", strPtr(archiveStanza1Path)); + TEST_RESULT_VOID(storagePathCreateNP(storageLocalWrite(), archiveDb1_3), "create db1 archive WAL3 directory"); + + String *archiveDb3 = strNewFmt("%s/9.4-3/0000000100000000", strPtr(archiveStanza1Path)); + TEST_RESULT_VOID(storagePathCreateNP(storageLocalWrite(), archiveDb3), "create db3 archive WAL1 directory"); + + harnessCfgLoad(strLstSize(argList), strLstPtr(argList)); + content = strNew + ( + "[backrest]\n" + "backrest-checksum=\"2edbac400200cc0bd559951f1ee166de5c6f5f49\"\n" + "backrest-format=5\n" + "backrest-version=\"2.04\"\n" + "\n" + "[db]\n" + "db-catalog-version=201409291\n" + "db-control-version=942\n" + "db-id=3\n" + "db-system-id=6569239123849665679\n" + "db-version=\"9.4\"\n" + "\n" + "[backup:current]\n" + "20181116-154756F={\"backrest-format\":5,\"backrest-version\":\"2.04\"," + "\"backup-archive-start\":null,\"backup-archive-stop\":null," + "\"backup-info-repo-size\":3159776,\"backup-info-repo-size-delta\":3159,\"backup-info-size\":26897030," + "\"backup-info-size-delta\":26897030,\"backup-timestamp-start\":1542383276,\"backup-timestamp-stop\":1542383289," + "\"backup-type\":\"full\",\"db-id\":1,\"option-archive-check\":true,\"option-archive-copy\":false," + "\"option-backup-standby\":false,\"option-checksum-page\":true,\"option-compress\":true,\"option-hardlink\":false," + "\"option-online\":true}\n" + "\n" + "[db:history]\n" + "1={\"db-catalog-version\":201409291,\"db-control-version\":942,\"db-system-id\":6569239123849665679," + "\"db-version\":\"9.4\"}\n" + "2={\"db-catalog-version\":201306121,\"db-control-version\":937,\"db-system-id\":6569239123849665666," + "\"db-version\":\"9.3\"}\n" + "3={\"db-catalog-version\":201409291,\"db-control-version\":942,\"db-system-id\":6569239123849665679," + "\"db-version\":\"9.4\"}\n" + ); + + TEST_RESULT_VOID( + storagePutNP(storageNewWriteNP(storageLocalWrite(), strNewFmt("%s/backup.info", strPtr(backupStanza1Path))), + bufNewStr(content)), "put backup info to file"); + + TEST_RESULT_STR(strPtr(infoRender()), + "[\n" + " {\n" + " \"archive\" : [\n" + " {\n" + " \"database\" : {\n" + " \"id\" : 3\n" + " },\n" + " \"id\" : \"9.4-3\",\n" + " \"max\" : null,\n" + " \"min\" : null\n" + " },\n" + " {\n" + " \"database\" : {\n" + " \"id\" : 1\n" + " },\n" + " \"id\" : \"9.4-1\",\n" + " \"max\" : \"000000020000000000000003\",\n" + " \"min\" : \"000000010000000000000002\"\n" + " }\n" + " ],\n" + " \"backup\" : [\n" + " {\n" + " \"archive\" : {\n" + " \"start\" : null,\n" + " \"stop\" : null\n" + " },\n" + " \"backrest\" : {\n" + " \"format\" : 5,\n" + " \"version\" : \"2.04\"\n" + " },\n" + " \"database\" : {\n" + " \"id\" : 1\n" + " },\n" + " \"info\" : {\n" + " \"delta\" : 26897030,\n" + " \"repository\" : {\n" + " \"delta\" : 3159,\n" + " \"size\" : 3159776\n" + " },\n" + " \"size\" : 26897030\n" + " },\n" + " \"label\" : \"20181116-154756F\",\n" + " \"prior\" : null,\n" + " \"reference\" : null,\n" + " \"timestamp\" : {\n" + " \"start\" : 1542383276,\n" + " \"stop\" : 1542383289\n" + " },\n" + " \"type\" : \"full\"\n" + " }\n" + " ],\n" + " \"cipher\" : \"none\",\n" + " \"db\" : [\n" + " {\n" + " \"id\" : 3,\n" + " \"system-id\" : 6569239123849665679,\n" + " \"version\" : \"9.4\"\n" + " },\n" + " {\n" + " \"id\" : 2,\n" + " \"system-id\" : 6569239123849665666,\n" + " \"version\" : \"9.3\"\n" + " },\n" + " {\n" + " \"id\" : 1,\n" + " \"system-id\" : 6569239123849665679,\n" + " \"version\" : \"9.4\"\n" + " }\n" + " ],\n" + " \"name\" : \"stanza1\",\n" + " \"status\" : {\n" + " \"code\" : 0,\n" + " \"message\" : \"ok\"\n" + " }\n" + " }\n" + "]\n", "json - single stanza, valid backup, no priors, no archives in latest DB"); + + harnessCfgLoad(strLstSize(argListText), strLstPtr(argListText)); + TEST_RESULT_STR(strPtr(infoRender()), + "stanza: stanza1\n" + " status: ok\n" + " cipher: none\n" + "\n" + " db (current)\n" + " wal archive min/max (9.4-3): none present\n" + "\n" + " db (prior)\n" + " wal archive min/max (9.4-1): 000000010000000000000002/000000020000000000000003\n" + "\n" + " full backup: 20181116-154756F\n" + " timestamp start/stop: 2018-11-16 15:47:56 / 2018-11-16 15:48:09\n" + " wal start/stop: n/a\n" + " database size: 25.7MB, backup size: 25.7MB\n" + " repository size: 3MB, repository backup size: 3KB\n" + ,"text - single stanza, valid backup, no priors, no archives in latest DB"); + + // backup.info/archive.info files exist, backups exist, archives exist + //-------------------------------------------------------------------------------------------------------------------------- + content = strNew + ( + "[backrest]\n" + "backrest-checksum=\"075a202d42c3b6a0257da5f73a68fa77b342f777\"\n" + "backrest-format=5\n" + "backrest-version=\"2.08dev\"\n" + "\n" + "[db]\n" + "db-id=2\n" + "db-system-id=6626363367545678089\n" + "db-version=\"9.5\"\n" + "\n" + "[db:history]\n" + "1={\"db-id\":6625592122879095702,\"db-version\":\"9.4\"}\n" + "2={\"db-id\":6626363367545678089,\"db-version\":\"9.5\"}\n" + ); + + TEST_RESULT_VOID( + storagePutNP(storageNewWriteNP(storageLocalWrite(), strNewFmt("%s/archive.info", strPtr(archiveStanza1Path))), + bufNewStr(content)), "put archive info to file - stanza1"); + + content = strNew + ( + "[backrest]\n" + "backrest-checksum=\"b50db7cf8f659ac15a0c7a2f45a0813f46a68c6b\"\n" + "backrest-format=5\n" + "backrest-version=\"2.08dev\"\n" + "\n" + "[backup:current]\n" + "20181119-152138F={" + "\"backrest-format\":5,\"backrest-version\":\"2.08dev\"," + "\"backup-archive-start\":\"000000010000000000000002\",\"backup-archive-stop\":\"000000010000000000000002\"," + "\"backup-info-repo-size\":2369186,\"backup-info-repo-size-delta\":2369186," + "\"backup-info-size\":20162900,\"backup-info-size-delta\":20162900," + "\"backup-timestamp-start\":1542640898,\"backup-timestamp-stop\":1542640911,\"backup-type\":\"full\"," + "\"db-id\":1,\"option-archive-check\":true,\"option-archive-copy\":false,\"option-backup-standby\":false," + "\"option-checksum-page\":true,\"option-compress\":true,\"option-hardlink\":false,\"option-online\":true}\n" + "20181119-152138F_20181119-152152D={" + "\"backrest-format\":5,\"backrest-version\":\"2.08dev\",\"backup-archive-start\":\"000000010000000000000003\"," + "\"backup-archive-stop\":\"000000010000000000000003\",\"backup-info-repo-size\":2369186," + "\"backup-info-repo-size-delta\":346,\"backup-info-size\":20162900,\"backup-info-size-delta\":8428," + "\"backup-prior\":\"20181119-152138F\",\"backup-reference\":[\"20181119-152138F\"]," + "\"backup-timestamp-start\":1542640912,\"backup-timestamp-stop\":1542640915,\"backup-type\":\"diff\"," + "\"db-id\":1,\"option-archive-check\":true,\"option-archive-copy\":false,\"option-backup-standby\":false," + "\"option-checksum-page\":true,\"option-compress\":true,\"option-hardlink\":false,\"option-online\":true}\n" + "20181119-152138F_20181119-152152I={" + "\"backrest-format\":5,\"backrest-version\":\"2.08dev\",\"backup-archive-start\":\"000000010000000000000003\"," + "\"backup-info-repo-size\":2369186," + "\"backup-info-repo-size-delta\":346,\"backup-info-size\":20162900,\"backup-info-size-delta\":8428," + "\"backup-prior\":\"20181119-152138F_20181119-152152D\"," + "\"backup-reference\":[\"20181119-152138F\",\"20181119-152138F_20181119-152152D\"]," + "\"backup-timestamp-start\":1542640912,\"backup-timestamp-stop\":1542640915,\"backup-type\":\"incr\"," + "\"db-id\":1,\"option-archive-check\":true,\"option-archive-copy\":false,\"option-backup-standby\":false," + "\"option-checksum-page\":true,\"option-compress\":true,\"option-hardlink\":false,\"option-online\":true}\n" + "\n" + "[db]\n" + "db-catalog-version=201510051\n" + "db-control-version=942\n" + "db-id=2\n" + "db-system-id=6626363367545678089\n" + "db-version=\"9.5\"\n" + "\n" + "[db:history]\n" + "1={\"db-catalog-version\":201409291,\"db-control-version\":942,\"db-system-id\":6625592122879095702," + "\"db-version\":\"9.4\"}\n" + "2={\"db-catalog-version\":201510051,\"db-control-version\":942,\"db-system-id\":6626363367545678089," + "\"db-version\":\"9.5\"}\n" + ); + + TEST_RESULT_VOID( + storagePutNP(storageNewWriteNP(storageLocalWrite(), strNewFmt("%s/backup.info", strPtr(backupStanza1Path))), + bufNewStr(content)), "put backup info to file - stanza1"); + + String *archiveStanza2Path = strNewFmt("%s/stanza2", strPtr(archivePath)); + String *backupStanza2Path = strNewFmt("%s/stanza2", strPtr(backupPath)); + TEST_RESULT_VOID(storagePathCreateNP(storageLocalWrite(), backupStanza1Path), "backup stanza2 directory"); + TEST_RESULT_VOID(storagePathCreateNP(storageLocalWrite(), archiveStanza1Path), "archive stanza2 directory"); + + content = strNew + ( + "[backrest]\n" + "backrest-checksum=\"6779d476833114925a73e058ef9ff04e5a8c7bd2\"\n" + "backrest-format=5\n" + "backrest-version=\"2.08dev\"\n" + "\n" + "[db]\n" + "db-id=1\n" + "db-system-id=6625633699176220261\n" + "db-version=\"9.4\"\n" + "\n" + "[db:history]\n" + "1={\"db-id\":6625633699176220261,\"db-version\":\"9.4\"}\n" + ); + + TEST_RESULT_VOID( + storagePutNP(storageNewWriteNP(storageLocalWrite(), strNewFmt("%s/archive.info", strPtr(archiveStanza2Path))), + bufNewStr(content)), "put archive info to file - stanza2"); + + content = strNew + ( + "[backrest]\n" + "backrest-checksum=\"2393c52cb48aff2d6c6e87e21a34a3e28200f42e\"\n" + "backrest-format=5\n" + "backrest-version=\"2.08dev\"\n" + "\n" + "[db]\n" + "db-catalog-version=201409291\n" + "db-control-version=942\n" + "db-id=1\n" + "db-system-id=6625633699176220261\n" + "db-version=\"9.4\"\n" + "\n" + "[db:history]\n" + "1={\"db-catalog-version\":201409291,\"db-control-version\":942,\"db-system-id\":6625633699176220261," + "\"db-version\":\"9.4\"}\n" + ); + + TEST_RESULT_VOID( + storagePutNP(storageNewWriteNP(storageLocalWrite(), strNewFmt("%s/backup.info", strPtr(backupStanza2Path))), + bufNewStr(content)), "put backup info to file - stanza2"); + + harnessCfgLoad(strLstSize(argList), strLstPtr(argList)); + TEST_RESULT_STR(strPtr(infoRender()), + "[\n" + " {\n" + " \"archive\" : [\n" + " {\n" + " \"database\" : {\n" + " \"id\" : 2\n" + " },\n" + " \"id\" : \"9.5-2\",\n" + " \"max\" : null,\n" + " \"min\" : null\n" + " },\n" + " {\n" + " \"database\" : {\n" + " \"id\" : 1\n" + " },\n" + " \"id\" : \"9.4-1\",\n" + " \"max\" : \"000000020000000000000003\",\n" + " \"min\" : \"000000010000000000000002\"\n" + " }\n" + " ],\n" + " \"backup\" : [\n" + " {\n" + " \"archive\" : {\n" + " \"start\" : \"000000010000000000000002\",\n" + " \"stop\" : \"000000010000000000000002\"\n" + " },\n" + " \"backrest\" : {\n" + " \"format\" : 5,\n" + " \"version\" : \"2.08dev\"\n" + " },\n" + " \"database\" : {\n" + " \"id\" : 1\n" + " },\n" + " \"info\" : {\n" + " \"delta\" : 20162900,\n" + " \"repository\" : {\n" + " \"delta\" : 2369186,\n" + " \"size\" : 2369186\n" + " },\n" + " \"size\" : 20162900\n" + " },\n" + " \"label\" : \"20181119-152138F\",\n" + " \"prior\" : null,\n" + " \"reference\" : null,\n" + " \"timestamp\" : {\n" + " \"start\" : 1542640898,\n" + " \"stop\" : 1542640911\n" + " },\n" + " \"type\" : \"full\"\n" + " },\n" + " {\n" + " \"archive\" : {\n" + " \"start\" : \"000000010000000000000003\",\n" + " \"stop\" : \"000000010000000000000003\"\n" + " },\n" + " \"backrest\" : {\n" + " \"format\" : 5,\n" + " \"version\" : \"2.08dev\"\n" + " },\n" + " \"database\" : {\n" + " \"id\" : 1\n" + " },\n" + " \"info\" : {\n" + " \"delta\" : 8428,\n" + " \"repository\" : {\n" + " \"delta\" : 346,\n" + " \"size\" : 2369186\n" + " },\n" + " \"size\" : 20162900\n" + " },\n" + " \"label\" : \"20181119-152138F_20181119-152152D\",\n" + " \"prior\" : \"20181119-152138F\",\n" + " \"reference\" : [\n" + " \"20181119-152138F\"\n" + " ],\n" + " \"timestamp\" : {\n" + " \"start\" : 1542640912,\n" + " \"stop\" : 1542640915\n" + " },\n" + " \"type\" : \"diff\"\n" + " },\n" + " {\n" + " \"archive\" : {\n" + " \"start\" : \"000000010000000000000003\",\n" + " \"stop\" : null\n" + " },\n" + " \"backrest\" : {\n" + " \"format\" : 5,\n" + " \"version\" : \"2.08dev\"\n" + " },\n" + " \"database\" : {\n" + " \"id\" : 1\n" + " },\n" + " \"info\" : {\n" + " \"delta\" : 8428,\n" + " \"repository\" : {\n" + " \"delta\" : 346,\n" + " \"size\" : 2369186\n" + " },\n" + " \"size\" : 20162900\n" + " },\n" + " \"label\" : \"20181119-152138F_20181119-152152I\",\n" + " \"prior\" : \"20181119-152138F_20181119-152152D\",\n" + " \"reference\" : [\n" + " \"20181119-152138F\",\n" + " \"20181119-152138F_20181119-152152D\"\n" + " ],\n" + " \"timestamp\" : {\n" + " \"start\" : 1542640912,\n" + " \"stop\" : 1542640915\n" + " },\n" + " \"type\" : \"incr\"\n" + " }\n" + " ],\n" + " \"cipher\" : \"none\",\n" + " \"db\" : [\n" + " {\n" + " \"id\" : 2,\n" + " \"system-id\" : 6626363367545678089,\n" + " \"version\" : \"9.5\"\n" + " },\n" + " {\n" + " \"id\" : 1,\n" + " \"system-id\" : 6625592122879095702,\n" + " \"version\" : \"9.4\"\n" + " }\n" + " ],\n" + " \"name\" : \"stanza1\",\n" + " \"status\" : {\n" + " \"code\" : 0,\n" + " \"message\" : \"ok\"\n" + " }\n" + " },\n" + " {\n" + " \"archive\" : [\n" + " {\n" + " \"database\" : {\n" + " \"id\" : 1\n" + " },\n" + " \"id\" : \"9.4-1\",\n" + " \"max\" : null,\n" + " \"min\" : null\n" + " }\n" + " ],\n" + " \"backup\" : [],\n" + " \"cipher\" : \"none\",\n" + " \"db\" : [\n" + " {\n" + " \"id\" : 1,\n" + " \"system-id\" : 6625633699176220261,\n" + " \"version\" : \"9.4\"\n" + " }\n" + " ],\n" + " \"name\" : \"stanza2\",\n" + " \"status\" : {\n" + " \"code\" : 2,\n" + " \"message\" : \"no valid backups\"\n" + " }\n" + " }\n" + "]\n", "json - multiple stanzas, one with valid backups, archives in latest DB"); + + harnessCfgLoad(strLstSize(argListText), strLstPtr(argListText)); + TEST_RESULT_STR(strPtr(infoRender()), + "stanza: stanza1\n" + " status: ok\n" + " cipher: none\n" + "\n" + " db (current)\n" + " wal archive min/max (9.5-2): none present\n" + "\n" + " db (prior)\n" + " wal archive min/max (9.4-1): 000000010000000000000002/000000020000000000000003\n" + "\n" + " full backup: 20181119-152138F\n" + " timestamp start/stop: 2018-11-19 15:21:38 / 2018-11-19 15:21:51\n" + " wal start/stop: 000000010000000000000002 / 000000010000000000000002\n" + " database size: 19.2MB, backup size: 19.2MB\n" + " repository size: 2.3MB, repository backup size: 2.3MB\n" + "\n" + " diff backup: 20181119-152138F_20181119-152152D\n" + " timestamp start/stop: 2018-11-19 15:21:52 / 2018-11-19 15:21:55\n" + " wal start/stop: 000000010000000000000003 / 000000010000000000000003\n" + " database size: 19.2MB, backup size: 8.2KB\n" + " repository size: 2.3MB, repository backup size: 346B\n" + " backup reference list: 20181119-152138F\n" + "\n" + " incr backup: 20181119-152138F_20181119-152152I\n" + " timestamp start/stop: 2018-11-19 15:21:52 / 2018-11-19 15:21:55\n" + " wal start/stop: n/a\n" + " database size: 19.2MB, backup size: 8.2KB\n" + " repository size: 2.3MB, repository backup size: 346B\n" + " backup reference list: 20181119-152138F, 20181119-152138F_20181119-152152D\n" + "\n" + "stanza: stanza2\n" + " status: error (no valid backups)\n" + " cipher: none\n" + "\n" + " db (current)\n" + " wal archive min/max (9.4-1): none present\n" + , "text - multiple stanzas, one with valid backups, archives in latest DB"); + + // Stanza not found + //-------------------------------------------------------------------------------------------------------------------------- + StringList *argList2 = strLstDup(argList); + strLstAddZ(argList2, "--stanza=silly"); + harnessCfgLoad(strLstSize(argList2), strLstPtr(argList2)); + TEST_RESULT_STR(strPtr(infoRender()), + "[\n" + " {\n" + " \"backup\" : [],\n" + " \"db\" : [],\n" + " \"name\" : \"silly\",\n" + " \"status\" : {\n" + " \"code\" : 1,\n" + " \"message\" : \"missing stanza path\"\n" + " }\n" + " }\n" + "]\n", "json - missing stanza path"); + + StringList *argListText2 = strLstDup(argListText); + strLstAddZ(argListText2, "--stanza=silly"); + harnessCfgLoad(strLstSize(argListText2), strLstPtr(argListText2)); + TEST_RESULT_STR(strPtr(infoRender()), + "stanza: silly\n" + " status: error (missing stanza path)\n" + , "text - missing stanza path"); + + // Stanza found + //-------------------------------------------------------------------------------------------------------------------------- + strLstAddZ(argList, "--stanza=stanza2"); + harnessCfgLoad(strLstSize(argList), strLstPtr(argList)); + TEST_RESULT_STR(strPtr(infoRender()), + "[\n" + " {\n" + " \"archive\" : [\n" + " {\n" + " \"database\" : {\n" + " \"id\" : 1\n" + " },\n" + " \"id\" : \"9.4-1\",\n" + " \"max\" : null,\n" + " \"min\" : null\n" + " }\n" + " ],\n" + " \"backup\" : [],\n" + " \"cipher\" : \"none\",\n" + " \"db\" : [\n" + " {\n" + " \"id\" : 1,\n" + " \"system-id\" : 6625633699176220261,\n" + " \"version\" : \"9.4\"\n" + " }\n" + " ],\n" + " \"name\" : \"stanza2\",\n" + " \"status\" : {\n" + " \"code\" : 2,\n" + " \"message\" : \"no valid backups\"\n" + " }\n" + " }\n" + "]\n" + , "json - multiple stanzas - selected found"); + + strLstAddZ(argListText, "--stanza=stanza2"); + harnessCfgLoad(strLstSize(argListText), strLstPtr(argListText)); + TEST_RESULT_STR(strPtr(infoRender()), + "stanza: stanza2\n" + " status: error (no valid backups)\n" + " cipher: none\n" + "\n" + " db (current)\n" + " wal archive min/max (9.4-1): none present\n" + ,"text - multiple stanzas - selected found"); + + // Crypto error + //-------------------------------------------------------------------------------------------------------------------------- + content = strNew + ( + "[global]\n" + "repo-cipher-pass=123abc\n" + ); + + TEST_RESULT_VOID( + storagePutNP(storageNewWriteNP(storageLocalWrite(), strNewFmt("%s/pgbackrest.conf", testPath())), + bufNewStr(content)), "put pgbackrest.conf file"); + strLstAddZ(argListText, "--repo-cipher-type=aes-256-cbc"); + strLstAdd(argListText, strNewFmt("--config=%s/pgbackrest.conf", testPath())); + harnessCfgLoad(strLstSize(argListText), strLstPtr(argListText)); + TEST_ERROR_FMT( + infoRender(), CryptoError, + "unable to load info file '%s/backup.info' or '%s/backup.info.copy':\n" + "CryptoError: '%s/backup.info' cipher header invalid\n" + "HINT: Is or was the repo encrypted?\n" + "FileMissingError: unable to open '%s/backup.info.copy' for read: [2] No such file or directory\n" + "HINT: backup.info cannot be opened and is required to perform a backup.\n" + "HINT: has a stanza-create been performed?\n" + "HINT: use option --stanza if encryption settings are different for the stanza than the global settings" + ,strPtr(backupStanza2Path), strPtr(backupStanza2Path), strPtr(backupStanza2Path), strPtr(backupStanza2Path)); + } + + //****************************************************************************************************************************** + if (testBegin("formatTextDb()")) + { + // These tests cover branches not covered in other tests + KeyValue *stanzaInfo = kvNew(); + VariantList *dbSection = varLstNew(); + Variant *pgInfo = varNewKv(); + kvPut(varKv(pgInfo), varNewStr(DB_KEY_ID_STR), varNewUInt64(1)); + kvPut(varKv(pgInfo), varNewStr(DB_KEY_SYSTEM_ID_STR), varNewUInt64(6625633699176220261)); + kvPut(varKv(pgInfo), varNewStr(DB_KEY_VERSION_STR), varNewStr(pgVersionToStr(90500))); + + varLstAdd(dbSection, pgInfo); + + // Add the database history, backup and archive sections to the stanza info + kvPut(stanzaInfo, varNewStr(STANZA_KEY_DB_STR), varNewVarLst(dbSection)); + + VariantList *backupSection = varLstNew(); + Variant *backupInfo = varNewKv(); + + kvPut(varKv(backupInfo), varNewStr(BACKUP_KEY_LABEL_STR), varNewStr(strNew("20181119-152138F"))); + kvPut(varKv(backupInfo), varNewStr(BACKUP_KEY_TYPE_STR), varNewStr(strNew("full"))); + kvPutKv(varKv(backupInfo), varNewStr(KEY_ARCHIVE_STR)); + KeyValue *infoInfo = kvPutKv(varKv(backupInfo), varNewStr(BACKUP_KEY_INFO_STR)); + kvPut(infoInfo, varNewStr(KEY_SIZE_STR), varNewUInt64(0)); + kvPut(infoInfo, varNewStr(KEY_DELTA_STR), varNewUInt64(0)); + KeyValue *repoInfo = kvPutKv(infoInfo, varNewStr(INFO_KEY_REPOSITORY_STR)); + kvAdd(repoInfo, varNewStr(KEY_SIZE_STR), varNewUInt64(0)); + kvAdd(repoInfo, varNewStr(KEY_DELTA_STR), varNewUInt64(0)); + KeyValue *databaseInfo = kvPutKv(varKv(backupInfo), varNewStr(KEY_DATABASE_STR)); + kvAdd(databaseInfo, varNewStr(DB_KEY_ID_STR), varNewUInt64(1)); + KeyValue *timeInfo = kvPutKv(varKv(backupInfo), varNewStr(BACKUP_KEY_TIMESTAMP_STR)); + kvAdd(timeInfo, varNewStr(KEY_START_STR), varNewUInt64(1542383276)); + kvAdd(timeInfo, varNewStr(KEY_STOP_STR), varNewUInt64(1542383289)); + + varLstAdd(backupSection, backupInfo); + + kvPut(stanzaInfo, varNewStr(STANZA_KEY_BACKUP_STR), varNewVarLst(backupSection)); + kvPut(stanzaInfo, varNewStr(KEY_ARCHIVE_STR), varNewVarLst(varLstNew())); + + String *result = strNew(""); + formatTextDb(stanzaInfo, result); + + TEST_RESULT_STR(strPtr(result), + "\n" + " db (current)\n" + " full backup: 20181119-152138F\n" + " timestamp start/stop: 2018-11-16 15:47:56 / 2018-11-16 15:48:09\n" + " wal start/stop: n/a\n" + " database size: 0B, backup size: 0B\n" + " repository size: 0B, repository backup size: 0B\n" + ,"formatTextDb only backup section (code cverage only)"); + } + + //****************************************************************************************************************************** + if (testBegin("cmdInfo()")) + { + StringList *argList = strLstNew(); + strLstAddZ(argList, "pgbackrest"); + strLstAdd(argList, strNewFmt("--repo-path=%s", strPtr(repoPath))); + strLstAddZ(argList, "info"); + harnessCfgLoad(strLstSize(argList), strLstPtr(argList)); + + storagePathCreateNP(storageLocalWrite(), archivePath); + storagePathCreateNP(storageLocalWrite(), backupPath); + + // Redirect stdout to a file + int stdoutSave = dup(STDOUT_FILENO); + String *stdoutFile = strNewFmt("%s/stdout.info", testPath()); + + if (freopen(strPtr(stdoutFile), "w", stdout) == NULL) // {uncoverable - does not fail} + THROW_SYS_ERROR(FileWriteError, "unable to reopen stdout"); // {uncoverable+} + + // Not in a test wrapper to avoid writing to stdout + cmdInfo(); + + // Restore normal stdout + dup2(stdoutSave, STDOUT_FILENO); + + const char *generalHelp = strPtr(strNewFmt("No stanzas exist in %s\n", strPtr(repoPath))); + + Storage *storage = storageDriverPosixInterface( + storageDriverPosixNew(strNew(testPath()), STORAGE_MODE_FILE_DEFAULT, STORAGE_MODE_PATH_DEFAULT, false, NULL)); + TEST_RESULT_STR(strPtr(strNewBuf(storageGetNP(storageNewReadNP(storage, stdoutFile)))), generalHelp, " check text"); + } + + FUNCTION_HARNESS_RESULT_VOID(); +}