From 80d41583d1e7c423f47a662e0d6dce042ca0e3fb Mon Sep 17 00:00:00 2001 From: Pavlo Bashynskyi Date: Tue, 12 May 2020 20:18:24 +0300 Subject: [PATCH] Add info command to redis module (#286) * Add info command to redis module * Fix sanity test * Create a separate redis_info module * Type of arguments in documentation was determined * Add redis_info test * Fix sanity test * Add integration test * Add integration platforms (centos7/8,fedora30/31,opensuse15+py2,ubuntu1604/1804) * Add centos6 support * Fix suggestions * Add contact email --- plugins/modules/database/misc/redis.py | 2 + plugins/modules/database/misc/redis_info.py | 241 ++++++++++++++++++ plugins/modules/redis_info.py | 1 + tests/integration/targets/redis_info/aliases | 5 + .../targets/redis_info/defaults/main.yml | 4 + .../targets/redis_info/meta/main.yml | 2 + .../targets/redis_info/tasks/main.yml | 42 +++ .../setup_redis_replication/defaults/main.yml | 35 +++ .../setup_redis_replication/handlers/main.yml | 34 +++ .../setup_redis_replication/meta/main.yml | 2 + .../setup_redis_replication/tasks/main.yml | 6 + .../tasks/setup_redis_cluster.yml | 105 ++++++++ .../unit/plugins/modules/database/__init__.py | 0 .../plugins/modules/database/misc/__init__.py | 0 .../modules/database/misc/test_redis_info.py | 76 ++++++ 15 files changed, 555 insertions(+) create mode 100644 plugins/modules/database/misc/redis_info.py create mode 120000 plugins/modules/redis_info.py create mode 100644 tests/integration/targets/redis_info/aliases create mode 100644 tests/integration/targets/redis_info/defaults/main.yml create mode 100644 tests/integration/targets/redis_info/meta/main.yml create mode 100644 tests/integration/targets/redis_info/tasks/main.yml create mode 100644 tests/integration/targets/setup_redis_replication/defaults/main.yml create mode 100644 tests/integration/targets/setup_redis_replication/handlers/main.yml create mode 100644 tests/integration/targets/setup_redis_replication/meta/main.yml create mode 100644 tests/integration/targets/setup_redis_replication/tasks/main.yml create mode 100644 tests/integration/targets/setup_redis_replication/tasks/setup_redis_cluster.yml create mode 100644 tests/unit/plugins/modules/database/__init__.py create mode 100644 tests/unit/plugins/modules/database/misc/__init__.py create mode 100644 tests/unit/plugins/modules/database/misc/test_redis_info.py diff --git a/plugins/modules/database/misc/redis.py b/plugins/modules/database/misc/redis.py index f998cc477e..38234e9aef 100644 --- a/plugins/modules/database/misc/redis.py +++ b/plugins/modules/database/misc/redis.py @@ -73,6 +73,8 @@ notes: - If the redis master instance we are making slave of is password protected this needs to be in the redis.conf in the masterauth variable +seealso: + - module: redis_info requirements: [ redis ] author: "Xabier Larrakoetxea (@slok)" ''' diff --git a/plugins/modules/database/misc/redis_info.py b/plugins/modules/database/misc/redis_info.py new file mode 100644 index 0000000000..2621137b0d --- /dev/null +++ b/plugins/modules/database/misc/redis_info.py @@ -0,0 +1,241 @@ +#!/usr/bin/python +# -*- coding: utf-8 -*- + +# Copyright: (c) 2020, Pavlo Bashynskyi (@levonet) +# GNU General Public License v3.0+ (see COPYING or https://www.gnu.org/licenses/gpl-3.0.txt) + +from __future__ import absolute_import, division, print_function +__metaclass__ = type + + +ANSIBLE_METADATA = { + 'metadata_version': '1.1', + 'status': ['preview'], + 'supported_by': 'community' +} + +DOCUMENTATION = r''' +--- +module: redis_info +short_description: Gather information about Redis servers +description: +- Gathers information and statistics about Redis servers. +options: + login_host: + description: + - The host running the database. + type: str + default: localhost + login_port: + description: + - The port to connect to. + type: int + default: 6379 + login_password: + description: + - The password used to authenticate with, when authentication is enabled for the Redis server. + type: str +notes: +- Requires the redis-py Python package on the remote host. You can + install it with pip (C(pip install redis)) or with a package manager. + U(https://github.com/andymccurdy/redis-py) +seealso: +- module: redis +requirements: [ redis ] +author: "Pavlo Bashynskyi (@levonet)" +''' + +EXAMPLES = r''' +- name: Get server information + community.general.redis_info: + register: result + +- name: Print server information + debug: + var: result.info +''' + +RETURN = r''' +info: + description: The default set of server information sections U(https://redis.io/commands/info). + returned: success + type: dict + sample: { + "active_defrag_hits": 0, + "active_defrag_key_hits": 0, + "active_defrag_key_misses": 0, + "active_defrag_misses": 0, + "active_defrag_running": 0, + "allocator_active": 932409344, + "allocator_allocated": 932062792, + "allocator_frag_bytes": 346552, + "allocator_frag_ratio": 1.0, + "allocator_resident": 947253248, + "allocator_rss_bytes": 14843904, + "allocator_rss_ratio": 1.02, + "aof_current_rewrite_time_sec": -1, + "aof_enabled": 0, + "aof_last_bgrewrite_status": "ok", + "aof_last_cow_size": 0, + "aof_last_rewrite_time_sec": -1, + "aof_last_write_status": "ok", + "aof_rewrite_in_progress": 0, + "aof_rewrite_scheduled": 0, + "arch_bits": 64, + "atomicvar_api": "atomic-builtin", + "blocked_clients": 0, + "client_recent_max_input_buffer": 4, + "client_recent_max_output_buffer": 0, + "cluster_enabled": 0, + "config_file": "", + "configured_hz": 10, + "connected_clients": 4, + "connected_slaves": 0, + "db0": { + "avg_ttl": 1945628530, + "expires": 16, + "keys": 3341411 + }, + "evicted_keys": 0, + "executable": "/data/redis-server", + "expired_keys": 9, + "expired_stale_perc": 1.72, + "expired_time_cap_reached_count": 0, + "gcc_version": "9.2.0", + "hz": 10, + "instantaneous_input_kbps": 0.0, + "instantaneous_ops_per_sec": 0, + "instantaneous_output_kbps": 0.0, + "keyspace_hits": 0, + "keyspace_misses": 0, + "latest_fork_usec": 0, + "lazyfree_pending_objects": 0, + "loading": 0, + "lru_clock": 11603632, + "master_repl_offset": 118831417, + "master_replid": "0d904704e424e38c3cd896783e9f9d28d4836e5e", + "master_replid2": "0000000000000000000000000000000000000000", + "maxmemory": 0, + "maxmemory_human": "0B", + "maxmemory_policy": "noeviction", + "mem_allocator": "jemalloc-5.1.0", + "mem_aof_buffer": 0, + "mem_clients_normal": 49694, + "mem_clients_slaves": 0, + "mem_fragmentation_bytes": 12355480, + "mem_fragmentation_ratio": 1.01, + "mem_not_counted_for_evict": 0, + "mem_replication_backlog": 1048576, + "migrate_cached_sockets": 0, + "multiplexing_api": "epoll", + "number_of_cached_scripts": 0, + "os": "Linux 3.10.0-862.14.4.el7.x86_64 x86_64", + "process_id": 1, + "pubsub_channels": 0, + "pubsub_patterns": 0, + "rdb_bgsave_in_progress": 0, + "rdb_changes_since_last_save": 671, + "rdb_current_bgsave_time_sec": -1, + "rdb_last_bgsave_status": "ok", + "rdb_last_bgsave_time_sec": -1, + "rdb_last_cow_size": 0, + "rdb_last_save_time": 1588702236, + "redis_build_id": "a31260535f820267", + "redis_git_dirty": 0, + "redis_git_sha1": 0, + "redis_mode": "standalone", + "redis_version": "999.999.999", + "rejected_connections": 0, + "repl_backlog_active": 1, + "repl_backlog_first_byte_offset": 118707937, + "repl_backlog_histlen": 123481, + "repl_backlog_size": 1048576, + "role": "master", + "rss_overhead_bytes": -3051520, + "rss_overhead_ratio": 1.0, + "run_id": "8d252f66c3ef89bd60a060cf8dc5cfe3d511c5e4", + "second_repl_offset": 118830003, + "slave_expires_tracked_keys": 0, + "sync_full": 0, + "sync_partial_err": 0, + "sync_partial_ok": 0, + "tcp_port": 6379, + "total_commands_processed": 885, + "total_connections_received": 10, + "total_net_input_bytes": 802709255, + "total_net_output_bytes": 31754, + "total_system_memory": 135029538816, + "total_system_memory_human": "125.76G", + "uptime_in_days": 53, + "uptime_in_seconds": 4631778, + "used_cpu_sys": 4.668282, + "used_cpu_sys_children": 0.002191, + "used_cpu_user": 4.21088, + "used_cpu_user_children": 0.0, + "used_memory": 931908760, + "used_memory_dataset": 910774306, + "used_memory_dataset_perc": "97.82%", + "used_memory_human": "888.74M", + "used_memory_lua": 37888, + "used_memory_lua_human": "37.00K", + "used_memory_overhead": 21134454, + "used_memory_peak": 932015216, + "used_memory_peak_human": "888.84M", + "used_memory_peak_perc": "99.99%", + "used_memory_rss": 944201728, + "used_memory_rss_human": "900.46M", + "used_memory_scripts": 0, + "used_memory_scripts_human": "0B", + "used_memory_startup": 791264 + } +''' + +import traceback + +REDIS_IMP_ERR = None +try: + from redis import StrictRedis + HAS_REDIS_PACKAGE = True +except ImportError: + REDIS_IMP_ERR = traceback.format_exc() + HAS_REDIS_PACKAGE = False + +from ansible.module_utils.basic import AnsibleModule, missing_required_lib +from ansible.module_utils._text import to_native + + +def redis_client(**client_params): + return StrictRedis(**client_params) + + +# Module execution. +def main(): + module = AnsibleModule( + argument_spec=dict( + login_host=dict(type='str', default='localhost'), + login_port=dict(type='int', default=6379), + login_password=dict(type='str', no_log=True), + ), + supports_check_mode=True, + ) + + if not HAS_REDIS_PACKAGE: + module.fail_json(msg=missing_required_lib('redis'), exception=REDIS_IMP_ERR) + + login_host = module.params['login_host'] + login_port = module.params['login_port'] + login_password = module.params['login_password'] + + # Connect and check + client = redis_client(host=login_host, port=login_port, password=login_password) + try: + client.ping() + except Exception as e: + module.fail_json(msg="unable to connect to database: %s" % to_native(e), exception=traceback.format_exc()) + + info = client.info() + module.exit_json(changed=False, info=info) + + +if __name__ == '__main__': + main() diff --git a/plugins/modules/redis_info.py b/plugins/modules/redis_info.py new file mode 120000 index 0000000000..c7080146f4 --- /dev/null +++ b/plugins/modules/redis_info.py @@ -0,0 +1 @@ +./database/misc/redis_info.py \ No newline at end of file diff --git a/tests/integration/targets/redis_info/aliases b/tests/integration/targets/redis_info/aliases new file mode 100644 index 0000000000..07a0fbcbba --- /dev/null +++ b/tests/integration/targets/redis_info/aliases @@ -0,0 +1,5 @@ +destructive +shippable/posix/group1 +skip/aix +skip/osx +skip/rhel diff --git a/tests/integration/targets/redis_info/defaults/main.yml b/tests/integration/targets/redis_info/defaults/main.yml new file mode 100644 index 0000000000..1352c55cc3 --- /dev/null +++ b/tests/integration/targets/redis_info/defaults/main.yml @@ -0,0 +1,4 @@ +--- +redis_password: PASS +master_port: 6379 +slave_port: 6380 diff --git a/tests/integration/targets/redis_info/meta/main.yml b/tests/integration/targets/redis_info/meta/main.yml new file mode 100644 index 0000000000..5b07837132 --- /dev/null +++ b/tests/integration/targets/redis_info/meta/main.yml @@ -0,0 +1,2 @@ +dependencies: +- setup_redis_replication diff --git a/tests/integration/targets/redis_info/tasks/main.yml b/tests/integration/targets/redis_info/tasks/main.yml new file mode 100644 index 0000000000..eafbb50d5b --- /dev/null +++ b/tests/integration/targets/redis_info/tasks/main.yml @@ -0,0 +1,42 @@ +# Copyright: (c) 2020, Pavlo Bashynskyi (@levonet) +# GNU General Public License v3.0+ (see COPYING or https://www.gnu.org/licenses/gpl-3.0.txt) + +- name: redis_info - connect to master with default host/port + community.general.redis_info: + login_password: "{{ redis_password }}" + register: result + +- assert: + that: + - result is not changed + - result.info is defined + - result.info.tcp_port == master_port + - result.info.role == 'master' + +- name: redis_info - connect to master (check) + community.general.redis_info: + login_host: 127.0.0.1 + login_port: "{{ master_port }}" + login_password: "{{ redis_password }}" + check_mode: yes + register: result + +- assert: + that: + - result is not changed + - result.info is defined + - result.info.tcp_port == master_port + - result.info.role == 'master' + +- name: redis_info - connect to slave + community.general.redis_info: + login_port: "{{ slave_port }}" + login_password: "{{ redis_password }}" + register: result + +- assert: + that: + - result is not changed + - result.info is defined + - result.info.tcp_port == slave_port + - result.info.role == 'slave' diff --git a/tests/integration/targets/setup_redis_replication/defaults/main.yml b/tests/integration/targets/setup_redis_replication/defaults/main.yml new file mode 100644 index 0000000000..bdbbbb2cac --- /dev/null +++ b/tests/integration/targets/setup_redis_replication/defaults/main.yml @@ -0,0 +1,35 @@ +# General +redis_packages: + Ubuntu: + - redis-server + openSUSE Leap: + - redis + Fedora: + - redis + CentOS: + - redis + FreeBSD: + - redis + +redis_bin: + Ubuntu: /usr/bin/redis-server + openSUSE Leap: /usr/sbin/redis-server + Fedora: /usr/bin/redis-server + CentOS: /usr/bin/redis-server + FreeBSD: /usr/local/bin/redis-server + +redis_module: "{{ (ansible_python_version is version('2.7', '>=')) | ternary('redis', 'redis==2.10.6') }}" + +redis_password: PASS + +# Master +master_port: 6379 +master_conf: /etc/redis-master.conf +master_datadir: /var/lib/redis-master +master_logdir: /var/log/redis-master + +# Slave +slave_port: 6380 +slave_conf: /etc/redis-slave.conf +slave_datadir: /var/lib/redis-slave +slave_logdir: /var/log/redis-slave diff --git a/tests/integration/targets/setup_redis_replication/handlers/main.yml b/tests/integration/targets/setup_redis_replication/handlers/main.yml new file mode 100644 index 0000000000..d4d535cdf7 --- /dev/null +++ b/tests/integration/targets/setup_redis_replication/handlers/main.yml @@ -0,0 +1,34 @@ +- name: stop redis services + shell: | + kill -TERM $(cat /var/run/redis_{{ master_port }}.pid) + kill -TERM $(cat /var/run/redis_{{ slave_port }}.pid) + listen: cleanup redis + +- name: remove redis packages + action: "{{ ansible_facts.pkg_mgr }}" + args: + name: "{{ item }}" + state: absent + loop: "{{ redis_packages[ansible_distribution] }}" + listen: cleanup redis + +- name: remove pip packages + pip: + name: redis + state: absent + listen: cleanup redis + +- name: remove redis data + file: + path: "{{ item }}" + state: absent + loop: + - "{{ master_conf }}" + - "{{ master_datadir }}" + - "{{ master_logdir }}" + - /var/run/redis_{{ master_port }}.pid + - "{{ slave_conf }}" + - "{{ slave_datadir }}" + - "{{ slave_logdir }}" + - /var/run/redis_{{ slave_port }}.pid + listen: cleanup redis diff --git a/tests/integration/targets/setup_redis_replication/meta/main.yml b/tests/integration/targets/setup_redis_replication/meta/main.yml new file mode 100644 index 0000000000..6f094434e5 --- /dev/null +++ b/tests/integration/targets/setup_redis_replication/meta/main.yml @@ -0,0 +1,2 @@ +dependencies: +- setup_pkg_mgr diff --git a/tests/integration/targets/setup_redis_replication/tasks/main.yml b/tests/integration/targets/setup_redis_replication/tasks/main.yml new file mode 100644 index 0000000000..be4221f019 --- /dev/null +++ b/tests/integration/targets/setup_redis_replication/tasks/main.yml @@ -0,0 +1,6 @@ +# Copyright: (c) 2020, Pavlo Bashynskyi (@levonet) +# GNU General Public License v3.0+ (see COPYING or https://www.gnu.org/licenses/gpl-3.0.txt) + +- import_tasks: setup_redis_cluster.yml + when: + - ansible_distribution in ['CentOS', 'Fedora', 'FreeBSD', 'openSUSE Leap', 'Ubuntu'] diff --git a/tests/integration/targets/setup_redis_replication/tasks/setup_redis_cluster.yml b/tests/integration/targets/setup_redis_replication/tasks/setup_redis_cluster.yml new file mode 100644 index 0000000000..6f6837e5cf --- /dev/null +++ b/tests/integration/targets/setup_redis_replication/tasks/setup_redis_cluster.yml @@ -0,0 +1,105 @@ +# We run two servers listening different ports +# to be able to check replication (one server for master, another for slave). + +- name: Install redis server apt dependencies + apt: + name: "{{ redis_packages[ansible_distribution] }}" + state: latest + policy_rc_d: 101 + when: + - ansible_facts.pkg_mgr == 'apt' + notify: cleanup redis + +- name: Install redis server rpm dependencies + yum: + name: "{{ redis_packages[ansible_distribution] }}" + state: latest + when: + - ansible_facts.pkg_mgr == 'yum' + notify: cleanup redis + +- name: Install redis rpm dependencies + dnf: + name: "{{ redis_packages[ansible_distribution] }}" + state: latest + when: ansible_facts.pkg_mgr == 'dnf' + notify: cleanup redis + +- name: Install redis server zypper dependencies + zypper: + name: "{{ redis_packages[ansible_distribution] }}" + state: latest + when: + - ansible_facts.pkg_mgr == 'community.general.zypper' + notify: cleanup redis + +- name: Install redis FreeBSD dependencies + community.general.pkgng: + name: "{{ redis_packages[ansible_distribution] }}" + state: latest + when: + - ansible_facts.pkg_mgr == 'community.general.pkgng' + notify: cleanup redis + +- name: Install redis module + pip: + name: "{{ redis_module }}" + state: present + notify: cleanup redis + +- name: Create redis directories + file: + path: "{{ item }}" + state: directory + owner: redis + group: redis + loop: + - "{{ master_datadir }}" + - "{{ master_logdir }}" + - "{{ slave_datadir }}" + - "{{ slave_logdir }}" + +- name: Create redis configs + copy: + dest: "{{ item.file }}" + content: | + daemonize yes + port {{ item.port }} + pidfile /var/run/redis_{{ item.port }}.pid + logfile {{ item.logdir }}/redis.log + dir {{ item.datadir }} + requirepass {{ redis_password }} + masterauth {{ redis_password }} + loop: + - file: "{{ master_conf }}" + port: "{{ master_port }}" + logdir: "{{ master_logdir }}" + datadir: "{{ master_datadir }}" + - file: "{{ slave_conf }}" + port: "{{ slave_port }}" + logdir: "{{ slave_logdir }}" + datadir: "{{ slave_datadir }}" + +- name: Start redis master + shell: "{{ redis_bin[ansible_distribution] }} {{ master_conf }}" + +- name: Start redis slave + shell: "{{ redis_bin[ansible_distribution] }} {{ slave_conf }} --slaveof 127.0.0.1 {{ master_port }}" + +- name: Wait for redis master to be started + wait_for: + host: 127.0.0.1 + port: "{{ master_port }}" + state: started + delay: 1 + connect_timeout: 5 + timeout: 30 + +- name: Wait for redis slave to be started + wait_for: + host: 127.0.0.1 + port: "{{ slave_port }}" + state: started + delay: 1 + connect_timeout: 5 + timeout: 30 diff --git a/tests/unit/plugins/modules/database/__init__.py b/tests/unit/plugins/modules/database/__init__.py new file mode 100644 index 0000000000..e69de29bb2 diff --git a/tests/unit/plugins/modules/database/misc/__init__.py b/tests/unit/plugins/modules/database/misc/__init__.py new file mode 100644 index 0000000000..e69de29bb2 diff --git a/tests/unit/plugins/modules/database/misc/test_redis_info.py b/tests/unit/plugins/modules/database/misc/test_redis_info.py new file mode 100644 index 0000000000..4ff1efc5c5 --- /dev/null +++ b/tests/unit/plugins/modules/database/misc/test_redis_info.py @@ -0,0 +1,76 @@ +# -*- coding: utf-8 -*- + +# Copyright: (c) 2020, Pavlo Bashynskyi (@levonet) +# GNU General Public License v3.0+ (see COPYING or https://www.gnu.org/licenses/gpl-3.0.txt) + +from __future__ import absolute_import, division, print_function +__metaclass__ = type + +from ansible_collections.community.general.tests.unit.compat.mock import patch, MagicMock +from ansible_collections.community.general.plugins.modules.database.misc import redis_info +from ansible_collections.community.general.tests.unit.plugins.modules.utils import AnsibleExitJson, AnsibleFailJson, ModuleTestCase, set_module_args + + +class FakeRedisClient(MagicMock): + + def ping(self): + pass + + def info(self): + return {'redis_version': '999.999.999'} + + +class FakeRedisClientFail(MagicMock): + + def ping(self): + raise Exception('Test Error') + + def info(self): + pass + + +class TestRedisInfoModule(ModuleTestCase): + + def setUp(self): + super(TestRedisInfoModule, self).setUp() + redis_info.HAS_REDIS_PACKAGE = True + self.module = redis_info + + def tearDown(self): + super(TestRedisInfoModule, self).tearDown() + + def patch_redis_client(self, **kwds): + return patch('ansible_collections.community.general.plugins.modules.database.misc.redis_info.redis_client', autospec=True, **kwds) + + def test_without_parameters(self): + """Test without parameters""" + with self.patch_redis_client(side_effect=FakeRedisClient) as redis_client: + with self.assertRaises(AnsibleExitJson) as result: + set_module_args({}) + self.module.main() + self.assertEqual(redis_client.call_count, 1) + self.assertEqual(redis_client.call_args, ({'host': 'localhost', 'port': 6379, 'password': None},)) + self.assertEqual(result.exception.args[0]['info']['redis_version'], '999.999.999') + + def test_with_parameters(self): + """Test with all parameters""" + with self.patch_redis_client(side_effect=FakeRedisClient) as redis_client: + with self.assertRaises(AnsibleExitJson) as result: + set_module_args({ + 'login_host': 'test', + 'login_port': 1234, + 'login_password': 'PASS' + }) + self.module.main() + self.assertEqual(redis_client.call_count, 1) + self.assertEqual(redis_client.call_args, ({'host': 'test', 'port': 1234, 'password': 'PASS'},)) + self.assertEqual(result.exception.args[0]['info']['redis_version'], '999.999.999') + + def test_with_fail_client(self): + """Test failure message""" + with self.patch_redis_client(side_effect=FakeRedisClientFail) as redis_client: + with self.assertRaises(AnsibleFailJson) as result: + set_module_args({}) + self.module.main() + self.assertEqual(redis_client.call_count, 1) + self.assertEqual(result.exception.args[0]['msg'], 'unable to connect to database: Test Error')