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