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