From 16cbfe381e0844acd174175e8e6670e24f18025e Mon Sep 17 00:00:00 2001 From: Suraj Kharage Date: Fri, 20 Dec 2019 16:19:44 +0530 Subject: [PATCH v6 2/3] Implementation of backup validator Patch by Suraj Kharage, inputs from Robert Haas, review from Jeevan Chalke, and Robert Haas. --- doc/src/sgml/ref/pg_basebackup.sgml | 12 + src/bin/pg_basebackup/pg_basebackup.c | 441 ++++++++++++++++++++++++++++++++++ src/common/encode.c | 18 ++ src/include/common/encode.h | 1 + src/tools/pgindent/typedefs.list | 2 + 5 files changed, 474 insertions(+) diff --git a/doc/src/sgml/ref/pg_basebackup.sgml b/doc/src/sgml/ref/pg_basebackup.sgml index af7c731..cb11f74 100644 --- a/doc/src/sgml/ref/pg_basebackup.sgml +++ b/doc/src/sgml/ref/pg_basebackup.sgml @@ -548,6 +548,18 @@ PostgreSQL documentation + + + + + Validate the given backup directory and detect the modification if any + without restarting the server. For plain backup, provide the backup + directory path with option. Tar format + backups can be verified after untarring. + + + + diff --git a/src/bin/pg_basebackup/pg_basebackup.c b/src/bin/pg_basebackup/pg_basebackup.c index 7d5ed0d..08960e0 100644 --- a/src/bin/pg_basebackup/pg_basebackup.c +++ b/src/bin/pg_basebackup/pg_basebackup.c @@ -27,9 +27,12 @@ #endif #include "access/xlog_internal.h" +#include "common/checksum_utils.h" +#include "common/encode.h" #include "common/file_perm.h" #include "common/file_utils.h" #include "common/logging.h" +#include "common/sha2.h" #include "common/string.h" #include "fe_utils/recovery_gen.h" #include "fe_utils/string_utils.h" @@ -97,6 +100,27 @@ typedef struct WriteManifestState typedef void (*WriteDataCallback) (size_t nbytes, char *buf, void *callback_data); +typedef struct DataDirectoryFileInfo +{ + char *filename; + int filesize; + char *checksum; + bool matched; + uint32 status; /* hash status */ +} DataDirectoryFileInfo; + +#define SH_PREFIX manifesthash +#define SH_ELEMENT_TYPE DataDirectoryFileInfo +#define SH_KEY_TYPE char* +#define SH_KEY filename +#define SH_HASH_KEY(tb, key) string_hash_sdbm(key) +#define SH_EQUAL(tb, a, b) (strcmp(a, b) == 0) +#define SH_SCOPE static inline +#define SH_RAW_ALLOCATOR pg_malloc +#define SH_DECLARE +#define SH_DEFINE +#include "lib/simplehash.h" + /* * pg_xlog has been renamed to pg_wal in version 10. This version number * should be compared with PQserverVersion(). @@ -142,6 +166,7 @@ static bool create_slot = false; static bool no_slot = false; static bool verify_checksums = true; static char *manifest_checksums = NULL; +static ChecksumAlgorithm checksum_type = MC_NONE; static bool success = false; static bool made_new_pgdata = false; @@ -201,6 +226,16 @@ static bool reached_end_position(XLogRecPtr segendpos, uint32 timeline, static const char *get_tablespace_mapping(const char *dir); static void tablespace_list_append(const char *arg); +static void VerifyBackup(void); +static manifesthash_hash *CreateManifestHash(char *manifest_path); +static void ScanDataDirectory(char *basepath, char *subdirpath, + manifesthash_hash *hashtab); +static void VerifyFile(struct dirent *de, char *filepath, + struct stat st, char *relative_filepath, + manifesthash_hash *hashtab); +static char *NextLine(char *buf); +static char *NextWord(char *line, char ch); +static char *ReadFileIntoBuffer(char *filename); static void cleanup_directories_atexit(void) @@ -401,6 +436,7 @@ usage(void) " do not verify checksums\n")); printf(_(" --manifest-checksums=SHA256|CRC32C|NONE\n" " calculate checksums for manifest files using provided algorithm\n")); + printf(_(" --verify-backup validate the backup\n")); printf(_(" -?, --help show this help, then exit\n")); printf(_("\nConnection options:\n")); printf(_(" -d, --dbname=CONNSTR connection string\n")); @@ -2167,11 +2203,13 @@ main(int argc, char **argv) {"no-slot", no_argument, NULL, 2}, {"no-verify-checksums", no_argument, NULL, 3}, {"manifest-checksums", required_argument, NULL, 'm'}, + {"verify-backup", no_argument, NULL, 4}, {NULL, 0, NULL, 0} }; int c; int option_index; + bool verify_backup = false; pg_logging_init(argv[0]); progname = get_progname(argv[0]); @@ -2338,6 +2376,9 @@ main(int argc, char **argv) case 'm': manifest_checksums = pg_strdup(optarg); break; + case 4: + verify_backup = true; + break; default: /* @@ -2460,6 +2501,12 @@ main(int argc, char **argv) } #endif + if (verify_backup) + { + VerifyBackup(); + return 0; + } + /* connection in replication mode to server */ conn = GetConnection(); if (!conn) @@ -2524,3 +2571,397 @@ main(int argc, char **argv) success = true; return 0; } + +static void +VerifyBackup(void) +{ + char manifest_path[MAXPGPATH]; + manifesthash_hash *hashtab; + manifesthash_iterator i; + DataDirectoryFileInfo *entry; + + snprintf(manifest_path, sizeof(manifest_path), "%s/%s", basedir, + "backup_manifest"); + + /* create hash table */ + hashtab = CreateManifestHash(manifest_path); + + ScanDataDirectory(basedir, "", hashtab); + + manifesthash_start_iterate(hashtab, &i); + while ((entry = manifesthash_iterate(hashtab, &i)) != NULL) + { + if (!entry->matched) + pg_log_info("file \"%s\" is present in manifest but missing from the backup", + entry->filename); + } +} + +/* + * Given a file path, read that file and generate the hash table for same. + * Also generate the SHA256 checksum for the records that are read from file + * and compare that with manifest checksum written in backup_manifest file. + * If both checksums are identical then proceed, otherwise throw an error and + * abort. + */ +static manifesthash_hash * +CreateManifestHash(char *manifest_path) +{ + manifesthash_hash *hashtab; + DataDirectoryFileInfo *entry; + char *buf; + ChecksumCtx cCtx; + + buf = ReadFileIntoBuffer(manifest_path); + + hashtab = manifesthash_create(1024, NULL); + + /* Read the first line of buffer */ + if (*buf != '\0' && *buf != EOF) + { + char *header_line; + int header_length; + + /* + * Read the header from file, here header_line is pointing to start of + * file. Advanced the buffer to next line and then buf - header_line + * will give us the header length. + */ + header_line = buf; + buf = NextLine(buf); + header_length = buf - header_line; + + /* + * Initialize the checksum for the first time. Since checksum for the + * manifest file is always generated with SHA256 so initializing with + * SHA256. + */ + initialize_checksum(&cCtx, MC_SHA256); + + /* feed the header to the checksum machinery */ + update_checksum(&cCtx, MC_SHA256, header_line, header_length); + } + + /* + * Once we read the header, then read the buffer line by line and check + * whether it is a File record or Manifest-Checksum entry and parse + * accordingly. + */ + while (*buf != '\0' && *buf != EOF) + { + int length; + char *line; + + line = buf; + /* read the next line and calculate the length for the line */ + buf = NextLine(buf); + length = buf - line; + + /* + * If it is a File record, then parse it into fields. With this we + * will get the filename, checksum and size. + */ + if (strncmp(line, "File", 4) == 0) + { + char *filename; + char *size; + long filesize; + bool found; + long filelength, + sizelength; + char *checksum_label; + + /* If is a FILE record, feed that line to checksum machinery */ + update_checksum(&cCtx, MC_SHA256, line, length); + + line[length - 1] = '\0'; + + /* skip the "File" field */ + line = NextWord(line, '\t'); + + /* parse the filename */ + filename = line; + line = NextWord(line, '\t'); + filelength = line - filename; + filename[filelength - 1] = '\0'; + + /* parse the filesize and convert it to long */ + size = line; + line = NextWord(line, '\t'); + sizelength = line - size; + size[sizelength - 1] = '\0'; + filesize = strtol(size, NULL, 10); + + /* skip mtime field */ + line = NextWord(line, '\t'); + + /* + * parse the checksum field. If it is a "-" that means no + * checksum. Otherwise split this field by ":" character to + * identify the checksum type. The remaining string in line + * pointer would be actual checksum value. + */ + checksum_label = line; + if (strcmp(checksum_label, "-") == 0) + checksum_type = MC_NONE; + else + { + line = NextWord(line, ':'); + length = line - checksum_label; + checksum_label[length - 1] = '\0'; + + if (!parse_checksum_algorithm(checksum_label, &checksum_type)) + { + pg_log_error("invalid checksums type \"%s\" found in \"%s\"", + checksum_label, manifest_path); + exit(1); + } + } + + /* insert the hash record */ + entry = manifesthash_insert(hashtab, filename, &found); + entry->filesize = filesize; + entry->checksum = line; + } + + /* + * If it is a Manifest-Checksum entry, then finalize the checksum and + * compare it with the manifest checksum parsed from the file + */ + else if (strncmp(line, "Manifest-Checksum", 17) == 0) + { + char checksumbuf[256]; + int checksumbuflen; + char encoded_checksum[256]; + char *checksum_label; + + line[length - 1] = '\0'; + + /* skip the Manifest-Checksum word */ + line = NextWord(line, '\t'); + + /* parse the checksum label for backup manifest checksum */ + checksum_label = line; + line = NextWord(line, ':'); + length = line - checksum_label; + + checksum_label[length - 1] = '\0'; + + if (strcmp(checksum_label, "SHA256") != 0) + { + pg_log_error("invalid manifest checksums type \"%s\" found in \"%s\"", + checksum_label, manifest_path); + exit(1); + } + + /* finalize the chekcksum */ + checksumbuflen = finalize_checksum(&cCtx, MC_SHA256, + (char *) checksumbuf); + checksumbuflen = hex_encode(checksumbuf, checksumbuflen, + encoded_checksum); + encoded_checksum[checksumbuflen] = '\0'; + + if (strcmp(encoded_checksum, line) != 0) + { + pg_log_error("backup manifest checksum difference. Aborting"); + exit(1); + } + } + else + { + pg_log_error("invalid manifest record found in \"%s\"", manifest_path); + exit(1); + } + } + return hashtab; +} + +/* + * Scan the given directory and for each regular file within that directory, + * call VerifyFile() + */ +static void +ScanDataDirectory(char *basepath, char *pathsuffix, + manifesthash_hash *hashtab) +{ + char path[MAXPGPATH]; + DIR *dir; + struct dirent *de; + + snprintf(path, MAXPGPATH, "%s%s", basepath, pathsuffix); + + dir = opendir(path); + if (!dir) + { + pg_log_error("could not open directory \"%s\": %m", path); + exit(1); + } + + while ((de = readdir(dir)) != NULL) + { + char fn[MAXPGPATH]; + char newpathsuffix[MAXPGPATH]; + struct stat st; + + if (strcmp(de->d_name, ".") == 0 || strcmp(de->d_name, "..") == 0) + continue; + + snprintf(newpathsuffix, MAXPGPATH, "%s/%s", pathsuffix, de->d_name); + + if (strcmp(newpathsuffix, "/pg_wal") == 0 || + strcmp(newpathsuffix, "/backup_manifest") == 0) + continue; + + snprintf(fn, sizeof(fn), "%s/%s", path, de->d_name); + if (stat(fn, &st) < 0) + { + pg_log_error("could not stat file \"%s\": %m", fn); + exit(1); + } + if (S_ISREG(st.st_mode)) + { + /* + * The forth parameter to VerifyFile() will pass the relative path + * of file to match exactly with the filename present in manifest. + */ + VerifyFile(de, fn, st, &fn[strlen(basepath) + 1], hashtab); + } + else if (S_ISDIR(st.st_mode)) + ScanDataDirectory(basepath, newpathsuffix, hashtab); + } + closedir(dir); +} + +/* + * Given the file and its details, check whether it is present in hash table + * and if yes, then compare its details with hash table entry. + */ +static void +VerifyFile(struct dirent *de, char *filepath, struct stat st, + char *relative_filepath, manifesthash_hash *hashtab) +{ + DataDirectoryFileInfo *record; + + /* + * Lookup into hash table and if record found then we match the file size + * and checksum (if enabled). Modified time cannot be compared with the + * file in the backup directory and its entry in the manifest as manifest + * entry gives mtime from server file whereas the same file in the backup + * will have different mtime. + */ + record = manifesthash_lookup(hashtab, relative_filepath); + if (record == NULL) + { + pg_log_info("file \"%s\" is present in backup but not in manifest", + relative_filepath); + return; + } + + record->matched = true; + if (record->filesize != st.st_size) + pg_log_info("file \"%s\" has size %d in manifest but size %lu in backup", + relative_filepath, record->filesize, st.st_size); + + /* + * Read the file and generate the checksum based on checksum method and + * compare that with the checksum present in hash entry. + */ + if (checksum_type != MC_NONE) + { + FILE *fp; + char buf[1048576]; /* 1MB chunk */ + pgoff_t len = 0; + off_t cnt; + char checksumbuf[256]; + char encode_checksumbuf[256]; + int checksumbuflen; + ChecksumCtx cCtx; + + initialize_checksum(&cCtx, checksum_type); + + fp = fopen(filepath, "r"); + if (!fp) + { + pg_log_error("could not open file \"%s\": %m", filepath); + exit(1); + } + + /* Read file in chunks [1 MB each chunk] */ + while ((cnt = fread(buf, 1, Min(sizeof(buf), st.st_size - len), fp)) > 0) + { + update_checksum(&cCtx, checksum_type, buf, cnt); + len += cnt; + } + + checksumbuflen = finalize_checksum(&cCtx, checksum_type, + checksumbuf); + + /* Convert checksum to hexadecimal. */ + checksumbuflen = hex_encode(checksumbuf, checksumbuflen, + encode_checksumbuf); + encode_checksumbuf[checksumbuflen] = '\0'; + + fclose(fp); + + if (strcmp(record->checksum, encode_checksumbuf) != 0) + pg_log_info("file \"%s\" has checksum %s in manifest but checksum %s in backup", + relative_filepath, record->checksum, encode_checksumbuf); + } +} + +/* + * Find out the next new line character from the provided string and return + * char pointer pointing to next character after that. + */ +static char * +NextLine(char *buf) +{ + while (*buf != '\0' && *buf != '\n' && *buf != EOF) + buf++; + + return ++buf; +} + +/* + * Advance the string until provided character or new line character or EOF is + * detected. Return the advanced string. + */ +static char * +NextWord(char *line, char ch) +{ + while (*line != '\0' && *line != ch && *line != '\n' && *line != EOF) + line++; + + return ++line; +} + +/* Read the given file into buffer and return that buffer */ +static char * +ReadFileIntoBuffer(char *filename) +{ + int fd; + char *buf; + struct stat stat; + + fd = open(filename, O_RDONLY, 0); + + if (fstat(fd, &stat)) + { + pg_log_error("could not stat file \"%s\": %m", filename); + close(fd); + exit(1); + } + + buf = pg_malloc(stat.st_size + 1); + + if (read(fd, buf, stat.st_size) != stat.st_size) + { + pg_log_error("could not read file \"%s\": %m", filename); + close(fd); + exit(1); + } + + close(fd); + + return buf; +} diff --git a/src/common/encode.c b/src/common/encode.c index a450c53..14f2ec2 100644 --- a/src/common/encode.c +++ b/src/common/encode.c @@ -36,3 +36,21 @@ hex_encode(const char *src, unsigned len, char *dst) } return len * 2; } + +/* + * Simple string hash function from http://www.cse.yorku.ca/~oz/hash.html + * + * The backend uses a more sophisticated function for hashing strings, + * but we don't really need that complexity here. + */ +uint32 +string_hash_sdbm(const char *key) +{ + uint32 hash = 0; + int c; + + while ((c = *key++)) + hash = c + (hash << 6) + (hash << 16) - hash; + + return hash; +} diff --git a/src/include/common/encode.h b/src/include/common/encode.h index 63328bc..44a062f 100644 --- a/src/include/common/encode.h +++ b/src/include/common/encode.h @@ -16,5 +16,6 @@ static const char hextbl[] = "0123456789abcdef"; extern unsigned hex_encode(const char *src, unsigned len, char *dst); +extern uint32 string_hash_sdbm(const char *key); #endif /* COMMON_ENCODE_H */ diff --git a/src/tools/pgindent/typedefs.list b/src/tools/pgindent/typedefs.list index 87556f6..14e475e 100644 --- a/src/tools/pgindent/typedefs.list +++ b/src/tools/pgindent/typedefs.list @@ -487,6 +487,7 @@ DR_sqlfunction DR_transientrel DSA DWORD +DataDirectoryFileInfo DataDumperPtr DataPageDeleteStack DateADT @@ -1353,6 +1354,7 @@ MultiXactOffset MultiXactStateData MultiXactStatus MyData +manifesthash_hash manifestinfo NDBOX NODE -- 1.8.3.1