1#!/usr/local/bin/python3.8
2# -*- coding: utf-8 -*-
3#
4# (c) 2015, Steve Gargan <steve.gargan@gmail.com>
5# (c) 2018 Genome Research Ltd.
6# GNU General Public License v3.0+ (see COPYING or https://www.gnu.org/licenses/gpl-3.0.txt)
7
8from __future__ import absolute_import, division, print_function
9__metaclass__ = type
10
11
12DOCUMENTATION = '''
13module: consul_kv
14short_description: Manipulate entries in the key/value store of a consul cluster
15description:
16  - Allows the retrieval, addition, modification and deletion of key/value entries in a
17    consul cluster via the agent. The entire contents of the record, including
18    the indices, flags and session are returned as C(value).
19  - If the C(key) represents a prefix then note that when a value is removed, the existing
20    value if any is returned as part of the results.
21  - See http://www.consul.io/docs/agent/http.html#kv for more details.
22requirements:
23  - python-consul
24  - requests
25author:
26  - Steve Gargan (@sgargan)
27  - Colin Nolan (@colin-nolan)
28options:
29    state:
30        description:
31          - The action to take with the supplied key and value. If the state is 'present' and `value` is set, the key
32            contents will be set to the value supplied and `changed` will be set to `true` only if the value was
33            different to the current contents. If the state is 'present' and `value` is not set, the existing value
34            associated to the key will be returned. The state 'absent' will remove the key/value pair,
35            again 'changed' will be set to true only if the key actually existed
36            prior to the removal. An attempt can be made to obtain or free the
37            lock associated with a key/value pair with the states 'acquire' or
38            'release' respectively. a valid session must be supplied to make the
39            attempt changed will be true if the attempt is successful, false
40            otherwise.
41        type: str
42        choices: [ absent, acquire, present, release ]
43        default: present
44    key:
45        description:
46          - The key at which the value should be stored.
47        type: str
48        required: yes
49    value:
50        description:
51          - The value should be associated with the given key, required if C(state)
52            is C(present).
53        type: str
54    recurse:
55        description:
56          - If the key represents a prefix, each entry with the prefix can be
57            retrieved by setting this to C(yes).
58        type: bool
59    retrieve:
60        description:
61            - If the I(state) is C(present) and I(value) is set, perform a
62              read after setting the value and return this value.
63        default: True
64        type: bool
65    session:
66        description:
67          - The session that should be used to acquire or release a lock
68            associated with a key/value pair.
69        type: str
70    token:
71        description:
72          - The token key identifying an ACL rule set that controls access to
73            the key value pair
74        type: str
75    cas:
76        description:
77          - Used when acquiring a lock with a session. If the C(cas) is C(0), then
78            Consul will only put the key if it does not already exist. If the
79            C(cas) value is non-zero, then the key is only set if the index matches
80            the ModifyIndex of that key.
81        type: str
82    flags:
83        description:
84          - Opaque positive integer value that can be passed when setting a value.
85        type: str
86    host:
87        description:
88          - Host of the consul agent.
89        type: str
90        default: localhost
91    port:
92        description:
93          - The port on which the consul agent is running.
94        type: int
95        default: 8500
96    scheme:
97        description:
98          - The protocol scheme on which the consul agent is running.
99        type: str
100        default: http
101    validate_certs:
102        description:
103          - Whether to verify the tls certificate of the consul agent.
104        type: bool
105        default: 'yes'
106'''
107
108
109EXAMPLES = '''
110# If the key does not exist, the value associated to the "data" property in `retrieved_key` will be `None`
111# If the key value is empty string, `retrieved_key["data"]["Value"]` will be `None`
112- name: Retrieve a value from the key/value store
113  community.general.consul_kv:
114    key: somekey
115  register: retrieved_key
116
117- name: Add or update the value associated with a key in the key/value store
118  community.general.consul_kv:
119    key: somekey
120    value: somevalue
121
122- name: Remove a key from the store
123  community.general.consul_kv:
124    key: somekey
125    state: absent
126
127- name: Add a node to an arbitrary group via consul inventory (see consul.ini)
128  community.general.consul_kv:
129    key: ansible/groups/dc1/somenode
130    value: top_secret
131
132- name: Register a key/value pair with an associated session
133  community.general.consul_kv:
134    key: stg/node/server_birthday
135    value: 20160509
136    session: "{{ sessionid }}"
137    state: acquire
138'''
139
140from ansible.module_utils.common.text.converters import to_text
141
142try:
143    import consul
144    from requests.exceptions import ConnectionError
145    python_consul_installed = True
146except ImportError:
147    python_consul_installed = False
148
149from ansible.module_utils.basic import AnsibleModule
150
151# Note: although the python-consul documentation implies that using a key with a value of `None` with `put` has a
152# special meaning (https://python-consul.readthedocs.io/en/latest/#consul-kv), if not set in the subsequently API call,
153# the value just defaults to an empty string (https://www.consul.io/api/kv.html#create-update-key)
154NOT_SET = None
155
156
157def _has_value_changed(consul_client, key, target_value):
158    """
159    Uses the given Consul client to determine if the value associated to the given key is different to the given target
160    value.
161    :param consul_client: Consul connected client
162    :param key: key in Consul
163    :param target_value: value to be associated to the key
164    :return: tuple where the first element is the value of the "X-Consul-Index" header and the second is `True` if the
165    value has changed (i.e. the stored value is not the target value)
166    """
167    index, existing = consul_client.kv.get(key)
168    if not existing:
169        return index, True
170    try:
171        changed = to_text(existing['Value'], errors='surrogate_or_strict') != target_value
172        return index, changed
173    except UnicodeError:
174        # Existing value was not decodable but all values we set are valid utf-8
175        return index, True
176
177
178def execute(module):
179    state = module.params.get('state')
180
181    if state == 'acquire' or state == 'release':
182        lock(module, state)
183    elif state == 'present':
184        if module.params.get('value') is NOT_SET:
185            get_value(module)
186        else:
187            set_value(module)
188    elif state == 'absent':
189        remove_value(module)
190    else:
191        module.exit_json(msg="Unsupported state: %s" % (state, ))
192
193
194def lock(module, state):
195
196    consul_api = get_consul_api(module)
197
198    session = module.params.get('session')
199    key = module.params.get('key')
200    value = module.params.get('value')
201
202    if not session:
203        module.fail(
204            msg='%s of lock for %s requested but no session supplied' %
205            (state, key))
206
207    index, changed = _has_value_changed(consul_api, key, value)
208
209    if changed and not module.check_mode:
210        if state == 'acquire':
211            changed = consul_api.kv.put(key, value,
212                                        cas=module.params.get('cas'),
213                                        acquire=session,
214                                        flags=module.params.get('flags'))
215        else:
216            changed = consul_api.kv.put(key, value,
217                                        cas=module.params.get('cas'),
218                                        release=session,
219                                        flags=module.params.get('flags'))
220
221    module.exit_json(changed=changed,
222                     index=index,
223                     key=key)
224
225
226def get_value(module):
227    consul_api = get_consul_api(module)
228    key = module.params.get('key')
229
230    index, existing_value = consul_api.kv.get(key, recurse=module.params.get('recurse'))
231
232    module.exit_json(changed=False, index=index, data=existing_value)
233
234
235def set_value(module):
236    consul_api = get_consul_api(module)
237
238    key = module.params.get('key')
239    value = module.params.get('value')
240
241    if value is NOT_SET:
242        raise AssertionError('Cannot set value of "%s" to `NOT_SET`' % key)
243
244    index, changed = _has_value_changed(consul_api, key, value)
245
246    if changed and not module.check_mode:
247        changed = consul_api.kv.put(key, value,
248                                    cas=module.params.get('cas'),
249                                    flags=module.params.get('flags'))
250
251    stored = None
252    if module.params.get('retrieve'):
253        index, stored = consul_api.kv.get(key)
254
255    module.exit_json(changed=changed,
256                     index=index,
257                     key=key,
258                     data=stored)
259
260
261def remove_value(module):
262    ''' remove the value associated with the given key. if the recurse parameter
263     is set then any key prefixed with the given key will be removed. '''
264    consul_api = get_consul_api(module)
265
266    key = module.params.get('key')
267
268    index, existing = consul_api.kv.get(
269        key, recurse=module.params.get('recurse'))
270
271    changed = existing is not None
272    if changed and not module.check_mode:
273        consul_api.kv.delete(key, module.params.get('recurse'))
274
275    module.exit_json(changed=changed,
276                     index=index,
277                     key=key,
278                     data=existing)
279
280
281def get_consul_api(module, token=None):
282    return consul.Consul(host=module.params.get('host'),
283                         port=module.params.get('port'),
284                         scheme=module.params.get('scheme'),
285                         verify=module.params.get('validate_certs'),
286                         token=module.params.get('token'))
287
288
289def test_dependencies(module):
290    if not python_consul_installed:
291        module.fail_json(msg="python-consul required for this module. "
292                             "see https://python-consul.readthedocs.io/en/latest/#installation")
293
294
295def main():
296
297    module = AnsibleModule(
298        argument_spec=dict(
299            cas=dict(type='str'),
300            flags=dict(type='str'),
301            key=dict(type='str', required=True, no_log=False),
302            host=dict(type='str', default='localhost'),
303            scheme=dict(type='str', default='http'),
304            validate_certs=dict(type='bool', default=True),
305            port=dict(type='int', default=8500),
306            recurse=dict(type='bool'),
307            retrieve=dict(type='bool', default=True),
308            state=dict(type='str', default='present', choices=['absent', 'acquire', 'present', 'release']),
309            token=dict(type='str', no_log=True),
310            value=dict(type='str', default=NOT_SET),
311            session=dict(type='str'),
312        ),
313        supports_check_mode=True
314    )
315
316    test_dependencies(module)
317
318    try:
319        execute(module)
320    except ConnectionError as e:
321        module.fail_json(msg='Could not connect to consul agent at %s:%s, error was %s' % (
322            module.params.get('host'), module.params.get('port'), e))
323    except Exception as e:
324        module.fail_json(msg=str(e))
325
326
327if __name__ == '__main__':
328    main()
329