diff --git a/.config/pep8.cfg b/.config/pep8.cfg deleted file mode 100644 index 3faf701d..00000000 --- a/.config/pep8.cfg +++ /dev/null @@ -1,4 +0,0 @@ -[pep8] -ignore = E402, E123 -# It's fine to have line-length of 99 -max-line-length = 99 diff --git a/.github/workflows/test_elasticsearch_modules.yml b/.github/workflows/test_elasticsearch_modules.yml deleted file mode 100644 index d1ea2675..00000000 --- a/.github/workflows/test_elasticsearch_modules.yml +++ /dev/null @@ -1,71 +0,0 @@ ---- -name: Test Elasticsearch modules -on: - workflow_dispatch: - inputs: - logLevel: - description: 'Log level' - required: true - default: 'warning' - type: choice - options: - - info - - warning - - debug - pull_request: - paths: - - '.github/workflows/test_elasticsearch_modules.yml' - - 'molecule/elasticsearch_test_modules/*' - -jobs: - molecule_elasticsearch_modules: - runs-on: ubuntu-latest - - env: - COLLECTION_NAMESPACE: netways - COLLECTION_NAME: elasticstack - - strategy: - fail-fast: false - matrix: - distro: - - ubuntu2204 - scenario: - - elasticsearch_test_modules - release: - - 8 - python_version: - - "3.11" - ansible_version: - - "ansible-core>=2.19,<2.20" #Correspond ansible>=12.0,<13.0 - - steps: - - name: Check out code - uses: actions/checkout@v6 - - - name: Set up Python ${{ matrix.python_version }} - uses: actions/setup-python@v6 - with: - python-version: ${{ matrix.python_version }} - - - name: Install dependencies - run: | - python3 -m pip install --upgrade pip - python3 -m pip install "${{ matrix.ansible_version }}" - python3 -m pip install -r requirements-test.txt - - - name: Install collection - run: | - mkdir -p ~/.ansible/collections/ansible_collections/$COLLECTION_NAMESPACE - cp -a ../ansible-collection-$COLLECTION_NAME ~/.ansible/collections/ansible_collections/$COLLECTION_NAMESPACE/$COLLECTION_NAME - - - name: Test with molecule - run: | - ansible --version - molecule --version - molecule test -s ${{ matrix.scenario }} - env: - MOLECULE_DISTRO: ${{ matrix.distro }} - PY_COLORS: '1' - ANSIBLE_FORCE_COLOR: '1' - ELASTIC_RELEASE: ${{ matrix.release }} diff --git a/.github/workflows/test_plugins.yml b/.github/workflows/test_plugins.yml index 743f9600..0045ca84 100644 --- a/.github/workflows/test_plugins.yml +++ b/.github/workflows/test_plugins.yml @@ -14,195 +14,224 @@ on: - debug pull_request: branches: - - 'feature/**' - - 'fix/**' - - '!doc/**' + - 'main' paths: - 'plugins/**' - - '.github/workflows/test_plugins.yml' - - 'molecule/plugins/**' - - '.config/pep8.yml' - 'tests/**' + - 'molecule/plugins/**' + - '.github/workflows/test_plugins.yml' jobs: - pep8: + sanity_ansible_18_19: runs-on: ubuntu-latest + env: + COLLECTION_NAMESPACE: netways + COLLECTION_NAME: elasticstack + strategy: + fail-fast: false + matrix: + python_version: + - "3.11" + - "3.12" + - "3.13" + ansible_version: + - "ansible-core>=2.18,<2.19" #Correspond ansible>=11.0,<12.0 + - "ansible-core>=2.19,<2.20" #Correspond ansible>=12.0,<13.0 steps: - - name: Check out the codebase. + - name: Check out code uses: actions/checkout@v6 - - name: Set up Python 3. + - name: Set up Python ${{ matrix.python_version }} uses: actions/setup-python@v6 with: - python-version: '3.x' + python-version: ${{ matrix.python_version }} - - name: Install test dependencies. + - name: Install Ansible run: | python3 -m pip install --upgrade pip - python3 -m pip install pep8 + python3 -m pip install "${{ matrix.ansible_version }}" - - name: Lint code. + - name: Install collection run: | - pep8 plugins/ --config=.config/pep8.cfg --statistics --count + mkdir -p ~/.ansible/collections/ansible_collections/$COLLECTION_NAMESPACE + cp -a ../ansible-collection-$COLLECTION_NAME ~/.ansible/collections/ansible_collections/$COLLECTION_NAMESPACE/$COLLECTION_NAME - unit-test: - needs: pep8 - runs-on: ubuntu-20.04 + - name: Run sanity tests + run: | + cd ~/.ansible/collections/ansible_collections/$COLLECTION_NAMESPACE/$COLLECTION_NAME + ansible-test sanity --python "${{ matrix.python_version }}" -v + env: + PY_COLORS: '1' + ANSIBLE_FORCE_COLOR: '1' + sanity_ansible_20: + runs-on: ubuntu-latest env: COLLECTION_NAMESPACE: netways COLLECTION_NAME: elasticstack - strategy: fail-fast: false - + matrix: + python_version: + - "3.12" + - "3.13" + - "3.14" + ansible_version: + - "ansible-core>=2.20,<2.21" #Correspond ansible>=13.0,<14.0 steps: - name: Check out code uses: actions/checkout@v6 - - name: Set up Python 3.9.14 + - name: Set up Python ${{ matrix.python_version }} uses: actions/setup-python@v6 with: - python-version: 3.9.14 + python-version: ${{ matrix.python_version }} - - name: Install dependencies + - name: Install Ansible run: | - python -m pip install --upgrade pip - python -m pip install install ansible + python3 -m pip install --upgrade pip + python3 -m pip install "${{ matrix.ansible_version }}" - name: Install collection run: | mkdir -p ~/.ansible/collections/ansible_collections/$COLLECTION_NAMESPACE cp -a ../ansible-collection-$COLLECTION_NAME ~/.ansible/collections/ansible_collections/$COLLECTION_NAMESPACE/$COLLECTION_NAME - - name: Test `cert_info` module + - name: Run sanity tests run: | - python tests/unit/plugins/modules/test_cert_info.py + cd ~/.ansible/collections/ansible_collections/$COLLECTION_NAMESPACE/$COLLECTION_NAME + ansible-test sanity --python "${{ matrix.python_version }}" -v env: PY_COLORS: '1' ANSIBLE_FORCE_COLOR: '1' - - name: Test `certs` module util - run: | - python tests/unit/plugins/module_utils/test_certs.py - env: - PY_COLORS: '1' - ANSIBLE_FORCE_COLOR: '1' - - python: - needs: unit-test - runs-on: ubuntu-20.04 - + unit-test: + needs: + - sanity_ansible_18_19 + - sanity_ansible_20 + runs-on: ubuntu-latest env: COLLECTION_NAMESPACE: netways COLLECTION_NAME: elasticstack - strategy: fail-fast: false - matrix: - python_version: [ 3.5.10, 3.6.15, 3.7.13, 3.8.16, 3.10.10 ] - steps: - name: Check out code uses: actions/checkout@v6 - - name: Set up Python ${{ matrix.python_version }} + - name: Set up Python 3.11 uses: actions/setup-python@v6 with: - python-version: ${{ matrix.python_version }} + python-version: 3.11 - name: Install dependencies run: | - python -m pip install --upgrade pip - python -m pip install install ansible + python3 -m pip install --upgrade pip + python3 -m pip install "ansible-core>=2.19,<2.20" - name: Install collection run: | mkdir -p ~/.ansible/collections/ansible_collections/$COLLECTION_NAMESPACE cp -a ../ansible-collection-$COLLECTION_NAME ~/.ansible/collections/ansible_collections/$COLLECTION_NAMESPACE/$COLLECTION_NAME - - name: Test with ansible-playbook + - name: Test `cert_info` module run: | - ansible-playbook molecule/plugins/converge.yml + PYTHONPATH=$HOME/.ansible/collections python3 tests/unit/plugins/modules/test_cert_info.py env: PY_COLORS: '1' ANSIBLE_FORCE_COLOR: '1' - ansible-core: - needs: python - runs-on: ubuntu-20.04 + - name: Test `certs` module util + run: | + PYTHONPATH=$HOME/.ansible/collections python3 tests/unit/plugins/module_utils/test_certs.py + env: + PY_COLORS: '1' + ANSIBLE_FORCE_COLOR: '1' + molecule_plugins: + needs: unit-test + runs-on: ubuntu-latest env: COLLECTION_NAMESPACE: netways COLLECTION_NAME: elasticstack - strategy: fail-fast: false matrix: - ansible_core_version: [ 2.11.12, 2.12.10, 2.13.8, 2.14.4 ] - + distro: + - ubuntu2204 + scenario: + - plugins + release: + - 8 + # - 9 # add when elasticsearch>=9 is supported by this collection steps: - name: Check out code uses: actions/checkout@v6 - - name: Set up Python 3.9.14 + - name: Set up Python 3.11 uses: actions/setup-python@v6 with: - python-version: 3.9.14 + python-version: "3.11" - name: Install dependencies run: | - python -m pip install --upgrade pip - python -m pip install install ansible-core==${{ matrix.ansible_core_version }} + python3 -m pip install --upgrade pip + python3 -m pip install "ansible-core>=2.19,<2.20" + python3 -m pip install -r requirements-test.txt - name: Install collection run: | mkdir -p ~/.ansible/collections/ansible_collections/$COLLECTION_NAMESPACE cp -a ../ansible-collection-$COLLECTION_NAME ~/.ansible/collections/ansible_collections/$COLLECTION_NAMESPACE/$COLLECTION_NAME - - name: Test with ansible-playbook + - name: Test with molecule run: | - ansible-playbook molecule/plugins/converge.yml + ansible --version + molecule --version + molecule test -s ${{ matrix.scenario }} env: + MOLECULE_DISTRO: ${{ matrix.distro }} PY_COLORS: '1' ANSIBLE_FORCE_COLOR: '1' + ELASTIC_RELEASE: ${{ matrix.release }} python-cryptography: - needs: ansible-core - runs-on: ubuntu-20.04 - + needs: unit-test + runs-on: ubuntu-latest env: COLLECTION_NAMESPACE: netways COLLECTION_NAME: elasticstack - strategy: fail-fast: false matrix: - python_cryptography_version: [ 2.5, 3.0, 3.1, 3.2, 3.3, 3.4, 35.0.0, 36.0.0, 38.0.0, 40.0.1] - + python_cryptography_version: + - "3.4.8" # Test loading certificate with 3 parameter (old way). + - "41.0.7" # Last version before not_valid_after() and not_valid_before() deprecation. + - "42.0.0" # First release with deprecation https://cryptography.io/en/latest/changelog/ + - "46.0.5" # Latest release steps: - name: Check out code uses: actions/checkout@v6 - - name: Set up Python 3.9.14 + - name: Set up Python 3.11 uses: actions/setup-python@v6 with: - python-version: 3.9.14 + python-version: 3.11 - name: Install dependencies run: | - python -m pip install --upgrade pip - python -m pip install install cryptography==${{ matrix.python_cryptography_version }} - python -m pip install install ansible + python3 -m pip install --upgrade pip + python3 -m pip install "ansible-core>=2.19,<2.20" + python3 -m pip install "cryptography==${{ matrix.python_cryptography_version }}" - name: Install collection run: | mkdir -p ~/.ansible/collections/ansible_collections/$COLLECTION_NAMESPACE cp -a ../ansible-collection-$COLLECTION_NAME ~/.ansible/collections/ansible_collections/$COLLECTION_NAMESPACE/$COLLECTION_NAME - - name: Test with ansible-playbook + - name: Test cert_info with cryptography ${{ matrix.python_cryptography_version }} run: | - ansible-playbook molecule/plugins/converge.yml + PYTHONPATH=$HOME/.ansible/collections python3 tests/unit/plugins/modules/test_cert_info.py env: PY_COLORS: '1' ANSIBLE_FORCE_COLOR: '1' diff --git a/docs/role-elasticsearch.md b/docs/role-elasticsearch.md index 3676a644..1be8ce3b 100644 --- a/docs/role-elasticsearch.md +++ b/docs/role-elasticsearch.md @@ -51,7 +51,7 @@ This variable activates a workaround to start on systems that have certain harde * *elasticsearch_ssl_verification_mode*: Defines how to verify the certificates presented by another party in the TLS connection * *elasticsearch_transport_port*: The port to bind for communication between nodes * *elasticsearch_seed_hosts*: Set elasticsearch seed hosts -* *elasticsearch_security_enrollment*: Controls enrollment (of nodes and Kibana) to a local node that’s been autoconfigured for security. +* *elasticsearch_security_enrollment*: Controls enrollment (of nodes and Kibana) to a local node that's been autoconfigured for security. The following variable was only integrated to speed up upgrades of non-production clusters. Use with caution and at your own risk: diff --git a/molecule/elasticsearch_test_modules/converge.yml b/molecule/elasticsearch_test_modules/converge.yml deleted file mode 100644 index 77ffdd66..00000000 --- a/molecule/elasticsearch_test_modules/converge.yml +++ /dev/null @@ -1,64 +0,0 @@ ---- -# The workaround for arbitrarily named role directory is important because the git repo has one name and the role within it another -# Found at: https://github.com/ansible-community/molecule/issues/1567#issuecomment-436876722 -- name: Converge - collections: - - netways.elasticstack - hosts: all - vars: - elasticstack_full_stack: false - elasticsearch_jna_workaround: true - elasticsearch_disable_systemcallfilterchecks: true - elasticstack_release: "{{ lookup('env', 'ELASTIC_RELEASE') | int}}" - elasticsearch_heap: "1" - elasticstack_no_log: false - tasks: - - name: Include Elastics repos role - ansible.builtin.include_role: - name: repos - - name: Include Elasticsearch - ansible.builtin.include_role: - name: elasticsearch - - - name: Fetch Elastic password # noqa: risky-shell-pipe - ansible.builtin.shell: > - if test -n "$(ps -p $$ | grep bash)"; then set -o pipefail; fi; - grep "PASSWORD elastic" /usr/share/elasticsearch/initial_passwords | - awk {' print $4 '} - register: elasticstack_password - changed_when: false - - - name: Create elasticsearch role 'new-role' - netways.elasticstack.elasticsearch_role: - name: new-role1 - cluster: - - manage_own_api_key - - delegate_pki - indicies: - - names: - - foobar321 - privileges: - - read - - write - state: present - host: https://localhost:9200 - auth_user: elastic - auth_pass: "{{ elasticstack_password.stdout }}" - verify_certs: false - - - name: Create elasticsearch user 'new-user' - netways.elasticstack.elasticsearch_user: - name: new-user1 - fullname: New User - password: "{{ lookup('community.general.random_string', length=12, min_lower=1, min_upper=1, min_numeric=1, min_special=1, override_special='-_=!') }}" - email: new@user.de - roles: - - new-role1 - - logstash-writer - enabled: true - state: present - host: https://localhost:9200 - auth_user: elastic - auth_pass: "{{ elasticstack_password.stdout }}" - verify_certs: false - ca_certs: /etc/elasticsearch/certs/http_ca.crt diff --git a/molecule/plugins/converge.yml b/molecule/plugins/converge.yml index 633082d5..13946437 100644 --- a/molecule/plugins/converge.yml +++ b/molecule/plugins/converge.yml @@ -1,44 +1,68 @@ --- -# The workaround for arbitrarily named role directory is important because the git repo has one name and the role within it another -# Found at: https://github.com/ansible-community/molecule/issues/1567#issuecomment-436876722 - name: Converge collections: - netways.elasticstack - hosts: localhost + hosts: all + vars: + elasticstack_full_stack: false + elasticsearch_jna_workaround: true + elasticsearch_disable_systemcallfilterchecks: true + elasticstack_release: "{{ lookup('env', 'ELASTIC_RELEASE') | int }}" + elasticsearch_heap: "1" + elasticstack_no_log: false tasks: - # - # Test modules - # - - name: Test - cert_info: - path: files/es-ca/elastic-stack-ca.p12 - passphrase: PleaseChangeMe - register: test - - name: Debug - ansible.builtin.debug: - msg: "{{ test }}" - - name: Test required parameters (missing path) - cert_info: - passphrase: PleaseChangeMe - ignore_errors: true - - name: Test wrong path - cert_info: - path: es-ca-wrong - passphrase: PleaseChangeMe - ignore_errors: true - - name: Debug with to_datetime() - (( test.not_valid_after | to_datetime()) - (ansible_date_time.date | to_datetime('%Y-%m-%d'))).days - ansible.builtin.debug: - msg: >- - "{{ (( test.not_valid_after | to_datetime()) - (ansible_date_time.date | to_datetime('%Y-%m-%d'))).days }}" - - name: Test wrong passphrase - cert_info: - path: files/es-ca/elastic-stack-ca.p12 - passphrase: PleaseChangeMe-wrong - ignore_errors: true - - name: Test no passphrase - cert_info: - path: files/es-ca/elastic-stack-ca.p12 - ignore_errors: true - - name: Test no parameters - cert_info: - ignore_errors: true + - name: Include Elastic repos role + ansible.builtin.include_role: + name: repos + + - name: Include Elasticsearch + ansible.builtin.include_role: + name: elasticsearch + + - name: Fetch Elastic password # noqa: risky-shell-pipe + ansible.builtin.shell: > + if test -n "$(ps -p $$ | grep bash)"; then set -o pipefail; fi; + grep "PASSWORD elastic" /usr/share/elasticsearch/initial_passwords | + awk {' print $4 '} + register: elasticstack_password + changed_when: false + + - name: Create Elasticsearch role + netways.elasticstack.elasticsearch_role: + name: test-role + cluster: + - manage_own_api_key + - delegate_pki + indicies: + - names: + - test-index + privileges: + - read + - write + state: present + host: https://localhost:9200 + auth_user: elastic + auth_pass: "{{ elasticstack_password.stdout }}" + verify_certs: false + + - name: Create Elasticsearch user + netways.elasticstack.elasticsearch_user: + name: test-user + fullname: Test User + password: "{{ lookup('community.general.random_string', length=12, min_lower=1, min_upper=1, min_numeric=1, min_special=1, override_special='-_=!') }}" + email: test@example.com + roles: + - test-role + enabled: true + state: present + host: https://localhost:9200 + auth_user: elastic + auth_pass: "{{ elasticstack_password.stdout }}" + verify_certs: false + ca_certs: /etc/elasticsearch/certs/http_ca.crt + + - name: Copy PKCS12 certificate for cert_info testing + ansible.builtin.copy: + src: files/es-ca/elastic-stack-ca.p12 + dest: /tmp/elastic-stack-ca.p12 + mode: '0644' diff --git a/molecule/elasticsearch_test_modules/molecule.yml b/molecule/plugins/molecule.yml similarity index 70% rename from molecule/elasticsearch_test_modules/molecule.yml rename to molecule/plugins/molecule.yml index 80e445aa..90a13bde 100644 --- a/molecule/elasticsearch_test_modules/molecule.yml +++ b/molecule/plugins/molecule.yml @@ -6,7 +6,7 @@ dependency: driver: name: docker platforms: - - name: "elasticsearch_default-${MOLECULE_DISTRO:-debian13}" + - name: "plugins-${MOLECULE_DISTRO:-debian13}" groups: - elasticsearch image: "geerlingguy/docker-${MOLECULE_DISTRO:-debian13}-ansible:latest" @@ -18,9 +18,5 @@ platforms: pre_build_image: true provisioner: name: ansible - # Just enable temporarily. Sometimes it's useful, but most of the time it's - # overwhelming - #env: - # ANSIBLE_VERBOSITY: 3 verifier: name: ansible diff --git a/molecule/elasticsearch_test_modules/prepare.yml b/molecule/plugins/prepare.yml similarity index 60% rename from molecule/elasticsearch_test_modules/prepare.yml rename to molecule/plugins/prepare.yml index 3ba4785a..e4b02224 100644 --- a/molecule/elasticsearch_test_modules/prepare.yml +++ b/molecule/plugins/prepare.yml @@ -3,7 +3,7 @@ hosts: all tasks: - name: Show discovered interpreter - debug: + ansible.builtin.debug: var: ansible_facts.discovered_interpreter_python - name: Install packages for Debian @@ -19,8 +19,8 @@ update_cache: yes when: ansible_os_family == "Debian" - - name: Install python module dependencies + - name: Install Python dependencies ansible.builtin.pip: - name: "{{ item }}" - loop: - - elasticsearch + name: + - "elasticsearch<9" # Version 9+ has breaking API changes incompatible with this collection + - cryptography # latest version; version matrix is tested in the python-cryptography CI job diff --git a/molecule/elasticsearch_test_modules/requirements.yml b/molecule/plugins/requirements.yml similarity index 100% rename from molecule/elasticsearch_test_modules/requirements.yml rename to molecule/plugins/requirements.yml diff --git a/molecule/plugins/verify.yml b/molecule/plugins/verify.yml new file mode 100644 index 00000000..1799c898 --- /dev/null +++ b/molecule/plugins/verify.yml @@ -0,0 +1,137 @@ +--- +- name: Verify all plugin modules + hosts: all + vars: + elasticstack_elasticsearch_http_port: 9200 + elasticstack_initial_passwords: /usr/share/elasticsearch/initial_passwords + tasks: + - name: Fetch Elastic password # noqa: risky-shell-pipe + ansible.builtin.shell: > + if test -n "$(ps -p $$ | grep bash)"; then set -o pipefail; fi; + grep "PASSWORD elastic" {{ elasticstack_initial_passwords }} | + awk {' print $4 '} + register: elasticstack_password + changed_when: false + + # --- cert_info --- + + - name: Test cert_info with valid certificate and passphrase + netways.elasticstack.cert_info: + path: /tmp/elastic-stack-ca.p12 + passphrase: PleaseChangeMe # uses default value. Change if default is changed + register: cert_result + + - name: Assert cert_info returns all expected fields + ansible.builtin.assert: + that: + - cert_result.issuer is defined + - cert_result.subject is defined + - cert_result.not_valid_after is defined + - cert_result.not_valid_before is defined + - cert_result.serial_number is defined + - cert_result.extensions is defined + - not cert_result.failed + fail_msg: "cert_info did not return all expected fields" + + - name: Test cert_info fails with wrong passphrase + netways.elasticstack.cert_info: + path: /tmp/elastic-stack-ca.p12 + passphrase: wrong-passphrase + register: cert_wrong_pass + ignore_errors: true + + - name: Assert cert_info failed with wrong passphrase + ansible.builtin.assert: + that: cert_wrong_pass.failed + fail_msg: "cert_info should fail with a wrong passphrase" + + - name: Test cert_info fails with non-existent path + netways.elasticstack.cert_info: + path: /tmp/does-not-exist.p12 + passphrase: PleaseChangeMe + register: cert_wrong_path + ignore_errors: true + + - name: Assert cert_info failed with non-existent path + ansible.builtin.assert: + that: cert_wrong_path.failed + fail_msg: "cert_info should fail with a non-existent path" + + # --- elasticsearch_role --- + + - name: Assert Elasticsearch role exists + ansible.builtin.uri: + url: "https://localhost:{{ elasticstack_elasticsearch_http_port }}/_security/role/test-role" + method: GET + force_basic_auth: true + user: elastic + password: "{{ elasticstack_password.stdout }}" + validate_certs: false + status_code: 200 + register: role_result + + - name: Assert role has expected cluster privileges + ansible.builtin.assert: + that: + - "'manage_own_api_key' in role_result.json['test-role'].cluster" + - "'delegate_pki' in role_result.json['test-role'].cluster" + fail_msg: "Role does not have expected cluster privileges" + + # --- elasticsearch_user --- + + - name: Assert Elasticsearch user exists + ansible.builtin.uri: + url: "https://localhost:{{ elasticstack_elasticsearch_http_port }}/_security/user/test-user" + method: GET + force_basic_auth: true + user: elastic + password: "{{ elasticstack_password.stdout }}" + validate_certs: false + status_code: 200 + register: user_result + + - name: Assert user has expected role assigned + ansible.builtin.assert: + that: + - "'test-role' in user_result.json['test-user'].roles" + fail_msg: "User does not have expected role assigned" + + # --- test state: absent (user first, then role due to dependency) --- + + - name: Delete Elasticsearch user + netways.elasticstack.elasticsearch_user: + name: test-user + state: absent + host: "https://localhost:{{ elasticstack_elasticsearch_http_port }}" + auth_user: elastic + auth_pass: "{{ elasticstack_password.stdout }}" + verify_certs: false + + - name: Assert Elasticsearch user was deleted + ansible.builtin.uri: + url: "https://localhost:{{ elasticstack_elasticsearch_http_port }}/_security/user/test-user" + method: GET + force_basic_auth: true + user: elastic + password: "{{ elasticstack_password.stdout }}" + validate_certs: false + status_code: 404 + + - name: Delete Elasticsearch role + netways.elasticstack.elasticsearch_role: + name: test-role + state: absent + host: "https://localhost:{{ elasticstack_elasticsearch_http_port }}" + auth_user: elastic + auth_pass: "{{ elasticstack_password.stdout }}" + verify_certs: false + + - name: Assert Elasticsearch role was deleted + ansible.builtin.uri: + url: "https://localhost:{{ elasticstack_elasticsearch_http_port }}/_security/role/test-role" + method: GET + force_basic_auth: true + user: elastic + password: "{{ elasticstack_password.stdout }}" + validate_certs: false + status_code: 404 diff --git a/plugins/module_utils/api.py b/plugins/module_utils/api.py index 6dada178..847e0c52 100644 --- a/plugins/module_utils/api.py +++ b/plugins/module_utils/api.py @@ -4,12 +4,35 @@ # GNU General Public License v3.0+ (see LICENSES/GPL-3.0-or-later.txt or # https://www.gnu.org/licenses/gpl-3.0.txt) -from elasticsearch import Elasticsearch +import traceback import ssl +from ansible.module_utils.basic import missing_required_lib + +try: + from elasticsearch import Elasticsearch +except ImportError: + HAS_ELASTICSEARCH = False + ELASTICSEARCH_IMPORT_ERROR = traceback.format_exc() +else: + HAS_ELASTICSEARCH = True + ELASTICSEARCH_IMPORT_ERROR = None + + class Api(): - def new_client_basic_auth(host, auth_user, auth_pass, ca_certs, verify_certs) -> Elasticsearch: + + @staticmethod + def new_client_basic_auth(host, auth_user, auth_pass, ca_certs, verify_certs): + if not HAS_ELASTICSEARCH: + raise ImportError(missing_required_lib('elasticsearch')) + ctx = ssl.create_default_context(cafile=ca_certs) ctx.check_hostname = False ctx.verify_mode = False - return Elasticsearch(hosts=[host], basic_auth=(auth_user, auth_pass), ssl_context=ctx, verify_certs=verify_certs) + + return Elasticsearch( + hosts=[host], + basic_auth=(auth_user, auth_pass), + ssl_context=ctx, + verify_certs=verify_certs + ) diff --git a/plugins/module_utils/certs.py b/plugins/module_utils/certs.py index 5c5eecf0..7efbbee3 100644 --- a/plugins/module_utils/certs.py +++ b/plugins/module_utils/certs.py @@ -29,16 +29,16 @@ 'basicConstraints': [ '_ca', '_path_length' - ], + ], 'subjectKeyIdentifier': [ '_digest' - ], + ], 'authorityKeyIdentifier': [ '_authority_cert_issuer', '_authority_cert_serial_number', '_key_identifier' - ] - } + ] +} # function returns and converts bytes to a hex string separated with ":" @@ -50,7 +50,7 @@ def bytes_to_hex(bytes_str): # seperate by ":" every two characters and upper() value = ':'.join( ascii_hex_str[i:i + 2] for i in range(0, len(ascii_hex_str), 2) - ).upper() + ).upper() return value @@ -79,8 +79,6 @@ def __init__(self, module, result): self.__passphrase = self.module.params['passphrase'] self.__path = self.module.params['path'] self.__cert = None - self.__private_key = None - self.__additional_certs = None self.load_certificate() self.load_info() @@ -91,7 +89,7 @@ def load_certificate(self): if not HAS_CRYPTOGRAPHY_PKCS12: self.module.fail_json( msg=missing_required_lib('cryptography >= 2.5') - ) + ) # read the pkcs12 file try: with open(self.__path, 'rb') as f: @@ -99,19 +97,19 @@ def load_certificate(self): except IOError as e: self.module.fail_json( msg='IOError: %s' % (to_native(e)) - ) + ) # try to load with 2 parameters # for cryptography >= 3.1.x try: __pkcs12_tuple = pkcs12.load_key_and_certificates( pkcs12_data, to_bytes(self.__passphrase), - ) + ) loaded = True except Exception: self.module.log( msg="Couldn't load certificate without backend. Trying with backend." - ) + ) # try to load with 3 parameters for # cryptography >= 2.5.x and <= 3.0.x if not loaded: @@ -122,14 +120,12 @@ def load_certificate(self): pkcs12_data, to_bytes(self.__passphrase), backend - ) + ) self.module.log( msg="Loaded certificate with backend." - ) + ) # map loaded certificate to object - self.__private_key = __pkcs12_tuple[0] self.__cert = __pkcs12_tuple[1] - self.__additional_certs = __pkcs12_tuple[2] def load_info(self): self.general_info() @@ -139,10 +135,10 @@ def general_info(self): # map object values to result dict issuer = to_text(self.__cert.issuer.get_attributes_for_oid( NameOID.COMMON_NAME)[0].value - ) + ) subject = to_text(self.__cert.subject.get_attributes_for_oid( NameOID.COMMON_NAME)[0].value - ) + ) self.result['issuer'] = to_text(issuer) self.result['subject'] = to_text(subject) self.result['not_valid_after'] = to_text(self.__cert.not_valid_after) diff --git a/plugins/module_utils/elasticsearch_role.py b/plugins/module_utils/elasticsearch_role.py index 77504af1..772eaa71 100644 --- a/plugins/module_utils/elasticsearch_role.py +++ b/plugins/module_utils/elasticsearch_role.py @@ -1,5 +1,3 @@ -#!/usr/bin/python - # Copyright (c) 2024, Tobias Bauriedel # GNU General Public License v3.0+ (see LICENSES/GPL-3.0-or-later.txt or # https://www.gnu.org/licenses/gpl-3.0.txt) @@ -8,44 +6,39 @@ Api ) + class Role(): - def __init__(self, result, role_name, cluster, indicies, state, host, auth_user, auth_pass, verify_certs, ca_certs): + def __init__(self, result, role_name, cluster, indicies, state, host, auth_user, auth_pass, verify_certs, ca_certs): self.role_name = role_name self.cluster = cluster self.indicies = indicies self.state = state self.result = result - self.client = Api.new_client_basic_auth(host=host, auth_user=auth_user, auth_pass=auth_pass, verify_certs=verify_certs, ca_certs=ca_certs) - self.handle() - def return_result(self) -> dict: return self.result - def handle(self): if self.state == 'absent': - self.handle_absent() + self.handle_absent() elif self.state == 'present': self.handle_present() - - return + return def handle_absent(self): if self.role_name not in self.get_all().raw: return - + res = self.delete() - if res['found'] == True: + if res['found'] is True: self.result['changed'] = True self.result['msg'] = self.role_name + " has been deleted" - - return + return def handle_present(self): if self.role_name in self.get_all().raw: @@ -55,12 +48,12 @@ def handle_present(self): res = self.put() - if res.raw['role']['created'] == True: + if res.raw['role']['created'] is True: self.result['changed'] = True self.result['msg'] = self.role_name + " has been created" return - if pre_role == None: + if pre_role is None: return if pre_role.raw != self.get().raw: @@ -68,19 +61,15 @@ def handle_present(self): self.result['msg'] = self.role_name + " has been updated" return - def get_all(self): return self.client.security.get_role() - def get(self): return self.client.security.get_role(name=self.role_name) - def put(self): return self.client.security.put_role(name=self.role_name, cluster=self.cluster, indices=self.indicies) - def delete(self): return self.client.security.delete_role(name=self.role_name) diff --git a/plugins/module_utils/elasticsearch_user.py b/plugins/module_utils/elasticsearch_user.py index c84da609..455724b5 100644 --- a/plugins/module_utils/elasticsearch_user.py +++ b/plugins/module_utils/elasticsearch_user.py @@ -1,5 +1,3 @@ -#!/usr/bin/python - # Copyright (c) 2024, Tobias Bauriedel # GNU General Public License v3.0+ (see LICENSES/GPL-3.0-or-later.txt or # https://www.gnu.org/licenses/gpl-3.0.txt) @@ -8,6 +6,7 @@ Api ) + class User(): def __init__(self, result, user_name, full_name, password, email, roles, enabled, state, host, auth_user, auth_pass, verify_certs, ca_certs): self.user_name = user_name @@ -23,32 +22,27 @@ def __init__(self, result, user_name, full_name, password, email, roles, enabled self.handle() - def return_result(self) -> dict: return self.result - def handle(self): if self.state == 'absent': self.handle_absent() elif self.state == 'present': self.handle_present() - return - def handle_absent(self): if self.user_name not in self.get_all().raw: return res = self.delete() - if res['found'] == True: + if res['found'] is True: self.result['changed'] = True self.result['msg'] = self.user_name + " has been deleted" return - def handle_present(self): if self.user_name in self.get_all().raw: pre_user = self.get() @@ -57,12 +51,12 @@ def handle_present(self): res = self.put() - if res.raw['created'] == True: + if res.raw['created'] is True: self.result['changed'] = True self.result['msg'] = self.user_name + " has been created" return - if pre_user == None: + if pre_user is None: return if pre_user.raw != self.get().raw: @@ -71,18 +65,21 @@ def handle_present(self): return - def get_all(self): return self.client.security.get_user() - def get(self): return self.client.security.get_user(username=self.user_name) - def put(self): - return self.client.security.put_user(username=self.user_name, password=self.password, email=self.email, full_name=self.full_name, enabled=self.enabled, roles=self.roles) + return self.client.security.put_user( + username=self.user_name, + password=self.password, + email=self.email, + full_name=self.full_name, + enabled=self.enabled, + roles=self.roles + ) - def delete(self): - return self.client.security.delete_user(username=self.user_name) \ No newline at end of file + return self.client.security.delete_user(username=self.user_name) diff --git a/plugins/modules/cert_info.py b/plugins/modules/cert_info.py index 3dc5163b..41cb73a4 100644 --- a/plugins/modules/cert_info.py +++ b/plugins/modules/cert_info.py @@ -9,6 +9,83 @@ from __future__ import (absolute_import, division, print_function) __metaclass__ = type +DOCUMENTATION = r''' +--- +module: cert_info +short_description: Retrieve information from a PKCS12 certificate +description: + - Reads a PKCS12 certificate file and returns details such as issuer, subject, + validity dates, serial number, and supported X.509 extensions. + - Requires the C(cryptography) Python library (>= 2.5) on the target host. +version_added: "1.0.0" +author: + - Daniel Patrick (@dpatrick) +requirements: + - cryptography >= 2.5 +options: + path: + description: Absolute path to the PKCS12 certificate file on the target host. + type: str + required: true + passphrase: + description: Passphrase to decrypt the PKCS12 file. Omit if the file is not encrypted. + type: str + required: false + default: null +''' + +EXAMPLES = r''' +- name: Get certificate information + netways.elasticstack.cert_info: + path: /etc/elasticsearch/certs/elastic-certificates.p12 + register: cert + +- name: Get certificate info with passphrase + netways.elasticstack.cert_info: + path: /etc/elasticsearch/certs/elastic-certificates.p12 + passphrase: "{{ cert_passphrase }}" + register: cert + +- name: Show certificate expiry date + ansible.builtin.debug: + msg: "Certificate expires: {{ cert.not_valid_after }}" +''' + +RETURN = r''' +changed: + description: Always false, this module does not modify anything. + type: bool + returned: always +issuer: + description: Common name of the certificate issuer. + type: str + returned: success +subject: + description: Common name of the certificate subject. + type: str + returned: success +not_valid_before: + description: Start of the certificate validity period. + type: str + returned: success +not_valid_after: + description: End of the certificate validity period. + type: str + returned: success +serial_number: + description: Serial number of the certificate. + type: str + returned: success +version: + description: X.509 version of the certificate. + type: str + returned: success +extensions: + description: Dictionary of supported X.509 extensions and their values. + type: dict + returned: success +''' + from ansible.module_utils.basic import ( AnsibleModule, to_native diff --git a/plugins/modules/elasticsearch_role.py b/plugins/modules/elasticsearch_role.py index 050a907a..05db0f33 100644 --- a/plugins/modules/elasticsearch_role.py +++ b/plugins/modules/elasticsearch_role.py @@ -7,36 +7,111 @@ from __future__ import (absolute_import, division, print_function) __metaclass__ = type -from ansible.module_utils.basic import AnsibleModule +DOCUMENTATION = r''' +--- +module: elasticsearch_role +short_description: Manage Elasticsearch roles +description: + - Creates, updates, or deletes Elasticsearch roles using the Security API. + - Requires the C(elasticsearch) Python library on the target host. +version_added: "1.0.0" +author: + - Tobias Bauriedel (@tbauriedel) +requirements: + - elasticsearch < 9 +options: + name: + description: Name of the Elasticsearch role. + type: str + required: true + cluster: + description: List of cluster-level privileges assigned to the role. + type: list + elements: str + required: false + indicies: + description: List of index permission objects defining index patterns and privileges. + type: list + elements: dict + required: false + state: + description: Whether the role should exist or not. + type: str + required: false + default: present + choices: [present, absent] + host: + description: URL of the Elasticsearch host, including protocol and port. + type: str + required: true + auth_user: + description: Username for authentication. + type: str + required: true + auth_pass: + description: Password for authentication. + type: str + required: true + ca_certs: + description: Path to the CA certificate file for TLS verification. + type: str + required: false + verify_certs: + description: Whether to verify TLS certificates. + type: bool + required: false + default: true +''' + +EXAMPLES = r''' +- name: Create an Elasticsearch role + netways.elasticstack.elasticsearch_role: + name: my-role + cluster: + - manage_own_api_key + indicies: + - names: + - my-index-* + privileges: + - read + - write + state: present + host: https://localhost:9200 + auth_user: elastic + auth_pass: "{{ elastic_password }}" + ca_certs: /etc/elasticsearch/certs/http_ca.crt + +- name: Delete an Elasticsearch role + netways.elasticstack.elasticsearch_role: + name: my-role + state: absent + host: https://localhost:9200 + auth_user: elastic + auth_pass: "{{ elastic_password }}" + verify_certs: false +''' + +RETURN = r''' +changed: + description: Whether the role was created, updated, or deleted. + type: bool + returned: always +msg: + description: Human-readable status message. + type: str + returned: on change +''' + +from ansible.module_utils.basic import AnsibleModule, missing_required_lib +from ansible_collections.netways.elasticstack.plugins.module_utils.api import ( + HAS_ELASTICSEARCH, ELASTICSEARCH_IMPORT_ERROR +) from ansible_collections.netways.elasticstack.plugins.module_utils.elasticsearch_role import ( Role ) -def run_module(): - ''' - Elasticsearch user management. - - ``` - netways.elasticstack.elasticsearch_role: - name: new-role - cluster: - - manage_own_api_key - - delegate_pki - indicies: - - names: - - foobar - privileges: - - read - - write - state: present - host: https://localhost:9200 - auth_user: elastic - auth_pass: changeMe123! - verify_certs: false - ca_certs: /etc/elasticsearch/certs/http_ca.crt - ``` - ''' +def run_module(): # get role # https://www.elastic.co/guide/en/elasticsearch/reference/current/security-api-get-role.html @@ -46,17 +121,17 @@ def run_module(): module = AnsibleModule( argument_spec=dict( # User args - name=dict(type=str, required=True), - cluster=dict(type=list, required=False), - indicies=dict(type=list, required=False), - state=dict(type=str, required=False, default='present'), + name=dict(type='str', required=True), + cluster=dict(type='list', elements='str', required=False), + indicies=dict(type='list', elements='dict', required=False), + state=dict(type='str', required=False, default='present', choices=['present', 'absent']), # Auth args - host=dict(type=str, required=True), - auth_user=dict(type=str, required=True), - auth_pass=dict(type=str, required=True, no_log=True), - ca_certs=dict(type=str, required=False), - verify_certs=dict(type=bool, required=False, default=True) + host=dict(type='str', required=True), + auth_user=dict(type='str', required=True), + auth_pass=dict(type='str', required=True, no_log=True), + ca_certs=dict(type='str', required=False), + verify_certs=dict(type='bool', required=False, default=True) ) ) @@ -65,16 +140,15 @@ def run_module(): changed=False ) - if module.params['state'] != 'absent' and module.params['state'] != 'present': - result['stderr'] = "Invalid state given. Please use 'absent' or 'present'" - result['failed'] = True - - module.exit_json(**result) - + if not HAS_ELASTICSEARCH: + module.fail_json( + msg=missing_required_lib('elasticsearch'), + exception=ELASTICSEARCH_IMPORT_ERROR + ) role = Role( - result=result, - role_name=module.params['name'], + result=result, + role_name=module.params['name'], cluster=module.params['cluster'], indicies=module.params['indicies'], state=module.params['state'], @@ -91,4 +165,4 @@ def run_module(): if __name__ == "__main__": - run_module() \ No newline at end of file + run_module() diff --git a/plugins/modules/elasticsearch_user.py b/plugins/modules/elasticsearch_user.py index 3e05e91b..d5b1dd4b 100644 --- a/plugins/modules/elasticsearch_user.py +++ b/plugins/modules/elasticsearch_user.py @@ -7,52 +7,145 @@ from __future__ import (absolute_import, division, print_function) __metaclass__ = type -from ansible.module_utils.basic import AnsibleModule +DOCUMENTATION = r''' +--- +module: elasticsearch_user +short_description: Manage Elasticsearch users +description: + - Creates, updates, or deletes Elasticsearch native users using the Security API. + - Requires the C(elasticsearch) Python library on the target host. +version_added: "1.0.0" +author: + - Tobias Bauriedel (@tbauriedel) +requirements: + - elasticsearch < 9 +options: + name: + description: Username of the Elasticsearch user. + type: str + required: true + fullname: + description: Full display name of the user. + type: str + required: false + password: + description: Password for the user. Required when (state=present). + type: str + required: false + default: null + email: + description: Email address of the user. + type: str + required: false + roles: + description: List of roles assigned to the user. Required when (state=present). + type: list + elements: str + required: false + default: null + enabled: + description: Whether the user account is enabled. + type: bool + required: false + default: true + state: + description: Whether the user should exist or not. + type: str + required: false + default: present + choices: [present, absent] + host: + description: URL of the Elasticsearch host, including protocol and port. + type: str + required: true + auth_user: + description: Username for authentication. + type: str + required: true + auth_pass: + description: Password for authentication. + type: str + required: true + ca_certs: + description: Path to the CA certificate file for TLS verification. + type: str + required: false + verify_certs: + description: Whether to verify TLS certificates. + type: bool + required: false + default: true +''' + +EXAMPLES = r''' +- name: Create an Elasticsearch user + netways.elasticstack.elasticsearch_user: + name: john + fullname: John Doe + password: "{{ user_password }}" + email: john@example.com + roles: + - my-role + enabled: true + state: present + host: https://localhost:9200 + auth_user: elastic + auth_pass: "{{ elastic_password }}" + ca_certs: /etc/elasticsearch/certs/http_ca.crt + +- name: Delete an Elasticsearch user + netways.elasticstack.elasticsearch_user: + name: john + state: absent + host: https://localhost:9200 + auth_user: elastic + auth_pass: "{{ elastic_password }}" + verify_certs: false +''' + +RETURN = r''' +changed: + description: Whether the user was created, updated, or deleted. + type: bool + returned: always +msg: + description: Human-readable status message. + type: str + returned: on change +''' + +from ansible.module_utils.basic import AnsibleModule, missing_required_lib +from ansible_collections.netways.elasticstack.plugins.module_utils.api import ( + HAS_ELASTICSEARCH, ELASTICSEARCH_IMPORT_ERROR +) from ansible_collections.netways.elasticstack.plugins.module_utils.elasticsearch_user import ( User ) def run_module(): - ''' - Elasticsearch user management. - - ``` - netways.elasticstack.elasticsearch_user: - name: new-user1 - fullname: New User - password: changeMe123! - email: "new@user.de" - roles: - - new-role1 - enabled: true - state: absent - host: https://localhost:9200 - auth_user: elastic - auth_pass: "{{ elasticstack_password.stdout }}" - verify_certs: false - ca_certs: /etc/elasticsearch/certs/http_ca.crt - ``` - ''' module = AnsibleModule( argument_spec=dict( # User args - name=dict(type=str, required=True), - fullname=dict(type=str, required=False), - password=dict(type=str, required=True, no_log=True), - email=dict(type=str, required=False), - roles=dict(type=list, required=True), - enabled=dict(type=bool, required=False, default=True), - state=dict(type=str, required=False, default="present"), - + name=dict(type='str', required=True), + fullname=dict(type='str', required=False), + password=dict(type='str', required=False, no_log=True), + email=dict(type='str', required=False), + roles=dict(type='list', elements='str', required=False), + enabled=dict(type='bool', required=False, default=True), + state=dict(type='str', required=False, default='present', choices=['present', 'absent']), + # Auth args - host=dict(type=str, required=True), - auth_user=dict(type=str, required=True), - auth_pass=dict(type=str, required=True, no_log=True), - ca_certs=dict(type=str, required=False), - verify_certs=dict(type=bool, required=False, default=True) - ) + host=dict(type='str', required=True), + auth_user=dict(type='str', required=True), + auth_pass=dict(type='str', required=True, no_log=True), + ca_certs=dict(type='str', required=False), + verify_certs=dict(type='bool', required=False, default=True) + ), + required_if=[ + ('state', 'present', ['password', 'roles']), + ] ) result = dict( @@ -60,12 +153,11 @@ def run_module(): changed=False ) - if module.params['state'] != 'absent' and module.params['state'] != 'present': - result['stderr'] = "Invalid state given. Please use 'absent' or 'present'" - result['failed'] = True - - module.exit_json(**result) - + if not HAS_ELASTICSEARCH: + module.fail_json( + msg=missing_required_lib('elasticsearch'), + exception=ELASTICSEARCH_IMPORT_ERROR + ) user = User( result=result, @@ -87,5 +179,6 @@ def run_module(): module.exit_json(**result) + if __name__ == "__main__": - run_module() \ No newline at end of file + run_module() diff --git a/tests/unit/plugins/module_utils/test_certs.py b/tests/unit/plugins/module_utils/test_certs.py index 4a691e8c..0c09b318 100644 --- a/tests/unit/plugins/module_utils/test_certs.py +++ b/tests/unit/plugins/module_utils/test_certs.py @@ -15,29 +15,29 @@ class TestCerts(unittest.TestCase): def test_bytes_to_hex_byte_string(self): bytes_string = unhexlify('82532011c773a75e2a77c1df22e423b4c450bacf') # or - #bytes_string = b'\x82S \x11\xc7s\xa7^*w\xc1\xdf"\xe4#\xb4\xc4P\xba\xcf' + # bytes_string = b'\x82S \x11\xc7s\xa7^*w\xc1\xdf"\xe4#\xb4\xc4P\xba\xcf' result = bytes_to_hex(bytes_str=bytes_string) - #print("Bytes converted: " + str(result)) + # print("Bytes converted: " + str(result)) self.assertEqual(result, '82:53:20:11:C7:73:A7:5E:2A:77:C1:DF:22:E4:23:B4:C4:50:BA:CF') def test_check_supported_extensions_with_supported_extension(self): result = check_supported_extensions(extension_name='authorityKeyIdentifier') - #print("Extension is supported: " + str(result)) + # print("Extension is supported: " + str(result)) self.assertEqual(result, True) def test_check_supported_extensions_with_unknown_extension(self): result = check_supported_extensions(extension_name='UnknownExtension') - #print("Extension is supported: " + str(result)) + # print("Extension is supported: " + str(result)) self.assertEqual(result, False) def test_check_supported_keys_with_known_key(self): result = check_supported_keys(key='_key_identifier', extension_name='authorityKeyIdentifier') - #print("Key is supported: " + str(result)) + # print("Key is supported: " + str(result)) self.assertEqual(result, True) def test_check_supported_keys_with_unknown_key(self): result = check_supported_keys(key='_unknown', extension_name='authorityKeyIdentifier') - #print("Key is supported: " + str(result)) + # print("Key is supported: " + str(result)) self.assertEqual(result, False) diff --git a/tests/unit/plugins/modules/test_cert_info.py b/tests/unit/plugins/modules/test_cert_info.py index 4151576e..6613bdbd 100644 --- a/tests/unit/plugins/modules/test_cert_info.py +++ b/tests/unit/plugins/modules/test_cert_info.py @@ -1,12 +1,22 @@ -import json import sys import unittest from unittest.mock import patch from ansible.module_utils import basic -from ansible.module_utils.common.text.converters import to_bytes sys.path.append('/home/runner/.ansible/collections/') from ansible_collections.netways.elasticstack.plugins.modules import cert_info +_current_module_args = {} + + +def set_module_args(args): + global _current_module_args + _current_module_args = args + + +def _mock_load_params(): + return _current_module_args + + certificate = { "changed": False, "extensions": { @@ -42,19 +52,14 @@ "serial_number": "719770426243590812378787092632593850366518596520", "subject": "Elastic Certificate Tool Autogenerated CA", "version": "Version.v3" - } - - -def set_module_args(args): - """prepare arguments so that they will be picked up during module creation""" - args = json.dumps({'ANSIBLE_MODULE_ARGS': args}) - basic._ANSIBLE_ARGS = to_bytes(args) +} class AnsibleExitJson(Exception): """Exception class to be raised by module.exit_json and caught by the test case""" pass + class AnsibleFailJson(Exception): """Exception class to be raised by module.fail_json and caught by the test case""" pass @@ -75,7 +80,7 @@ def exit_json(*args, **kwargs): for item in certificate: if certificate[item] != kwargs[item]: checks_passed = False - + if checks_passed: raise AnsibleExitJson(kwargs) @@ -95,6 +100,11 @@ def setUp(self): self.mock_module_helper.start() self.addCleanup(self.mock_module_helper.stop) + self.load_params_patcher = patch('ansible.module_utils.basic._load_params', + side_effect=_mock_load_params) + self.load_params_patcher.start() + self.addCleanup(self.load_params_patcher.stop) + def test_module_fail_when_required_args_missing(self): with self.assertRaises(AnsibleFailJson): set_module_args({})