Repository: ansible/ansible

Issue:
Extend the ETCD Lookup to get all variables from a folder
##### ISSUE TYPE
- Feature Idea

##### COMPONENT NAME
Plugin ETCD Lookup

##### ANSIBLE VERSION
All

##### CONFIGURATION
N/A

##### OS / ENVIRONMENT
OS: Linux

##### SUMMARY
The feature basically is calling to get a ETCD key/value, when you make an API call to request a folder, the response must be all the keys-values inside of it.

##### STEPS TO REPRODUCE

```yaml
- name: 'ETCD Variables'
  hosts: 127.0.0.1
  connection: local
  remote_user: root
  vars:
    - REGION: 'SAMPLE'
    - RELEASE: 'liberty'
  pre_tasks:
    ## This step will fullfil the vars to be used on Ansible Roles/Playbooks
    - name: 'Getting Variables from ETCD Folder'
      set_fact:
        VAR_MAP: "{{ lookup('etcd2', '/{{ REGION }}/{{ RELEASE }}') }}"

    - debug:
        var: VAR_MAP
```

Output:
```
PLAY [ETCD Variables] *********************************************************************************************************************************************

TASK [Gathering Facts] ***********************************************************************************************************************************************************************
ok: [localhost]

TASK [Getting Variables from ETCD Folder] ****************************************************************************************************************************************************
ok: [localhost]

TASK [debug] *********************************************************************************************************************************************************************************
ok: [localhost] => {
    "changed": false,
    "msg": {
        ...
        ...
        ...
        "CEILOMETER_API_PORT": "8777",
        "CEILOMETER_LOG": "LOG_LOCAL6",
        "CEILOMETER_TTL": "7776000",
        "CEILOMETER_USER": "ceilometer",
        "CEPH_VERSION": "0.80.8",
        "CINDER_BCK_DRIVER": "cinder.backup.drivers.ceph",
        "CINDER_CEPH_BCK_USER": "backup",
        "CINDER_CEPH_CHUNK": "134217728",
        "CINDER_DB": "cinder",
        "CINDER_DEF_VOL": "standard",
        "CINDER_ENABLE_BACKEND": "standard,ssd",
        ...
        ...
      }
}

TASK [00_entry_validation : Creating controller files] ***************************************************************************************************************************************
changed: [localhost] => (item={'key': u'ceilometer', 'value': {u'origin': u'/etc/ceilometer/ceilometer.conf', u'tmp': u'/home/test/ceilometer.conf', u'template': u'ceilometer-controller.conf.j2'}})

PLAY RECAP ***********************************************************************************************************************************************************************************
localhost                  : ok=4    changed=1    unreachable=0    failed=0   
```

##### EXPECTED RESULTS
Inside of fact {{ VAR_MAP }} you will find a dictionary with all variable mapping from an ETCD folder 

##### ACTUAL RESULTS
Actually  you get a empty variable



Discussion:
Actually I have the code to make it works on another repository, let me some time and I will perform a PR with it ;)

Resolving patch:
diff --git a/lib/ansible/plugins/lookup/etcd.py b/lib/ansible/plugins/lookup/etcd.py
--- a/lib/ansible/plugins/lookup/etcd.py
+++ b/lib/ansible/plugins/lookup/etcd.py
@@ -1,4 +1,5 @@
 # (c) 2013, Jan-Piet Mens <jpmens(at)gmail.com>
+# (m) 2017, Juan Manuel Parrilla <jparrill@redhat.com>
 #
 # This file is part of Ansible
 #
@@ -44,6 +45,9 @@
                 - name: ANSIBLE_ETCD_VERSION
 EXAMPLES:
     - name: "a value from a locally running etcd"
+      debug: msg={{ lookup('etcd', 'foo/bar') }}
+
+    - name: "a values from a folder on a locally running etcd"
       debug: msg={{ lookup('etcd', 'foo') }}
 RETURN:
     _list:
@@ -51,7 +55,6 @@
             - list of values associated with input keys
         type: strings
 '''
-
 from __future__ import (absolute_import, division, print_function)
 __metaclass__ = type
 
@@ -82,28 +85,47 @@ def __init__(self, url=ANSIBLE_ETCD_URL, version=ANSIBLE_ETCD_VERSION,
         self.baseurl = '%s/%s/keys' % (self.url,self.version)
         self.validate_certs = validate_certs
 
-    def get(self, key):
-        url = "%s/%s" % (self.baseurl, key)
+    def _parse_node(self, node):
+        # This function will receive all etcd tree,
+        # if the level requested has any node, the recursion starts
+        # create a list in the dir variable and it is passed to the
+        # recursive function, and so on, if we get a variable,
+        # the function will create a key-value at this level and
+        # undoing the loop.
+        path = {}
+        if node.get('dir', False):
+            for n in node.get('nodes', []):
+                path[n['key'].split('/')[-1]] = self._parse_node(n)
 
+        else:
+            path = node['value']
+
+        return path
+
+    def get(self, key):
+        url = "%s/%s?recursive=true" % (self.baseurl, key)
         data = None
-        value = ""
+        value = {}
         try:
             r = open_url(url, validate_certs=self.validate_certs)
             data = r.read()
         except:
-            return value
+            return None
 
         try:
-            # {"action":"get","key":"/name","value":"Jane Jolie","index":5}
+            # I will not support Version 1 of etcd for folder parsing
             item = json.loads(data)
             if self.version == 'v1':
+                # When ETCD are working with just v1
                 if 'value' in item:
                     value = item['value']
             else:
                 if 'node' in item:
-                    value = item['node']['value']
+                    # When a usual result from ETCD
+                    value = self._parse_node(item['node'])
 
             if 'errorCode' in item:
+                # Here return an error when an unknown entry responds
                 value = "ENOENT"
         except:
             raise


Test patch: