1#!/usr/local/bin/python3.8
2"""
3Copyright (c) 2017 Ansible Project
4GNU General Public License v3.0+ (see COPYING or https://www.gnu.org/licenses/gpl-3.0.txt)
5"""
6
7from __future__ import absolute_import, division, print_function
8__metaclass__ = type
9
10
11DOCUMENTATION = '''
12---
13module: aws_s3_bucket_info
14version_added: 1.0.0
15author: "Gerben Geijteman (@hyperized)"
16short_description: lists S3 buckets in AWS
17requirements:
18  - boto3 >= 1.4.4
19  - python >= 2.6
20description:
21    - Lists S3 buckets and details about those buckets.
22    - This module was called C(aws_s3_bucket_facts) before Ansible 2.9, returning C(ansible_facts).
23      Note that the M(community.aws.aws_s3_bucket_info) module no longer returns C(ansible_facts)!
24options:
25  name:
26    description:
27      - Name of bucket to query.
28    type: str
29    default: ""
30    version_added: 1.4.0
31  name_filter:
32    description:
33      - Limits buckets to only buckets who's name contain the string in I(name_filter).
34    type: str
35    default: ""
36    version_added: 1.4.0
37  bucket_facts:
38    description:
39      - Retrieve requested S3 bucket detailed information
40      - Each bucket_X option executes one API call, hence many options being set to C(true) will cause slower module execution.
41      - You can limit buckets by using the I(name) or I(name_filter) option.
42    suboptions:
43      bucket_accelerate_configuration:
44        description: Retrive S3 accelerate configuration.
45        type: bool
46        default: False
47      bucket_location:
48        description: Retrive S3 bucket location.
49        type: bool
50        default: False
51      bucket_replication:
52        description: Retrive S3 bucket replication.
53        type: bool
54        default: False
55      bucket_acl:
56        description: Retrive S3 bucket ACLs.
57        type: bool
58        default: False
59      bucket_logging:
60        description: Retrive S3 bucket logging.
61        type: bool
62        default: False
63      bucket_request_payment:
64        description: Retrive S3 bucket request payment.
65        type: bool
66        default: False
67      bucket_tagging:
68        description: Retrive S3 bucket tagging.
69        type: bool
70        default: False
71      bucket_cors:
72        description: Retrive S3 bucket CORS configuration.
73        type: bool
74        default: False
75      bucket_notification_configuration:
76        description: Retrive S3 bucket notification configuration.
77        type: bool
78        default: False
79      bucket_encryption:
80        description: Retrive S3 bucket encryption.
81        type: bool
82        default: False
83      bucket_ownership_controls:
84        description: Retrive S3 ownership controls.
85        type: bool
86        default: False
87      bucket_website:
88        description: Retrive S3 bucket website.
89        type: bool
90        default: False
91      bucket_policy:
92        description: Retrive S3 bucket policy.
93        type: bool
94        default: False
95      bucket_policy_status:
96        description: Retrive S3 bucket policy status.
97        type: bool
98        default: False
99      bucket_lifecycle_configuration:
100        description: Retrive S3 bucket lifecycle configuration.
101        type: bool
102        default: False
103      public_access_block:
104        description: Retrive S3 bucket public access block.
105        type: bool
106        default: False
107    type: dict
108    version_added: 1.4.0
109  transform_location:
110    description:
111      - S3 bucket location for default us-east-1 is normally reported as C(null).
112      - Setting this option to C(true) will return C(us-east-1) instead.
113      - Affects only queries with I(bucket_facts=true) and I(bucket_location=true).
114    type: bool
115    default: False
116    version_added: 1.4.0
117extends_documentation_fragment:
118- amazon.aws.aws
119- amazon.aws.ec2
120'''
121
122EXAMPLES = '''
123# Note: These examples do not set authentication details, see the AWS Guide for details.
124
125# Note: Only AWS S3 is currently supported
126
127# Lists all s3 buckets
128- community.aws.aws_s3_bucket_info:
129  register: result
130
131# Retrieve detailed bucket information
132- community.aws.aws_s3_bucket_info:
133    # Show only buckets with name matching
134    name_filter: your.testing
135    # Choose facts to retrieve
136    bucket_facts:
137      # bucket_accelerate_configuration: true
138      bucket_acl: true
139      bucket_cors: true
140      bucket_encryption: true
141      # bucket_lifecycle_configuration: true
142      bucket_location: true
143      # bucket_logging: true
144      # bucket_notification_configuration: true
145      # bucket_ownership_controls: true
146      # bucket_policy: true
147      # bucket_policy_status: true
148      # bucket_replication: true
149      # bucket_request_payment: true
150      # bucket_tagging: true
151      # bucket_website: true
152      # public_access_block: true
153    transform_location: true
154    register: result
155
156# Print out result
157- name: List buckets
158  ansible.builtin.debug:
159    msg: "{{ result['buckets'] }}"
160'''
161
162RETURN = '''
163bucket_list:
164  description: "List of buckets"
165  returned: always
166  type: complex
167  contains:
168    name:
169      description: Bucket name.
170      returned: always
171      type: str
172      sample: a-testing-bucket-name
173    creation_date:
174      description: Bucket creation date timestamp.
175      returned: always
176      type: str
177      sample: "2021-01-21T12:44:10+00:00"
178    public_access_block:
179      description: Bucket public access block configuration.
180      returned: when I(bucket_facts=true) and I(public_access_block=true)
181      type: complex
182      contains:
183        PublicAccessBlockConfiguration:
184          description: PublicAccessBlockConfiguration data.
185          returned: when PublicAccessBlockConfiguration is defined for the bucket
186          type: complex
187          contains:
188            BlockPublicAcls:
189              description: BlockPublicAcls setting value.
190              type: bool
191              sample: true
192            BlockPublicPolicy:
193              description: BlockPublicPolicy setting value.
194              type: bool
195              sample: true
196            IgnorePublicAcls:
197              description: IgnorePublicAcls setting value.
198              type: bool
199              sample: true
200            RestrictPublicBuckets:
201              description: RestrictPublicBuckets setting value.
202              type: bool
203              sample: true
204    bucket_name_filter:
205      description: String used to limit buckets. See I(name_filter).
206      returned: when I(name_filter) is defined
207      type: str
208      sample: filter-by-this-string
209    bucket_acl:
210      description: Bucket ACL configuration.
211      returned: when I(bucket_facts=true) and I(bucket_acl=true)
212      type: complex
213      contains:
214        Grants:
215          description: List of ACL grants.
216          type: list
217          sample: []
218        Owner:
219          description: Bucket owner information.
220          type: complex
221          contains:
222            DisplayName:
223              description: Bucket owner user display name.
224              returned: always
225              type: str
226              sample: username
227            ID:
228              description: Bucket owner user ID.
229              returned: always
230              type: str
231              sample: 123894e509349etc
232    bucket_cors:
233      description: Bucket CORS configuration.
234      returned: when I(bucket_facts=true) and I(bucket_cors=true)
235      type: complex
236      contains:
237        CORSRules:
238          description: Bucket CORS configuration.
239          returned: when CORS rules are defined for the bucket
240          type: list
241          sample: []
242    bucket_encryption:
243      description: Bucket encryption configuration.
244      returned: when I(bucket_facts=true) and I(bucket_encryption=true)
245      type: complex
246      contains:
247        ServerSideEncryptionConfiguration:
248          description: ServerSideEncryptionConfiguration configuration.
249          returned: when encryption is enabled on the bucket
250          type: complex
251          contains:
252            Rules:
253              description: List of applied encryptio rules.
254              returned: when encryption is enabled on the bucket
255              type: list
256              sample: { "ApplyServerSideEncryptionByDefault": { "SSEAlgorithm": "AES256" }, "BucketKeyEnabled": False }
257    bucket_lifecycle_configuration:
258      description: Bucket lifecycle configuration settings.
259      returned: when I(bucket_facts=true) and I(bucket_lifecycle_configuration=true)
260      type: complex
261      contains:
262        Rules:
263          description: List of lifecycle management rules.
264          returned: when lifecycle configuration is present
265          type: list
266          sample: [{ "Status": "Enabled", "ID": "example-rule" }]
267    bucket_location:
268      description: Bucket location.
269      returned: when I(bucket_facts=true) and I(bucket_location=true)
270      type: complex
271      contains:
272        LocationConstraint:
273          description: AWS region.
274          returned: always
275          type: str
276          sample: us-east-2
277    bucket_logging:
278      description: Server access logging configuration.
279      returned: when I(bucket_facts=true) and I(bucket_logging=true)
280      type: complex
281      contains:
282        LoggingEnabled:
283          description: Server access logging configuration.
284          returned: when server access logging is defined for the bucket
285          type: complex
286          contains:
287            TargetBucket:
288              description: Target bucket name.
289              returned: always
290              type: str
291              sample: logging-bucket-name
292            TargetPrefix:
293              description: Prefix in target bucket.
294              returned: always
295              type: str
296              sample: ""
297    bucket_notification_configuration:
298      description: Bucket notification settings.
299      returned: when I(bucket_facts=true) and I(bucket_notification_configuration=true)
300      type: complex
301      contains:
302        TopicConfigurations:
303          description: List of notification events configurations.
304          returned: when at least one notification is configured
305          type: list
306          sample: []
307    bucket_ownership_controls:
308      description: Preffered object ownership settings.
309      returned: when I(bucket_facts=true) and I(bucket_ownership_controls=true)
310      type: complex
311      contains:
312        OwnershipControls:
313          description: Object ownership settings.
314          returned: when ownership controls are defined for the bucket
315          type: complex
316          contains:
317            Rules:
318              description: List of ownership rules.
319              returned: when ownership rule is defined
320              type: list
321              sample: [{ "ObjectOwnership:": "ObjectWriter" }]
322    bucket_policy:
323      description: Bucket policy contents.
324      returned: when I(bucket_facts=true) and I(bucket_policy=true)
325      type: str
326      sample: '{"Version":"2012-10-17","Statement":[{"Sid":"AddCannedAcl","Effect":"Allow",..}}]}'
327    bucket_policy_status:
328      description: Status of bucket policy.
329      returned: when I(bucket_facts=true) and I(bucket_policy_status=true)
330      type: complex
331      contains:
332        PolicyStatus:
333          description: Status of bucket policy.
334          returned: when bucket policy is present
335          type: complex
336          contains:
337            IsPublic:
338              description: Report bucket policy public status.
339              returned: when bucket policy is present
340              type: bool
341              sample: True
342    bucket_replication:
343      description: Replication configuration settings.
344      returned: when I(bucket_facts=true) and I(bucket_replication=true)
345      type: complex
346      contains:
347        Role:
348          description: IAM role used for replication.
349          returned: when replication rule is defined
350          type: str
351          sample: "arn:aws:iam::123:role/example-role"
352        Rules:
353          description: List of replication rules.
354          returned: when replication rule is defined
355          type: list
356          sample: [{ "ID": "rule-1", "Filter": "{}" }]
357    bucket_request_payment:
358      description: Requester pays setting.
359      returned: when I(bucket_facts=true) and I(bucket_request_payment=true)
360      type: complex
361      contains:
362        Payer:
363          description: Current payer.
364          returned: always
365          type: str
366          sample: BucketOwner
367    bucket_tagging:
368      description: Bucket tags.
369      returned: when I(bucket_facts=true) and I(bucket_tagging=true)
370      type: dict
371      sample: { "Tag1": "Value1", "Tag2": "Value2" }
372    bucket_website:
373      description: Static website hosting.
374      returned: when I(bucket_facts=true) and I(bucket_website=true)
375      type: complex
376      contains:
377        ErrorDocument:
378          description: Object serving as HTTP error page.
379          returned: when static website hosting is enabled
380          type: dict
381          sample: { "Key": "error.html" }
382        IndexDocument:
383          description: Object serving as HTTP index page.
384          returned: when static website hosting is enabled
385          type: dict
386          sample: { "Suffix": "error.html" }
387        RedirectAllRequestsTo:
388          description: Website redict settings.
389          returned: when redirect requests is configured
390          type: complex
391          contains:
392            HostName:
393              description: Hostname to redirect.
394              returned: always
395              type: str
396              sample: www.example.com
397            Protocol:
398              description: Protocol used for redirect.
399              returned: always
400              type: str
401              sample: https
402'''
403
404try:
405    import botocore
406except ImportError:
407    pass  # Handled by AnsibleAWSModule
408
409from ansible_collections.amazon.aws.plugins.module_utils.core import AnsibleAWSModule
410from ansible_collections.amazon.aws.plugins.module_utils.ec2 import AWSRetry
411from ansible_collections.amazon.aws.plugins.module_utils.ec2 import boto3_tag_list_to_ansible_dict
412from ansible_collections.amazon.aws.plugins.module_utils.ec2 import camel_dict_to_snake_dict
413
414
415def get_bucket_list(module, connection, name="", name_filter=""):
416    """
417    Return result of list_buckets json encoded
418    Filter only buckets matching 'name' or name_filter if defined
419    :param module:
420    :param connection:
421    :return:
422    """
423    buckets = []
424    filtered_buckets = []
425    final_buckets = []
426
427    # Get all buckets
428    try:
429        buckets = camel_dict_to_snake_dict(connection.list_buckets())['buckets']
430    except (botocore.exceptions.ClientError, botocore.exceptions.BotoCoreError) as err_code:
431        module.fail_json_aws(err_code, msg="Failed to list buckets")
432
433    # Filter buckets if requested
434    if name_filter:
435        for bucket in buckets:
436            if name_filter in bucket['name']:
437                filtered_buckets.append(bucket)
438    elif name:
439        for bucket in buckets:
440            if name == bucket['name']:
441                filtered_buckets.append(bucket)
442
443    # Return proper list (filtered or all)
444    if name or name_filter:
445        final_buckets = filtered_buckets
446    else:
447        final_buckets = buckets
448    return(final_buckets)
449
450
451def get_buckets_facts(connection, buckets, requested_facts, transform_location):
452    """
453    Retrive additional information about S3 buckets
454    """
455    full_bucket_list = []
456    # Iterate over all buckets and append retrived facts to bucket
457    for bucket in buckets:
458        bucket.update(get_bucket_details(connection, bucket['name'], requested_facts, transform_location))
459        full_bucket_list.append(bucket)
460
461    return(full_bucket_list)
462
463
464def get_bucket_details(connection, name, requested_facts, transform_location):
465    """
466    Execute all enabled S3API get calls for selected bucket
467    """
468    all_facts = {}
469
470    for key in requested_facts:
471        if requested_facts[key]:
472            if key == 'bucket_location':
473                all_facts[key] = {}
474                try:
475                    all_facts[key] = get_bucket_location(name, connection, transform_location)
476                # we just pass on error - error means that resources is undefined
477                except botocore.exceptions.ClientError:
478                    pass
479            elif key == 'bucket_tagging':
480                all_facts[key] = {}
481                try:
482                    all_facts[key] = get_bucket_tagging(name, connection)
483                # we just pass on error - error means that resources is undefined
484                except botocore.exceptions.ClientError:
485                    pass
486            else:
487                all_facts[key] = {}
488                try:
489                    all_facts[key] = get_bucket_property(name, connection, key)
490                # we just pass on error - error means that resources is undefined
491                except botocore.exceptions.ClientError:
492                    pass
493
494    return(all_facts)
495
496
497@AWSRetry.jittered_backoff(max_delay=120, catch_extra_error_codes=['NoSuchBucket', 'OperationAborted'])
498def get_bucket_location(name, connection, transform_location=False):
499    """
500    Get bucket location and optionally transform 'null' to 'us-east-1'
501    """
502    data = connection.get_bucket_location(Bucket=name)
503
504    # Replace 'null' with 'us-east-1'?
505    if transform_location:
506        try:
507            if not data['LocationConstraint']:
508                data['LocationConstraint'] = 'us-east-1'
509        except KeyError:
510            pass
511    # Strip response metadata (not needed)
512    try:
513        data.pop('ResponseMetadata')
514        return(data)
515    except KeyError:
516        return(data)
517
518
519@AWSRetry.jittered_backoff(max_delay=120, catch_extra_error_codes=['NoSuchBucket', 'OperationAborted'])
520def get_bucket_tagging(name, connection):
521    """
522    Get bucket tags and transform them using `boto3_tag_list_to_ansible_dict` function
523    """
524    data = connection.get_bucket_tagging(Bucket=name)
525
526    try:
527        bucket_tags = boto3_tag_list_to_ansible_dict(data['TagSet'])
528        return(bucket_tags)
529    except KeyError:
530        # Strip response metadata (not needed)
531        try:
532            data.pop('ResponseMetadata')
533            return(data)
534        except KeyError:
535            return(data)
536
537
538@AWSRetry.jittered_backoff(max_delay=120, catch_extra_error_codes=['NoSuchBucket', 'OperationAborted'])
539def get_bucket_property(name, connection, get_api_name):
540    """
541    Get bucket property
542    """
543    api_call = "get_" + get_api_name
544    api_function = getattr(connection, api_call)
545    data = api_function(Bucket=name)
546
547    # Strip response metadata (not needed)
548    try:
549        data.pop('ResponseMetadata')
550        return(data)
551    except KeyError:
552        return(data)
553
554
555def main():
556    """
557    Get list of S3 buckets
558    :return:
559    """
560    argument_spec = dict(
561        name=dict(type='str', default=""),
562        name_filter=dict(type='str', default=""),
563        bucket_facts=dict(type='dict', options=dict(
564            bucket_accelerate_configuration=dict(type='bool', default=False),
565            bucket_acl=dict(type='bool', default=False),
566            bucket_cors=dict(type='bool', default=False),
567            bucket_encryption=dict(type='bool', default=False),
568            bucket_lifecycle_configuration=dict(type='bool', default=False),
569            bucket_location=dict(type='bool', default=False),
570            bucket_logging=dict(type='bool', default=False),
571            bucket_notification_configuration=dict(type='bool', default=False),
572            bucket_ownership_controls=dict(type='bool', default=False),
573            bucket_policy=dict(type='bool', default=False),
574            bucket_policy_status=dict(type='bool', default=False),
575            bucket_replication=dict(type='bool', default=False),
576            bucket_request_payment=dict(type='bool', default=False),
577            bucket_tagging=dict(type='bool', default=False),
578            bucket_website=dict(type='bool', default=False),
579            public_access_block=dict(type='bool', default=False),
580        )),
581        transform_location=dict(type='bool', default=False)
582    )
583
584    # Ensure we have an empty dict
585    result = {}
586
587    # Define mutually exclusive options
588    mutually_exclusive = [
589        ['name', 'name_filter']
590    ]
591
592    # Including ec2 argument spec
593    module = AnsibleAWSModule(argument_spec=argument_spec, supports_check_mode=True, mutually_exclusive=mutually_exclusive)
594    is_old_facts = module._name == 'aws_s3_bucket_facts'
595    if is_old_facts:
596        module.deprecate("The 'aws_s3_bucket_facts' module has been renamed to 'aws_s3_bucket_info', "
597                         "and the renamed one no longer returns ansible_facts", date='2021-12-01', collection_name='community.aws')
598
599    # Get parameters
600    name = module.params.get("name")
601    name_filter = module.params.get("name_filter")
602    requested_facts = module.params.get("bucket_facts")
603    transform_location = module.params.get("bucket_facts")
604
605    # Set up connection
606    connection = {}
607    try:
608        connection = module.client('s3')
609    except (connection.exceptions.ClientError, botocore.exceptions.BotoCoreError) as err_code:
610        module.fail_json_aws(err_code, msg='Failed to connect to AWS')
611
612    # Get basic bucket list (name + creation date)
613    bucket_list = get_bucket_list(module, connection, name, name_filter)
614
615    # Add information about name/name_filter to result
616    if name:
617        result['bucket_name'] = name
618    elif name_filter:
619        result['bucket_name_filter'] = name_filter
620
621    # Gather detailed information about buckets if requested
622    bucket_facts = module.params.get("bucket_facts")
623    if bucket_facts:
624        result['buckets'] = get_buckets_facts(connection, bucket_list, requested_facts, transform_location)
625    else:
626        result['buckets'] = bucket_list
627
628    # Send exit
629    if is_old_facts:
630        module.exit_json(msg="Retrieved s3 facts.", ansible_facts=result)
631    else:
632        module.exit_json(msg="Retrieved s3 info.", **result)
633
634
635# MAIN
636if __name__ == '__main__':
637    main()
638