1#!/usr/local/bin/python3.8 2# -*- coding: utf-8 -*- 3# 4# (c) 2018, Jean-Philippe Evrard <jean-philippe@evrard.me> 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 11DOCUMENTATION = ''' 12--- 13module: etcd3 14short_description: "Set or delete key value pairs from an etcd3 cluster" 15requirements: 16 - etcd3 17description: 18 - Sets or deletes values in etcd3 cluster using its v3 api. 19 - Needs python etcd3 lib to work 20options: 21 key: 22 type: str 23 description: 24 - the key where the information is stored in the cluster 25 required: true 26 value: 27 type: str 28 description: 29 - the information stored 30 required: true 31 host: 32 type: str 33 description: 34 - the IP address of the cluster 35 default: 'localhost' 36 port: 37 type: int 38 description: 39 - the port number used to connect to the cluster 40 default: 2379 41 state: 42 type: str 43 description: 44 - the state of the value for the key. 45 - can be present or absent 46 required: true 47 choices: [ present, absent ] 48 user: 49 type: str 50 description: 51 - The etcd user to authenticate with. 52 password: 53 type: str 54 description: 55 - The password to use for authentication. 56 - Required if I(user) is defined. 57 ca_cert: 58 type: path 59 description: 60 - The Certificate Authority to use to verify the etcd host. 61 - Required if I(client_cert) and I(client_key) are defined. 62 client_cert: 63 type: path 64 description: 65 - PEM formatted certificate chain file to be used for SSL client authentication. 66 - Required if I(client_key) is defined. 67 client_key: 68 type: path 69 description: 70 - PEM formatted file that contains your private key to be used for SSL client authentication. 71 - Required if I(client_cert) is defined. 72 timeout: 73 type: int 74 description: 75 - The socket level timeout in seconds. 76author: 77 - Jean-Philippe Evrard (@evrardjp) 78 - Victor Fauth (@vfauth) 79''' 80 81EXAMPLES = """ 82- name: Store a value "bar" under the key "foo" for a cluster located "http://localhost:2379" 83 community.general.etcd3: 84 key: "foo" 85 value: "baz3" 86 host: "localhost" 87 port: 2379 88 state: "present" 89 90- name: Authenticate using user/password combination with a timeout of 10 seconds 91 community.general.etcd3: 92 key: "foo" 93 value: "baz3" 94 state: "present" 95 user: "someone" 96 password: "password123" 97 timeout: 10 98 99- name: Authenticate using TLS certificates 100 community.general.etcd3: 101 key: "foo" 102 value: "baz3" 103 state: "present" 104 ca_cert: "/etc/ssl/certs/CA_CERT.pem" 105 client_cert: "/etc/ssl/certs/cert.crt" 106 client_key: "/etc/ssl/private/key.pem" 107""" 108 109RETURN = ''' 110key: 111 description: The key that was queried 112 returned: always 113 type: str 114old_value: 115 description: The previous value in the cluster 116 returned: always 117 type: str 118''' 119 120import traceback 121 122from ansible.module_utils.basic import AnsibleModule, missing_required_lib 123from ansible.module_utils.common.text.converters import to_native 124 125 126try: 127 import etcd3 128 HAS_ETCD = True 129except ImportError: 130 ETCD_IMP_ERR = traceback.format_exc() 131 HAS_ETCD = False 132 133 134def run_module(): 135 # define the available arguments/parameters that a user can pass to 136 # the module 137 module_args = dict( 138 key=dict(type='str', required=True, no_log=False), 139 value=dict(type='str', required=True), 140 host=dict(type='str', default='localhost'), 141 port=dict(type='int', default=2379), 142 state=dict(type='str', required=True, choices=['present', 'absent']), 143 user=dict(type='str'), 144 password=dict(type='str', no_log=True), 145 ca_cert=dict(type='path'), 146 client_cert=dict(type='path'), 147 client_key=dict(type='path'), 148 timeout=dict(type='int'), 149 ) 150 151 # seed the result dict in the object 152 # we primarily care about changed and state 153 # change is if this module effectively modified the target 154 # state will include any data that you want your module to pass back 155 # for consumption, for example, in a subsequent task 156 result = dict( 157 changed=False, 158 ) 159 160 # the AnsibleModule object will be our abstraction working with Ansible 161 # this includes instantiation, a couple of common attr would be the 162 # args/params passed to the execution, as well as if the module 163 # supports check mode 164 module = AnsibleModule( 165 argument_spec=module_args, 166 supports_check_mode=True, 167 required_together=[['client_cert', 'client_key'], ['user', 'password']], 168 ) 169 170 # It is possible to set `ca_cert` to verify the server identity without 171 # setting `client_cert` or `client_key` to authenticate the client 172 # so required_together is enough 173 # Due to `required_together=[['client_cert', 'client_key']]`, checking the presence 174 # of either `client_cert` or `client_key` is enough 175 if module.params['ca_cert'] is None and module.params['client_cert'] is not None: 176 module.fail_json(msg="The 'ca_cert' parameter must be defined when 'client_cert' and 'client_key' are present.") 177 178 result['key'] = module.params.get('key') 179 module.params['cert_cert'] = module.params.pop('client_cert') 180 module.params['cert_key'] = module.params.pop('client_key') 181 182 if not HAS_ETCD: 183 module.fail_json(msg=missing_required_lib('etcd3'), exception=ETCD_IMP_ERR) 184 185 allowed_keys = ['host', 'port', 'ca_cert', 'cert_cert', 'cert_key', 186 'timeout', 'user', 'password'] 187 # TODO(evrardjp): Move this back to a dict comprehension when python 2.7 is 188 # the minimum supported version 189 # client_params = {key: value for key, value in module.params.items() if key in allowed_keys} 190 client_params = dict() 191 for key, value in module.params.items(): 192 if key in allowed_keys: 193 client_params[key] = value 194 try: 195 etcd = etcd3.client(**client_params) 196 except Exception as exp: 197 module.fail_json(msg='Cannot connect to etcd cluster: %s' % (to_native(exp)), 198 exception=traceback.format_exc()) 199 try: 200 cluster_value = etcd.get(module.params['key']) 201 except Exception as exp: 202 module.fail_json(msg='Cannot reach data: %s' % (to_native(exp)), 203 exception=traceback.format_exc()) 204 205 # Make the cluster_value[0] a string for string comparisons 206 result['old_value'] = to_native(cluster_value[0]) 207 208 if module.params['state'] == 'absent': 209 if cluster_value[0] is not None: 210 if module.check_mode: 211 result['changed'] = True 212 else: 213 try: 214 etcd.delete(module.params['key']) 215 except Exception as exp: 216 module.fail_json(msg='Cannot delete %s: %s' % (module.params['key'], to_native(exp)), 217 exception=traceback.format_exc()) 218 else: 219 result['changed'] = True 220 elif module.params['state'] == 'present': 221 if result['old_value'] != module.params['value']: 222 if module.check_mode: 223 result['changed'] = True 224 else: 225 try: 226 etcd.put(module.params['key'], module.params['value']) 227 except Exception as exp: 228 module.fail_json(msg='Cannot add or edit key %s: %s' % (module.params['key'], to_native(exp)), 229 exception=traceback.format_exc()) 230 else: 231 result['changed'] = True 232 else: 233 module.fail_json(msg="State not recognized") 234 235 # manipulate or modify the state as needed (this is going to be the 236 # part where your module will do what it needs to do) 237 238 # during the execution of the module, if there is an exception or a 239 # conditional state that effectively causes a failure, run 240 # AnsibleModule.fail_json() to pass in the message and the result 241 242 # in the event of a successful module execution, you will want to 243 # simple AnsibleModule.exit_json(), passing the key/value results 244 module.exit_json(**result) 245 246 247def main(): 248 run_module() 249 250 251if __name__ == '__main__': 252 main() 253