From 7c8b2f1ad635bc669e95b7a2000d4836468f74b5 Mon Sep 17 00:00:00 2001 From: Suraj Kharage Date: Fri, 20 Dec 2019 16:19:44 +0530 Subject: [PATCH v5 2/2] 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 | 461 ++++++++++++++++++++++++++++++++++ src/common/encode.c | 18 ++ src/include/common/encode.h | 1 + src/tools/pgindent/typedefs.list | 2 + 5 files changed, 494 insertions(+) diff --git a/doc/src/sgml/ref/pg_basebackup.sgml b/doc/src/sgml/ref/pg_basebackup.sgml index af7c731..043ee39 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..aa82c46 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,29 @@ 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; + +struct manifesthash_hash *hashtab; + +#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 +168,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 +228,15 @@ 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 *create_manifest_hash(char *manifest_path); +static void scan_data_directory(char *basedir, const char *subdirpath, + manifesthash_hash *hashtab); +static void verify_file(struct dirent *de, char fn[MAXPGPATH], + struct stat st, char relative_path[MAXPGPATH], + manifesthash_hash *hashtab); +static char *nextLine(char *buf); +static char *nextWord(char *line); static void cleanup_directories_atexit(void) @@ -401,6 +437,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 +2204,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 +2377,9 @@ main(int argc, char **argv) case 'm': manifest_checksums = pg_strdup(optarg); break; + case 4: + verify_backup = true; + break; default: /* @@ -2460,6 +2502,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 +2572,416 @@ 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 = create_manifest_hash(manifest_path); + + scan_data_directory(basedir, NULL, 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 checksum for the records that are read from file and + * compare that with checksum written in backup_manifest file. If both + * checksums are identical then proceed, otherwise throw an error and abort. + */ +static manifesthash_hash * +create_manifest_hash(char *manifest_path) +{ + manifesthash_hash *hashtab; + DataDirectoryFileInfo *entry; + char *buf; + int fd; + struct stat stat; + + fd = open(manifest_path, O_RDONLY, 0); + + if (fstat(fd, &stat)) + { + pg_log_error("could not stat file \"%s\": %m", manifest_path); + close(fd); + exit(1); + } + + buf = pg_malloc(stat.st_size); + + if (buf == NULL) + { + pg_log_error("out of memory - Could not allocate enough memory to read file \"%s\".", + manifest_path); + close(fd); + exit(1); + } + + hashtab = manifesthash_create(1024, NULL); + + if (read(fd, buf, stat.st_size) != stat.st_size) + { + pg_log_error("could not read file \"%s\": %m", manifest_path); + + close(fd); + exit(1); + } + + /* Read the buffer line by line and parse each line into fields */ + if (*buf != '\0') + { + char *header_line; + int header_length; + bool checksum_initiated = false; + ChecksumCtx cCtx; + int checksum_label_length = 0; + + /* + * 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; + + /* + * 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') + { + 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 Manifest-Checksum entry, then finalize the checksum + * and compare it with the manifest checksum parsed from the file + */ + if (strncmp(line, "Manifest-Checksum", 17) == 0) + { + char checksumbuf[256]; + int checksumbuflen; + char encoded_checksum[256]; + + checksumbuflen = finalize_checksum(&cCtx, checksum_type, + (char *) checksumbuf); + checksumbuflen = hex_encode(checksumbuf, checksumbuflen, + encoded_checksum); + encoded_checksum[checksumbuflen] = '\0'; + + line[length - 1] = '\0'; + + line = nextWord(line); + + line = line + checksum_label_length; + + if (strcmp(encoded_checksum, line) != 0) + { + pg_log_error("backup manifest checksum difference. Aborting"); + exit(1); + } + } + + /* + * If it is a File record, then parse it into fields. With this we + * will get the pointers for filename, checksum and size in long + */ + else if (strncmp(line, "File", 4) == 0) + { + char *filename; + char *checksum; + char *size; + long filesize; + bool found; + char *record; /* pointer for a single record */ + long filelength, + sizelength; + + record = line; + + /* skip the "File" field */ + line = nextWord(line); + + filename = line; + line = nextWord(line); + filelength = line - filename; + + size = line; + line = nextWord(line); + sizelength = line - size; + + /* skip mtime field */ + line = nextWord(line); + + checksum = line; + + if (!checksum_initiated) + { + if (strncmp(checksum, "SHA256", 6) == 0) + { + checksum_type = MC_SHA256; + checksum_label_length = strlen("SHA256:"); + } + else if (strncmp(checksum, "CRC32C", 6) == 0) + { + checksum_type = MC_CRC32C; + checksum_label_length = strlen("CRC32C:"); + } + else if (strncmp(checksum, "-", 1) == 0) + checksum_type = MC_NONE; + else + { + pg_log_error("unknown checksum method"); + exit(1); + } + + /* + * we don't have checksum type in the header, so need to + * read through the first file entry to find the checksum + * type for the manifest file and initialize the checksum + * for the manifest file itself. + */ + if (checksum_type != MC_NONE) + { + /* initialize the checksum for the first time */ + initialize_checksum(&cCtx, checksum_type); + + /* feed the header to the checksum machinery */ + update_checksum(&cCtx, checksum_type, header_line, + header_length); + } + + checksum_initiated = true; + } + + /* feed line to the checksum machinery. */ + if (checksum_type != MC_NONE) + update_checksum(&cCtx, checksum_type, record, length); + + /* terminate the each field */ + record[length - 1] = '\0'; + filename[filelength - 1] = '\0'; + size[sizelength - 1] = '\0'; + + /* convert from string to long */ + filesize = atol(size); + + /* + * Increase the checksum by its label length so that we can + * get the exact checksum + */ + checksum = checksum + checksum_label_length; + + /* insert the hash record */ + entry = manifesthash_insert(hashtab, filename, &found); + entry->filesize = filesize; + entry->checksum = checksum; + } + else + { + pg_log_error("invalid record found in \"%s\"", manifest_path); + exit(1); + } + } + } + close(fd); + return hashtab; +} + +/* + * Scan the data directory and check whether each file entry present in hash + * table with the correct details, i.e. filesize and checksum. + */ +static void +scan_data_directory(char *basedir, const char *subdirpath, + manifesthash_hash *hashtab) +{ + char path[MAXPGPATH]; + char relative_path[MAXPGPATH] = ""; + DIR *dir; + struct dirent *de; + + if (subdirpath) + { + snprintf(path, sizeof(path), "%s/%s", basedir, + subdirpath); + snprintf(relative_path, sizeof(relative_path), "%s/", subdirpath); + + } + else + snprintf(path, sizeof(path), "%s", basedir); + + 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]; + struct stat st; + + if (strcmp(de->d_name, ".") == 0 || strcmp(de->d_name, "..") == 0 || + (strcmp(de->d_name, "pg_wal") == 0 && !subdirpath)) + 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)) + { + verify_file(de, fn, st, relative_path, hashtab); + } + else if (S_ISDIR(st.st_mode)) + { + char newsubdirpath[MAXPGPATH]; + + if (subdirpath) + snprintf(newsubdirpath, MAXPGPATH, "%s/%s", subdirpath, + de->d_name); + else + snprintf(newsubdirpath, MAXPGPATH, "%s", de->d_name); + + scan_data_directory(basedir, newsubdirpath, 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 +verify_file(struct dirent *de, char fn[MAXPGPATH], struct stat st, + char relative_path[MAXPGPATH], manifesthash_hash *hashtab) +{ + char filename[MAXPGPATH]; + DataDirectoryFileInfo *record; + + /* Skip backup manifest file. */ + if (strcmp(de->d_name, "backup_manifest") == 0) + return; + + snprintf(filename, MAXPGPATH, "%s%s", relative_path, de->d_name); + + /* + * Compare the hash 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, filename);; + if (record) + { + record->matched = true; + if (record->filesize != st.st_size) + pg_log_info("file \"%s\" has size %d in manifest but size %lu in backup", + filename, 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(fn, "r"); + if (!fp) + { + pg_log_error("could not open file \"%s\": %m", de->d_name); + 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", + filename, record->checksum, encode_checksumbuf); + } + } + else + pg_log_info("file \"%s\" is present in backup but not in manifest", + filename); +} + +/* + * 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 = buf + 1; + + return buf + 1; +} + +/* + * Find out the next tab or new line character from the provided string and + * return char pointer pointing to next character after that. + */ +static char * +nextWord(char *line) +{ + while (*line != '\0' && *line != '\t' && *line != '\n') + line = line + 1; + + return line + 1; +} 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