diff options
-rw-r--r-- | sentinel.conf | 18 | ||||
-rw-r--r-- | src/anet.c | 15 | ||||
-rw-r--r-- | src/anet.h | 3 | ||||
-rw-r--r-- | src/sentinel.c | 326 | ||||
-rw-r--r-- | tests/instances.tcl | 11 | ||||
-rw-r--r-- | tests/sentinel/tests/08-hostname-conf.tcl | 62 | ||||
-rw-r--r-- | tests/sentinel/tests/09-acl-support.tcl | 50 | ||||
-rw-r--r-- | tests/sentinel/tests/includes/init-tests.tcl | 6 |
8 files changed, 411 insertions, 80 deletions
diff --git a/sentinel.conf b/sentinel.conf index 39d6929e7..8647379d8 100644 --- a/sentinel.conf +++ b/sentinel.conf @@ -321,3 +321,21 @@ sentinel deny-scripts-reconfig yes # is possible to just rename a command to itself: # # SENTINEL rename-command mymaster CONFIG CONFIG + +# HOSTNAMES SUPPORT +# +# Normally Sentinel uses only IP addresses and requires SENTINEL MONITOR +# to specify an IP address. Also, it requires the Redis replica-announce-ip +# keyword to specify only IP addresses. +# +# You may enable hostnames support by enabling resolve-hostnames. Note +# that you must make sure your DNS is configured properly and that DNS +# resolution does not introduce very long delays. +# +SENTINEL resolve-hostnames no + +# When resolve-hostnames is enabled, Sentinel still uses IP addresses +# when exposing instances to users, configuration files, etc. If you want +# to retain the hostnames when announced, enable announce-hostnames below. +# +SENTINEL announce-hostnames no diff --git a/src/anet.c b/src/anet.c index f2c39b200..0bfa575f5 100644 --- a/src/anet.c +++ b/src/anet.c @@ -235,14 +235,13 @@ int anetRecvTimeout(char *err, int fd, long long ms) { return ANET_OK; } -/* anetGenericResolve() is called by anetResolve() and anetResolveIP() to - * do the actual work. It resolves the hostname "host" and set the string - * representation of the IP address into the buffer pointed by "ipbuf". +/* Resolve the hostname "host" and set the string representation of the + * IP address into the buffer pointed by "ipbuf". * * If flags is set to ANET_IP_ONLY the function only resolves hostnames * that are actually already IPv4 or IPv6 addresses. This turns the function * into a validating / normalizing function. */ -int anetGenericResolve(char *err, char *host, char *ipbuf, size_t ipbuf_len, +int anetResolve(char *err, char *host, char *ipbuf, size_t ipbuf_len, int flags) { struct addrinfo hints, *info; @@ -269,14 +268,6 @@ int anetGenericResolve(char *err, char *host, char *ipbuf, size_t ipbuf_len, return ANET_OK; } -int anetResolve(char *err, char *host, char *ipbuf, size_t ipbuf_len) { - return anetGenericResolve(err,host,ipbuf,ipbuf_len,ANET_NONE); -} - -int anetResolveIP(char *err, char *host, char *ipbuf, size_t ipbuf_len) { - return anetGenericResolve(err,host,ipbuf,ipbuf_len,ANET_IP_ONLY); -} - static int anetSetReuseAddr(char *err, int fd) { int yes = 1; /* Make sure connection-intensive things like the redis benchmark diff --git a/src/anet.h b/src/anet.h index dc3cbeb32..5da2f3b46 100644 --- a/src/anet.h +++ b/src/anet.h @@ -60,8 +60,7 @@ int anetTcpNonBlockBestEffortBindConnect(char *err, const char *addr, int port, int anetUnixConnect(char *err, const char *path); int anetUnixNonBlockConnect(char *err, const char *path); int anetRead(int fd, char *buf, int count); -int anetResolve(char *err, char *host, char *ipbuf, size_t ipbuf_len); -int anetResolveIP(char *err, char *host, char *ipbuf, size_t ipbuf_len); +int anetResolve(char *err, char *host, char *ipbuf, size_t ipbuf_len, int flags); int anetTcpServer(char *err, int port, char *bindaddr, int backlog); int anetTcp6Server(char *err, int port, char *bindaddr, int backlog); int anetUnixServer(char *err, char *path, mode_t perm, int backlog); diff --git a/src/sentinel.c b/src/sentinel.c index 7f79e702a..a87766ebe 100644 --- a/src/sentinel.c +++ b/src/sentinel.c @@ -55,7 +55,8 @@ extern SSL_CTX *redis_tls_client_ctx; /* Address object, used to describe an ip:port pair. */ typedef struct sentinelAddr { - char *ip; + char *hostname; /* Hostname OR address, as specified */ + char *ip; /* Always a resolved address */ int port; } sentinelAddr; @@ -94,6 +95,8 @@ typedef struct sentinelAddr { #define SENTINEL_ELECTION_TIMEOUT 10000 #define SENTINEL_MAX_DESYNC 1000 #define SENTINEL_DEFAULT_DENY_SCRIPTS_RECONFIG 1 +#define SENTINEL_DEFAULT_RESOLVE_HOSTNAMES 0 +#define SENTINEL_DEFAULT_ANNOUNCE_HOSTNAMES 0 /* Failover machine different states. */ #define SENTINEL_FAILOVER_STATE_NONE 0 /* No failover in progress. */ @@ -260,6 +263,8 @@ struct sentinelState { paths at runtime? */ char *sentinel_auth_pass; /* Password to use for AUTH against other sentinel */ char *sentinel_auth_user; /* Username for ACLs AUTH against other sentinel. */ + int resolve_hostnames; /* Support use of hostnames, assuming DNS is well configured. */ + int announce_hostnames; /* Announce hostnames instead of IPs when we have them. */ } sentinel; /* A script execution job. */ @@ -387,7 +392,7 @@ sentinelRedisInstance *sentinelSelectSlave(sentinelRedisInstance *master); void sentinelScheduleScriptExecution(char *path, ...); void sentinelStartFailover(sentinelRedisInstance *master); void sentinelDiscardReplyCallback(redisAsyncContext *c, void *reply, void *privdata); -int sentinelSendSlaveOf(sentinelRedisInstance *ri, char *host, int port); +int sentinelSendSlaveOf(sentinelRedisInstance *ri, const sentinelAddr *addr); char *sentinelVoteLeader(sentinelRedisInstance *master, uint64_t req_epoch, char *req_runid, uint64_t *leader_epoch); void sentinelFlushConfig(void); void sentinelGenerateInitialMonitorEvents(void); @@ -455,6 +460,8 @@ void sentinelInfoCommand(client *c); void sentinelSetCommand(client *c); void sentinelPublishCommand(client *c); void sentinelRoleCommand(client *c); +void sentinelConfigGetCommand(client *c); +void sentinelConfigSetCommand(client *c); struct redisCommand sentinelcmds[] = { {"ping",pingCommand,1,"fast @connection",0,NULL,0,0,0,0,0}, @@ -535,6 +542,8 @@ void initSentinel(void) { sentinel.deny_scripts_reconfig = SENTINEL_DEFAULT_DENY_SCRIPTS_RECONFIG; sentinel.sentinel_auth_pass = NULL; sentinel.sentinel_auth_user = NULL; + sentinel.resolve_hostnames = SENTINEL_DEFAULT_RESOLVE_HOSTNAMES; + sentinel.announce_hostnames = SENTINEL_DEFAULT_ANNOUNCE_HOSTNAMES; memset(sentinel.myid,0,sizeof(sentinel.myid)); server.sentinel_config = NULL; } @@ -590,11 +599,13 @@ sentinelAddr *createSentinelAddr(char *hostname, int port) { errno = EINVAL; return NULL; } - if (anetResolve(NULL,hostname,ip,sizeof(ip)) == ANET_ERR) { + if (anetResolve(NULL,hostname,ip,sizeof(ip), + sentinel.resolve_hostnames ? ANET_NONE : ANET_IP_ONLY) == ANET_ERR) { errno = ENOENT; return NULL; } sa = zmalloc(sizeof(*sa)); + sa->hostname = sdsnew(hostname); sa->ip = sdsnew(ip); sa->port = port; return sa; @@ -605,6 +616,7 @@ sentinelAddr *dupSentinelAddr(sentinelAddr *src) { sentinelAddr *sa; sa = zmalloc(sizeof(*sa)); + sa->hostname = sdsnew(src->hostname); sa->ip = sdsnew(src->ip); sa->port = src->port; return sa; @@ -612,6 +624,7 @@ sentinelAddr *dupSentinelAddr(sentinelAddr *src) { /* Free a Sentinel address. Can't fail. */ void releaseSentinelAddr(sentinelAddr *sa) { + sdsfree(sa->hostname); sdsfree(sa->ip); zfree(sa); } @@ -621,6 +634,21 @@ int sentinelAddrIsEqual(sentinelAddr *a, sentinelAddr *b) { return a->port == b->port && !strcasecmp(a->ip,b->ip); } +/* Return non-zero if a hostname matches an address. */ +int sentinelAddrEqualsHostname(sentinelAddr *a, char *hostname) { + char ip[NET_IP_STR_LEN]; + + /* We always resolve the hostname and compare it to the address */ + if (anetResolve(NULL, hostname, ip, sizeof(ip), + sentinel.resolve_hostnames ? ANET_NONE : ANET_IP_ONLY) == ANET_ERR) + return 0; + return !strcasecmp(a->ip, ip); +} + +const char *announceSentinelAddr(const sentinelAddr *a) { + return sentinel.announce_hostnames ? a->hostname : a->ip; +} + /* =========================== Events notification ========================== */ /* Send an event to log, pub/sub, user notification script. @@ -661,12 +689,12 @@ void sentinelEvent(int level, char *type, sentinelRedisInstance *ri, if (master) { snprintf(msg, sizeof(msg), "%s %s %s %d @ %s %s %d", sentinelRedisInstanceTypeStr(ri), - ri->name, ri->addr->ip, ri->addr->port, - master->name, master->addr->ip, master->addr->port); + ri->name, announceSentinelAddr(ri->addr), ri->addr->port, + master->name, announceSentinelAddr(master->addr), master->addr->port); } else { snprintf(msg, sizeof(msg), "%s %s %s %d", sentinelRedisInstanceTypeStr(ri), - ri->name, ri->addr->ip, ri->addr->port); + ri->name, announceSentinelAddr(ri->addr), ri->addr->port); } fmt += 2; } else { @@ -988,7 +1016,8 @@ void sentinelCallClientReconfScript(sentinelRedisInstance *master, int role, cha sentinelScheduleScriptExecution(master->client_reconfig_script, master->name, (role == SENTINEL_LEADER) ? "leader" : "observer", - state, from->ip, fromport, to->ip, toport, NULL); + state, announceSentinelAddr(from), fromport, + announceSentinelAddr(to), toport, NULL); } /* =============================== instanceLink ============================= */ @@ -1114,6 +1143,35 @@ int sentinelTryConnectionSharing(sentinelRedisInstance *ri) { return C_ERR; } +/* Drop all connections to other sentinels. Returns the number of connections + * dropped.*/ +int sentinelDropConnections(void) { + dictIterator *di; + dictEntry *de; + int dropped = 0; + + di = dictGetIterator(sentinel.masters); + while ((de = dictNext(di)) != NULL) { + dictIterator *sdi; + dictEntry *sde; + + sentinelRedisInstance *ri = dictGetVal(de); + sdi = dictGetIterator(ri->sentinels); + while ((sde = dictNext(sdi)) != NULL) { + sentinelRedisInstance *si = dictGetVal(sde); + if (!si->link->disconnected) { + instanceLinkCloseConnection(si->link, si->link->pc); + instanceLinkCloseConnection(si->link, si->link->cc); + dropped++; + } + } + dictReleaseIterator(sdi); + } + dictReleaseIterator(di); + + return dropped; +} + /* When we detect a Sentinel to switch address (reporting a different IP/port * pair in Hello messages), let's update all the matching Sentinels in the * context of other masters as well and disconnect the links, so that everybody @@ -1226,7 +1284,7 @@ sentinelRedisInstance *createSentinelRedisInstance(char *name, int flags, char * /* For slaves use ip:port as name. */ if (flags & SRI_SLAVE) { - anetFormatAddr(slavename, sizeof(slavename), hostname, port); + anetFormatAddr(slavename, sizeof(slavename), addr->ip, port); name = slavename; } @@ -1337,14 +1395,25 @@ void releaseSentinelRedisInstance(sentinelRedisInstance *ri) { /* Lookup a slave in a master Redis instance, by ip and port. */ sentinelRedisInstance *sentinelRedisInstanceLookupSlave( - sentinelRedisInstance *ri, char *ip, int port) + sentinelRedisInstance *ri, char *slave_addr, int port) { sds key; sentinelRedisInstance *slave; char buf[NET_ADDR_STR_LEN]; + sentinelAddr *addr; serverAssert(ri->flags & SRI_MASTER); - anetFormatAddr(buf,sizeof(buf),ip,port); + + /* We need to handle a slave_addr that is potentially a hostname. + * If that is the case, depending on configuration we either resolve + * it and use the IP addres or fail. + */ + addr = createSentinelAddr(slave_addr, port); + if (!addr) return NULL; + + anetFormatAddr(buf,sizeof(buf),addr->ip,addr->port); + releaseSentinelAddr(addr); + key = sdsnew(buf); slave = dictFetchValue(ri->slaves,key); sdsfree(key); @@ -1394,21 +1463,27 @@ int removeMatchingSentinelFromMaster(sentinelRedisInstance *master, char *runid) * of instances. Return NULL if not found, otherwise return the instance * pointer. * - * runid or ip can be NULL. In such a case the search is performed only + * runid or addr can be NULL. In such a case the search is performed only * by the non-NULL field. */ -sentinelRedisInstance *getSentinelRedisInstanceByAddrAndRunID(dict *instances, char *ip, int port, char *runid) { +sentinelRedisInstance *getSentinelRedisInstanceByAddrAndRunID(dict *instances, char *addr, int port, char *runid) { dictIterator *di; dictEntry *de; sentinelRedisInstance *instance = NULL; + sentinelAddr *ri_addr = NULL; - serverAssert(ip || runid); /* User must pass at least one search param. */ + serverAssert(addr || runid); /* User must pass at least one search param. */ + if (addr != NULL) { + /* Resolve addr, we use the IP as a key even if a hostname is used */ + ri_addr = createSentinelAddr(addr, port); + if (!ri_addr) return NULL; + } di = dictGetIterator(instances); while((de = dictNext(di)) != NULL) { sentinelRedisInstance *ri = dictGetVal(de); if (runid && !ri->runid) continue; if ((runid == NULL || strcmp(ri->runid, runid) == 0) && - (ip == NULL || (strcmp(ri->addr->ip, ip) == 0 && + (addr == NULL || (strcmp(ri->addr->ip, ri_addr->ip) == 0 && ri->addr->port == port))) { instance = ri; @@ -1416,6 +1491,9 @@ sentinelRedisInstance *getSentinelRedisInstanceByAddrAndRunID(dict *instances, c } } dictReleaseIterator(di); + if (ri_addr != NULL) + releaseSentinelAddr(ri_addr); + return instance; } @@ -1530,14 +1608,14 @@ int sentinelResetMastersByPattern(char *pattern, int flags) { * * The function returns C_ERR if the address can't be resolved for some * reason. Otherwise C_OK is returned. */ -int sentinelResetMasterAndChangeAddress(sentinelRedisInstance *master, char *ip, int port) { +int sentinelResetMasterAndChangeAddress(sentinelRedisInstance *master, char *hostname, int port) { sentinelAddr *oldaddr, *newaddr; sentinelAddr **slaves = NULL; int numslaves = 0, j; dictIterator *di; dictEntry *de; - newaddr = createSentinelAddr(ip,port); + newaddr = createSentinelAddr(hostname,port); if (newaddr == NULL) return C_ERR; /* There can be only 0 or 1 slave that has the newaddr. @@ -1551,8 +1629,7 @@ int sentinelResetMasterAndChangeAddress(sentinelRedisInstance *master, char *ip, sentinelRedisInstance *slave = dictGetVal(de); if (sentinelAddrIsEqual(slave->addr,newaddr)) continue; - slaves[numslaves++] = createSentinelAddr(slave->addr->ip, - slave->addr->port); + slaves[numslaves++] = dupSentinelAddr(slave->addr); } dictReleaseIterator(di); @@ -1560,8 +1637,7 @@ int sentinelResetMasterAndChangeAddress(sentinelRedisInstance *master, char *ip, * as a slave as well, so that we'll be able to sense / reconfigure * the old master. */ if (!sentinelAddrIsEqual(newaddr,master->addr)) { - slaves[numslaves++] = createSentinelAddr(master->addr->ip, - master->addr->port); + slaves[numslaves++] = dupSentinelAddr(master->addr); } /* Reset and switch address. */ @@ -1575,7 +1651,7 @@ int sentinelResetMasterAndChangeAddress(sentinelRedisInstance *master, char *ip, for (j = 0; j < numslaves; j++) { sentinelRedisInstance *slave; - slave = createSentinelRedisInstance(NULL,SRI_SLAVE,slaves[j]->ip, + slave = createSentinelRedisInstance(NULL,SRI_SLAVE,slaves[j]->hostname, slaves[j]->port, master->quorum, master); releaseSentinelAddr(slaves[j]); if (slave) sentinelEvent(LL_NOTICE,"+slave",slave,"%@"); @@ -1959,6 +2035,16 @@ const char *sentinelHandleConfiguration(char **argv, int argc) { /* sentinel-pass <password> */ if (strlen(argv[1])) sentinel.sentinel_auth_pass = sdsnew(argv[1]); + } else if (!strcasecmp(argv[0],"resolve-hostnames") && argc == 2) { + /* resolve-hostnames <yes|no> */ + if ((sentinel.resolve_hostnames = yesnotoi(argv[1])) == -1) { + return "Please specify yes or not for the resolve-hostnames option."; + } + } else if (!strcasecmp(argv[0],"announce-hostnames") && argc == 2) { + /* announce-hostnames <yes|no> */ + if ((sentinel.announce_hostnames = yesnotoi(argv[1])) == -1) { + return "Please specify yes or not for the announce-hostnames option."; + } } else { return "Unrecognized sentinel configuration statement."; } @@ -1985,6 +2071,21 @@ void rewriteConfigSentinelOption(struct rewriteConfigState *state) { rewriteConfigRewriteLine(state,"sentinel deny-scripts-reconfig",line, sentinel.deny_scripts_reconfig != SENTINEL_DEFAULT_DENY_SCRIPTS_RECONFIG); + /* sentinel resolve-hostnames. + * This must be included early in the file so it is already in effect + * when reading the file. + */ + line = sdscatprintf(sdsempty(), "sentinel resolve-hostnames %s", + sentinel.resolve_hostnames ? "yes" : "no"); + rewriteConfigRewriteLine(state,"sentinel",line, + sentinel.resolve_hostnames != SENTINEL_DEFAULT_RESOLVE_HOSTNAMES); + + /* sentinel announce-hostnames. */ + line = sdscatprintf(sdsempty(), "sentinel announce-hostnames %s", + sentinel.announce_hostnames ? "yes" : "no"); + rewriteConfigRewriteLine(state,"sentinel",line, + sentinel.announce_hostnames != SENTINEL_DEFAULT_ANNOUNCE_HOSTNAMES); + /* For every master emit a "sentinel monitor" config entry. */ di = dictGetIterator(sentinel.masters); while((de = dictNext(di)) != NULL) { @@ -1995,7 +2096,7 @@ void rewriteConfigSentinelOption(struct rewriteConfigState *state) { master = dictGetVal(de); master_addr = sentinelGetCurrentMasterAddress(master); line = sdscatprintf(sdsempty(),"sentinel monitor %s %s %d %d", - master->name, master_addr->ip, master_addr->port, + master->name, announceSentinelAddr(master_addr), master_addr->port, master->quorum); rewriteConfigRewriteLine(state,"sentinel monitor",line,1); /* rewriteConfigMarkAsProcessed is handled after the loop */ @@ -2095,7 +2196,7 @@ void rewriteConfigSentinelOption(struct rewriteConfigState *state) { slave_addr = master->addr; line = sdscatprintf(sdsempty(), "sentinel known-replica %s %s %d", - master->name, slave_addr->ip, slave_addr->port); + master->name, announceSentinelAddr(slave_addr), slave_addr->port); rewriteConfigRewriteLine(state,"sentinel known-replica",line,1); /* rewriteConfigMarkAsProcessed is handled after the loop */ } @@ -2108,7 +2209,7 @@ void rewriteConfigSentinelOption(struct rewriteConfigState *state) { if (ri->runid == NULL) continue; line = sdscatprintf(sdsempty(), "sentinel known-sentinel %s %s %d %s", - master->name, ri->addr->ip, ri->addr->port, ri->runid); + master->name, announceSentinelAddr(ri->addr), ri->addr->port, ri->runid); rewriteConfigRewriteLine(state,"sentinel known-sentinel",line,1); /* rewriteConfigMarkAsProcessed is handled after the loop */ } @@ -2242,7 +2343,7 @@ void sentinelSendAuthIfNeeded(sentinelRedisInstance *ri, redisAsyncContext *c) { auth_user = ri->master->auth_user; } else if (ri->flags & SRI_SENTINEL) { /* If sentinel_auth_user is NULL, AUTH will use default user - with sentinel_auth_pass to autenticate */ + with sentinel_auth_pass to authenticate */ if (sentinel.sentinel_auth_pass) { auth_pass = sentinel.sentinel_auth_pass; auth_user = sentinel.sentinel_auth_user; @@ -2589,9 +2690,7 @@ void sentinelRefreshInstanceInfo(sentinelRedisInstance *ri, const char *info) { sentinelRedisInstanceNoDownFor(ri,wait_time) && mstime() - ri->role_reported_time > wait_time) { - int retval = sentinelSendSlaveOf(ri, - ri->master->addr->ip, - ri->master->addr->port); + int retval = sentinelSendSlaveOf(ri,ri->master->addr); if (retval == C_OK) sentinelEvent(LL_NOTICE,"+convert-to-slave",ri,"%@"); } @@ -2602,7 +2701,7 @@ void sentinelRefreshInstanceInfo(sentinelRedisInstance *ri, const char *info) { if ((ri->flags & SRI_SLAVE) && role == SRI_SLAVE && (ri->slave_master_port != ri->master->addr->port || - strcasecmp(ri->slave_master_host,ri->master->addr->ip))) + !sentinelAddrEqualsHostname(ri->master->addr, ri->slave_master_host))) { mstime_t wait_time = ri->master->failover_timeout; @@ -2612,9 +2711,7 @@ void sentinelRefreshInstanceInfo(sentinelRedisInstance *ri, const char *info) { sentinelRedisInstanceNoDownFor(ri,wait_time) && mstime() - ri->slave_conf_change_time > wait_time) { - int retval = sentinelSendSlaveOf(ri, - ri->master->addr->ip, - ri->master->addr->port); + int retval = sentinelSendSlaveOf(ri,ri->master->addr); if (retval == C_OK) sentinelEvent(LL_NOTICE,"+fix-slave-config",ri,"%@"); } @@ -2628,8 +2725,8 @@ void sentinelRefreshInstanceInfo(sentinelRedisInstance *ri, const char *info) { /* SRI_RECONF_SENT -> SRI_RECONF_INPROG. */ if ((ri->flags & SRI_RECONF_SENT) && ri->slave_master_host && - strcmp(ri->slave_master_host, - ri->master->promoted_slave->addr->ip) == 0 && + sentinelAddrEqualsHostname(ri->master->promoted_slave->addr, + ri->slave_master_host) && ri->slave_master_port == ri->master->promoted_slave->addr->port) { ri->flags &= ~SRI_RECONF_SENT; @@ -2806,7 +2903,7 @@ void sentinelProcessHelloMessage(char *hello, int hello_len) { if (si && master->config_epoch < master_config_epoch) { master->config_epoch = master_config_epoch; if (master_port != master->addr->port || - strcmp(master->addr->ip, token[5])) + !sentinelAddrEqualsHostname(master->addr, token[5])) { sentinelAddr *old_addr; @@ -2814,7 +2911,7 @@ void sentinelProcessHelloMessage(char *hello, int hello_len) { sentinelEvent(LL_WARNING,"+switch-master", master,"%s %s %d %s %d", master->name, - master->addr->ip, master->addr->port, + announceSentinelAddr(master->addr), master->addr->port, token[5], master_port); old_addr = dupSentinelAddr(master->addr); @@ -2907,7 +3004,7 @@ int sentinelSendHello(sentinelRedisInstance *ri) { announce_ip, announce_port, sentinel.myid, (unsigned long long) sentinel.current_epoch, /* --- */ - master->name,master_addr->ip,master_addr->port, + master->name,announceSentinelAddr(master_addr),master_addr->port, (unsigned long long) master->config_epoch); retval = redisAsyncCommand(ri->link->cc, sentinelPublishReplyCallback, ri, "%s %s %s", @@ -3041,6 +3138,101 @@ void sentinelSendPeriodicCommands(sentinelRedisInstance *ri) { /* =========================== SENTINEL command ============================= */ +/* SENTINEL CONFIG SET <option> */ +void sentinelConfigSetCommand(client *c) { + robj *o = c->argv[3]; + robj *val = c->argv[4]; + long long numval; + int drop_conns = 0; + + if (!strcasecmp(o->ptr, "resolve-hostnames")) { + if ((numval = yesnotoi(val->ptr)) == -1) goto badfmt; + sentinel.resolve_hostnames = numval; + } else if (!strcasecmp(o->ptr, "announce-hostnames")) { + if ((numval = yesnotoi(val->ptr)) == -1) goto badfmt; + sentinel.announce_hostnames = numval; + } else if (!strcasecmp(o->ptr, "announce-ip")) { + if (sentinel.announce_ip) sdsfree(sentinel.announce_ip); + sentinel.announce_ip = sdsnew(val->ptr); + } else if (!strcasecmp(o->ptr, "announce-port")) { + if (getLongLongFromObject(val, &numval) == C_ERR || + numval < 0 || numval > 65535) + goto badfmt; + sentinel.announce_port = numval; + } else if (!strcasecmp(o->ptr, "sentinel-user")) { + sdsfree(sentinel.sentinel_auth_user); + sentinel.sentinel_auth_user = sdsnew(val->ptr); + drop_conns = 1; + } else if (!strcasecmp(o->ptr, "sentinel-pass")) { + sdsfree(sentinel.sentinel_auth_pass); + sentinel.sentinel_auth_pass = sdsnew(val->ptr); + drop_conns = 1; + } else { + addReplyErrorFormat(c, "Invalid argument '%s' to SENTINEL CONFIG SET", + (char *) o->ptr); + return; + } + + sentinelFlushConfig(); + addReply(c, shared.ok); + + /* Drop Sentinel connections to initiate a reconnect if needed. */ + if (drop_conns) + sentinelDropConnections(); + + return; + +badfmt: + addReplyErrorFormat(c, "Invalid value '%s' to SENTINEL CONFIG SET '%s'", + (char *) val->ptr, (char *) o->ptr); +} + +/* SENTINEL CONFIG GET <option> */ +void sentinelConfigGetCommand(client *c) { + robj *o = c->argv[3]; + const char *pattern = o->ptr; + void *replylen = addReplyDeferredLen(c); + int matches = 0; + + if (stringmatch(pattern,"resolve-hostnames",1)) { + addReplyBulkCString(c,"resolve-hostnames"); + addReplyBulkCString(c,sentinel.resolve_hostnames ? "yes" : "no"); + matches++; + } + + if (stringmatch(pattern, "announce-hostnames", 1)) { + addReplyBulkCString(c,"announce-hostnames"); + addReplyBulkCString(c,sentinel.announce_hostnames ? "yes" : "no"); + matches++; + } + + if (stringmatch(pattern, "announce-ip", 1)) { + addReplyBulkCString(c,"announce-ip"); + addReplyBulkCString(c,sentinel.announce_ip ? sentinel.announce_ip : ""); + matches++; + } + + if (stringmatch(pattern, "announce-port", 1)) { + addReplyBulkCString(c, "announce-port"); + addReplyBulkLongLong(c, sentinel.announce_port); + matches++; + } + + if (stringmatch(pattern, "sentinel-user", 1)) { + addReplyBulkCString(c, "sentinel-user"); + addReplyBulkCString(c, sentinel.sentinel_auth_user ? sentinel.sentinel_auth_user : ""); + matches++; + } + + if (stringmatch(pattern, "sentinel-pass", 1)) { + addReplyBulkCString(c, "sentinel-pass"); + addReplyBulkCString(c, sentinel.sentinel_auth_pass ? sentinel.sentinel_auth_pass : ""); + matches++; + } + + setDeferredMapLen(c, replylen, matches); +} + const char *sentinelFailoverStateStr(int state) { switch(state) { case SENTINEL_FAILOVER_STATE_NONE: return "none"; @@ -3067,7 +3259,7 @@ void addReplySentinelRedisInstance(client *c, sentinelRedisInstance *ri) { fields++; addReplyBulkCString(c,"ip"); - addReplyBulkCString(c,ri->addr->ip); + addReplyBulkCString(c,announceSentinelAddr(ri->addr)); fields++; addReplyBulkCString(c,"port"); @@ -3308,6 +3500,10 @@ void sentinelCommand(client *c) { " Check if the current Sentinel configuration is able to reach the quorum", " needed to failover a master and the majority needed to authorize the", " failover.", +"CONFIG SET <param> <value>", +" Set a global Sentinel configuration parameter.", +"CONFIG GET <param>", +" Get global Sentinel configuration parameter.", "GET-MASTER-ADDR-BY-NAME <master-name>", " Return the ip and port number of the master with that name.", "FAILOVER <master-name>", @@ -3449,7 +3645,7 @@ NULL sentinelAddr *addr = sentinelGetCurrentMasterAddress(ri); addReplyArrayLen(c,2); - addReplyBulkCString(c,addr->ip); + addReplyBulkCString(c,announceSentinelAddr(addr)); addReplyBulkLongLong(c,addr->port); } } else if (!strcasecmp(c->argv[1]->ptr,"failover")) { @@ -3494,11 +3690,12 @@ NULL return; } - /* Make sure the IP field is actually a valid IP before passing it - * to createSentinelRedisInstance(), otherwise we may trigger a - * DNS lookup at runtime. */ - if (anetResolveIP(NULL,c->argv[3]->ptr,ip,sizeof(ip)) == ANET_ERR) { - addReplyError(c,"Invalid IP address specified"); + /* If resolve-hostnames is used, actual DNS resolution may take place. + * Otherwise just validate address. + */ + if (anetResolve(NULL,c->argv[3]->ptr,ip,sizeof(ip), + sentinel.resolve_hostnames ? ANET_NONE : ANET_IP_ONLY) == ANET_ERR) { + addReplyError(c, "Invalid IP address or hostname specified"); return; } @@ -3568,6 +3765,14 @@ NULL } else if (!strcasecmp(c->argv[1]->ptr,"set")) { if (c->argc < 3) goto numargserr; sentinelSetCommand(c); + } else if (!strcasecmp(c->argv[1]->ptr,"config")) { + if (c->argc < 3) goto numargserr; + if (!strcasecmp(c->argv[2]->ptr,"set") && c->argc == 5) + sentinelConfigSetCommand(c); + else if (!strcasecmp(c->argv[2]->ptr,"get") && c->argc == 4) + sentinelConfigGetCommand(c); + else + addReplyError(c, "Only SENTINEL CONFIG GET <option> / SET <option> <value> are supported."); } else if (!strcasecmp(c->argv[1]->ptr,"info-cache")) { /* SENTINEL INFO-CACHE <name> */ if (c->argc < 2) goto numargserr; @@ -3731,7 +3936,7 @@ void sentinelInfoCommand(client *c) { "master%d:name=%s,status=%s,address=%s:%d," "slaves=%lu,sentinels=%lu\r\n", master_id++, ri->name, status, - ri->addr->ip, ri->addr->port, + announceSentinelAddr(ri->addr), ri->addr->port, dictSize(ri->slaves), dictSize(ri->sentinels)+1); } @@ -4127,7 +4332,7 @@ void sentinelAskMasterStateToOtherSentinels(sentinelRedisInstance *master, int f sentinelReceiveIsMasterDownReply, ri, "%s is-master-down-by-addr %s %s %llu %s", sentinelInstanceMapCommand(ri,"SENTINEL"), - master->addr->ip, port, + announceSentinelAddr(master->addr), port, sentinel.current_epoch, (master->failover_state > SENTINEL_FAILOVER_STATE_NONE) ? sentinel.myid : "*"); @@ -4281,17 +4486,19 @@ char *sentinelGetLeader(sentinelRedisInstance *master, uint64_t epoch) { * The command returns C_OK if the SLAVEOF command was accepted for * (later) delivery otherwise C_ERR. The command replies are just * discarded. */ -int sentinelSendSlaveOf(sentinelRedisInstance *ri, char *host, int port) { +int sentinelSendSlaveOf(sentinelRedisInstance *ri, const sentinelAddr *addr) { char portstr[32]; + const char *host; int retval; - ll2string(portstr,sizeof(portstr),port); - /* If host is NULL we send SLAVEOF NO ONE that will turn the instance - * into a master. */ - if (host == NULL) { + * into a master. */ + if (!addr) { host = "NO"; memcpy(portstr,"ONE",4); + } else { + host = announceSentinelAddr(addr); + ll2string(portstr,sizeof(portstr),addr->port); } /* In order to send SLAVEOF in a safe way, we send a transaction performing @@ -4576,7 +4783,7 @@ void sentinelFailoverSendSlaveOfNoOne(sentinelRedisInstance *ri) { * We actually register a generic callback for this command as we don't * really care about the reply. We check if it worked indirectly observing * if INFO returns a different role (master instead of slave). */ - retval = sentinelSendSlaveOf(ri->promoted_slave,NULL,0); + retval = sentinelSendSlaveOf(ri->promoted_slave,NULL); if (retval != C_OK) return; sentinelEvent(LL_NOTICE, "+failover-state-wait-promotion", ri->promoted_slave,"%@"); @@ -4646,9 +4853,7 @@ void sentinelFailoverDetectEnd(sentinelRedisInstance *master) { if (slave->flags & (SRI_PROMOTED|SRI_RECONF_DONE|SRI_RECONF_SENT)) continue; if (slave->link->disconnected) continue; - retval = sentinelSendSlaveOf(slave, - master->promoted_slave->addr->ip, - master->promoted_slave->addr->port); + retval = sentinelSendSlaveOf(slave,master->promoted_slave->addr); if (retval == C_OK) { sentinelEvent(LL_NOTICE,"+slave-reconf-sent-be",slave,"%@"); slave->flags |= SRI_RECONF_SENT; @@ -4703,9 +4908,7 @@ void sentinelFailoverReconfNextSlave(sentinelRedisInstance *master) { if (slave->link->disconnected) continue; /* Send SLAVEOF <new master>. */ - retval = sentinelSendSlaveOf(slave, - master->promoted_slave->addr->ip, - master->promoted_slave->addr->port); + retval = sentinelSendSlaveOf(slave,master->promoted_slave->addr); if (retval == C_OK) { slave->flags |= SRI_RECONF_SENT; slave->slave_reconf_sent_time = mstime(); @@ -4727,10 +4930,10 @@ void sentinelFailoverSwitchToPromotedSlave(sentinelRedisInstance *master) { master->promoted_slave : master; sentinelEvent(LL_WARNING,"+switch-master",master,"%s %s %d %s %d", - master->name, master->addr->ip, master->addr->port, - ref->addr->ip, ref->addr->port); + master->name, announceSentinelAddr(master->addr), master->addr->port, + announceSentinelAddr(ref->addr), ref->addr->port); - sentinelResetMasterAndChangeAddress(master,ref->addr->ip,ref->addr->port); + sentinelResetMasterAndChangeAddress(master,ref->addr->hostname,ref->addr->port); } void sentinelFailoverStateMachine(sentinelRedisInstance *ri) { @@ -4887,4 +5090,3 @@ void sentinelTimer(void) { * election because of split brain voting). */ server.hz = CONFIG_DEFAULT_HZ + rand() % CONFIG_DEFAULT_HZ; } - diff --git a/tests/instances.tcl b/tests/instances.tcl index 03af2408b..b9d1b3fe5 100644 --- a/tests/instances.tcl +++ b/tests/instances.tcl @@ -28,6 +28,7 @@ set ::global_config {} set ::sentinel_base_port 20000 set ::redis_base_port 30000 set ::redis_port_count 1024 +set ::host "127.0.0.1" set ::pids {} ; # We kill everything at exit set ::dirs {} ; # We remove all the temp dirs at exit set ::run_matching {} ; # If non empty, only tests matching pattern are run. @@ -128,18 +129,18 @@ proc spawn_instance {type base_port count {conf {}} {base_conf_file ""}} { } # Check availability finally - if {[server_is_up 127.0.0.1 $port 100] == 0} { + if {[server_is_up $::host $port 100] == 0} { set logfile [file join $dirname log.txt] puts [exec tail $logfile] abort_sentinel_test "Problems starting $type #$j: ping timeout, maybe server start failed, check $logfile" } # Push the instance into the right list - set link [redis 127.0.0.1 $port 0 $::tls] + set link [redis $::host $port 0 $::tls] $link reconnect 1 lappend ::${type}_instances [list \ pid $pid \ - host 127.0.0.1 \ + host $::host \ port $port \ link $link \ ] @@ -241,6 +242,9 @@ proc parse_options {} { set ::simulate_error 1 } elseif {$opt eq {--valgrind}} { set ::valgrind 1 + } elseif {$opt eq {--host}} { + incr j + set ::host ${val} } elseif {$opt eq {--tls}} { package require tls 1.6 ::tls::init \ @@ -259,6 +263,7 @@ proc parse_options {} { puts "--fail Simulate a test failure." puts "--valgrind Run with valgrind." puts "--tls Run tests in TLS mode." + puts "--host <host> Use hostname instead of 127.0.0.1." puts "--config <k> <v> Extra config argument(s)." puts "--help Shows this help." exit 0 diff --git a/tests/sentinel/tests/08-hostname-conf.tcl b/tests/sentinel/tests/08-hostname-conf.tcl new file mode 100644 index 000000000..45bb56687 --- /dev/null +++ b/tests/sentinel/tests/08-hostname-conf.tcl @@ -0,0 +1,62 @@ +proc set_redis_announce_ip {addr} { + foreach_redis_id id { + R $id config set replica-announce-ip $addr + } +} + +proc set_sentinel_config {keyword value} { + foreach_sentinel_id id { + S $id sentinel config set $keyword $value + } +} + +proc set_all_instances_hostname {hostname} { + foreach_sentinel_id id { + set_instance_attrib sentinel $id host $hostname + } + foreach_redis_id id { + set_instance_attrib redis $id host $hostname + } +} + +test "(pre-init) Configure instances and sentinel for hostname use" { + set ::host "localhost" + restart_killed_instances + set_all_instances_hostname $::host + set_redis_announce_ip $::host + set_sentinel_config resolve-hostnames yes + set_sentinel_config announce-hostnames yes +} + +source "../tests/includes/init-tests.tcl" + +proc verify_hostname_announced {hostname} { + foreach_sentinel_id id { + # Master is reported with its hostname + assert_equal [lindex [S $id SENTINEL GET-MASTER-ADDR-BY-NAME mymaster] 0] $hostname + + # Replicas are reported with their hostnames + foreach replica [S $id SENTINEL REPLICAS mymaster] { + assert_equal [dict get $replica ip] $hostname + } + } +} + +test "Sentinel announces hostnames" { + # Check initial state + verify_hostname_announced $::host + + # Disable announce-hostnames and confirm IPs are used + set_sentinel_config announce-hostnames no + verify_hostname_announced "127.0.0.1" +} + +# We need to revert any special configuration because all tests currently +# share the same instances. +test "(post-cleanup) Configure instances and sentinel for IPs" { + set ::host "127.0.0.1" + set_all_instances_hostname $::host + set_redis_announce_ip $::host + set_sentinel_config resolve-hostnames no + set_sentinel_config announce-hostnames no +}
\ No newline at end of file diff --git a/tests/sentinel/tests/09-acl-support.tcl b/tests/sentinel/tests/09-acl-support.tcl new file mode 100644 index 000000000..1366fc4d5 --- /dev/null +++ b/tests/sentinel/tests/09-acl-support.tcl @@ -0,0 +1,50 @@ + +source "../tests/includes/init-tests.tcl" + +set ::user "testuser" +set ::password "secret" + +proc setup_acl {} { + foreach_sentinel_id id { + assert_equal {OK} [S $id ACL SETUSER $::user >$::password +@all on] + assert_equal {OK} [S $id ACL SETUSER default off] + + S $id CLIENT KILL USER default SKIPME no + assert_equal {OK} [S $id AUTH $::user $::password] + } +} + +proc teardown_acl {} { + foreach_sentinel_id id { + assert_equal {OK} [S $id ACL SETUSER default on] + assert_equal {1} [S $id ACL DELUSER $::user] + + S $id SENTINEL CONFIG SET sentinel-user "" + S $id SENTINEL CONFIG SET sentinel-pass "" + } +} + +test "(post-init) Set up ACL configuration" { + setup_acl + assert_equal $::user [S 1 ACL WHOAMI] +} + +test "SENTINEL CONFIG SET handles on-the-fly credentials reconfiguration" { + # Make sure we're starting with a broken state... + after 5000 + catch {S 1 SENTINEL CKQUORUM mymaster} err + assert_match {*NOQUORUM*} $err + + foreach_sentinel_id id { + assert_equal {OK} [S $id SENTINEL CONFIG SET sentinel-user $::user] + assert_equal {OK} [S $id SENTINEL CONFIG SET sentinel-pass $::password] + } + + after 5000 + assert_match {*OK*} [S 1 SENTINEL CKQUORUM mymaster] +} + +test "(post-cleanup) Tear down ACL configuration" { + teardown_acl +} + diff --git a/tests/sentinel/tests/includes/init-tests.tcl b/tests/sentinel/tests/includes/init-tests.tcl index d6796fda6..b4626caed 100644 --- a/tests/sentinel/tests/includes/init-tests.tcl +++ b/tests/sentinel/tests/includes/init-tests.tcl @@ -1,6 +1,6 @@ # Initialization tests -- most units will start including this. -test "(init) Restart killed instances" { +proc restart_killed_instances {} { foreach type {redis sentinel} { foreach_${type}_id id { if {[get_instance_attrib $type $id pid] == -1} { @@ -12,6 +12,10 @@ test "(init) Restart killed instances" { } } +test "(init) Restart killed instances" { + restart_killed_instances +} + test "(init) Remove old master entry from sentinels" { foreach_sentinel_id id { catch {S $id SENTINEL REMOVE mymaster} |