github.com/spotahome/redis-operator@v1.2.4/service/redis/client.go (about) 1 package redis 2 3 import ( 4 "context" 5 "errors" 6 "fmt" 7 "net" 8 "regexp" 9 "strconv" 10 "strings" 11 12 rediscli "github.com/go-redis/redis/v8" 13 "github.com/spotahome/redis-operator/log" 14 "github.com/spotahome/redis-operator/metrics" 15 ) 16 17 // Client defines the functions neccesary to connect to redis and sentinel to get or set what we nned 18 type Client interface { 19 GetNumberSentinelsInMemory(ip string) (int32, error) 20 GetNumberSentinelSlavesInMemory(ip string) (int32, error) 21 ResetSentinel(ip string) error 22 GetSlaveOf(ip, port, password string) (string, error) 23 IsMaster(ip, port, password string) (bool, error) 24 MonitorRedis(ip, monitor, quorum, password string) error 25 MonitorRedisWithPort(ip, monitor, port, quorum, password string) error 26 MakeMaster(ip, port, password string) error 27 MakeSlaveOf(ip, masterIP, password string) error 28 MakeSlaveOfWithPort(ip, masterIP, masterPort, password string) error 29 GetSentinelMonitor(ip string) (string, string, error) 30 SetCustomSentinelConfig(ip string, configs []string) error 31 SetCustomRedisConfig(ip string, port string, configs []string, password string) error 32 SlaveIsReady(ip, port, password string) (bool, error) 33 SentinelCheckQuorum(ip string) error 34 } 35 36 type client struct { 37 metricsRecorder metrics.Recorder 38 } 39 40 // New returns a redis client 41 func New(metricsRecorder metrics.Recorder) Client { 42 return &client{ 43 metricsRecorder: metricsRecorder, 44 } 45 } 46 47 const ( 48 sentinelsNumberREString = "sentinels=([0-9]+)" 49 slaveNumberREString = "slaves=([0-9]+)" 50 sentinelStatusREString = "status=([a-z]+)" 51 redisMasterHostREString = "master_host:([0-9.]+)" 52 redisRoleMaster = "role:master" 53 redisSyncing = "master_sync_in_progress:1" 54 redisMasterSillPending = "master_host:127.0.0.1" 55 redisLinkUp = "master_link_status:up" 56 redisPort = "6379" 57 sentinelPort = "26379" 58 masterName = "mymaster" 59 ) 60 61 var ( 62 sentinelNumberRE = regexp.MustCompile(sentinelsNumberREString) 63 sentinelStatusRE = regexp.MustCompile(sentinelStatusREString) 64 slaveNumberRE = regexp.MustCompile(slaveNumberREString) 65 redisMasterHostRE = regexp.MustCompile(redisMasterHostREString) 66 ) 67 68 // GetNumberSentinelsInMemory return the number of sentinels that the requested sentinel has 69 func (c *client) GetNumberSentinelsInMemory(ip string) (int32, error) { 70 options := &rediscli.Options{ 71 Addr: net.JoinHostPort(ip, sentinelPort), 72 Password: "", 73 DB: 0, 74 } 75 rClient := rediscli.NewClient(options) 76 defer rClient.Close() 77 info, err := rClient.Info(context.TODO(), "sentinel").Result() 78 if err != nil { 79 c.metricsRecorder.RecordRedisOperation(metrics.KIND_SENTINEL, ip, metrics.GET_NUM_SENTINELS_IN_MEM, metrics.FAIL, getRedisError(err)) 80 return 0, err 81 } 82 if err2 := isSentinelReady(info); err2 != nil { 83 c.metricsRecorder.RecordRedisOperation(metrics.KIND_SENTINEL, ip, metrics.GET_NUM_SENTINELS_IN_MEM, metrics.FAIL, metrics.SENTINEL_NOT_READY) 84 return 0, err2 85 } 86 match := sentinelNumberRE.FindStringSubmatch(info) 87 if len(match) == 0 { 88 c.metricsRecorder.RecordRedisOperation(metrics.KIND_SENTINEL, ip, metrics.GET_NUM_SENTINELS_IN_MEM, metrics.FAIL, metrics.REGEX_NOT_FOUND) 89 return 0, errors.New("seninel regex not found") 90 } 91 nSentinels, err := strconv.Atoi(match[1]) 92 if err != nil { 93 c.metricsRecorder.RecordRedisOperation(metrics.KIND_SENTINEL, ip, metrics.GET_NUM_SENTINELS_IN_MEM, metrics.FAIL, metrics.MISC) 94 return 0, err 95 } 96 c.metricsRecorder.RecordRedisOperation(metrics.KIND_SENTINEL, ip, metrics.GET_NUM_SENTINELS_IN_MEM, metrics.SUCCESS, metrics.NOT_APPLICABLE) 97 return int32(nSentinels), nil 98 } 99 100 // GetNumberSentinelsInMemory return the number of sentinels that the requested sentinel has 101 func (c *client) GetNumberSentinelSlavesInMemory(ip string) (int32, error) { 102 options := &rediscli.Options{ 103 Addr: net.JoinHostPort(ip, sentinelPort), 104 Password: "", 105 DB: 0, 106 } 107 rClient := rediscli.NewClient(options) 108 defer rClient.Close() 109 info, err := rClient.Info(context.TODO(), "sentinel").Result() 110 if err != nil { 111 c.metricsRecorder.RecordRedisOperation(metrics.KIND_SENTINEL, ip, metrics.GET_NUM_REDIS_SLAVES_IN_MEM, metrics.FAIL, getRedisError(err)) 112 return 0, err 113 } 114 if err2 := isSentinelReady(info); err2 != nil { 115 c.metricsRecorder.RecordRedisOperation(metrics.KIND_SENTINEL, ip, metrics.GET_NUM_REDIS_SLAVES_IN_MEM, metrics.FAIL, metrics.SENTINEL_NOT_READY) 116 return 0, err2 117 } 118 match := slaveNumberRE.FindStringSubmatch(info) 119 if len(match) == 0 { 120 c.metricsRecorder.RecordRedisOperation(metrics.KIND_SENTINEL, ip, metrics.GET_NUM_REDIS_SLAVES_IN_MEM, metrics.FAIL, metrics.REGEX_NOT_FOUND) 121 return 0, errors.New("slaves regex not found") 122 } 123 nSlaves, err := strconv.Atoi(match[1]) 124 if err != nil { 125 c.metricsRecorder.RecordRedisOperation(metrics.KIND_SENTINEL, ip, metrics.GET_NUM_REDIS_SLAVES_IN_MEM, metrics.FAIL, metrics.MISC) 126 return 0, err 127 } 128 c.metricsRecorder.RecordRedisOperation(metrics.KIND_SENTINEL, ip, metrics.GET_NUM_REDIS_SLAVES_IN_MEM, metrics.SUCCESS, metrics.NOT_APPLICABLE) 129 return int32(nSlaves), nil 130 } 131 132 func isSentinelReady(info string) error { 133 matchStatus := sentinelStatusRE.FindStringSubmatch(info) 134 if len(matchStatus) == 0 || matchStatus[1] != "ok" { 135 return errors.New("sentinels not ready") 136 } 137 return nil 138 } 139 140 // ResetSentinel sends a sentinel reset * for the given sentinel 141 func (c *client) ResetSentinel(ip string) error { 142 options := &rediscli.Options{ 143 Addr: net.JoinHostPort(ip, sentinelPort), 144 Password: "", 145 DB: 0, 146 } 147 rClient := rediscli.NewClient(options) 148 defer rClient.Close() 149 cmd := rediscli.NewIntCmd(context.TODO(), "SENTINEL", "reset", "*") 150 err := rClient.Process(context.TODO(), cmd) 151 if err != nil { 152 c.metricsRecorder.RecordRedisOperation(metrics.KIND_SENTINEL, ip, metrics.RESET_SENTINEL, metrics.FAIL, getRedisError(err)) 153 return err 154 } 155 _, err = cmd.Result() 156 if err != nil { 157 c.metricsRecorder.RecordRedisOperation(metrics.KIND_SENTINEL, ip, metrics.RESET_SENTINEL, metrics.FAIL, getRedisError(err)) 158 return err 159 } 160 c.metricsRecorder.RecordRedisOperation(metrics.KIND_SENTINEL, ip, metrics.RESET_SENTINEL, metrics.SUCCESS, metrics.NOT_APPLICABLE) 161 return nil 162 } 163 164 // GetSlaveOf returns the master of the given redis, or nil if it's master 165 func (c *client) GetSlaveOf(ip, port, password string) (string, error) { 166 167 options := &rediscli.Options{ 168 Addr: net.JoinHostPort(ip, port), 169 Password: password, 170 DB: 0, 171 } 172 rClient := rediscli.NewClient(options) 173 defer rClient.Close() 174 info, err := rClient.Info(context.TODO(), "replication").Result() 175 if err != nil { 176 c.metricsRecorder.RecordRedisOperation(metrics.KIND_REDIS, ip, metrics.GET_SLAVE_OF, metrics.FAIL, getRedisError(err)) 177 log.Errorf("error while getting masterIP : Failed to get info replication while querying redis instance %v", ip) 178 return "", err 179 } 180 match := redisMasterHostRE.FindStringSubmatch(info) 181 if len(match) == 0 { 182 c.metricsRecorder.RecordRedisOperation(metrics.KIND_REDIS, ip, metrics.GET_SLAVE_OF, metrics.SUCCESS, metrics.NOT_APPLICABLE) 183 return "", nil 184 } 185 c.metricsRecorder.RecordRedisOperation(metrics.KIND_REDIS, ip, metrics.GET_SLAVE_OF, metrics.SUCCESS, metrics.NOT_APPLICABLE) 186 return match[1], nil 187 } 188 189 func (c *client) IsMaster(ip, port, password string) (bool, error) { 190 options := &rediscli.Options{ 191 Addr: net.JoinHostPort(ip, port), 192 Password: password, 193 DB: 0, 194 } 195 rClient := rediscli.NewClient(options) 196 defer rClient.Close() 197 info, err := rClient.Info(context.TODO(), "replication").Result() 198 if err != nil { 199 c.metricsRecorder.RecordRedisOperation(metrics.KIND_REDIS, ip, metrics.IS_MASTER, metrics.FAIL, getRedisError(err)) 200 return false, err 201 } 202 c.metricsRecorder.RecordRedisOperation(metrics.KIND_REDIS, ip, metrics.IS_MASTER, metrics.SUCCESS, metrics.NOT_APPLICABLE) 203 return strings.Contains(info, redisRoleMaster), nil 204 } 205 206 func (c *client) MonitorRedis(ip, monitor, quorum, password string) error { 207 return c.MonitorRedisWithPort(ip, monitor, redisPort, quorum, password) 208 } 209 210 func (c *client) MonitorRedisWithPort(ip, monitor, port, quorum, password string) error { 211 options := &rediscli.Options{ 212 Addr: net.JoinHostPort(ip, sentinelPort), 213 Password: "", 214 DB: 0, 215 } 216 rClient := rediscli.NewClient(options) 217 defer rClient.Close() 218 cmd := rediscli.NewBoolCmd(context.TODO(), "SENTINEL", "REMOVE", masterName) 219 _ = rClient.Process(context.TODO(), cmd) 220 // We'll continue even if it fails, the priority is to have the redises monitored 221 cmd = rediscli.NewBoolCmd(context.TODO(), "SENTINEL", "MONITOR", masterName, monitor, port, quorum) 222 err := rClient.Process(context.TODO(), cmd) 223 if err != nil { 224 c.metricsRecorder.RecordRedisOperation(metrics.KIND_REDIS, ip, metrics.MONITOR_REDIS_WITH_PORT, metrics.FAIL, getRedisError(err)) 225 return err 226 } 227 _, err = cmd.Result() 228 if err != nil { 229 c.metricsRecorder.RecordRedisOperation(metrics.KIND_REDIS, ip, metrics.MONITOR_REDIS_WITH_PORT, metrics.FAIL, getRedisError(err)) 230 return err 231 } 232 233 if password != "" { 234 cmd = rediscli.NewBoolCmd(context.TODO(), "SENTINEL", "SET", masterName, "auth-pass", password) 235 err := rClient.Process(context.TODO(), cmd) 236 if err != nil { 237 c.metricsRecorder.RecordRedisOperation(metrics.KIND_REDIS, ip, metrics.MONITOR_REDIS_WITH_PORT, metrics.FAIL, getRedisError(err)) 238 return err 239 } 240 _, err = cmd.Result() 241 if err != nil { 242 c.metricsRecorder.RecordRedisOperation(metrics.KIND_REDIS, ip, metrics.MONITOR_REDIS_WITH_PORT, metrics.FAIL, getRedisError(err)) 243 return err 244 } 245 } 246 c.metricsRecorder.RecordRedisOperation(metrics.KIND_REDIS, ip, metrics.MONITOR_REDIS_WITH_PORT, metrics.SUCCESS, metrics.NOT_APPLICABLE) 247 return nil 248 } 249 250 func (c *client) MakeMaster(ip string, port string, password string) error { 251 options := &rediscli.Options{ 252 Addr: net.JoinHostPort(ip, port), 253 Password: password, 254 DB: 0, 255 } 256 rClient := rediscli.NewClient(options) 257 defer rClient.Close() 258 if res := rClient.SlaveOf(context.TODO(), "NO", "ONE"); res.Err() != nil { 259 c.metricsRecorder.RecordRedisOperation(metrics.KIND_REDIS, ip, metrics.MAKE_MASTER, metrics.FAIL, getRedisError(res.Err())) 260 return res.Err() 261 } 262 c.metricsRecorder.RecordRedisOperation(metrics.KIND_REDIS, ip, metrics.MAKE_MASTER, metrics.SUCCESS, metrics.NOT_APPLICABLE) 263 return nil 264 } 265 266 func (c *client) MakeSlaveOf(ip, masterIP, password string) error { 267 return c.MakeSlaveOfWithPort(ip, masterIP, redisPort, password) 268 } 269 270 func (c *client) MakeSlaveOfWithPort(ip, masterIP, masterPort, password string) error { 271 options := &rediscli.Options{ 272 Addr: net.JoinHostPort(ip, masterPort), // this is IP and Port for the RedisFailover redis 273 Password: password, 274 DB: 0, 275 } 276 rClient := rediscli.NewClient(options) 277 defer rClient.Close() 278 if res := rClient.SlaveOf(context.TODO(), masterIP, masterPort); res.Err() != nil { 279 c.metricsRecorder.RecordRedisOperation(metrics.KIND_REDIS, ip, metrics.MAKE_SLAVE_OF, metrics.FAIL, getRedisError(res.Err())) 280 return res.Err() 281 } 282 c.metricsRecorder.RecordRedisOperation(metrics.KIND_REDIS, ip, metrics.MAKE_SLAVE_OF, metrics.SUCCESS, metrics.NOT_APPLICABLE) 283 return nil 284 } 285 286 func (c *client) GetSentinelMonitor(ip string) (string, string, error) { 287 options := &rediscli.Options{ 288 Addr: net.JoinHostPort(ip, sentinelPort), 289 Password: "", 290 DB: 0, 291 } 292 rClient := rediscli.NewClient(options) 293 defer rClient.Close() 294 cmd := rediscli.NewSliceCmd(context.TODO(), "SENTINEL", "master", masterName) 295 err := rClient.Process(context.TODO(), cmd) 296 if err != nil { 297 c.metricsRecorder.RecordRedisOperation(metrics.KIND_SENTINEL, ip, metrics.GET_SENTINEL_MONITOR, metrics.FAIL, getRedisError(err)) 298 return "", "", err 299 } 300 res, err := cmd.Result() 301 if err != nil { 302 c.metricsRecorder.RecordRedisOperation(metrics.KIND_SENTINEL, ip, metrics.GET_SENTINEL_MONITOR, metrics.FAIL, getRedisError(err)) 303 return "", "", err 304 } 305 masterIP := res[3].(string) 306 masterPort := res[5].(string) 307 c.metricsRecorder.RecordRedisOperation(metrics.KIND_SENTINEL, ip, metrics.GET_SENTINEL_MONITOR, metrics.SUCCESS, metrics.NOT_APPLICABLE) 308 return masterIP, masterPort, nil 309 } 310 311 func (c *client) SetCustomSentinelConfig(ip string, configs []string) error { 312 options := &rediscli.Options{ 313 Addr: net.JoinHostPort(ip, sentinelPort), 314 Password: "", 315 DB: 0, 316 } 317 rClient := rediscli.NewClient(options) 318 defer rClient.Close() 319 320 for _, config := range configs { 321 param, value, err := c.getConfigParameters(config) 322 if err != nil { 323 return err 324 } 325 if err := c.applySentinelConfig(param, value, rClient); err != nil { 326 return err 327 } 328 } 329 return nil 330 } 331 332 func (c *client) SentinelCheckQuorum(ip string) error { 333 334 options := &rediscli.Options{ 335 Addr: net.JoinHostPort(ip, sentinelPort), 336 Password: "", 337 DB: 0, 338 } 339 rClient := rediscli.NewSentinelClient(options) 340 defer rClient.Close() 341 cmd := rClient.CkQuorum(context.TODO(), masterName) 342 res, err := cmd.Result() 343 344 if err != nil { 345 log.Warnf("Unable to get result for CKQUORUM comand") 346 c.metricsRecorder.RecordRedisOperation(metrics.KIND_SENTINEL, ip, metrics.CHECK_SENTINEL_QUORUM, metrics.FAIL, getRedisError(err)) 347 return err 348 } 349 log.Debugf("SentinelCheckQuorum cmd result: %s", res) 350 s := strings.Split(res, " ") 351 status := s[0] 352 quorum := s[1] 353 354 if status == "" { 355 log.Errorf("quorum command result unexpected output") 356 c.metricsRecorder.RecordRedisOperation(metrics.KIND_SENTINEL, ip, metrics.CHECK_SENTINEL_QUORUM, metrics.FAIL, "quorum command result unexpected output") 357 return fmt.Errorf("quorum command result unexpected output") 358 } 359 if status == "(error)" && quorum == "NOQUORUM" { 360 c.metricsRecorder.RecordRedisOperation(metrics.KIND_SENTINEL, ip, metrics.CHECK_SENTINEL_QUORUM, metrics.SUCCESS, "NOQUORUM") 361 return fmt.Errorf("quorum Not available") 362 363 } else if status == "OK" { 364 c.metricsRecorder.RecordRedisOperation(metrics.KIND_SENTINEL, ip, metrics.CHECK_SENTINEL_QUORUM, metrics.SUCCESS, "QUORUM") 365 return nil 366 } else { 367 log.Errorf("quorum command status unexpected !!!") 368 c.metricsRecorder.RecordRedisOperation(metrics.KIND_SENTINEL, ip, metrics.CHECK_SENTINEL_QUORUM, metrics.FAIL, "quorum command status unexpected output") 369 return fmt.Errorf("quorum status unexpected %s", status) 370 } 371 372 } 373 func (c *client) SetCustomRedisConfig(ip string, port string, configs []string, password string) error { 374 options := &rediscli.Options{ 375 Addr: net.JoinHostPort(ip, port), 376 Password: password, 377 DB: 0, 378 } 379 rClient := rediscli.NewClient(options) 380 defer rClient.Close() 381 382 for _, config := range configs { 383 param, value, err := c.getConfigParameters(config) 384 if err != nil { 385 return err 386 } 387 // If the configuration is an empty line , it will result in an incorrect configSet, which will not run properly down the line. 388 // `config set save ""` should support 389 if strings.TrimSpace(param) == "" { 390 continue 391 } 392 if err := c.applyRedisConfig(param, value, rClient); err != nil { 393 return err 394 } 395 } 396 return nil 397 } 398 399 func (c *client) applyRedisConfig(parameter string, value string, rClient *rediscli.Client) error { 400 result := rClient.ConfigSet(context.TODO(), parameter, value) 401 if nil != result.Err() { 402 c.metricsRecorder.RecordRedisOperation(metrics.KIND_REDIS, strings.Split(rClient.Options().Addr, ":")[0], metrics.APPLY_REDIS_CONFIG, metrics.FAIL, getRedisError(result.Err())) 403 return result.Err() 404 } 405 c.metricsRecorder.RecordRedisOperation(metrics.KIND_REDIS, strings.Split(rClient.Options().Addr, ":")[0], metrics.APPLY_REDIS_CONFIG, metrics.SUCCESS, metrics.NOT_APPLICABLE) 406 return result.Err() 407 } 408 409 func (c *client) applySentinelConfig(parameter string, value string, rClient *rediscli.Client) error { 410 cmd := rediscli.NewStatusCmd(context.TODO(), "SENTINEL", "set", masterName, parameter, value) 411 err := rClient.Process(context.TODO(), cmd) 412 if err != nil { 413 c.metricsRecorder.RecordRedisOperation(metrics.KIND_SENTINEL, strings.Split(rClient.Options().Addr, ":")[0], metrics.APPLY_SENTINEL_CONFIG, metrics.FAIL, getRedisError(err)) 414 return err 415 } 416 c.metricsRecorder.RecordRedisOperation(metrics.KIND_SENTINEL, strings.Split(rClient.Options().Addr, ":")[0], metrics.APPLY_SENTINEL_CONFIG, metrics.SUCCESS, metrics.NOT_APPLICABLE) 417 return cmd.Err() 418 } 419 420 func (c *client) getConfigParameters(config string) (parameter string, value string, err error) { 421 s := strings.Split(config, " ") 422 if len(s) < 2 { 423 return "", "", fmt.Errorf("configuration '%s' malformed", config) 424 } 425 if len(s) == 2 && s[1] == `""` { 426 return s[0], "", nil 427 } 428 return s[0], strings.Join(s[1:], " "), nil 429 } 430 431 func (c *client) SlaveIsReady(ip, port, password string) (bool, error) { 432 options := &rediscli.Options{ 433 Addr: net.JoinHostPort(ip, port), 434 Password: password, 435 DB: 0, 436 } 437 rClient := rediscli.NewClient(options) 438 defer rClient.Close() 439 info, err := rClient.Info(context.TODO(), "replication").Result() 440 if err != nil { 441 c.metricsRecorder.RecordRedisOperation(metrics.KIND_REDIS, strings.Split(rClient.Options().Addr, ":")[0], metrics.SLAVE_IS_READY, metrics.FAIL, getRedisError(err)) 442 return false, err 443 } 444 445 ok := !strings.Contains(info, redisSyncing) && 446 !strings.Contains(info, redisMasterSillPending) && 447 strings.Contains(info, redisLinkUp) 448 c.metricsRecorder.RecordRedisOperation(metrics.KIND_REDIS, strings.Split(rClient.Options().Addr, ":")[0], metrics.SLAVE_IS_READY, metrics.SUCCESS, metrics.NOT_APPLICABLE) 449 return ok, nil 450 } 451 452 func getRedisError(err error) string { 453 if strings.Contains(err.Error(), "NOAUTH") { 454 return metrics.NOAUTH 455 } else if strings.Contains(err.Error(), "WRONGPASS") { 456 return metrics.WRONG_PASSWORD_USED 457 } else if strings.Contains(err.Error(), "NOPERM") { 458 return metrics.NOPERM 459 } else if strings.Contains(err.Error(), "i/o timeout") { 460 return metrics.IO_TIMEOUT 461 } else if strings.Contains(err.Error(), "connection refused") { 462 return metrics.CONNECTION_REFUSED 463 } else { 464 return "MISC" 465 } 466 }