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