Update AWS modules that use to implicitly retry on NotFound errors (#67369)

* Update AWS modules that expect to retry on exception codes that match the regex '^\w+.NotFound'

Modules should intentionally define any extra error codes

Use a waiter for ec2_vpc_igw after creating an internet gateway instead of retrying on InvalidInternetGatewayID.NotFound
This commit is contained in:
Sloane Hertel 2020-02-21 08:17:24 -05:00 committed by GitHub
parent ad8df69b58
commit 195ded6527
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
11 changed files with 53 additions and 26 deletions

View file

@ -71,18 +71,18 @@ class ACMServiceManager(object):
kwargs['CertificateStatuses'] = statuses kwargs['CertificateStatuses'] = statuses
return paginator.paginate(**kwargs).build_full_result()['CertificateSummaryList'] return paginator.paginate(**kwargs).build_full_result()['CertificateSummaryList']
@AWSRetry.backoff(tries=5, delay=5, backoff=2.0) @AWSRetry.backoff(tries=5, delay=5, backoff=2.0, catch_extra_error_codes=['ResourceNotFoundException'])
def get_certificate_with_backoff(self, client, certificate_arn): def get_certificate_with_backoff(self, client, certificate_arn):
response = client.get_certificate(CertificateArn=certificate_arn) response = client.get_certificate(CertificateArn=certificate_arn)
# strip out response metadata # strip out response metadata
return {'Certificate': response['Certificate'], return {'Certificate': response['Certificate'],
'CertificateChain': response['CertificateChain']} 'CertificateChain': response['CertificateChain']}
@AWSRetry.backoff(tries=5, delay=5, backoff=2.0) @AWSRetry.backoff(tries=5, delay=5, backoff=2.0, catch_extra_error_codes=['ResourceNotFoundException'])
def describe_certificate_with_backoff(self, client, certificate_arn): def describe_certificate_with_backoff(self, client, certificate_arn):
return client.describe_certificate(CertificateArn=certificate_arn)['Certificate'] return client.describe_certificate(CertificateArn=certificate_arn)['Certificate']
@AWSRetry.backoff(tries=5, delay=5, backoff=2.0) @AWSRetry.backoff(tries=5, delay=5, backoff=2.0, catch_extra_error_codes=['ResourceNotFoundException'])
def list_certificate_tags_with_backoff(self, client, certificate_arn): def list_certificate_tags_with_backoff(self, client, certificate_arn):
return client.list_tags_for_certificate(CertificateArn=certificate_arn)['Tags'] return client.list_tags_for_certificate(CertificateArn=certificate_arn)['Tags']

View file

@ -13,6 +13,24 @@ except ImportError:
ec2_data = { ec2_data = {
"version": 2, "version": 2,
"waiters": { "waiters": {
"InternetGatewayExists": {
"delay": 5,
"maxAttempts": 40,
"operation": "DescribeInternetGateways",
"acceptors": [
{
"matcher": "path",
"expected": True,
"argument": "length(InternetGateways) > `0`",
"state": "success"
},
{
"matcher": "error",
"expected": "InvalidInternetGatewayID.NotFound",
"state": "retry"
},
]
},
"RouteTableExists": { "RouteTableExists": {
"delay": 5, "delay": 5,
"maxAttempts": 40, "maxAttempts": 40,
@ -280,6 +298,12 @@ def rds_model(name):
waiters_by_name = { waiters_by_name = {
('EC2', 'internet_gateway_exists'): lambda ec2: core_waiter.Waiter(
'internet_gateway_exists',
ec2_model('InternetGatewayExists'),
core_waiter.NormalizedOperationMethod(
ec2.describe_internet_gateways
)),
('EC2', 'route_table_exists'): lambda ec2: core_waiter.Waiter( ('EC2', 'route_table_exists'): lambda ec2: core_waiter.Waiter(
'route_table_exists', 'route_table_exists',
ec2_model('RouteTableExists'), ec2_model('RouteTableExists'),

View file

@ -370,8 +370,7 @@ def stack_set_facts(cfn, stack_set_name):
ss['Tags'] = boto3_tag_list_to_ansible_dict(ss['Tags']) ss['Tags'] = boto3_tag_list_to_ansible_dict(ss['Tags'])
return ss return ss
except cfn.exceptions.from_code('StackSetNotFound'): except cfn.exceptions.from_code('StackSetNotFound'):
# catch NotFound error before the retry kicks in to avoid waiting # Return None if the stack doesn't exist
# if the stack does not exist
return return
@ -429,14 +428,15 @@ def await_stack_instance_completion(module, cfn, stack_set_name, max_wait):
def await_stack_set_exists(cfn, stack_set_name): def await_stack_set_exists(cfn, stack_set_name):
# AWSRetry will retry on `NotFound` errors for us # AWSRetry will retry on `StackSetNotFound` errors for us
ss = cfn.describe_stack_set(StackSetName=stack_set_name, aws_retry=True)['StackSet'] ss = cfn.describe_stack_set(StackSetName=stack_set_name, aws_retry=True)['StackSet']
ss['Tags'] = boto3_tag_list_to_ansible_dict(ss['Tags']) ss['Tags'] = boto3_tag_list_to_ansible_dict(ss['Tags'])
return camel_dict_to_snake_dict(ss, ignore_list=('Tags',)) return camel_dict_to_snake_dict(ss, ignore_list=('Tags',))
def describe_stack_tree(module, stack_set_name, operation_ids=None): def describe_stack_tree(module, stack_set_name, operation_ids=None):
cfn = module.client('cloudformation', retry_decorator=AWSRetry.jittered_backoff(retries=5, delay=3, max_delay=5)) jittered_backoff_decorator = AWSRetry.jittered_backoff(retries=5, delay=3, max_delay=5, catch_extra_error_codes=['StackSetNotFound'])
cfn = module.client('cloudformation', retry_decorator=jittered_backoff_decorator)
result = dict() result = dict()
result['stack_set'] = camel_dict_to_snake_dict( result['stack_set'] = camel_dict_to_snake_dict(
cfn.describe_stack_set( cfn.describe_stack_set(
@ -536,7 +536,8 @@ def main():
# Wrap the cloudformation client methods that this module uses with # Wrap the cloudformation client methods that this module uses with
# automatic backoff / retry for throttling error codes # automatic backoff / retry for throttling error codes
cfn = module.client('cloudformation', retry_decorator=AWSRetry.jittered_backoff(retries=10, delay=3, max_delay=30)) jittered_backoff_decorator = AWSRetry.jittered_backoff(retries=10, delay=3, max_delay=30, catch_extra_error_codes=['StackSetNotFound'])
cfn = module.client('cloudformation', retry_decorator=jittered_backoff_decorator)
existing_stack_set = stack_set_facts(cfn, module.params['name']) existing_stack_set = stack_set_facts(cfn, module.params['name'])
operation_uuid = to_native(uuid.uuid4()) operation_uuid = to_native(uuid.uuid4())

View file

@ -545,7 +545,7 @@ def rule_from_group_permission(perm):
) )
@AWSRetry.backoff(tries=5, delay=5, backoff=2.0) @AWSRetry.backoff(tries=5, delay=5, backoff=2.0, catch_extra_error_codes=['InvalidGroup.NotFound'])
def get_security_groups_with_backoff(connection, **kwargs): def get_security_groups_with_backoff(connection, **kwargs):
return connection.describe_security_groups(**kwargs) return connection.describe_security_groups(**kwargs)

View file

@ -479,7 +479,7 @@ def delete_template(module):
def create_or_update(module, template_options): def create_or_update(module, template_options):
ec2 = module.client('ec2', retry_decorator=AWSRetry.jittered_backoff()) ec2 = module.client('ec2', retry_decorator=AWSRetry.jittered_backoff(catch_extra_error_codes=['InvalidLaunchTemplateId.NotFound']))
template, template_versions = existing_templates(module) template, template_versions = existing_templates(module)
out = {} out = {}
lt_data = params_to_launch_data(module, dict((k, v) for k, v in module.params.items() if k in template_options)) lt_data = params_to_launch_data(module, dict((k, v) for k, v in module.params.items() if k in template_options))

View file

@ -213,12 +213,11 @@ class AnsibleEc2TgwInfo(object):
try: try:
response = self._connection.describe_transit_gateways( response = self._connection.describe_transit_gateways(
TransitGatewayIds=transit_gateway_ids, Filters=filters) TransitGatewayIds=transit_gateway_ids, Filters=filters)
except (BotoCoreError, ClientError) as e: except ClientError as e:
if e.response['Error']['Code'] == 'InvalidTransitGatewayID.NotFound': if e.response['Error']['Code'] == 'InvalidTransitGatewayID.NotFound':
self._results['transit_gateways'] = [] self._results['transit_gateways'] = []
return return
else: raise
self._module.fail_json_aws(e)
for transit_gateway in response['TransitGateways']: for transit_gateway in response['TransitGateways']:
transit_gateway_info.append(camel_dict_to_snake_dict(transit_gateway, ignore_list=['Tags'])) transit_gateway_info.append(camel_dict_to_snake_dict(transit_gateway, ignore_list=['Tags']))
@ -257,7 +256,10 @@ def main():
) )
tgwf_manager = AnsibleEc2TgwInfo(module=module, results=results) tgwf_manager = AnsibleEc2TgwInfo(module=module, results=results)
try:
tgwf_manager.describe_transit_gateways() tgwf_manager.describe_transit_gateways()
except (BotoCoreError, ClientError) as e:
module.fail_json_aws(e)
module.exit_json(**results) module.exit_json(**results)

View file

@ -219,7 +219,7 @@ def retry_not_found(to_call, *args, **kwargs):
try: try:
return to_call(*args, **kwargs) return to_call(*args, **kwargs)
except EC2ResponseError as e: except EC2ResponseError as e:
if e.error_code == 'InvalidDhcpOptionID.NotFound': if e.error_code in ['InvalidDhcpOptionID.NotFound', 'InvalidDhcpOptionsID.NotFound']:
sleep(3) sleep(3)
continue continue
raise e raise e

View file

@ -91,6 +91,7 @@ except ImportError:
pass # caught by AnsibleAWSModule pass # caught by AnsibleAWSModule
from ansible.module_utils.aws.core import AnsibleAWSModule from ansible.module_utils.aws.core import AnsibleAWSModule
from ansible.module_utils.aws.waiters import get_waiter
from ansible.module_utils.ec2 import ( from ansible.module_utils.ec2 import (
AWSRetry, AWSRetry,
camel_dict_to_snake_dict, camel_dict_to_snake_dict,
@ -237,6 +238,11 @@ class AnsibleEc2Igw(object):
try: try:
response = self._connection.create_internet_gateway() response = self._connection.create_internet_gateway()
# Ensure the gateway exists before trying to attach it or add tags
waiter = get_waiter(self._connection, 'internet_gateway_exists')
waiter.wait(InternetGatewayIds=[response['InternetGateway']['InternetGatewayId']])
igw = camel_dict_to_snake_dict(response['InternetGateway']) igw = camel_dict_to_snake_dict(response['InternetGateway'])
self._connection.attach_internet_gateway(InternetGatewayId=igw['internet_gateway_id'], VpcId=vpc_id) self._connection.attach_internet_gateway(InternetGatewayId=igw['internet_gateway_id'], VpcId=vpc_id)
self._results['changed'] = True self._results['changed'] = True

View file

@ -253,10 +253,7 @@ def get_vpc(module, connection, vpc_id):
module.fail_json_aws(e, msg="Unable to wait for VPC {0} to be available.".format(vpc_id)) module.fail_json_aws(e, msg="Unable to wait for VPC {0} to be available.".format(vpc_id))
try: try:
vpc_obj = AWSRetry.backoff( vpc_obj = connection.describe_vpcs(VpcIds=[vpc_id], aws_retry=True)['Vpcs'][0]
delay=3, tries=8,
catch_extra_error_codes=['InvalidVpcID.NotFound'],
)(connection.describe_vpcs)(VpcIds=[vpc_id])['Vpcs'][0]
except (botocore.exceptions.ClientError, botocore.exceptions.BotoCoreError) as e: except (botocore.exceptions.ClientError, botocore.exceptions.BotoCoreError) as e:
module.fail_json_aws(e, msg="Failed to describe VPCs") module.fail_json_aws(e, msg="Failed to describe VPCs")
try: try:
@ -279,10 +276,7 @@ def update_vpc_tags(connection, module, vpc_id, tags, name):
if tags_to_update: if tags_to_update:
if not module.check_mode: if not module.check_mode:
tags = ansible_dict_to_boto3_tag_list(tags_to_update) tags = ansible_dict_to_boto3_tag_list(tags_to_update)
vpc_obj = AWSRetry.backoff( vpc_obj = connection.create_tags(Resources=[vpc_id], Tags=tags, aws_retry=True)
delay=1, tries=5,
catch_extra_error_codes=['InvalidVpcID.NotFound'],
)(connection.create_tags)(Resources=[vpc_id], Tags=tags)
# Wait for tags to be updated # Wait for tags to be updated
expected_tags = boto3_tag_list_to_ansible_dict(tags) expected_tags = boto3_tag_list_to_ansible_dict(tags)

View file

@ -126,7 +126,7 @@ except ImportError:
HAS_BOTO3 = False HAS_BOTO3 = False
@AWSRetry.jittered_backoff(retries=10, delay=10) @AWSRetry.jittered_backoff(retries=10, delay=10, catch_extra_error_codes=['TargetGroupNotFound'])
def describe_target_groups_with_backoff(connection, tg_name): def describe_target_groups_with_backoff(connection, tg_name):
return connection.describe_target_groups(Names=[tg_name]) return connection.describe_target_groups(Names=[tg_name])
@ -147,7 +147,7 @@ def convert_tg_name_to_arn(connection, module, tg_name):
return tg_arn return tg_arn
@AWSRetry.jittered_backoff(retries=10, delay=10) @AWSRetry.jittered_backoff(retries=10, delay=10, catch_extra_error_codes=['TargetGroupNotFound'])
def describe_targets_with_backoff(connection, tg_arn, target): def describe_targets_with_backoff(connection, tg_arn, target):
if target is None: if target is None:
tg = [] tg = []

View file

@ -338,7 +338,7 @@ def main():
required_if=[['state', 'present', ['db_instance_identifier']]] required_if=[['state', 'present', ['db_instance_identifier']]]
) )
client = module.client('rds', retry_decorator=AWSRetry.jittered_backoff(retries=10)) client = module.client('rds', retry_decorator=AWSRetry.jittered_backoff(retries=10, catch_extra_error_codes=['DBSnapshotNotFound']))
if module.params['state'] == 'absent': if module.params['state'] == 'absent':
ret_dict = ensure_snapshot_absent(client, module) ret_dict = ensure_snapshot_absent(client, module)