A slave now can optionally register with the master providing the

info on how to connect to the slave for the clients that connect to
the master, but would like to know where the slaves are
parent 196e995c
...@@ -42,7 +42,8 @@ enum enum_server_command {COM_SLEEP,COM_QUIT,COM_INIT_DB,COM_QUERY, ...@@ -42,7 +42,8 @@ enum enum_server_command {COM_SLEEP,COM_QUIT,COM_INIT_DB,COM_QUERY,
COM_PROCESS_INFO,COM_CONNECT,COM_PROCESS_KILL, COM_PROCESS_INFO,COM_CONNECT,COM_PROCESS_KILL,
COM_DEBUG,COM_PING,COM_TIME,COM_DELAYED_INSERT, COM_DEBUG,COM_PING,COM_TIME,COM_DELAYED_INSERT,
COM_CHANGE_USER, COM_BINLOG_DUMP, COM_CHANGE_USER, COM_BINLOG_DUMP,
COM_TABLE_DUMP, COM_CONNECT_OUT}; COM_TABLE_DUMP, COM_CONNECT_OUT,
COM_REGISTER_SLAVE};
#define NOT_NULL_FLAG 1 /* Field can't be NULL */ #define NOT_NULL_FLAG 1 /* Field can't be NULL */
#define PRI_KEY_FLAG 2 /* Field is part of a primary key */ #define PRI_KEY_FLAG 2 /* Field is part of a primary key */
......
...@@ -520,6 +520,8 @@ start_slave() ...@@ -520,6 +520,8 @@ start_slave()
--tmpdir=$MYSQL_TMP_DIR \ --tmpdir=$MYSQL_TMP_DIR \
--language=english \ --language=english \
--skip-innodb --skip-slave-start \ --skip-innodb --skip-slave-start \
--report-host=127.0.0.1 --report-user=root \
--report-port=$SLAVE_MYPORT \
$SMALL_SERVER \ $SMALL_SERVER \
$EXTRA_SLAVE_OPT $EXTRA_SLAVE_MYSQLD_OPT" $EXTRA_SLAVE_OPT $EXTRA_SLAVE_MYSQLD_OPT"
if [ x$DO_DDD = x1 ] if [ x$DO_DDD = x1 ]
......
...@@ -2,6 +2,8 @@ n ...@@ -2,6 +2,8 @@ n
2000 2000
2001 2001
2002 2002
Server_id Host User Password Port
2 127.0.0.1 root 9307
id created id created
1 1970-01-01 06:25:45 1 1970-01-01 06:25:45
id created id created
......
...@@ -11,6 +11,7 @@ use test; ...@@ -11,6 +11,7 @@ use test;
sync_with_master; sync_with_master;
select * from t1; select * from t1;
connection master; connection master;
show slave hosts;
drop table t1; drop table t1;
save_master_pos; save_master_pos;
connection slave; connection slave;
......
...@@ -520,7 +520,7 @@ extern pthread_mutex_t LOCK_mysql_create_db,LOCK_Acl,LOCK_open, ...@@ -520,7 +520,7 @@ extern pthread_mutex_t LOCK_mysql_create_db,LOCK_Acl,LOCK_open,
LOCK_thread_count,LOCK_mapped_file,LOCK_user_locks, LOCK_status, LOCK_thread_count,LOCK_mapped_file,LOCK_user_locks, LOCK_status,
LOCK_grant, LOCK_error_log, LOCK_delayed_insert, LOCK_grant, LOCK_error_log, LOCK_delayed_insert,
LOCK_delayed_status, LOCK_delayed_create, LOCK_crypt, LOCK_timezone, LOCK_delayed_status, LOCK_delayed_create, LOCK_crypt, LOCK_timezone,
LOCK_binlog_update, LOCK_slave, LOCK_server_id; LOCK_binlog_update, LOCK_slave, LOCK_server_id, LOCK_slave_list;
extern pthread_cond_t COND_refresh,COND_thread_count, COND_binlog_update, extern pthread_cond_t COND_refresh,COND_thread_count, COND_binlog_update,
COND_slave_stopped, COND_slave_start; COND_slave_stopped, COND_slave_start;
extern pthread_attr_t connection_attrib; extern pthread_attr_t connection_attrib;
......
...@@ -20,6 +20,7 @@ ...@@ -20,6 +20,7 @@
#include <my_dir.h> #include <my_dir.h>
#include "sql_acl.h" #include "sql_acl.h"
#include "slave.h" #include "slave.h"
#include "sql_repl.h"
#include "stacktrace.h" #include "stacktrace.h"
#ifdef HAVE_BERKELEY_DB #ifdef HAVE_BERKELEY_DB
#include "ha_berkeley.h" #include "ha_berkeley.h"
...@@ -277,9 +278,12 @@ volatile ulong cached_thread_count=0; ...@@ -277,9 +278,12 @@ volatile ulong cached_thread_count=0;
// replication parameters, if master_host is not NULL, we are a slave // replication parameters, if master_host is not NULL, we are a slave
my_string master_user = (char*) "test", master_password = 0, master_host=0, my_string master_user = (char*) "test", master_password = 0, master_host=0,
master_info_file = (char*) "master.info"; master_info_file = (char*) "master.info";
my_string report_user = (char*) "test", report_password = 0, report_host=0;
const char *localhost=LOCAL_HOST; const char *localhost=LOCAL_HOST;
const char *delayed_user="DELAYED"; const char *delayed_user="DELAYED";
uint master_port = MYSQL_PORT, master_connect_retry = 60; uint master_port = MYSQL_PORT, master_connect_retry = 60;
uint report_port = MYSQL_PORT;
ulong max_tmp_tables,max_heap_table_size; ulong max_tmp_tables,max_heap_table_size;
ulong bytes_sent = 0L, bytes_received = 0L; ulong bytes_sent = 0L, bytes_received = 0L;
...@@ -341,7 +345,7 @@ pthread_mutex_t LOCK_mysql_create_db, LOCK_Acl, LOCK_open, LOCK_thread_count, ...@@ -341,7 +345,7 @@ pthread_mutex_t LOCK_mysql_create_db, LOCK_Acl, LOCK_open, LOCK_thread_count,
LOCK_delayed_insert, LOCK_delayed_status, LOCK_delayed_create, LOCK_delayed_insert, LOCK_delayed_status, LOCK_delayed_create,
LOCK_crypt, LOCK_bytes_sent, LOCK_bytes_received, LOCK_crypt, LOCK_bytes_sent, LOCK_bytes_received,
LOCK_binlog_update, LOCK_slave, LOCK_server_id, LOCK_binlog_update, LOCK_slave, LOCK_server_id,
LOCK_user_conn; LOCK_user_conn, LOCK_slave_list;
pthread_cond_t COND_refresh,COND_thread_count,COND_binlog_update, pthread_cond_t COND_refresh,COND_thread_count,COND_binlog_update,
COND_slave_stopped, COND_slave_start; COND_slave_stopped, COND_slave_start;
...@@ -695,6 +699,7 @@ void clean_up(bool print_message) ...@@ -695,6 +699,7 @@ void clean_up(bool print_message)
bitmap_free(&temp_pool); bitmap_free(&temp_pool);
free_max_user_conn(); free_max_user_conn();
end_slave(); end_slave();
end_slave_list();
#ifndef __WIN__ #ifndef __WIN__
if (!opt_bootstrap) if (!opt_bootstrap)
(void) my_delete(pidfile_name,MYF(0)); // This may not always exist (void) my_delete(pidfile_name,MYF(0)); // This may not always exist
...@@ -1685,6 +1690,7 @@ int main(int argc, char **argv) ...@@ -1685,6 +1690,7 @@ int main(int argc, char **argv)
randominit(&sql_rand,(ulong) start_time,(ulong) start_time/2); randominit(&sql_rand,(ulong) start_time,(ulong) start_time/2);
reset_floating_point_exceptions(); reset_floating_point_exceptions();
init_thr_lock(); init_thr_lock();
init_slave_list();
/* Fix varibles that are base 1024*1024 */ /* Fix varibles that are base 1024*1024 */
myisam_max_temp_length= (my_off_t) min(((ulonglong) myisam_max_sort_file_size)*1024*1024, (ulonglong) MAX_FILE_SIZE); myisam_max_temp_length= (my_off_t) min(((ulonglong) myisam_max_sort_file_size)*1024*1024, (ulonglong) MAX_FILE_SIZE);
...@@ -2479,7 +2485,8 @@ enum options { ...@@ -2479,7 +2485,8 @@ enum options {
OPT_TEMP_POOL, OPT_DO_PSTACK, OPT_TX_ISOLATION, OPT_TEMP_POOL, OPT_DO_PSTACK, OPT_TX_ISOLATION,
OPT_GEMINI_FLUSH_LOG, OPT_GEMINI_RECOVER, OPT_GEMINI_FLUSH_LOG, OPT_GEMINI_RECOVER,
OPT_GEMINI_UNBUFFERED_IO, OPT_SKIP_SAFEMALLOC, OPT_GEMINI_UNBUFFERED_IO, OPT_SKIP_SAFEMALLOC,
OPT_SKIP_STACK_TRACE OPT_SKIP_STACK_TRACE, OPT_REPORT_HOST,
OPT_REPORT_USER, OPT_REPORT_PASSWORD, OPT_REPORT_PORT
}; };
static struct option long_options[] = { static struct option long_options[] = {
...@@ -2587,6 +2594,12 @@ static struct option long_options[] = { ...@@ -2587,6 +2594,12 @@ static struct option long_options[] = {
(int) OPT_REPLICATE_WILD_IGNORE_TABLE}, (int) OPT_REPLICATE_WILD_IGNORE_TABLE},
{"replicate-rewrite-db", required_argument, 0, {"replicate-rewrite-db", required_argument, 0,
(int) OPT_REPLICATE_REWRITE_DB}, (int) OPT_REPLICATE_REWRITE_DB},
// In replication, we may need to tell the other servers how to connect
// to us
{"report-host", required_argument, 0, (int) OPT_REPORT_HOST},
{"report-user", required_argument, 0, (int) OPT_REPORT_USER},
{"report-password", required_argument, 0, (int) OPT_REPORT_PASSWORD},
{"report-port", required_argument, 0, (int) OPT_REPORT_PORT},
{"safe-mode", no_argument, 0, (int) OPT_SAFE}, {"safe-mode", no_argument, 0, (int) OPT_SAFE},
{"safe-show-database", no_argument, 0, (int) OPT_SAFE_SHOW_DB}, {"safe-show-database", no_argument, 0, (int) OPT_SAFE_SHOW_DB},
{"socket", required_argument, 0, (int) OPT_SOCKET}, {"socket", required_argument, 0, (int) OPT_SOCKET},
...@@ -3712,6 +3725,18 @@ static void get_options(int argc,char **argv) ...@@ -3712,6 +3725,18 @@ static void get_options(int argc,char **argv)
case OPT_MASTER_PORT: case OPT_MASTER_PORT:
master_port= atoi(optarg); master_port= atoi(optarg);
break; break;
case OPT_REPORT_HOST:
report_host=optarg;
break;
case OPT_REPORT_USER:
report_user=optarg;
break;
case OPT_REPORT_PASSWORD:
report_password=optarg;
break;
case OPT_REPORT_PORT:
report_port= atoi(optarg);
break;
case OPT_MASTER_CONNECT_RETRY: case OPT_MASTER_CONNECT_RETRY:
master_connect_retry= atoi(optarg); master_connect_retry= atoi(optarg);
break; break;
......
...@@ -569,6 +569,52 @@ error: ...@@ -569,6 +569,52 @@ error:
return 1; return 1;
} }
int register_slave_on_master(MYSQL* mysql)
{
String packet;
uint len;
char buf[4];
if(!report_host)
return 0;
int4store(buf, server_id);
packet.append(buf, 4);
len = strlen(report_host);
packet.append((char)(uchar)len);
packet.append(report_host, len);
len = strlen(report_user);
packet.append((char)(uchar)len);
packet.append(report_user, len);
if(report_password)
{
len = strlen(report_password);
packet.append((char)(uchar)len);
packet.append(report_password, len);
}
else
{
packet.append((char)0);
}
int2store(buf, (uint16)report_port);
packet.append(buf, 2);
if(mc_simple_command(mysql, COM_REGISTER_SLAVE, (char*)packet.ptr(),
packet.length(), 0))
{
sql_print_error("Error on COM_REGISTER_SLAVE: '%s'",
mc_mysql_error(mysql));
return 1;
}
return 0;
}
int show_master_info(THD* thd) int show_master_info(THD* thd)
{ {
DBUG_ENTER("show_master_info"); DBUG_ENTER("show_master_info");
...@@ -1246,6 +1292,12 @@ pthread_handler_decl(handle_slave,arg __attribute__((unused))) ...@@ -1246,6 +1292,12 @@ pthread_handler_decl(handle_slave,arg __attribute__((unused)))
goto err; goto err;
} }
// register ourselves with the master
// if fails, this is not fatal - we just print the error message and go
// on with life
thd->proc_info = "Registering slave on master";
register_slave_on_master(mysql);
while (!slave_killed(thd)) while (!slave_killed(thd))
{ {
thd->proc_info = "Requesting binlog dump"; thd->proc_info = "Requesting binlog dump";
......
...@@ -66,6 +66,7 @@ typedef struct st_table_rule_ent ...@@ -66,6 +66,7 @@ typedef struct st_table_rule_ent
#define TABLE_RULE_ARR_SIZE 16 #define TABLE_RULE_ARR_SIZE 16
int flush_master_info(MASTER_INFO* mi); int flush_master_info(MASTER_INFO* mi);
int register_slave_on_master(MYSQL* mysql);
int mysql_table_dump(THD* thd, const char* db, int mysql_table_dump(THD* thd, const char* db,
const char* tbl_name, int fd = -1); const char* tbl_name, int fd = -1);
...@@ -117,9 +118,9 @@ extern int disconnect_slave_event_count, abort_slave_event_count ; ...@@ -117,9 +118,9 @@ extern int disconnect_slave_event_count, abort_slave_event_count ;
#endif #endif
// the master variables are defaults read from my.cnf or command line // the master variables are defaults read from my.cnf or command line
extern uint master_port, master_connect_retry; extern uint master_port, master_connect_retry, report_port;
extern my_string master_user, master_password, master_host, extern my_string master_user, master_password, master_host,
master_info_file; master_info_file, report_user, report_host, report_password;
extern I_List<i_string> replicate_do_db, replicate_ignore_db; extern I_List<i_string> replicate_do_db, replicate_ignore_db;
extern I_List<i_string_pair> replicate_rewrite_db; extern I_List<i_string_pair> replicate_rewrite_db;
......
...@@ -54,7 +54,8 @@ enum enum_sql_command { ...@@ -54,7 +54,8 @@ enum enum_sql_command {
SQLCOM_RENAME_TABLE, SQLCOM_BACKUP_TABLE, SQLCOM_RESTORE_TABLE, SQLCOM_RENAME_TABLE, SQLCOM_BACKUP_TABLE, SQLCOM_RESTORE_TABLE,
SQLCOM_RESET, SQLCOM_PURGE, SQLCOM_SHOW_BINLOGS, SQLCOM_RESET, SQLCOM_PURGE, SQLCOM_SHOW_BINLOGS,
SQLCOM_SHOW_OPEN_TABLES, SQLCOM_LOAD_MASTER_DATA, SQLCOM_SHOW_OPEN_TABLES, SQLCOM_LOAD_MASTER_DATA,
SQLCOM_HA_OPEN, SQLCOM_HA_CLOSE, SQLCOM_HA_READ SQLCOM_HA_OPEN, SQLCOM_HA_CLOSE, SQLCOM_HA_READ,
SQLCOM_SHOW_SLAVE_HOSTS
}; };
enum lex_states { STATE_START, STATE_CHAR, STATE_IDENT, enum lex_states { STATE_START, STATE_CHAR, STATE_IDENT,
......
...@@ -53,7 +53,7 @@ const char *command_name[]={ ...@@ -53,7 +53,7 @@ const char *command_name[]={
"Sleep", "Quit", "Init DB", "Query", "Field List", "Create DB", "Sleep", "Quit", "Init DB", "Query", "Field List", "Create DB",
"Drop DB", "Refresh", "Shutdown", "Statistics", "Processlist", "Drop DB", "Refresh", "Shutdown", "Statistics", "Processlist",
"Connect","Kill","Debug","Ping","Time","Delayed_insert","Change user", "Connect","Kill","Debug","Ping","Time","Delayed_insert","Change user",
"Binlog Dump","Table Dump", "Connect Out" "Binlog Dump","Table Dump", "Connect Out", "Register Slave"
}; };
bool volatile abort_slave = 0; bool volatile abort_slave = 0;
...@@ -766,6 +766,14 @@ bool dispatch_command(enum enum_server_command command, THD *thd, ...@@ -766,6 +766,14 @@ bool dispatch_command(enum enum_server_command command, THD *thd,
if (!mysql_change_db(thd,packet)) if (!mysql_change_db(thd,packet))
mysql_log.write(thd,command,"%s",thd->db); mysql_log.write(thd,command,"%s",thd->db);
break; break;
case COM_REGISTER_SLAVE:
{
if(register_slave(thd, (uchar*)packet, packet_length))
send_error(&thd->net);
else
send_ok(&thd->net);
break;
}
case COM_TABLE_DUMP: case COM_TABLE_DUMP:
{ {
slow_command = TRUE; slow_command = TRUE;
...@@ -1163,6 +1171,13 @@ mysql_execute_command(void) ...@@ -1163,6 +1171,13 @@ mysql_execute_command(void)
res = purge_master_logs(thd, lex->to_log); res = purge_master_logs(thd, lex->to_log);
break; break;
} }
case SQLCOM_SHOW_SLAVE_HOSTS:
{
if(check_access(thd, FILE_ACL, any_db))
goto error;
res = show_slave_hosts(thd);
break;
}
case SQLCOM_BACKUP_TABLE: case SQLCOM_BACKUP_TABLE:
{ {
if (check_db_used(thd,tables) || if (check_db_used(thd,tables) ||
......
...@@ -25,9 +25,39 @@ ...@@ -25,9 +25,39 @@
#include <thr_alarm.h> #include <thr_alarm.h>
#include <my_dir.h> #include <my_dir.h>
#define SLAVE_LIST_CHUNK 128
extern const char* any_db; extern const char* any_db;
extern pthread_handler_decl(handle_slave,arg); extern pthread_handler_decl(handle_slave,arg);
HASH slave_list;
static uint32* slave_list_key(SLAVE_INFO* si, uint* len,
my_bool not_used __attribute__((unused)))
{
*len = 4;
return &si->server_id;
}
static void slave_info_free(void *s)
{
my_free((byte*)s, MYF(MY_WME));
}
void init_slave_list()
{
hash_init(&slave_list, SLAVE_LIST_CHUNK, 0, 0,
(hash_get_key) slave_list_key, slave_info_free, 0);
pthread_mutex_init(&LOCK_slave_list, MY_MUTEX_INIT_FAST);
}
void end_slave_list()
{
pthread_mutex_lock(&LOCK_slave_list);
hash_free(&slave_list);
pthread_mutex_unlock(&LOCK_slave_list);
pthread_mutex_destroy(&LOCK_slave_list);
}
static int fake_rotate_event(NET* net, String* packet, char* log_file_name, static int fake_rotate_event(NET* net, String* packet, char* log_file_name,
const char**errmsg) const char**errmsg)
...@@ -56,6 +86,55 @@ static int fake_rotate_event(NET* net, String* packet, char* log_file_name, ...@@ -56,6 +86,55 @@ static int fake_rotate_event(NET* net, String* packet, char* log_file_name,
return 0; return 0;
} }
int register_slave(THD* thd, uchar* packet, uint packet_length)
{
uint len;
SLAVE_INFO* si, *old_si;
int res = 1;
uchar* p = packet, *p_end = packet + packet_length;
if(check_access(thd, FILE_ACL, any_db))
return 1;
if(!(si = (SLAVE_INFO*)my_malloc(sizeof(SLAVE_INFO), MYF(MY_WME))))
goto err;
si->server_id = uint4korr(p);
p += 4;
len = (uint)*p++;
if(p + len > p_end || len > sizeof(si->host) - 1)
goto err;
memcpy(si->host, p, len);
si->host[len] = 0;
p += len;
len = *p++;
if(p + len > p_end || len > sizeof(si->user) - 1)
goto err;
memcpy(si->user, p, len);
si->user[len] = 0;
p += len;
len = *p++;
if(p + len > p_end || len > sizeof(si->password) - 1)
goto err;
memcpy(si->password, p, len);
si->password[len] = 0;
p += len;
si->port = uint2korr(p);
pthread_mutex_lock(&LOCK_slave_list);
if((old_si = (SLAVE_INFO*)hash_search(&slave_list,
(byte*)&si->server_id, 4)))
hash_delete(&slave_list, (byte*)old_si);
res = hash_insert(&slave_list, (byte*)si);
pthread_mutex_unlock(&LOCK_slave_list);
return res;
err:
if(si)
my_free((byte*)si, MYF(MY_WME));
return res;
}
static int send_file(THD *thd) static int send_file(THD *thd)
{ {
...@@ -742,6 +821,44 @@ void reset_master() ...@@ -742,6 +821,44 @@ void reset_master()
} }
int show_slave_hosts(THD* thd)
{
DBUG_ENTER("show_slave_hosts");
List<Item> field_list;
field_list.push_back(new Item_empty_string("Server_id", 20));
field_list.push_back(new Item_empty_string("Host", 20));
field_list.push_back(new Item_empty_string("User",20));
field_list.push_back(new Item_empty_string("Password",20));
field_list.push_back(new Item_empty_string("Port",20));
if(send_fields(thd, field_list, 1))
DBUG_RETURN(-1);
String* packet = &thd->packet;
uint i;
NET* net = &thd->net;
pthread_mutex_lock(&LOCK_slave_list);
for(i = 0; i < slave_list.records; ++i)
{
SLAVE_INFO* si = (SLAVE_INFO*)hash_element(&slave_list, i);
packet->length(0);
net_store_data(packet, si->server_id);
net_store_data(packet, si->host);
net_store_data(packet, si->user);
net_store_data(packet, si->password);
net_store_data(packet, (uint)si->port);
if(my_net_write(net, (char*)packet->ptr(), packet->length()))
{
pthread_mutex_unlock(&LOCK_slave_list);
DBUG_RETURN(-1);
}
}
pthread_mutex_unlock(&LOCK_slave_list);
send_eof(net);
DBUG_RETURN(0);
}
int show_binlog_info(THD* thd) int show_binlog_info(THD* thd)
{ {
DBUG_ENTER("show_binlog_info"); DBUG_ENTER("show_binlog_info");
......
...@@ -3,6 +3,16 @@ ...@@ -3,6 +3,16 @@
#include "slave.h" #include "slave.h"
typedef struct st_slave_info
{
uint32 server_id;
char host[HOSTNAME_LENGTH+1];
char user[USERNAME_LENGTH+1];
char password[HASH_PASSWORD_LENGTH+1];
uint16 port;
} SLAVE_INFO;
extern HASH slave_list;
extern char* master_host; extern char* master_host;
extern my_string opt_bin_logname, master_info_file; extern my_string opt_bin_logname, master_info_file;
extern uint32 server_id; extern uint32 server_id;
...@@ -17,8 +27,12 @@ int stop_slave(THD* thd = 0, bool net_report = 1); ...@@ -17,8 +27,12 @@ int stop_slave(THD* thd = 0, bool net_report = 1);
int load_master_data(THD* thd); int load_master_data(THD* thd);
int connect_to_master(THD *thd, MYSQL* mysql, MASTER_INFO* mi); int connect_to_master(THD *thd, MYSQL* mysql, MASTER_INFO* mi);
int change_master(THD* thd); int change_master(THD* thd);
int show_slave_hosts(THD* thd);
void reset_slave(); void reset_slave();
void reset_master(); void reset_master();
void init_slave_list();
void end_slave_list();
int register_slave(THD* thd, uchar* packet, uint packet_length);
int purge_master_logs(THD* thd, const char* to_log); int purge_master_logs(THD* thd, const char* to_log);
bool log_in_use(const char* log_name); bool log_in_use(const char* log_name);
void adjust_linfo_offsets(my_off_t purge_offset); void adjust_linfo_offsets(my_off_t purge_offset);
......
...@@ -2247,6 +2247,10 @@ show_param: ...@@ -2247,6 +2247,10 @@ show_param:
{ {
Lex->sql_command = SQLCOM_SHOW_BINLOGS; Lex->sql_command = SQLCOM_SHOW_BINLOGS;
} }
| SLAVE HOSTS_SYM
{
Lex->sql_command = SQLCOM_SHOW_SLAVE_HOSTS;
}
| keys_or_index FROM table_ident opt_db | keys_or_index FROM table_ident opt_db
{ {
Lex->sql_command= SQLCOM_SHOW_KEYS; Lex->sql_command= SQLCOM_SHOW_KEYS;
......
Markdown is supported
0%
or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment