diff --git a/changelogs/fragments/838-acme_certificate_renewal_info.yml b/changelogs/fragments/838-acme_certificate_renewal_info.yml new file mode 100644 index 000000000..a45420940 --- /dev/null +++ b/changelogs/fragments/838-acme_certificate_renewal_info.yml @@ -0,0 +1,3 @@ +minor_changes: + - "acme_certificate_renewal_info - add ``exists`` and ``parsable`` return values and ``treat_parsing_error_as_non_existing`` option + (https://github.com/ansible-collections/community.crypto/pull/838)." diff --git a/plugins/module_utils/acme/backend_openssl_cli.py b/plugins/module_utils/acme/backend_openssl_cli.py index 175ecfe85..88f042674 100644 --- a/plugins/module_utils/acme/backend_openssl_cli.py +++ b/plugins/module_utils/acme/backend_openssl_cli.py @@ -122,8 +122,10 @@ def parse_key(self, key_file=None, key_content=None, passphrase=None): raise KeyParsingError('unknown key type "%s"' % account_key_type) openssl_keydump_cmd = [self.openssl_binary, account_key_type, "-in", key_file, "-noout", "-text"] - dummy, out, dummy = self.module.run_command( - openssl_keydump_cmd, check_rc=True, environ_update=_OPENSSL_ENVIRONMENT_UPDATE) + rc, out, err = self.module.run_command( + openssl_keydump_cmd, check_rc=False, environ_update=_OPENSSL_ENVIRONMENT_UPDATE) + if rc != 0: + raise BackendException('Error while running {cmd}: {stderr}'.format(cmd=' '.join(openssl_keydump_cmd), stderr=to_text(err))) out_text = to_text(out, errors='surrogate_or_strict') @@ -205,8 +207,10 @@ def sign(self, payload64, protected64, key_data): cmd_postfix = ["-sign", key_data['key_file']] openssl_sign_cmd = [self.openssl_binary, "dgst", "-{0}".format(key_data['hash'])] + cmd_postfix - dummy, out, dummy = self.module.run_command( - openssl_sign_cmd, data=sign_payload, check_rc=True, binary_data=True, environ_update=_OPENSSL_ENVIRONMENT_UPDATE) + rc, out, err = self.module.run_command( + openssl_sign_cmd, data=sign_payload, check_rc=False, binary_data=True, environ_update=_OPENSSL_ENVIRONMENT_UPDATE) + if rc != 0: + raise BackendException('Error while running {cmd}: {stderr}'.format(cmd=' '.join(openssl_sign_cmd), stderr=to_text(err))) if key_data['type'] == 'ec': dummy, der_out, dummy = self.module.run_command( @@ -281,8 +285,10 @@ def get_ordered_csr_identifiers(self, csr_filename=None, csr_content=None): data = csr_content.encode('utf-8') openssl_csr_cmd = [self.openssl_binary, "req", "-in", filename, "-noout", "-text"] - dummy, out, dummy = self.module.run_command( - openssl_csr_cmd, data=data, check_rc=True, binary_data=True, environ_update=_OPENSSL_ENVIRONMENT_UPDATE) + rc, out, err = self.module.run_command( + openssl_csr_cmd, data=data, check_rc=False, binary_data=True, environ_update=_OPENSSL_ENVIRONMENT_UPDATE) + if rc != 0: + raise BackendException('Error while running {cmd}: {stderr}'.format(cmd=' '.join(openssl_csr_cmd), stderr=to_text(err))) identifiers = set() result = [] @@ -341,8 +347,11 @@ def get_cert_days(self, cert_filename=None, cert_content=None, now=None): return -1 openssl_cert_cmd = [self.openssl_binary, "x509", "-in", filename, "-noout", "-text"] - dummy, out, dummy = self.module.run_command( - openssl_cert_cmd, data=data, check_rc=True, binary_data=True, environ_update=_OPENSSL_ENVIRONMENT_UPDATE) + rc, out, err = self.module.run_command( + openssl_cert_cmd, data=data, check_rc=False, binary_data=True, environ_update=_OPENSSL_ENVIRONMENT_UPDATE) + if rc != 0: + raise BackendException('Error while running {cmd}: {stderr}'.format(cmd=' '.join(openssl_cert_cmd), stderr=to_text(err))) + out_text = to_text(out, errors='surrogate_or_strict') not_after = _extract_date(out_text, 'Not After', cert_filename_suffix=cert_filename_suffix) if now is None: @@ -371,8 +380,11 @@ def get_cert_information(self, cert_filename=None, cert_content=None): cert_filename_suffix = '' openssl_cert_cmd = [self.openssl_binary, "x509", "-in", filename, "-noout", "-text"] - dummy, out, dummy = self.module.run_command( - openssl_cert_cmd, data=data, check_rc=True, binary_data=True, environ_update=_OPENSSL_ENVIRONMENT_UPDATE) + rc, out, err = self.module.run_command( + openssl_cert_cmd, data=data, check_rc=False, binary_data=True, environ_update=_OPENSSL_ENVIRONMENT_UPDATE) + if rc != 0: + raise BackendException('Error while running {cmd}: {stderr}'.format(cmd=' '.join(openssl_cert_cmd), stderr=to_text(err))) + out_text = to_text(out, errors='surrogate_or_strict') not_after = _extract_date(out_text, 'Not After', cert_filename_suffix=cert_filename_suffix) diff --git a/plugins/modules/acme_certificate_renewal_info.py b/plugins/modules/acme_certificate_renewal_info.py index d4b62acb5..d9f811b1d 100644 --- a/plugins/modules/acme_certificate_renewal_info.py +++ b/plugins/modules/acme_certificate_renewal_info.py @@ -74,6 +74,15 @@ - Valid format is C([+-]timespec | ASN.1 TIME) where timespec can be an integer + C([w | d | h | m | s]) (for example V(+32w1d2h)). type: str + treat_parsing_error_as_non_existing: + description: + - Determines the behavior when the certificate file exists or its contents are provided, but the certificate cannot be parsed. + - If V(true), will exit successfully with RV(exists=true), RV(parsable=false), and RV(should_renew=true). + - If V(false), the module will fail. + - If the file exists, but cannot be loaded due to I/O errors or permission errors, the module always fails. + type: bool + default: false + version_added: 2.24.0 seealso: - module: community.crypto.acme_certificate description: Allows to obtain a certificate using the ACME protocol. @@ -101,6 +110,23 @@ type: bool sample: true +exists: + description: + - Whether the certificate file exists, or O(certificate_content) was provided. + returned: success + type: bool + sample: true + version_added: 2.24.0 + +parsable: + description: + - Whether the certificate file exists, or O(certificate_content) was provided, and the certificate can be parsed. + - Can only differ from RV(exists) if O(treat_parsing_error_as_non_existing=true). + returned: success + type: bool + sample: true + version_added: 2.24.0 + msg: description: - Information on the reason for renewal. @@ -139,6 +165,8 @@ from ansible_collections.community.crypto.plugins.module_utils.acme.errors import ModuleFailException +from ansible_collections.community.crypto.plugins.module_utils.acme.io import read_file + from ansible_collections.community.crypto.plugins.module_utils.acme.utils import compute_cert_id @@ -152,6 +180,7 @@ def main(): remaining_days=dict(type='int'), remaining_percentage=dict(type='float'), now=dict(type='str'), + treat_parsing_error_as_non_existing=dict(type='bool', default=False), ) argument_spec.update( mutually_exclusive=( @@ -164,6 +193,8 @@ def main(): result = dict( changed=False, msg='The certificate is still valid and no condition was reached', + exists=False, + parsable=False, supports_ari=False, ) @@ -175,14 +206,28 @@ def complete(should_renew, **kwargs): if not module.params['certificate_path'] and not module.params['certificate_content']: complete(True, msg='No certificate was specified') - if module.params['certificate_path'] is not None and not os.path.exists(module.params['certificate_path']): - complete(True, msg='The certificate file does not exist') + if module.params['certificate_path'] is not None: + if not os.path.exists(module.params['certificate_path']): + complete(True, msg='The certificate file does not exist') + if module.params['treat_parsing_error_as_non_existing']: + try: + read_file(module.params['certificate_path']) + except ModuleFailException as e: + e.do_fail(module) + result['exists'] = True try: cert_info = backend.get_cert_information( cert_filename=module.params['certificate_path'], cert_content=module.params['certificate_content'], ) + except ModuleFailException as e: + if module.params['treat_parsing_error_as_non_existing']: + complete(True, msg='Certificate cannot be parsed: {0}'.format(e.msg)) + e.do_fail(module) + + result['parsable'] = True + try: cert_id = compute_cert_id(backend, cert_info=cert_info, none_if_required_information_is_missing=True) if cert_id is not None: result['cert_id'] = cert_id diff --git a/tests/integration/targets/acme_certificate_renewal_info/tasks/impl.yml b/tests/integration/targets/acme_certificate_renewal_info/tasks/impl.yml index b30808ed5..143b286b2 100644 --- a/tests/integration/targets/acme_certificate_renewal_info/tasks/impl.yml +++ b/tests/integration/targets/acme_certificate_renewal_info/tasks/impl.yml @@ -49,27 +49,25 @@ slurp: src: '{{ remote_tmp_dir }}/cert-1.pem' register: slurp_cert_1 -- name: Obtain certificate information (1/9) +- name: Obtain certificate information (1/11) acme_certificate_renewal_info: select_crypto_backend: "{{ select_crypto_backend }}" certificate_path: "{{ remote_tmp_dir }}/cert-1.pem" acme_version: 2 acme_directory: https://{{ acme_host }}:14000/dir validate_certs: false - # Certificate is valid for ~1826 days register: cert_1_renewal_1 -- name: Obtain certificate information (2/9) +- name: Obtain certificate information (2/11) acme_certificate_renewal_info: select_crypto_backend: "{{ select_crypto_backend }}" certificate_path: "{{ remote_tmp_dir }}/cert-1.pem" acme_version: 2 acme_directory: https://{{ acme_host }}:14000/dir validate_certs: false - # Certificate is valid for ~1826 days remaining_days: 1000 remaining_percentage: 0.5 register: cert_1_renewal_2 -- name: Obtain certificate information (3/9) +- name: Obtain certificate information (3/11) acme_certificate_renewal_info: select_crypto_backend: "{{ select_crypto_backend }}" certificate_content: "{{ slurp_cert_1.content | b64decode }}" @@ -77,9 +75,8 @@ acme_directory: https://{{ acme_host }}:14000/dir validate_certs: false now: +1800d - # Certificate is valid for ~26 days register: cert_1_renewal_3 -- name: Obtain certificate information (4/9) +- name: Obtain certificate information (4/11) acme_certificate_renewal_info: select_crypto_backend: "{{ select_crypto_backend }}" certificate_path: "{{ remote_tmp_dir }}/cert-1.pem" @@ -87,11 +84,10 @@ acme_directory: https://{{ acme_host }}:14000/dir validate_certs: false now: +1800d - # Certificate is valid for ~26 days remaining_days: 30 remaining_percentage: 0.1 register: cert_1_renewal_4 -- name: Obtain certificate information (5/9) +- name: Obtain certificate information (5/11) acme_certificate_renewal_info: select_crypto_backend: "{{ select_crypto_backend }}" certificate_path: "{{ remote_tmp_dir }}/cert-1.pem" @@ -99,11 +95,10 @@ acme_directory: https://{{ acme_host }}:14000/dir validate_certs: false now: +1800d - # Certificate is valid for ~26 days remaining_days: 30 remaining_percentage: 0.01 register: cert_1_renewal_5 -- name: Obtain certificate information (6/9) +- name: Obtain certificate information (6/11) acme_certificate_renewal_info: select_crypto_backend: "{{ select_crypto_backend }}" certificate_path: "{{ remote_tmp_dir }}/cert-1.pem" @@ -111,11 +106,10 @@ acme_directory: https://{{ acme_host }}:14000/dir validate_certs: false now: +1800d - # Certificate is valid for ~26 days remaining_days: 10 remaining_percentage: 0.03 register: cert_1_renewal_6 -- name: Obtain certificate information (7/9) +- name: Obtain certificate information (7/11) acme_certificate_renewal_info: select_crypto_backend: "{{ select_crypto_backend }}" certificate_path: "{{ remote_tmp_dir }}/cert-1.pem" @@ -123,23 +117,44 @@ acme_directory: https://{{ acme_host }}:14000/dir validate_certs: false now: +1830d - # Certificate is no longer valid register: cert_1_renewal_7 -- name: Obtain certificate information (8/9) +- name: Obtain certificate information (8/11) acme_certificate_renewal_info: select_crypto_backend: "{{ select_crypto_backend }}" acme_version: 2 acme_directory: https://{{ acme_host }}:14000/dir validate_certs: false now: +1830d - # Certificate is no longer valid register: cert_1_renewal_8 -- name: Obtain certificate information (9/9) +- name: Obtain certificate information (9/11) acme_certificate_renewal_info: select_crypto_backend: "{{ select_crypto_backend }}" certificate_path: "{{ remote_tmp_dir }}/cert-does-not-exist.pem" acme_version: 2 acme_directory: https://{{ acme_host }}:14000/dir validate_certs: false - # Certificate is no longer valid register: cert_1_renewal_9 +- name: Create broken file + copy: + dest: "{{ remote_tmp_dir }}/cert-is-broken.pem" + content: | + --- THIS IS NOT A CERT --- +- name: Obtain certificate information (10/11) + acme_certificate_renewal_info: + treat_parsing_error_as_non_existing: false + select_crypto_backend: "{{ select_crypto_backend }}" + certificate_path: "{{ remote_tmp_dir }}/cert-is-broken.pem" + acme_version: 2 + acme_directory: https://{{ acme_host }}:14000/dir + validate_certs: false + register: cert_1_renewal_10 + ignore_errors: true +- name: Obtain certificate information (11/11) + acme_certificate_renewal_info: + treat_parsing_error_as_non_existing: true + select_crypto_backend: "{{ select_crypto_backend }}" + certificate_path: "{{ remote_tmp_dir }}/cert-is-broken.pem" + acme_version: 2 + acme_directory: https://{{ acme_host }}:14000/dir + validate_certs: false + register: cert_1_renewal_11 diff --git a/tests/integration/targets/acme_certificate_renewal_info/tests/validate.yml b/tests/integration/targets/acme_certificate_renewal_info/tests/validate.yml index 116e524c4..ecf752e3a 100644 --- a/tests/integration/targets/acme_certificate_renewal_info/tests/validate.yml +++ b/tests/integration/targets/acme_certificate_renewal_info/tests/validate.yml @@ -10,38 +10,67 @@ - cert_1_renewal_1.msg == 'The certificate is still valid and no condition was reached' - cert_1_renewal_1.supports_ari == supports_ari - cert_1_renewal_1.cert_id is string or not can_have_cert_id + - cert_1_renewal_1.exists == true + - cert_1_renewal_1.parsable == true - cert_1_renewal_2.should_renew == false - cert_1_renewal_2.msg == 'The certificate is still valid and no condition was reached' - cert_1_renewal_2.supports_ari == supports_ari - cert_1_renewal_2.cert_id is string or not can_have_cert_id + - cert_1_renewal_2.exists == true + - cert_1_renewal_2.parsable == true - cert_1_renewal_3.should_renew == false - cert_1_renewal_3.msg == 'The certificate is still valid and no condition was reached' - cert_1_renewal_3.supports_ari == supports_ari - cert_1_renewal_3.cert_id is string or not can_have_cert_id + - cert_1_renewal_3.exists == true + - cert_1_renewal_3.parsable == true - cert_1_renewal_4.should_renew == true - cert_1_renewal_4.msg == 'The certificate expires in 25 days' - cert_1_renewal_4.supports_ari == supports_ari - cert_1_renewal_4.cert_id is string or not can_have_cert_id + - cert_1_renewal_4.exists == true + - cert_1_renewal_4.parsable == true - cert_1_renewal_5.should_renew == true - cert_1_renewal_5.msg == 'The certificate expires in 25 days' - cert_1_renewal_5.supports_ari == supports_ari - cert_1_renewal_5.cert_id is string or not can_have_cert_id + - cert_1_renewal_5.exists == true + - cert_1_renewal_5.parsable == true - cert_1_renewal_6.should_renew == true - cert_1_renewal_6.msg.startswith("The remaining percentage 3.0% of the certificate's lifespan was reached on ") - cert_1_renewal_6.supports_ari == supports_ari - cert_1_renewal_6.cert_id is string or not can_have_cert_id + - cert_1_renewal_6.exists == true + - cert_1_renewal_6.parsable == true - cert_1_renewal_7.should_renew == true - cert_1_renewal_7.msg == 'The certificate has already expired' - cert_1_renewal_7.supports_ari == false - cert_1_renewal_7.cert_id is string or not can_have_cert_id + - cert_1_renewal_7.exists == true + - cert_1_renewal_7.parsable == true - cert_1_renewal_8.should_renew == true - cert_1_renewal_8.msg == 'No certificate was specified' - cert_1_renewal_8.supports_ari == false - cert_1_renewal_8.cert_id is not defined + - cert_1_renewal_8.exists == false + - cert_1_renewal_8.parsable == false - cert_1_renewal_9.should_renew == true - cert_1_renewal_9.msg == 'The certificate file does not exist' - cert_1_renewal_9.supports_ari == false - cert_1_renewal_9.cert_id is not defined + - cert_1_renewal_9.exists == false + - cert_1_renewal_9.parsable == false + - cert_1_renewal_10 is failed + - cert_1_renewal_10.msg.startswith('Error while running ') or + cert_1_renewal_10.msg.startswith('Cannot parse certificate ') + - cert_1_renewal_11.should_renew == true + - >- + cert_1_renewal_11.msg.startswith('Certificate cannot be parsed: Error while running ') or + cert_1_renewal_11.msg.startswith('Certificate cannot be parsed: Cannot parse certificate ') + - cert_1_renewal_11.supports_ari == false + - cert_1_renewal_11.cert_id is not defined + - cert_1_renewal_11.exists == true + - cert_1_renewal_11.parsable == false vars: can_have_cert_id: cert_1_info.authority_key_identifier is string supports_ari: false