Repository: ansible/ansible-modules-core

Issue:
digitalocean tag support
##### ISSUE TYPE
- Feature Idea
##### COMPONENT NAME

digital_ocean
##### ANSIBLE VERSION

```
ansible 2.1.0.0
  config file = /Users/vivo/workspaces/me/ansible-digitalocean/ansible.cfg
  configured module search path = ['/Users/vivo/workspaces/me/ansible-modules-core']
```
##### CONFIGURATION

N/A
##### OS / ENVIRONMENT

N/A
##### SUMMARY

DigitalOcean now supports tagging of Droplets.
Ansible should offer support for using tags

E.g. to ensure that a tag exists:

```
  digital_ocean: >
    state=present
    command=tag
    name="foo"
    api_token="{{ lookup('env','DO_API_TOKEN') }}"
```

I am already working on adding the support. Just wanted to create an issue to avoid duplication of effort



Discussion:
Thanks for creating the issue.  Please let me know if I can help.  Was also thinking of adding the recent block storage update to the api.

@atulhm I would love some help :-)
I have finished adding tag creation/deletion (as a new 'command' inside the digital_ocean module) and wanted to start on actions for tagged droplets.
But: after thinking about the current digital_ocean module I recognized that creating/deleting should be its own module (like the digita_locean_sshkey module). But the current digital_ocean module also allows to create sshkeys ...

So I wanted to start a discussion: how should the Ansible modules look like? 
1. Should we remove sshkey creation from the digital_ocean module? 
2. Could we at least deprecate that part of the digital_ocean module? 
3. Should it be possible to create a droplet and a tag with one Ansible command? (I started along this road, but do not like it)
4. which actions for tagged module should be supported and how should the 'interface' (as Ansible module and its parameters) look like?

@kontrafiktion great questions! 

 If we decide to tackle a reorganization of this, should we also consider disconnect or forking dopy?  I'm wondering if we can we reuse the core uri functionality so users don't have to install the dopy dependency and future improvements to the module aren't dependent on dopy upgrades.  

My overall thought is that we should expose as much of the api that is possible along the lines of what the api enables.  So if tags are allowed during droplet creation, I think we should allow that as well.

So to answer your questions:
1.  Should we remove sshkey creation from the digital_ocean module? 
-- I think so.  It seemed strange and confusing to use a "command mechanism" because the parameters could vary per command and it make documentation a pain.
2.  Could we at least deprecate that part of the digital_ocean module?
-- Probably could, but we should ask the community
3.  Should it be possible to create a droplet and a tag with one Ansible command? (I started along this road, but do not like it).  
-- Answered earlier, but I think so since the api allows it in a single call.
4.  Which actions for tagged module should be supported and how should the 'interface' (as Ansible module and its parameters) look like?
-- Good question.  I've been thinking about where do we draw the line between inventory/ansible calls and being able to do batch commands with tags.  Maybe it's a v2.5 thing?  

I'm not strong in py but not afraid of the challenge.  I do have an architecture in mind that I think would let us provide a quick and easily extendible module(s) for digital ocean and leverage the dictionary capabilities in python.  

Look forward to your thoughts/feedback

Hi,
I've got an implementation for **tag** support I'm using since months and will be happy to share.
However, it's using a dedicated module instead of a `command` module because I prefer separation of concerns. Also have an implementation for **floating IP** if interested too.

BTW I'm using `requests` library to do so.


Resolving patch:
diff --git a/cloud/digital_ocean/digital_ocean_tag.py b/cloud/digital_ocean/digital_ocean_tag.py
new file mode 100644
--- /dev/null
+++ b/cloud/digital_ocean/digital_ocean_tag.py
@@ -0,0 +1,256 @@
+#!/usr/bin/python
+# -*- coding: utf-8 -*-
+
+# This file is part of Ansible
+#
+# Ansible is free software: you can redistribute it and/or modify
+# it under the terms of the GNU General Public License as published by
+# the Free Software Foundation, either version 3 of the License, or
+# (at your option) any later version.
+#
+# Ansible is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+# GNU General Public License for more details.
+#
+# You should have received a copy of the GNU General Public License
+# along with Ansible.  If not, see <http://www.gnu.org/licenses/>.
+DOCUMENTATION = '''
+---
+module: digital_ocean_tag
+short_description: Create and remove tag(s) to DigitalOcean resource.
+description:
+    - Create and remove tag(s) to DigitalOcean resource.
+version_added: "2.2"
+options:
+  name:
+    description:
+     - The name of the tag. The supported characters for names include
+       alphanumeric characters, dashes, and underscores.
+  resource_id:
+    description:
+    - The ID of the resource to operate on.
+  resource_type:
+    description:
+    - The type of resource to operate on. Currently only tagging of
+      droplets is supported.
+    default: droplet
+    choices: ['droplet']
+  state:
+    description:
+     - Whether the tag should be present or absent on the resource.
+    default: present
+    choices: ['present', 'absent']
+  api_token:
+    description:
+     - DigitalOcean api token.
+
+notes:
+  - Two environment variables can be used, DO_API_KEY and DO_API_TOKEN.
+    They both refer to the v2 token.
+  - As of Ansible 2.0, Version 2 of the DigitalOcean API is used.
+
+requirements:
+  - "python >= 2.6"
+'''
+
+
+EXAMPLES = '''
+- name: create a tag
+  digital_ocean_tag:
+    name: production
+    state: present
+
+- name: tag a resource; creating the tag if it does not exists
+  digital_ocean_tag:
+    name: "{{ item }}"
+    resource_id: YYY
+    state: present
+  with_items:
+    - staging
+    - dbserver
+
+- name: untag a resource
+  digital_ocean_tag:
+    name: staging
+    resource_id: YYY
+    state: absent
+
+# Deleting a tag also untags all the resources that have previously been
+# tagged with it
+- name: remove a tag
+  digital_ocean_tag:
+    name: dbserver
+    state: absent
+'''
+
+
+RETURN = '''
+data:
+    description: a DigitalOcean Tag resource
+    returned: success and no resource constraint
+    type: dict
+    sample: {
+        "tag": {
+        "name": "awesome",
+        "resources": {
+          "droplets": {
+            "count": 0,
+            "last_tagged": null
+          }
+        }
+      }
+    }
+'''
+
+import json
+
+
+class Response(object):
+
+    def __init__(self, resp, info):
+        self.body = None
+        if resp:
+            self.body = resp.read()
+        self.info = info
+
+    @property
+    def json(self):
+        if not self.body:
+            if "body" in self.info:
+                return json.loads(self.info["body"])
+            return None
+        try:
+            return json.loads(self.body)
+        except ValueError as e:
+            return None
+
+    @property
+    def status_code(self):
+        return self.info["status"]
+
+
+class Rest(object):
+
+    def __init__(self, module, headers):
+        self.module = module
+        self.headers = headers
+        self.baseurl = 'https://api.digitalocean.com/v2'
+
+    def _url_builder(self, path):
+        if path[0] == '/':
+            path = path[1:]
+        return '%s/%s' % (self.baseurl, path)
+
+    def send(self, method, path, data=None, headers=None):
+        url = self._url_builder(path)
+        data = self.module.jsonify(data)
+
+        resp, info = fetch_url(self.module, url, data=data, headers=self.headers, method=method)
+
+        return Response(resp, info)
+
+    def get(self, path, data=None, headers=None):
+        return self.send('GET', path, data, headers)
+
+    def put(self, path, data=None, headers=None):
+        return self.send('PUT', path, data, headers)
+
+    def post(self, path, data=None, headers=None):
+        return self.send('POST', path, data, headers)
+
+    def delete(self, path, data=None, headers=None):
+        return self.send('DELETE', path, data, headers)
+
+
+def core(module):
+    try:
+        api_token = module.params['api_token'] or \
+            os.environ['DO_API_TOKEN'] or os.environ['DO_API_KEY']
+    except KeyError as e:
+        module.fail_json(msg='Unable to load %s' % e.message)
+
+    state = module.params['state']
+    name = module.params['name']
+    resource_id = module.params['resource_id']
+    resource_type = module.params['resource_type']
+
+    rest = Rest(module, {'Authorization': 'Bearer {}'.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})
+        status_code = response.status_code
+        json = response.json
+        if status_code == 201:
+            changed = True
+        elif status_code == 422:
+            changed = False
+        else:
+            module.exit_json(changed=False, data=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)
+        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)
+            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')
+
+        if resource_id:
+            url = "tags/{}/resources".format(name)
+            payload = {
+                'resources': [{
+                    'resource_id': resource_id,
+                    'resource_type': resource_type}]}
+            response = rest.delete(url, data=payload)
+        else:
+            url = "tags/{}".format(name)
+            response = rest.delete(url)
+        if response.status_code == 204:
+            module.exit_json(changed=True)
+        else:
+            module.exit_json(changed=False, data=response.json)
+
+
+def main():
+    module = AnsibleModule(
+        argument_spec=dict(
+            name=dict(type='str', required=True),
+            resource_id=dict(aliases=['droplet_id'], type='int'),
+            resource_type=dict(choices=['droplet'], default='droplet'),
+            state=dict(choices=['present', 'absent'], default='present'),
+            api_token=dict(aliases=['API_TOKEN'], no_log=True),
+        )
+    )
+
+    try:
+        core(module)
+    except Exception as e:
+        module.fail_json(msg=str(e))
+
+# import module snippets
+from ansible.module_utils.basic import *  # noqa
+from ansible.module_utils.urls import *
+if __name__ == '__main__':
+    main()


Test patch: