Repository: ansible/ansible

Issue:
ec2_group IPv6 rule fails with error CIDR is malformed
##### ISSUE TYPE
<!--- Pick one below and delete the rest: -->
 - Feature Idea

##### COMPONENT NAME
Module ec2_group

##### ANSIBLE VERSION
<!--- Paste verbatim output from “ansible --version” between quotes below -->
```
ansible 2.2.2.0
  config file = /vagrant/ansible.cfg
  configured module search path = Default w/o overrides
```

##### CONFIGURATION
<!---
Mention any settings you have changed/added/removed in ansible.cfg
(or using the ANSIBLE_* environment variables).
-->

##### OS / ENVIRONMENT
<!---
Mention the OS you are running Ansible from, and the OS you are
managing, or say “N/A” for anything that is not platform-specific.
-->
Ubuntu 14.04

##### SUMMARY
<!--- Explain the problem briefly -->
I am trying to add security group rule in aws. I can successfully add ipv4 cidr , but , fails when it's ipv6.

I believe it's not yet supported , but wanted to confirm for sure. 

failed: [127.0.0.1] (item={u'direction': u'ingress', u'protocol': u'tcp', u'remote_ip_prefix': u'*:*:*::/40', u'port_range_max': u'*', u'port_range_min': u'*', u'ethertype': u'IPv4'}) => {
    "failed": true, 
    "invocation": {
        "module_name": "ec2_group"
    }, 
    "item": {
        "direction": "ingress", 
        "ethertype": "IPv6", 
        "port_range_max": "*", 
        "port_range_min": "*", 
        "protocol": "tcp", 
        "remote_ip_prefix": "*:*:*::/40"
    }, 
    
Error - CIDR block *:*:*::/40 is malformed

##### STEPS TO REPRODUCE
<!---
For bugs, show exactly how to reproduce the problem, using a minimal test-case.
For new features, show how the feature would be used.
-->

<!--- Paste example playbooks or commands between quotes below -->
```yaml

```

<!--- You can also paste gist.github.com links for larger files -->

##### EXPECTED RESULTS
<!--- What did you expect to happen when running the steps above? -->

##### ACTUAL RESULTS
<!--- What actually happened? If possible run with extra verbosity (-vvvv) -->

<!--- Paste verbatim command output between quotes below -->
```

```



Discussion:
Hi, thanks for opening an issue for this. ec2_group doesn't have a remote_ip_prefix option. Are you trying to use the [os_security_group_rule module](http://docs.ansible.com/ansible/os_security_group_rule_module.html)? Can you share your task, please?
Here is the task.
```
# file: roles/ec2_security_group/tasks/main.yml
- name: add amazon ec2 credentials
  include_vars: ec2_credentials.vault
  no_log: true

- name: Create security group
  ec2_group:
    name: "{{ secgroup.name }}"
    description: "{{ secgroup.description }}"
    vpc_id: "{{ servers.vpc_id }}"
    region: "{{ region_id }}"
    aws_secret_key: "{{ aws_secret_key }}"
    aws_access_key: "{{ aws_access_key }}"
    rules:
      - proto: "{{ item.protocol }}"
        from_port: "{{ item.port_range_min }}"
        to_port: "{{ item.port_range_max }}"
        cidr_ip: "{{ item.remote_ip_prefix }}"
    purge_rules: false
  with_items: "{{ secgroup.rules }}"
  when: secgroup.rules is defined

- name: Programmatically create group rules
  ec2_group:
    name: "{{ secgroup.name }}"
    description: "{{ secgroup.description }}"
    vpc_id: "{{ servers.vpc_id }}"
    region: "{{ region_id }}"
    aws_secret_key: "{{ aws_secret_key }}"
    aws_access_key: "{{ aws_access_key }}"
    rules:
      - proto: "{{ secgroup.protocol }}"
        from_port: '{{ item[0].min }}'
        to_port: '{{ item[0].max }}'
        cidr_ip: '{{ item[1] }}'
  with_nested:
    - '{{ secgroup.incoming_ports }}'
    - '{{ secgroup.incoming_ips }}'
  when: secgroup.incoming_ports is defined
```
Thanks! I will look at this. I think this will be a feature request. At a cursory glance, it appears cidr_ip is expecting an explicitly stated ip or a list of ips. I don't think ip prefix is supported. By the way, when sharing yaml if you use three ticks (`) above and below your tasks it will format blocks of things nicely for you! :) Like this:
```
- name: add amazon ec2 credentials
  include_vars: ec2_credentials.vault
  no_log: true
```
Updating this to reflect a feature idea rather than a bug.
ansible currently uses boto and seeing their code , boto actually handles only ipv4 ranges
Does ansible work with boto3? 
```
if cidr_ip:
            if not isinstance(cidr_ip, list):
                cidr_ip = [cidr_ip]
            for i, single_cidr_ip in enumerate(cidr_ip):
                params['IpPermissions.1.IpRanges.%d.CidrIp' % (i + 1)] = \
                    single_cidr_ip
        if dry_run:
            params['DryRun'] = 'true'

        return self.get_status('AuthorizeSecurityGroupIngress',
                               params, verb='POST')


Some Ansible modules use boto and others use boto3 (the requirements will be in the module documentation). New AWS modules use boto3. The goal is to port modules to boto3. There isn't currently a PR open for ec2_group to work with boto3 (but they are definitely welcome).
Sure. Haven't contributed before , I can definitely work on this.
Thanks @s-hertel 
@sramakr Cool, that would be great. Feel free to ping me on the PR if you make one and I'll review.
@s-hertel I have PR ready , and validate module works good my change , but , before I create a PR I want to check if there any existing ec2 unit tests available. Having a hard time mocking boto3. appreciate any help. 
Awesome! We don't have ec2_group unit tests yet but there are some integration tests for it. But if you're hoping to write some unit tests, using placebo_fixtures might be better than mocking it (if you haven't used it before it basically allows you to run the tests on your cloud resources and record it and then on subsequent runs you can just use the recordings that were created rather than use cloud resources).

Resolving patch:
diff --git a/lib/ansible/modules/cloud/amazon/ec2_group.py b/lib/ansible/modules/cloud/amazon/ec2_group.py
--- a/lib/ansible/modules/cloud/amazon/ec2_group.py
+++ b/lib/ansible/modules/cloud/amazon/ec2_group.py
@@ -19,12 +19,12 @@
                     '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 @@
         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 @@
         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 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())
-
-    for curGroup in security_groups:
-        groups[curGroup.id] = curGroup
-        if curGroup.name in groups:
+        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 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 group_id:
+                    rule_id = make_rule_key('in', rule, group.id, group_id)
+                    if rule_id in groupRules:
+                        del groupRules[rule_id]
+                    else:
                         if not module.check_mode:
-                            group.authorize(rule['proto'], rule['from_port'], rule['to_port'], thisip, grantGroup)
+                            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
-                    else:
-                        del groupRules[ruleId]
-
+                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()


Test patch:
diff --git a/hacking/aws_config/testing_policies/ec2-policy.json b/hacking/aws_config/testing_policies/ec2-policy.json
--- 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/test/integration/targets/ec2_group/tasks/main.yml b/test/integration/targets/ec2_group/tasks/main.yml
--- 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: