diff --git a/lib/ansible/module_utils/aws/waf.py b/lib/ansible/module_utils/aws/waf.py index 6758b40ed6..39de220227 100644 --- a/lib/ansible/module_utils/aws/waf.py +++ b/lib/ansible/module_utils/aws/waf.py @@ -30,6 +30,7 @@ This module adds shared support for Web Application Firewall modules """ from ansible.module_utils.ec2 import camel_dict_to_snake_dict, AWSRetry +from ansible.module_utils.aws.waiters import get_waiter try: import botocore @@ -183,6 +184,13 @@ def get_change_token(client, module): @AWSRetry.backoff(tries=10, delay=2, backoff=2.0, catch_extra_error_codes=['WAFStaleDataException']) -def run_func_with_change_token_backoff(client, module, params, func): +def run_func_with_change_token_backoff(client, module, params, func, wait=False): params['ChangeToken'] = get_change_token(client, module) - return func(**params) + result = func(**params) + if wait: + get_waiter( + client, 'change_token_in_sync', + ).wait( + ChangeToken=result['ChangeToken'] + ) + return result diff --git a/lib/ansible/module_utils/aws/waiters.py b/lib/ansible/module_utils/aws/waiters.py index 4d59fa3077..7adb0c41cc 100644 --- a/lib/ansible/module_utils/aws/waiters.py +++ b/lib/ansible/module_utils/aws/waiters.py @@ -119,54 +119,90 @@ ec2_data = { } -def model_for(name): +waf_data = { + "version": 2, + "waiters": { + "ChangeTokenInSync": { + "delay": 20, + "maxAttempts": 60, + "operation": "GetChangeTokenStatus", + "acceptors": [ + { + "matcher": "path", + "expected": True, + "argument": "ChangeTokenStatus == 'INSYNC'", + "state": "success" + }, + { + "matcher": "error", + "expected": "WAFInternalErrorException", + "state": "retry" + } + ] + } + } +} + + +def ec2_model(name): ec2_models = core_waiter.WaiterModel(waiter_config=ec2_data) return ec2_models.get_waiter(name) +def waf_model(name): + waf_models = core_waiter.WaiterModel(waiter_config=waf_data) + return waf_models.get_waiter(name) + + waiters_by_name = { ('EC2', 'route_table_exists'): lambda ec2: core_waiter.Waiter( 'route_table_exists', - model_for('RouteTableExists'), + ec2_model('RouteTableExists'), core_waiter.NormalizedOperationMethod( ec2.describe_route_tables )), ('EC2', 'subnet_exists'): lambda ec2: core_waiter.Waiter( 'subnet_exists', - model_for('SubnetExists'), + ec2_model('SubnetExists'), core_waiter.NormalizedOperationMethod( ec2.describe_subnets )), ('EC2', 'subnet_has_map_public'): lambda ec2: core_waiter.Waiter( 'subnet_has_map_public', - model_for('SubnetHasMapPublic'), + ec2_model('SubnetHasMapPublic'), core_waiter.NormalizedOperationMethod( ec2.describe_subnets )), ('EC2', 'subnet_no_map_public'): lambda ec2: core_waiter.Waiter( 'subnet_no_map_public', - model_for('SubnetNoMapPublic'), + ec2_model('SubnetNoMapPublic'), core_waiter.NormalizedOperationMethod( ec2.describe_subnets )), ('EC2', 'subnet_has_assign_ipv6'): lambda ec2: core_waiter.Waiter( 'subnet_has_assign_ipv6', - model_for('SubnetHasAssignIpv6'), + ec2_model('SubnetHasAssignIpv6'), core_waiter.NormalizedOperationMethod( ec2.describe_subnets )), ('EC2', 'subnet_no_assign_ipv6'): lambda ec2: core_waiter.Waiter( 'subnet_no_assign_ipv6', - model_for('SubnetNoAssignIpv6'), + ec2_model('SubnetNoAssignIpv6'), core_waiter.NormalizedOperationMethod( ec2.describe_subnets )), ('EC2', 'subnet_deleted'): lambda ec2: core_waiter.Waiter( 'subnet_deleted', - model_for('SubnetDeleted'), + ec2_model('SubnetDeleted'), core_waiter.NormalizedOperationMethod( ec2.describe_subnets )), + ('WAF', 'change_token_in_sync'): lambda waf: core_waiter.Waiter( + 'change_token_in_sync', + waf_model('ChangeTokenInSync'), + core_waiter.NormalizedOperationMethod( + waf.get_change_token_status + )), } diff --git a/lib/ansible/modules/cloud/amazon/aws_waf_condition.py b/lib/ansible/modules/cloud/amazon/aws_waf_condition.py index 4ae88d1d1d..006aa3fa64 100644 --- a/lib/ansible/modules/cloud/amazon/aws_waf_condition.py +++ b/lib/ansible/modules/cloud/amazon/aws_waf_condition.py @@ -451,7 +451,7 @@ class Condition(object): updates.extend([{'Action': 'DELETE', 'RegexPatternString': pattern} for pattern in extra]) run_func_with_change_token_backoff(self.client, self.module, {'RegexPatternSetId': pattern_set['RegexPatternSetId'], 'Updates': updates}, - self.client.update_regex_pattern_set) + self.client.update_regex_pattern_set, wait=True) return self.get_regex_pattern_set_with_backoff(pattern_set['RegexPatternSetId'])['RegexPatternSet'] def delete_unused_regex_pattern(self, regex_pattern_set_id): @@ -466,8 +466,10 @@ class Condition(object): run_func_with_change_token_backoff(self.client, self.module, {'RegexPatternSetId': regex_pattern_set_id}, - self.client.delete_regex_pattern_set) + self.client.delete_regex_pattern_set, wait=True) except (botocore.exceptions.ClientError, botocore.exceptions.BotoCoreError) as e: + if e.response['Error']['Code'] == 'WAFNonexistentItemException': + return self.module.fail_json_aws(e, msg='Could not delete regex pattern') def get_condition_by_name(self, name): @@ -537,6 +539,7 @@ class Condition(object): func = getattr(self.client, 'update_' + self.method_suffix) params = self.format_for_deletion(current_condition) try: + # We do not need to wait for the conditiontuple delete because we wait later for the delete_* call run_func_with_change_token_backoff(self.client, self.module, params, func) except (botocore.exceptions.ClientError, botocore.exceptions.BotoCoreError) as e: self.module.fail_json_aws(e, msg='Could not delete filters from condition') @@ -544,7 +547,7 @@ class Condition(object): params = dict() params[self.conditionsetid] = condition_set_id try: - run_func_with_change_token_backoff(self.client, self.module, params, func) + run_func_with_change_token_backoff(self.client, self.module, params, func, wait=True) except (botocore.exceptions.ClientError, botocore.exceptions.BotoCoreError) as e: self.module.fail_json_aws(e, msg='Could not delete condition') # tidy up regex patterns @@ -580,7 +583,7 @@ class Condition(object): update['Updates'] = missing + extra func = getattr(self.client, 'update_' + self.method_suffix) try: - run_func_with_change_token_backoff(self.client, self.module, update, func) + result = run_func_with_change_token_backoff(self.client, self.module, update, func, wait=True) except (botocore.exceptions.ClientError, botocore.exceptions.BotoCoreError) as e: self.module.fail_json_aws(e, msg='Could not update condition') return changed, self.get_condition_by_id(condition_set_id) diff --git a/lib/ansible/modules/cloud/amazon/aws_waf_rule.py b/lib/ansible/modules/cloud/amazon/aws_waf_rule.py index afc5fab15e..5c0b6785f2 100644 --- a/lib/ansible/modules/cloud/amazon/aws_waf_rule.py +++ b/lib/ansible/modules/cloud/amazon/aws_waf_rule.py @@ -207,7 +207,7 @@ def find_and_update_rule(client, module, rule_id): } if changed: try: - run_func_with_change_token_backoff(client, module, update, client.update_rule) + run_func_with_change_token_backoff(client, module, update, client.update_rule, wait=True) except (botocore.exceptions.ClientError, botocore.exceptions.BotoCoreError) as e: module.fail_json_aws(e, msg='Could not update rule conditions') @@ -282,7 +282,7 @@ def ensure_rule_absent(client, module): if rule_id: remove_rule_conditions(client, module, rule_id) try: - return True, run_func_with_change_token_backoff(client, module, {'RuleId': rule_id}, client.delete_rule) + return True, run_func_with_change_token_backoff(client, module, {'RuleId': rule_id}, client.delete_rule, wait=True) except (botocore.exceptions.ClientError, botocore.exceptions.BotoCoreError) as e: module.fail_json_aws(e, msg='Could not delete rule') return False, {} diff --git a/lib/ansible/modules/cloud/amazon/aws_waf_web_acl.py b/lib/ansible/modules/cloud/amazon/aws_waf_web_acl.py index 2b0ea67714..0fb2007564 100644 --- a/lib/ansible/modules/cloud/amazon/aws_waf_web_acl.py +++ b/lib/ansible/modules/cloud/amazon/aws_waf_web_acl.py @@ -135,6 +135,7 @@ except ImportError: import re from ansible.module_utils.aws.core import AnsibleAWSModule +from ansible.module_utils.aws.waiters import get_waiter from ansible.module_utils.ec2 import boto3_conn, get_aws_connection_info, ec2_argument_spec, camel_dict_to_snake_dict from ansible.module_utils.aws.waf import list_rules_with_backoff, list_web_acls_with_backoff, run_func_with_change_token_backoff @@ -193,18 +194,33 @@ def find_and_update_web_acl(client, module, web_acl_id): 'WebACLId': acl['WebACLId'], 'DefaultAction': acl['DefaultAction'] } + change_tokens = [] if deletions: try: params['Updates'] = deletions - run_func_with_change_token_backoff(client, module, params, client.update_web_acl) + result = run_func_with_change_token_backoff(client, module, params, client.update_web_acl) + change_tokens.append(result['ChangeToken']) + get_waiter( + client, 'change_token_in_sync', + ).wait( + ChangeToken=result['ChangeToken'] + ) except (botocore.exceptions.ClientError, botocore.exceptions.BotoCoreError) as e: module.fail_json_aws(e, msg='Could not update Web ACL') if insertions: try: params['Updates'] = insertions - run_func_with_change_token_backoff(client, module, params, client.update_web_acl) + result = run_func_with_change_token_backoff(client, module, params, client.update_web_acl) + change_tokens.append(result['ChangeToken']) except (botocore.exceptions.ClientError, botocore.exceptions.BotoCoreError) as e: module.fail_json_aws(e, msg='Could not update Web ACL') + if change_tokens: + for token in change_tokens: + get_waiter( + client, 'change_token_in_sync', + ).wait( + ChangeToken=token + ) if changed: acl = get_web_acl(client, module, web_acl_id) return changed, acl @@ -261,7 +277,7 @@ def ensure_web_acl_absent(client, module): if web_acl['Rules']: remove_rules_from_web_acl(client, module, web_acl_id) try: - run_func_with_change_token_backoff(client, module, {'WebACLId': web_acl_id}, client.delete_web_acl) + run_func_with_change_token_backoff(client, module, {'WebACLId': web_acl_id}, client.delete_web_acl, wait=True) return True, {} except (botocore.exceptions.ClientError, botocore.exceptions.BotoCoreError) as e: module.fail_json_aws(e, msg='Could not delete Web ACL')