mirror of https://github.com/ansible/ansible.git
Add rds_snapshot module (#39994)
* new module uses modern ansible AWS standards * adds additional tests for snapshots * Update return_skeleton_generator for python3 - should set type to `str`, not `string`.pull/58446/head
parent
f109184753
commit
eda5dd826f
@ -0,0 +1,349 @@
|
|||||||
|
#!/usr/bin/python
|
||||||
|
# Copyright (c) 2014 Ansible Project
|
||||||
|
# Copyright (c) 2017, 2018, 2019 Will Thames
|
||||||
|
# Copyright (c) 2017, 2018 Michael De La Rue
|
||||||
|
# GNU General Public License v3.0+ (see COPYING or https://www.gnu.org/licenses/gpl-3.0.txt)
|
||||||
|
|
||||||
|
|
||||||
|
ANSIBLE_METADATA = {'status': ['preview'],
|
||||||
|
'supported_by': 'community',
|
||||||
|
'metadata_version': '1.1'}
|
||||||
|
|
||||||
|
DOCUMENTATION = '''
|
||||||
|
---
|
||||||
|
module: rds_snapshot
|
||||||
|
version_added: "2.9"
|
||||||
|
short_description: manage Amazon RDS snapshots.
|
||||||
|
description:
|
||||||
|
- Creates or deletes RDS snapshots.
|
||||||
|
options:
|
||||||
|
state:
|
||||||
|
description:
|
||||||
|
- Specify the desired state of the snapshot.
|
||||||
|
default: present
|
||||||
|
choices: [ 'present', 'absent']
|
||||||
|
type: str
|
||||||
|
db_snapshot_identifier:
|
||||||
|
description:
|
||||||
|
- The snapshot to manage.
|
||||||
|
required: true
|
||||||
|
aliases:
|
||||||
|
- id
|
||||||
|
- snapshot_id
|
||||||
|
type: str
|
||||||
|
db_instance_identifier:
|
||||||
|
description:
|
||||||
|
- Database instance identifier. Required when state is present.
|
||||||
|
aliases:
|
||||||
|
- instance_id
|
||||||
|
type: str
|
||||||
|
wait:
|
||||||
|
description:
|
||||||
|
- Whether or not to wait for snapshot creation or deletion.
|
||||||
|
type: bool
|
||||||
|
default: 'no'
|
||||||
|
wait_timeout:
|
||||||
|
description:
|
||||||
|
- how long before wait gives up, in seconds.
|
||||||
|
default: 300
|
||||||
|
type: int
|
||||||
|
tags:
|
||||||
|
description:
|
||||||
|
- tags dict to apply to a snapshot.
|
||||||
|
type: dict
|
||||||
|
purge_tags:
|
||||||
|
description:
|
||||||
|
- whether to remove tags not present in the C(tags) parameter.
|
||||||
|
default: True
|
||||||
|
type: bool
|
||||||
|
requirements:
|
||||||
|
- "python >= 2.6"
|
||||||
|
- "boto3"
|
||||||
|
author:
|
||||||
|
- "Will Thames (@willthames)"
|
||||||
|
- "Michael De La Rue (@mikedlr)"
|
||||||
|
extends_documentation_fragment:
|
||||||
|
- aws
|
||||||
|
- ec2
|
||||||
|
'''
|
||||||
|
|
||||||
|
EXAMPLES = '''
|
||||||
|
# Create snapshot
|
||||||
|
- rds_snapshot:
|
||||||
|
db_instance_identifier: new-database
|
||||||
|
db_snapshot_identifier: new-database-snapshot
|
||||||
|
|
||||||
|
# Delete snapshot
|
||||||
|
- rds_snapshot:
|
||||||
|
db_snapshot_identifier: new-database-snapshot
|
||||||
|
state: absent
|
||||||
|
'''
|
||||||
|
|
||||||
|
RETURN = '''
|
||||||
|
allocated_storage:
|
||||||
|
description: How much storage is allocated in GB.
|
||||||
|
returned: always
|
||||||
|
type: int
|
||||||
|
sample: 20
|
||||||
|
availability_zone:
|
||||||
|
description: Availability zone of the database from which the snapshot was created.
|
||||||
|
returned: always
|
||||||
|
type: str
|
||||||
|
sample: us-west-2a
|
||||||
|
db_instance_identifier:
|
||||||
|
description: Database from which the snapshot was created.
|
||||||
|
returned: always
|
||||||
|
type: str
|
||||||
|
sample: ansible-test-16638696
|
||||||
|
db_snapshot_arn:
|
||||||
|
description: Amazon Resource Name for the snapshot.
|
||||||
|
returned: always
|
||||||
|
type: str
|
||||||
|
sample: arn:aws:rds:us-west-2:123456789012:snapshot:ansible-test-16638696-test-snapshot
|
||||||
|
db_snapshot_identifier:
|
||||||
|
description: Name of the snapshot.
|
||||||
|
returned: always
|
||||||
|
type: str
|
||||||
|
sample: ansible-test-16638696-test-snapshot
|
||||||
|
dbi_resource_id:
|
||||||
|
description: The identifier for the source DB instance, which can't be changed and which is unique to an AWS Region.
|
||||||
|
returned: always
|
||||||
|
type: str
|
||||||
|
sample: db-MM4P2U35RQRAMWD3QDOXWPZP4U
|
||||||
|
encrypted:
|
||||||
|
description: Whether the snapshot is encrypted.
|
||||||
|
returned: always
|
||||||
|
type: bool
|
||||||
|
sample: false
|
||||||
|
engine:
|
||||||
|
description: Engine of the database from which the snapshot was created.
|
||||||
|
returned: always
|
||||||
|
type: str
|
||||||
|
sample: mariadb
|
||||||
|
engine_version:
|
||||||
|
description: Version of the database from which the snapshot was created.
|
||||||
|
returned: always
|
||||||
|
type: str
|
||||||
|
sample: 10.2.21
|
||||||
|
iam_database_authentication_enabled:
|
||||||
|
description: Whether IAM database authentication is enabled.
|
||||||
|
returned: always
|
||||||
|
type: bool
|
||||||
|
sample: false
|
||||||
|
instance_create_time:
|
||||||
|
description: Creation time of the instance from which the snapshot was created.
|
||||||
|
returned: always
|
||||||
|
type: str
|
||||||
|
sample: '2019-06-15T10:15:56.221000+00:00'
|
||||||
|
license_model:
|
||||||
|
description: License model of the database.
|
||||||
|
returned: always
|
||||||
|
type: str
|
||||||
|
sample: general-public-license
|
||||||
|
master_username:
|
||||||
|
description: Master username of the database.
|
||||||
|
returned: always
|
||||||
|
type: str
|
||||||
|
sample: test
|
||||||
|
option_group_name:
|
||||||
|
description: Option group of the database.
|
||||||
|
returned: always
|
||||||
|
type: str
|
||||||
|
sample: default:mariadb-10-2
|
||||||
|
percent_progress:
|
||||||
|
description: How much progress has been made taking the snapshot. Will be 100 for an available snapshot.
|
||||||
|
returned: always
|
||||||
|
type: int
|
||||||
|
sample: 100
|
||||||
|
port:
|
||||||
|
description: Port on which the database is listening.
|
||||||
|
returned: always
|
||||||
|
type: int
|
||||||
|
sample: 3306
|
||||||
|
processor_features:
|
||||||
|
description: List of processor features of the database.
|
||||||
|
returned: always
|
||||||
|
type: list
|
||||||
|
sample: []
|
||||||
|
snapshot_create_time:
|
||||||
|
description: Creation time of the snapshot.
|
||||||
|
returned: always
|
||||||
|
type: str
|
||||||
|
sample: '2019-06-15T10:46:23.776000+00:00'
|
||||||
|
snapshot_type:
|
||||||
|
description: How the snapshot was created (always manual for this module!).
|
||||||
|
returned: always
|
||||||
|
type: str
|
||||||
|
sample: manual
|
||||||
|
status:
|
||||||
|
description: Status of the snapshot.
|
||||||
|
returned: always
|
||||||
|
type: str
|
||||||
|
sample: available
|
||||||
|
storage_type:
|
||||||
|
description: Storage type of the database.
|
||||||
|
returned: always
|
||||||
|
type: str
|
||||||
|
sample: gp2
|
||||||
|
tags:
|
||||||
|
description: Tags applied to the snapshot.
|
||||||
|
returned: always
|
||||||
|
type: complex
|
||||||
|
contains: {}
|
||||||
|
vpc_id:
|
||||||
|
description: ID of the VPC in which the DB lives.
|
||||||
|
returned: always
|
||||||
|
type: str
|
||||||
|
sample: vpc-09ff232e222710ae0
|
||||||
|
'''
|
||||||
|
|
||||||
|
try:
|
||||||
|
import botocore
|
||||||
|
except ImportError:
|
||||||
|
pass # protected by AnsibleAWSModule
|
||||||
|
|
||||||
|
# import module snippets
|
||||||
|
from ansible.module_utils.aws.core import AnsibleAWSModule
|
||||||
|
from ansible.module_utils.ec2 import camel_dict_to_snake_dict, AWSRetry, compare_aws_tags
|
||||||
|
from ansible.module_utils.ec2 import boto3_tag_list_to_ansible_dict, ansible_dict_to_boto3_tag_list
|
||||||
|
|
||||||
|
|
||||||
|
def get_snapshot(client, module, snapshot_id):
|
||||||
|
try:
|
||||||
|
response = client.describe_db_snapshots(DBSnapshotIdentifier=snapshot_id)
|
||||||
|
except client.exceptions.DBSnapshotNotFoundFault:
|
||||||
|
return None
|
||||||
|
except (botocore.exceptions.BotoCoreError, botocore.exceptions.ClientError) as e:
|
||||||
|
module.fail_json_aws(e, msg="Couldn't get snapshot {0}".format(snapshot_id))
|
||||||
|
return response['DBSnapshots'][0]
|
||||||
|
|
||||||
|
|
||||||
|
def snapshot_to_facts(client, module, snapshot):
|
||||||
|
try:
|
||||||
|
snapshot['Tags'] = boto3_tag_list_to_ansible_dict(client.list_tags_for_resource(ResourceName=snapshot['DBSnapshotArn'],
|
||||||
|
aws_retry=True)['TagList'])
|
||||||
|
except (botocore.exceptions.ClientError, botocore.exceptions.BotoCoreError) as e:
|
||||||
|
module.fail_json_aws(e, "Couldn't get tags for snapshot %s" % snapshot['DBSnapshotIdentifier'])
|
||||||
|
except KeyError:
|
||||||
|
module.fail_json(msg=str(snapshot))
|
||||||
|
|
||||||
|
return camel_dict_to_snake_dict(snapshot, ignore_list=['Tags'])
|
||||||
|
|
||||||
|
|
||||||
|
def wait_for_snapshot_status(client, module, db_snapshot_id, waiter_name):
|
||||||
|
if not module.params['wait']:
|
||||||
|
return
|
||||||
|
timeout = module.params['wait_timeout']
|
||||||
|
try:
|
||||||
|
client.get_waiter(waiter_name).wait(DBSnapshotIdentifier=db_snapshot_id,
|
||||||
|
WaiterConfig=dict(
|
||||||
|
Delay=5,
|
||||||
|
MaxAttempts=int((timeout + 2.5) / 5)
|
||||||
|
))
|
||||||
|
except botocore.exceptions.WaiterError as e:
|
||||||
|
if waiter_name == 'db_snapshot_deleted':
|
||||||
|
msg = "Failed to wait for DB snapshot {0} to be deleted".format(db_snapshot_id)
|
||||||
|
else:
|
||||||
|
msg = "Failed to wait for DB snapshot {0} to be available".format(db_snapshot_id)
|
||||||
|
module.fail_json_aws(e, msg=msg)
|
||||||
|
except (botocore.exceptions.BotoCoreError, botocore.exceptions.ClientError) as e:
|
||||||
|
module.fail_json_aws(e, msg="Failed with an unexpected error while waiting for the DB cluster {0}".format(db_snapshot_id))
|
||||||
|
|
||||||
|
|
||||||
|
def ensure_snapshot_absent(client, module):
|
||||||
|
snapshot_name = module.params.get('db_snapshot_identifier')
|
||||||
|
changed = False
|
||||||
|
|
||||||
|
snapshot = get_snapshot(client, module, snapshot_name)
|
||||||
|
if snapshot and snapshot['Status'] != 'deleting':
|
||||||
|
try:
|
||||||
|
client.delete_db_snapshot(DBSnapshotIdentifier=snapshot_name)
|
||||||
|
changed = True
|
||||||
|
except (botocore.exceptions.ClientError, botocore.exceptions.BotoCoreError) as e:
|
||||||
|
module.fail_json_aws(e, msg="trying to delete snapshot")
|
||||||
|
|
||||||
|
# If we're not waiting for a delete to complete then we're all done
|
||||||
|
# so just return
|
||||||
|
if not snapshot or not module.params.get('wait'):
|
||||||
|
return dict(changed=changed)
|
||||||
|
try:
|
||||||
|
wait_for_snapshot_status(client, module, snapshot_name, 'db_snapshot_deleted')
|
||||||
|
return dict(changed=changed)
|
||||||
|
except client.exceptions.DBSnapshotNotFoundFault:
|
||||||
|
return dict(changed=changed)
|
||||||
|
except (botocore.exceptions.ClientError, botocore.exceptions.BotoCoreError) as e:
|
||||||
|
module.fail_json_aws(e, "awaiting snapshot deletion")
|
||||||
|
|
||||||
|
|
||||||
|
def ensure_tags(client, module, resource_arn, existing_tags, tags, purge_tags):
|
||||||
|
if tags is None:
|
||||||
|
return False
|
||||||
|
tags_to_add, tags_to_remove = compare_aws_tags(existing_tags, tags, purge_tags)
|
||||||
|
changed = bool(tags_to_add or tags_to_remove)
|
||||||
|
if tags_to_add:
|
||||||
|
try:
|
||||||
|
client.add_tags_to_resource(ResourceName=resource_arn, Tags=ansible_dict_to_boto3_tag_list(tags_to_add))
|
||||||
|
except (botocore.exceptions.ClientError, botocore.exceptions.BotoCoreError) as e:
|
||||||
|
module.fail_json_aws(e, "Couldn't add tags to snapshot {0}".format(resource_arn))
|
||||||
|
if tags_to_remove:
|
||||||
|
try:
|
||||||
|
client.remove_tags_from_resource(ResourceName=resource_arn, TagKeys=tags_to_remove)
|
||||||
|
except (botocore.exceptions.ClientError, botocore.exceptions.BotoCoreError) as e:
|
||||||
|
module.fail_json_aws(e, "Couldn't remove tags from snapshot {0}".format(resource_arn))
|
||||||
|
return changed
|
||||||
|
|
||||||
|
|
||||||
|
def ensure_snapshot_present(client, module):
|
||||||
|
db_instance_identifier = module.params.get('db_instance_identifier')
|
||||||
|
snapshot_name = module.params.get('db_snapshot_identifier')
|
||||||
|
changed = False
|
||||||
|
snapshot = get_snapshot(client, module, snapshot_name)
|
||||||
|
if not snapshot:
|
||||||
|
try:
|
||||||
|
snapshot = client.create_db_snapshot(DBSnapshotIdentifier=snapshot_name,
|
||||||
|
DBInstanceIdentifier=db_instance_identifier)['DBSnapshot']
|
||||||
|
changed = True
|
||||||
|
except (botocore.exceptions.ClientError, botocore.exceptions.BotoCoreError) as e:
|
||||||
|
module.fail_json_aws(e, msg="trying to create db snapshot")
|
||||||
|
|
||||||
|
if module.params.get('wait'):
|
||||||
|
wait_for_snapshot_status(client, module, snapshot_name, 'db_snapshot_available')
|
||||||
|
|
||||||
|
existing_tags = boto3_tag_list_to_ansible_dict(client.list_tags_for_resource(ResourceName=snapshot['DBSnapshotArn'],
|
||||||
|
aws_retry=True)['TagList'])
|
||||||
|
desired_tags = module.params['tags']
|
||||||
|
purge_tags = module.params['purge_tags']
|
||||||
|
changed |= ensure_tags(client, module, snapshot['DBSnapshotArn'], existing_tags, desired_tags, purge_tags)
|
||||||
|
|
||||||
|
snapshot = get_snapshot(client, module, snapshot_name)
|
||||||
|
|
||||||
|
return dict(changed=changed, **snapshot_to_facts(client, module, snapshot))
|
||||||
|
|
||||||
|
|
||||||
|
def main():
|
||||||
|
|
||||||
|
module = AnsibleAWSModule(
|
||||||
|
argument_spec=dict(
|
||||||
|
state=dict(choices=['present', 'absent'], default='present'),
|
||||||
|
db_snapshot_identifier=dict(aliases=['id', 'snapshot_id'], required=True),
|
||||||
|
db_instance_identifier=dict(aliases=['instance_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),
|
||||||
|
),
|
||||||
|
required_if=[['state', 'present', ['db_instance_identifier']]]
|
||||||
|
)
|
||||||
|
|
||||||
|
client = module.client('rds', retry_decorator=AWSRetry.jittered_backoff(retries=10))
|
||||||
|
|
||||||
|
if module.params['state'] == 'absent':
|
||||||
|
ret_dict = ensure_snapshot_absent(client, module)
|
||||||
|
else:
|
||||||
|
ret_dict = ensure_snapshot_present(client, module)
|
||||||
|
|
||||||
|
module.exit_json(**ret_dict)
|
||||||
|
|
||||||
|
|
||||||
|
if __name__ == '__main__':
|
||||||
|
main()
|
Loading…
Reference in New Issue