Repository: ansible/ansible-modules-core

Issue:
ec2_group module failures with ICMP protocol
##### Issue Type:

Bug Report
##### Ansible Version:

1.8.2
##### Environment:

localhost / OSX / *
##### Summary:

The `ec2_group` module fails on ICMP protocol operations that should normally be unchanged. That is, the first time the module is called to create and/or change a group, the operation succeeds.  The second and subsequent times the module is called, where the operation should normally be idempotent, the module instead fails.
##### Steps To Reproduce:

Call the `ec2_group` module twice in a row where one of the `rules` **or** `egress_rules` has `proto=ICMP`.
##### Expected Results:

According to the [boto documentation](http://boto.readthedocs.org/en/latest/security_groups.html), the correct way to allow all ICMP types is to use the range -1:-1.  I would expect the following task to idempotently allow the instance to respond to all ICMP types

```
- ec2_group:
    name: IcmpAllInboundSG
    description: respond to all inbound ICMP traffic
    region: us-west-2
    rules:
      - proto: ICMP
        from_port: -1
        to_port: -1
        cidr_ip: 0.0.0.0/0
```

Likewise, I would expect the following task to idempotently allow the instance to respond only to traceroute requests

```
- ec2_group:
    name: EX2
    description: respond to all inbound traceroute requests
    region: us-west-2
    rules:
      - proto: ICMP
        from_port: 30
        to_port: 30
        cidr_ip: 0.0.0.0/0
```

And likewise again, only to ping requests

```
- ec2_group:
    name: EX3
    description: respond to all inbound ping requests
    region: us-west-2
    rules:
      - proto: ICMP
        from_port: 0
        to_port: 0
        cidr_ip: 0.0.0.0/0
```
##### Actual Results:

In the above three examples, the first time the operation is ran (and thus the results "changed"), the operation succeeds.  In subsequent playbook runs, the operation fails with the following output.

```
TASK: [security group to allow ping] ******************************************
failed: [127.0.0.1] => {"failed": true, "parsed": false}
Traceback (most recent call last):
  File "/Users/kfranz/.ansible/tmp/ansible-tmp-1418809244.68-137009819118878/ec2_group", line 2145, in <module>
    main()
  File "/Users/kfranz/.ansible/tmp/ansible-tmp-1418809244.68-137009819118878/ec2_group", line 293, in main
    group.authorize(rule['proto'], rule['from_port'], rule['to_port'], ip, grantGroup)
  File "/Library/Python/2.7/site-packages/boto-2.34.0-py2.7.egg/boto/ec2/securitygroup.py", line 203, in authorize
    dry_run=dry_run)
  File "/Library/Python/2.7/site-packages/boto-2.34.0-py2.7.egg/boto/ec2/connection.py", line 3192, in authorize_security_group
    params, verb='POST')
  File "/Library/Python/2.7/site-packages/boto-2.34.0-py2.7.egg/boto/connection.py", line 1223, in get_status
    raise self.ResponseError(response.status, response.reason, body)
boto.exception.EC2ResponseError: EC2ResponseError: 400 Bad Request
<?xml version="1.0" encoding="UTF-8"?>
<Response><Errors><Error><Code>InvalidPermission.Duplicate</Code><Message>the specified rule "peer: 0.0.0.0/0, ICMP, type: 30, code: 30, ALLOW" already exists</Message></Error></Errors><RequestID>0e8274ad-7884-4050-adca-5c742fe0c697</RequestID></Response>


FATAL: all hosts have already failed -- aborting
```



Discussion:

Resolving patch:
diff --git a/cloud/amazon/ec2_group.py b/cloud/amazon/ec2_group.py
--- a/cloud/amazon/ec2_group.py
+++ b/cloud/amazon/ec2_group.py
@@ -114,11 +114,21 @@
     sys.exit(1)
 
 
+def make_rule_key(prefix, rule, group_id, cidr_ip):
+    """Creates a unique key for an individual group rule"""
+    if isinstance(rule, dict):
+        proto, from_port, to_port = [rule.get(x, None) for x in ('proto', 'from_port', 'to_port')]
+    else:  # isinstance boto.ec2.securitygroup.IPPermissions
+        proto, from_port, to_port = [getattr(rule, x, None) for x in ('ip_protocol', 'from_port', 'to_port')]
+
+    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, dict):
     for rule in rules:
         for grant in rule.grants:
-            dict["%s-%s-%s-%s-%s-%s" % (prefix, rule.ip_protocol, rule.from_port, rule.to_port,
-                                        grant.group_id, grant.cidr_ip)] = rule
+            dict[make_rule_key(prefix, rule, grant.group_id, grant.cidr_ip)] = rule
 
 
 def get_target_from_rule(module, ec2, rule, name, group, groups, vpc_id):
@@ -279,7 +289,7 @@ def main():
                     rule['to_port'] = None
 
                 # If rule already exists, don't later delete it
-                ruleId = "%s-%s-%s-%s-%s-%s" % ('in', rule['proto'], rule['from_port'], rule['to_port'], group_id, ip)
+                ruleId = make_rule_key('in', rule, group_id, ip)
                 if ruleId in groupRules:
                     del groupRules[ruleId]
                 # Otherwise, add new rule
@@ -320,7 +330,7 @@ def main():
                     rule['to_port'] = None
 
                 # If rule already exists, don't later delete it
-                ruleId = "%s-%s-%s-%s-%s-%s" % ('out', rule['proto'], rule['from_port'], rule['to_port'], group_id, ip)
+                ruleId = make_rule_key('out', rule, group_id, ip)
                 if ruleId in groupRules:
                     del groupRules[ruleId]
                 # Otherwise, add new rule


Test patch: