From 71a0871232c2d1501c7d05bf6c187c25707d81f9 Mon Sep 17 00:00:00 2001 From: Ian Barwick Date: Tue, 11 Jul 2017 15:38:04 +0900 Subject: [PATCH] Add "repmgr bdr register" --- Makefile.in | 6 +- README.md | 66 ++++++++++++ dbutils.c | 111 +++++++++++++++++++- dbutils.h | 8 +- repmgr-action-bdr.c | 221 ++++++++++++++++++++++++++++++++++++++++ repmgr-action-bdr.h | 13 +++ repmgr-action-standby.h | 2 +- repmgr-client.c | 48 +++++++-- 8 files changed, 458 insertions(+), 17 deletions(-) create mode 100644 repmgr-action-bdr.c create mode 100644 repmgr-action-bdr.h diff --git a/Makefile.in b/Makefile.in index 252e1811..bcd8eacc 100644 --- a/Makefile.in +++ b/Makefile.in @@ -26,7 +26,8 @@ include Makefile.global $(info Building against PostgreSQL $(MAJORVERSION)) -REPMGR_CLIENT_OBJS = repmgr-client.o repmgr-action-primary.o repmgr-action-standby.o repmgr-action-cluster.o \ +REPMGR_CLIENT_OBJS = repmgr-client.o \ + repmgr-action-primary.o repmgr-action-standby.o repmgr-action-bdr.o repmgr-action-cluster.o \ config.o log.o strutil.o dbutils.o dirutil.o compat.o controldata.o REPMGRD_OBJS = repmgrd.o config.o log.o dbutils.o strutil.o @@ -52,9 +53,10 @@ maintainer-clean: additional-maintainer-clean additional-clean: rm -f repmgr-client.o - rm -f repmgr-action-cluster.o rm -f repmgr-action-primary.o rm -f repmgr-action-standby.o + rm -f repmgr-action-bdr.o + rm -f repmgr-action-cluster.o rm -f repmgrd.o rm -f compat.o rm -f config.o diff --git a/README.md b/README.md index a3a74bec..2036d865 100644 --- a/README.md +++ b/README.md @@ -37,6 +37,8 @@ Currently available: repmgr standby promote repmgr standby follow + repmgr bdr register + repmgr cluster event [--all] [--node-id] [--node-name] [--event] [--event-matching] @@ -63,3 +65,67 @@ and vice-versa, e.g. to avoid hard-coding things like a node's upstream ID which might change. TODO: possibly add a config file conversion script/function. + +Generating event notifications with repmgr/repmgrd +-------------------------------------------------- + +Each time `repmgr` or `repmgrd` perform a significant event, a record +of that event is written into the `repl_events` table together with +a timestamp, an indication of failure or success, and further details +if appropriate. This is useful for gaining an overview of events +affecting the replication cluster. However note that this table has +advisory character and should be used in combination with the `repmgr` +and PostgreSQL logs to obtain details of any events. + +Example output after a master was registered and a standby cloned +and registered: + + repmgr=# SELECT * from repmgr_test.repl_events ; + node_id | event | successful | event_timestamp | details + ---------+------------------+------------+-------------------------------+------------------------------------------------------------------------------------- + 1 | master_register | t | 2016-01-08 15:04:39.781733+09 | + 2 | standby_clone | t | 2016-01-08 15:04:49.530001+09 | Cloned from host 'repmgr_node1', port 5432; backup method: pg_basebackup; --force: N + 2 | standby_register | t | 2016-01-08 15:04:50.621292+09 | + (3 rows) + +Additionally, event notifications can be passed to a user-defined program +or script which can take further action, e.g. send email notifications. +This is done by setting the `event_notification_command` parameter in +`repmgr.conf`. + +This parameter accepts the following format placeholders: + + %n - node ID + %e - event type + %s - success (1 or 0) + %t - timestamp + %d - details + +The values provided for "%t" and "%d" will probably contain spaces, +so should be quoted in the provided command configuration, e.g.: + + event_notification_command='/path/to/some/script %n %e %s "%t" "%d"' + +Additionally the following format placeholders are available for the even +type `bdr_failover`: + + %c - conninfo string of the next available node + %a - name of the next available node + +These should always be quoted. + +By default, all notification type will be passed to the designated script; +the notification types can be filtered to explicitly named ones: + + event_notifications=master_register,standby_register + +The following event types are available: + + ... + * `bdr_failover` + * `bdr_register` + * `bdr_unregister` + +Note that under some circumstances (e.g. no replication cluster master could +be located), it will not be possible to write an entry into the `repl_events` +table, in which case `event_notification_command` can serve as a fallback. diff --git a/dbutils.c b/dbutils.c index bfd5aff1..e2b1b82c 100644 --- a/dbutils.c +++ b/dbutils.c @@ -47,6 +47,33 @@ parse_lsn(const char *str) } +/* + * Wrap query with appropriate DDL function, if required. + */ +void +wrap_ddl_query(PQExpBufferData *query_buf, int replication_type, const char *fmt, ...) +{ + va_list arglist; + char buf[MAXLEN]; + + if (replication_type == REPLICATION_TYPE_BDR) + { + appendPQExpBuffer(query_buf, "SELECT bdr.bdr_replicate_ddl_command($repmgr$"); + } + + va_start(arglist, fmt); + vsnprintf(buf, MAXLEN, fmt, arglist); + va_end(arglist); + + appendPQExpBuffer(query_buf, "%s", buf); + + if (replication_type == REPLICATION_TYPE_BDR) + { + appendPQExpBuffer(query_buf, "$repmgr$)"); + } +} + + /* ==================== */ /* Connection functions */ /* ==================== */ @@ -1517,6 +1544,7 @@ update_node_record(PGconn *conn, char *repmgr_action, t_node_info *node_info) return _create_update_node_record(conn, "update", node_info); } + static bool _create_update_node_record(PGconn *conn, char *action, t_node_info *node_info) { @@ -2793,7 +2821,7 @@ is_bdr_repmgr(PGconn *conn) bool -is_table_in_bdr_replication_set(PGconn *conn, char *tablename, char *set) +is_table_in_bdr_replication_set(PGconn *conn, const char *tablename, const char *set) { PQExpBufferData query; PGresult *res; @@ -2829,10 +2857,10 @@ is_table_in_bdr_replication_set(PGconn *conn, char *tablename, char *set) bool -add_table_to_bdr_replication_set(PGconn *conn, char *tablename, char *set) +add_table_to_bdr_replication_set(PGconn *conn, const char *tablename, const char *set) { PQExpBufferData query; - PGresult *res; + PGresult *res; initPQExpBuffer(&query); @@ -2861,3 +2889,80 @@ add_table_to_bdr_replication_set(PGconn *conn, char *tablename, char *set) return true; } + + +bool +bdr_node_exists(PGconn *conn, const char *node_name) +{ + PQExpBufferData query; + PGresult *res; + bool node_exists; + + initPQExpBuffer(&query); + + appendPQExpBuffer( + &query, + "SELECT COUNT(*)" + " FROM bdr.bdr_nodes" + " WHERE node_name = '%s'", + node_name); + + res = PQexec(conn, query.data); + termPQExpBuffer(&query); + + if (!res || PQresultStatus(res) != PGRES_TUPLES_OK) + { + node_exists = false; + } + else + { + node_exists = atoi(PQgetvalue(res, 0, 0)) == 1 ? true : false; + } + + PQclear(res); + + return node_exists; +} + + +void +add_extension_tables_to_bdr_replication_set(PGconn *conn) +{ + PQExpBufferData query; + PGresult *res; + + initPQExpBuffer(&query); + + appendPQExpBuffer( + &query, + " SELECT c.relname " + " FROM pg_class c " + "INNER JOIN pg_namespace n " + " ON c.relnamespace = n.oid " + " WHERE n.nspname = 'repmgr' " + " AND c.relkind = 'r' "); + + res = PQexec(conn, query.data); + termPQExpBuffer(&query); + + if (!res || PQresultStatus(res) != PGRES_TUPLES_OK) + { + // + } + else + { + int i; + + for (i = 0; i < PQntuples(res); i++) + { + add_table_to_bdr_replication_set( + conn, + PQgetvalue(res, i, 0), + "repmgr"); + } + } + + PQclear(res); + + return; +} diff --git a/dbutils.h b/dbutils.h index e8afc689..8253ecf5 100644 --- a/dbutils.h +++ b/dbutils.h @@ -148,6 +148,8 @@ typedef struct s_connection_user XLogRecPtr parse_lsn(const char *str); +extern void wrap_ddl_query(PQExpBufferData *query_buf, int replication_type, const char *fmt, ...) +__attribute__((format(PG_PRINTF_ATTRIBUTE, 3, 4))); /* connection functions */ PGconn *establish_db_connection(const char *conninfo, @@ -268,8 +270,10 @@ XLogRecPtr get_last_wal_receive_location(PGconn *conn); bool is_bdr_db(PGconn *conn); bool is_bdr_repmgr(PGconn *conn); -bool is_table_in_bdr_replication_set(PGconn *conn, char *tablename, char *set); -bool add_table_to_bdr_replication_set(PGconn *conn, char *tablename, char *set); +bool is_table_in_bdr_replication_set(PGconn *conn, const char *tablename, const char *set); +bool add_table_to_bdr_replication_set(PGconn *conn, const char *tablename, const char *set); +bool bdr_node_exists(PGconn *conn, const char *node_name); +void add_extension_tables_to_bdr_replication_set(PGconn *conn); #endif /* dbutils.h */ diff --git a/repmgr-action-bdr.c b/repmgr-action-bdr.c new file mode 100644 index 00000000..da497748 --- /dev/null +++ b/repmgr-action-bdr.c @@ -0,0 +1,221 @@ +/* + * repmgr-action-standby.c + * + * Implements BDR-related actions for the repmgr command line utility + * + * Copyright (c) 2ndQuadrant, 2010-2017 + */ + +#include "repmgr.h" + +#include "repmgr-client-global.h" +#include "repmgr-action-bdr.h" + + +/* + * do_bdr_register() + * + * As each BDR node is its own master, registering a BDR node + * will create the repmgr metadata schema if necessary. + */ +void +do_bdr_register(void) +{ + PGconn *conn = NULL; + ExtensionStatus extension_status; + t_node_info node_info = T_NODE_INFO_INITIALIZER; + RecordStatus record_status; + PQExpBufferData event_details; + bool success = true; + + /* sanity-check configuration for BDR-compatability */ + if (config_file_options.replication_type != REPLICATION_TYPE_BDR) + { + log_error(_("cannot run BDR REGISTER on a non-BDR node")); + exit(ERR_BAD_CONFIG); + } + + conn = establish_db_connection(config_file_options.conninfo, true); + + if (!is_bdr_db(conn)) + { + /* TODO: name database */ + log_error(_("database is not BDR-enabled")); + log_hint(_("when using repmgr with BDR, the repmgr schema must be stored in the BDR database")); + exit(ERR_BAD_CONFIG); + } + + /* check whether repmgr extension exists, and that any other nodes are BDR */ + extension_status = get_repmgr_extension_status(conn); + + if (extension_status == REPMGR_UNKNOWN) + { + log_error(_("unable to determine status of \"repmgr\" extension")); + PQfinish(conn); + } + + + if (extension_status == REPMGR_UNAVAILABLE) + { + log_error(_("\"repmgr\" extension is not available")); + PQfinish(conn); + } + + if (extension_status == REPMGR_INSTALLED) + { + if (!is_bdr_repmgr(conn)) + { + log_error(_("repmgr metadatabase contains records for non-BDR nodes")); + exit(ERR_BAD_CONFIG); + } + } + else + { + //log_info(_("bdr register: creating database objects inside the %s schema"), + // get_repmgr_schema()); + + begin_transaction(conn); + + if (!create_repmgr_extension(conn)) + { + log_error(_("unable to create repmgr extension - see preceding error message(s); aborting")); + rollback_transaction(conn); + PQfinish(conn); + exit(ERR_BAD_CONFIG); + } + + commit_transaction(conn); + } + + /* check for a matching BDR node */ + { + bool node_exists = bdr_node_exists(conn, config_file_options.node_name); + + if (node_exists == false) + { + log_error(_("no BDR node with node_name '%s' found"), config_file_options.node_name); + log_hint(_("'node_name' in repmgr.conf must match 'node_name' in bdr.bdr_nodes\n")); + PQfinish(conn); + exit(ERR_BAD_CONFIG); + } + } + + initPQExpBuffer(&event_details); + + begin_transaction(conn); + + /* + * we'll check if a record exists (even if the schema was just created), + * as there's a faint chance of a race condition + */ + + record_status = get_node_record(conn, config_file_options.node_id, &node_info); + + /* Update internal node record */ + + node_info.type = BDR; + node_info.node_id = config_file_options.node_id; + node_info.upstream_node_id = NO_UPSTREAM_NODE; + node_info.active = true; + node_info.priority = config_file_options.priority; + + strncpy(node_info.node_name, config_file_options.node_name, MAXLEN); + strncpy(node_info.location, config_file_options.location, MAXLEN); + strncpy(node_info.conninfo, config_file_options.conninfo, MAXLEN); + + if (record_status == RECORD_FOUND) + { + bool node_updated; + /* + * At this point we will have established there are no non-BDR records, + * so no need to verify the node type + */ + if (!runtime_options.force) + { + log_error(_("this node is already registered")); + log_hint(_("use -F/--force to overwrite the existing node record")); + rollback_transaction(conn); + PQfinish(conn); + exit(ERR_BAD_CONFIG); + } + + /* + * don't permit changing the node name - this must match the + * BDR node name set when the node was registered. + */ + + if (strncmp(node_info.node_name, config_file_options.node_name, MAXLEN) != 0) + { + log_error(_("a record for node %i is already registered with node_name '%s'"), + config_file_options.node_id, node_info.node_name); + log_hint(_("node_name configured in repmgr.conf is '%s'"), config_file_options.node_name); + + rollback_transaction(conn); + PQfinish(conn); + exit(ERR_BAD_CONFIG); + } + + node_updated = update_node_record(conn, "bdr register", &node_info); + + if (node_updated == true) + { + appendPQExpBuffer(&event_details, _("node record updated for node '%s' (%i)"), + config_file_options.node_name, config_file_options.node_id); + log_verbose(LOG_NOTICE, "%s\n", event_details.data); + } + { + success = false; + } + + } + else + { + /* create new node record */ + bool node_created = create_node_record(conn, "bdr register", &node_info); + + if (node_created == true) + { + appendPQExpBuffer(&event_details, + _("node record created for node '%s' (ID: %i)"), + config_file_options.node_name, config_file_options.node_id); + log_notice("%s", event_details.data); + } + else + { + success = false; + } + } + + if (success == false) + { + rollback_transaction(conn); + PQfinish(conn); + exit(ERR_DB_QUERY); + } + + commit_transaction(conn); + /* Log the event */ + create_event_notification( + conn, + &config_file_options, + config_file_options.node_id, + "bdr_register", + true, + event_details.data); + + termPQExpBuffer(&event_details); + + PQfinish(conn); + + log_notice(_("BDR node %i registered (conninfo: %s)"), + config_file_options.node_id, config_file_options.conninfo); + + return; +} + + +void +do_bdr_unregister(void) +{ + return; +} diff --git a/repmgr-action-bdr.h b/repmgr-action-bdr.h new file mode 100644 index 00000000..e8c91135 --- /dev/null +++ b/repmgr-action-bdr.h @@ -0,0 +1,13 @@ +/* + * repmgr-action-bdr.h + * Copyright (c) 2ndQuadrant, 2010-2017 + */ + +#ifndef _REPMGR_ACTION_BDR_H_ +#define _REPMGR_ACTION_BDR_H_ + +extern void do_bdr_register(void); +extern void do_bdr_unregister(void); + + +#endif /* _REPMGR_ACTION_BDR_H_ */ diff --git a/repmgr-action-standby.h b/repmgr-action-standby.h index b99a6354..83551f90 100644 --- a/repmgr-action-standby.h +++ b/repmgr-action-standby.h @@ -33,4 +33,4 @@ typedef struct #define T_CONFIGFILE_LIST_INITIALIZER { 0, 0, NULL } -#endif +#endif /* _REPMGR_ACTION_STANDBY_H_ */ diff --git a/repmgr-client.c b/repmgr-client.c index b3c788d2..03a660cd 100644 --- a/repmgr-client.c +++ b/repmgr-client.c @@ -28,6 +28,8 @@ #include "repmgr-client-global.h" #include "repmgr-action-primary.h" #include "repmgr-action-standby.h" +#include "repmgr-action-bdr.h" + #include "repmgr-action-cluster.h" #include /* for PG_TEMP_FILE_PREFIX */ @@ -579,6 +581,7 @@ main(int argc, char **argv) else if (strcasecmp(repmgr_action, "UNREGISTER") == 0) action = PRIMARY_UNREGISTER; } + else if (strcasecmp(repmgr_node_type, "STANDBY") == 0) { if (strcasecmp(repmgr_action, "CLONE") == 0) @@ -599,6 +602,14 @@ main(int argc, char **argv) action = STANDBY_RESTORE_CONFIG; } + else if (strcasecmp(repmgr_node_type, "BDR") == 0) + { + if (strcasecmp(repmgr_action, "REGISTER") == 0) + action = BDR_REGISTER; + else if (strcasecmp(repmgr_action, "UNREGISTER") == 0) + action = BDR_UNREGISTER; + } + else if (strcasecmp(repmgr_node_type, "CLUSTER") == 0) { if (strcasecmp(repmgr_action, "EVENT") == 0) @@ -845,6 +856,7 @@ main(int argc, char **argv) switch (action) { + /* PRIMARY */ case PRIMARY_REGISTER: do_primary_register(); break; @@ -852,6 +864,7 @@ main(int argc, char **argv) do_primary_unregister(); break; + /* STANDBY */ case STANDBY_CLONE: do_standby_clone(); break; @@ -877,6 +890,15 @@ main(int argc, char **argv) do_standby_restore_config(); break; + /* BDR */ + case BDR_REGISTER: + do_bdr_register(); + break; + case BDR_UNREGISTER: + do_bdr_unregister(); + break; + + /* CLUSTER */ case CLUSTER_EVENT: do_cluster_event(); break; @@ -1336,14 +1358,14 @@ create_repmgr_extension(PGconn *conn) /* 4. Create extension */ initPQExpBuffer(&query); - appendPQExpBuffer(&query, - "CREATE EXTENSION repmgr"); + wrap_ddl_query(&query, config_file_options.replication_type, + "CREATE EXTENSION repmgr"); res = PQexec(schema_create_conn, query.data); termPQExpBuffer(&query); - if (PQresultStatus(res) != PGRES_COMMAND_OK) + if ((PQresultStatus(res) != PGRES_COMMAND_OK && PQresultStatus(res) != PGRES_TUPLES_OK)) { log_error(_("unable to create \"repmgr\" extension:\n %s"), PQerrorMessage(schema_create_conn)); @@ -1357,14 +1379,21 @@ create_repmgr_extension(PGconn *conn) PQclear(res); + /* For BDR, we'll need to add the repmgr extension tables to a replication set */ + if (config_file_options.replication_type == REPLICATION_TYPE_BDR) + { + add_extension_tables_to_bdr_replication_set(conn); + } + /* 5. If not superuser, grant usage */ if (is_superuser == false) { initPQExpBuffer(&query); - appendPQExpBuffer(&query, - "GRANT USAGE ON SCHEMA repmgr TO %s", - userinfo.username); + wrap_ddl_query(&query, config_file_options.replication_type, + "GRANT USAGE ON SCHEMA repmgr TO %s", + userinfo.username); + res = PQexec(schema_create_conn, query.data); termPQExpBuffer(&query); @@ -1382,9 +1411,10 @@ create_repmgr_extension(PGconn *conn) } initPQExpBuffer(&query); - appendPQExpBuffer(&query, - "GRANT ALL ON ALL TABLES IN SCHEMA repmgr TO %s", - userinfo.username); + wrap_ddl_query(&query, config_file_options.replication_type, + "GRANT ALL ON ALL TABLES IN SCHEMA repmgr TO %s", + userinfo.username); + res = PQexec(schema_create_conn, query.data); termPQExpBuffer(&query);