Letsencrypt authz deactivation (#36362)
* Allow to deactivate authz objects. Currently only after success. * Making sure cleanup is done even when module fails (except if fetch_url() fails). * Make deactivate_authzs eat exceptions so that all authzs are deactivated in case of errors.
This commit is contained in:
parent
99627ab99d
commit
cd9d554186
1 changed files with 125 additions and 67 deletions
|
@ -159,6 +159,17 @@ options:
|
||||||
required: false
|
required: false
|
||||||
default: true
|
default: true
|
||||||
version_added: 2.5
|
version_added: 2.5
|
||||||
|
deactivate_authzs:
|
||||||
|
description:
|
||||||
|
- "Deactivate authentication objects (authz) after issuing a certificate,
|
||||||
|
or when issuing the certificate failed."
|
||||||
|
- "Authentication objects are bound to an account key and remain valid
|
||||||
|
for a certain amount of time, and can be used to issue certificates
|
||||||
|
without having to re-authenticate the domain. This can be a security
|
||||||
|
concern. "
|
||||||
|
required: false
|
||||||
|
default: false
|
||||||
|
version_added: 2.6
|
||||||
'''
|
'''
|
||||||
|
|
||||||
EXAMPLES = '''
|
EXAMPLES = '''
|
||||||
|
@ -335,6 +346,19 @@ from ansible.module_utils._text import to_native, to_text, to_bytes
|
||||||
from ansible.module_utils.urls import fetch_url as _fetch_url
|
from ansible.module_utils.urls import fetch_url as _fetch_url
|
||||||
|
|
||||||
|
|
||||||
|
class ModuleFailException(Exception):
|
||||||
|
'''
|
||||||
|
If raised, module.fail_json() will be called with the given parameters after cleanup.
|
||||||
|
'''
|
||||||
|
def __init__(self, msg, **args):
|
||||||
|
super(ModuleFailException, self).__init__(self, msg)
|
||||||
|
self.msg = msg
|
||||||
|
self.args = args
|
||||||
|
|
||||||
|
def do_fail(self, module):
|
||||||
|
module.fail_json(msg=self.msg, **self.args)
|
||||||
|
|
||||||
|
|
||||||
def _lowercase_fetch_url(*args, **kwargs):
|
def _lowercase_fetch_url(*args, **kwargs):
|
||||||
'''
|
'''
|
||||||
Add lowercase representations of the header names as dict keys
|
Add lowercase representations of the header names as dict keys
|
||||||
|
@ -367,12 +391,12 @@ def simple_get(module, url):
|
||||||
try:
|
try:
|
||||||
result = module.from_json(content.decode('utf8'))
|
result = module.from_json(content.decode('utf8'))
|
||||||
except ValueError:
|
except ValueError:
|
||||||
module.fail_json(msg="Failed to parse the ACME response: {0} {1}".format(url, content))
|
raise ModuleFailException("Failed to parse the ACME response: {0} {1}".format(url, content))
|
||||||
else:
|
else:
|
||||||
result = content
|
result = content
|
||||||
|
|
||||||
if info['status'] >= 400:
|
if info['status'] >= 400:
|
||||||
module.fail_json(msg="ACME request failed: CODE: {0} RESULT: {1}".format(info['status'], result))
|
raise ModuleFailException("ACME request failed: CODE: {0} RESULT: {1}".format(info['status'], result))
|
||||||
return result
|
return result
|
||||||
|
|
||||||
|
|
||||||
|
@ -392,9 +416,9 @@ def get_cert_days(module, cert_file):
|
||||||
not_after_str = re.search(r"\s+Not After\s*:\s+(.*)", out.decode('utf8')).group(1)
|
not_after_str = re.search(r"\s+Not After\s*:\s+(.*)", out.decode('utf8')).group(1)
|
||||||
not_after = datetime.fromtimestamp(time.mktime(time.strptime(not_after_str, '%b %d %H:%M:%S %Y %Z')))
|
not_after = datetime.fromtimestamp(time.mktime(time.strptime(not_after_str, '%b %d %H:%M:%S %Y %Z')))
|
||||||
except AttributeError:
|
except AttributeError:
|
||||||
module.fail_json(msg="No 'Not after' date found in {0}".format(cert_file))
|
raise ModuleFailException("No 'Not after' date found in {0}".format(cert_file))
|
||||||
except ValueError:
|
except ValueError:
|
||||||
module.fail_json(msg="Failed to parse 'Not after' date of {0}".format(cert_file))
|
raise ModuleFailException("Failed to parse 'Not after' date of {0}".format(cert_file))
|
||||||
now = datetime.utcnow()
|
now = datetime.utcnow()
|
||||||
return (not_after - now).days
|
return (not_after - now).days
|
||||||
|
|
||||||
|
@ -414,10 +438,10 @@ def write_file(module, dest, content):
|
||||||
except Exception as err:
|
except Exception as err:
|
||||||
try:
|
try:
|
||||||
f.close()
|
f.close()
|
||||||
except:
|
except Exception as e:
|
||||||
pass
|
pass
|
||||||
os.remove(tmpsrc)
|
os.remove(tmpsrc)
|
||||||
module.fail_json(msg="failed to create temporary content file: %s" % to_native(err), exception=traceback.format_exc())
|
raise ModuleFailException("failed to create temporary content file: %s" % to_native(err), exception=traceback.format_exc())
|
||||||
f.close()
|
f.close()
|
||||||
checksum_src = None
|
checksum_src = None
|
||||||
checksum_dest = None
|
checksum_dest = None
|
||||||
|
@ -425,34 +449,34 @@ def write_file(module, dest, content):
|
||||||
if not os.path.exists(tmpsrc):
|
if not os.path.exists(tmpsrc):
|
||||||
try:
|
try:
|
||||||
os.remove(tmpsrc)
|
os.remove(tmpsrc)
|
||||||
except:
|
except Exception as e:
|
||||||
pass
|
pass
|
||||||
module.fail_json(msg="Source %s does not exist" % (tmpsrc))
|
raise ModuleFailException("Source %s does not exist" % (tmpsrc))
|
||||||
if not os.access(tmpsrc, os.R_OK):
|
if not os.access(tmpsrc, os.R_OK):
|
||||||
os.remove(tmpsrc)
|
os.remove(tmpsrc)
|
||||||
module.fail_json(msg="Source %s not readable" % (tmpsrc))
|
raise ModuleFailException("Source %s not readable" % (tmpsrc))
|
||||||
checksum_src = module.sha1(tmpsrc)
|
checksum_src = module.sha1(tmpsrc)
|
||||||
# check if there is no dest file
|
# check if there is no dest file
|
||||||
if os.path.exists(dest):
|
if os.path.exists(dest):
|
||||||
# raise an error if copy has no permission on dest
|
# raise an error if copy has no permission on dest
|
||||||
if not os.access(dest, os.W_OK):
|
if not os.access(dest, os.W_OK):
|
||||||
os.remove(tmpsrc)
|
os.remove(tmpsrc)
|
||||||
module.fail_json(msg="Destination %s not writable" % (dest))
|
raise ModuleFailException("Destination %s not writable" % (dest))
|
||||||
if not os.access(dest, os.R_OK):
|
if not os.access(dest, os.R_OK):
|
||||||
os.remove(tmpsrc)
|
os.remove(tmpsrc)
|
||||||
module.fail_json(msg="Destination %s not readable" % (dest))
|
raise ModuleFailException("Destination %s not readable" % (dest))
|
||||||
checksum_dest = module.sha1(dest)
|
checksum_dest = module.sha1(dest)
|
||||||
else:
|
else:
|
||||||
if not os.access(os.path.dirname(dest), os.W_OK):
|
if not os.access(os.path.dirname(dest), os.W_OK):
|
||||||
os.remove(tmpsrc)
|
os.remove(tmpsrc)
|
||||||
module.fail_json(msg="Destination dir %s not writable" % (os.path.dirname(dest)))
|
raise ModuleFailException("Destination dir %s not writable" % (os.path.dirname(dest)))
|
||||||
if checksum_src != checksum_dest:
|
if checksum_src != checksum_dest:
|
||||||
try:
|
try:
|
||||||
shutil.copyfile(tmpsrc, dest)
|
shutil.copyfile(tmpsrc, dest)
|
||||||
changed = True
|
changed = True
|
||||||
except Exception as err:
|
except Exception as err:
|
||||||
os.remove(tmpsrc)
|
os.remove(tmpsrc)
|
||||||
module.fail_json(msg="failed to copy %s to %s: %s" % (tmpsrc, dest, to_native(err)), exception=traceback.format_exc())
|
raise ModuleFailException("failed to copy %s to %s: %s" % (tmpsrc, dest, to_native(err)), exception=traceback.format_exc())
|
||||||
os.remove(tmpsrc)
|
os.remove(tmpsrc)
|
||||||
return changed
|
return changed
|
||||||
|
|
||||||
|
@ -477,11 +501,11 @@ class ACMEDirectory(object):
|
||||||
if self.version == 1:
|
if self.version == 1:
|
||||||
for key in ('new-reg', 'new-authz', 'new-cert'):
|
for key in ('new-reg', 'new-authz', 'new-cert'):
|
||||||
if key not in self.directory:
|
if key not in self.directory:
|
||||||
self.module.fail_json(msg="ACME directory does not seem to follow protocol ACME v1")
|
raise ModuleFailException("ACME directory does not seem to follow protocol ACME v1")
|
||||||
if self.version == 2:
|
if self.version == 2:
|
||||||
for key in ('newNonce', 'newAccount', 'newOrder'):
|
for key in ('newNonce', 'newAccount', 'newOrder'):
|
||||||
if key not in self.directory:
|
if key not in self.directory:
|
||||||
self.module.fail_json(msg="ACME directory does not seem to follow protocol ACME v2")
|
raise ModuleFailException("ACME directory does not seem to follow protocol ACME v2")
|
||||||
|
|
||||||
def __getitem__(self, key):
|
def __getitem__(self, key):
|
||||||
return self.directory[key]
|
return self.directory[key]
|
||||||
|
@ -492,7 +516,7 @@ class ACMEDirectory(object):
|
||||||
url = resource
|
url = resource
|
||||||
dummy, info = fetch_url(self.module, url, method='HEAD')
|
dummy, info = fetch_url(self.module, url, method='HEAD')
|
||||||
if info['status'] not in (200, 204):
|
if info['status'] not in (200, 204):
|
||||||
self.module.fail_json(msg="Failed to get replay-nonce, got status {0}".format(info['status']))
|
raise ModuleFailException("Failed to get replay-nonce, got status {0}".format(info['status']))
|
||||||
return info['replay-nonce']
|
return info['replay-nonce']
|
||||||
|
|
||||||
|
|
||||||
|
@ -530,14 +554,14 @@ class ACMEAccount(object):
|
||||||
except Exception as err:
|
except Exception as err:
|
||||||
try:
|
try:
|
||||||
f.close()
|
f.close()
|
||||||
except:
|
except Exception as e:
|
||||||
pass
|
pass
|
||||||
module.fail_json(msg="failed to create temporary content file: %s" % to_native(err), exception=traceback.format_exc())
|
raise ModuleFailException("failed to create temporary content file: %s" % to_native(err), exception=traceback.format_exc())
|
||||||
f.close()
|
f.close()
|
||||||
|
|
||||||
error, self.key_data = self._parse_account_key(self.key)
|
error, self.key_data = self._parse_account_key(self.key)
|
||||||
if error:
|
if error:
|
||||||
module.fail_json(msg="error while parsing account key: %s" % error)
|
raise ModuleFailException("error while parsing account key: %s" % error)
|
||||||
self.jwk = self.key_data['jwk']
|
self.jwk = self.key_data['jwk']
|
||||||
self.jws_header = {
|
self.jws_header = {
|
||||||
"alg": self.key_data['alg'],
|
"alg": self.key_data['alg'],
|
||||||
|
@ -656,7 +680,7 @@ class ACMEAccount(object):
|
||||||
payload64 = nopad_b64(self.module.jsonify(payload).encode('utf8'))
|
payload64 = nopad_b64(self.module.jsonify(payload).encode('utf8'))
|
||||||
protected64 = nopad_b64(self.module.jsonify(protected).encode('utf8'))
|
protected64 = nopad_b64(self.module.jsonify(protected).encode('utf8'))
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
self.module.fail_json(msg="Failed to encode payload / headers as JSON: {0}".format(e))
|
raise ModuleFailException("Failed to encode payload / headers as JSON: {0}".format(e))
|
||||||
|
|
||||||
openssl_sign_cmd = [self._openssl_bin, "dgst", "-{0}".format(self.key_data['hash']), "-sign", self.key]
|
openssl_sign_cmd = [self._openssl_bin, "dgst", "-{0}".format(self.key_data['hash']), "-sign", self.key]
|
||||||
sign_payload = "{0}.{1}".format(protected64, payload64).encode('utf8')
|
sign_payload = "{0}.{1}".format(protected64, payload64).encode('utf8')
|
||||||
|
@ -671,8 +695,8 @@ class ACMEAccount(object):
|
||||||
r"prim:\s+INTEGER\s+:([0-9A-F]{1,%s})\n" % expected_len,
|
r"prim:\s+INTEGER\s+:([0-9A-F]{1,%s})\n" % expected_len,
|
||||||
to_text(der_out, errors='surrogate_or_strict'))
|
to_text(der_out, errors='surrogate_or_strict'))
|
||||||
if len(sig) != 2:
|
if len(sig) != 2:
|
||||||
self.module.fail_json(
|
raise ModuleFailException(
|
||||||
msg="failed to generate Elliptic Curve signature; cannot parse DER output: {0}".format(
|
"failed to generate Elliptic Curve signature; cannot parse DER output: {0}".format(
|
||||||
to_text(der_out, errors='surrogate_or_strict')))
|
to_text(der_out, errors='surrogate_or_strict')))
|
||||||
sig[0] = (expected_len - len(sig[0])) * '0' + sig[0]
|
sig[0] = (expected_len - len(sig[0])) * '0' + sig[0]
|
||||||
sig[1] = (expected_len - len(sig[1])) * '0' + sig[1]
|
sig[1] = (expected_len - len(sig[1])) * '0' + sig[1]
|
||||||
|
@ -706,7 +730,7 @@ class ACMEAccount(object):
|
||||||
failed_tries += 1
|
failed_tries += 1
|
||||||
continue
|
continue
|
||||||
except ValueError:
|
except ValueError:
|
||||||
self.module.fail_json(msg="Failed to parse the ACME response: {0} {1}".format(url, content))
|
raise ModuleFailException("Failed to parse the ACME response: {0} {1}".format(url, content))
|
||||||
else:
|
else:
|
||||||
result = content
|
result = content
|
||||||
|
|
||||||
|
@ -757,7 +781,7 @@ class ACMEAccount(object):
|
||||||
# Account did exist
|
# Account did exist
|
||||||
return False
|
return False
|
||||||
else:
|
else:
|
||||||
self.module.fail_json(msg="Error registering: {0} {1}".format(info['status'], result))
|
raise ModuleFailException("Error registering: {0} {1}".format(info['status'], result))
|
||||||
|
|
||||||
def init_account(self):
|
def init_account(self):
|
||||||
'''
|
'''
|
||||||
|
@ -819,7 +843,7 @@ class ACMEClient(object):
|
||||||
self.finalize_uri = self.data.get('finalize_uri') if self.data else None
|
self.finalize_uri = self.data.get('finalize_uri') if self.data else None
|
||||||
|
|
||||||
if not os.path.exists(self.csr):
|
if not os.path.exists(self.csr):
|
||||||
module.fail_json(msg="CSR %s not found" % (self.csr))
|
raise ModuleFailException("CSR %s not found" % (self.csr))
|
||||||
|
|
||||||
self._openssl_bin = module.get_bin_path('openssl', True)
|
self._openssl_bin = module.get_bin_path('openssl', True)
|
||||||
self.domains = self._get_csr_domains()
|
self.domains = self._get_csr_domains()
|
||||||
|
@ -871,7 +895,7 @@ class ACMEClient(object):
|
||||||
|
|
||||||
result, info = self.account.send_signed_request(self.directory['new-authz'], new_authz)
|
result, info = self.account.send_signed_request(self.directory['new-authz'], new_authz)
|
||||||
if info['status'] not in [200, 201]:
|
if info['status'] not in [200, 201]:
|
||||||
self.module.fail_json(msg="Error requesting challenges: CODE: {0} RESULT: {1}".format(info['status'], result))
|
raise ModuleFailException("Error requesting challenges: CODE: {0} RESULT: {1}".format(info['status'], result))
|
||||||
else:
|
else:
|
||||||
result['uri'] = info['location']
|
result['uri'] = info['location']
|
||||||
return result
|
return result
|
||||||
|
@ -935,7 +959,7 @@ class ACMEClient(object):
|
||||||
error_details += ' DETAILS: {0};'.format(challenge['error']['detail'])
|
error_details += ' DETAILS: {0};'.format(challenge['error']['detail'])
|
||||||
else:
|
else:
|
||||||
error_details += ';'
|
error_details += ';'
|
||||||
self.module.fail_json(msg="{0}: {1}".format(error.format(domain), error_details))
|
raise ModuleFailException("{0}: {1}".format(error.format(domain), error_details))
|
||||||
|
|
||||||
def _validate_challenges(self, domain, auth):
|
def _validate_challenges(self, domain, auth):
|
||||||
'''
|
'''
|
||||||
|
@ -956,7 +980,7 @@ class ACMEClient(object):
|
||||||
}
|
}
|
||||||
result, info = self.account.send_signed_request(uri, challenge_response)
|
result, info = self.account.send_signed_request(uri, challenge_response)
|
||||||
if info['status'] not in [200, 202]:
|
if info['status'] not in [200, 202]:
|
||||||
self.module.fail_json(msg="Error validating challenge: CODE: {0} RESULT: {1}".format(info['status'], result))
|
raise ModuleFailException("Error validating challenge: CODE: {0} RESULT: {1}".format(info['status'], result))
|
||||||
|
|
||||||
status = ''
|
status = ''
|
||||||
|
|
||||||
|
@ -993,7 +1017,7 @@ class ACMEClient(object):
|
||||||
}
|
}
|
||||||
result, info = self.account.send_signed_request(self.finalize_uri, new_cert)
|
result, info = self.account.send_signed_request(self.finalize_uri, new_cert)
|
||||||
if info['status'] not in [200]:
|
if info['status'] not in [200]:
|
||||||
self.module.fail_json(msg="Error new cert: CODE: {0} RESULT: {1}".format(info['status'], result))
|
raise ModuleFailException("Error new cert: CODE: {0} RESULT: {1}".format(info['status'], result))
|
||||||
|
|
||||||
order = info['location']
|
order = info['location']
|
||||||
|
|
||||||
|
@ -1004,7 +1028,7 @@ class ACMEClient(object):
|
||||||
status = result['status']
|
status = result['status']
|
||||||
|
|
||||||
if status != 'valid':
|
if status != 'valid':
|
||||||
self.module.fail_json(msg="Error new cert: CODE: {0} STATUS: {1} RESULT: {2}".format(info['status'], status, result))
|
raise ModuleFailException("Error new cert: CODE: {0} STATUS: {1} RESULT: {2}".format(info['status'], status, result))
|
||||||
|
|
||||||
return result['certificate']
|
return result['certificate']
|
||||||
|
|
||||||
|
@ -1028,7 +1052,7 @@ class ACMEClient(object):
|
||||||
content = info.get('body')
|
content = info.get('body')
|
||||||
|
|
||||||
if not content or not info['content-type'].startswith('application/pem-certificate-chain'):
|
if not content or not info['content-type'].startswith('application/pem-certificate-chain'):
|
||||||
self.module.fail_json(msg="Cannot download certificate chain from {0}: {1} (headers: {2})".format(url, content, info))
|
raise ModuleFailException("Cannot download certificate chain from {0}: {1} (headers: {2})".format(url, content, info))
|
||||||
|
|
||||||
cert = None
|
cert = None
|
||||||
chain = []
|
chain = []
|
||||||
|
@ -1057,7 +1081,7 @@ class ACMEClient(object):
|
||||||
chain.append(self._der_to_pem(chain_result.read()))
|
chain.append(self._der_to_pem(chain_result.read()))
|
||||||
|
|
||||||
if cert is None or current:
|
if cert is None or current:
|
||||||
self.module.fail_json(msg="Failed to parse certificate chain download from {0}: {1} (headers: {2})".format(url, content, info))
|
raise ModuleFailException("Failed to parse certificate chain download from {0}: {1} (headers: {2})".format(url, content, info))
|
||||||
return {'cert': cert, 'chain': chain}
|
return {'cert': cert, 'chain': chain}
|
||||||
|
|
||||||
def _new_cert_v1(self):
|
def _new_cert_v1(self):
|
||||||
|
@ -1086,7 +1110,7 @@ class ACMEClient(object):
|
||||||
chain = [self._der_to_pem(chain_result.read())]
|
chain = [self._der_to_pem(chain_result.read())]
|
||||||
|
|
||||||
if info['status'] not in [200, 201]:
|
if info['status'] not in [200, 201]:
|
||||||
self.module.fail_json(msg="Error new cert: CODE: {0} RESULT: {1}".format(info['status'], result))
|
raise ModuleFailException("Error new cert: CODE: {0} RESULT: {1}".format(info['status'], result))
|
||||||
else:
|
else:
|
||||||
return {'cert': self._der_to_pem(result), 'uri': info['location'], 'chain': chain}
|
return {'cert': self._der_to_pem(result), 'uri': info['location'], 'chain': chain}
|
||||||
|
|
||||||
|
@ -1107,7 +1131,7 @@ class ACMEClient(object):
|
||||||
result, info = self.account.send_signed_request(self.directory['newOrder'], new_order)
|
result, info = self.account.send_signed_request(self.directory['newOrder'], new_order)
|
||||||
|
|
||||||
if info['status'] not in [201]:
|
if info['status'] not in [201]:
|
||||||
self.module.fail_json(msg="Error new order: CODE: {0} RESULT: {1}".format(info['status'], result))
|
raise ModuleFailException("Error new order: CODE: {0} RESULT: {1}".format(info['status'], result))
|
||||||
|
|
||||||
for identifier, auth_uri in zip(result['identifiers'], result['authorizations']):
|
for identifier, auth_uri in zip(result['identifiers'], result['authorizations']):
|
||||||
domain = identifier['value']
|
domain = identifier['value']
|
||||||
|
@ -1183,7 +1207,7 @@ class ACMEClient(object):
|
||||||
for domain in self.domains:
|
for domain in self.domains:
|
||||||
auth = self.authorizations.get(domain)
|
auth = self.authorizations.get(domain)
|
||||||
if auth is None:
|
if auth is None:
|
||||||
self.module.fail_json(msg='Found no authorization information for "{0}"!'.format(domain))
|
raise ModuleFailException('Found no authorization information for "{0}"!'.format(domain))
|
||||||
if 'status' not in auth:
|
if 'status' not in auth:
|
||||||
self._fail_challenge(domain, auth, 'Authorization for {0} returned no status')
|
self._fail_challenge(domain, auth, 'Authorization for {0} returned no status')
|
||||||
if auth['status'] != 'valid':
|
if auth['status'] != 'valid':
|
||||||
|
@ -1211,6 +1235,32 @@ class ACMEClient(object):
|
||||||
if self.chain_dest and write_file(self.module, self.chain_dest, ("\n".join(chain)).encode('utf8')):
|
if self.chain_dest and write_file(self.module, self.chain_dest, ("\n".join(chain)).encode('utf8')):
|
||||||
self.changed = True
|
self.changed = True
|
||||||
|
|
||||||
|
def deactivate_authzs(self):
|
||||||
|
'''
|
||||||
|
Deactivates all valid authz's. Does not raise exceptions.
|
||||||
|
https://community.letsencrypt.org/t/authorization-deactivation/19860/2
|
||||||
|
https://tools.ietf.org/html/draft-ietf-acme-acme-09#section-7.5.2
|
||||||
|
'''
|
||||||
|
authz_deactivate = {
|
||||||
|
'status': 'deactivated'
|
||||||
|
}
|
||||||
|
if self.version == 1:
|
||||||
|
authz_deactivate['resource'] = 'authz'
|
||||||
|
if self.authorizations:
|
||||||
|
for domain in self.domains:
|
||||||
|
auth = self.authorizations.get(domain)
|
||||||
|
if auth is None or auth.get('status') != 'valid':
|
||||||
|
continue
|
||||||
|
try:
|
||||||
|
result, info = self.account.send_signed_request(auth['uri'], authz_deactivate)
|
||||||
|
if 200 <= info['status'] < 300 and result.get('status') == 'deactivated':
|
||||||
|
auth['status'] = 'deactivated'
|
||||||
|
except Exception as e:
|
||||||
|
# Ignore errors on deactivating authzs
|
||||||
|
pass
|
||||||
|
if auth.get('status') != 'deactivated':
|
||||||
|
self.module.warn(warning='Could not deactivate authz object {0}.'.format(auth['uri']))
|
||||||
|
|
||||||
|
|
||||||
def main():
|
def main():
|
||||||
module = AnsibleModule(
|
module = AnsibleModule(
|
||||||
|
@ -1230,6 +1280,7 @@ def main():
|
||||||
chain_dest=dict(required=False, default=None, aliases=['chain'], type='path'),
|
chain_dest=dict(required=False, default=None, aliases=['chain'], type='path'),
|
||||||
remaining_days=dict(required=False, default=10, type='int'),
|
remaining_days=dict(required=False, default=10, type='int'),
|
||||||
validate_certs=dict(required=False, default=True, type='bool'),
|
validate_certs=dict(required=False, default=True, type='bool'),
|
||||||
|
deactivate_authzs=dict(required=False, default=False, type='bool'),
|
||||||
),
|
),
|
||||||
required_one_of=(
|
required_one_of=(
|
||||||
['account_key_src', 'account_key_content'],
|
['account_key_src', 'account_key_content'],
|
||||||
|
@ -1250,6 +1301,7 @@ def main():
|
||||||
'This should only be done for testing against a local ACME server for ' +
|
'This should only be done for testing against a local ACME server for ' +
|
||||||
'development purposes, but *never* for production purposes.')
|
'development purposes, but *never* for production purposes.')
|
||||||
|
|
||||||
|
try:
|
||||||
if module.params.get('dest'):
|
if module.params.get('dest'):
|
||||||
cert_days = get_cert_days(module, module.params['dest'])
|
cert_days = get_cert_days(module, module.params['dest'])
|
||||||
else:
|
else:
|
||||||
|
@ -1269,8 +1321,12 @@ def main():
|
||||||
client.start_challenges()
|
client.start_challenges()
|
||||||
else:
|
else:
|
||||||
# Second run: finish challenges, and get certificate
|
# Second run: finish challenges, and get certificate
|
||||||
|
try:
|
||||||
client.finish_challenges()
|
client.finish_challenges()
|
||||||
client.get_certificate()
|
client.get_certificate()
|
||||||
|
finally:
|
||||||
|
if module.params['deactivate_authzs']:
|
||||||
|
client.deactivate_authzs()
|
||||||
data, data_dns = client.get_challenges_data()
|
data, data_dns = client.get_challenges_data()
|
||||||
module.exit_json(
|
module.exit_json(
|
||||||
changed=client.changed,
|
changed=client.changed,
|
||||||
|
@ -1284,6 +1340,8 @@ def main():
|
||||||
)
|
)
|
||||||
else:
|
else:
|
||||||
module.exit_json(changed=False, cert_days=cert_days)
|
module.exit_json(changed=False, cert_days=cert_days)
|
||||||
|
except ModuleFailException as e:
|
||||||
|
e.do_fail(module)
|
||||||
|
|
||||||
|
|
||||||
if __name__ == '__main__':
|
if __name__ == '__main__':
|
||||||
|
|
Loading…
Reference in a new issue