Repository: ansible/ansible

Issue:
digital_ocean_tag reporting that tag on droplet changed, when it didn't.
<!---
Verify first that your issue/request is not already reported on GitHub.
Also test if the latest release, and master branch are affected too.
-->

##### ISSUE TYPE
<!--- Pick one below and delete the rest: -->
 - Bug Report

##### COMPONENT NAME
<!--- Name of the module/plugin/task/feature -->
digital_ocean_tag

##### ANSIBLE VERSION
<!--- Paste verbatim output from “ansible --version” between quotes below -->
```
ansible 2.3.0.0
  config file = /Users/brad/Projects/systems/vpn/ansible/ansible.cfg
  configured module search path = Default w/o overrides
  python version = 2.7.10 (default, Feb  6 2017, 23:53:20) [GCC 4.2.1 Compatible Apple LLVM 8.0.0 (clang-800.0.34)]
```

##### 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.
-->
MacOS

##### SUMMARY
<!--- Explain the problem briefly -->
When using digital_ocean_tag to create a tag, the task is marked "changed" even if the tag already exists.  When using digital_ocean_tag to apply a tag to a droplet, the task is marked "changed" even if the droplet already has that tag.  

##### 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.
-->
Simply run the playbook containing that task twice, and notice that it is "changed" the second time.

<!--- Paste example playbooks or commands between quotes below -->
```yaml
    - name: Tag the droplet
      digital_ocean_tag:
        name: "{{ do_tag }}"
        resource_id: "{{ do.droplet.id }}"
        api_token: "{{ do_token }}"
        state: present

```

<!--- You can also paste gist.github.com links for larger files -->

##### EXPECTED RESULTS
<!--- What did you expect to happen when running the steps above? -->
When the state is "present", and the tag is already present on the droplet, the task result should be "ok", not "changed".

##### ACTUAL RESULTS
<!--- What actually happened? If possible run with extra verbosity (-vvvv) -->
The task result is invariably "changed" (even when the droplet tag state did not change).




Discussion:
cc @kontrafiktion
[click here for bot help](https://github.com/ansible/ansibullbot/blob/master/ISSUE_HELP.md)
<!--- boilerplate: notify --->

Resolving patch:
diff --git a/lib/ansible/modules/cloud/digital_ocean/digital_ocean_tag.py b/lib/ansible/modules/cloud/digital_ocean/digital_ocean_tag.py
--- a/lib/ansible/modules/cloud/digital_ocean/digital_ocean_tag.py
+++ b/lib/ansible/modules/cloud/digital_ocean/digital_ocean_tag.py
@@ -39,7 +39,7 @@
     - The ID of the resource to operate on.
   resource_type:
     description:
-    - The type of resource to operate on. Currently only tagging of
+    - The type of resource to operate on. Currently, only tagging of
       droplets is supported.
     default: droplet
     choices: ['droplet']
@@ -153,7 +153,7 @@ def _url_builder(self, path):
             path = path[1:]
         return '%s/%s' % (self.baseurl, path)
 
-    def send(self, method, path, data=None, headers=None):
+    def send(self, method, path, data=None):
         url = self._url_builder(path)
         data = self.module.jsonify(data)
 
@@ -161,17 +161,17 @@ def send(self, method, path, data=None, headers=None):
 
         return Response(resp, info)
 
-    def get(self, path, data=None, headers=None):
-        return self.send('GET', path, data, headers)
+    def get(self, path, data=None):
+        return self.send('GET', path, data)
 
-    def put(self, path, data=None, headers=None):
-        return self.send('PUT', path, data, headers)
+    def put(self, path, data=None):
+        return self.send('PUT', path, data)
 
-    def post(self, path, data=None, headers=None):
-        return self.send('POST', path, data, headers)
+    def post(self, path, data=None):
+        return self.send('POST', path, data)
 
-    def delete(self, path, data=None, headers=None):
-        return self.send('DELETE', path, data, headers)
+    def delete(self, path, data=None):
+        return self.send('DELETE', path, data)
 
 
 def core(module):
@@ -186,57 +186,77 @@ def core(module):
     resource_id = module.params['resource_id']
     resource_type = module.params['resource_type']
 
-    rest = Rest(module, {'Authorization': 'Bearer {}'.format(api_token),
+    rest = Rest(module, {'Authorization': 'Bearer {0}'.format(api_token),
                          'Content-type': 'application/json'})
 
-    if state in ('present'):
-        if name is None:
-            module.fail_json(msg='parameter `name` is missing')
-
-        # Ensure Tag exists
-        response = rest.post("tags", data={'name': name})
+    # Check if api_token is valid or not
+    response = rest.get('account')
+    if response.status_code == 401:
+        module.fail_json(msg='Failed to login using api_token, please verify '
+                             'validity of api_token')
+    if state == 'present':
+        response = rest.get('tags/{0}'.format(name))
         status_code = response.status_code
-        json = response.json
-        if status_code == 201:
-            changed = True
-        elif status_code == 422:
+        resp_json = response.json
+        changed = False
+        if status_code == 200 and resp_json['tag']['name'] == name:
             changed = False
         else:
-            module.exit_json(changed=False, data=json)
+            # Ensure Tag exists
+            response = rest.post("tags", data={'name': name})
+            status_code = response.status_code
+            resp_json = response.json
+            if status_code == 201:
+                changed = True
+            elif status_code == 422:
+                changed = False
+            else:
+                module.exit_json(changed=False, data=resp_json)
 
         if resource_id is None:
             # No resource defined, we're done.
-            if json is None:
-                module.exit_json(changed=changed, data=json)
-            else:
-                module.exit_json(changed=changed, data=json)
+            module.exit_json(changed=changed, data=resp_json)
         else:
-            # Tag a resource
-            url = "tags/{}/resources".format(name)
-            payload = {
-                'resources': [{
-                    'resource_id': resource_id,
-                    'resource_type': resource_type}]}
-            response = rest.post(url, data=payload)
-            if response.status_code == 204:
-                module.exit_json(changed=True)
+            # Check if resource is already tagged or not
+            found = False
+            url = "{0}?tag_name={1}".format(resource_type, name)
+            response = rest.get(url)
+            status_code = response.status_code
+            resp_json = response.json
+            if status_code == 200:
+                for resource in resp_json['droplets']:
+                    if not found and resource['id'] == int(resource_id):
+                        found = True
+                        break
+                if not found:
+                    # If resource is not tagged, tag a resource
+                    url = "tags/{0}/resources".format(name)
+                    payload = {
+                        'resources': [{
+                            'resource_id': resource_id,
+                            'resource_type': resource_type}]}
+                    response = rest.post(url, data=payload)
+                    if response.status_code == 204:
+                        module.exit_json(changed=True)
+                    else:
+                        module.fail_json(msg="error tagging resource '{0}': {1}".format(resource_id, response.json["message"]))
+                else:
+                    # Already tagged resource
+                    module.exit_json(changed=False)
             else:
-                module.fail_json(msg="error tagging resource '{}': {}".format(
-                    resource_id, response.json["message"]))
-
-    elif state in ('absent'):
-        if name is None:
-            module.fail_json(msg='parameter `name` is missing')
+                # Unable to find resource specified by user
+                module.fail_json(msg=resp_json['message'])
 
+    elif state == 'absent':
         if resource_id:
-            url = "tags/{}/resources".format(name)
+            url = "tags/{0}/resources".format(name)
             payload = {
                 'resources': [{
                     'resource_id': resource_id,
                     'resource_type': resource_type}]}
             response = rest.delete(url, data=payload)
         else:
-            url = "tags/{}".format(name)
+            url = "tags/{0}".format(name)
             response = rest.delete(url)
         if response.status_code == 204:
             module.exit_json(changed=True)


Test patch: