diff options
-rw-r--r-- | hacking/aws_config/testing_policies/security-policy.json | 3 | ||||
-rw-r--r-- | lib/ansible/modules/cloud/amazon/iam_role.py | 496 |
2 files changed, 276 insertions, 223 deletions
diff --git a/hacking/aws_config/testing_policies/security-policy.json b/hacking/aws_config/testing_policies/security-policy.json index f0d0768a7a..adacf0040c 100644 --- a/hacking/aws_config/testing_policies/security-policy.json +++ b/hacking/aws_config/testing_policies/security-policy.json @@ -20,6 +20,7 @@ "iam:ListPolicies", "iam:ListRoles", "iam:ListRolePolicies", + "iam:ListRoleTags", "iam:ListUsers", "iam:ListAccountAliases" ], @@ -50,6 +51,8 @@ "iam:PassRole", "iam:PutRolePolicy", "iam:PutRolePermissionsBoundary", + "iam:TagRole", + "iam:UntagRole", "iam:UpdateAssumeRolePolicy", "iam:UpdateRole", "iam:UpdateRoleDescription", diff --git a/lib/ansible/modules/cloud/amazon/iam_role.py b/lib/ansible/modules/cloud/amazon/iam_role.py index 7e841ff3d1..30369e3b02 100644 --- a/lib/ansible/modules/cloud/amazon/iam_role.py +++ b/lib/ansible/modules/cloud/amazon/iam_role.py @@ -221,28 +221,17 @@ def compare_assume_role_policy_doc(current_policy_doc, new_policy_doc): return False -def compare_attached_role_policies(current_attached_policies, new_attached_policies): - - # If new_attached_policies is None it means we want to remove all policies - if len(current_attached_policies) > 0 and new_attached_policies is None: - return False - - current_attached_policies_arn_list = [] - for policy in current_attached_policies: - current_attached_policies_arn_list.append(policy['PolicyArn']) - - if set(current_attached_policies_arn_list) == set(new_attached_policies): - return True - else: - return False +@AWSRetry.jittered_backoff() +def _list_policies(connection): + paginator = connection.get_paginator('list_policies') + policies = paginator.paginate().build_full_result()['Policies'] def convert_friendly_names_to_arns(connection, module, policy_names): if not any([not policy.startswith('arn:') for policy in policy_names]): return policy_names allpolicies = {} - paginator = connection.get_paginator('list_policies') - policies = paginator.paginate().build_full_result()['Policies'] + policies = _list_policies(connection) for policy in policies: allpolicies[policy['PolicyName']] = policy['Arn'] @@ -253,244 +242,310 @@ def convert_friendly_names_to_arns(connection, module, policy_names): module.fail_json_aws(e, msg="Couldn't find policy") +def attach_policies(connection, module, policies_to_attach, params): + changed = False + for policy_arn in policies_to_attach: + try: + if not module.check_mode: + connection.attach_role_policy(RoleName=params['RoleName'], PolicyArn=policy_arn, aws_retry=True) + except (BotoCoreError, ClientError) as e: + module.fail_json_aws(e, msg="Unable to attach policy {0} to role {1}".format(policy_arn, params['RoleName'])) + changed = True + return changed + + def remove_policies(connection, module, policies_to_remove, params): changed = False for policy in policies_to_remove: try: if not module.check_mode: - connection.detach_role_policy(RoleName=params['RoleName'], PolicyArn=policy) - except ClientError as e: - module.fail_json_aws(e, msg="Unable to detach policy {0} from {1}".format(policy, params['RoleName'])) - except BotoCoreError as e: + connection.detach_role_policy(RoleName=params['RoleName'], PolicyArn=policy, aws_retry=True) + except (BotoCoreError, ClientError) as e: module.fail_json_aws(e, msg="Unable to detach policy {0} from {1}".format(policy, params['RoleName'])) changed = True return changed -def create_or_update_role(connection, module): +def generate_create_params(module): params = dict() params['Path'] = module.params.get('path') params['RoleName'] = module.params.get('name') params['AssumeRolePolicyDocument'] = module.params.get('assume_role_policy_document') - if module.params.get('max_session_duration') is not None: - params['MaxSessionDuration'] = module.params.get('max_session_duration') if module.params.get('description') is not None: params['Description'] = module.params.get('description') + if module.params.get('max_session_duration') is not None: + params['MaxSessionDuration'] = module.params.get('max_session_duration') if module.params.get('boundary') is not None: params['PermissionsBoundary'] = module.params.get('boundary') - managed_policies = module.params.get('managed_policies') + if module.params.get('tags') is not None: + params['Tags'] = ansible_dict_to_boto3_tag_list(module.params.get('tags')) + + return params + + +def create_basic_role(connection, module, params): + """ + Perform the Role creation. + Assumes tests for the role existing have already been performed. + """ + + try: + if not module.check_mode: + role = connection.create_role(aws_retry=True, **params) + # 'Description' is documented as key of the role returned by create_role + # but appears to be an AWS bug (the value is not returned using the AWS CLI either). + # Get the role after creating it. + role = get_role_with_backoff(connection, module, params['RoleName']) + else: + role = {'MadeInCheckMode': True} + role['AssumeRolePolicyDocument'] = json.loads(params['AssumeRolePolicyDocument']) + except (BotoCoreError, ClientError) as e: + module.fail_json_aws(e, msg="Unable to create role") + + return role + + +def update_role_assumed_policy(connection, module, params, role): + # Check Assumed Policy document + if compare_assume_role_policy_doc(role['AssumeRolePolicyDocument'], params['AssumeRolePolicyDocument']): + return False + + if module.check_mode: + return True + + try: + connection.update_assume_role_policy( + RoleName=params['RoleName'], + PolicyDocument=json.dumps(json.loads(params['AssumeRolePolicyDocument'])), + aws_retry=True) + except (BotoCoreError, ClientError) as e: + module.fail_json_aws(e, msg="Unable to update assume role policy for role {0}".format(params['RoleName'])) + return True + + +def update_role_description(connection, module, params, role): + # Check Description update + if params.get('Description') is None: + return False + if role.get('Description') == params['Description']: + return False + + if module.check_mode: + return True + + try: + connection.update_role_description(RoleName=params['RoleName'], Description=params['Description'], aws_retry=True) + except (BotoCoreError, ClientError) as e: + module.fail_json_aws(e, msg="Unable to update description for role {0}".format(params['RoleName'])) + return True + + +def update_role_max_session_duration(connection, module, params, role): + # Check MaxSessionDuration update + if params.get('MaxSessionDuration') is None: + return False + if role.get('MaxSessionDuration') == params['MaxSessionDuration']: + return False + + if module.check_mode: + return True + + try: + connection.update_role(RoleName=params['RoleName'], MaxSessionDuration=params['MaxSessionDuration'], aws_retry=True) + except (BotoCoreError, ClientError) as e: + module.fail_json_aws(e, msg="Unable to update maximum session duration for role {0}".format(params['RoleName'])) + return True + + +def update_role_permissions_boundary(connection, module, params, role): + # Check PermissionsBoundary + if params.get('PermissionsBoundary') is None: + return False + if params.get('PermissionsBoundary') == role.get('PermissionsBoundary', {}).get('PermissionsBoundaryArn', ''): + return False + + if module.check_mode: + return True + + if params.get('PermissionsBoundary') == '': + try: + connection.delete_role_permissions_boundary(RoleName=params['RoleName'], aws_retry=True) + except (BotoCoreError, ClientError) as e: + module.fail_json_aws(e, msg="Unable to remove permission boundary for role {0}".format(params['RoleName'])) + else: + try: + connection.put_role_permissions_boundary(RoleName=params['RoleName'], PermissionsBoundary=params['PermissionsBoundary'], aws_retry=True) + except (BotoCoreError, ClientError) as e: + module.fail_json_aws(e, msg="Unable to update permission boundary for role {0}".format(params['RoleName'])) + return True + + +def update_managed_policies(connection, module, params, role, managed_policies, purge_policies): + # Check Managed Policies + if managed_policies is None: + return False + + # If we're manipulating a fake role + if role.get('MadeInCheckMode', False): + role['AttachedPolicies'] = list(map(lambda x: {'PolicyArn': x, 'PolicyName': x.split(':')[5]}, managed_policies)) + return True + + # Get list of current attached managed policies + current_attached_policies = get_attached_policy_list(connection, module, params['RoleName']) + current_attached_policies_arn_list = [policy['PolicyArn'] for policy in current_attached_policies] + + if len(managed_policies) == 1 and managed_policies[0] is None: + managed_policies = [] + + policies_to_remove = set(current_attached_policies_arn_list) - set(managed_policies) + policies_to_attach = set(managed_policies) - set(current_attached_policies_arn_list) + + changed = False + + if purge_policies: + changed |= remove_policies(connection, module, policies_to_remove, params) + + changed |= attach_policies(connection, module, policies_to_attach, params) + + return changed + + +def create_or_update_role(connection, module): + + params = generate_create_params(module) + role_name = params['RoleName'] + create_instance_profile = module.params.get('create_instance_profile') purge_policies = module.params.get('purge_policies') if purge_policies is None: purge_policies = True - create_instance_profile = module.params.get('create_instance_profile') + managed_policies = module.params.get('managed_policies') if managed_policies: + # Attempt to list the policies early so we don't leave things behind if we can't find them. managed_policies = convert_friendly_names_to_arns(connection, module, managed_policies) + changed = False # Get role - role = get_role(connection, module, params['RoleName']) + role = get_role(connection, module, role_name) # If role is None, create it if role is None: - try: - if not module.check_mode: - role = connection.create_role(**params) - # 'Description' is documented as key of the role returned by create_role - # but appears to be an AWS bug (the value is not returned using the AWS CLI either). - # Get the role after creating it. - role = get_role_with_backoff(connection, module, params['RoleName']) - else: - role = {'MadeInCheckMode': True} - role['AssumeRolePolicyDocument'] = json.loads(params['AssumeRolePolicyDocument']) - changed = True - except ClientError as e: - module.fail_json_aws(e, msg="Unable to create role") - except BotoCoreError as e: - module.fail_json_aws(e, msg="Unable to create role") + role = create_basic_role(connection, module, params) + changed = True else: - # Check Assumed Policy document - if not compare_assume_role_policy_doc(role['AssumeRolePolicyDocument'], params['AssumeRolePolicyDocument']): - try: - if not module.check_mode: - connection.update_assume_role_policy(RoleName=params['RoleName'], PolicyDocument=json.dumps(json.loads(params['AssumeRolePolicyDocument']))) - changed = True - except ClientError as e: - module.fail_json_aws(e, msg="Unable to update assume role policy for role {0}".format(params['RoleName'])) - except BotoCoreError as e: - module.fail_json_aws(e, msg="Unable to update assume role policy for role {0}".format(params['RoleName'])) - - if managed_policies is not None: - # Get list of current attached managed policies - current_attached_policies = get_attached_policy_list(connection, module, params['RoleName']) - current_attached_policies_arn_list = [policy['PolicyArn'] for policy in current_attached_policies] - - # If a single empty list item then all managed policies to be removed - if len(managed_policies) == 1 and not managed_policies[0] and purge_policies: - - # Detach policies not present - if remove_policies(connection, module, set(current_attached_policies_arn_list) - set(managed_policies), params): - changed = True - else: - # Make a list of the ARNs from the attached policies - - # Detach roles not defined in task - if purge_policies: - if remove_policies(connection, module, set(current_attached_policies_arn_list) - set(managed_policies), params): - changed = True - - # Attach roles not already attached - for policy_arn in set(managed_policies) - set(current_attached_policies_arn_list): - try: - if not module.check_mode: - connection.attach_role_policy(RoleName=params['RoleName'], PolicyArn=policy_arn) - except ClientError as e: - module.fail_json_aws(e, msg="Unable to attach policy {0} to role {1}".format(policy_arn, params['RoleName'])) - except BotoCoreError as e: - module.fail_json_aws(e, msg="Unable to attach policy {0} to role {1}".format(policy_arn, params['RoleName'])) - changed = True - - # Instance profile - if create_instance_profile and not role.get('MadeInCheckMode', False): - try: - instance_profiles = connection.list_instance_profiles_for_role(RoleName=params['RoleName'])['InstanceProfiles'] - except ClientError as e: - module.fail_json_aws(e, msg="Unable to list instance profiles for role {0}".format(params['RoleName'])) - except BotoCoreError as e: - module.fail_json_aws(e, msg="Unable to list instance profiles for role {0}".format(params['RoleName'])) - if not any(p['InstanceProfileName'] == params['RoleName'] for p in instance_profiles): - # Make sure an instance profile is attached - try: - if not module.check_mode: - connection.create_instance_profile(InstanceProfileName=params['RoleName'], Path=params['Path']) - changed = True - except ClientError as e: - # If the profile already exists, no problem, move on - if e.response['Error']['Code'] == 'EntityAlreadyExists': - pass - else: - module.fail_json_aws(e, msg="Unable to create instance profile for role {0}".format(params['RoleName'])) - except BotoCoreError as e: - module.fail_json_aws(e, msg="Unable to create instance profile for role {0}".format(params['RoleName'])) - if not module.check_mode: - connection.add_role_to_instance_profile(InstanceProfileName=params['RoleName'], RoleName=params['RoleName']) + changed |= update_role_tags(connection, module, params, role) + changed |= update_role_assumed_policy(connection, module, params, role) + changed |= update_role_description(connection, module, params, role) + changed |= update_role_max_session_duration(connection, module, params, role) + changed |= update_role_permissions_boundary(connection, module, params, role) - # Check Description update - if not role.get('MadeInCheckMode') and params.get('Description') and role.get('Description') != params['Description']: - try: - if not module.check_mode: - connection.update_role_description(RoleName=params['RoleName'], Description=params['Description']) + if create_instance_profile: + changed |= create_instance_profiles(connection, module, params, role) - changed = True - except (BotoCoreError, ClientError) as e: - module.fail_json_aws(e, msg="Unable to update description for role {0}".format(params['RoleName'])) - - # Check MaxSessionDuration update - if not role.get('MadeInCheckMode') and params.get('MaxSessionDuration') and role.get('MaxSessionDuration') != params['MaxSessionDuration']: - try: - if not module.check_mode: - connection.update_role(RoleName=params['RoleName'], MaxSessionDuration=params['MaxSessionDuration']) - - changed = True - except (BotoCoreError, ClientError) as e: - module.fail_json_aws(e, msg="Unable to update maximum session duration for role {0}".format(params['RoleName'])) - - # Check if permission boundary needs update - if not role.get('MadeInCheckMode') and ( - (role.get('PermissionsBoundary') or {}).get('PermissionsBoundaryArn') or - params.get('PermissionsBoundary') is not None): - # the existing role has a boundary - if module.params.get('boundary') is None: - pass - elif module.params.get('boundary') == '': - if (role.get('PermissionsBoundary') or {}).get('PermissionsBoundaryArn'): - try: - if not module.check_mode: - connection.delete_role_permissions_boundary(RoleName=params['RoleName']) - changed = True - except (BotoCoreError, ClientError) as e: - module.fail_json_aws(e, msg="Unable to remove permission boundary for role {0}".format(params['RoleName'])) - elif (role.get('PermissionsBoundary') or {}).get('PermissionsBoundaryArn') != params['PermissionsBoundary']: - try: - if not module.check_mode: - connection.put_role_permissions_boundary(RoleName=params['RoleName'], PermissionsBoundary=params['PermissionsBoundary']) - changed = True - except (BotoCoreError, ClientError) as e: - module.fail_json_aws(e, msg="Unable to update permission boundary for role {0}".format(params['RoleName'])) + changed |= update_managed_policies(connection, module, params, role, managed_policies, purge_policies) # Get the role again if not role.get('MadeInCheckMode', False): role = get_role(connection, module, params['RoleName']) - role['attached_policies'] = get_attached_policy_list(connection, module, params['RoleName']) + role['AttachedPolicies'] = get_attached_policy_list(connection, module, params['RoleName']) + role['tags'] = get_role_tags(connection, module) - # Manage tags - tags_changed = update_role_tags(connection, module) - changed |= tags_changed - role['tags'] = get_role_tags(connection, module) + module.exit_json( + changed=changed, iam_role=camel_dict_to_snake_dict(role, ignore_list=['tags']), + **camel_dict_to_snake_dict(role, ignore_list=['tags'])) - module.exit_json(changed=changed, iam_role=camel_dict_to_snake_dict(role, ignore_list=['tags']), **camel_dict_to_snake_dict(role, ignore_list=['tags'])) +def create_instance_profiles(connection, module, params, role): -def destroy_role(connection, module): + if role.get('MadeInCheckMode', False): + return False - params = dict() - params['RoleName'] = module.params.get('name') + # Fetch existing Profiles + try: + instance_profiles = connection.list_instance_profiles_for_role(RoleName=params['RoleName'], aws_retry=True)['InstanceProfiles'] + except (BotoCoreError, ClientError) as e: + module.fail_json_aws(e, msg="Unable to list instance profiles for role {0}".format(params['RoleName'])) - role = get_role(connection, module, params['RoleName']) + # Profile already exists + if any(p['InstanceProfileName'] == params['RoleName'] for p in instance_profiles): + return False - if role: + if module.check_mode: + return True - # We need to remove any instance profiles from the role before we delete it - try: - instance_profiles = connection.list_instance_profiles_for_role(RoleName=params['RoleName'])['InstanceProfiles'] - except ClientError as e: - module.fail_json_aws(e, msg="Unable to list instance profiles for role {0}".format(params['RoleName'])) - except BotoCoreError as e: - module.fail_json_aws(e, msg="Unable to list instance profiles for role {0}".format(params['RoleName'])) - - if role.get('PermissionsBoundary') is not None: - try: - connection.delete_role_permissions_boundary(RoleName=params['RoleName']) - except (ClientError, BotoCoreError) as e: - module.fail_json_aws(e, msg="Could not delete role permission boundary on role {0}".format(params['RoleName'])) - - # Now remove the role from the instance profile(s) - for profile in instance_profiles: - try: - if not module.check_mode: - connection.remove_role_from_instance_profile(InstanceProfileName=profile['InstanceProfileName'], RoleName=params['RoleName']) - if profile['InstanceProfileName'] == params['RoleName']: - if module.params.get("delete_instance_profile"): - try: - connection.delete_instance_profile(InstanceProfileName=profile['InstanceProfileName']) - except ClientError as e: - module.fail_json_aws(e, msg="Unable to remove instance profile {0}".format(profile['InstanceProfileName'])) - except ClientError as e: - module.fail_json_aws(e, msg="Unable to remove role {0} from instance profile {1}".format( - params['RoleName'], profile['InstanceProfileName'])) - except BotoCoreError as e: - module.fail_json_aws(e, msg="Unable to remove role {0} from instance profile {1}".format( - params['RoleName'], profile['InstanceProfileName'])) - - # Now remove any attached policies otherwise deletion fails - try: - for policy in get_attached_policy_list(connection, module, params['RoleName']): - if not module.check_mode: - connection.detach_role_policy(RoleName=params['RoleName'], PolicyArn=policy['PolicyArn']) - except ClientError as e: - module.fail_json_aws(e, msg="Unable to detach policy {0} from role {1}".format(policy['PolicyArn'], params['RoleName'])) - except BotoCoreError as e: - module.fail_json_aws(e, msg="Unable to detach policy {0} from role {1}".format(policy['PolicyArn'], params['RoleName'])) + # Make sure an instance profile is created + try: + connection.create_instance_profile(InstanceProfileName=params['RoleName'], Path=params['Path'], aws_retry=True) + except ClientError as e: + # If the profile already exists, no problem, move on. + # Implies someone's changing things at the same time... + if e.response['Error']['Code'] == 'EntityAlreadyExists': + return False + else: + module.fail_json_aws(e, msg="Unable to create instance profile for role {0}".format(params['RoleName'])) + except BotoCoreError as e: + module.fail_json_aws(e, msg="Unable to create instance profile for role {0}".format(params['RoleName'])) + + # And attach the role to the profile + try: + connection.add_role_to_instance_profile(InstanceProfileName=params['RoleName'], RoleName=params['RoleName'], aws_retry=True) + except (BotoCoreError, ClientError) as e: + module.fail_json_aws(e, msg="Unable to attach role {0} to instance profile {0}".format(params['RoleName'])) + + return True + + +def remove_instance_profiles(connection, module, role_params, role): + role_name = module.params.get('name') + delete_profiles = module.params.get("delete_instance_profile") + + try: + instance_profiles = connection.list_instance_profiles_for_role(aws_retry=True, **role_params)['InstanceProfiles'] + except (BotoCoreError, ClientError) as e: + module.fail_json_aws(e, msg="Unable to list instance profiles for role {0}".format(role_name)) + # Remove the role from the instance profile(s) + for profile in instance_profiles: + profile_name = profile['InstanceProfileName'] try: if not module.check_mode: - connection.delete_role(**params) - except ClientError as e: - module.fail_json_aws(e, msg="Unable to delete role") - except BotoCoreError as e: - module.fail_json_aws(e, msg="Unable to delete role") - else: + connection.remove_role_from_instance_profile(aws_retry=True, InstanceProfileName=profile_name, **role_params) + if profile_name == role_name: + if delete_profiles: + try: + connection.delete_instance_profile(InstanceProfileName=profile_name, aws_retry=True) + except (BotoCoreError, ClientError) as e: + module.fail_json_aws(e, msg="Unable to remove instance profile {0}".format(profile_name)) + except (BotoCoreError, ClientError) as e: + module.fail_json_aws(e, msg="Unable to remove role {0} from instance profile {1}".format(role_name, profile_name)) + + +def destroy_role(connection, module): + + role_name = module.params.get('name') + role = get_role(connection, module, role_name) + role_params = dict() + role_params['RoleName'] = role_name + boundary_params = dict(role_params) + boundary_params['PermissionsBoundary'] = '' + + if role is None: module.exit_json(changed=False) + # Before we try to delete the role we need to remove any + # - attached instance profiles + # - attached managed policies + # - permissions boundary + remove_instance_profiles(connection, module, role_params, role) + update_managed_policies(connection, module, role_params, role, [], True) + update_role_permissions_boundary(connection, module, boundary_params, role) + + try: + if not module.check_mode: + connection.delete_role(aws_retry=True, **role_params) + except (BotoCoreError, ClientError) as e: + module.fail_json_aws(e, msg="Unable to delete role") + module.exit_json(changed=True) @@ -503,7 +558,7 @@ def get_role_with_backoff(connection, module, name): def get_role(connection, module, name): try: - return connection.get_role(RoleName=name)['Role'] + return connection.get_role(RoleName=name, aws_retry=True)['Role'] except ClientError as e: if e.response['Error']['Code'] == 'NoSuchEntity': return None @@ -514,15 +569,9 @@ def get_role(connection, module, name): def get_attached_policy_list(connection, module, name): - try: - return connection.list_attached_role_policies(RoleName=name)['AttachedPolicies'] - except ClientError as e: - if e.response['Error']['Code'] == 'NoSuchEntity': - return [] - else: - module.fail_json_aws(e, msg="Unable to list attached policies for role {0}".format(name)) - except BotoCoreError as e: + return connection.list_attached_role_policies(RoleName=name, aws_retry=True)['AttachedPolicies'] + except (ClientError, BotoCoreError) as e: module.fail_json_aws(e, msg="Unable to list attached policies for role {0}".format(name)) @@ -531,21 +580,22 @@ def get_role_tags(connection, module): if not hasattr(connection, 'list_role_tags'): return {} try: - return boto3_tag_list_to_ansible_dict(connection.list_role_tags(RoleName=role_name)['Tags']) - except ClientError: - return {} + return boto3_tag_list_to_ansible_dict(connection.list_role_tags(RoleName=role_name, aws_retry=True)['Tags']) + except (ClientError, BotoCoreError) as e: + module.fail_json_aws(e, msg="Unable to list tags for role {0}".format(role_name)) -def update_role_tags(connection, module): - new_tags = module.params.get('tags') +def update_role_tags(connection, module, params, role): + new_tags = params.get('Tags') if new_tags is None: return False + new_tags = boto3_tag_list_to_ansible_dict(new_tags) role_name = module.params.get('name') purge_tags = module.params.get('purge_tags') try: - existing_tags = boto3_tag_list_to_ansible_dict(connection.list_role_tags(RoleName=role_name)['Tags']) + existing_tags = boto3_tag_list_to_ansible_dict(connection.list_role_tags(RoleName=role_name, aws_retry=True)['Tags']) except (ClientError, KeyError): existing_tags = {} @@ -554,9 +604,9 @@ def update_role_tags(connection, module): if not module.check_mode: try: if tags_to_remove: - connection.untag_role(RoleName=role_name, TagKeys=tags_to_remove) + connection.untag_role(RoleName=role_name, TagKeys=tags_to_remove, aws_retry=True) if tags_to_add: - connection.tag_role(RoleName=role_name, Tags=ansible_dict_to_boto3_tag_list(tags_to_add)) + connection.tag_role(RoleName=role_name, Tags=ansible_dict_to_boto3_tag_list(tags_to_add), aws_retry=True) except (ClientError, BotoCoreError) as e: module.fail_json_aws(e, msg='Unable to set tags for role %s' % role_name) @@ -609,7 +659,7 @@ def main(): if not path.endswith('/') or not path.startswith('/'): module.fail_json(msg="path must begin and end with /") - connection = module.client('iam') + connection = module.client('iam', retry_decorator=AWSRetry.jittered_backoff()) state = module.params.get("state") |