Skip to content

Commit

Permalink
acme_certificate_renewal_info: add treat_parsing_error_as_non_existin…
Browse files Browse the repository at this point in the history
…g option and existing and parsable return values (#838)

* Fix error reporting for OpenSSL backend: raise BackendExceptions instead of directly failing the module.

* Add treat_parsing_error_as_non_existing option and existing and parsable return values.
  • Loading branch information
felixfontein authored Jan 12, 2025
1 parent 49354f2 commit 01e7bf1
Show file tree
Hide file tree
Showing 5 changed files with 134 additions and 30 deletions.
3 changes: 3 additions & 0 deletions changelogs/fragments/838-acme_certificate_renewal_info.yml
Original file line number Diff line number Diff line change
@@ -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)."
32 changes: 22 additions & 10 deletions plugins/module_utils/acme/backend_openssl_cli.py
Original file line number Diff line number Diff line change
Expand Up @@ -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')

Expand Down Expand Up @@ -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(
Expand Down Expand Up @@ -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 = []
Expand Down Expand Up @@ -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:
Expand Down Expand Up @@ -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)
Expand Down
49 changes: 47 additions & 2 deletions plugins/modules/acme_certificate_renewal_info.py
Original file line number Diff line number Diff line change
Expand Up @@ -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.
Expand Down Expand Up @@ -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.
Expand Down Expand Up @@ -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


Expand All @@ -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=(
Expand All @@ -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,
)

Expand All @@ -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
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -49,97 +49,112 @@
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 }}"
acme_version: 2
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"
acme_version: 2
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"
acme_version: 2
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"
acme_version: 2
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"
acme_version: 2
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
Original file line number Diff line number Diff line change
Expand Up @@ -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

0 comments on commit 01e7bf1

Please sign in to comment.