acme_certificate_renewal_info: add treat_parsing_error_as_non_existing 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.
This commit is contained in:
Felix Fontein
2025-01-12 21:42:24 +01:00
committed by GitHub
parent 49354f2121
commit 01e7bf1f33
5 changed files with 134 additions and 30 deletions

View File

@@ -122,8 +122,10 @@ class OpenSSLCLIBackend(CryptoBackend):
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 @@ class OpenSSLCLIBackend(CryptoBackend):
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 @@ class OpenSSLCLIBackend(CryptoBackend):
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 @@ class OpenSSLCLIBackend(CryptoBackend):
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 @@ class OpenSSLCLIBackend(CryptoBackend):
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)

View File

@@ -74,6 +74,15 @@ options:
- 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 @@ should_renew:
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.acme import
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 main():
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