1#!/usr/local/bin/python3.8 2# -*- coding: utf-8 -*- 3 4# Copyright: Ansible Project 5# GNU General Public License v3.0+ (see COPYING or https://www.gnu.org/licenses/gpl-3.0.txt) 6 7from __future__ import absolute_import, division, print_function 8__metaclass__ = type 9 10DOCUMENTATION = ''' 11--- 12module: redis 13short_description: Various redis commands, replica and flush 14description: 15 - Unified utility to interact with redis instances. 16options: 17 command: 18 description: 19 - The selected redis command 20 - C(config) ensures a configuration setting on an instance. 21 - C(flush) flushes all the instance or a specified db. 22 - C(replica) sets a redis instance in replica or master mode. (C(slave) is an alias for C(replica).) 23 choices: [ config, flush, replica, slave ] 24 type: str 25 login_password: 26 description: 27 - The password used to authenticate with (usually not used) 28 type: str 29 login_host: 30 description: 31 - The host running the database 32 default: localhost 33 type: str 34 login_port: 35 description: 36 - The port to connect to 37 default: 6379 38 type: int 39 master_host: 40 description: 41 - The host of the master instance [replica command] 42 type: str 43 master_port: 44 description: 45 - The port of the master instance [replica command] 46 type: int 47 replica_mode: 48 description: 49 - The mode of the redis instance [replica command] 50 - C(slave) is an alias for C(replica). 51 default: replica 52 choices: [ master, replica, slave ] 53 type: str 54 aliases: 55 - slave_mode 56 db: 57 description: 58 - The database to flush (used in db mode) [flush command] 59 type: int 60 flush_mode: 61 description: 62 - Type of flush (all the dbs in a redis instance or a specific one) 63 [flush command] 64 default: all 65 choices: [ all, db ] 66 type: str 67 name: 68 description: 69 - A redis config key. 70 type: str 71 value: 72 description: 73 - A redis config value. When memory size is needed, it is possible 74 to specify it in the usal form of 1KB, 2M, 400MB where the base is 1024. 75 Units are case insensitive i.e. 1m = 1mb = 1M = 1MB. 76 type: str 77 78notes: 79 - Requires the redis-py Python package on the remote host. You can 80 install it with pip (pip install redis) or with a package manager. 81 https://github.com/andymccurdy/redis-py 82 - If the redis master instance we are making replica of is password protected 83 this needs to be in the redis.conf in the masterauth variable 84 85seealso: 86 - module: community.general.redis_info 87requirements: [ redis ] 88author: "Xabier Larrakoetxea (@slok)" 89''' 90 91EXAMPLES = ''' 92- name: Set local redis instance to be a replica of melee.island on port 6377 93 community.general.redis: 94 command: replica 95 master_host: melee.island 96 master_port: 6377 97 98- name: Deactivate replica mode 99 community.general.redis: 100 command: replica 101 replica_mode: master 102 103- name: Flush all the redis db 104 community.general.redis: 105 command: flush 106 flush_mode: all 107 108- name: Flush only one db in a redis instance 109 community.general.redis: 110 command: flush 111 db: 1 112 flush_mode: db 113 114- name: Configure local redis to have 10000 max clients 115 community.general.redis: 116 command: config 117 name: maxclients 118 value: 10000 119 120- name: Configure local redis maxmemory to 4GB 121 community.general.redis: 122 command: config 123 name: maxmemory 124 value: 4GB 125 126- name: Configure local redis to have lua time limit of 100 ms 127 community.general.redis: 128 command: config 129 name: lua-time-limit 130 value: 100 131''' 132 133import traceback 134 135REDIS_IMP_ERR = None 136try: 137 import redis 138except ImportError: 139 REDIS_IMP_ERR = traceback.format_exc() 140 redis_found = False 141else: 142 redis_found = True 143 144from ansible.module_utils.basic import AnsibleModule, missing_required_lib 145from ansible.module_utils.common.text.formatters import human_to_bytes 146from ansible.module_utils.common.text.converters import to_native 147import re 148 149 150# Redis module specific support methods. 151def set_replica_mode(client, master_host, master_port): 152 try: 153 return client.slaveof(master_host, master_port) 154 except Exception: 155 return False 156 157 158def set_master_mode(client): 159 try: 160 return client.slaveof() 161 except Exception: 162 return False 163 164 165def flush(client, db=None): 166 try: 167 if not isinstance(db, int): 168 return client.flushall() 169 else: 170 # The passed client has been connected to the database already 171 return client.flushdb() 172 except Exception: 173 return False 174 175 176# Module execution. 177def main(): 178 module = AnsibleModule( 179 argument_spec=dict( 180 command=dict(type='str', choices=['config', 'flush', 'replica', 'slave']), 181 login_password=dict(type='str', no_log=True), 182 login_host=dict(type='str', default='localhost'), 183 login_port=dict(type='int', default=6379), 184 master_host=dict(type='str'), 185 master_port=dict(type='int'), 186 replica_mode=dict(type='str', default='replica', choices=['master', 'replica', 'slave'], aliases=["slave_mode"]), 187 db=dict(type='int'), 188 flush_mode=dict(type='str', default='all', choices=['all', 'db']), 189 name=dict(type='str'), 190 value=dict(type='str') 191 ), 192 supports_check_mode=True, 193 ) 194 195 if not redis_found: 196 module.fail_json(msg=missing_required_lib('redis'), exception=REDIS_IMP_ERR) 197 198 login_password = module.params['login_password'] 199 login_host = module.params['login_host'] 200 login_port = module.params['login_port'] 201 command = module.params['command'] 202 if command == "slave": 203 command = "replica" 204 205 # Replica Command section ----------- 206 if command == "replica": 207 master_host = module.params['master_host'] 208 master_port = module.params['master_port'] 209 mode = module.params['replica_mode'] 210 if mode == "slave": 211 mode = "replica" 212 213 # Check if we have all the data 214 if mode == "replica": # Only need data if we want to be replica 215 if not master_host: 216 module.fail_json(msg='In replica mode master host must be provided') 217 218 if not master_port: 219 module.fail_json(msg='In replica mode master port must be provided') 220 221 # Connect and check 222 r = redis.StrictRedis(host=login_host, port=login_port, password=login_password) 223 try: 224 r.ping() 225 except Exception as e: 226 module.fail_json(msg="unable to connect to database: %s" % to_native(e), exception=traceback.format_exc()) 227 228 # Check if we are already in the mode that we want 229 info = r.info() 230 if mode == "master" and info["role"] == "master": 231 module.exit_json(changed=False, mode=mode) 232 233 elif mode == "replica" and info["role"] == "slave" and info["master_host"] == master_host and info["master_port"] == master_port: 234 status = dict( 235 status=mode, 236 master_host=master_host, 237 master_port=master_port, 238 ) 239 module.exit_json(changed=False, mode=status) 240 else: 241 # Do the stuff 242 # (Check Check_mode before commands so the commands aren't evaluated 243 # if not necessary) 244 if mode == "replica": 245 if module.check_mode or set_replica_mode(r, master_host, master_port): 246 info = r.info() 247 status = { 248 'status': mode, 249 'master_host': master_host, 250 'master_port': master_port, 251 } 252 module.exit_json(changed=True, mode=status) 253 else: 254 module.fail_json(msg='Unable to set replica mode') 255 256 else: 257 if module.check_mode or set_master_mode(r): 258 module.exit_json(changed=True, mode=mode) 259 else: 260 module.fail_json(msg='Unable to set master mode') 261 262 # flush Command section ----------- 263 elif command == "flush": 264 db = module.params['db'] 265 mode = module.params['flush_mode'] 266 267 # Check if we have all the data 268 if mode == "db": 269 if db is None: 270 module.fail_json(msg="In db mode the db number must be provided") 271 272 # Connect and check 273 r = redis.StrictRedis(host=login_host, port=login_port, password=login_password, db=db) 274 try: 275 r.ping() 276 except Exception as e: 277 module.fail_json(msg="unable to connect to database: %s" % to_native(e), exception=traceback.format_exc()) 278 279 # Do the stuff 280 # (Check Check_mode before commands so the commands aren't evaluated 281 # if not necessary) 282 if mode == "all": 283 if module.check_mode or flush(r): 284 module.exit_json(changed=True, flushed=True) 285 else: # Flush never fails :) 286 module.fail_json(msg="Unable to flush all databases") 287 288 else: 289 if module.check_mode or flush(r, db): 290 module.exit_json(changed=True, flushed=True, db=db) 291 else: # Flush never fails :) 292 module.fail_json(msg="Unable to flush '%d' database" % db) 293 elif command == 'config': 294 name = module.params['name'] 295 296 try: # try to parse the value as if it were the memory size 297 if re.match(r'^\s*(\d*\.?\d*)\s*([A-Za-z]+)?\s*$', module.params['value'].upper()): 298 value = str(human_to_bytes(module.params['value'].upper())) 299 else: 300 value = module.params['value'] 301 except ValueError: 302 value = module.params['value'] 303 304 r = redis.StrictRedis(host=login_host, port=login_port, password=login_password) 305 306 try: 307 r.ping() 308 except Exception as e: 309 module.fail_json(msg="unable to connect to database: %s" % to_native(e), exception=traceback.format_exc()) 310 311 try: 312 old_value = r.config_get(name)[name] 313 except Exception as e: 314 module.fail_json(msg="unable to read config: %s" % to_native(e), exception=traceback.format_exc()) 315 changed = old_value != value 316 317 if module.check_mode or not changed: 318 module.exit_json(changed=changed, name=name, value=value) 319 else: 320 try: 321 r.config_set(name, value) 322 except Exception as e: 323 module.fail_json(msg="unable to write config: %s" % to_native(e), exception=traceback.format_exc()) 324 module.exit_json(changed=changed, name=name, value=value) 325 else: 326 module.fail_json(msg='A valid command must be provided') 327 328 329if __name__ == '__main__': 330 main() 331