[cloud] change iam_managed_policy return to snake_case (#26219)
* Change iam_managed_policy to return snake case Import module_utils.ec2 methods explicitly * iam_managed_policy: tidy argument spec Remove unnecessary specifications (`default=None`, `required=False`) Use `required_if` to check for `policy` when `state` is `present` * Provide exception handling in iam_managed_policy Pretty much all AWS API calls can go wrong, and we should handle them. Update line wrapping to improve readability of method calls * Improve error handling when policy version limit exceeded Better document policy version limit exceeded, and check the error code to see that that's actually what happened (rather than e.g. no permissions to add a new version) * iam_managed_policy: better handle pagination and retry
This commit is contained in:
parent
179e2882f7
commit
df321e8579
1 changed files with 179 additions and 113 deletions
|
@ -102,137 +102,191 @@ policy:
|
||||||
returned: success
|
returned: success
|
||||||
type: string
|
type: string
|
||||||
sample: '{
|
sample: '{
|
||||||
"Arn": "arn:aws:iam::aws:policy/AdministratorAccess "
|
"arn": "arn:aws:iam::aws:policy/AdministratorAccess "
|
||||||
"AttachmentCount": 0,
|
"attachment_count": 0,
|
||||||
"CreateDate": "2017-03-01T15:42:55.981000+00:00",
|
"create_date": "2017-03-01T15:42:55.981000+00:00",
|
||||||
"DefaultVersionId": "v1",
|
"default_version_id": "v1",
|
||||||
"IsAttachable": true,
|
"is_attachable": true,
|
||||||
"Path": "/",
|
"path": "/",
|
||||||
"PolicyId": "ANPALM4KLDMTFXGOOJIHL",
|
"policy_id": "ANPALM4KLDMTFXGOOJIHL",
|
||||||
"PolicyName": "AdministratorAccess",
|
"policy_name": "AdministratorAccess",
|
||||||
"UpdateDate": "2017-03-01T15:42:55.981000+00:00"
|
"update_date": "2017-03-01T15:42:55.981000+00:00"
|
||||||
}'
|
}'
|
||||||
'''
|
'''
|
||||||
|
|
||||||
from ansible.module_utils.basic import AnsibleModule
|
from ansible.module_utils.basic import AnsibleModule
|
||||||
import ansible.module_utils.ec2
|
from ansible.module_utils.ec2 import boto3_conn, get_aws_connection_info, ec2_argument_spec, AWSRetry
|
||||||
from ansible.module_utils.ec2 import sort_json_policy_dict
|
from ansible.module_utils.ec2 import sort_json_policy_dict, camel_dict_to_snake_dict, HAS_BOTO3
|
||||||
import json
|
import json
|
||||||
|
import traceback
|
||||||
|
|
||||||
try:
|
try:
|
||||||
import boto3
|
|
||||||
import botocore
|
import botocore
|
||||||
HAS_BOTO3 = True
|
|
||||||
except ImportError:
|
except ImportError:
|
||||||
HAS_BOTO3 = False
|
pass # caught by imported HAS_BOTO3
|
||||||
|
|
||||||
|
|
||||||
def get_policy_by_name(iam, name, **kwargs):
|
@AWSRetry.backoff(tries=5, delay=5, backoff=2.0)
|
||||||
response = iam.list_policies(Scope='Local', **kwargs)
|
def list_policies_with_backoff(iam):
|
||||||
|
paginator = iam.get_paginator('list_policies')
|
||||||
|
return paginator.paginate(Scope='Local').build_full_result()
|
||||||
|
|
||||||
|
|
||||||
|
def get_policy_by_name(module, iam, name):
|
||||||
|
try:
|
||||||
|
response = list_policies_with_backoff(iam)
|
||||||
|
except botocore.exceptions.ClientError as e:
|
||||||
|
module.fail_json(msg="Couldn't list policies: %s" % str(e),
|
||||||
|
exception=traceback.format_exc(),
|
||||||
|
**camel_dict_to_snake_dict(e.response))
|
||||||
for policy in response['Policies']:
|
for policy in response['Policies']:
|
||||||
if policy['PolicyName'] == name:
|
if policy['PolicyName'] == name:
|
||||||
return policy
|
return policy
|
||||||
if response['IsTruncated']:
|
|
||||||
return get_policy_by_name(iam, name, marker=response['marker'])
|
|
||||||
return None
|
return None
|
||||||
|
|
||||||
|
|
||||||
def delete_oldest_non_default_version(iam, policy):
|
def delete_oldest_non_default_version(module, iam, policy):
|
||||||
versions = [v for v in iam.list_policy_versions(PolicyArn=policy['Arn'])[
|
try:
|
||||||
'Versions'] if not v['IsDefaultVersion']]
|
versions = [v for v in iam.list_policy_versions(PolicyArn=policy['Arn'])['Versions']
|
||||||
|
if not v['IsDefaultVersion']]
|
||||||
|
except botocore.exceptions.ClientError as e:
|
||||||
|
module.fail_json(msg="Couldn't list policy versions: %s" % str(e),
|
||||||
|
exception=traceback.format_exc(),
|
||||||
|
**camel_dict_to_snake_dict(e.response))
|
||||||
versions.sort(key=lambda v: v['CreateDate'], reverse=True)
|
versions.sort(key=lambda v: v['CreateDate'], reverse=True)
|
||||||
for v in versions[-1:]:
|
for v in versions[-1:]:
|
||||||
iam.delete_policy_version(
|
try:
|
||||||
PolicyArn=policy['Arn'],
|
iam.delete_policy_version(PolicyArn=policy['Arn'], VersionId=v['VersionId'])
|
||||||
VersionId=v['VersionId']
|
except botocore.exceptions.ClientError as e:
|
||||||
)
|
module.fail_json(msg="Couldn't delete policy version: %s" % str(e),
|
||||||
|
exception=traceback.format_exc(),
|
||||||
|
**camel_dict_to_snake_dict(e.response))
|
||||||
|
|
||||||
|
|
||||||
# This needs to return policy_version, changed
|
# This needs to return policy_version, changed
|
||||||
|
def get_or_create_policy_version(module, iam, policy, policy_document):
|
||||||
|
try:
|
||||||
def get_or_create_policy_version(iam, policy, policy_document):
|
|
||||||
versions = iam.list_policy_versions(PolicyArn=policy['Arn'])['Versions']
|
versions = iam.list_policy_versions(PolicyArn=policy['Arn'])['Versions']
|
||||||
|
except botocore.exceptions.ClientError as e:
|
||||||
|
module.fail_json(msg="Couldn't list policy versions: %s" % str(e),
|
||||||
|
exception=traceback.format_exc(),
|
||||||
|
**camel_dict_to_snake_dict(e.response))
|
||||||
for v in versions:
|
for v in versions:
|
||||||
document = iam.get_policy_version(
|
try:
|
||||||
PolicyArn=policy['Arn'],
|
document = iam.get_policy_version(PolicyArn=policy['Arn'],
|
||||||
VersionId=v['VersionId'])['PolicyVersion']['Document']
|
VersionId=v['VersionId'])['PolicyVersion']['Document']
|
||||||
|
except botocore.exceptions.ClientError as e:
|
||||||
|
module.fail_json(msg="Couldn't get policy version %s: %s" % (v['VersionId'], str(e)),
|
||||||
|
exception=traceback.format_exc(),
|
||||||
|
**camel_dict_to_snake_dict(e.response))
|
||||||
if sort_json_policy_dict(document) == sort_json_policy_dict(
|
if sort_json_policy_dict(document) == sort_json_policy_dict(
|
||||||
json.loads(policy_document)):
|
json.loads(policy_document)):
|
||||||
return v, False
|
return v, False
|
||||||
|
|
||||||
# No existing version so create one
|
# No existing version so create one
|
||||||
# Instead of testing the versions list for the magic number 5, we are
|
# There is a service limit (typically 5) of policy versions.
|
||||||
# going to attempt to create a version and catch the error
|
#
|
||||||
|
# Rather than assume that it is 5, we'll try to create the policy
|
||||||
|
# and if that doesn't work, delete the oldest non default policy version
|
||||||
|
# and try again.
|
||||||
try:
|
try:
|
||||||
return iam.create_policy_version(
|
version = iam.create_policy_version(PolicyArn=policy['Arn'], PolicyDocument=policy_document)['PolicyVersion']
|
||||||
PolicyArn=policy['Arn'],
|
return version, True
|
||||||
PolicyDocument=policy_document
|
except botocore.exceptions.ClientError as e:
|
||||||
)['PolicyVersion'], True
|
if e['Error']['Code'] == 'LimitExceeded':
|
||||||
except Exception as e:
|
delete_oldest_non_default_version(module, iam, policy)
|
||||||
delete_oldest_non_default_version(iam, policy)
|
try:
|
||||||
return iam.create_policy_version(
|
version = iam.create_policy_version(PolicyArn=policy['Arn'], PolicyDocument=policy_document)['PolicyVersion']
|
||||||
PolicyArn=policy['Arn'],
|
return version, True
|
||||||
PolicyDocument=policy_document
|
except botocore.exceptions.ClientError as e:
|
||||||
)['PolicyVersion'], True
|
pass
|
||||||
|
# Handle both when the exception isn't LimitExceeded or
|
||||||
|
# the second attempt still failed
|
||||||
|
module.fail_json(msg="Couldn't create policy version: %s" % str(e),
|
||||||
|
exception=traceback.format_exc(),
|
||||||
|
**camel_dict_to_snake_dict(e.response))
|
||||||
|
|
||||||
|
|
||||||
def set_if_default(iam, policy, policy_version, is_default):
|
def set_if_default(module, iam, policy, policy_version, is_default):
|
||||||
if is_default and not policy_version['IsDefaultVersion']:
|
if is_default and not policy_version['IsDefaultVersion']:
|
||||||
iam.set_default_policy_version(
|
try:
|
||||||
PolicyArn=policy['Arn'],
|
iam.set_default_policy_version(PolicyArn=policy['Arn'], VersionId=policy_version['VersionId'])
|
||||||
VersionId=policy_version['VersionId']
|
except botocore.exceptions.ClientError as e:
|
||||||
)
|
module.fail_json(msg="Couldn't set default policy version: %s" % str(e),
|
||||||
|
exception=traceback.format_exc(),
|
||||||
|
**camel_dict_to_snake_dict(e.response))
|
||||||
return True
|
return True
|
||||||
return False
|
return False
|
||||||
|
|
||||||
|
|
||||||
def set_if_only(iam, policy, policy_version, is_only):
|
def set_if_only(module, iam, policy, policy_version, is_only):
|
||||||
if is_only:
|
if is_only:
|
||||||
|
try:
|
||||||
versions = [v for v in iam.list_policy_versions(PolicyArn=policy['Arn'])[
|
versions = [v for v in iam.list_policy_versions(PolicyArn=policy['Arn'])[
|
||||||
'Versions'] if not v['IsDefaultVersion']]
|
'Versions'] if not v['IsDefaultVersion']]
|
||||||
|
except botocore.exceptions.ClientError as e:
|
||||||
|
module.fail_json(msg="Couldn't list policy versions: %s" % str(e),
|
||||||
|
exception=traceback.format_exc(),
|
||||||
|
**camel_dict_to_snake_dict(e.response))
|
||||||
for v in versions:
|
for v in versions:
|
||||||
iam.delete_policy_version(
|
try:
|
||||||
PolicyArn=policy['Arn'],
|
iam.delete_policy_version(PolicyArn=policy['Arn'], VersionId=v['VersionId'])
|
||||||
VersionId=v['VersionId']
|
except botocore.exceptions.ClientError as e:
|
||||||
)
|
module.fail_json(msg="Couldn't delete policy version: %s" % str(e),
|
||||||
|
exception=traceback.format_exc(),
|
||||||
|
**camel_dict_to_snake_dict(e.response))
|
||||||
return len(versions) > 0
|
return len(versions) > 0
|
||||||
return False
|
return False
|
||||||
|
|
||||||
|
|
||||||
def detach_all_entities(iam, policy, **kwargs):
|
def detach_all_entities(module, iam, policy, **kwargs):
|
||||||
|
try:
|
||||||
entities = iam.list_entities_for_policy(PolicyArn=policy['Arn'], **kwargs)
|
entities = iam.list_entities_for_policy(PolicyArn=policy['Arn'], **kwargs)
|
||||||
|
except botocore.exceptions.ClientError as e:
|
||||||
|
module.fail_json(msg="Couldn't detach list entities for policy %s: %s" % (policy['PolicyName'], str(e)),
|
||||||
|
exception=traceback.format_exc(),
|
||||||
|
**camel_dict_to_snake_dict(e.response))
|
||||||
|
|
||||||
for g in entities['PolicyGroups']:
|
for g in entities['PolicyGroups']:
|
||||||
iam.detach_group_policy(
|
try:
|
||||||
PolicyArn=policy['Arn'],
|
iam.detach_group_policy(PolicyArn=policy['Arn'], GroupName=g['GroupName'])
|
||||||
GroupName=g['GroupName']
|
except botocore.exceptions.ClientError as e:
|
||||||
)
|
module.fail_json(msg="Couldn't detach group policy %s: %s" % (g['GroupName'], str(e)),
|
||||||
|
exception=traceback.format_exc(),
|
||||||
|
**camel_dict_to_snake_dict(e.response))
|
||||||
for u in entities['PolicyUsers']:
|
for u in entities['PolicyUsers']:
|
||||||
iam.detach_user_policy(
|
try:
|
||||||
PolicyArn=policy['Arn'],
|
iam.detach_user_policy(PolicyArn=policy['Arn'], UserName=u['UserName'])
|
||||||
UserName=u['UserName']
|
except botocore.exceptions.ClientError as e:
|
||||||
)
|
module.fail_json(msg="Couldn't detach user policy %s: %s" % (u['UserName'], str(e)),
|
||||||
|
exception=traceback.format_exc(),
|
||||||
|
**camel_dict_to_snake_dict(e.response))
|
||||||
for r in entities['PolicyRoles']:
|
for r in entities['PolicyRoles']:
|
||||||
iam.detach_role_policy(
|
try:
|
||||||
PolicyArn=policy['Arn'],
|
iam.detach_role_policy(PolicyArn=policy['Arn'], RoleName=r['RoleName'])
|
||||||
RoleName=r['RoleName']
|
except botocore.exceptions.ClientError as e:
|
||||||
)
|
module.fail_json(msg="Couldn't detach role policy %s: %s" % (r['RoleName'], str(e)),
|
||||||
|
exception=traceback.format_exc(),
|
||||||
|
**camel_dict_to_snake_dict(e.response))
|
||||||
if entities['IsTruncated']:
|
if entities['IsTruncated']:
|
||||||
detach_all_entities(iam, policy, marker=entities['Marker'])
|
detach_all_entities(module, iam, policy, marker=entities['Marker'])
|
||||||
|
|
||||||
|
|
||||||
def main():
|
def main():
|
||||||
argument_spec = ansible.module_utils.ec2.ec2_argument_spec()
|
argument_spec = ec2_argument_spec()
|
||||||
argument_spec.update(dict(
|
argument_spec.update(dict(
|
||||||
policy_name=dict(required=True),
|
policy_name=dict(required=True),
|
||||||
policy_description=dict(required=False, default=''),
|
policy_description=dict(default=''),
|
||||||
policy=dict(type='json', required=False, default=None),
|
policy=dict(type='json'),
|
||||||
make_default=dict(type='bool', required=False, default=True),
|
make_default=dict(type='bool', default=True),
|
||||||
only_version=dict(type='bool', required=False, default=False),
|
only_version=dict(type='bool', default=False),
|
||||||
fail_on_delete=dict(type='bool', required=False, default=True),
|
fail_on_delete=dict(type='bool', default=True),
|
||||||
state=dict(required=True, default=None, choices=['present', 'absent']),
|
state=dict(required=True, choices=['present', 'absent']),
|
||||||
))
|
))
|
||||||
|
|
||||||
module = AnsibleModule(
|
module = AnsibleModule(
|
||||||
argument_spec=argument_spec,
|
argument_spec=argument_spec,
|
||||||
|
required_if=[['state', 'present', ['policy']]]
|
||||||
)
|
)
|
||||||
|
|
||||||
if not HAS_BOTO3:
|
if not HAS_BOTO3:
|
||||||
|
@ -249,58 +303,70 @@ def main():
|
||||||
if module.params.get('policy') is not None:
|
if module.params.get('policy') is not None:
|
||||||
policy = json.dumps(json.loads(module.params.get('policy')))
|
policy = json.dumps(json.loads(module.params.get('policy')))
|
||||||
|
|
||||||
if state == 'present' and policy is None:
|
|
||||||
module.fail_json(msg='if state is present policy is required')
|
|
||||||
|
|
||||||
try:
|
try:
|
||||||
region, ec2_url, aws_connect_kwargs = ansible.module_utils.ec2.get_aws_connection_info(
|
region, ec2_url, aws_connect_kwargs = get_aws_connection_info(module, boto3=True)
|
||||||
module, boto3=True)
|
iam = boto3_conn(module, conn_type='client', resource='iam',
|
||||||
iam = ansible.module_utils.ec2.boto3_conn(
|
region=region, endpoint=ec2_url, **aws_connect_kwargs)
|
||||||
module,
|
except (botocore.exceptions.NoCredentialsError, botocore.exceptions.ProfileNotFound) as e:
|
||||||
conn_type='client',
|
module.fail_json(msg="Can't authorize connection. Check your credentials and profile.",
|
||||||
resource='iam',
|
exceptions=traceback.format_exc(), **camel_dict_to_snake_dict(e.response))
|
||||||
region=region,
|
|
||||||
endpoint=ec2_url,
|
|
||||||
**aws_connect_kwargs)
|
|
||||||
except botocore.exceptions.NoCredentialsError as e:
|
|
||||||
module.fail_json(msg=boto_exception(e))
|
|
||||||
|
|
||||||
p = get_policy_by_name(iam, name)
|
p = get_policy_by_name(module, iam, name)
|
||||||
if state == 'present':
|
if state == 'present':
|
||||||
if p is None:
|
if p is None:
|
||||||
# No Policy so just create one
|
# No Policy so just create one
|
||||||
rvalue = iam.create_policy(
|
try:
|
||||||
PolicyName=name,
|
rvalue = iam.create_policy(PolicyName=name, Path='/',
|
||||||
Path='/',
|
PolicyDocument=policy, Description=description)
|
||||||
PolicyDocument=policy,
|
except:
|
||||||
Description=description
|
module.fail_json(msg="Couldn't create policy %s: %s" % (name, str(e)),
|
||||||
)
|
exception=traceback.format_exc(),
|
||||||
module.exit_json(changed=True, policy=rvalue['Policy'])
|
**camel_dict_to_snake_dict(e.response))
|
||||||
|
|
||||||
|
module.exit_json(changed=True, policy=camel_dict_to_snake_dict(rvalue['Policy']))
|
||||||
else:
|
else:
|
||||||
policy_version, changed = get_or_create_policy_version(
|
policy_version, changed = get_or_create_policy_version(module, iam, p, policy)
|
||||||
iam, p, policy)
|
changed = set_if_default(module, iam, p, policy_version, default) or changed
|
||||||
changed = set_if_default(
|
changed = set_if_only(module, iam, p, policy_version, only) or changed
|
||||||
iam, p, policy_version, default) or changed
|
|
||||||
changed = set_if_only(iam, p, policy_version, only) or changed
|
|
||||||
# If anything has changed we needto refresh the policy
|
# If anything has changed we needto refresh the policy
|
||||||
if changed:
|
if changed:
|
||||||
|
try:
|
||||||
p = iam.get_policy(PolicyArn=p['Arn'])['Policy']
|
p = iam.get_policy(PolicyArn=p['Arn'])['Policy']
|
||||||
|
except:
|
||||||
|
module.fail_json(msg="Couldn't get policy: %s" % str(e),
|
||||||
|
exception=traceback.format_exc(),
|
||||||
|
**camel_dict_to_snake_dict(e.response))
|
||||||
|
|
||||||
module.exit_json(changed=changed, policy=p)
|
module.exit_json(changed=changed, policy=camel_dict_to_snake_dict(p))
|
||||||
else:
|
else:
|
||||||
# Check for existing policy
|
# Check for existing policy
|
||||||
if p:
|
if p:
|
||||||
# Detach policy
|
# Detach policy
|
||||||
detach_all_entities(iam, p)
|
detach_all_entities(module, iam, p)
|
||||||
# Delete Versions
|
# Delete Versions
|
||||||
for v in iam.list_policy_versions(PolicyArn=p['Arn'])['Versions']:
|
try:
|
||||||
|
versions = iam.list_policy_versions(PolicyArn=p['Arn'])['Versions']
|
||||||
|
except botocore.exceptions.ClientError as e:
|
||||||
|
module.fail_json(msg="Couldn't list policy versions: %s" % str(e),
|
||||||
|
exception=traceback.format_exc(),
|
||||||
|
**camel_dict_to_snake_dict(e.response))
|
||||||
|
for v in versions:
|
||||||
if not v['IsDefaultVersion']:
|
if not v['IsDefaultVersion']:
|
||||||
iam.delete_policy_version(
|
try:
|
||||||
PolicyArn=p['Arn'], VersionId=v['VersionId'])
|
iam.delete_policy_version(PolicyArn=p['Arn'], VersionId=v['VersionId'])
|
||||||
|
except botocore.exceptions.ClientError as e:
|
||||||
|
module.fail_json(msg="Couldn't delete policy version %s: %s" % (v['VersionId'], str(e)),
|
||||||
|
exception=traceback.format_exc(),
|
||||||
|
**camel_dict_to_snake_dict(e.response))
|
||||||
# Delete policy
|
# Delete policy
|
||||||
|
try:
|
||||||
iam.delete_policy(PolicyArn=p['Arn'])
|
iam.delete_policy(PolicyArn=p['Arn'])
|
||||||
|
except:
|
||||||
|
module.fail_json(msg="Couldn't delete policy %s: %s" % (p['PolicyName'], str(e)),
|
||||||
|
exception=traceback.format_exc(),
|
||||||
|
**camel_dict_to_snake_dict(e.response))
|
||||||
# This is the one case where we will return the old policy
|
# This is the one case where we will return the old policy
|
||||||
module.exit_json(changed=True, policy=p)
|
module.exit_json(changed=True, policy=camel_dict_to_snake_dict(p))
|
||||||
else:
|
else:
|
||||||
module.exit_json(changed=False, policy=None)
|
module.exit_json(changed=False, policy=None)
|
||||||
# end main
|
# end main
|
||||||
|
|
Loading…
Reference in a new issue