AnsibleAWSModule related cleanup - redshift (#66779)

* AnsibleAWSModule related cleanup - redshift

* Apply a backoff on modify_cluster to cope with concurrent operations

* Add AWS 'hacking' policy to allow creation of Redshift ServiceRole

* Adding the retry policies makes the redshift test suite more reliable
This commit is contained in:
Mark Chappell 2020-02-12 19:17:18 +01:00 committed by GitHub
parent a6bb3ae291
commit 8d574c3770
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
4 changed files with 76 additions and 22 deletions

View file

@ -0,0 +1,2 @@
minor_changes:
- 'redshift: Add AWSRetry calls for errors outside our control'

View file

@ -11,6 +11,15 @@
} }
} }
}, },
{
"Action": "iam:CreateServiceLinkedRole",
"Effect": "Allow",
"Resource": "arn:aws:iam::*:role/aws-service-role/redshift.amazonaws.com/AWSServiceRoleForRedshift",
"Condition": {
"StringLike": {
"iam:AWSServiceName": "redshift.amazonaws.com"}
}
},
{ {
"Sid": "AllowRDSReadEverywhere", "Sid": "AllowRDSReadEverywhere",
"Effect": "Allow", "Effect": "Allow",

View file

@ -261,8 +261,9 @@ cluster:
try: try:
import botocore import botocore
except ImportError: except ImportError:
pass # handled by AnsibleAWSModule pass # caught by AnsibleAWSModule
from ansible.module_utils.ec2 import ec2_argument_spec, snake_dict_to_camel_dict
from ansible.module_utils.ec2 import AWSRetry, snake_dict_to_camel_dict
from ansible.module_utils.aws.core import AnsibleAWSModule, is_boto3_error_code from ansible.module_utils.aws.core import AnsibleAWSModule, is_boto3_error_code
@ -303,6 +304,45 @@ def _collect_facts(resource):
return facts return facts
@AWSRetry.jittered_backoff()
def _describe_cluster(redshift, identifier):
'''
Basic wrapper around describe_clusters with a retry applied
'''
return redshift.describe_clusters(ClusterIdentifier=identifier)['Clusters'][0]
@AWSRetry.jittered_backoff()
def _create_cluster(redshift, **kwargs):
'''
Basic wrapper around create_cluster with a retry applied
'''
return redshift.create_cluster(**kwargs)
# Simple wrapper around delete, try to avoid throwing an error if some other
# operation is in progress
@AWSRetry.jittered_backoff(catch_extra_error_codes=['InvalidClusterState'])
def _delete_cluster(redshift, **kwargs):
'''
Basic wrapper around delete_cluster with a retry applied.
Explicitly catches 'InvalidClusterState' (~ Operation in progress) so that
we can still delete a cluster if some kind of change operation was in
progress.
'''
return redshift.delete_cluster(**kwargs)
@AWSRetry.jittered_backoff(catch_extra_error_codes=['InvalidClusterState'])
def _modify_cluster(redshift, **kwargs):
'''
Basic wrapper around modify_cluster with a retry applied.
Explicitly catches 'InvalidClusterState' (~ Operation in progress) for cases
where another modification is still in progress
'''
return redshift.modify_cluster(**kwargs)
def create_cluster(module, redshift): def create_cluster(module, redshift):
""" """
Create a new cluster Create a new cluster
@ -340,11 +380,12 @@ def create_cluster(module, redshift):
params['d_b_name'] = d_b_name params['d_b_name'] = d_b_name
try: try:
redshift.describe_clusters(ClusterIdentifier=identifier)['Clusters'][0] _describe_cluster(redshift, identifier)
changed = False changed = False
except is_boto3_error_code('ClusterNotFound'): except is_boto3_error_code('ClusterNotFound'):
try: try:
redshift.create_cluster(ClusterIdentifier=identifier, _create_cluster(redshift,
ClusterIdentifier=identifier,
NodeType=node_type, NodeType=node_type,
MasterUsername=username, MasterUsername=username,
MasterUserPassword=password, MasterUserPassword=password,
@ -364,7 +405,7 @@ def create_cluster(module, redshift):
except (botocore.exceptions.ClientError, botocore.exceptions.BotoCoreError) as e: except (botocore.exceptions.ClientError, botocore.exceptions.BotoCoreError) as e:
module.fail_json_aws(e, msg="Timeout waiting for the cluster creation") module.fail_json_aws(e, msg="Timeout waiting for the cluster creation")
try: try:
resource = redshift.describe_clusters(ClusterIdentifier=identifier)['Clusters'][0] resource = _describe_cluster(redshift, identifier)
except (botocore.exceptions.BotoCoreError, botocore.exceptions.ClientError) as e: except (botocore.exceptions.BotoCoreError, botocore.exceptions.ClientError) as e:
module.fail_json_aws(e, msg="Failed to describe cluster") module.fail_json_aws(e, msg="Failed to describe cluster")
@ -381,7 +422,7 @@ def describe_cluster(module, redshift):
identifier = module.params.get('identifier') identifier = module.params.get('identifier')
try: try:
resource = redshift.describe_clusters(ClusterIdentifier=identifier)['Clusters'][0] resource = _describe_cluster(redshift, identifier)
except (botocore.exceptions.BotoCoreError, botocore.exceptions.ClientError) as e: except (botocore.exceptions.BotoCoreError, botocore.exceptions.ClientError) as e:
module.fail_json_aws(e, msg="Error describing cluster") module.fail_json_aws(e, msg="Error describing cluster")
@ -409,10 +450,10 @@ def delete_cluster(module, redshift):
params[p] = module.params.get(p) params[p] = module.params.get(p)
try: try:
redshift.delete_cluster( _delete_cluster(
redshift,
ClusterIdentifier=identifier, ClusterIdentifier=identifier,
**snake_dict_to_camel_dict(params, capitalize_first=True) **snake_dict_to_camel_dict(params, capitalize_first=True))
)
except is_boto3_error_code('ClusterNotFound'): except is_boto3_error_code('ClusterNotFound'):
return(False, {}) return(False, {})
except (botocore.exceptions.ClientError, botocore.exceptions.BotoCoreError) as e: # pylint: disable=duplicate-except except (botocore.exceptions.ClientError, botocore.exceptions.BotoCoreError) as e: # pylint: disable=duplicate-except
@ -459,7 +500,9 @@ def modify_cluster(module, redshift):
# enhanced_vpc_routing parameter change needs an exclusive request # enhanced_vpc_routing parameter change needs an exclusive request
if module.params.get('enhanced_vpc_routing') is not None: if module.params.get('enhanced_vpc_routing') is not None:
try: try:
redshift.modify_cluster(ClusterIdentifier=identifier, _modify_cluster(
redshift,
ClusterIdentifier=identifier,
EnhancedVpcRouting=module.params.get('enhanced_vpc_routing')) EnhancedVpcRouting=module.params.get('enhanced_vpc_routing'))
except (botocore.exceptions.BotoCoreError, botocore.exceptions.ClientError) as e: except (botocore.exceptions.BotoCoreError, botocore.exceptions.ClientError) as e:
module.fail_json_aws(e, msg="Couldn't modify redshift cluster %s " % identifier) module.fail_json_aws(e, msg="Couldn't modify redshift cluster %s " % identifier)
@ -478,7 +521,9 @@ def modify_cluster(module, redshift):
# change the rest # change the rest
try: try:
redshift.modify_cluster(ClusterIdentifier=identifier, _modify_cluster(
redshift,
ClusterIdentifier=identifier,
**snake_dict_to_camel_dict(params, capitalize_first=True)) **snake_dict_to_camel_dict(params, capitalize_first=True))
except (botocore.exceptions.BotoCoreError, botocore.exceptions.ClientError) as e: except (botocore.exceptions.BotoCoreError, botocore.exceptions.ClientError) as e:
module.fail_json_aws(e, msg="Couldn't modify redshift cluster %s " % identifier) module.fail_json_aws(e, msg="Couldn't modify redshift cluster %s " % identifier)
@ -497,7 +542,7 @@ def modify_cluster(module, redshift):
except (botocore.exceptions.ClientError, botocore.exceptions.BotoCoreError) as e: except (botocore.exceptions.ClientError, botocore.exceptions.BotoCoreError) as e:
module.fail_json_aws(e, msg="Timeout waiting for cluster modification") module.fail_json_aws(e, msg="Timeout waiting for cluster modification")
try: try:
resource = redshift.describe_clusters(ClusterIdentifier=identifier)['Clusters'][0] resource = _describe_cluster(redshift, identifier)
except (botocore.exceptions.BotoCoreError, botocore.exceptions.ClientError) as e: except (botocore.exceptions.BotoCoreError, botocore.exceptions.ClientError) as e:
module.fail_json(e, msg="Couldn't modify redshift cluster %s " % identifier) module.fail_json(e, msg="Couldn't modify redshift cluster %s " % identifier)
@ -505,8 +550,7 @@ def modify_cluster(module, redshift):
def main(): def main():
argument_spec = ec2_argument_spec() argument_spec = dict(
argument_spec.update(dict(
command=dict(choices=['create', 'facts', 'delete', 'modify'], required=True), command=dict(choices=['create', 'facts', 'delete', 'modify'], required=True),
identifier=dict(required=True), identifier=dict(required=True),
node_type=dict(choices=['ds1.xlarge', 'ds1.8xlarge', 'ds2.xlarge', node_type=dict(choices=['ds1.xlarge', 'ds1.8xlarge', 'ds2.xlarge',
@ -538,7 +582,7 @@ def main():
enhanced_vpc_routing=dict(type='bool', default=False), enhanced_vpc_routing=dict(type='bool', default=False),
wait=dict(type='bool', default=False), wait=dict(type='bool', default=False),
wait_timeout=dict(type='int', default=300), wait_timeout=dict(type='int', default=300),
)) )
required_if = [ required_if = [
('command', 'delete', ['skip_final_cluster_snapshot']), ('command', 'delete', ['skip_final_cluster_snapshot']),

View file

@ -1,3 +1,2 @@
unstable
cloud/aws cloud/aws
shippable/aws/group1 shippable/aws/group1