From b980a5c02ae8af23719066631e49b67ff04d3884 Mon Sep 17 00:00:00 2001 From: sramakr Date: Sun, 16 Jul 2017 22:03:31 -0400 Subject: [PATCH] Use Boto3 for ec2_group Fixes #23507 (#25340) * Use Boto3 for ec2_group Currently boto doesn't support ipv6. To support ipv6 in ec2_group, we need boto3. boto3 has significant API changes, which caused more re-factoring for ec2_group module. Added additional integration test to test_ec2_group role. * Follow the standard for boto3 ansible Fixed imports. Use boto3 ansible exception with camel_dict_to_snake_dict. Refactored the call to authorize/revoke ingress and egress. * Removed dependancy with module ipaddress Added new parameter called cidr_ipv6 for specifying ipv6 addresses inline with how boto3 handles ipv6 addresses. * Updated integration test * Added ipv6 integration test for ec2_group * Set purge_rules to false for integration test * Fixed import statements Added example for ipv6. Removed defining HAS_BOTO3 variable and import HAS_BOTO3 from ec2. Cleaned up import statements. * Fixed exception handling * Add IAM permissions for ec2_group tests Missing AuthorizeSecurityGroupEgress necessary for latest tests * Wrapped botocore import in try/except block Import just botocore to be more similar to other modules --- .../testing_policies/ec2-policy.json | 1 + lib/ansible/modules/cloud/amazon/ec2_group.py | 401 ++++++++++++------ .../targets/ec2_group/tasks/main.yml | 124 +++++- 3 files changed, 370 insertions(+), 156 deletions(-) diff --git a/hacking/aws_config/testing_policies/ec2-policy.json b/hacking/aws_config/testing_policies/ec2-policy.json index 099e78b159..5a47f1b7dc 100644 --- a/hacking/aws_config/testing_policies/ec2-policy.json +++ b/hacking/aws_config/testing_policies/ec2-policy.json @@ -35,6 +35,7 @@ "Effect": "Allow", "Action": [ "ec2:AuthorizeSecurityGroupIngress", + "ec2:AuthorizeSecurityGroupEgress", "ec2:CreateTags", "ec2:DeleteRouteTable", "ec2:DeleteSecurityGroup", diff --git a/lib/ansible/modules/cloud/amazon/ec2_group.py b/lib/ansible/modules/cloud/amazon/ec2_group.py index 15c9e0930e..4fcd658c54 100644 --- a/lib/ansible/modules/cloud/amazon/ec2_group.py +++ b/lib/ansible/modules/cloud/amazon/ec2_group.py @@ -19,12 +19,12 @@ ANSIBLE_METADATA = {'metadata_version': '1.0', 'status': ['stableinterface'], 'supported_by': 'curated'} - DOCUMENTATION = ''' --- module: ec2_group author: "Andrew de Quincey (@adq)" version_added: "1.3" +requirements: [ boto3 ] short_description: maintain an ec2 VPC security group. description: - maintains ec2 security groups. This module has a dependency on python-boto >= 2.5 @@ -143,6 +143,7 @@ EXAMPLES = ''' from_port: 80 to_port: 80 cidr_ip: 0.0.0.0/0 + cidr_ipv6: 64:ff9b::/96 group_name: example-other # description to use if example-other needs to be created group_desc: other example EC2 group @@ -178,6 +179,9 @@ EXAMPLES = ''' cidr_ip: - 172.16.1.0/24 - 172.16.17.0/24 + cidr_ipv6: + - 2607:F8B0::/32 + - 64:ff9b::/96 group_id: - sg-edcd9784 @@ -191,17 +195,17 @@ import json import re import time from ansible.module_utils.basic import AnsibleModule -from ansible.module_utils.ec2 import ec2_connect, ec2_argument_spec +from ansible.module_utils.ec2 import boto3_conn +from ansible.module_utils.ec2 import get_aws_connection_info +from ansible.module_utils.ec2 import ec2_argument_spec +from ansible.module_utils.ec2 import camel_dict_to_snake_dict +from ansible.module_utils.ec2 import HAS_BOTO3 +import traceback try: - import boto.ec2 - from boto.ec2.securitygroup import SecurityGroup - from boto.exception import BotoServerError - HAS_BOTO = True + import botocore except ImportError: - HAS_BOTO = False - -import traceback + pass # caught by imported HAS_BOTO3 def deduplicate_rules_args(rules): @@ -212,35 +216,33 @@ def deduplicate_rules_args(rules): def make_rule_key(prefix, rule, group_id, cidr_ip): - """Creates a unique key for an individual group rule""" - if isinstance(rule, dict): + if 'proto' in rule: proto, from_port, to_port = [rule.get(x, None) for x in ('proto', 'from_port', 'to_port')] - # fix for 11177 - if proto not in ['icmp', 'tcp', 'udp'] and from_port == -1 and to_port == -1: - from_port = 'none' - to_port = 'none' - - else: # isinstance boto.ec2.securitygroup.IPPermissions - proto, from_port, to_port = [getattr(rule, x, None) for x in ('ip_protocol', 'from_port', 'to_port')] - + elif 'IpProtocol' in rule: + proto, from_port, to_port = [rule.get(x, None) for x in ('IpProtocol', 'FromPort', 'ToPort')] + if proto not in ['icmp', 'tcp', 'udp'] and from_port == -1 and to_port == -1: + from_port = 'none' + to_port = 'none' key = "%s-%s-%s-%s-%s-%s" % (prefix, proto, from_port, to_port, group_id, cidr_ip) return key.lower().replace('-none', '-None') -def addRulesToLookup(rules, prefix, rules_dict): - for rule in rules: - for grant in rule.grants: - rules_dict[make_rule_key(prefix, rule, grant.group_id, grant.cidr_ip)] = (rule, grant) +def add_rules_to_loopkup(ipPermissions, group_id, prefix, dict): + for rule in ipPermissions: + for groupGrant in rule.get('UserIdGroupPairs'): + dict[make_rule_key(prefix, rule, group_id, groupGrant.get('GroupId'))] = (rule, groupGrant) + for ipv4Grants in rule.get('IpRanges'): + dict[make_rule_key(prefix, rule, group_id, ipv4Grants.get('CidrIp'))] = (rule, ipv4Grants) + for ipv6Grants in rule.get('Ipv6Ranges'): + dict[make_rule_key(prefix, rule, group_id, ipv6Grants.get('CidrIpv6'))] = (rule, ipv6Grants) def validate_rule(module, rule): - VALID_PARAMS = ('cidr_ip', + VALID_PARAMS = ('cidr_ip', 'cidr_ipv6', 'group_id', 'group_name', 'group_desc', 'proto', 'from_port', 'to_port') - if not isinstance(rule, dict): module.fail_json(msg='Invalid rule parameter type [%s].' % type(rule)) - for k in rule: if k not in VALID_PARAMS: module.fail_json(msg='Invalid rule parameter \'{}\''.format(k)) @@ -249,6 +251,12 @@ def validate_rule(module, rule): module.fail_json(msg='Specify group_id OR cidr_ip, not both') elif 'group_name' in rule and 'cidr_ip' in rule: module.fail_json(msg='Specify group_name OR cidr_ip, not both') + elif 'group_id' in rule and 'cidr_ipv6' in rule: + module.fail_json(msg="Specify group_id OR cidr_ipv6, not both") + elif 'group_name' in rule and 'cidr_ipv6' in rule: + module.fail_json(msg="Specify group_name OR cidr_ipv6, not both") + elif 'cidr_ip' in rule and 'cidr_ipv6' in rule: + module.fail_json(msg="Specify cidr_ip OR cidr_ipv6, not both") elif 'group_id' in rule and 'group_name' in rule: module.fail_json(msg='Specify group_id OR group_name, not both') @@ -270,17 +278,25 @@ def get_target_from_rule(module, ec2, rule, name, group, groups, vpc_id): group_id = None group_name = None ip = None + ipv6 = None target_group_created = False + if 'group_id' in rule and 'cidr_ip' in rule: module.fail_json(msg="Specify group_id OR cidr_ip, not both") elif 'group_name' in rule and 'cidr_ip' in rule: module.fail_json(msg="Specify group_name OR cidr_ip, not both") + elif 'group_id' in rule and 'cidr_ipv6' in rule: + module.fail_json(msg="Specify group_id OR cidr_ipv6, not both") + elif 'group_name' in rule and 'cidr_ipv6' in rule: + module.fail_json(msg="Specify group_name OR cidr_ipv6, not both") elif 'group_id' in rule and 'group_name' in rule: module.fail_json(msg="Specify group_id OR group_name, not both") - elif rule.get('group_id') and re.match(FOREIGN_SECURITY_GROUP_REGEX, rule['group_id']): + elif 'cidr_ip' in rule and 'cidr_ipv6' in rule: + module.fail_json(msg="Specify cidr_ip OR cidr_ipv6, not both") + elif 'group_id' in rule and re.match(FOREIGN_SECURITY_GROUP_REGEX, rule['group_id']): # this is a foreign Security Group. Since you can't fetch it you must create an instance of it owner_id, group_id, group_name = re.match(FOREIGN_SECURITY_GROUP_REGEX, rule['group_id']).groups() - group_instance = SecurityGroup(owner_id=owner_id, name=group_name, id=group_id) + group_instance = SecurityGroup(owner_id=owner_id, group_name=group_name, id=group_id) groups[group_id] = group_instance groups[group_name] = group_instance elif 'group_id' in rule: @@ -295,7 +311,8 @@ def get_target_from_rule(module, ec2, rule, name, group, groups, vpc_id): group_id = groups[group_name].id else: if not rule.get('group_desc', '').strip(): - module.fail_json(msg="group %s will be automatically created by rule %s and no description was provided" % (group_name, rule)) + module.fail_json(msg="group %s will be automatically created by rule %s and " + "no description was provided" % (group_name, rule)) if not module.check_mode: auto_group = ec2.create_security_group(group_name, rule['group_desc'], vpc_id=vpc_id) group_id = auto_group.id @@ -304,8 +321,10 @@ def get_target_from_rule(module, ec2, rule, name, group, groups, vpc_id): target_group_created = True elif 'cidr_ip' in rule: ip = rule['cidr_ip'] + elif 'cidr_ipv6' in rule: + ipv6 = rule['cidr_ipv6'] - return group_id, ip, target_group_created + return group_id, ip, ipv6, target_group_created def ports_expand(ports): @@ -351,7 +370,7 @@ def rules_expand_ports(rules): def rule_expand_source(rule, source_type): # takes a rule dict and returns a list of expanded rule dicts for specified source_type sources = rule[source_type] if isinstance(rule[source_type], list) else [rule[source_type]] - source_types_all = ('cidr_ip', 'group_id', 'group_name') + source_types_all = ('cidr_ip', 'cidr_ipv6', 'group_id', 'group_name') rule_expanded = [] for source in sources: @@ -366,7 +385,7 @@ def rule_expand_source(rule, source_type): def rule_expand_sources(rule): # takes a rule dict and returns a list of expanded rule discts - source_types = (stype for stype in ('cidr_ip', 'group_id', 'group_name') if stype in rule) + source_types = (stype for stype in ('cidr_ip', 'cidr_ipv6', 'group_id', 'group_name') if stype in rule) return [r for stype in source_types for r in rule_expand_source(rule, stype)] @@ -381,6 +400,79 @@ def rules_expand_sources(rules): for rule in rule_expand_sources(rule_complex)] +def authorize_ip(type, changed, client, group, groupRules, + ip, ip_permission, module, rule, ethertype): + # If rule already exists, don't later delete it + for thisip in ip: + rule_id = make_rule_key(type, rule, group.id, thisip) + if rule_id in groupRules: + del groupRules[rule_id] + else: + if not module.check_mode: + ip_permission = serialize_ip_grant(rule, thisip, ethertype) + if ip_permission: + try: + if type == "in": + client.authorize_security_group_ingress(GroupId=group.group_id, + IpPermissions=[ip_permission]) + elif type == "out": + client.authorize_security_group_egress(GroupId=group.group_id, + IpPermissions=[ip_permission]) + except botocore.exceptions.ClientError as e: + module.fail_json(msg="Unable to authorize %s for ip %s security group '%s' - %s" % + (type, thisip, group.group_name, e), + exception=traceback.format_exc(), **camel_dict_to_snake_dict(e.response)) + changed = True + return changed, ip_permission + + +def serialize_group_grant(group_id, rule): + permission = {'IpProtocol': rule['proto'], + 'FromPort': rule['from_port'], + 'ToPort': rule['to_port'], + 'UserIdGroupPairs': [{'GroupId': group_id}]} + return permission + + +def serialize_revoke(grant, rule): + permission = dict() + fromPort = rule['FromPort'] if 'FromPort' in rule else None + toPort = rule['ToPort'] if 'ToPort' in rule else None + if 'GroupId' in grant: + permission = {'IpProtocol': rule['IpProtocol'], + 'FromPort': fromPort, + 'ToPort': toPort, + 'UserIdGroupPairs': [{'GroupId': grant['GroupId'], 'UserId': grant['UserId']}] + } + elif 'CidrIp' in grant: + permission = {'IpProtocol': rule['IpProtocol'], + 'FromPort': fromPort, + 'ToPort': toPort, + 'IpRanges': [grant] + } + elif 'CidrIpv6' in grant: + permission = {'IpProtocol': rule['IpProtocol'], + 'FromPort': fromPort, + 'ToPort': toPort, + 'Ipv6Ranges': [grant] + } + if rule['IpProtocol'] in ('all', '-1', -1): + del permission['FromPort'] + del permission['ToPort'] + return permission + + +def serialize_ip_grant(rule, thisip, ethertype): + permission = {'IpProtocol': rule['proto'], + 'FromPort': rule['from_port'], + 'ToPort': rule['to_port']} + if ethertype == "ipv4": + permission.update({'IpRanges': [{'CidrIp': thisip}]}) + elif ethertype == "ipv6": + permission.update({'Ipv6Ranges': [{'CidrIpv6': thisip}]}) + return permission + + def main(): argument_spec = ec2_argument_spec() argument_spec.update(dict( @@ -392,8 +484,7 @@ def main(): rules_egress=dict(type='list'), state=dict(default='present', type='str', choices=['present', 'absent']), purge_rules=dict(default=True, required=False, type='bool'), - purge_rules_egress=dict(default=True, required=False, type='bool'), - + purge_rules_egress=dict(default=True, required=False, type='bool') ) ) module = AnsibleModule( @@ -403,8 +494,8 @@ def main(): required_if=[['state', 'present', ['name']]], ) - if not HAS_BOTO: - module.fail_json(msg='boto required for this module') + if not HAS_BOTO3: + module.fail_json(msg='boto3 required for this module') name = module.params['name'] group_id = module.params['group_id'] @@ -420,32 +511,43 @@ def main(): module.fail_json(msg='Must provide description when state is present.') changed = False - - ec2 = ec2_connect(module) - - # find the group if present + region, ec2_url, aws_connect_params = get_aws_connection_info(module, boto3=True) + if not region: + module.fail_json(msg="The AWS region must be specified as an " + "environment variable or in the AWS credentials " + "profile.") + client, ec2 = boto3_conn(module, conn_type='both', resource='ec2', endpoint=ec2_url, region=region, **aws_connect_params) group = None - groups = {} - + groups = dict() + security_groups = [] + # do get all security groups + # find if the group is present try: - security_groups = ec2.get_all_security_groups() - except BotoServerError as e: - module.fail_json(msg="Error in get_all_security_groups: %s" % e.message, exception=traceback.format_exc()) + response = client.describe_security_groups() + if 'SecurityGroups' in response: + security_groups = response.get('SecurityGroups') + except botocore.exceptions.NoCredentialsError as e: + module.fail_json(msg="Error in describe_security_groups: %s" % "Unable to locate credentials", exception=traceback.format_exc()) + except botocore.exceptions.ClientError as e: + module.fail_json(msg="Error in describe_security_groups: %s" % e, exception=traceback.format_exc(), + **camel_dict_to_snake_dict(e.response)) - for curGroup in security_groups: - groups[curGroup.id] = curGroup - if curGroup.name in groups: + for sg in security_groups: + curGroup = ec2.SecurityGroup(sg['GroupId']) + groups[curGroup.id] = ec2.SecurityGroup(curGroup.id) + groupName = curGroup.group_name + if groupName in groups: # Prioritise groups from the current VPC if vpc_id is None or curGroup.vpc_id == vpc_id: - groups[curGroup.name] = curGroup + groups[groupName] = curGroup else: - groups[curGroup.name] = curGroup + groups[groupName] = curGroup if group_id: if curGroup.id == group_id: group = curGroup else: - if curGroup.name == name and (vpc_id is None or curGroup.vpc_id == vpc_id): + if groupName == name and (vpc_id is None or curGroup.vpc_id == vpc_id): group = curGroup # Ensure requested group is absent @@ -455,8 +557,9 @@ def main(): try: if not module.check_mode: group.delete() - except BotoServerError as e: - module.fail_json(msg="Unable to delete security group '%s' - %s" % (group, e.message), exception=traceback.format_exc()) + except botocore.exceptions.ClientError as e: + module.fail_json(msg="Unable to delete security group '%s' - %s" % (group, e), + exception=traceback.format_exc(), **camel_dict_to_snake_dict(e.response)) else: group = None changed = True @@ -469,40 +572,41 @@ def main(): if group: # existing group if group.description != description: - module.fail_json(msg="Group description does not match existing group. ec2_group does not support this case.") + module.fail_json( + msg="Group description does not match existing group. ec2_group does not support this case.") # if the group doesn't exist, create it now else: # no match found, create it if not module.check_mode: - group = ec2.create_security_group(name, description, vpc_id=vpc_id) - + group = client.create_security_group(GroupName=name, Description=description) + groupId = group.get('GroupId') # When a group is created, an egress_rule ALLOW ALL # to 0.0.0.0/0 is added automatically but it's not # reflected in the object returned by the AWS API # call. We re-read the group for getting an updated object # amazon sometimes takes a couple seconds to update the security group so wait till it exists - while len(ec2.get_all_security_groups(filters={'group_id': group.id})) == 0: + while len(client.describe_security_groups(GroupIds=[groupId]) + ['SecurityGroups'][0]['IpPermissionsEgress']) == 0: time.sleep(0.1) - group = ec2.get_all_security_groups(group_ids=(group.id,))[0] + group = ec2.SecurityGroup(groupId) changed = True else: module.fail_json(msg="Unsupported state requested: %s" % state) # create a lookup for all existing rules on the group if group: - # Manage ingress rules groupRules = {} - addRulesToLookup(group.rules, 'in', groupRules) - + add_rules_to_loopkup(group.ip_permissions, group.id, 'in', groupRules) # Now, go through all provided rules and ensure they are there. if rules is not None: + ip_permission = [] for rule in rules: validate_rule(module, rule) - - group_id, ip, target_group_created = get_target_from_rule(module, ec2, rule, name, group, groups, vpc_id) + group_id, ip, ipv6, target_group_created = get_target_from_rule(module, ec2, rule, name, + group, groups, vpc_id) if target_group_created: changed = True @@ -511,49 +615,63 @@ def main(): rule['from_port'] = None rule['to_port'] = None - # Convert ip to list we can iterate over - if not isinstance(ip, list): - ip = [ip] - - # If rule already exists, don't later delete it - for thisip in ip: - ruleId = make_rule_key('in', rule, group_id, thisip) - if ruleId not in groupRules: - grantGroup = None - if group_id: - grantGroup = groups[group_id] - - if not module.check_mode: - group.authorize(rule['proto'], rule['from_port'], rule['to_port'], thisip, grantGroup) - changed = True + if group_id: + rule_id = make_rule_key('in', rule, group.id, group_id) + if rule_id in groupRules: + del groupRules[rule_id] else: - del groupRules[ruleId] + if not module.check_mode: + ip_permission = serialize_group_grant(group_id, rule) + if ip_permission: + ips = ip_permission + if vpc_id: + [useridpair.update({'VpcId': vpc_id}) for useridpair in + ip_permission.get('UserIdGroupPairs')] + try: + client.authorize_security_group_ingress(GroupId=group.group_id, IpPermissions=[ips]) + except botocore.exceptions.ClientError as e: + module.fail_json( + msg="Unable to authorize ingress for group %s security group '%s' - %s" % + (group_id, group.group_name, e), + exception=traceback.format_exc(), **camel_dict_to_snake_dict(e.response)) + changed = True + elif ip: + # Convert ip to list we can iterate over + if ip and not isinstance(ip, list): + ip = [ip] + changed, ip_permission = authorize_ip("in", changed, client, group, groupRules, ip, ip_permission, + module, rule, "ipv4") + elif ipv6: + # Convert ip to list we can iterate over + if not isinstance(ipv6, list): + ipv6 = [ipv6] + # If rule already exists, don't later delete it + changed, ip_permission = authorize_ip("in", changed, client, group, groupRules, ipv6, ip_permission, + module, rule, "ipv6") # Finally, remove anything left in the groupRules -- these will be defunct rules if purge_rules: for (rule, grant) in groupRules.values(): - grantGroup = None - if grant.group_id: - if grant.owner_id != group.owner_id: - # this is a foreign Security Group. Since you can't fetch it you must create an instance of it - group_instance = SecurityGroup(owner_id=grant.owner_id, name=grant.name, id=grant.group_id) - groups[grant.group_id] = group_instance - groups[grant.name] = group_instance - grantGroup = groups[grant.group_id] + ip_permission = serialize_revoke(grant, rule) if not module.check_mode: - group.revoke(rule.ip_protocol, rule.from_port, rule.to_port, grant.cidr_ip, grantGroup) + try: + client.revoke_security_group_ingress(GroupId=group.group_id, IpPermissions=[ip_permission]) + except botocore.exceptions.ClientError as e: + module.fail_json( + msg="Unable to revoke ingress for security group '%s' - %s" % + (group.group_name, e), + exception=traceback.format_exc(), **camel_dict_to_snake_dict(e.response)) changed = True # Manage egress rules groupRules = {} - addRulesToLookup(group.rules_egress, 'out', groupRules) - + add_rules_to_loopkup(group.ip_permissions_egress, group.id, 'out', groupRules) # Now, go through all provided rules and ensure they are there. if rules_egress is not None: for rule in rules_egress: validate_rule(module, rule) - - group_id, ip, target_group_created = get_target_from_rule(module, ec2, rule, name, group, groups, vpc_id) + group_id, ip, ipv6, target_group_created = get_target_from_rule(module, ec2, rule, name, + group, groups, vpc_id) if target_group_created: changed = True @@ -562,45 +680,58 @@ def main(): rule['from_port'] = None rule['to_port'] = None - # Convert ip to list we can iterate over - if not isinstance(ip, list): - ip = [ip] - - # If rule already exists, don't later delete it - for thisip in ip: - ruleId = make_rule_key('out', rule, group_id, thisip) - if ruleId in groupRules: - del groupRules[ruleId] - # Otherwise, add new rule + if group_id: + rule_id = make_rule_key('out', rule, group.id, group_id) + if rule_id in groupRules: + del groupRules[rule_id] else: - grantGroup = None - if group_id: - grantGroup = groups[group_id].id - if not module.check_mode: - ec2.authorize_security_group_egress( - group_id=group.id, - ip_protocol=rule['proto'], - from_port=rule['from_port'], - to_port=rule['to_port'], - src_group_id=grantGroup, - cidr_ip=thisip) + ip_permission = serialize_group_grant(group_id, rule) + if ip_permission: + ips = ip_permission + if vpc_id: + [useridpair.update({'VpcId': vpc_id}) for useridpair in + ip_permission.get('UserIdGroupPairs')] + try: + client.authorize_security_group_egress(GroupId=group.group_id, IpPermissions=[ips]) + except botocore.exceptions.ClientError as e: + module.fail_json( + msg="Unable to authorize egress for group %s security group '%s' - %s" % + (group_id, group.group_name, e), + exception=traceback.format_exc(), **camel_dict_to_snake_dict(e.response)) changed = True + elif ip: + # Convert ip to list we can iterate over + if not isinstance(ip, list): + ip = [ip] + changed, ip_permission = authorize_ip("out", changed, client, group, groupRules, ip, + ip_permission, module, rule, "ipv4") + elif ipv6: + # Convert ip to list we can iterate over + if not isinstance(ipv6, list): + ipv6 = [ipv6] + # If rule already exists, don't later delete it + changed, ip_permission = authorize_ip("out", changed, client, group, groupRules, ipv6, + ip_permission, module, rule, "ipv6") else: # when no egress rules are specified, # we add in a default allow all out rule, which was the # default behavior before egress rules were added - default_egress_rule = 'out--1-None-None-None-0.0.0.0/0' + default_egress_rule = 'out--1-None-None-' + group.id + '-0.0.0.0/0' if default_egress_rule not in groupRules: if not module.check_mode: - ec2.authorize_security_group_egress( - group_id=group.id, - ip_protocol=-1, - from_port=None, - to_port=None, - src_group_id=None, - cidr_ip='0.0.0.0/0' - ) + ip_permission = [{'IpProtocol': '-1', + 'IpRanges': [{'CidrIp': '0.0.0.0/0'}] + } + ] + try: + client.authorize_security_group_egress(GroupId=group.group_id, IpPermissions=ip_permission) + except botocore.exceptions.ClientError as e: + module.fail_json(msg="Unable to authorize egress for ip %s security group '%s' - %s" % + ('0.0.0.0/0', + group.group_name, + e), + exception=traceback.format_exc(), **camel_dict_to_snake_dict(e.response)) changed = True else: # make sure the default egress rule is not removed @@ -609,24 +740,24 @@ def main(): # Finally, remove anything left in the groupRules -- these will be defunct rules if purge_rules_egress: for (rule, grant) in groupRules.values(): - grantGroup = None - if grant.group_id: - grantGroup = groups[grant.group_id].id - if not module.check_mode: - ec2.revoke_security_group_egress( - group_id=group.id, - ip_protocol=rule.ip_protocol, - from_port=rule.from_port, - to_port=rule.to_port, - src_group_id=grantGroup, - cidr_ip=grant.cidr_ip) - changed = True + # we shouldn't be revoking 0.0.0.0 egress + if grant != '0.0.0.0/0': + ip_permission = serialize_revoke(grant, rule) + if not module.check_mode: + try: + client.revoke_security_group_egress(GroupId=group.group_id, IpPermissions=[ip_permission]) + except botocore.exceptions.ClientError as e: + module.fail_json(msg="Unable to revoke egress for ip %s security group '%s' - %s" % + (grant, + group.group_name, + e), + exception=traceback.format_exc(), **camel_dict_to_snake_dict(e.response)) + changed = True if group: module.exit_json(changed=changed, group_id=group.id) else: module.exit_json(changed=changed, group_id=None) - if __name__ == '__main__': main() diff --git a/test/integration/targets/ec2_group/tasks/main.yml b/test/integration/targets/ec2_group/tasks/main.yml index 9a30d4fbe2..77a84450ac 100644 --- a/test/integration/targets/ec2_group/tasks/main.yml +++ b/test/integration/targets/ec2_group/tasks/main.yml @@ -63,21 +63,6 @@ - 'result.failed' - 'result.msg == "Must provide description when state is present."' - # ============================================================ - - name: test invalid region parameter - ec2_group: - name='{{ec2_group_name}}' - description='{{ec2_group_description}}' - region='asdf querty 1234' - register: result - ignore_errors: true - - - name: assert invalid region parameter - assert: - that: - - 'result.failed' - - 'result.msg.startswith("Region asdf querty 1234 does not seem to be available for aws module boto.ec2. If the region definitely exists, you may need to upgrade boto or extend with endpoints_path")' - # ============================================================ - name: test valid region parameter ec2_group: @@ -91,7 +76,7 @@ assert: that: - 'result.failed' - - 'result.msg.startswith("No handler was ready to authenticate.")' + - '"Unable to locate credentials" in result.msg' # ============================================================ - name: test environment variable EC2_REGION @@ -107,7 +92,7 @@ assert: that: - 'result.failed' - - 'result.msg.startswith("No handler was ready to authenticate.")' + - '"Unable to locate credentials" in result.msg' # ============================================================ - name: test invalid ec2_url parameter @@ -123,7 +108,7 @@ assert: that: - 'result.failed' - - 'result.msg.startswith("No handler was ready to authenticate.")' + - 'result.msg.startswith("The AWS region must be specified as an environment variable or in the AWS credentials profile")' # ============================================================ - name: test valid ec2_url parameter @@ -139,7 +124,7 @@ assert: that: - 'result.failed' - - 'result.msg.startswith("No handler was ready to authenticate.")' + - 'result.msg.startswith("The AWS region must be specified as an environment variable or in the AWS credentials profile")' # ============================================================ - name: test credentials from environment @@ -157,7 +142,7 @@ assert: that: - 'result.failed' - - '"Error in get_all_security_groups: AWS was not able to validate the provided access credentials" in result.msg' + - '"validate the provided access credentials" in result.msg' # ============================================================ - name: test credential parameters @@ -174,7 +159,7 @@ assert: that: - 'result.failed' - - '"Error in get_all_security_groups: AWS was not able to validate the provided access credentials" in result.msg' + - '"validate the provided access credentials" in result.msg' # ============================================================ - name: test state=absent @@ -243,6 +228,103 @@ - 'not result.changed' - 'result.group_id.startswith("sg-")' + # ============================================================ + - name: test state=present for ipv6 (expected changed=true) + ec2_group: + name: '{{ec2_group_name}}' + description: '{{ec2_group_description}}' + ec2_region: '{{ec2_region}}' + ec2_access_key: '{{ec2_access_key}}' + ec2_secret_key: '{{ec2_secret_key}}' + security_token: '{{security_token}}' + state: present + rules: + - proto: "tcp" + from_port: 8182 + to_port: 8182 + cidr_ipv6: "64:ff9b::/96" + register: result + + - name: assert state=present (expected changed=true) + assert: + that: + - 'result.changed' + - 'result.group_id.startswith("sg-")' + + # ============================================================ + - name: test rules_egress state=present for ipv6 (expected changed=true) + ec2_group: + name: '{{ec2_group_name}}' + description: '{{ec2_group_description}}' + ec2_region: '{{ec2_region}}' + ec2_access_key: '{{ec2_access_key}}' + ec2_secret_key: '{{ec2_secret_key}}' + security_token: '{{security_token}}' + state: present + rules: + - proto: "tcp" + from_port: 8182 + to_port: 8182 + cidr_ipv6: "64:ff9b::/96" + rules_egress: + - proto: "tcp" + from_port: 8181 + to_port: 8181 + cidr_ipv6: "64:ff9b::/96" + register: result + + - name: assert state=present (expected changed=true) + assert: + that: + - 'result.changed' + - 'result.group_id.startswith("sg-")' + + # ============================================================ + - name: test state=present for ipv4 (expected changed=true) + ec2_group: + name: '{{ec2_group_name}}' + description: '{{ec2_group_description}}' + ec2_region: '{{ec2_region}}' + ec2_access_key: '{{ec2_access_key}}' + ec2_secret_key: '{{ec2_secret_key}}' + security_token: '{{security_token}}' + state: present + rules: + - proto: "tcp" + from_port: 8182 + to_port: 8182 + cidr_ip: "1.1.1.1/32" + register: result + + - name: assert state=present (expected changed=true) + assert: + that: + - 'result.changed' + - 'result.group_id.startswith("sg-")' + + # ============================================================ + - name: add same rule to the existing group (expected changed=false) + ec2_group: + name: '{{ec2_group_name}}' + description: '{{ec2_group_description}}' + ec2_region: '{{ec2_region}}' + ec2_access_key: '{{ec2_access_key}}' + ec2_secret_key: '{{ec2_secret_key}}' + security_token: '{{security_token}}' + state: present + rules: + - proto: "tcp" + from_port: 8182 + to_port: 8182 + cidr_ip: "1.1.1.1/32" + register: result + + - name: assert state=present (expected changed=false) + assert: + that: + - 'not result.changed' + - 'result.group_id.startswith("sg-")' + # ============================================================ - name: test state=absent (expected changed=true) ec2_group: