From 6b207bce4ce4929a8979bf22f3f3543c597e5ef5 Mon Sep 17 00:00:00 2001 From: Andreas Botzner Date: Wed, 8 Sep 2021 07:14:37 +0200 Subject: [PATCH] Adds redis_data_info module (#3227) * Added redis_data_info module Added: - redis_data_info module and suggested 'exists' return flag. - module_utils for redis with a base class that handles database connections. - inhereited unit tests and added some new ones for the exit flag * Docfix and sanity * typo * Suggested doc changes and ssl option * TLS and validate_certs fix * Set support_check_mode for info plugin * Docfix and import errors * Redis versioning Fix * version bump and append fixes --- .github/BOTMETA.yml | 2 + plugins/doc_fragments/redis.py | 57 +++++++++ plugins/module_utils/redis.py | 93 ++++++++++++++ .../modules/database/misc/redis_data_info.py | 111 +++++++++++++++++ plugins/modules/redis_data_info.py | 1 + .../database/misc/test_redis_data_info.py | 113 ++++++++++++++++++ 6 files changed, 377 insertions(+) create mode 100644 plugins/doc_fragments/redis.py create mode 100644 plugins/module_utils/redis.py create mode 100644 plugins/modules/database/misc/redis_data_info.py create mode 120000 plugins/modules/redis_data_info.py create mode 100644 tests/unit/plugins/modules/database/misc/test_redis_data_info.py diff --git a/.github/BOTMETA.yml b/.github/BOTMETA.yml index b07f95e8cc..5b55449a67 100644 --- a/.github/BOTMETA.yml +++ b/.github/BOTMETA.yml @@ -458,6 +458,8 @@ files: maintainers: slok $modules/database/misc/redis_info.py: maintainers: levonet + $modules/database/misc/redis_data_info.py: + maintainers: paginabianca $modules/database/misc/riak.py: maintainers: drewkerrigan jsmartin $modules/database/mssql/mssql_db.py: diff --git a/plugins/doc_fragments/redis.py b/plugins/doc_fragments/redis.py new file mode 100644 index 0000000000..e7af25ec8f --- /dev/null +++ b/plugins/doc_fragments/redis.py @@ -0,0 +1,57 @@ +# -*- coding: utf-8 -*- + +# Copyright: (c) 2021, Andreas Botzner +# 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 + + +class ModuleDocFragment(object): + # Common parameters for Redis modules + DOCUMENTATION = r''' +options: + login_host: + description: + - Specify the target host running the database. + default: localhost + type: str + login_port: + description: + - Specify the port to connect to. + default: 6379 + type: int + login_user: + description: + - Specify the user to authenticate with. + - Requires L(redis,https://pypi.org/project/redis) >= 3.4.0. + type: str + login_password: + description: + - Specify the password to authenticate with. + - Usually not used when target is localhost. + type: str + tls: + description: + - Specify whether or not to use TLS for the connection. + type: bool + default: true + validate_certs: + description: + - Specify whether or not to validate TLS certificates. + - This should only be turned off for personally controlled sites or with + C(localhost) as target. + type: bool + default: true + ca_certs: + description: + - Path to root certificates file. If not set and I(tls) is + set to C(true), certifi ca-certificates will be used. + type: str +requirements: [ "redis", "certifi" ] + +notes: + - Requires the C(redis) Python package on the remote host. You can + install it with pip (C(pip install redis)) or with a package manager. + Information on the library can be found at U(https://github.com/andymccurdy/redis-py). +''' diff --git a/plugins/module_utils/redis.py b/plugins/module_utils/redis.py new file mode 100644 index 0000000000..9d55aecad0 --- /dev/null +++ b/plugins/module_utils/redis.py @@ -0,0 +1,93 @@ +# -*- coding: utf-8 -*- +# +# Copyright: (c) 2021, Andreas Botzner +# 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 + +from ansible.module_utils.basic import missing_required_lib +__metaclass__ = type + +import traceback + +REDIS_IMP_ERR = None +try: + from redis import Redis + from redis import __version__ as redis_version + HAS_REDIS_PACKAGE = True +except ImportError: + REDIS_IMP_ERR = traceback.format_exc() + HAS_REDIS_PACKAGE = False + +try: + import certifi + HAS_CERTIFI_PACKAGE = True +except ImportError: + CERTIFI_IMPORT_ERROR = traceback.format_exc() + HAS_CERTIFI_PACKAGE = False + + +def fail_imports(module): + errors = [] + traceback = [] + if not HAS_REDIS_PACKAGE: + errors.append(missing_required_lib('redis')) + traceback.append(REDIS_IMP_ERR) + if not HAS_CERTIFI_PACKAGE: + errors.append(missing_required_lib('certifi')) + traceback.append(CERTIFI_IMPORT_ERROR) + if errors: + module.fail_json(errors=errors, traceback='\n'.join(traceback)) + + +def redis_auth_argument_spec(): + return dict( + login_host=dict(type='str', + default='localhost',), + login_user=dict(type='str'), + login_password=dict(type='str', + no_log=True + ), + login_port=dict(type='int', default=6379), + tls=dict(type='bool', + default=True), + validate_certs=dict(type='bool', + default=True + ), + ca_certs=dict(type='str') + ) + + +class RedisAnsible(object): + '''Base class for Redis module''' + + def __init__(self, module): + self.module = module + self.connection = self._connect() + + def _connect(self): + login_host = self.module.params['login_host'] + login_user = self.module.params['login_user'] + login_password = self.module.params['login_password'] + login_port = self.module.params['login_port'] + tls = self.module.params['tls'] + validate_certs = 'required' if self.module.params['validate_certs'] else None + ca_certs = self.module.params['ca_certs'] + if tls and ca_certs is None: + ca_certs = str(certifi.where()) + if tuple(map(int, redis_version.split('.'))) < (3, 4, 0) and login_user is not None: + self.module.fail_json( + msg='The option `username` in only supported with redis >= 3.4.0.') + params = {'host': login_host, + 'port': login_port, + 'password': login_password, + 'ssl_ca_certs': ca_certs, + 'ssl_cert_reqs': validate_certs, + 'ssl': tls} + if login_user is not None: + params['username'] = login_user + try: + return Redis(**params) + except Exception as e: + self.module.fail_json(msg='{0}'.format(str(e))) + return None diff --git a/plugins/modules/database/misc/redis_data_info.py b/plugins/modules/database/misc/redis_data_info.py new file mode 100644 index 0000000000..866bda62d1 --- /dev/null +++ b/plugins/modules/database/misc/redis_data_info.py @@ -0,0 +1,111 @@ +#!/usr/bin/python +# -*- coding: utf-8 -*- + +# Copyright: (c) 2021, Andreas Botzner +# 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 + +DOCUMENTATION = ''' +--- +module: redis_data_info +short_description: Get value of key in Redis database +version_added: 3.7.0 +description: + - Get value of keys in Redis database. +author: "Andreas Botzner (@paginabianca)" +options: + key: + description: + - Database key. + type: str + required: true + +extends_documentation_fragment: + - community.general.redis + +seealso: + - module: community.general.redis_info + - module: community.general.redis +''' + +EXAMPLES = ''' +- name: Get key foo=bar from loalhost with no username + community.general.redis_data_info: + login_host: localhost + login_password: supersecret + key: foo + +- name: Get key foo=bar on redishost with custom ca-cert file + community.general.redis_data_info: + login_host: redishost + login_password: supersecret + login_user: somuser + validate_certs: true + ssl_ca_certs: /path/to/ca/certs + key: foo +''' + +RETURN = ''' +exists: + description: If they key exists in the database. + returned: on success + type: bool +value: + description: Value key was set to. + returned: if existing + type: str + sample: 'value_of_some_key' +msg: + description: A short message. + returned: always + type: str + sample: 'Got key: foo with value: bar' +''' + +from ansible.module_utils.basic import AnsibleModule +from ansible_collections.community.general.plugins.module_utils.redis import ( + fail_imports, redis_auth_argument_spec, RedisAnsible) + + +def main(): + redis_auth_args = redis_auth_argument_spec() + module_args = dict( + key=dict(type='str', required=True, no_log=False), + ) + module_args.update(redis_auth_args) + + module = AnsibleModule( + argument_spec=module_args, + supports_check_mode=True, + ) + fail_imports(module) + + redis = RedisAnsible(module) + + key = module.params['key'] + result = {'changed': False} + + value = None + try: + value = redis.connection.get(key) + except Exception as e: + msg = 'Failed to get value of key "{0}" with exception: {1}'.format( + key, str(e)) + result['msg'] = msg + module.fail_json(**result) + + if value is None: + msg = 'Key "{0}" does not exist in database'.format(key) + result['exists'] = False + else: + msg = 'Got key "{0}"'.format(key) + result['value'] = value + result['exists'] = True + result['msg'] = msg + module.exit_json(**result) + + +if __name__ == '__main__': + main() diff --git a/plugins/modules/redis_data_info.py b/plugins/modules/redis_data_info.py new file mode 120000 index 0000000000..14c54fb2d3 --- /dev/null +++ b/plugins/modules/redis_data_info.py @@ -0,0 +1 @@ +database/misc/redis_data_info.py \ No newline at end of file diff --git a/tests/unit/plugins/modules/database/misc/test_redis_data_info.py b/tests/unit/plugins/modules/database/misc/test_redis_data_info.py new file mode 100644 index 0000000000..808c583e37 --- /dev/null +++ b/tests/unit/plugins/modules/database/misc/test_redis_data_info.py @@ -0,0 +1,113 @@ +# -*- coding: utf-8 -*- +# +# Copyright: (c) 2021, Andreas Botzner +# 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 + + +import pytest +import json +from redis import __version__ + +from ansible_collections.community.general.plugins.modules.database.misc import ( + redis_data_info) +from ansible_collections.community.general.tests.unit.plugins.modules.utils import set_module_args + + +HAS_REDIS_USERNAME_OPTION = True +if tuple(map(int, __version__.split('.'))) < (3, 4, 0): + HAS_REDIS_USERNAME_OPTION = False + + +def test_redis_data_info_without_arguments(capfd): + set_module_args({}) + with pytest.raises(SystemExit): + redis_data_info.main() + out, err = capfd.readouterr() + assert not err + assert json.loads(out)['failed'] + + +@pytest.mark.skipif(not HAS_REDIS_USERNAME_OPTION, reason="Redis version < 3.4.0") +def test_redis_data_info_existing_key(capfd, mocker): + set_module_args({'login_host': 'localhost', + 'login_user': 'root', + 'login_password': 'secret', + 'key': 'foo', + '_ansible_check_mode': False}) + mocker.patch('redis.Redis.get', return_value='bar') + with pytest.raises(SystemExit): + redis_data_info.main() + out, err = capfd.readouterr() + print(out) + assert not err + assert json.loads(out)['exists'] + assert json.loads(out)['value'] == 'bar' + + +@pytest.mark.skipif(not HAS_REDIS_USERNAME_OPTION, reason="Redis version < 3.4.0") +def test_redis_data_info_absent_key(capfd, mocker): + set_module_args({'login_host': 'localhost', + 'login_user': 'root', + 'login_password': 'secret', + 'key': 'foo', + '_ansible_check_mode': False}) + mocker.patch('redis.Redis.get', return_value=None) + with pytest.raises(SystemExit): + redis_data_info.main() + out, err = capfd.readouterr() + print(out) + assert not err + assert not json.loads(out)['exists'] + assert 'value' not in json.loads(out) + + +@pytest.mark.skipif(HAS_REDIS_USERNAME_OPTION, reason="Redis version > 3.4.0") +def test_redis_data_fail_username(capfd, mocker): + set_module_args({'login_host': 'localhost', + 'login_user': 'root', + 'login_password': 'secret', + 'key': 'foo', + '_ansible_check_mode': False}) + with pytest.raises(SystemExit): + redis_data_info.main() + out, err = capfd.readouterr() + print(out) + assert not err + assert json.loads(out)['failed'] + assert json.loads( + out)['msg'] == 'The option `username` in only supported with redis >= 3.4.0.' + + +@pytest.mark.skipif(HAS_REDIS_USERNAME_OPTION, reason="Redis version > 3.4.0") +def test_redis_data_info_absent_key_no_username(capfd, mocker): + set_module_args({'login_host': 'localhost', + 'login_password': 'secret', + 'key': 'foo', + '_ansible_check_mode': False}) + mocker.patch('redis.Redis.get', return_value=None) + with pytest.raises(SystemExit): + redis_data_info.main() + out, err = capfd.readouterr() + print(out) + assert not err + assert not json.loads(out)['exists'] + assert 'value' not in json.loads(out) + + +@pytest.mark.skipif(HAS_REDIS_USERNAME_OPTION, reason="Redis version > 3.4.0") +def test_redis_data_info_existing_key_no_username(capfd, mocker): + set_module_args({'login_host': 'localhost', + 'login_password': 'secret', + 'key': 'foo', + '_ansible_check_mode': False}) + mocker.patch('redis.Redis.get', return_value='bar') + with pytest.raises(SystemExit): + redis_data_info.main() + out, err = capfd.readouterr() + print(out) + assert not err + assert json.loads(out)['exists'] + assert json.loads(out)['value'] == 'bar'