1# -*- coding: utf-8 -*-
2# (c) 2015, Steve Gargan <steve.gargan@gmail.com>
3# (c) 2017 Ansible Project
4# GNU General Public License v3.0+ (see COPYING or https://www.gnu.org/licenses/gpl-3.0.txt)
5from __future__ import (absolute_import, division, print_function)
6
7__metaclass__ = type
8
9DOCUMENTATION = '''
10    author: Unknown (!UNKNOWN)
11    name: consul_kv
12    short_description: Fetch metadata from a Consul key value store.
13    description:
14      - Lookup metadata for a playbook from the key value store in a Consul cluster.
15        Values can be easily set in the kv store with simple rest commands
16      - C(curl -X PUT -d 'some-value' http://localhost:8500/v1/kv/ansible/somedata)
17    requirements:
18      - 'python-consul python library U(https://python-consul.readthedocs.io/en/latest/#installation)'
19    options:
20      _raw:
21        description: List of key(s) to retrieve.
22        type: list
23      recurse:
24        type: boolean
25        description: If true, will retrieve all the values that have the given key as prefix.
26        default: False
27      index:
28        description:
29          - If the key has a value with the specified index then this is returned allowing access to historical values.
30      datacenter:
31        description:
32          - Retrieve the key from a consul datacenter other than the default for the consul host.
33      token:
34        description: The acl token to allow access to restricted values.
35      host:
36        default: localhost
37        description:
38          - The target to connect to, must be a resolvable address.
39            Will be determined from C(ANSIBLE_CONSUL_URL) if that is set.
40          - "C(ANSIBLE_CONSUL_URL) should look like this: C(https://my.consul.server:8500)"
41        env:
42          - name: ANSIBLE_CONSUL_URL
43        ini:
44          - section: lookup_consul
45            key: host
46      port:
47        description:
48          - The port of the target host to connect to.
49          - If you use C(ANSIBLE_CONSUL_URL) this value will be used from there.
50        default: 8500
51      scheme:
52        default: http
53        description:
54          - Whether to use http or https.
55          - If you use C(ANSIBLE_CONSUL_URL) this value will be used from there.
56      validate_certs:
57        default: True
58        description: Whether to verify the ssl connection or not.
59        env:
60          - name: ANSIBLE_CONSUL_VALIDATE_CERTS
61        ini:
62          - section: lookup_consul
63            key: validate_certs
64      client_cert:
65        description: The client cert to verify the ssl connection.
66        env:
67          - name: ANSIBLE_CONSUL_CLIENT_CERT
68        ini:
69          - section: lookup_consul
70            key: client_cert
71      url:
72        description: "The target to connect to, should look like this: C(https://my.consul.server:8500)."
73        type: str
74        version_added: 1.0.0
75        env:
76          - name: ANSIBLE_CONSUL_URL
77        ini:
78          - section: lookup_consul
79            key: url
80'''
81
82EXAMPLES = """
83  - ansible.builtin.debug:
84      msg: 'key contains {{item}}'
85    with_community.general.consul_kv:
86      - 'key/to/retrieve'
87
88  - name: Parameters can be provided after the key be more specific about what to retrieve
89    ansible.builtin.debug:
90      msg: 'key contains {{item}}'
91    with_community.general.consul_kv:
92      - 'key/to recurse=true token=E6C060A9-26FB-407A-B83E-12DDAFCB4D98'
93
94  - name: retrieving a KV from a remote cluster on non default port
95    ansible.builtin.debug:
96      msg: "{{ lookup('community.general.consul_kv', 'my/key', host='10.10.10.10', port='2000') }}"
97"""
98
99RETURN = """
100  _raw:
101    description:
102      - Value(s) stored in consul.
103    type: dict
104"""
105
106import os
107from ansible.module_utils.six.moves.urllib.parse import urlparse
108from ansible.errors import AnsibleError, AnsibleAssertionError
109from ansible.plugins.lookup import LookupBase
110from ansible.module_utils.common.text.converters import to_text
111
112try:
113    import consul
114
115    HAS_CONSUL = True
116except ImportError as e:
117    HAS_CONSUL = False
118
119
120class LookupModule(LookupBase):
121
122    def run(self, terms, variables=None, **kwargs):
123
124        if not HAS_CONSUL:
125            raise AnsibleError(
126                'python-consul is required for consul_kv lookup. see http://python-consul.readthedocs.org/en/latest/#installation')
127
128        # get options
129        self.set_options(direct=kwargs)
130
131        scheme = self.get_option('scheme')
132        host = self.get_option('host')
133        port = self.get_option('port')
134        url = self.get_option('url')
135        if url is not None:
136            u = urlparse(url)
137            if u.scheme:
138                scheme = u.scheme
139            host = u.hostname
140            if u.port is not None:
141                port = u.port
142
143        validate_certs = self.get_option('validate_certs')
144        client_cert = self.get_option('client_cert')
145
146        values = []
147        try:
148            for term in terms:
149                params = self.parse_params(term)
150                consul_api = consul.Consul(host=host, port=port, scheme=scheme, verify=validate_certs, cert=client_cert)
151
152                results = consul_api.kv.get(params['key'],
153                                            token=params['token'],
154                                            index=params['index'],
155                                            recurse=params['recurse'],
156                                            dc=params['datacenter'])
157                if results[1]:
158                    # responds with a single or list of result maps
159                    if isinstance(results[1], list):
160                        for r in results[1]:
161                            values.append(to_text(r['Value']))
162                    else:
163                        values.append(to_text(results[1]['Value']))
164        except Exception as e:
165            raise AnsibleError(
166                "Error locating '%s' in kv store. Error was %s" % (term, e))
167
168        return values
169
170    def parse_params(self, term):
171        params = term.split(' ')
172
173        paramvals = {
174            'key': params[0],
175            'token': self.get_option('token'),
176            'recurse': self.get_option('recurse'),
177            'index': self.get_option('index'),
178            'datacenter': self.get_option('datacenter')
179        }
180
181        # parameters specified?
182        try:
183            for param in params[1:]:
184                if param and len(param) > 0:
185                    name, value = param.split('=')
186                    if name not in paramvals:
187                        raise AnsibleAssertionError("%s not a valid consul lookup parameter" % name)
188                    paramvals[name] = value
189        except (ValueError, AssertionError) as e:
190            raise AnsibleError(e)
191
192        return paramvals
193