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