v5-0002-Implementation-of-backup-validator.patch
application/octet-stream
Filename: v5-0002-Implementation-of-backup-validator.patch
Type: application/octet-stream
Part: 1
Message:
Re: backup manifests
Patch
Same data as JSON:
GET /api/v1/attachments/:id/patch
the parsed metadata as JSON — format, series position, per-file stats; never the diff bytes.
API reference →
Format: format-patch
Series: patch v5-0002
Subject: Implementation of backup validator
| File | + | − |
|---|---|---|
| doc/src/sgml/ref/pg_basebackup.sgml | 12 | 0 |
| src/bin/pg_basebackup/pg_basebackup.c | 461 | 0 |
| src/common/encode.c | 18 | 0 |
| src/include/common/encode.h | 1 | 0 |
| src/tools/pgindent/typedefs.list | 2 | 0 |
From 7c8b2f1ad635bc669e95b7a2000d4836468f74b5 Mon Sep 17 00:00:00 2001
From: Suraj Kharage <suraj.kharage@enterprisedb.com>
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
</listitem>
</varlistentry>
+ <varlistentry>
+ <term><option>--verify-backup </option></term>
+ <listitem>
+ <para>
+ 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>--pgdata</option> option. Tar format
+ backups can be verified after untarring.
+ </para>
+ </listitem>
+ </varlistentry>
+
</variablelist>
</para>
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