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