diff --git a/changelogs/fragments/842-acme-certificate-order-errors.yml b/changelogs/fragments/842-acme-certificate-order-errors.yml new file mode 100644 index 00000000..176c5a2c --- /dev/null +++ b/changelogs/fragments/842-acme-certificate-order-errors.yml @@ -0,0 +1,6 @@ +minor_changes: + - "acme_certificate - add options ``order_creation_error_strategy`` and ``order_creation_max_retries`` + which allow to configure the error handling behavior if creating a new ACME order fails. This is + particularly important when using the ``include_renewal_cert_id`` option, and the default value + ``auto`` for ``order_creation_error_strategy`` tries to gracefully handle related errors + (https://github.com/ansible-collections/community.crypto/pull/842)." diff --git a/plugins/module_utils/acme/certificate.py b/plugins/module_utils/acme/certificate.py index 4966b6b0..8a6a856b 100644 --- a/plugins/module_utils/acme/certificate.py +++ b/plugins/module_utils/acme/certificate.py @@ -19,6 +19,7 @@ from ansible_collections.community.crypto.plugins.module_utils.acme.account impo ) from ansible_collections.community.crypto.plugins.module_utils.acme.challenges import ( + Authorization, wait_for_validation, ) @@ -63,6 +64,8 @@ class ACMECertificateClient(object): account = ACMEAccount(self.client) self.account = account self.order_uri = module.params.get('order_uri') + self.order_creation_error_strategy = module.params.get('order_creation_error_strategy', 'auto') + self.order_creation_max_retries = module.params.get('order_creation_max_retries', 3) # Make sure account exists dummy, account_data = self.account.setup_account(allow_creation=False) @@ -102,7 +105,15 @@ class ACMECertificateClient(object): ''' if self.identifiers is None: raise ModuleFailException('No identifiers have been provided') - order = Order.create(self.client, self.identifiers, replaces_cert_id=replaces_cert_id, profile=profile) + order = Order.create_with_error_handling( + self.client, + self.identifiers, + error_strategy=self.order_creation_error_strategy, + error_max_retries=self.order_creation_max_retries, + replaces_cert_id=replaces_cert_id, + profile=profile, + message_callback=self.module.warn, + ) self.order_uri = order.url order.load_authorizations(self.client) return order @@ -248,11 +259,22 @@ class ACMECertificateClient(object): https://community.letsencrypt.org/t/authorization-deactivation/19860/2 https://tools.ietf.org/html/rfc8555#section-7.5.2 ''' - for authz in order.authorizations.values(): - try: - authz.deactivate(self.client) - except Exception: - # ignore errors - pass - if authz.status != 'deactivated': - self.module.warn(warning='Could not deactivate authz object {0}.'.format(authz.url)) + if len(order.authorization_uris) > len(order.authorizations): + for authz_uri in order.authorization_uris: + authz = None + try: + authz = Authorization.deactivate_url(self.client, authz_uri) + except Exception: + # ignore errors + pass + if authz is None or authz.status != 'deactivated': + self.module.warn(warning='Could not deactivate authz object {0}.'.format(authz_uri)) + else: + for authz in order.authorizations.values(): + try: + authz.deactivate(self.client) + except Exception: + # ignore errors + pass + if authz.status != 'deactivated': + self.module.warn(warning='Could not deactivate authz object {0}.'.format(authz.url)) diff --git a/plugins/module_utils/acme/challenges.py b/plugins/module_utils/acme/challenges.py index 640a54ab..492ca5f0 100644 --- a/plugins/module_utils/acme/challenges.py +++ b/plugins/module_utils/acme/challenges.py @@ -322,6 +322,23 @@ class Authorization(object): return True return False + @classmethod + def deactivate_url(cls, client, url): + ''' + Deactivates this authorization. + https://community.letsencrypt.org/t/authorization-deactivation/19860/2 + https://tools.ietf.org/html/rfc8555#section-7.5.2 + ''' + authz = cls(url) + authz_deactivate = { + 'status': 'deactivated' + } + if client.version == 1: + authz_deactivate['resource'] = 'authz' + result, info = client.send_signed_request(url, authz_deactivate, fail_on_error=True) + authz._setup(client, result) + return authz + def wait_for_validation(authzs, client): ''' diff --git a/plugins/module_utils/acme/errors.py b/plugins/module_utils/acme/errors.py index c29831be..0fc5af28 100644 --- a/plugins/module_utils/acme/errors.py +++ b/plugins/module_utils/acme/errors.py @@ -84,6 +84,8 @@ class ACMEProtocolException(ModuleFailException): pass extras = extras or dict() + error_code = None + error_type = None if msg is None: msg = 'ACME request failed' @@ -94,7 +96,9 @@ class ACMEProtocolException(ModuleFailException): code = info['status'] extras['http_url'] = url extras['http_status'] = code + error_code = code if code is not None and code >= 400 and content_json is not None and 'type' in content_json: + error_type = content_json['type'] if 'status' in content_json and content_json['status'] != code: code_msg = 'status {problem_code} (HTTP status: {http_code})'.format( http_code=format_http_status(code), problem_code=content_json['status']) @@ -134,6 +138,8 @@ class ACMEProtocolException(ModuleFailException): ) self.problem = {} self.subproblems = [] + self.error_code = error_code + self.error_type = error_type for k, v in extras.items(): setattr(self, k, v) diff --git a/plugins/module_utils/acme/orders.py b/plugins/module_utils/acme/orders.py index 5734abf6..5a287d27 100644 --- a/plugins/module_utils/acme/orders.py +++ b/plugins/module_utils/acme/orders.py @@ -87,6 +87,55 @@ class Order(object): client.directory['newOrder'], new_order, error_msg='Failed to start new order', expected_status_codes=[201]) return cls.from_json(client, result, info['location']) + @classmethod + def create_with_error_handling( + cls, + client, + identifiers, + error_strategy='auto', + error_max_retries=3, + replaces_cert_id=None, + profile=None, + message_callback=None, + ): + """ + error_strategy can be one of the following strings: + + * ``fail``: simply fail. (Same behavior as ``Order.create()``.) + * ``retry_without_replaces_cert_id``: if ``replaces_cert_id`` is not ``None``, set it to ``None`` and retry. + The only exception is an error of type ``urn:ietf:params:acme:error:alreadyReplaced``, that indicates that + the certificate was already replaced. + * ``auto``: try to be clever. Right now this is identical to ``retry_without_replaces_cert_id``, but that can + change at any time in the future. + * ``always``: always retry until ``error_max_retries`` has been reached. + """ + tries = 0 + while True: + tries += 1 + try: + return cls.create(client, identifiers, replaces_cert_id=replaces_cert_id, profile=profile) + except ACMEProtocolException as exc: + if tries <= error_max_retries + 1 and error_strategy != 'fail': + if error_strategy == 'always': + continue + + if ( + error_strategy in ('auto', 'retry_without_replaces_cert_id') and + replaces_cert_id is not None and + not (exc.error_code == 409 and exc.error_type == 'urn:ietf:params:acme:error:alreadyReplaced') + ): + replaces_cert_id = None + if message_callback: + message_callback( + 'Stop passing `replaces` due to error {code} {type} when creating ACME order'.format( + code=exc.error_code, + type=exc.error_type, + ) + ) + continue + + raise + def refresh(self, client): result, dummy = client.get_request(self.url) changed = self.data != result diff --git a/plugins/modules/acme_certificate.py b/plugins/modules/acme_certificate.py index 90b150ce..7ca930ff 100644 --- a/plugins/modules/acme_certificate.py +++ b/plugins/modules/acme_certificate.py @@ -243,8 +243,6 @@ options: - Determines whether to request renewal of an existing certificate according to L(the ACME ARI draft 3, https://www.ietf.org/archive/id/draft-ietf-acme-ari-03.html#section-5). - This is only used when the certificate specified in O(dest) or O(fullchain_dest) already exists. - - V(never) never sends the certificate ID of the certificate to renew. V(always) will always send it. - - V(when_ari_supported) only sends the certificate ID if the ARI endpoint is found in the ACME directory. - Generally you should use V(when_ari_supported) if you know that the ACME service supports a compatible draft (or final version, once it is out) of the ARI extension. V(always) should never be necessary. If you are not sure, or if you receive strange errors on invalid C(replaces) values in order objects, use V(never), which also happens to be the @@ -252,15 +250,19 @@ options: - ACME servers might refuse to create new orders with C(replaces) for certificates that already have an existing order. This can happen if this module is used to create an order, and then the playbook/role fails in case the challenges cannot be set up. If the playbook/role does not record the order data to continue with the existing order, but tries - to create a new one on the next run, creating the new order might fail. For this reason, this option should only be - set to a value different from V(never) if the role/playbook using it keeps track of order data accross restarts, or - if it takes care to deactivate orders whose processing is aborted. Orders can be deactivated with the + to create a new one on the next run, creating the new order might fail. If O(order_creation_error_strategy=fail) + this will make the module fail. O(order_creation_error_strategy=auto) and + O(order_creation_error_strategy=retry_without_replaces_cert_id) will avoid this by leaving away C(replaces) + on retries. + - If O(order_creation_error_strategy=fail), for the above reason, this option should only be set to a value different + from V(never) if the role/playbook using it keeps track of order data accross restarts, or if it takes care to + deactivate orders whose processing is aborted. Orders can be deactivated with the M(community.crypto.acme_certificate_deactivate_authz) module. type: str choices: - - never - - when_ari_supported - - always + never: Never send the certificate ID of the certificate to renew. + when_ari_supported: Only send the certificate ID if the ARI endpoint is found in the ACME directory. + always: Will always send the certificate ID of the certificate to renew. default: never version_added: 2.20.0 profile: @@ -271,6 +273,32 @@ options: for more information. type: str version_added: 2.24.0 + order_creation_error_strategy: + description: + - Selects the error handling strategy for ACME protocol errors if creating a new ACME order fails. + type: str + choices: + auto: + - An unspecified algorithm that tries to be clever. + - Right now identical to V(retry_without_replaces_cert_id). + always: + - Always retry, until the limit in O(order_creation_max_retries) has been reached. + fail: + - Simply fail in case of errors. Do not attempt to retry. + - This has been the default before community.crypto 2.24.0. + retry_without_replaces_cert_id: + - If O(include_renewal_cert_id) is present, creating the order will be tried again without C(replaces). + - The only exception is an error of type C(urn:ietf:params:acme:error:alreadyReplaced), that indicates that + the certificate was already replaced. This usually means something went wrong and the user should investigate. + default: auto + version_added: 2.24.0 + order_creation_max_retries: + description: + - Depending on the strategy selected in O(order_creation_error_strategy), will retry creating new orders + for at most the specified amount of times. + type: int + default: 3 + version_added: 2.24.0 """ EXAMPLES = r""" @@ -613,6 +641,8 @@ class ACMECertificateClient(object): self.select_chain_matcher = [] self.include_renewal_cert_id = module.params['include_renewal_cert_id'] self.profile = module.params['profile'] + self.order_creation_error_strategy = module.params['order_creation_error_strategy'] + self.order_creation_max_retries = module.params['order_creation_max_retries'] if self.module.params['select_chain']: for criterium_idx, criterium in enumerate(self.module.params['select_chain']): @@ -712,7 +742,15 @@ class ACMECertificateClient(object): cert_info=cert_info, none_if_required_information_is_missing=True, ) - self.order = Order.create(self.client, self.identifiers, replaces_cert_id, profile=self.profile) + self.order = Order.create_with_error_handling( + self.client, + self.identifiers, + error_strategy=self.order_creation_error_strategy, + error_max_retries=self.order_creation_max_retries, + replaces_cert_id=replaces_cert_id, + profile=self.profile, + message_callback=self.module.warn, + ) self.order_uri = self.order.url self.order.load_authorizations(self.client) self.authorizations.update(self.order.authorizations) @@ -899,6 +937,8 @@ def main(): )), include_renewal_cert_id=dict(type='str', choices=['never', 'when_ari_supported', 'always'], default='never'), profile=dict(type='str'), + order_creation_error_strategy=dict(type='str', default='auto', choices=['auto', 'always', 'fail', 'retry_without_replaces_cert_id']), + order_creation_max_retries=dict(type='int', default=3), ) argument_spec.update( required_one_of=[ diff --git a/plugins/modules/acme_certificate_order_create.py b/plugins/modules/acme_certificate_order_create.py index 75c1d539..08102281 100644 --- a/plugins/modules/acme_certificate_order_create.py +++ b/plugins/modules/acme_certificate_order_create.py @@ -113,15 +113,18 @@ options: according to L(the ACME ARI draft 3, https://www.ietf.org/archive/id/draft-ietf-acme-ari-03.html#section-5). - This certificate ID must be computed as specified in L(the ACME ARI draft 3, https://www.ietf.org/archive/id/draft-ietf-acme-ari-03.html#section-4.1). - It is returned as RV(community.crypto.acme_certificate_renewal_info#module:cert_id) of the + It is returned as return value RV(community.crypto.acme_certificate_renewal_info#module:cert_id) of the M(community.crypto.acme_certificate_renewal_info) module. - ACME servers might refuse to create new orders that indicate to replace a certificate for which an active replacement order already exists. This can happen if this module is used to create an order, and then the playbook/role fails in case the challenges cannot be set up. If the playbook/role does not record the order data to continue with the existing order, but tries to create a new one on the next run, - creating the new order might fail. For this reason, this option should only be used if the role/playbook - using it keeps track of order data accross restarts, or if it takes care to deactivate orders whose - processing is aborted. Orders can be deactivated with the + creating the new order might fail. If O(order_creation_error_strategy=fail) this will make the module fail. + O(order_creation_error_strategy=auto) and O(order_creation_error_strategy=retry_without_replaces_cert_id) + will avoid this by leaving away C(replaces) on retries. + - If O(order_creation_error_strategy=fail), for the above reason, this option should only be used + if the role/playbook using it keeps track of order data accross restarts, or if it takes care to + deactivate orders whose processing is aborted. Orders can be deactivated with the M(community.crypto.acme_certificate_deactivate_authz) module. type: str profile: @@ -131,6 +134,29 @@ options: L(draft-aaron-acme-profiles-00, https://datatracker.ietf.org/doc/draft-aaron-acme-profiles/) for more information. type: str + order_creation_error_strategy: + description: + - Selects the error handling strategy for ACME protocol errors if creating a new ACME order fails. + type: str + choices: + auto: + - An unspecified algorithm that tries to be clever. + - Right now identical to V(retry_without_replaces_cert_id). + always: + - Always retry, until the limit in O(order_creation_max_retries) has been reached. + fail: + - Simply fail in case of errors. Do not attempt to retry. + retry_without_replaces_cert_id: + - If O(replaces_cert_id) is present, creating the order will be tried again without C(replaces). + - The only exception is an error of type C(urn:ietf:params:acme:error:alreadyReplaced), that indicates that + the certificate was already replaced. This usually means something went wrong and the user should investigate. + default: auto + order_creation_max_retries: + description: + - Depending on the strategy selected in O(order_creation_error_strategy), will retry creating new orders + for at most the specified amount of times. + type: int + default: 3 ''' EXAMPLES = r''' @@ -370,6 +396,8 @@ def main(): deactivate_authzs=dict(type='bool', default=True), replaces_cert_id=dict(type='str'), profile=dict(type='str'), + order_creation_error_strategy=dict(type='str', default='auto', choices=['auto', 'always', 'fail', 'retry_without_replaces_cert_id']), + order_creation_max_retries=dict(type='int', default=3), ) module = argument_spec.create_ansible_module() if module.params['acme_version'] == 1: @@ -382,7 +410,7 @@ def main(): profile = module.params['profile'] if profile is not None: - meta_profiles = (client.directory.get('meta') or {}).get('profiles') or {} + meta_profiles = (client.client.directory.get('meta') or {}).get('profiles') or {} if not meta_profiles: raise ModuleFailException(msg='The ACME CA does not support profiles. Please omit the "profile" option.') if profile not in meta_profiles: diff --git a/plugins/modules/acme_certificate_order_info.py b/plugins/modules/acme_certificate_order_info.py index 78103e42..5837e329 100644 --- a/plugins/modules/acme_certificate_order_info.py +++ b/plugins/modules/acme_certificate_order_info.py @@ -175,6 +175,13 @@ order: - A URL for the certificate that has been issued in response to this order. type: str returned: when the certificate has been issued + replaces: + description: + - If the order was created to replace an existing certificate using the C(replaces) mechanism from + L(draft-ietf-acme-ari, https://datatracker.ietf.org/doc/draft-ietf-acme-ari/), this provides the + certificate ID of the certificate that will be replaced by this order. + type: str + returned: when the certificate order is replacing a certificate through draft-ietf-acme-ari authorizations_by_identifier: description: - A dictionary mapping identifiers to their authorization objects. diff --git a/tests/integration/targets/acme_certificate_order/tasks/impl.yml b/tests/integration/targets/acme_certificate_order/tasks/impl.yml index 54a954c6..632571b9 100644 --- a/tests/integration/targets/acme_certificate_order/tasks/impl.yml +++ b/tests/integration/targets/acme_certificate_order/tasks/impl.yml @@ -3,23 +3,23 @@ # GNU General Public License v3.0+ (see LICENSES/GPL-3.0-or-later.txt or https://www.gnu.org/licenses/gpl-3.0.txt) # SPDX-License-Identifier: GPL-3.0-or-later -- name: Generate random domain name +- name: "({{ select_crypto_backend }}) Generate random domain name" set_fact: domain_name: "host{{ '%0x' % ((2**32) | random) }}.example.com" -- name: Generate account key +- name: "({{ select_crypto_backend }}) Generate account key" openssl_privatekey: path: "{{ remote_tmp_dir }}/accountkey.pem" type: ECC curve: secp256r1 force: true -- name: Parse account keys (to ease debugging some test failures) +- name: "({{ select_crypto_backend }}) Parse account keys (to ease debugging some test failures)" openssl_privatekey_info: path: "{{ remote_tmp_dir }}/accountkey.pem" return_private_key_data: true -- name: Create ACME account +- name: "({{ select_crypto_backend }}) Create ACME account" acme_account: acme_directory: "{{ acme_directory_url }}" acme_version: 2 @@ -30,14 +30,14 @@ state: present register: account -- name: Generate certificate key +- name: "({{ select_crypto_backend }}) Generate certificate key" openssl_privatekey: path: "{{ remote_tmp_dir }}/cert.key" type: ECC curve: secp256r1 force: true -- name: Generate certificate CSR +- name: "({{ select_crypto_backend }}) Generate certificate CSR" openssl_csr: path: "{{ remote_tmp_dir }}/cert.csr" privatekey_path: "{{ remote_tmp_dir }}/cert.key" @@ -46,7 +46,7 @@ return_content: true register: csr -- name: Create certificate order +- name: "({{ select_crypto_backend }}) Create certificate order" acme_certificate_order_create: acme_directory: "{{ acme_directory_url }}" acme_version: 2 @@ -56,11 +56,11 @@ csr: "{{ remote_tmp_dir }}/cert.csr" register: order -- name: Show order information +- name: "({{ select_crypto_backend }}) Show order information" debug: var: order -- name: Check order +- name: "({{ select_crypto_backend }}) Check order" assert: that: - order is changed @@ -80,7 +80,7 @@ - order.challenge_data_dns['_acme-challenge.' ~ domain_name] | length == 1 - order.account_uri == account.account_uri -- name: Get order information +- name: "({{ select_crypto_backend }}) Get order information" acme_certificate_order_info: acme_directory: "{{ acme_directory_url }}" acme_version: 2 @@ -90,11 +90,11 @@ order_uri: "{{ order.order_uri }}" register: order_info_1 -- name: Show order information +- name: "({{ select_crypto_backend }}) Show order information" debug: var: order_info_1 -- name: Check order information +- name: "({{ select_crypto_backend }}) Check order information" assert: that: - order_info_1 is not changed @@ -115,10 +115,11 @@ - order_info_1.order.authorizations[0] == order_info_1.authorizations_by_identifier['dns:' ~ domain_name].uri - "'certificate' not in order_info_1.order" - order_info_1.order.status == 'pending' + - order_info_1.order.replaces is not defined - order_info_1.order_uri == order.order_uri - order_info_1.account_uri == account.account_uri -- name: Create HTTP challenges +- name: "({{ select_crypto_backend }}) Create HTTP challenges" uri: url: "http://{{ acme_host }}:5000/http/{{ item.identifier }}/{{ item.challenges['http-01'].resource[('.well-known/acme-challenge/'|length):] }}" method: PUT @@ -129,7 +130,7 @@ loop: "{{ order.challenge_data }}" when: "'http-01' in item.challenges" -- name: Let the challenge be validated +- name: "({{ select_crypto_backend }}) Let the challenge be validated" community.crypto.acme_certificate_order_validate: acme_directory: "{{ acme_directory_url }}" acme_version: 2 @@ -140,18 +141,18 @@ challenge: http-01 register: validate_1 -- name: Check validation result +- name: "({{ select_crypto_backend }}) Check validation result" assert: that: - validate_1 is changed - validate_1.account_uri == account.account_uri -- name: Wait until we know that the challenges have been validated for ansible-core <= 2.11 +- name: "({{ select_crypto_backend }}) Wait until we know that the challenges have been validated for ansible-core <= 2.11" pause: seconds: 5 when: ansible_version.full is version('2.12', '<') -- name: Get order information +- name: "({{ select_crypto_backend }}) Get order information" acme_certificate_order_info: acme_directory: "{{ acme_directory_url }}" acme_version: 2 @@ -161,11 +162,11 @@ order_uri: "{{ order.order_uri }}" register: order_info_2 -- name: Show order information +- name: "({{ select_crypto_backend }}) Show order information" debug: var: order_info_2 -- name: Check order information +- name: "({{ select_crypto_backend }}) Check order information" assert: that: - order_info_2 is not changed @@ -186,10 +187,11 @@ - order_info_2.order.authorizations[0] == order_info_2.authorizations_by_identifier['dns:' ~ domain_name].uri - "'certificate' not in order_info_2.order" - order_info_2.order.status in ['pending', 'ready'] + - order_info_2.order.replaces is not defined - order_info_2.order_uri == order.order_uri - order_info_2.account_uri == account.account_uri -- name: Let the challenge be validated (idempotent) +- name: "({{ select_crypto_backend }}) Let the challenge be validated (idempotent)" community.crypto.acme_certificate_order_validate: acme_directory: "{{ acme_directory_url }}" acme_version: 2 @@ -200,13 +202,13 @@ challenge: http-01 register: validate_2 -- name: Check validation result +- name: "({{ select_crypto_backend }}) Check validation result" assert: that: - validate_2 is not changed - validate_2.account_uri == account.account_uri -- name: Retrieve the cert and intermediate certificate +- name: "({{ select_crypto_backend }}) Retrieve the cert and intermediate certificate" community.crypto.acme_certificate_order_finalize: acme_directory: "{{ acme_directory_url }}" acme_version: 2 @@ -214,6 +216,7 @@ account_key_src: "{{ remote_tmp_dir }}/accountkey.pem" select_crypto_backend: "{{ select_crypto_backend }}" order_uri: "{{ order.order_uri }}" + deactivate_authzs: never retrieve_all_alternates: true csr: "{{ remote_tmp_dir }}/cert.csr" cert_dest: "{{ remote_tmp_dir }}/cert.pem" @@ -221,7 +224,7 @@ fullchain_dest: "{{ remote_tmp_dir }}/cert-fullchain.pem" register: finalize_1 -- name: Check finalization result +- name: "({{ select_crypto_backend }}) Check finalization result" assert: that: - finalize_1 is changed @@ -232,7 +235,7 @@ - finalize_1.selected_chain.chain.startswith('-----BEGIN CERTIFICATE-----\nMII') - finalize_1.selected_chain.full_chain == finalize_1.selected_chain.cert + finalize_1.selected_chain.chain -- name: Read files from disk +- name: "({{ select_crypto_backend }}) Read files from disk" slurp: src: "{{ remote_tmp_dir }}/{{ item }}.pem" loop: @@ -241,14 +244,14 @@ - cert-fullchain register: slurp -- name: Compare finalization result with files on disk +- name: "({{ select_crypto_backend }}) Compare finalization result with files on disk" assert: that: - finalize_1.selected_chain.cert == slurp.results[0].content | b64decode - finalize_1.selected_chain.chain == slurp.results[1].content | b64decode - finalize_1.selected_chain.full_chain == slurp.results[2].content | b64decode -- name: Get order information +- name: "({{ select_crypto_backend }}) Get order information" acme_certificate_order_info: acme_directory: "{{ acme_directory_url }}" acme_version: 2 @@ -258,11 +261,11 @@ order_uri: "{{ order.order_uri }}" register: order_info_3 -- name: Show order information +- name: "({{ select_crypto_backend }}) Show order information" debug: var: order_info_3 -- name: Check order information +- name: "({{ select_crypto_backend }}) Check order information" assert: that: - order_info_3 is not changed @@ -284,7 +287,7 @@ - order_info_3.order_uri == order.order_uri - order_info_3.account_uri == account.account_uri -- name: Retrieve the cert and intermediate certificate (idempotent) +- name: "({{ select_crypto_backend }}) Retrieve the cert and intermediate certificate (idempotent, but deactivate authzs)" community.crypto.acme_certificate_order_finalize: acme_directory: "{{ acme_directory_url }}" acme_version: 2 @@ -300,7 +303,7 @@ fullchain_dest: "{{ remote_tmp_dir }}/cert-fullchain.pem" register: finalize_2 -- name: Check finalization result +- name: "({{ select_crypto_backend }}) Check finalization result" assert: that: - finalize_2 is not changed @@ -312,7 +315,7 @@ - finalize_2.selected_chain.full_chain == finalize_2.selected_chain.cert + finalize_2.selected_chain.chain - finalize_2.selected_chain == finalize_1.selected_chain -- name: Get order information +- name: "({{ select_crypto_backend }}) Get order information" acme_certificate_order_info: acme_directory: "{{ acme_directory_url }}" acme_version: 2 @@ -322,11 +325,11 @@ order_uri: "{{ order.order_uri }}" register: order_info_4 -- name: Show order information +- name: "({{ select_crypto_backend }}) Show order information" debug: var: order_info_4 -- name: Check order information +- name: "({{ select_crypto_backend }}) Check order information" assert: that: - order_info_4 is not changed @@ -347,3 +350,385 @@ - order_info_4.order.status == 'deactivated' - order_info_4.order_uri == order.order_uri - order_info_4.account_uri == account.account_uri + +# Test ARI support + +- when: acme_supports_ari + block: + - name: "({{ select_crypto_backend }}) Get certificate renewal information" + acme_certificate_renewal_info: + acme_directory: "{{ acme_directory_url }}" + acme_version: 2 + validate_certs: false + # account_key_src: "{{ remote_tmp_dir }}/accountkey.pem" + select_crypto_backend: "{{ select_crypto_backend }}" + certificate_path: "{{ remote_tmp_dir }}/cert.pem" + register: cert_info + + - name: "({{ select_crypto_backend }}) Verify information" + assert: + that: + - cert_info.supports_ari == true + - cert_info.should_renew == false + - cert_info.cert_id is string + + - name: "({{ select_crypto_backend }}) Create replacement order 1" + acme_certificate_order_create: + acme_directory: "{{ acme_directory_url }}" + acme_version: 2 + validate_certs: false + account_key_src: "{{ remote_tmp_dir }}/accountkey.pem" + select_crypto_backend: "{{ select_crypto_backend }}" + csr: "{{ remote_tmp_dir }}/cert.csr" + replaces_cert_id: "{{ cert_info.cert_id }}" + order_creation_error_strategy: fail + register: replacement_order_1 + + - name: "({{ select_crypto_backend }}) Get replacement order 1 information" + acme_certificate_order_info: + acme_directory: "{{ acme_directory_url }}" + acme_version: 2 + validate_certs: false + account_key_src: "{{ remote_tmp_dir }}/accountkey.pem" + select_crypto_backend: "{{ select_crypto_backend }}" + order_uri: "{{ replacement_order_1.order_uri }}" + register: order_info_5 + + - name: "({{ select_crypto_backend }}) Check replacement order 1" + assert: + that: + - replacement_order_1 is changed + - replacement_order_1.order_uri.startswith('https://' ~ acme_host ~ ':14000/') + - replacement_order_1.challenge_data | length == 1 + - replacement_order_1.challenge_data[0].identifier_type == 'dns' + - replacement_order_1.challenge_data[0].identifier == domain_name + - replacement_order_1.challenge_data[0].challenges | length >= 2 + - "'http-01' in replacement_order_1.challenge_data[0].challenges" + - "'dns-01' in replacement_order_1.challenge_data[0].challenges" + - replacement_order_1.challenge_data[0].challenges['http-01'].resource.startswith('.well-known/acme-challenge/') + - replacement_order_1.challenge_data[0].challenges['http-01'].resource_value is string + - replacement_order_1.challenge_data[0].challenges['dns-01'].record == '_acme-challenge.' ~ domain_name + - replacement_order_1.challenge_data[0].challenges['dns-01'].resource == '_acme-challenge' + - replacement_order_1.challenge_data[0].challenges['dns-01'].resource_value is string + - replacement_order_1.challenge_data_dns | length == 1 + - replacement_order_1.challenge_data_dns['_acme-challenge.' ~ domain_name] | length == 1 + - replacement_order_1.account_uri == account.account_uri + - replacement_order_1.order_uri not in [order.order_uri] + + - name: "({{ select_crypto_backend }}) Check replacement order 1 information" + assert: + that: + - order_info_5 is not changed + - order_info_5.authorizations_by_identifier | length == 1 + - order_info_5.authorizations_by_identifier['dns:' ~ domain_name].identifier.type == 'dns' + - order_info_5.authorizations_by_identifier['dns:' ~ domain_name].identifier.value == domain_name + - order_info_5.authorizations_by_identifier['dns:' ~ domain_name].status == 'pending' + - (order_info_5.authorizations_by_identifier['dns:' ~ domain_name].challenges | selectattr('type', 'equalto', 'http-01') | first).status == 'pending' + - (order_info_5.authorizations_by_identifier['dns:' ~ domain_name].challenges | selectattr('type', 'equalto', 'dns-01') | first).status == 'pending' + - order_info_5.authorizations_by_status['deactivated'] | length == 0 + - order_info_5.authorizations_by_status['expired'] | length == 0 + - order_info_5.authorizations_by_status['invalid'] | length == 0 + - order_info_5.authorizations_by_status['pending'] | length == 1 + - order_info_5.authorizations_by_status['pending'][0] == 'dns:' ~ domain_name + - order_info_5.authorizations_by_status['revoked'] | length == 0 + - order_info_5.authorizations_by_status['valid'] | length == 0 + - order_info_5.order.authorizations | length == 1 + - order_info_5.order.authorizations[0] == order_info_5.authorizations_by_identifier['dns:' ~ domain_name].uri + - "'certificate' not in order_info_5.order" + - order_info_5.order.status == 'pending' + - order_info_5.order.replaces == cert_info.cert_id + - order_info_5.order_uri == replacement_order_1.order_uri + - order_info_5.account_uri == account.account_uri + + # Right now Pebble does not reject duplicate replacement orders... + + - when: false # TODO get Pebble improved + block: + - name: "({{ select_crypto_backend }}) Create replacement order 2 (should fail)" + acme_certificate_order_create: + acme_directory: "{{ acme_directory_url }}" + acme_version: 2 + validate_certs: false + account_key_src: "{{ remote_tmp_dir }}/accountkey.pem" + select_crypto_backend: "{{ select_crypto_backend }}" + csr: "{{ remote_tmp_dir }}/cert.csr" + replaces_cert_id: "{{ cert_info.cert_id }}" + order_creation_error_strategy: fail + register: replacement_order_2 + ignore_errors: true + + - name: "({{ select_crypto_backend }}) Check replacement order 2" + assert: + that: + - replacement_order_2 is failed + - >- + replacement_order_2.msg.startswith( + 'Failed to start new order for ' + ~ acme_directory_url + ~ '/order-plz with status 409 Conflict. Error urn:ietf:params:acme:error:malformed:' + ) + + - name: "({{ select_crypto_backend }}) Create replacement order 3 with error handling" + acme_certificate_order_create: + acme_directory: "{{ acme_directory_url }}" + acme_version: 2 + validate_certs: false + account_key_src: "{{ remote_tmp_dir }}/accountkey.pem" + select_crypto_backend: "{{ select_crypto_backend }}" + csr: "{{ remote_tmp_dir }}/cert.csr" + replaces_cert_id: "{{ cert_info.cert_id }}" + order_creation_error_strategy: retry_without_replaces_cert_id + register: replacement_order_3 + + - name: "({{ select_crypto_backend }}) Get replacement order 3 information" + acme_certificate_order_info: + acme_directory: "{{ acme_directory_url }}" + acme_version: 2 + validate_certs: false + account_key_src: "{{ remote_tmp_dir }}/accountkey.pem" + select_crypto_backend: "{{ select_crypto_backend }}" + order_uri: "{{ replacement_order_3.order_uri }}" + register: order_info_6 + + - name: "({{ select_crypto_backend }}) Check replacement order 3" + assert: + that: + - replacement_order_3 is changed + - replacement_order_3.order_uri.startswith('https://' ~ acme_host ~ ':14000/') + - replacement_order_3.challenge_data | length == 1 + - replacement_order_3.challenge_data[0].identifier_type == 'dns' + - replacement_order_3.challenge_data[0].identifier == domain_name + - replacement_order_3.challenge_data[0].challenges | length >= 2 + - "'http-01' in replacement_order_3.challenge_data[0].challenges" + - "'dns-01' in replacement_order_3.challenge_data[0].challenges" + - replacement_order_3.challenge_data[0].challenges['http-01'].resource.startswith('.well-known/acme-challenge/') + - replacement_order_3.challenge_data[0].challenges['http-01'].resource_value is string + - replacement_order_3.challenge_data[0].challenges['dns-01'].record == '_acme-challenge.' ~ domain_name + - replacement_order_3.challenge_data[0].challenges['dns-01'].resource == '_acme-challenge' + - replacement_order_3.challenge_data[0].challenges['dns-01'].resource_value is string + - replacement_order_3.challenge_data_dns | length == 1 + - replacement_order_3.challenge_data_dns['_acme-challenge.' ~ domain_name] | length == 1 + - replacement_order_3.account_uri == account.account_uri + - replacement_order_3.order_uri not in [order.order_uri, replacement_order_1.order_uri] + - >- + 'Stop passing `replaces` due to error 409 urn:ietf:params:acme:error:malformed when creating ACME order' in replacement_order_3.warnings + + - name: "({{ select_crypto_backend }}) Check replacement order 3 information" + assert: + that: + - order_info_6 is not changed + - order_info_6.authorizations_by_identifier | length == 1 + - order_info_6.authorizations_by_identifier['dns:' ~ domain_name].identifier.type == 'dns' + - order_info_6.authorizations_by_identifier['dns:' ~ domain_name].identifier.value == domain_name + - order_info_6.authorizations_by_identifier['dns:' ~ domain_name].status == 'pending' + - (order_info_6.authorizations_by_identifier['dns:' ~ domain_name].challenges | selectattr('type', 'equalto', 'http-01') | first).status == 'pending' + - (order_info_6.authorizations_by_identifier['dns:' ~ domain_name].challenges | selectattr('type', 'equalto', 'dns-01') | first).status == 'pending' + - order_info_6.authorizations_by_status['deactivated'] | length == 0 + - order_info_6.authorizations_by_status['expired'] | length == 0 + - order_info_6.authorizations_by_status['invalid'] | length == 0 + - order_info_6.authorizations_by_status['pending'] | length == 1 + - order_info_6.authorizations_by_status['pending'][0] == 'dns:' ~ domain_name + - order_info_6.authorizations_by_status['revoked'] | length == 0 + - order_info_6.authorizations_by_status['valid'] | length == 0 + - order_info_6.order.authorizations | length == 1 + - order_info_6.order.authorizations[0] == order_info_6.authorizations_by_identifier['dns:' ~ domain_name].uri + - "'certificate' not in order_info_6.order" + - order_info_6.order.status == 'pending' + - order_info_6.order.replaces is not defined + - order_info_6.order_uri == replacement_order_3.order_uri + - order_info_6.account_uri == account.account_uri + + - name: "({{ select_crypto_backend }}) Deactivate authzs for replacement order 3" + acme_certificate_deactivate_authz: + acme_directory: "{{ acme_directory_url }}" + acme_version: 2 + validate_certs: false + account_key_src: "{{ remote_tmp_dir }}/accountkey.pem" + select_crypto_backend: "{{ select_crypto_backend }}" + order_uri: "{{ replacement_order_3.order_uri }}" + + # Complete replacement order 1 + + - name: "({{ select_crypto_backend }}) Create HTTP challenges (replacement order 1)" + uri: + url: "http://{{ acme_host }}:5000/http/{{ item.identifier }}/{{ item.challenges['http-01'].resource[('.well-known/acme-challenge/'|length):] }}" + method: PUT + body_format: raw + body: "{{ item.challenges['http-01'].resource_value }}" + headers: + content-type: "application/octet-stream" + loop: "{{ replacement_order_1.challenge_data }}" + when: "'http-01' in item.challenges" + + - name: "({{ select_crypto_backend }}) Let the challenge be validated (replacement order 1)" + community.crypto.acme_certificate_order_validate: + acme_directory: "{{ acme_directory_url }}" + acme_version: 2 + validate_certs: false + account_key_src: "{{ remote_tmp_dir }}/accountkey.pem" + select_crypto_backend: "{{ select_crypto_backend }}" + order_uri: "{{ replacement_order_1.order_uri }}" + challenge: http-01 + + - name: "({{ select_crypto_backend }}) Retrieve the cert and intermediate certificate (replacement order 1)" + community.crypto.acme_certificate_order_finalize: + acme_directory: "{{ acme_directory_url }}" + acme_version: 2 + validate_certs: false + account_key_src: "{{ remote_tmp_dir }}/accountkey.pem" + select_crypto_backend: "{{ select_crypto_backend }}" + order_uri: "{{ replacement_order_1.order_uri }}" + deactivate_authzs: on_success + retrieve_all_alternates: true + csr: "{{ remote_tmp_dir }}/cert.csr" + cert_dest: "{{ remote_tmp_dir }}/cert-repl.pem" + chain_dest: "{{ remote_tmp_dir }}/cert-repl-chain.pem" + fullchain_dest: "{{ remote_tmp_dir }}/cert-repl-fullchain.pem" + + # Pebble *does* check against *completed* replacement orders + - when: true + block: + - name: "({{ select_crypto_backend }}) Create replacement order 4 (should fail)" + acme_certificate_order_create: + acme_directory: "{{ acme_directory_url }}" + acme_version: 2 + validate_certs: false + account_key_src: "{{ remote_tmp_dir }}/accountkey.pem" + select_crypto_backend: "{{ select_crypto_backend }}" + csr: "{{ remote_tmp_dir }}/cert.csr" + replaces_cert_id: "{{ cert_info.cert_id }}" + order_creation_error_strategy: fail + register: replacement_order_4 + ignore_errors: true + + - name: "({{ select_crypto_backend }}) Check replacement order 4" + assert: + that: + - replacement_order_4 is failed + - replacement_order_4.msg.startswith('Failed to start new order for https://' ~ acme_host) + - >- + ' with status 409 Conflict. Error urn:ietf:params:acme:error:malformed: ' in replacement_order_4.msg + + - name: "({{ select_crypto_backend }}) Create replacement order 5 with error handling" + acme_certificate_order_create: + acme_directory: "{{ acme_directory_url }}" + acme_version: 2 + validate_certs: false + account_key_src: "{{ remote_tmp_dir }}/accountkey.pem" + select_crypto_backend: "{{ select_crypto_backend }}" + csr: "{{ remote_tmp_dir }}/cert.csr" + replaces_cert_id: "{{ cert_info.cert_id }}" + order_creation_error_strategy: retry_without_replaces_cert_id + register: replacement_order_5 + + - name: "({{ select_crypto_backend }}) Get replacement order 5 information" + acme_certificate_order_info: + acme_directory: "{{ acme_directory_url }}" + acme_version: 2 + validate_certs: false + account_key_src: "{{ remote_tmp_dir }}/accountkey.pem" + select_crypto_backend: "{{ select_crypto_backend }}" + order_uri: "{{ replacement_order_5.order_uri }}" + register: order_info_7 + + - name: "({{ select_crypto_backend }}) Check replacement order 5" + assert: + that: + - replacement_order_5 is changed + - replacement_order_5.order_uri.startswith('https://' ~ acme_host ~ ':14000/') + - replacement_order_5.challenge_data | length == 1 + - replacement_order_5.challenge_data[0].identifier_type == 'dns' + - replacement_order_5.challenge_data[0].identifier == domain_name + - replacement_order_5.challenge_data[0].challenges | length >= 2 + - "'http-01' in replacement_order_5.challenge_data[0].challenges" + - "'dns-01' in replacement_order_5.challenge_data[0].challenges" + - replacement_order_5.challenge_data[0].challenges['http-01'].resource.startswith('.well-known/acme-challenge/') + - replacement_order_5.challenge_data[0].challenges['http-01'].resource_value is string + - replacement_order_5.challenge_data[0].challenges['dns-01'].record == '_acme-challenge.' ~ domain_name + - replacement_order_5.challenge_data[0].challenges['dns-01'].resource == '_acme-challenge' + - replacement_order_5.challenge_data[0].challenges['dns-01'].resource_value is string + - replacement_order_5.challenge_data_dns | length == 1 + - replacement_order_5.challenge_data_dns['_acme-challenge.' ~ domain_name] | length == 1 + - replacement_order_5.account_uri == account.account_uri + - replacement_order_5.order_uri not in [order.order_uri, replacement_order_1.order_uri] + - >- + 'Stop passing `replaces` due to error 409 urn:ietf:params:acme:error:malformed when creating ACME order' in replacement_order_5.warnings + + - name: "({{ select_crypto_backend }}) Check replacement order 5 information" + assert: + that: + - order_info_7 is not changed + - order_info_7.authorizations_by_identifier | length == 1 + - order_info_7.authorizations_by_identifier['dns:' ~ domain_name].identifier.type == 'dns' + - order_info_7.authorizations_by_identifier['dns:' ~ domain_name].identifier.value == domain_name + - order_info_7.authorizations_by_identifier['dns:' ~ domain_name].status == 'pending' + - (order_info_7.authorizations_by_identifier['dns:' ~ domain_name].challenges | selectattr('type', 'equalto', 'http-01') | first).status == 'pending' + - (order_info_7.authorizations_by_identifier['dns:' ~ domain_name].challenges | selectattr('type', 'equalto', 'dns-01') | first).status == 'pending' + - order_info_7.authorizations_by_status['deactivated'] | length == 0 + - order_info_7.authorizations_by_status['expired'] | length == 0 + - order_info_7.authorizations_by_status['invalid'] | length == 0 + - order_info_7.authorizations_by_status['pending'] | length == 1 + - order_info_7.authorizations_by_status['pending'][0] == 'dns:' ~ domain_name + - order_info_7.authorizations_by_status['revoked'] | length == 0 + - order_info_7.authorizations_by_status['valid'] | length == 0 + - order_info_7.order.authorizations | length == 1 + - order_info_7.order.authorizations[0] == order_info_7.authorizations_by_identifier['dns:' ~ domain_name].uri + - "'certificate' not in order_info_7.order" + - order_info_7.order.status == 'pending' + - order_info_7.order.replaces is not defined + - order_info_7.order_uri == replacement_order_5.order_uri + - order_info_7.account_uri == account.account_uri + + - name: "({{ select_crypto_backend }}) Deactivate authzs for replacement order 5" + acme_certificate_deactivate_authz: + acme_directory: "{{ acme_directory_url }}" + acme_version: 2 + validate_certs: false + account_key_src: "{{ remote_tmp_dir }}/accountkey.pem" + select_crypto_backend: "{{ select_crypto_backend }}" + order_uri: "{{ replacement_order_5.order_uri }}" + +# Test invalid profile + +- when: acme_supports_profiles + block: + - name: "({{ select_crypto_backend }}) Create order with invalid profile (should fail)" + acme_certificate_order_create: + acme_directory: "{{ acme_directory_url }}" + acme_version: 2 + validate_certs: false + account_key_src: "{{ remote_tmp_dir }}/accountkey.pem" + select_crypto_backend: "{{ select_crypto_backend }}" + csr: "{{ remote_tmp_dir }}/cert.csr" + profile: does-not-exist + order_creation_error_strategy: fail + register: invalid_profile_order + ignore_errors: true + + - name: "({{ select_crypto_backend }}) Check invalid profile order" + assert: + that: + - invalid_profile_order is failed + - invalid_profile_order.msg == "The ACME CA does not support selected profile 'does-not-exist'." + +# Test profile when server does not support it + +- when: not acme_supports_profiles + block: + - name: "({{ select_crypto_backend }}) Create order with profile when server does not support it (should fail)" + acme_certificate_order_create: + acme_directory: "{{ acme_directory_url }}" + acme_version: 2 + validate_certs: false + account_key_src: "{{ remote_tmp_dir }}/accountkey.pem" + select_crypto_backend: "{{ select_crypto_backend }}" + csr: "{{ remote_tmp_dir }}/cert.csr" + profile: default + register: profile_without_server_support + ignore_errors: true + + - name: "({{ select_crypto_backend }}) Check profile without server support order" + assert: + that: + - profile_without_server_support is failed + - profile_without_server_support.msg == 'The ACME CA does not support profiles. Please omit the "profile" option.' diff --git a/tests/sanity/ignore-2.10.txt b/tests/sanity/ignore-2.10.txt index 5a3ddf3c..881fe6db 100644 --- a/tests/sanity/ignore-2.10.txt +++ b/tests/sanity/ignore-2.10.txt @@ -6,6 +6,8 @@ .azure-pipelines/scripts/publish-codecov.py metaclass-boilerplate docs/docsite/rst/guide_selfsigned.rst rstcheck plugins/modules/acme_account_info.py validate-modules:return-syntax-error +plugins/modules/acme_certificate.py validate-modules:invalid-documentation +plugins/modules/acme_certificate_order_create.py validate-modules:invalid-documentation plugins/modules/acme_certificate_order_create.py validate-modules:return-syntax-error plugins/modules/acme_certificate_order_info.py validate-modules:return-syntax-error plugins/modules/acme_certificate_order_validate.py validate-modules:return-syntax-error diff --git a/tests/sanity/ignore-2.11.txt b/tests/sanity/ignore-2.11.txt index b11b40e9..1b383227 100644 --- a/tests/sanity/ignore-2.11.txt +++ b/tests/sanity/ignore-2.11.txt @@ -5,6 +5,8 @@ .azure-pipelines/scripts/publish-codecov.py future-import-boilerplate .azure-pipelines/scripts/publish-codecov.py metaclass-boilerplate plugins/modules/acme_account_info.py validate-modules:return-syntax-error +plugins/modules/acme_certificate.py validate-modules:invalid-documentation +plugins/modules/acme_certificate_order_create.py validate-modules:invalid-documentation plugins/modules/acme_certificate_order_create.py validate-modules:return-syntax-error plugins/modules/acme_certificate_order_info.py validate-modules:return-syntax-error plugins/modules/acme_certificate_order_validate.py validate-modules:return-syntax-error diff --git a/tests/sanity/ignore-2.12.txt b/tests/sanity/ignore-2.12.txt index 71bda10a..b7da8d66 100644 --- a/tests/sanity/ignore-2.12.txt +++ b/tests/sanity/ignore-2.12.txt @@ -1,5 +1,7 @@ .azure-pipelines/scripts/publish-codecov.py replace-urlopen plugins/modules/acme_account_info.py validate-modules:return-syntax-error +plugins/modules/acme_certificate.py validate-modules:invalid-documentation +plugins/modules/acme_certificate_order_create.py validate-modules:invalid-documentation plugins/modules/acme_certificate_order_create.py validate-modules:return-syntax-error plugins/modules/acme_certificate_order_info.py validate-modules:return-syntax-error plugins/modules/acme_certificate_order_validate.py validate-modules:return-syntax-error diff --git a/tests/sanity/ignore-2.13.txt b/tests/sanity/ignore-2.13.txt index 6e3b04e3..397ad66e 100644 --- a/tests/sanity/ignore-2.13.txt +++ b/tests/sanity/ignore-2.13.txt @@ -1,5 +1,7 @@ .azure-pipelines/scripts/publish-codecov.py replace-urlopen plugins/lookup/gpg_fingerprint.py validate-modules:invalid-documentation +plugins/modules/acme_certificate.py validate-modules:invalid-documentation +plugins/modules/acme_certificate_order_create.py validate-modules:invalid-documentation plugins/modules/ecs_certificate.py validate-modules:invalid-documentation plugins/modules/get_certificate.py validate-modules:invalid-documentation plugins/modules/luks_device.py validate-modules:invalid-documentation diff --git a/tests/sanity/ignore-2.14.txt b/tests/sanity/ignore-2.14.txt index 6e3b04e3..397ad66e 100644 --- a/tests/sanity/ignore-2.14.txt +++ b/tests/sanity/ignore-2.14.txt @@ -1,5 +1,7 @@ .azure-pipelines/scripts/publish-codecov.py replace-urlopen plugins/lookup/gpg_fingerprint.py validate-modules:invalid-documentation +plugins/modules/acme_certificate.py validate-modules:invalid-documentation +plugins/modules/acme_certificate_order_create.py validate-modules:invalid-documentation plugins/modules/ecs_certificate.py validate-modules:invalid-documentation plugins/modules/get_certificate.py validate-modules:invalid-documentation plugins/modules/luks_device.py validate-modules:invalid-documentation diff --git a/tests/sanity/ignore-2.15.txt b/tests/sanity/ignore-2.15.txt index c5ae58af..87da0412 100644 --- a/tests/sanity/ignore-2.15.txt +++ b/tests/sanity/ignore-2.15.txt @@ -1,4 +1,6 @@ .azure-pipelines/scripts/publish-codecov.py replace-urlopen +plugins/modules/acme_certificate.py validate-modules:invalid-documentation +plugins/modules/acme_certificate_order_create.py validate-modules:invalid-documentation plugins/modules/luks_device.py validate-modules:invalid-documentation tests/ee/roles/smoke/library/smoke_ipaddress.py shebang tests/ee/roles/smoke/library/smoke_pyyaml.py shebang diff --git a/tests/sanity/ignore-2.16.txt b/tests/sanity/ignore-2.16.txt index a4cdbdb3..16907c34 100644 --- a/tests/sanity/ignore-2.16.txt +++ b/tests/sanity/ignore-2.16.txt @@ -1,3 +1,5 @@ +plugins/modules/acme_certificate.py validate-modules:invalid-documentation +plugins/modules/acme_certificate_order_create.py validate-modules:invalid-documentation plugins/modules/luks_device.py validate-modules:invalid-documentation tests/ee/roles/smoke/library/smoke_ipaddress.py shebang tests/ee/roles/smoke/library/smoke_pyyaml.py shebang diff --git a/tests/sanity/ignore-2.9.txt b/tests/sanity/ignore-2.9.txt index 5d05bbbb..15d6374d 100644 --- a/tests/sanity/ignore-2.9.txt +++ b/tests/sanity/ignore-2.9.txt @@ -5,6 +5,8 @@ .azure-pipelines/scripts/publish-codecov.py future-import-boilerplate .azure-pipelines/scripts/publish-codecov.py metaclass-boilerplate docs/docsite/rst/guide_selfsigned.rst rstcheck +plugins/modules/acme_certificate.py validate-modules:invalid-documentation +plugins/modules/acme_certificate_order_create.py validate-modules:invalid-documentation plugins/modules/acme_challenge_cert_helper.py validate-modules:return-syntax-error plugins/modules/ecs_certificate.py validate-modules:invalid-documentation plugins/modules/get_certificate.py validate-modules:invalid-documentation