From 31daeb4b8559c65705966da478d56c9914f6d7ee Mon Sep 17 00:00:00 2001 From: Will Thames Date: Tue, 19 Sep 2017 16:10:39 -0700 Subject: [PATCH] New module for querying ACM certificates (#29430) Not using AnsibleAWSModule so that it can be dropped into a module library of Ansible 2.3 --- .../modules/cloud/amazon/aws_acm_facts.py | 330 ++++++++++++++++++ 1 file changed, 330 insertions(+) create mode 100644 lib/ansible/modules/cloud/amazon/aws_acm_facts.py diff --git a/lib/ansible/modules/cloud/amazon/aws_acm_facts.py b/lib/ansible/modules/cloud/amazon/aws_acm_facts.py new file mode 100644 index 00000000000..6e435df5329 --- /dev/null +++ b/lib/ansible/modules/cloud/amazon/aws_acm_facts.py @@ -0,0 +1,330 @@ +#!/usr/bin/python +# Copyright (c) 2017 Ansible Project +# GNU General Public License v3.0+ (see COPYING or https://www.gnu.org/licenses/gpl-3.0.txt) + +ANSIBLE_METADATA = {'metadata_version': '1.1', + 'status': ['preview'], + 'supported_by': 'community'} + +DOCUMENTATION = ''' +module: aws_acm_facts +short_description: Retrieve certificate facts from AWS Certificate Manager service +description: + - Retrieve facts for ACM certificates +version_added: "2.5" +options: + name: + description: + - The name of an ACM certificate + status: + description: + - Status to filter the certificate results + choices: ['PENDING_VALIDATION', 'ISSUED', 'INACTIVE', 'EXPIRED', 'VALIDATION_TIMED_OUT'] + +requirements: + - boto3 +author: + - Will Thames (@willthames) +extends_documentation_fragment: aws +''' + +EXAMPLES = ''' +- name: obtain all ACM certificates + aws_acm_facts: + +- name: obtain all facts for a single ACM certificate + aws_acm_facts: + name: "*.example_com" + +- name: obtain all certificates pending validiation + aws_acm_facts: + statuses: + - PENDING_VALIDATION +''' + +RETURN = ''' +certificates: + description: A list of certificates + returned: always + type: complex + contains: + certificate: + description: The ACM Certificate body + returned: when certificate creation is complete + sample: '-----BEGIN CERTIFICATE-----\\nMII.....-----END CERTIFICATE-----\\n' + type: string + certificate_arn: + description: Certificate ARN + returned: always + sample: arn:aws:acm:ap-southeast-2:123456789012:certificate/abcd1234-abcd-1234-abcd-123456789abc + type: string + certificate_chain: + description: Full certificate chain for the certificate + returned: when certificate creation is complete + sample: '-----BEGIN CERTIFICATE-----\\nMII...\\n-----END CERTIFICATE-----\\n-----BEGIN CERTIFICATE-----\\n...' + type: string + created_at: + description: Date certificate was created + returned: always + sample: '2017-08-15T10:31:19+10:00' + type: string + domain_name: + description: Domain name for the certificate + returned: always + sample: '*.example.com' + type: string + domain_validation_options: + description: Options used by ACM to validate the certificate + returned: when certificate type is AMAZON_ISSUED + type: complex + contains: + domain_name: + description: Fully qualified domain name of the certificate + returned: always + sample: example.com + type: string + validation_domain: + description: The domain name ACM used to send validation emails + returned: always + sample: example.com + type: string + validation_emails: + description: A list of email addresses that ACM used to send domain validation emails + returned: always + sample: + - admin@example.com + - postmaster@example.com + type: list + validation_status: + description: Validation status of the domain + returned: always + sample: SUCCESS + type: string + failure_reason: + description: Reason certificate request failed + returned: only when certificate issuing failed + type: string + sample: NO_AVAILABLE_CONTACTS + in_use_by: + description: A list of ARNs for the AWS resources that are using the certificate. + returned: always + sample: [] + type: list + issued_at: + description: Date certificate was issued + returned: always + sample: '2017-01-01T00:00:00+10:00' + type: string + issuer: + description: Issuer of the certificate + returned: always + sample: Amazon + type: string + key_algorithm: + description: Algorithm used to generate the certificate + returned: always + sample: RSA-2048 + type: string + not_after: + description: Date after which the certificate is not valid + returned: always + sample: '2019-01-01T00:00:00+10:00' + type: string + not_before: + description: Date before which the certificate is not valid + returned: always + sample: '2017-01-01T00:00:00+10:00' + type: string + renewal_summary: + description: Information about managed renewal process + returned: when certificate is issued by Amazon and a renewal has been started + type: complex + contains: + domain_validation_options: + description: Options used by ACM to validate the certificate + returned: when certificate type is AMAZON_ISSUED + type: complex + contains: + domain_name: + description: Fully qualified domain name of the certificate + returned: always + sample: example.com + type: string + validation_domain: + description: The domain name ACM used to send validation emails + returned: always + sample: example.com + type: string + validation_emails: + description: A list of email addresses that ACM used to send domain validation emails + returned: always + sample: + - admin@example.com + - postmaster@example.com + type: list + validation_status: + description: Validation status of the domain + returned: always + sample: SUCCESS + type: string + renewal_status: + description: Status of the domain renewal + returned: always + sample: PENDING_AUTO_RENEWAL + type: string + revocation_reason: + description: Reason for certificate revocation + returned: when the certificate has been revoked + sample: SUPERCEDED + type: string + revoked_at: + description: Date certificate was revoked + returned: when the certificate has been revoked + sample: '2017-09-01T10:00:00+10:00' + type: string + serial: + description: The serial number of the certificate + returned: always + sample: 00:01:02:03:04:05:06:07:08:09:0a:0b:0c:0d:0e:0f + type: string + signature_algorithm: + description: Algorithm used to sign the certificate + returned: always + sample: SHA256WITHRSA + type: string + status: + description: Status of the certificate in ACM + returned: always + sample: ISSUED + type: string + subject: + description: The name of the entity that is associated with the public key contained in the certificate + returned: always + sample: CN=*.example.com + type: string + subject_alternative_names: + description: Subject Alternative Names for the certificate + returned: always + sample: + - '*.example.com' + type: list + tags: + description: Tags associated with the certificate + returned: always + type: dict + sample: + Application: helloworld + Environment: test + type: + description: The source of the certificate + returned: always + sample: AMAZON_ISSUED + type: string +''' + +import traceback + +from ansible.module_utils.basic import AnsibleModule +from ansible.module_utils.ec2 import boto3_conn, ec2_argument_spec, get_aws_connection_info +from ansible.module_utils.ec2 import camel_dict_to_snake_dict, AWSRetry, HAS_BOTO3, boto3_tag_list_to_ansible_dict + + +try: + import botocore +except ImportError: + pass # caught by imported HAS_BOTO3 + + +@AWSRetry.backoff(tries=5, delay=5, backoff=2.0) +def list_certificates_with_backoff(client, statuses=None): + paginator = client.get_paginator('list_certificates') + kwargs = dict() + if statuses: + kwargs['CertificateStatuses'] = statuses + return paginator.paginate(**kwargs).build_full_result()['CertificateSummaryList'] + + +@AWSRetry.backoff(tries=5, delay=5, backoff=2.0) +def get_certificate_with_backoff(client, certificate_arn): + response = client.get_certificate(CertificateArn=certificate_arn) + # strip out response metadata + return {'Certificate': response['Certificate'], + 'CertificateChain': response['CertificateChain']} + + +@AWSRetry.backoff(tries=5, delay=5, backoff=2.0) +def describe_certificate_with_backoff(client, certificate_arn): + return client.describe_certificate(CertificateArn=certificate_arn)['Certificate'] + + +@AWSRetry.backoff(tries=5, delay=5, backoff=2.0) +def list_certificate_tags_with_backoff(client, certificate_arn): + return client.list_tags_for_certificate(CertificateArn=certificate_arn)['Tags'] + + +def get_certificates(client, module, name=None, statuses=None): + try: + all_certificates = list_certificates_with_backoff(client, statuses) + except botocore.exceptions.ClientError as e: + module.fail_json(msg="Couldn't obtain certificates", + exception=traceback.format_exc(), + **camel_dict_to_snake_dict(e.response)) + if name: + certificates = [cert for cert in all_certificates + if cert['DomainName'] == name] + else: + certificates = all_certificates + + results = [] + for certificate in certificates: + try: + cert_data = describe_certificate_with_backoff(client, certificate['CertificateArn']) + except botocore.exceptions.ClientError as e: + module.fail_json(msg="Couldn't obtain certificate metadata for domain %s" % certificate['DomainName'], + exception=traceback.format_exc(), + **camel_dict_to_snake_dict(e.response)) + try: + cert_data.update(get_certificate_with_backoff(client, certificate['CertificateArn'])) + except botocore.exceptions.ClientError as e: + if e.response['Error']['Code'] != "RequestInProgressException": + module.fail_json(msg="Couldn't obtain certificate data for domain %s" % certificate['DomainName'], + exception=traceback.format_exc(), + **camel_dict_to_snake_dict(e.response)) + cert_data = camel_dict_to_snake_dict(cert_data) + try: + tags = list_certificate_tags_with_backoff(client, certificate['CertificateArn']) + except botocore.exceptions.ClientError as e: + module.fail_json(msg="Couldn't obtain tags for domain %s" % certificate['DomainName'], + exception=traceback.format_exc(), + **camel_dict_to_snake_dict(e.response)) + cert_data['tags'] = boto3_tag_list_to_ansible_dict(tags) + results.append(cert_data) + return results + + +def main(): + argument_spec = ec2_argument_spec() + argument_spec.update( + dict( + name=dict(), + statuses=dict(type='list'), + ) + ) + module = AnsibleModule(argument_spec=argument_spec, supports_check_mode=True) + + if not HAS_BOTO3: + module.fail_json('boto3 and botocore are required by this module') + + try: + region, ec2_url, aws_connect_kwargs = get_aws_connection_info(module, boto3=True) + client = boto3_conn(module, conn_type='client', resource='acm', + region=region, endpoint=ec2_url, **aws_connect_kwargs) + except (botocore.exceptions.NoCredentialsError, botocore.exceptions.ProfileNotFound) as e: + module.fail_json(msg="Can't authorize connection - " + str(e)) + + certificates = get_certificates(client, module, name=module.params['name'], statuses=module.params['statuses']) + module.exit_json(certificates=certificates) + + +if __name__ == '__main__': + main()