Repository: ansible/ansible-modules-core

Issue:
gce_net dont update existing firewall rule
#### Issue type

Bug Report
#### Ansible Version

ansible 1.9.3
#### Environnement

 Ubuntu 15.04
#### Summary

Update firewall rule succeed without doing the job
#### Steps to reproduce

$ ansible localhost -i inventory, -m gce_net -a "name=mynet fwname=testrule allowed=tcp:22"

``` sh
 localhost | success >> {
    "allowed": "tcp:22",
    "changed": true,
    "fwname": "testrule",
    "ipv4_range": "172.17.0.0/24",
    "name": "mynet",
    "src_range": null,
    "src_tags": null,
    "state": "present",
    "target_tags": null
 }
```

$ ansible localhost -i inventory, -m gce_net -a "name=mynet fwname=testrule allowed=tcp:25"

``` sh
 localhost | success >> {
    "allowed": "tcp:25",
    "changed": false,
    "fwname": "testrule",
    "ipv4_range": "172.17.0.0/24",
    "name": "mynet",
    "src_range": null,
    "src_tags": null,
    "state": "present",
    "target_tags": null
 }
```
#### Expected result

The rule is not allowing tcp:22 port but tcp:25 (or the task failled)
#### Actual result

 $ gcloud compute firewall-rules describe testrule

``` sh
allowed:
- IPProtocol: tcp
  ports:
 - '22'
creationTimestamp: '2015-09-18T14:54:07.154-07:00'
description: ''
id: '5595550375581137168'
kind: compute#firewall
name: testrule
network: mynet
sourceRanges:
- 0.0.0.0/0
```



Discussion:
cc @erjohnso


Resolving patch:
diff --git a/cloud/google/gce_net.py b/cloud/google/gce_net.py
--- a/cloud/google/gce_net.py
+++ b/cloud/google/gce_net.py
@@ -34,6 +34,7 @@
   allowed:
     description:
       - the protocol:ports to allow ('tcp:80' or 'tcp:80,443' or 'tcp:80-800;udp:1-25')
+        this parameter is mandatory when creating or updating a firewall rule
     required: false
     default: null
     aliases: []
@@ -173,6 +174,14 @@ def format_allowed(allowed):
             return_value.append(format_allowed_section(section))
     return return_value
 
+def sorted_allowed_list(allowed_list):
+    """Sort allowed_list (output of format_allowed) by protocol and port."""
+    # sort by protocol
+    allowed_by_protocol = sorted(allowed_list,key=lambda x: x['IPProtocol'])
+    # sort the ports list
+    return sorted(allowed_by_protocol, key=lambda y: y['ports'].sort())
+
+
 def main():
     module = AnsibleModule(
         argument_spec = dict(
@@ -246,12 +255,65 @@ def main():
 
             allowed_list = format_allowed(allowed)
 
+            # Fetch existing rule and if it exists, compare attributes
+            # update if attributes changed.  Create if doesn't exist.
             try:
-                gce.ex_create_firewall(fwname, allowed_list, network=name,
+                fw_changed = False
+                fw = gce.ex_get_firewall(fwname)
+
+                # If old and new attributes are different, we update the firewall rule.
+                # This implicitly let's us clear out attributes as well.
+                # allowed_list is required and must not be None for firewall rules.
+                if allowed_list and (sorted_allowed_list(allowed_list) != sorted_allowed_list(fw.allowed)):
+                    fw.allowed = allowed_list
+                    fw_changed = True
+
+                # If these attributes are lists, we sort them first, then compare.
+                # Otherwise, we update if they differ.
+                if fw.source_ranges != src_range:
+                    if isinstance(src_range, list):
+                        if sorted(fw.source_ranges) != sorted(src_range):
+                            fw.source_ranges = src_range
+                            fw_changed = True
+                    else:
+                        fw.source_ranges = src_range
+                        fw_changed = True
+
+                if fw.source_tags != src_tags:
+                    if isinstance(src_range, list):
+                        if sorted(fw.source_tags) != sorted(src_tags):
+                            fw.source_tags = src_tags
+                            fw_changed = True
+                    else:
+                        fw.source_tags = src_tags
+                        fw_changed = True
+
+                if fw.target_tags != target_tags:
+                    if isinstance(target_tags, list):
+                        if sorted(fw.target_tags) != sorted(target_tags):
+                            fw.target_tags = target_tags
+                            fw_changed = True
+                    else:
+                        fw.target_tags = target_tags
+                        fw_changed = True
+
+                if fw_changed is True:
+                    try:
+                        gce.ex_update_firewall(fw)
+                        changed = True
+                    except Exception as e:
+                        module.fail_json(msg=unexpected_error_msg(e), changed=False)
+
+            # Firewall rule not found so we try to create it.
+            except ResourceNotFoundError:
+                try:
+                    gce.ex_create_firewall(fwname, allowed_list, network=name,
                         source_ranges=src_range, source_tags=src_tags, target_tags=target_tags)
-                changed = True
-            except ResourceExistsError:
-                pass
+                    changed = True
+
+                except Exception as e:
+                    module.fail_json(msg=unexpected_error_msg(e), changed=False)
+
             except Exception as e:
                 module.fail_json(msg=unexpected_error_msg(e), changed=False)
 
@@ -279,17 +341,13 @@ def main():
             network = None
             try:
                 network = gce.ex_get_network(name)
-#                json_output['d1'] = 'found network name %s' % name
+
             except ResourceNotFoundError:
-#                json_output['d2'] = 'not found network name %s' % name
                 pass
             except Exception as e:
-#                json_output['d3'] = 'error with %s' % name
                 module.fail_json(msg=unexpected_error_msg(e), changed=False)
             if network:
-#                json_output['d4'] = 'deleting %s' % name
                 gce.ex_destroy_network(network)
-#                json_output['d5'] = 'deleted %s' % name
                 changed = True
 
     json_output['changed'] = changed


Test patch: