1#!/usr/local/bin/python3.8 2# Copyright (c) 2014 Ansible Project 3# Copyright (c) 2017, 2018, 2019 Will Thames 4# Copyright (c) 2017, 2018 Michael De La Rue 5# GNU General Public License v3.0+ (see COPYING or https://www.gnu.org/licenses/gpl-3.0.txt) 6 7from __future__ import absolute_import, division, print_function 8__metaclass__ = type 9 10 11DOCUMENTATION = ''' 12--- 13module: rds_snapshot 14version_added: 1.0.0 15short_description: manage Amazon RDS snapshots. 16description: 17 - Creates or deletes RDS snapshots. 18options: 19 state: 20 description: 21 - Specify the desired state of the snapshot. 22 default: present 23 choices: [ 'present', 'absent'] 24 type: str 25 db_snapshot_identifier: 26 description: 27 - The snapshot to manage. 28 required: true 29 aliases: 30 - id 31 - snapshot_id 32 type: str 33 db_instance_identifier: 34 description: 35 - Database instance identifier. Required when state is present. 36 aliases: 37 - instance_id 38 type: str 39 wait: 40 description: 41 - Whether or not to wait for snapshot creation or deletion. 42 type: bool 43 default: 'no' 44 wait_timeout: 45 description: 46 - how long before wait gives up, in seconds. 47 default: 300 48 type: int 49 tags: 50 description: 51 - tags dict to apply to a snapshot. 52 type: dict 53 purge_tags: 54 description: 55 - whether to remove tags not present in the C(tags) parameter. 56 default: True 57 type: bool 58requirements: 59 - "python >= 2.6" 60 - "boto3" 61author: 62 - "Will Thames (@willthames)" 63 - "Michael De La Rue (@mikedlr)" 64extends_documentation_fragment: 65- amazon.aws.aws 66- amazon.aws.ec2 67 68''' 69 70EXAMPLES = ''' 71- name: Create snapshot 72 community.aws.rds_snapshot: 73 db_instance_identifier: new-database 74 db_snapshot_identifier: new-database-snapshot 75 76- name: Delete snapshot 77 community.aws.rds_snapshot: 78 db_snapshot_identifier: new-database-snapshot 79 state: absent 80''' 81 82RETURN = ''' 83allocated_storage: 84 description: How much storage is allocated in GB. 85 returned: always 86 type: int 87 sample: 20 88availability_zone: 89 description: Availability zone of the database from which the snapshot was created. 90 returned: always 91 type: str 92 sample: us-west-2a 93db_instance_identifier: 94 description: Database from which the snapshot was created. 95 returned: always 96 type: str 97 sample: ansible-test-16638696 98db_snapshot_arn: 99 description: Amazon Resource Name for the snapshot. 100 returned: always 101 type: str 102 sample: arn:aws:rds:us-west-2:123456789012:snapshot:ansible-test-16638696-test-snapshot 103db_snapshot_identifier: 104 description: Name of the snapshot. 105 returned: always 106 type: str 107 sample: ansible-test-16638696-test-snapshot 108dbi_resource_id: 109 description: The identifier for the source DB instance, which can't be changed and which is unique to an AWS Region. 110 returned: always 111 type: str 112 sample: db-MM4P2U35RQRAMWD3QDOXWPZP4U 113encrypted: 114 description: Whether the snapshot is encrypted. 115 returned: always 116 type: bool 117 sample: false 118engine: 119 description: Engine of the database from which the snapshot was created. 120 returned: always 121 type: str 122 sample: mariadb 123engine_version: 124 description: Version of the database from which the snapshot was created. 125 returned: always 126 type: str 127 sample: 10.2.21 128iam_database_authentication_enabled: 129 description: Whether IAM database authentication is enabled. 130 returned: always 131 type: bool 132 sample: false 133instance_create_time: 134 description: Creation time of the instance from which the snapshot was created. 135 returned: always 136 type: str 137 sample: '2019-06-15T10:15:56.221000+00:00' 138license_model: 139 description: License model of the database. 140 returned: always 141 type: str 142 sample: general-public-license 143master_username: 144 description: Master username of the database. 145 returned: always 146 type: str 147 sample: test 148option_group_name: 149 description: Option group of the database. 150 returned: always 151 type: str 152 sample: default:mariadb-10-2 153percent_progress: 154 description: How much progress has been made taking the snapshot. Will be 100 for an available snapshot. 155 returned: always 156 type: int 157 sample: 100 158port: 159 description: Port on which the database is listening. 160 returned: always 161 type: int 162 sample: 3306 163processor_features: 164 description: List of processor features of the database. 165 returned: always 166 type: list 167 sample: [] 168snapshot_create_time: 169 description: Creation time of the snapshot. 170 returned: always 171 type: str 172 sample: '2019-06-15T10:46:23.776000+00:00' 173snapshot_type: 174 description: How the snapshot was created (always manual for this module!). 175 returned: always 176 type: str 177 sample: manual 178status: 179 description: Status of the snapshot. 180 returned: always 181 type: str 182 sample: available 183storage_type: 184 description: Storage type of the database. 185 returned: always 186 type: str 187 sample: gp2 188tags: 189 description: Tags applied to the snapshot. 190 returned: always 191 type: complex 192 contains: {} 193vpc_id: 194 description: ID of the VPC in which the DB lives. 195 returned: always 196 type: str 197 sample: vpc-09ff232e222710ae0 198''' 199 200try: 201 import botocore 202except ImportError: 203 pass # protected by AnsibleAWSModule 204 205# import module snippets 206from ansible_collections.amazon.aws.plugins.module_utils.core import AnsibleAWSModule 207from ansible_collections.amazon.aws.plugins.module_utils.ec2 import camel_dict_to_snake_dict, AWSRetry, compare_aws_tags 208from ansible_collections.amazon.aws.plugins.module_utils.ec2 import boto3_tag_list_to_ansible_dict, ansible_dict_to_boto3_tag_list 209 210 211def get_snapshot(client, module, snapshot_id): 212 try: 213 response = client.describe_db_snapshots(DBSnapshotIdentifier=snapshot_id) 214 except client.exceptions.DBSnapshotNotFoundFault: 215 return None 216 except (botocore.exceptions.BotoCoreError, botocore.exceptions.ClientError) as e: 217 module.fail_json_aws(e, msg="Couldn't get snapshot {0}".format(snapshot_id)) 218 return response['DBSnapshots'][0] 219 220 221def snapshot_to_facts(client, module, snapshot): 222 try: 223 snapshot['Tags'] = boto3_tag_list_to_ansible_dict(client.list_tags_for_resource(ResourceName=snapshot['DBSnapshotArn'], 224 aws_retry=True)['TagList']) 225 except (botocore.exceptions.ClientError, botocore.exceptions.BotoCoreError) as e: 226 module.fail_json_aws(e, "Couldn't get tags for snapshot %s" % snapshot['DBSnapshotIdentifier']) 227 except KeyError: 228 module.fail_json(msg=str(snapshot)) 229 230 return camel_dict_to_snake_dict(snapshot, ignore_list=['Tags']) 231 232 233def wait_for_snapshot_status(client, module, db_snapshot_id, waiter_name): 234 if not module.params['wait']: 235 return 236 timeout = module.params['wait_timeout'] 237 try: 238 client.get_waiter(waiter_name).wait(DBSnapshotIdentifier=db_snapshot_id, 239 WaiterConfig=dict( 240 Delay=5, 241 MaxAttempts=int((timeout + 2.5) / 5) 242 )) 243 except botocore.exceptions.WaiterError as e: 244 if waiter_name == 'db_snapshot_deleted': 245 msg = "Failed to wait for DB snapshot {0} to be deleted".format(db_snapshot_id) 246 else: 247 msg = "Failed to wait for DB snapshot {0} to be available".format(db_snapshot_id) 248 module.fail_json_aws(e, msg=msg) 249 except (botocore.exceptions.BotoCoreError, botocore.exceptions.ClientError) as e: 250 module.fail_json_aws(e, msg="Failed with an unexpected error while waiting for the DB cluster {0}".format(db_snapshot_id)) 251 252 253def ensure_snapshot_absent(client, module): 254 snapshot_name = module.params.get('db_snapshot_identifier') 255 changed = False 256 257 snapshot = get_snapshot(client, module, snapshot_name) 258 if snapshot and snapshot['Status'] != 'deleting': 259 try: 260 client.delete_db_snapshot(DBSnapshotIdentifier=snapshot_name) 261 changed = True 262 except (botocore.exceptions.ClientError, botocore.exceptions.BotoCoreError) as e: 263 module.fail_json_aws(e, msg="trying to delete snapshot") 264 265 # If we're not waiting for a delete to complete then we're all done 266 # so just return 267 if not snapshot or not module.params.get('wait'): 268 return dict(changed=changed) 269 try: 270 wait_for_snapshot_status(client, module, snapshot_name, 'db_snapshot_deleted') 271 return dict(changed=changed) 272 except client.exceptions.DBSnapshotNotFoundFault: 273 return dict(changed=changed) 274 except (botocore.exceptions.ClientError, botocore.exceptions.BotoCoreError) as e: 275 module.fail_json_aws(e, "awaiting snapshot deletion") 276 277 278def ensure_tags(client, module, resource_arn, existing_tags, tags, purge_tags): 279 if tags is None: 280 return False 281 tags_to_add, tags_to_remove = compare_aws_tags(existing_tags, tags, purge_tags) 282 changed = bool(tags_to_add or tags_to_remove) 283 if tags_to_add: 284 try: 285 client.add_tags_to_resource(ResourceName=resource_arn, Tags=ansible_dict_to_boto3_tag_list(tags_to_add)) 286 except (botocore.exceptions.ClientError, botocore.exceptions.BotoCoreError) as e: 287 module.fail_json_aws(e, "Couldn't add tags to snapshot {0}".format(resource_arn)) 288 if tags_to_remove: 289 try: 290 client.remove_tags_from_resource(ResourceName=resource_arn, TagKeys=tags_to_remove) 291 except (botocore.exceptions.ClientError, botocore.exceptions.BotoCoreError) as e: 292 module.fail_json_aws(e, "Couldn't remove tags from snapshot {0}".format(resource_arn)) 293 return changed 294 295 296def ensure_snapshot_present(client, module): 297 db_instance_identifier = module.params.get('db_instance_identifier') 298 snapshot_name = module.params.get('db_snapshot_identifier') 299 changed = False 300 snapshot = get_snapshot(client, module, snapshot_name) 301 if not snapshot: 302 try: 303 snapshot = client.create_db_snapshot(DBSnapshotIdentifier=snapshot_name, 304 DBInstanceIdentifier=db_instance_identifier)['DBSnapshot'] 305 changed = True 306 except (botocore.exceptions.ClientError, botocore.exceptions.BotoCoreError) as e: 307 module.fail_json_aws(e, msg="trying to create db snapshot") 308 309 if module.params.get('wait'): 310 wait_for_snapshot_status(client, module, snapshot_name, 'db_snapshot_available') 311 312 existing_tags = boto3_tag_list_to_ansible_dict(client.list_tags_for_resource(ResourceName=snapshot['DBSnapshotArn'], 313 aws_retry=True)['TagList']) 314 desired_tags = module.params['tags'] 315 purge_tags = module.params['purge_tags'] 316 changed |= ensure_tags(client, module, snapshot['DBSnapshotArn'], existing_tags, desired_tags, purge_tags) 317 318 snapshot = get_snapshot(client, module, snapshot_name) 319 320 return dict(changed=changed, **snapshot_to_facts(client, module, snapshot)) 321 322 323def main(): 324 325 module = AnsibleAWSModule( 326 argument_spec=dict( 327 state=dict(choices=['present', 'absent'], default='present'), 328 db_snapshot_identifier=dict(aliases=['id', 'snapshot_id'], required=True), 329 db_instance_identifier=dict(aliases=['instance_id']), 330 wait=dict(type='bool', default=False), 331 wait_timeout=dict(type='int', default=300), 332 tags=dict(type='dict'), 333 purge_tags=dict(type='bool', default=True), 334 ), 335 required_if=[['state', 'present', ['db_instance_identifier']]] 336 ) 337 338 client = module.client('rds', retry_decorator=AWSRetry.jittered_backoff(retries=10, catch_extra_error_codes=['DBSnapshotNotFound'])) 339 340 if module.params['state'] == 'absent': 341 ret_dict = ensure_snapshot_absent(client, module) 342 else: 343 ret_dict = ensure_snapshot_present(client, module) 344 345 module.exit_json(**ret_dict) 346 347 348if __name__ == '__main__': 349 main() 350