From dc347f14844be6d2aa08374fee3514e1fdeb0082 Mon Sep 17 00:00:00 2001 From: Ian Barwick Date: Fri, 28 Apr 2017 22:00:26 +0900 Subject: [PATCH] Additional "standby clone" code We'll break up the unwieldy "do_standby_clone()" function into discrete unit for easier maintenance. --- Makefile.in | 10 +- config.c | 12 ++ config.h | 3 +- dbutils.c | 48 +++++- dbutils.h | 2 + dirutil.c | 341 ++++++++++++++++++++++++++++++++++++++++ dirutil.h | 19 +++ repmgr-action-standby.c | 196 ++++++++++++++++++++++- repmgr-client-global.h | 1 + repmgr-client.c | 44 ++++++ repmgr-client.h | 1 - repmgr.h | 1 + strutil.c | 2 + strutil.h | 2 + 14 files changed, 668 insertions(+), 14 deletions(-) create mode 100644 dirutil.c create mode 100644 dirutil.h diff --git a/Makefile.in b/Makefile.in index 27c1e507..8815eca1 100644 --- a/Makefile.in +++ b/Makefile.in @@ -27,7 +27,7 @@ include Makefile.global $(info Building against PostgreSQL $(MAJORVERSION)) REPMGR_CLIENT_OBJS = repmgr-client.o repmgr-action-master.o repmgr-action-standby.o repmgr-action-cluster.o \ - config.o log.o strutil.o dbutils.o + config.o log.o strutil.o dbutils.o dirutil.o REPMGRD_OBJS = repmgrd.o $(REPMGR_CLIENT_OBJS): repmgr-client.h @@ -52,7 +52,15 @@ maintainer-clean: additional-maintainer-clean additional-clean: rm -f repmgr-client.o + rm -f repmgr-action-cluster.o + rm -f repmgr-action-master.o + rm -f repmgr-action-standby.o rm -f repmgrd.o + rm -f config.o + rm -f dbutils.o + rm -f dirutil.o + rm -f log.o + rm -f strutil.o maintainer-additional-clean: clean rm -f configure diff --git a/config.c b/config.c index c39c405e..7e421f3c 100644 --- a/config.c +++ b/config.c @@ -441,6 +441,8 @@ _parse_config(t_configuration_options *options, ItemList *error_list, ItemList * } /* barman settings */ + else if (strcmp(name, "barman_host") == 0) + strncpy(options->barman_host, value, MAXLEN); else if (strcmp(name, "barman_server") == 0) strncpy(options->barman_server, value, MAXLEN); else if (strcmp(name, "barman_config") == 0) @@ -535,6 +537,16 @@ _parse_config(t_configuration_options *options, ItemList *error_list, ItemList * PQconninfoFree(conninfo_options); } + + /* add warning about changed "barman_" parameter meanings */ + if (options->barman_server[0] == '\0' && options->barman_server[0] != '\0') + { + item_list_append(warning_list, + _("use \"barman_host\" for the hostname of the Barman server")); + item_list_append(warning_list, + _("use \"barman_server\" for the name of the [server] section in the Barman configururation file")); + + } } diff --git a/config.h b/config.h index 9023c6af..565e276f 100644 --- a/config.h +++ b/config.h @@ -95,6 +95,7 @@ typedef struct int bdr_monitoring_mode; /* barman settings */ + char barman_host[MAXLEN]; char barman_server[MAXLEN]; char barman_config[MAXLEN]; } t_configuration_options; @@ -122,7 +123,7 @@ typedef struct /* bdr settings */ \ BDR_MONITORING_LOCAL, \ /* barman settings */ \ - "", "" } + "", "", "" } diff --git a/dbutils.c b/dbutils.c index a3b9311a..8b6e894d 100644 --- a/dbutils.c +++ b/dbutils.c @@ -195,6 +195,48 @@ establish_db_connection_by_params(const char *keywords[], const char *values[], /* =============================== */ +/* + * get_conninfo_value() + * + * Extract the value represented by 'keyword' in 'conninfo' and copy + * it to the 'output' buffer. + * + * Returns true on success, or false on failure (conninfo string could + * not be parsed, or provided keyword not found). + */ + +bool +get_conninfo_value(const char *conninfo, const char *keyword, char *output) +{ + PQconninfoOption *conninfo_options; + PQconninfoOption *conninfo_option; + + conninfo_options = PQconninfoParse(conninfo, NULL); + + if (conninfo_options == NULL) + { + log_error(_("unable to parse provided conninfo string \"%s\""), conninfo); + return false; + } + + for (conninfo_option = conninfo_options; conninfo_option->keyword != NULL; conninfo_option++) + { + if (strcmp(conninfo_option->keyword, keyword) == 0) + { + if (conninfo_option->val != NULL && conninfo_option->val[0] != '\0') + { + strncpy(output, conninfo_option->val, MAXLEN); + break; + } + } + } + + PQconninfoFree(conninfo_options); + + return true; +} + + void initialize_conninfo_params(t_conninfo_param_list *param_list, bool set_defaults) { @@ -383,7 +425,7 @@ begin_transaction(PGconn *conn) if (PQresultStatus(res) != PGRES_COMMAND_OK) { - log_error(_("Unable to begin transaction:\n %s"), + log_error(_("unable to begin transaction:\n %s"), PQerrorMessage(conn)); PQclear(res); @@ -407,7 +449,7 @@ commit_transaction(PGconn *conn) if (PQresultStatus(res) != PGRES_COMMAND_OK) { - log_error(_("Unable to commit transaction:\n %s"), + log_error(_("unable to commit transaction:\n %s"), PQerrorMessage(conn)); PQclear(res); @@ -431,7 +473,7 @@ rollback_transaction(PGconn *conn) if (PQresultStatus(res) != PGRES_COMMAND_OK) { - log_error(_("Unable to rollback transaction:\n %s"), + log_error(_("unable to rollback transaction:\n %s"), PQerrorMessage(conn)); PQclear(res); diff --git a/dbutils.h b/dbutils.h index 673acb9c..5f792134 100644 --- a/dbutils.h +++ b/dbutils.h @@ -121,6 +121,8 @@ PGconn *establish_db_connection_by_params(const char *keywords[], /* conninfo manipulation functions */ +bool get_conninfo_value(const char *conninfo, const char *keyword, char *output); + void initialize_conninfo_params(t_conninfo_param_list *param_list, bool set_defaults); void copy_conninfo_params(t_conninfo_param_list *dest_list, t_conninfo_param_list *source_list); void conn_to_param_list(PGconn *conn, t_conninfo_param_list *param_list); diff --git a/dirutil.c b/dirutil.c new file mode 100644 index 00000000..d2c1126f --- /dev/null +++ b/dirutil.c @@ -0,0 +1,341 @@ +/* + * + * dirmod.c + * directory handling functions + * + * Copyright (c) 2ndQuadrant, 2010-2017 + */ + +#include +#include +#include +#include +#include +#include +#include + +/* NB: postgres_fe must be included BEFORE check_dir */ +#include +#include + +#include "dirutil.h" +#include "strutil.h" +#include "log.h" + + +static bool _create_pg_dir(char *dir, bool force, bool for_witness); +static int unlink_dir_callback(const char *fpath, const struct stat *sb, int typeflag, struct FTW *ftwbuf); + + + +/* + * make sure the directory either doesn't exist or is empty + * we use this function to check the new data directory and + * the directories for tablespaces + * + * This is the same check initdb does on the new PGDATA dir + * + * Returns 0 if nonexistent, 1 if exists and empty, 2 if not empty, + * or -1 if trouble accessing directory + */ +int +check_dir(char *path) +{ + DIR *chkdir; + struct dirent *file; + int result = 1; + + errno = 0; + + chkdir = opendir(path); + + if (!chkdir) + return (errno == ENOENT) ? 0 : -1; + + while ((file = readdir(chkdir)) != NULL) + { + if (strcmp(".", file->d_name) == 0 || + strcmp("..", file->d_name) == 0) + { + /* skip this and parent directory */ + continue; + } + else + { + result = 2; /* not empty */ + break; + } + } + +#ifdef WIN32 + + /* + * This fix is in mingw cvs (runtime/mingwex/dirent.c rev 1.4), but not in + * released version + */ + if (GetLastError() == ERROR_NO_MORE_FILES) + errno = 0; +#endif + + closedir(chkdir); + + if (errno != 0) + return -1; /* some kind of I/O error? */ + + return result; +} + + +/* + * Create directory with error log message when failing + */ +bool +create_dir(char *path) +{ + if (mkdir_p(path, 0700) == 0) + return true; + + log_error(_("unable to create directory \"%s\": %s"), + path, strerror(errno)); + + return false; +} + +bool +set_dir_permissions(char *path) +{ + return (chmod(path, 0700) != 0) ? false : true; +} + + + +/* function from initdb.c */ +/* source adapted from FreeBSD /src/bin/mkdir/mkdir.c */ + +/* + * this tries to build all the elements of a path to a directory a la mkdir -p + * we assume the path is in canonical form, i.e. uses / as the separator + * we also assume it isn't null. + * + * note that on failure, the path arg has been modified to show the particular + * directory level we had problems with. + */ +int +mkdir_p(char *path, mode_t omode) +{ + struct stat sb; + mode_t numask, + oumask; + int first, + last, + retval; + char *p; + + p = path; + oumask = 0; + retval = 0; + +#ifdef WIN32 + /* skip network and drive specifiers for win32 */ + if (strlen(p) >= 2) + { + if (p[0] == '/' && p[1] == '/') + { + /* network drive */ + p = strstr(p + 2, "/"); + if (p == NULL) + return 1; + } + else if (p[1] == ':' && + ((p[0] >= 'a' && p[0] <= 'z') || + (p[0] >= 'A' && p[0] <= 'Z'))) + { + /* local drive */ + p += 2; + } + } +#endif + + if (p[0] == '/') /* Skip leading '/'. */ + ++p; + for (first = 1, last = 0; !last; ++p) + { + if (p[0] == '\0') + last = 1; + else if (p[0] != '/') + continue; + *p = '\0'; + if (!last && p[1] == '\0') + last = 1; + if (first) + { + /* + * POSIX 1003.2: For each dir operand that does not name an + * existing directory, effects equivalent to those caused by the + * following command shall occcur: + * + * mkdir -p -m $(umask -S),u+wx $(dirname dir) && mkdir [-m mode] + * dir + * + * We change the user's umask and then restore it, instead of + * doing chmod's. + */ + oumask = umask(0); + numask = oumask & ~(S_IWUSR | S_IXUSR); + (void) umask(numask); + first = 0; + } + if (last) + (void) umask(oumask); + + /* check for pre-existing directory; ok if it's a parent */ + if (stat(path, &sb) == 0) + { + if (!S_ISDIR(sb.st_mode)) + { + if (last) + errno = EEXIST; + else + errno = ENOTDIR; + retval = 1; + break; + } + } + else if (mkdir(path, last ? omode : S_IRWXU | S_IRWXG | S_IRWXO) < 0) + { + retval = 1; + break; + } + if (!last) + *p = '/'; + } + if (!first && !last) + (void) umask(oumask); + return retval; +} + + +bool +is_pg_dir(char *path) +{ + const size_t buf_sz = 8192; + char dirpath[buf_sz]; + struct stat sb; + int r; + + /* test pgdata */ + snprintf(dirpath, buf_sz, "%s/PG_VERSION", path); + if (stat(dirpath, &sb) == 0) + return true; + + /* test tablespace dir */ + sprintf(dirpath, "ls %s/PG_*/ -I*", path); + r = system(dirpath); + if (r == 0) + return true; + + return false; +} + + +bool +create_pg_dir(char *path, bool force) +{ + return _create_pg_dir(path, force, false); +} + +bool +create_witness_pg_dir(char *path, bool force) +{ + return _create_pg_dir(path, force, true); +} + + +static bool +_create_pg_dir(char *path, bool force, bool for_witness) +{ + bool pg_dir = false; + + /* Check this directory could be used as a PGDATA dir */ + switch (check_dir(path)) + { + case 0: + /* dir not there, must create it */ + log_info(_("creating directory \"%s\"...\n"), path); + + if (!create_dir(path)) + { + log_error(_("unable to create directory \"%s\"..."), + path); + return false; + } + break; + case 1: + /* Present but empty, fix permissions and use it */ + log_info(_("checking and correcting permissions on existing directory %s ...\n"), + path); + + if (!set_dir_permissions(path)) + { + log_error(_("unable to change permissions of directory \"%s\": %s"), + path, strerror(errno)); + return false; + } + break; + case 2: + /* Present and not empty */ + log_warning(_("directory \"%s\" exists but is not empty\n"), + path); + + pg_dir = is_pg_dir(path); + + + if (pg_dir && force) + { + + /* + * The witness server does not store any data other than a copy of the + * repmgr metadata, so in --force mode we can simply overwrite the + * directory. + * + * For non-witness servers, we'll leave the data in place, both to reduce + * the risk of unintentional data loss and to make it possible for the + * data directory to be brought up-to-date with rsync. + */ + if (for_witness) + { + log_notice(_("deleting existing data directory \"%s\""), path); + nftw(path, unlink_dir_callback, 64, FTW_DEPTH | FTW_PHYS); + } + /* Let it continue */ + break; + } + else if (pg_dir && !force) + { + log_hint(_("This looks like a PostgreSQL directory.\n" + "If you are sure you want to clone here, " + "please check there is no PostgreSQL server " + "running and use the -F/--force option\n")); + return false; + } + + return false; + default: + log_error(_("could not access directory \"%s\": %s"), + path, strerror(errno)); + return false; + } + return true; +} + +static int +unlink_dir_callback(const char *fpath, const struct stat *sb, int typeflag, struct FTW *ftwbuf) +{ + int rv = remove(fpath); + + if (rv) + perror(fpath); + + return rv; +} + diff --git a/dirutil.h b/dirutil.h new file mode 100644 index 00000000..15ab75f3 --- /dev/null +++ b/dirutil.h @@ -0,0 +1,19 @@ +/* + * dirutil.h + * Copyright (c) 2ndQuadrant, 2010-2017 + * + */ + +#ifndef _DIRUTIL_H_ +#define _DIRUTIL_H_ + +extern int mkdir_p(char *path, mode_t omode); +extern bool set_dir_permissions(char *path); + +extern int check_dir(char *path); +extern bool create_dir(char *path); +extern bool is_pg_dir(char *path); +extern bool create_pg_dir(char *path, bool force); +extern bool create_witness_pg_dir(char *path, bool force); + +#endif diff --git a/repmgr-action-standby.c b/repmgr-action-standby.c index d4738415..131c456b 100644 --- a/repmgr-action-standby.c +++ b/repmgr-action-standby.c @@ -7,10 +7,19 @@ */ #include "repmgr.h" +#include "dirutil.h" #include "repmgr-client-global.h" #include "repmgr-action-standby.h" +static char local_data_directory[MAXPGPATH]; + +/* used by barman mode */ +static char local_repmgr_tmp_directory[MAXPGPATH]; + + +static void check_barman_config(void); +static char *make_barman_ssh_command(char *buf); void @@ -20,7 +29,7 @@ do_standby_clone(void) PGconn *source_conn = NULL; PGresult *res; - int server_version_num = -1; + int server_version_num = UNKNOWN_SERVER_VERSION_NUM; char cluster_size[MAXLEN]; /* @@ -32,6 +41,8 @@ do_standby_clone(void) bool upstream_record_found = false; int upstream_node_id = UNKNOWN_NODE_ID; + char upstream_data_directory[MAXPGPATH]; + bool local_data_directory_provided = false; enum { barman, @@ -39,19 +50,13 @@ do_standby_clone(void) pg_basebackup } mode; - /* used by barman mode */ - char datadir_list_filename[MAXLEN]; - char local_repmgr_tmp_directory[MAXPGPATH]; - - puts("standby clone"); - /* * detecting the cloning mode */ if (runtime_options.rsync_only) mode = rsync; - else if (strcmp(config_file_options.barman_server, "") != 0 && ! runtime_options.without_barman) + else if (strcmp(config_file_options.barman_host, "") != 0 && ! runtime_options.without_barman) mode = barman; else mode = pg_basebackup; @@ -72,4 +77,179 @@ do_standby_clone(void) } } + /* + * If dest_dir (-D/--pgdata) was provided, this will become the new data + * directory (otherwise repmgr will default to using the same directory + * path as on the source host). + * + * Note that barman mode requires -D/--pgdata. + * + * If -D/--pgdata is not supplied, and we're not cloning from barman, + * the source host's data directory will be fetched later, after + * we've connected to it. + */ + if (runtime_options.data_dir[0]) + { + local_data_directory_provided = true; + log_notice(_("destination directory '%s' provided"), + runtime_options.data_dir); + } + else if (mode == barman) + { + log_error(_("Barman mode requires a data directory")); + log_hint(_("use -D/--pgdata to explicitly specify a data directory")); + exit(ERR_BAD_CONFIG); + } + + /* Sanity-check barman connection and installation */ + if (mode == barman) + { + /* this will exit with ERR_BARMAN if problems found */ + check_barman_config(); + } + + + /* + * target directory (-D/--pgdata) provided - use that as new data directory + * (useful when executing backup on local machine only or creating the backup + * in a different local directory when backup source is a remote host) + */ + if (local_data_directory_provided == true) + { + strncpy(local_data_directory, runtime_options.data_dir, MAXPGPATH); + } + + /* + * Initialise list of conninfo parameters which will later be used + * to create the `primary_conninfo` string in recovery.conf . + * + * We'll initialise it with the default values as seen by libpq, + * and overwrite them with the host settings specified on the command + * line. As it's possible the standby will be cloned from a node different + * to its intended upstream, we'll later attempt to fetch the + * upstream node record and overwrite the values set here with + * those from the upstream node record (excluding that record's + * application_name) + */ + initialize_conninfo_params(&recovery_conninfo, true); + + copy_conninfo_params(&recovery_conninfo, &source_conninfo); + + /* + * If application_name is set in repmgr.conf's conninfo parameter, use + * this value (if the source host was provided as a conninfo string, any + * application_name values set there will be overridden; we assume the only + * reason to pass an application_name via the command line is in the + * rare corner case where a user wishes to clone a server without + * providing repmgr.conf) + */ + if (strlen(config_file_options.conninfo)) + { + char application_name[MAXLEN] = ""; + + get_conninfo_value(config_file_options.conninfo, "application_name", application_name); + if (strlen(application_name)) + { + param_set(&recovery_conninfo, "application_name", application_name); + } + } + +} + + + +void +check_barman_config(void) +{ + char datadir_list_filename[MAXLEN]; + char barman_command_buf[MAXLEN] = ""; + + char command[MAXLEN]; + bool command_ok; + + /* + * Check that there is at least one valid backup + */ + + log_info(_("connecting to Barman server to verify backup for %s"), config_file_options.barman_server); + + maxlen_snprintf(command, "%s show-backup %s latest > /dev/null", + make_barman_ssh_command(barman_command_buf), + config_file_options.barman_server); + + command_ok = local_command(command, NULL); + + if (command_ok == false) + { + log_error(_("no valid backup for server %s was found in the Barman catalogue"), + config_file_options.barman_server); + log_hint(_("refer to the Barman documentation for more information\n")); + + exit(ERR_BARMAN); + } + + /* + * Create the local repmgr subdirectory + */ + + maxlen_snprintf(local_repmgr_tmp_directory, + "%s/repmgr", local_data_directory); + + maxlen_snprintf(datadir_list_filename, + "%s/data.txt", local_repmgr_tmp_directory); + + if (!create_pg_dir(local_data_directory, runtime_options.force)) + { + log_error(_("unable to use directory %s"), + local_data_directory); + log_hint(_("use -F/--force option to force this directory to be overwritten\n")); + exit(ERR_BAD_CONFIG); + } + + if (!create_pg_dir(local_repmgr_tmp_directory, runtime_options.force)) + { + log_error(_("unable to create directory \"%s\""), + local_repmgr_tmp_directory); + + exit(ERR_BAD_CONFIG); + } + + /* + * Fetch server parameters from Barman + */ + log_info(_("connecting to Barman server to fetch server parameters")); + + maxlen_snprintf(command, "%s show-server %s > %s/show-server.txt", + make_barman_ssh_command(barman_command_buf), + config_file_options.barman_server, + local_repmgr_tmp_directory); + + command_ok = local_command(command, NULL); + + if (command_ok == false) + { + log_error(_("unable to fetch server parameters from Barman server")); + + exit(ERR_BARMAN); + } + +} + + +static char * +make_barman_ssh_command(char *buf) +{ + static char config_opt[MAXLEN] = ""; + + if (strlen(config_file_options.barman_config)) + maxlen_snprintf(config_opt, + " --config=%s", + config_file_options.barman_config); + + maxlen_snprintf(buf, + "ssh %s barman%s", + config_file_options.barman_server, + config_opt); + + return buf; } diff --git a/repmgr-client-global.h b/repmgr-client-global.h index 95338279..3b5d743d 100644 --- a/repmgr-client-global.h +++ b/repmgr-client-global.h @@ -85,6 +85,7 @@ extern t_node_info target_node_info; extern int check_server_version(PGconn *conn, char *server_type, bool exit_on_error, char *server_version_string); extern bool create_repmgr_extension(PGconn *conn); extern int test_ssh_connection(char *host, char *remote_user); +extern bool local_command(const char *command, PQExpBufferData *outputbuf); #endif diff --git a/repmgr-client.c b/repmgr-client.c index 67965d99..2ad9c09e 100644 --- a/repmgr-client.c +++ b/repmgr-client.c @@ -1047,3 +1047,47 @@ test_ssh_connection(char *host, char *remote_user) return r; } + + +/* + * Execute a command locally. If outputbuf == NULL, discard the + * output. + */ +bool +local_command(const char *command, PQExpBufferData *outputbuf) +{ + FILE *fp; + char output[MAXLEN]; + int retval; + + if (outputbuf == NULL) + { + retval = system(command); + return (retval == 0) ? true : false; + } + else + { + fp = popen(command, "r"); + + if (fp == NULL) + { + log_error(_("unable to execute local command:\n%s"), command); + return false; + } + + /* TODO: better error handling */ + while (fgets(output, MAXLEN, fp) != NULL) + { + appendPQExpBuffer(outputbuf, "%s", output); + } + + pclose(fp); + + if (outputbuf->data != NULL) + log_verbose(LOG_DEBUG, "local_command(): output returned was:\n%s", outputbuf->data); + else + log_verbose(LOG_DEBUG, "local_command(): no output returned"); + + return true; + } +} diff --git a/repmgr-client.h b/repmgr-client.h index 1fe0f00d..cee10e8a 100644 --- a/repmgr-client.h +++ b/repmgr-client.h @@ -130,7 +130,6 @@ static struct option long_options[] = static void do_help(void); -static void do_standby_clone(void); static const char *action_name(const int action); diff --git a/repmgr.h b/repmgr.h index ca23d7e8..6e1ddd9c 100644 --- a/repmgr.h +++ b/repmgr.h @@ -21,6 +21,7 @@ #define MIN_SUPPORTED_VERSION "9.3" #define MIN_SUPPORTED_VERSION_NUM 90300 +#define UNKNOWN_SERVER_VERSION_NUM -1 #define NODE_NOT_FOUND -1 #define NO_UPSTREAM_NODE -1 diff --git a/strutil.c b/strutil.c index 28055e3c..0e07d0cf 100644 --- a/strutil.c +++ b/strutil.c @@ -8,6 +8,8 @@ #include #include + + #include "log.h" #include "strutil.h" diff --git a/strutil.h b/strutil.h index b9d4a4a2..ed739518 100644 --- a/strutil.h +++ b/strutil.h @@ -6,6 +6,8 @@ #ifndef _STRUTIL_H_ #define _STRUTIL_H_ +#include + #define MAXLEN 1024 #define MAX_QUERY_LEN 8192