From 43c12b82fa9cf63d2258565f1d62d5dc0a0075ff Mon Sep 17 00:00:00 2001 From: Abhijeet Kasurde Date: Thu, 27 May 2021 22:39:26 +0530 Subject: [PATCH] random_string: a new lookup plugin (#2572) New lookup plugin to generate random string based upon constraints. Signed-off-by: Abhijeet Kasurde --- plugins/lookup/random_string.py | 220 ++++++++++++++++++ .../targets/lookup_random_string/aliases | 3 + .../targets/lookup_random_string/runme.sh | 6 + .../targets/lookup_random_string/test.yml | 48 ++++ 4 files changed, 277 insertions(+) create mode 100644 plugins/lookup/random_string.py create mode 100644 tests/integration/targets/lookup_random_string/aliases create mode 100755 tests/integration/targets/lookup_random_string/runme.sh create mode 100644 tests/integration/targets/lookup_random_string/test.yml diff --git a/plugins/lookup/random_string.py b/plugins/lookup/random_string.py new file mode 100644 index 0000000000..6a05cfd041 --- /dev/null +++ b/plugins/lookup/random_string.py @@ -0,0 +1,220 @@ +# -*- coding: utf-8 -*- +# Copyright: (c) 2021, Abhijeet Kasurde +# Copyright: (c) 2018, Ansible Project +# 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 = r""" + name: random_string + author: + - Abhijeet Kasurde (@Akasurde) + short_description: Generates random string + version_added: '3.2.0' + description: + - Generates random string based upon the given constraints. + options: + length: + description: The length of the string. + default: 8 + type: int + upper: + description: + - Include uppercase letters in the string. + default: true + type: bool + lower: + description: + - Include lowercase letters in the string. + default: true + type: bool + numbers: + description: + - Include numbers in the string. + default: true + type: bool + special: + description: + - Include special characters in the string. + - Special characters are taken from Python standard library C(string). + See L(the documentation of string.punctuation,https://docs.python.org/3/library/string.html#string.punctuation) + for which characters will be used. + - The choice of special characters can be changed to setting I(override_special). + default: true + type: bool + min_numeric: + description: + - Minimum number of numeric characters in the string. + - If set, overrides I(numbers=false). + default: 0 + type: int + min_upper: + description: + - Minimum number of uppercase alphabets in the string. + - If set, overrides I(upper=false). + default: 0 + type: int + min_lower: + description: + - Minimum number of lowercase alphabets in the string. + - If set, overrides I(lower=false). + default: 0 + type: int + min_special: + description: + - Minimum number of special character in the string. + default: 0 + type: int + override_special: + description: + - Overide a list of special characters to use in the string. + - If set I(min_special) should be set to a non-default value. + type: str + override_all: + description: + - Override all values of I(numbers), I(upper), I(lower), and I(special) with + the given list of characters. + type: str + base64: + description: + - Returns base64 encoded string. + type: bool + default: false +""" + +EXAMPLES = r""" +- name: Generate random string + ansible.builtin.debug: + var: lookup('community.general.random_string') + # Example result: ['DeadBeeF'] + +- name: Generate random string with length 12 + ansible.builtin.debug: + var: lookup('community.general.random_string', length=12) + # Example result: ['Uan0hUiX5kVG'] + +- name: Generate base64 encoded random string + ansible.builtin.debug: + var: lookup('community.general.random_string', base64=True) + # Example result: ['NHZ6eWN5Qk0='] + +- name: Generate a random string with 1 lower, 1 upper, 1 number and 1 special char (atleast) + ansible.builtin.debug: + var: lookup('community.general.random_string', min_lower=1, min_upper=1, min_special=1, min_numeric=1) + # Example result: ['&Qw2|E[-'] + +- name: Generate a random string with all lower case characters + debug: + var: query('community.general.random_string', upper=false, numbers=false, special=false) + # Example result: ['exolxzyz'] + +- name: Generate random hexadecimal string + debug: + var: query('community.general.random_string', upper=false, lower=false, override_special=hex_chars, numbers=false) + vars: + hex_chars: '0123456789ABCDEF' + # Example result: ['D2A40737'] + +- name: Generate random hexadecimal string with override_all + debug: + var: query('community.general.random_string', override_all=hex_chars) + vars: + hex_chars: '0123456789ABCDEF' + # Example result: ['D2A40737'] +""" + +RETURN = r""" + _raw: + description: A one-element list containing a random string + type: list + elements: str +""" + +import base64 +import random +import string + +from ansible.errors import AnsibleLookupError +from ansible.plugins.lookup import LookupBase +from ansible.module_utils._text import to_bytes, to_text + + +class LookupModule(LookupBase): + @staticmethod + def get_random(random_generator, chars, length): + if not chars: + raise AnsibleLookupError( + "Available characters cannot be None, please change constraints" + ) + return "".join(random_generator.choice(chars) for dummy in range(length)) + + @staticmethod + def b64encode(string_value, encoding="utf-8"): + return to_text( + base64.b64encode( + to_bytes(string_value, encoding=encoding, errors="surrogate_or_strict") + ) + ) + + def run(self, terms, variables=None, **kwargs): + number_chars = string.digits + lower_chars = string.ascii_lowercase + upper_chars = string.ascii_uppercase + special_chars = string.punctuation + random_generator = random.SystemRandom() + + self.set_options(var_options=variables, direct=kwargs) + + length = self.get_option("length") + base64_flag = self.get_option("base64") + override_all = self.get_option("override_all") + values = "" + available_chars_set = "" + + if override_all: + # Override all the values + available_chars_set = override_all + else: + upper = self.get_option("upper") + lower = self.get_option("lower") + numbers = self.get_option("numbers") + special = self.get_option("special") + override_special = self.get_option("override_special") + + if override_special: + special_chars = override_special + + if upper: + available_chars_set += upper_chars + if lower: + available_chars_set += lower_chars + if numbers: + available_chars_set += number_chars + if special: + available_chars_set += special_chars + + mapping = { + "min_numeric": number_chars, + "min_lower": lower_chars, + "min_upper": upper_chars, + "min_special": special_chars, + } + + for m in mapping: + if self.get_option(m): + values += self.get_random(random_generator, mapping[m], self.get_option(m)) + + remaining_pass_len = length - len(values) + values += self.get_random(random_generator, available_chars_set, remaining_pass_len) + + # Get pseudo randomization + shuffled_values = list(values) + # Randomize the order + random.shuffle(shuffled_values) + + if base64_flag: + return [self.b64encode("".join(shuffled_values))] + + return ["".join(shuffled_values)] diff --git a/tests/integration/targets/lookup_random_string/aliases b/tests/integration/targets/lookup_random_string/aliases new file mode 100644 index 0000000000..bc987654d9 --- /dev/null +++ b/tests/integration/targets/lookup_random_string/aliases @@ -0,0 +1,3 @@ +shippable/posix/group2 +skip/aix +skip/python2.6 # lookups are controller only, and we no longer support Python 2.6 on the controller diff --git a/tests/integration/targets/lookup_random_string/runme.sh b/tests/integration/targets/lookup_random_string/runme.sh new file mode 100755 index 0000000000..8ed6373823 --- /dev/null +++ b/tests/integration/targets/lookup_random_string/runme.sh @@ -0,0 +1,6 @@ +#!/usr/bin/env bash +# GNU General Public License v3.0+ (see COPYING or https://www.gnu.org/licenses/gpl-3.0.txt) +set -eux + +ANSIBLE_ROLES_PATH=../ \ + ansible-playbook test.yml -v "$@" diff --git a/tests/integration/targets/lookup_random_string/test.yml b/tests/integration/targets/lookup_random_string/test.yml new file mode 100644 index 0000000000..52a572379b --- /dev/null +++ b/tests/integration/targets/lookup_random_string/test.yml @@ -0,0 +1,48 @@ +- hosts: localhost + gather_facts: no + tasks: + - name: Call plugin + set_fact: + result1: "{{ query('community.general.random_string') }}" + result2: "{{ query('community.general.random_string', length=0) }}" + result3: "{{ query('community.general.random_string', length=10) }}" + result4: "{{ query('community.general.random_string', length=-1) }}" + result5: "{{ query('community.general.random_string', override_special='_', min_special=1) }}" + result6: "{{ query('community.general.random_string', upper=false, special=false) }}" # lower case only + result7: "{{ query('community.general.random_string', lower=false) }}" # upper case only + result8: "{{ query('community.general.random_string', lower=false, upper=false, special=false) }}" # number only + result9: "{{ query('community.general.random_string', lower=false, upper=false, special=false, min_numeric=1, length=1) }}" # single digit only + result10: "{{ query('community.general.random_string', numbers=false, upper=false, special=false, min_lower=1, length=1) }}" # single lowercase character only + result11: "{{ query('community.general.random_string', base64=true, length=8) }}" + result12: "{{ query('community.general.random_string', upper=false, numbers=false, special=false) }}" # all lower case + result13: "{{ query('community.general.random_string', override_all='0', length=2) }}" + + - name: Raise error when impossible constraints are provided + set_fact: + impossible: "{{ query('community.general.random_string', upper=false, lower=false, special=false, numbers=false) }}" + ignore_errors: yes + register: impossible_result + + - name: Check results + assert: + that: + - result1[0] | length == 8 + - result2[0] | length == 0 + - result3[0] | length == 10 + - result4[0] | length == 0 + - result5[0] | length == 8 + - "'_' in result5[0]" + - result6[0] is lower + - result7[0] is upper + - result8[0] | regex_replace('^(\d+)$', '') == '' + - result9[0] | regex_replace('^(\d+)$', '') == '' + - result9[0] | length == 1 + - result10[0] | length == 1 + - result10[0] is lower + # if input string is not multiple of 3, base64 encoded string will be padded with = + - result11[0].endswith('=') + - result12[0] is lower + - result13[0] | length == 2 + - result13[0] == '00' + - impossible_result is failed + - "'Available characters cannot' in impossible_result.msg"