diff --git a/.github/BOTMETA.yml b/.github/BOTMETA.yml index 7072bcdff8..451701dd2d 100644 --- a/.github/BOTMETA.yml +++ b/.github/BOTMETA.yml @@ -217,6 +217,8 @@ files: maintainers: Akasurde $lookups/random_string.py: maintainers: Akasurde + $lookups/random_words.py: + maintainers: konstruktoid $lookups/redis.py: maintainers: $team_ansible_core jpmens $lookups/shelvefile.py: {} diff --git a/plugins/lookup/random_words.py b/plugins/lookup/random_words.py new file mode 100644 index 0000000000..a2381aa38f --- /dev/null +++ b/plugins/lookup/random_words.py @@ -0,0 +1,119 @@ +# -*- coding: utf-8 -*- +# GNU General Public License v3.0+ (see COPYING or https://www.gnu.org/licenses/gpl-3.0.txt) + +"""The community.general.random_words Ansible lookup plugin.""" + +from __future__ import absolute_import, division, print_function + +__metaclass__ = type + +DOCUMENTATION = r""" + name: random_words + author: + - Thomas Sjögren (@konstruktoid) + short_description: Return a number of random words + version_added: "4.0.0" + requirements: + - xkcdpass U(https://github.com/redacted/XKCD-password-generator) + description: + - Returns a number of random words. The output can for example be used for + passwords. + - See U(https://xkcd.com/936/) for background. + options: + numwords: + description: + - The number of words. + default: 6 + type: int + min_length: + description: + - Minimum length of words to make password. + default: 5 + type: int + max_length: + description: + - Maximum length of words to make password. + default: 9 + type: int + delimiter: + description: + - The delimiter character between words. + default: " " + type: str + case: + description: + - The method for setting the case of each word in the passphrase. + choices: ["alternating", "upper", "lower", "random", "capitalize"] + default: "lower" + type: str +""" + +EXAMPLES = r""" +- name: Generate password with default settings + ansible.builtin.debug: + var: lookup('community.general.random_words') + # Example result: 'traitor gigabyte cesarean unless aspect clear' + +- name: Generate password with six, five character, words + ansible.builtin.debug: + var: lookup('community.general.random_words', min_length=5, max_length=5) + # Example result: 'brink banjo getup staff trump comfy' + +- name: Generate password with three capitalized words and the '-' delimiter + ansible.builtin.debug: + var: lookup('community.general.random_words', numwords=3, delimiter='-', case='capitalize') + # Example result: 'Overlabor-Faucet-Coastline' + +- name: Generate password with three words without any delimiter + ansible.builtin.debug: + var: lookup('community.general.random_words', numwords=3, delimiter='') + # Example result: 'deskworkmonopolystriking' + # https://www.ncsc.gov.uk/blog-post/the-logic-behind-three-random-words +""" + +RETURN = r""" + _raw: + description: A single-element list containing random words. + type: list + elements: str +""" + +from ansible.errors import AnsibleLookupError +from ansible.plugins.lookup import LookupBase + +try: + from xkcdpass import xkcd_password as xp + + HAS_XKCDPASS = True +except ImportError: + HAS_XKCDPASS = False + + +class LookupModule(LookupBase): + """The random_words Ansible lookup class.""" + + def run(self, terms, variables=None, **kwargs): + + if not HAS_XKCDPASS: + raise AnsibleLookupError( + "Python xkcdpass library is required. " + 'Please install using "pip install xkcdpass"' + ) + + self.set_options(var_options=variables, direct=kwargs) + method = self.get_option("case") + delimiter = self.get_option("delimiter") + max_length = self.get_option("max_length") + min_length = self.get_option("min_length") + numwords = self.get_option("numwords") + + words = xp.locate_wordfile() + wordlist = xp.generate_wordlist( + max_length=max_length, min_length=min_length, wordfile=words + ) + + values = xp.generate_xkcdpassword( + wordlist, case=method, delimiter=delimiter, numwords=numwords + ) + + return [values] diff --git a/tests/integration/targets/lookup_random_words/aliases b/tests/integration/targets/lookup_random_words/aliases new file mode 100644 index 0000000000..bc987654d9 --- /dev/null +++ b/tests/integration/targets/lookup_random_words/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_words/dependencies.yml b/tests/integration/targets/lookup_random_words/dependencies.yml new file mode 100644 index 0000000000..eef89942d7 --- /dev/null +++ b/tests/integration/targets/lookup_random_words/dependencies.yml @@ -0,0 +1,6 @@ +--- +- hosts: localhost + tasks: + - name: Install xkcdpass Python package + pip: + name: xkcdpass diff --git a/tests/integration/targets/lookup_random_words/runme.sh b/tests/integration/targets/lookup_random_words/runme.sh new file mode 100755 index 0000000000..afdff7bb9d --- /dev/null +++ b/tests/integration/targets/lookup_random_words/runme.sh @@ -0,0 +1,9 @@ +#!/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 dependencies.yml -v "$@" + +ANSIBLE_ROLES_PATH=../ \ + ansible-playbook test.yml -v "$@" diff --git a/tests/integration/targets/lookup_random_words/test.yml b/tests/integration/targets/lookup_random_words/test.yml new file mode 100644 index 0000000000..b33c14b605 --- /dev/null +++ b/tests/integration/targets/lookup_random_words/test.yml @@ -0,0 +1,28 @@ +--- +- hosts: localhost + gather_facts: false + tasks: + - name: Call random_words plugin + set_fact: + result1: "{{ query('community.general.random_words') }}" + result2: "{{ query('community.general.random_words', min_length=5, max_length=5) }}" + result3: "{{ query('community.general.random_words', delimiter='!') }}" + result4: "{{ query('community.general.random_words', numwords=3, delimiter='-', case='capitalize') }}" + result5: "{{ query('community.general.random_words', numwords=3, delimiter='') }}" + + - name: Check results + assert: + that: + - result1 | length == 1 + - result1[0] | length >= 35 + - result2 | length == 1 + - result2[0] | length == 35 + - result3 | length == 1 + - result3[0].count("!") == 5 + - result4 | length == 1 + - result4[0] | length >= 17 + - result4[0] | length <= 29 + - result4[0] | regex_findall("[A-Z]") | length == 3 + - result4[0].count("-") == 2 + - result5 | length == 1 + - result5[0] | length >= 18