diff --git a/.github/BOTMETA.yml b/.github/BOTMETA.yml index 9582aea4bf..7a2abf43bd 100644 --- a/.github/BOTMETA.yml +++ b/.github/BOTMETA.yml @@ -471,6 +471,8 @@ files: maintainers: paginabianca $modules/database/misc/redis_data.py: maintainers: paginabianca + $modules/database/misc/redis_data_incr.py: + maintainers: paginabianca $modules/database/misc/riak.py: maintainers: drewkerrigan jsmartin $modules/database/mssql/mssql_db.py: diff --git a/plugins/modules/database/misc/redis_data_incr.py b/plugins/modules/database/misc/redis_data_incr.py new file mode 100644 index 0000000000..008cd183e9 --- /dev/null +++ b/plugins/modules/database/misc/redis_data_incr.py @@ -0,0 +1,187 @@ +#!/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_incr +short_description: Increment keys in Redis +version_added: 4.0.0 +description: + - Increment integers or float keys in Redis database and get new value. + - Default increment for all keys is 1. For specific increments use the + I(increment_int) and I(increment_float) options. + - When using I(check_mode) the module will try to calculate the value that + Redis would return. If the key is not present, 0.0 is used as value. +author: "Andreas Botzner (@paginabianca)" +options: + key: + description: + - Database key. + type: str + required: true + increment_int: + description: + - Integer amount to increment the key by. + required: false + type: int + increment_float: + description: + - Float amount to increment the key by. + - This only works with keys that contain float values + in their string representation. + type: float + required: false + + +extends_documentation_fragment: + - community.general.redis.documentation + +notes: + - For C(check_mode) to work, the specified I(redis_user) needs permission to + run the C(GET) command on the key, otherwise the module will fail. + +seealso: + - module: community.general.redis_set + - module: community.general.redis_data_info + - module: community.general.redis +''' + +EXAMPLES = ''' +- name: Increment integer key foo on localhost with no username and print new value + community.general.redis_data_incr: + login_host: localhost + login_password: supersecret + key: foo + increment_int: 1 + register: result +- name: Print new value + debug: + var: result.value + +- name: Increment float key foo by 20.4 + community.general.redis_data_incr: + login_host: redishost + login_user: redisuser + login_password: somepass + key: foo + increment_float: '20.4' +''' + +RETURN = ''' +value: + description: Incremented value of key + returned: on success + type: float + sample: '4039.4' +msg: + description: A short message. + returned: always + type: str + sample: 'Incremented key: foo by 20.4 to 65.9' +''' + +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), + increment_int=dict(type='int', required=False), + increment_float=dict(type='float', required=False), + ) + module_args.update(redis_auth_args) + + module = AnsibleModule( + argument_spec=module_args, + supports_check_mode=True, + mutually_exclusive=[['increment_int', 'increment_float']], + ) + fail_imports(module) + + redis = RedisAnsible(module) + key = module.params['key'] + increment_float = module.params['increment_float'] + increment_int = module.params['increment_int'] + increment = 1 + if increment_float is not None: + increment = increment_float + elif increment_int is not None: + increment = increment_int + + result = {'changed': False} + if module.check_mode: + value = 0.0 + try: + res = redis.connection.get(key) + if res is not None: + value = float(res) + except ValueError as e: + msg = 'Value: {0} of key: {1} is not incrementable(int or float)'.format( + res, key) + result['msg'] = msg + module.fail_json(**result) + 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) + msg = 'Incremented key: {0} by {1} to {2}'.format( + key, increment, value + increment) + result['msg'] = msg + result['value'] = float(value + increment) + module.exit_json(**result) + + if increment_float is not None: + try: + value = redis.connection.incrbyfloat(key, increment) + msg = 'Incremented key: {0} by {1} to {2}'.format( + key, increment, value) + result['msg'] = msg + result['value'] = float(value) + result['changed'] = True + module.exit_json(**result) + except Exception as e: + msg = 'Failed to increment key: {0} by {1} with exception: {2}'.format( + key, increment, str(e)) + result['msg'] = msg + module.fail_json(**result) + elif increment_int is not None: + try: + value = redis.connection.incrby(key, increment) + msg = 'Incremented key: {0} by {1} to {2}'.format( + key, increment, value) + result['msg'] = msg + result['value'] = float(value) + result['changed'] = True + module.exit_json(**result) + except Exception as e: + msg = 'Failed to increment key: {0} by {1} with exception: {2}'.format( + key, increment, str(e)) + result['msg'] = msg + module.fail_json(**result) + else: + try: + value = redis.connection.incr(key) + msg = 'Incremented key: {0} to {1}'.format(key, value) + result['msg'] = msg + result['value'] = float(value) + result['changed'] = True + module.exit_json(**result) + except Exception as e: + msg = 'Failed to increment key: {0} with exception: {1}'.format( + key, str(e)) + result['msg'] = msg + module.fail_json(**result) + + +if __name__ == '__main__': + main() diff --git a/plugins/modules/redis_data_incr.py b/plugins/modules/redis_data_incr.py new file mode 120000 index 0000000000..07d54aa8af --- /dev/null +++ b/plugins/modules/redis_data_incr.py @@ -0,0 +1 @@ +./database/misc/redis_data_incr.py \ No newline at end of file diff --git a/tests/unit/plugins/modules/database/misc/test_redis_data_incr.py b/tests/unit/plugins/modules/database/misc/test_redis_data_incr.py new file mode 100644 index 0000000000..be7ebfbdfb --- /dev/null +++ b/tests/unit/plugins/modules/database/misc/test_redis_data_incr.py @@ -0,0 +1,207 @@ +# -*- 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 +import redis +from redis import __version__ + +from ansible_collections.community.general.plugins.modules.database.misc import redis_data_incr +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 +if HAS_REDIS_USERNAME_OPTION: + from redis.exceptions import NoPermissionError, RedisError, ResponseError + + +def test_redis_data_incr_without_arguments(capfd): + set_module_args({}) + with pytest.raises(SystemExit) as results: + redis_data_incr.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_incr(capfd, mocker): + set_module_args({'login_host': 'localhost', + 'login_user': 'root', + 'login_password': 'secret', + 'key': 'foo', }) + mocker.patch('redis.Redis.incr', return_value=57) + with pytest.raises(SystemExit): + redis_data_incr.main() + out, err = capfd.readouterr() + print(out) + assert not err + assert json.loads(out)['value'] == 57.0 + assert json.loads( + out)['msg'] == 'Incremented key: foo to 57' + assert json.loads(out)['changed'] + + +@pytest.mark.skipif(not HAS_REDIS_USERNAME_OPTION, reason="Redis version < 3.4.0") +def test_redis_data_incr_int(capfd, mocker): + set_module_args({'login_host': 'localhost', + 'login_user': 'root', + 'login_password': 'secret', + 'key': 'foo', + 'increment_int': 10}) + mocker.patch('redis.Redis.incrby', return_value=57) + with pytest.raises(SystemExit): + redis_data_incr.main() + out, err = capfd.readouterr() + print(out) + assert not err + assert json.loads(out)['value'] == 57.0 + assert json.loads( + out)['msg'] == 'Incremented key: foo by 10 to 57' + assert json.loads(out)['changed'] + + +@pytest.mark.skipif(not HAS_REDIS_USERNAME_OPTION, reason="Redis version < 3.4.0") +def test_redis_data_inc_float(capfd, mocker): + set_module_args({'login_host': 'localhost', + 'login_user': 'root', + 'login_password': 'secret', + 'key': 'foo', + 'increment_float': '5.5'}) + mocker.patch('redis.Redis.incrbyfloat', return_value=57.45) + with pytest.raises(SystemExit): + redis_data_incr.main() + out, err = capfd.readouterr() + print(out) + assert not err + assert json.loads(out)['value'] == 57.45 + assert json.loads( + out)['msg'] == 'Incremented key: foo by 5.5 to 57.45' + assert json.loads(out)['changed'] + + +@pytest.mark.skipif(not HAS_REDIS_USERNAME_OPTION, reason="Redis version < 3.4.0") +def test_redis_data_incr_float_wrong_value(capfd): + set_module_args({'login_host': 'localhost', + 'login_user': 'root', + 'login_password': 'secret', + 'key': 'foo', + 'increment_float': 'not_a_number'}) + with pytest.raises(SystemExit): + redis_data_incr.main() + out, err = capfd.readouterr() + print(out) + assert not err + assert json.loads(out)['failed'] + + +@pytest.mark.skipif(HAS_REDIS_USERNAME_OPTION, reason="Redis version > 3.4.0") +def test_redis_data_incr_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_incr.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.' + + +def test_redis_data_incr_no_username(capfd, mocker): + set_module_args({'login_host': 'localhost', + 'login_password': 'secret', + 'key': 'foo', }) + mocker.patch('redis.Redis.incr', return_value=57) + with pytest.raises(SystemExit): + redis_data_incr.main() + out, err = capfd.readouterr() + print(out) + assert not err + assert json.loads(out)['value'] == 57.0 + assert json.loads( + out)['msg'] == 'Incremented key: foo to 57' + assert json.loads(out)['changed'] + + +def test_redis_data_incr_float_no_username(capfd, mocker): + set_module_args({'login_host': 'localhost', + 'login_password': 'secret', + 'key': 'foo', + 'increment_float': '5.5'}) + mocker.patch('redis.Redis.incrbyfloat', return_value=57.45) + with pytest.raises(SystemExit): + redis_data_incr.main() + out, err = capfd.readouterr() + print(out) + assert not err + assert json.loads(out)['value'] == 57.45 + assert json.loads( + out)['msg'] == 'Incremented key: foo by 5.5 to 57.45' + assert json.loads(out)['changed'] + + +def test_redis_data_incr_check_mode(capfd, mocker): + set_module_args({'login_host': 'localhost', + 'login_password': 'secret', + 'key': 'foo', + '_ansible_check_mode': True}) + mocker.patch('redis.Redis.get', return_value=10) + with pytest.raises(SystemExit): + redis_data_incr.main() + out, err = capfd.readouterr() + print(out) + assert not err + assert json.loads(out)['value'] == 11.0 + assert json.loads(out)['msg'] == 'Incremented key: foo by 1 to 11.0' + assert not json.loads(out)['changed'] + + +def test_redis_data_incr_check_mode_not_incrementable(capfd, mocker): + set_module_args({'login_host': 'localhost', + 'login_password': 'secret', + 'key': 'foo', + '_ansible_check_mode': True}) + mocker.patch('redis.Redis.get', return_value='bar') + with pytest.raises(SystemExit): + redis_data_incr.main() + out, err = capfd.readouterr() + print(out) + assert not err + assert json.loads(out)['failed'] + assert json.loads(out)[ + 'msg'] == "Value: bar of key: foo is not incrementable(int or float)" + assert 'value' not in json.loads(out) + assert not json.loads(out)['changed'] + + +@pytest.mark.skipif(not HAS_REDIS_USERNAME_OPTION, reason="Redis version < 3.4.0") +def test_redis_data_incr_check_mode_permissions(capfd, mocker): + set_module_args({'login_host': 'localhost', + 'login_password': 'secret', + 'key': 'foo', + '_ansible_check_mode': True}) + redis.Redis.get = mocker.Mock(side_effect=NoPermissionError( + "this user has no permissions to run the 'get' command or its subcommand")) + with pytest.raises(SystemExit): + redis_data_incr.main() + out, err = capfd.readouterr() + print(out) + assert not err + assert json.loads(out)['failed'] + assert json.loads(out)['msg'].startswith( + 'Failed to get value of key: foo with exception:') + assert 'value' not in json.loads(out) + assert not json.loads(out)['changed']