From 3eaafd895ba78bc8f619b25d2f45677ed60ead11 Mon Sep 17 00:00:00 2001 From: Joseph Torcasso <87090265+jatorcasso@users.noreply.github.com> Date: Wed, 25 May 2022 00:41:29 -0400 Subject: [PATCH] rds_instance_snapshot - add copy snapshot functionality (#1078) rds_instance_snapshot - add copy snapshot functionality Depends-On: ansible-collections/amazon.aws#776 Depends-On: #1116 SUMMARY Add support for copying a snapshot Fixes #210 Don't require db_instance_identifier on state = present (only required for creation) ISSUE TYPE Feature Pull Request COMPONENT NAME rds_instance_snapshot Reviewed-by: Markus Bergholz Reviewed-by: Joseph Torcasso Reviewed-by: Alina Buzachis --- rds_instance_snapshot.py | 163 +++++++++++++++++++++++++++++---------- 1 file changed, 121 insertions(+), 42 deletions(-) diff --git a/rds_instance_snapshot.py b/rds_instance_snapshot.py index 2fa30f92d09..0d7a50a06e7 100644 --- a/rds_instance_snapshot.py +++ b/rds_instance_snapshot.py @@ -32,15 +32,37 @@ type: str db_instance_identifier: description: - - Database instance identifier. Required when state is present. + - Database instance identifier. Required when creating a snapshot. aliases: - instance_id type: str + source_db_snapshot_identifier: + description: + - The identifier of the source DB snapshot. + - Required when copying a snapshot. + - If the source snapshot is in the same AWS region as the copy, specify the snapshot's identifier. + - If the source snapshot is in a different AWS region as the copy, specify the snapshot's ARN. + aliases: + - source_id + - source_snapshot_id + type: str + version_added: 3.3.0 + source_region: + description: + - The region that contains the snapshot to be copied. + type: str + version_added: 3.3.0 + copy_tags: + description: + - Whether to copy all tags from I(source_db_snapshot_identifier) to I(db_instance_identifier). + type: bool + default: False + version_added: 3.3.0 wait: description: - Whether or not to wait for snapshot creation or deletion. type: bool - default: 'no' + default: False wait_timeout: description: - how long before wait gives up, in seconds. @@ -52,13 +74,14 @@ type: dict purge_tags: description: - - whether to remove tags not present in the C(tags) parameter. + - whether to remove tags not present in the I(tags) parameter. default: True type: bool author: - "Will Thames (@willthames)" - "Michael De La Rue (@mikedlr)" - "Alina Buzachis (@alinabuzachis)" + - "Joseph Torcasso (@jatorcasso)" extends_documentation_fragment: - amazon.aws.aws - amazon.aws.ec2 @@ -70,6 +93,15 @@ community.aws.rds_instance_snapshot: db_instance_identifier: new-database db_snapshot_identifier: new-database-snapshot + register: snapshot + +- name: Copy snapshot from a different region and copy its tags + community.aws.rds_instance_snapshot: + id: new-database-snapshot-copy + region: us-east-1 + source_id: "{{ snapshot.db_snapshot_arn }}" + source_region: us-east-2 + copy_tags: yes - name: Delete snapshot community.aws.rds_instance_snapshot: @@ -163,6 +195,12 @@ returned: always type: list sample: [] +source_db_snapshot_identifier: + description: The DB snapshot ARN that the DB snapshot was copied from. + returned: when snapshot is a copy + type: str + sample: arn:aws:rds:us-west-2:123456789012:snapshot:ansible-test-16638696-test-snapshot-source + version_added: 3.3.0 snapshot_create_time: description: Creation time of the snapshot. returned: always @@ -202,31 +240,41 @@ # import module snippets from ansible_collections.amazon.aws.plugins.module_utils.core import AnsibleAWSModule +from ansible_collections.amazon.aws.plugins.module_utils.core import get_boto3_client_method_parameters from ansible_collections.amazon.aws.plugins.module_utils.core import is_boto3_error_code +from ansible_collections.amazon.aws.plugins.module_utils.ec2 import ansible_dict_to_boto3_tag_list from ansible_collections.amazon.aws.plugins.module_utils.ec2 import AWSRetry from ansible_collections.amazon.aws.plugins.module_utils.ec2 import camel_dict_to_snake_dict -from ansible_collections.amazon.aws.plugins.module_utils.ec2 import ansible_dict_to_boto3_tag_list -from ansible_collections.amazon.aws.plugins.module_utils.rds import get_tags -from ansible_collections.amazon.aws.plugins.module_utils.rds import ensure_tags +from ansible_collections.amazon.aws.plugins.module_utils.rds import arg_spec_to_rds_params from ansible_collections.amazon.aws.plugins.module_utils.rds import call_method +from ansible_collections.amazon.aws.plugins.module_utils.rds import ensure_tags +from ansible_collections.amazon.aws.plugins.module_utils.rds import get_rds_method_attribute +from ansible_collections.amazon.aws.plugins.module_utils.rds import get_tags def get_snapshot(snapshot_id): try: - response = client.describe_db_snapshots(DBSnapshotIdentifier=snapshot_id) - except is_boto3_error_code("DBSnapshotNotFoundFault"): - return None - except is_boto3_error_code("DBSnapshotNotFound"): # pylint: disable=duplicate-except - return None + snapshot = client.describe_db_snapshots(DBSnapshotIdentifier=snapshot_id)['DBSnapshots'][0] + snapshot['Tags'] = get_tags(client, module, snapshot['DBSnapshotArn']) + except is_boto3_error_code("DBSnapshotNotFound"): + return {} except (botocore.exceptions.BotoCoreError, botocore.exceptions.ClientError) as e: # pylint: disable=duplicate-except module.fail_json_aws(e, msg="Couldn't get snapshot {0}".format(snapshot_id)) - return response['DBSnapshots'][0] + return snapshot -def fetch_tags(snapshot): - snapshot["Tags"] = get_tags(client, module, snapshot["DBSnapshotArn"]) +def get_parameters(parameters, method_name): + if method_name == 'copy_db_snapshot': + parameters['TargetDBSnapshotIdentifier'] = module.params['db_snapshot_identifier'] - return camel_dict_to_snake_dict(snapshot, ignore_list=["Tags"]) + required_options = get_boto3_client_method_parameters(client, method_name, required=True) + if any(parameters.get(k) is None for k in required_options): + module.fail_json(msg='To {0} requires the parameters: {1}'.format( + get_rds_method_attribute(method_name, module).operation_description, required_options)) + options = get_boto3_client_method_parameters(client, method_name) + parameters = dict((k, v) for k, v in parameters.items() if k in options and v is not None) + + return parameters def ensure_snapshot_absent(): @@ -236,40 +284,68 @@ def ensure_snapshot_absent(): snapshot = get_snapshot(snapshot_name) if not snapshot: - return dict(changed=changed) + module.exit_json(changed=changed) elif snapshot and snapshot["Status"] != "deleting": snapshot, changed = call_method(client, module, "delete_db_snapshot", params) - return dict(changed=changed) + module.exit_json(changed=changed) -def ensure_snapshot_present(): - db_instance_identifier = module.params.get('db_instance_identifier') +def ensure_snapshot_present(params): + source_id = module.params.get('source_db_snapshot_identifier') snapshot_name = module.params.get('db_snapshot_identifier') changed = False snapshot = get_snapshot(snapshot_name) + + # Copy snapshot + if source_id: + changed |= copy_snapshot(params) + + # Create snapshot + elif not snapshot: + changed |= create_snapshot(params) + + # Snapshot exists and we're not creating a copy - modify exising snapshot + else: + changed |= modify_snapshot() + + snapshot = get_snapshot(snapshot_name) + module.exit_json(changed=changed, **camel_dict_to_snake_dict(snapshot, ignore_list=['Tags'])) + + +def create_snapshot(params): + method_params = get_parameters(params, 'create_db_snapshot') + if method_params.get('Tags'): + method_params['Tags'] = ansible_dict_to_boto3_tag_list(method_params['Tags']) + snapshot, changed = call_method(client, module, 'create_db_snapshot', method_params) + + return changed + + +def copy_snapshot(params): + changed = False + snapshot_id = module.params.get('db_snapshot_identifier') + snapshot = get_snapshot(snapshot_id) + if not snapshot: - params = { - "DBSnapshotIdentifier": snapshot_name, - "DBInstanceIdentifier": db_instance_identifier - } - if module.params.get("tags"): - params['Tags'] = ansible_dict_to_boto3_tag_list(module.params.get("tags")) - _result, changed = call_method(client, module, "create_db_snapshot", params) + method_params = get_parameters(params, 'copy_db_snapshot') + if method_params.get('Tags'): + method_params['Tags'] = ansible_dict_to_boto3_tag_list(method_params['Tags']) + result, changed = call_method(client, module, 'copy_db_snapshot', method_params) - if module.check_mode: - return dict(changed=changed) + return changed - return dict(changed=changed, **fetch_tags(get_snapshot(snapshot_name))) - existing_tags = get_tags(client, module, snapshot["DBSnapshotArn"]) - changed |= ensure_tags(client, module, snapshot["DBSnapshotArn"], existing_tags, - module.params["tags"], module.params["purge_tags"]) +def modify_snapshot(): + # TODO - add other modifications aside from purely tags + changed = False + snapshot_id = module.params.get('db_snapshot_identifier') + snapshot = get_snapshot(snapshot_id) - if module.check_mode: - return dict(changed=changed) + if module.params.get('tags'): + changed |= ensure_tags(client, module, snapshot['DBSnapshotArn'], snapshot['Tags'], module.params['tags'], module.params['purge_tags']) - return dict(changed=changed, **fetch_tags(get_snapshot(snapshot_name))) + return changed def main(): @@ -280,16 +356,18 @@ def main(): state=dict(choices=['present', 'absent'], default='present'), db_snapshot_identifier=dict(aliases=['id', 'snapshot_id'], required=True), db_instance_identifier=dict(aliases=['instance_id']), + source_db_snapshot_identifier=dict(aliases=['source_id', 'source_snapshot_id']), wait=dict(type='bool', default=False), wait_timeout=dict(type='int', default=300), tags=dict(type='dict'), purge_tags=dict(type='bool', default=True), + copy_tags=dict(type='bool', default=False), + source_region=dict(type='str'), ) module = AnsibleAWSModule( argument_spec=argument_spec, - required_if=[['state', 'present', ['db_instance_identifier']]], - supports_check_mode=True, + supports_check_mode=True ) retry_decorator = AWSRetry.jittered_backoff(retries=10) @@ -298,12 +376,13 @@ def main(): except (botocore.exceptions.ClientError, botocore.exceptions.BotoCoreError) as e: module.fail_json_aws(e, msg="Failed to connect to AWS.") - if module.params['state'] == 'absent': - ret_dict = ensure_snapshot_absent() - else: - ret_dict = ensure_snapshot_present() + state = module.params.get("state") + if state == 'absent': + ensure_snapshot_absent() - module.exit_json(**ret_dict) + elif state == 'present': + params = arg_spec_to_rds_params(dict((k, module.params[k]) for k in module.params if k in argument_spec)) + ensure_snapshot_present(params) if __name__ == '__main__':