From 3cb67e4ece6b25f355903d91abbb52fc51ad3697 Mon Sep 17 00:00:00 2001 From: Mark Chappell Date: Fri, 17 Sep 2021 20:38:34 +0200 Subject: [PATCH] Migrate elasticache_subnet_group to boto3 --- .../723-elasticache_subnet_group-boto3.yml | 2 + plugins/modules/elasticache_subnet_group.py | 196 ++++++++++++------ 2 files changed, 129 insertions(+), 69 deletions(-) create mode 100644 changelogs/fragments/723-elasticache_subnet_group-boto3.yml diff --git a/changelogs/fragments/723-elasticache_subnet_group-boto3.yml b/changelogs/fragments/723-elasticache_subnet_group-boto3.yml new file mode 100644 index 00000000000..a54e451ec7b --- /dev/null +++ b/changelogs/fragments/723-elasticache_subnet_group-boto3.yml @@ -0,0 +1,2 @@ +minor_changes: +- elasticache_subnet_group - module migrated to boto3 AWS SDK (https://github.com/ansible-collections/community.aws/pull/723). diff --git a/plugins/modules/elasticache_subnet_group.py b/plugins/modules/elasticache_subnet_group.py index 44a3e39ae6f..f982a7a253c 100644 --- a/plugins/modules/elasticache_subnet_group.py +++ b/plugins/modules/elasticache_subnet_group.py @@ -12,34 +12,35 @@ version_added: 1.0.0 short_description: manage ElastiCache subnet groups description: - - Creates, modifies, and deletes ElastiCache subnet groups. This module has a dependency on python-boto >= 2.5. + - Creates, modifies, and deletes ElastiCache subnet groups. options: state: description: - Specifies whether the subnet should be present or absent. - required: true choices: [ 'present' , 'absent' ] + default: 'present' type: str name: description: - Database subnet group identifier. + - This value is automatically converted to lowercase. required: true type: str description: description: - - ElastiCache subnet group description. Only set when a new group is added. + - ElastiCache subnet group description. + - When not provided defaults to I(name) on subnet group creation. type: str subnets: description: - List of subnet IDs that make up the ElastiCache subnet group. type: list elements: str -author: "Tim Mahoney (@timmahoney)" +author: + - "Tim Mahoney (@timmahoney)" extends_documentation_fragment: -- amazon.aws.aws -- amazon.aws.ec2 -requirements: -- boto >= 2.49.0 + - amazon.aws.aws + - amazon.aws.ec2 ''' EXAMPLES = r''' @@ -59,86 +60,143 @@ ''' try: - import boto - from boto.elasticache import connect_to_region - from boto.exception import BotoServerError + import botocore except ImportError: - pass # Handled by HAS_BOTO + pass # Handled by AnsibleAWSModule + +from ansible.module_utils.common.dict_transformations import camel_dict_to_snake_dict -from ansible.module_utils._text import to_native from ansible_collections.amazon.aws.plugins.module_utils.core import AnsibleAWSModule -from ansible_collections.amazon.aws.plugins.module_utils.ec2 import HAS_BOTO -from ansible_collections.amazon.aws.plugins.module_utils.ec2 import get_aws_connection_info +from ansible_collections.amazon.aws.plugins.module_utils.core import is_boto3_error_code +from ansible_collections.amazon.aws.plugins.module_utils.ec2 import AWSRetry + + +def get_subnet_group(name): + try: + groups = client.describe_cache_subnet_groups( + aws_retry=True, + CacheSubnetGroupName=name, + )['CacheSubnetGroups'] + except is_boto3_error_code('CacheSubnetGroupNotFoundFault'): + return None + except (botocore.exceptions.ClientError, botocore.exceptions.BotoCoreError) as e: # pylint: disable=duplicate-except + module.fail_json_aws(e, msg="Failed to describe subnet group") + + if not groups: + return None + + if len(groups) > 1: + module.fail_aws( + msg="Found multiple matches for subnet group", + cache_subnet_groups=camel_dict_to_snake_dict(groups), + ) + + subnet_group = camel_dict_to_snake_dict(groups[0]) + + subnet_group['name'] = subnet_group['cache_subnet_group_name'] + subnet_group['description'] = subnet_group['cache_subnet_group_description'] + + subnet_ids = list(s['subnet_identifier'] for s in subnet_group['subnets']) + subnet_group['subnet_ids'] = subnet_ids + + return subnet_group + + +def create_subnet_group(name, description, subnets): + try: + if not description: + description = name + if not subnets: + subnets = [] + client.create_cache_subnet_group( + aws_retry=True, + CacheSubnetGroupName=name, + CacheSubnetGroupDescription=description, + SubnetIds=subnets, + ) + return True + except (botocore.exceptions.ClientError, botocore.exceptions.BotoCoreError) as e: + module.fail_json_aws(e, msg="Failed to create subnet group") + + +def update_subnet_group(subnet_group, name, description, subnets): + update_params = dict() + if description and subnet_group['description'] != description: + update_params['CacheSubnetGroupDescription'] = description + if subnets: + old_subnets = set(subnet_group['subnet_ids']) + new_subnets = set(subnets) + if old_subnets != new_subnets: + update_params['SubnetIds'] = list(subnets) + + if not update_params: + return False + + try: + client.modify_cache_subnet_group( + aws_retry=True, + CacheSubnetGroupName=name, + **update_params, + ) + except (botocore.exceptions.ClientError, botocore.exceptions.BotoCoreError) as e: + module.fail_json_aws(e, msg="Failed to update subnet group") + + return True + + +def delete_subnet_group(name): + try: + client.delete_cache_subnet_group( + aws_retry=True, + CacheSubnetGroupName=name, + ) + return True + except is_boto3_error_code('CacheSubnetGroupNotFoundFault'): + # AWS is "eventually consistent", cope with the race conditions where + # deletion hadn't completed when we ran describe + return False + except (botocore.exceptions.ClientError, botocore.exceptions.BotoCoreError) as e: # pylint: disable=duplicate-except + module.fail_json_aws(e, msg="Failed to delete subnet group") def main(): argument_spec = dict( - state=dict(required=True, choices=['present', 'absent']), + state=dict(default='present', choices=['present', 'absent']), name=dict(required=True), description=dict(required=False), subnets=dict(required=False, type='list', elements='str'), ) - module = AnsibleAWSModule(argument_spec=argument_spec, check_boto3=False) - if not HAS_BOTO: - module.fail_json(msg='boto required for this module') + global module + global client - state = module.params.get('state') - group_name = module.params.get('name').lower() - group_description = module.params.get('description') - group_subnets = module.params.get('subnets') or {} + module = AnsibleAWSModule(argument_spec=argument_spec) - if state == 'present': - for required in ['name', 'description', 'subnets']: - if not module.params.get(required): - module.fail_json(msg=str("Parameter %s required for state='present'" % required)) - else: - for not_allowed in ['description', 'subnets']: - if module.params.get(not_allowed): - module.fail_json(msg=str("Parameter %s not allowed for state='absent'" % not_allowed)) - - # Retrieve any AWS settings from the environment. - region, ec2_url, aws_connect_kwargs = get_aws_connection_info(module) + state = module.params.get('state') + name = module.params.get('name').lower() + description = module.params.get('description') + subnets = module.params.get('subnets') - if not region: - module.fail_json(msg=str("Either region or AWS_REGION or EC2_REGION environment variable or boto config aws_region or ec2_region must be set.")) + client = module.client('elasticache', retry_decorator=AWSRetry.jittered_backoff()) - """Get an elasticache connection""" - try: - conn = connect_to_region(region_name=region, **aws_connect_kwargs) - except boto.exception.NoAuthHandlerFound as e: - module.fail_json(msg=to_native(e)) + subnet_group = get_subnet_group(name) + changed = False - try: - changed = False - exists = False - - try: - matching_groups = conn.describe_cache_subnet_groups(group_name, max_records=100) - exists = len(matching_groups) > 0 - except BotoServerError as e: - if e.error_code != 'CacheSubnetGroupNotFoundFault': - module.fail_json(msg=e.error_message) - - if state == 'absent': - if exists: - conn.delete_cache_subnet_group(group_name) - changed = True - else: - if not exists: - new_group = conn.create_cache_subnet_group(group_name, cache_subnet_group_description=group_description, subnet_ids=group_subnets) - changed = True - else: - changed_group = conn.modify_cache_subnet_group(group_name, cache_subnet_group_description=group_description, subnet_ids=group_subnets) - changed = True - - except BotoServerError as e: - if e.error_message != 'No modifications were requested.': - module.fail_json(msg=e.error_message) + if state == 'present': + if not subnet_group: + result = create_subnet_group(name, description, subnets) + changed |= result else: - changed = False + result = update_subnet_group(subnet_group, name, description, subnets) + changed |= result + subnet_group = get_subnet_group(name) + else: + if subnet_group: + result = delete_subnet_group(name) + changed |= result + subnet_group = None - module.exit_json(changed=changed) + module.exit_json(changed=changed, cache_subnet_group=subnet_group) if __name__ == '__main__':