mirror of
https://github.com/EnterpriseDB/repmgr.git
synced 2026-03-27 00:46:29 +00:00
Further "standby clone" code
This commit is contained in:
@@ -27,7 +27,7 @@ include Makefile.global
|
|||||||
$(info Building against PostgreSQL $(MAJORVERSION))
|
$(info Building against PostgreSQL $(MAJORVERSION))
|
||||||
|
|
||||||
REPMGR_CLIENT_OBJS = repmgr-client.o repmgr-action-master.o repmgr-action-standby.o repmgr-action-cluster.o \
|
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 dirutil.o
|
config.o log.o strutil.o dbutils.o dirutil.o compat.o
|
||||||
REPMGRD_OBJS = repmgrd.o
|
REPMGRD_OBJS = repmgrd.o
|
||||||
|
|
||||||
$(REPMGR_CLIENT_OBJS): repmgr-client.h
|
$(REPMGR_CLIENT_OBJS): repmgr-client.h
|
||||||
@@ -56,6 +56,7 @@ additional-clean:
|
|||||||
rm -f repmgr-action-master.o
|
rm -f repmgr-action-master.o
|
||||||
rm -f repmgr-action-standby.o
|
rm -f repmgr-action-standby.o
|
||||||
rm -f repmgrd.o
|
rm -f repmgrd.o
|
||||||
|
rm -f compat.o
|
||||||
rm -f config.o
|
rm -f config.o
|
||||||
rm -f dbutils.o
|
rm -f dbutils.o
|
||||||
rm -f dirutil.o
|
rm -f dirutil.o
|
||||||
|
|||||||
107
compat.c
Normal file
107
compat.c
Normal file
@@ -0,0 +1,107 @@
|
|||||||
|
/*
|
||||||
|
*
|
||||||
|
* compat.c
|
||||||
|
* Provides a couple of useful string utility functions adapted
|
||||||
|
* from the backend code, which are not publicly exposed in all
|
||||||
|
* supported PostgreSQL versions. They're unlikely to change but
|
||||||
|
* it would be worth keeping an eye on them for any fixes/improvements.
|
||||||
|
*
|
||||||
|
* Copyright (c) 2ndQuadrant, 2010-2017
|
||||||
|
*
|
||||||
|
* Portions Copyright (c) 1996-2013, PostgreSQL Global Development Group
|
||||||
|
* Portions Copyright (c) 1994, Regents of the University of California
|
||||||
|
*
|
||||||
|
* This program is free software: you can redistribute it and/or modify
|
||||||
|
* it under the terms of the GNU General Public License as published by
|
||||||
|
* the Free Software Foundation, either version 3 of the License, or
|
||||||
|
* (at your option) any later version.
|
||||||
|
*
|
||||||
|
* This program is distributed in the hope that it will be useful,
|
||||||
|
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||||
|
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||||
|
* GNU General Public License for more details.
|
||||||
|
*
|
||||||
|
* You should have received a copy of the GNU General Public License
|
||||||
|
* along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||||
|
*
|
||||||
|
*/
|
||||||
|
|
||||||
|
#include "repmgr.h"
|
||||||
|
#include "compat.h"
|
||||||
|
|
||||||
|
/*
|
||||||
|
* Append the given string to the buffer, with suitable quoting for passing
|
||||||
|
* the string as a value, in a keyword/pair value in a libpq connection
|
||||||
|
* string
|
||||||
|
*
|
||||||
|
* This function is adapted from src/fe_utils/string_utils.c (before 9.6
|
||||||
|
* located in: src/bin/pg_dump/dumputils.c)
|
||||||
|
*/
|
||||||
|
void
|
||||||
|
appendConnStrVal(PQExpBuffer buf, const char *str)
|
||||||
|
{
|
||||||
|
const char *s;
|
||||||
|
bool needquotes;
|
||||||
|
|
||||||
|
/*
|
||||||
|
* If the string is one or more plain ASCII characters, no need to quote
|
||||||
|
* it. This is quite conservative, but better safe than sorry.
|
||||||
|
*/
|
||||||
|
needquotes = true;
|
||||||
|
for (s = str; *s; s++)
|
||||||
|
{
|
||||||
|
if (!((*s >= 'a' && *s <= 'z') || (*s >= 'A' && *s <= 'Z') ||
|
||||||
|
(*s >= '0' && *s <= '9') || *s == '_' || *s == '.'))
|
||||||
|
{
|
||||||
|
needquotes = true;
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
needquotes = false;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (needquotes)
|
||||||
|
{
|
||||||
|
appendPQExpBufferChar(buf, '\'');
|
||||||
|
while (*str)
|
||||||
|
{
|
||||||
|
/* ' and \ must be escaped by to \' and \\ */
|
||||||
|
if (*str == '\'' || *str == '\\')
|
||||||
|
appendPQExpBufferChar(buf, '\\');
|
||||||
|
|
||||||
|
appendPQExpBufferChar(buf, *str);
|
||||||
|
str++;
|
||||||
|
}
|
||||||
|
appendPQExpBufferChar(buf, '\'');
|
||||||
|
}
|
||||||
|
else
|
||||||
|
appendPQExpBufferStr(buf, str);
|
||||||
|
}
|
||||||
|
|
||||||
|
/*
|
||||||
|
* Adapted from: src/fe_utils/string_utils.c
|
||||||
|
*/
|
||||||
|
void
|
||||||
|
appendShellString(PQExpBuffer buf, const char *str)
|
||||||
|
{
|
||||||
|
const char *p;
|
||||||
|
|
||||||
|
appendPQExpBufferChar(buf, '\'');
|
||||||
|
for (p = str; *p; p++)
|
||||||
|
{
|
||||||
|
if (*p == '\n' || *p == '\r')
|
||||||
|
{
|
||||||
|
fprintf(stderr,
|
||||||
|
_("shell command argument contains a newline or carriage return: \"%s\"\n"),
|
||||||
|
str);
|
||||||
|
exit(ERR_BAD_CONFIG);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (*p == '\'')
|
||||||
|
appendPQExpBufferStr(buf, "'\"'\"'");
|
||||||
|
else
|
||||||
|
appendPQExpBufferChar(buf, *p);
|
||||||
|
}
|
||||||
|
|
||||||
|
appendPQExpBufferChar(buf, '\'');
|
||||||
|
}
|
||||||
|
|
||||||
32
compat.h
Normal file
32
compat.h
Normal file
@@ -0,0 +1,32 @@
|
|||||||
|
/*
|
||||||
|
* compat.h
|
||||||
|
* Copyright (c) 2ndQuadrant, 2010-2017
|
||||||
|
*
|
||||||
|
* Portions Copyright (c) 1996-2013, PostgreSQL Global Development Group
|
||||||
|
* Portions Copyright (c) 1994, Regents of the University of California
|
||||||
|
*
|
||||||
|
* This program is free software: you can redistribute it and/or modify
|
||||||
|
* it under the terms of the GNU General Public License as published by
|
||||||
|
* the Free Software Foundation, either version 3 of the License, or
|
||||||
|
* (at your option) any later version.
|
||||||
|
*
|
||||||
|
* This program is distributed in the hope that it will be useful,
|
||||||
|
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||||
|
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||||
|
* GNU General Public License for more details.
|
||||||
|
*
|
||||||
|
* You should have received a copy of the GNU General Public License
|
||||||
|
* along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||||
|
*
|
||||||
|
*/
|
||||||
|
|
||||||
|
#ifndef _COMPAT_H_
|
||||||
|
#define _COMPAT_H_
|
||||||
|
|
||||||
|
extern void
|
||||||
|
appendConnStrVal(PQExpBuffer buf, const char *str);
|
||||||
|
|
||||||
|
extern void
|
||||||
|
appendShellString(PQExpBuffer buf, const char *str);
|
||||||
|
|
||||||
|
#endif
|
||||||
@@ -8,6 +8,7 @@
|
|||||||
|
|
||||||
#include "repmgr.h"
|
#include "repmgr.h"
|
||||||
#include "dirutil.h"
|
#include "dirutil.h"
|
||||||
|
#include "compat.h"
|
||||||
|
|
||||||
#include "repmgr-client-global.h"
|
#include "repmgr-client-global.h"
|
||||||
#include "repmgr-action-standby.h"
|
#include "repmgr-action-standby.h"
|
||||||
@@ -33,8 +34,11 @@ static char local_repmgr_tmp_directory[MAXPGPATH];
|
|||||||
|
|
||||||
|
|
||||||
static void check_barman_config(void);
|
static void check_barman_config(void);
|
||||||
static char *make_barman_ssh_command(char *buf);
|
|
||||||
static void check_source_server(void);
|
static void check_source_server(void);
|
||||||
|
static void check_source_server_via_barman(void);
|
||||||
|
|
||||||
|
static void get_barman_property(char *dst, char *name, char *local_repmgr_directory);
|
||||||
|
static char *make_barman_ssh_command(char *buf);
|
||||||
|
|
||||||
|
|
||||||
void
|
void
|
||||||
@@ -152,7 +156,7 @@ do_standby_clone(void)
|
|||||||
*/
|
*/
|
||||||
|
|
||||||
if (*runtime_options.upstream_conninfo)
|
if (*runtime_options.upstream_conninfo)
|
||||||
runtime_options.no_upstream_connection = false;
|
runtime_options.no_upstream_connection = true;
|
||||||
|
|
||||||
/* By default attempt to connect to the source server */
|
/* By default attempt to connect to the source server */
|
||||||
if (runtime_options.no_upstream_connection == false)
|
if (runtime_options.no_upstream_connection == false)
|
||||||
@@ -160,6 +164,19 @@ do_standby_clone(void)
|
|||||||
check_source_server();
|
check_source_server();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (mode == barman && PQstatus(source_conn) != CONNECTION_OK)
|
||||||
|
{
|
||||||
|
/*
|
||||||
|
* Here we don't have a connection to the upstream node, and are executing
|
||||||
|
* in Barman mode - we can try and connect via the Barman server to extract
|
||||||
|
* the upstream node's conninfo string.
|
||||||
|
*
|
||||||
|
* To do this we need to extract Barman's conninfo string, replace the database
|
||||||
|
* name with the repmgr one (they could well be different) and remotely execute
|
||||||
|
* psql.
|
||||||
|
*/
|
||||||
|
check_source_server_via_barman();
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
@@ -242,24 +259,6 @@ check_barman_config(void)
|
|||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
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;
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
static void
|
static void
|
||||||
check_source_server()
|
check_source_server()
|
||||||
@@ -267,7 +266,7 @@ check_source_server()
|
|||||||
int server_version_num = UNKNOWN_SERVER_VERSION_NUM;
|
int server_version_num = UNKNOWN_SERVER_VERSION_NUM;
|
||||||
char cluster_size[MAXLEN];
|
char cluster_size[MAXLEN];
|
||||||
t_node_info node_record = T_NODE_INFO_INITIALIZER;
|
t_node_info node_record = T_NODE_INFO_INITIALIZER;
|
||||||
int query_result;
|
int query_result;
|
||||||
t_extension_status extension_status;
|
t_extension_status extension_status;
|
||||||
|
|
||||||
/* Attempt to connect to the upstream server to verify its configuration */
|
/* Attempt to connect to the upstream server to verify its configuration */
|
||||||
@@ -283,15 +282,12 @@ check_source_server()
|
|||||||
*/
|
*/
|
||||||
if (PQstatus(source_conn) != CONNECTION_OK)
|
if (PQstatus(source_conn) != CONNECTION_OK)
|
||||||
{
|
{
|
||||||
|
PQfinish(source_conn);
|
||||||
|
|
||||||
if (mode == barman)
|
if (mode == barman)
|
||||||
{
|
|
||||||
return;
|
return;
|
||||||
}
|
|
||||||
else
|
else
|
||||||
{
|
|
||||||
PQfinish(source_conn);
|
|
||||||
exit(ERR_DB_CON);
|
exit(ERR_DB_CON);
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/*
|
/*
|
||||||
@@ -301,7 +297,7 @@ check_source_server()
|
|||||||
|
|
||||||
|
|
||||||
/* Verify that upstream node is a supported server version */
|
/* Verify that upstream node is a supported server version */
|
||||||
log_verbose(LOG_INFO, _("connected to upstream node, checking its state"));
|
log_verbose(LOG_INFO, _("connected to source node, checking its state"));
|
||||||
|
|
||||||
server_version_num = check_server_version(source_conn, "master", true, NULL);
|
server_version_num = check_server_version(source_conn, "master", true, NULL);
|
||||||
|
|
||||||
@@ -357,19 +353,19 @@ check_source_server()
|
|||||||
if (!runtime_options.force)
|
if (!runtime_options.force)
|
||||||
{
|
{
|
||||||
/* schema doesn't exist */
|
/* schema doesn't exist */
|
||||||
log_error(_("repmgr extension not found on upstream server"));
|
log_error(_("repmgr extension not found on source node"));
|
||||||
log_hint(_("check that the upstream server is part of a repmgr cluster"));
|
log_hint(_("check that the upstream server is part of a repmgr cluster"));
|
||||||
PQfinish(source_conn);
|
PQfinish(source_conn);
|
||||||
exit(ERR_BAD_CONFIG);
|
exit(ERR_BAD_CONFIG);
|
||||||
}
|
}
|
||||||
|
|
||||||
log_warning(_("repmgr extension not found on upstream server"));
|
log_warning(_("repmgr extension not found on source node"));
|
||||||
}
|
}
|
||||||
|
|
||||||
/* Fetch the source's data directory */
|
/* Fetch the source's data directory */
|
||||||
if (get_pg_setting(source_conn, "data_directory", upstream_data_directory) == false)
|
if (get_pg_setting(source_conn, "data_directory", upstream_data_directory) == false)
|
||||||
{
|
{
|
||||||
log_error(_("unable to retrieve upstream node's data directory"));
|
log_error(_("unable to retrieve source node's data directory"));
|
||||||
log_hint(_("STANDBY CLONE must be run as a database superuser"));
|
log_hint(_("STANDBY CLONE must be run as a database superuser"));
|
||||||
PQfinish(source_conn);
|
PQfinish(source_conn);
|
||||||
exit(ERR_BAD_CONFIG);
|
exit(ERR_BAD_CONFIG);
|
||||||
@@ -430,3 +426,147 @@ check_source_server()
|
|||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
static void
|
||||||
|
check_source_server_via_barman()
|
||||||
|
{
|
||||||
|
char buf[MAXLEN];
|
||||||
|
char barman_conninfo_str[MAXLEN];
|
||||||
|
t_conninfo_param_list barman_conninfo;
|
||||||
|
char *errmsg = NULL;
|
||||||
|
bool parse_success,
|
||||||
|
command_success;
|
||||||
|
char where_condition[MAXLEN];
|
||||||
|
PQExpBufferData command_output;
|
||||||
|
PQExpBufferData repmgr_conninfo_buf;
|
||||||
|
|
||||||
|
int c;
|
||||||
|
|
||||||
|
get_barman_property(barman_conninfo_str, "conninfo", local_repmgr_tmp_directory);
|
||||||
|
|
||||||
|
initialize_conninfo_params(&barman_conninfo, false);
|
||||||
|
|
||||||
|
/* parse_conninfo_string() here will remove the upstream's `application_name`, if set */
|
||||||
|
parse_success = parse_conninfo_string(barman_conninfo_str, &barman_conninfo, errmsg, true);
|
||||||
|
|
||||||
|
if (parse_success == false)
|
||||||
|
{
|
||||||
|
log_error(_("Unable to parse barman conninfo string \"%s\":\n%s"),
|
||||||
|
barman_conninfo_str, errmsg);
|
||||||
|
exit(ERR_BARMAN);
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Overwrite database name in the parsed parameter list */
|
||||||
|
param_set(&barman_conninfo, "dbname", runtime_options.dbname);
|
||||||
|
|
||||||
|
/* Rebuild the Barman conninfo string */
|
||||||
|
initPQExpBuffer(&repmgr_conninfo_buf);
|
||||||
|
|
||||||
|
for (c = 0; c < barman_conninfo.size && barman_conninfo.keywords[c] != NULL; c++)
|
||||||
|
{
|
||||||
|
if (repmgr_conninfo_buf.len != 0)
|
||||||
|
appendPQExpBufferChar(&repmgr_conninfo_buf, ' ');
|
||||||
|
|
||||||
|
appendPQExpBuffer(&repmgr_conninfo_buf, "%s=",
|
||||||
|
barman_conninfo.keywords[c]);
|
||||||
|
appendConnStrVal(&repmgr_conninfo_buf,
|
||||||
|
barman_conninfo.values[c]);
|
||||||
|
}
|
||||||
|
|
||||||
|
log_verbose(LOG_DEBUG,
|
||||||
|
"repmgr database conninfo string on barman server: %s",
|
||||||
|
repmgr_conninfo_buf.data);
|
||||||
|
|
||||||
|
switch(config_file_options.upstream_node_id)
|
||||||
|
{
|
||||||
|
case NO_UPSTREAM_NODE:
|
||||||
|
// XXX did we get the upstream node ID earlier?
|
||||||
|
maxlen_snprintf(where_condition, "type='master'");
|
||||||
|
break;
|
||||||
|
default:
|
||||||
|
maxlen_snprintf(where_condition, "node_id=%d", config_file_options.upstream_node_id);
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
|
||||||
|
initPQExpBuffer(&command_output);
|
||||||
|
maxlen_snprintf(buf,
|
||||||
|
"ssh %s \"psql -Aqt \\\"%s\\\" -c \\\""
|
||||||
|
" SELECT conninfo"
|
||||||
|
" FROM repmgr.nodes"
|
||||||
|
" WHERE %s"
|
||||||
|
" AND active IS TRUE"
|
||||||
|
"\\\"\"",
|
||||||
|
config_file_options.barman_host,
|
||||||
|
repmgr_conninfo_buf.data,
|
||||||
|
where_condition);
|
||||||
|
|
||||||
|
termPQExpBuffer(&repmgr_conninfo_buf);
|
||||||
|
|
||||||
|
command_success = local_command(buf, &command_output);
|
||||||
|
|
||||||
|
if (command_success == false)
|
||||||
|
{
|
||||||
|
log_error(_("unable to execute database query via Barman server"));
|
||||||
|
exit(ERR_BARMAN);
|
||||||
|
}
|
||||||
|
|
||||||
|
maxlen_snprintf(recovery_conninfo_str, "%s", command_output.data);
|
||||||
|
string_remove_trailing_newlines(recovery_conninfo_str);
|
||||||
|
|
||||||
|
upstream_record_found = true;
|
||||||
|
log_verbose(LOG_DEBUG,
|
||||||
|
"upstream node conninfo string extracted via barman server: %s",
|
||||||
|
recovery_conninfo_str);
|
||||||
|
|
||||||
|
termPQExpBuffer(&command_output);
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
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;
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
void
|
||||||
|
get_barman_property(char *dst, char *name, char *local_repmgr_directory)
|
||||||
|
{
|
||||||
|
PQExpBufferData command_output;
|
||||||
|
char buf[MAXLEN];
|
||||||
|
char command[MAXLEN];
|
||||||
|
char *p;
|
||||||
|
|
||||||
|
initPQExpBuffer(&command_output);
|
||||||
|
|
||||||
|
maxlen_snprintf(command,
|
||||||
|
"grep \"^\t%s:\" %s/show-server.txt",
|
||||||
|
name, local_repmgr_tmp_directory);
|
||||||
|
(void)local_command(command, &command_output);
|
||||||
|
|
||||||
|
maxlen_snprintf(buf, "\t%s: ", name);
|
||||||
|
p = string_skip_prefix(buf, command_output.data);
|
||||||
|
if (p == NULL)
|
||||||
|
{
|
||||||
|
log_error("unexpected output from Barman: %s",
|
||||||
|
command_output.data);
|
||||||
|
exit(ERR_INTERNAL);
|
||||||
|
}
|
||||||
|
|
||||||
|
strncpy(dst, p, MAXLEN);
|
||||||
|
string_remove_trailing_newlines(dst);
|
||||||
|
|
||||||
|
termPQExpBuffer(&command_output);
|
||||||
|
}
|
||||||
|
|
||||||
|
|||||||
28
strutil.c
28
strutil.c
@@ -159,3 +159,31 @@ escape_string(PGconn *conn, const char *string)
|
|||||||
|
|
||||||
return escaped_string;
|
return escaped_string;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
char *
|
||||||
|
string_skip_prefix(const char *prefix, char *string)
|
||||||
|
{
|
||||||
|
int n;
|
||||||
|
|
||||||
|
n = strlen(prefix);
|
||||||
|
|
||||||
|
if (strncmp(prefix, string, n))
|
||||||
|
return NULL;
|
||||||
|
else
|
||||||
|
return string + n;
|
||||||
|
}
|
||||||
|
|
||||||
|
char *
|
||||||
|
string_remove_trailing_newlines(char *string)
|
||||||
|
{
|
||||||
|
int n;
|
||||||
|
|
||||||
|
n = strlen(string) - 1;
|
||||||
|
|
||||||
|
while (n >= 0 && string[n] == '\n')
|
||||||
|
string[n] = 0;
|
||||||
|
|
||||||
|
return string;
|
||||||
|
}
|
||||||
|
|||||||
@@ -57,5 +57,11 @@ extern void
|
|||||||
append_where_clause(PQExpBufferData *where_clause, const char *clause, ...)
|
append_where_clause(PQExpBufferData *where_clause, const char *clause, ...)
|
||||||
__attribute__((format(PG_PRINTF_ATTRIBUTE, 2, 3)));
|
__attribute__((format(PG_PRINTF_ATTRIBUTE, 2, 3)));
|
||||||
|
|
||||||
|
extern char *
|
||||||
|
string_skip_prefix(const char *prefix, char *string);
|
||||||
|
|
||||||
|
extern char
|
||||||
|
*string_remove_trailing_newlines(char *string);
|
||||||
|
|
||||||
|
|
||||||
#endif /* _STRUTIL_H_ */
|
#endif /* _STRUTIL_H_ */
|
||||||
|
|||||||
Reference in New Issue
Block a user