#!/usr/bin/python # -*- coding: utf-8 -*- # Copyright 2015 WP Engine, Inc. All rights reserved. # GNU General Public License v3.0+ (see LICENSES/GPL-3.0-or-later.txt or https://www.gnu.org/licenses/gpl-3.0.txt) # SPDX-License-Identifier: GPL-3.0-or-later from __future__ import absolute_import, division, print_function __metaclass__ = type DOCUMENTATION = ''' --- module: znode short_description: Create, delete, retrieve, and update znodes using ZooKeeper description: - Create, delete, retrieve, and update znodes using ZooKeeper. attributes: check_mode: support: none diff_mode: support: none extends_documentation_fragment: - community.general.attributes options: hosts: description: - A list of ZooKeeper servers (format '[server]:[port]'). required: true type: str name: description: - The path of the znode. required: true type: str value: description: - The value assigned to the znode. type: str op: description: - An operation to perform. Mutually exclusive with state. choices: [ get, wait, list ] type: str state: description: - The state to enforce. Mutually exclusive with op. choices: [ present, absent ] type: str timeout: description: - The amount of time to wait for a node to appear. default: 300 type: int recursive: description: - Recursively delete node and all its children. type: bool default: false auth_scheme: description: - 'Authentication scheme.' choices: [ digest, sasl ] type: str default: "digest" required: false version_added: 5.8.0 auth_credential: description: - The authentication credential value. Depends on O(auth_scheme). - The format for O(auth_scheme=digest) is C(user:password), and the format for O(auth_scheme=sasl) is C(user:password). type: str required: false version_added: 5.8.0 use_tls: description: - Using TLS/SSL or not. type: bool default: false required: false version_added: '6.5.0' requirements: - kazoo >= 2.1 - python >= 2.6 author: "Trey Perry (@treyperry)" ''' EXAMPLES = """ - name: Creating or updating a znode with a given value community.general.znode: hosts: 'localhost:2181' name: /mypath value: myvalue state: present - name: Getting the value and stat structure for a znode community.general.znode: hosts: 'localhost:2181' name: /mypath op: get - name: Getting the value and stat structure for a znode using digest authentication community.general.znode: hosts: 'localhost:2181' auth_credential: 'user1:s3cr3t' name: /secretmypath op: get - name: Listing a particular znode's children community.general.znode: hosts: 'localhost:2181' name: /zookeeper op: list - name: Waiting 20 seconds for a znode to appear at path /mypath community.general.znode: hosts: 'localhost:2181' name: /mypath op: wait timeout: 20 - name: Deleting a znode at path /mypath community.general.znode: hosts: 'localhost:2181' name: /mypath state: absent - name: Creating or updating a znode with a given value on a remote Zookeeper community.general.znode: hosts: 'my-zookeeper-node:2181' name: /mypath value: myvalue state: present delegate_to: 127.0.0.1 """ import time import traceback KAZOO_IMP_ERR = None try: from kazoo.client import KazooClient from kazoo.handlers.threading import KazooTimeoutError KAZOO_INSTALLED = True except ImportError: KAZOO_IMP_ERR = traceback.format_exc() KAZOO_INSTALLED = False from ansible.module_utils.basic import AnsibleModule, missing_required_lib from ansible.module_utils.common.text.converters import to_bytes def main(): module = AnsibleModule( argument_spec=dict( hosts=dict(required=True, type='str'), name=dict(required=True, type='str'), value=dict(type='str'), op=dict(choices=['get', 'wait', 'list']), state=dict(choices=['present', 'absent']), timeout=dict(default=300, type='int'), recursive=dict(default=False, type='bool'), auth_scheme=dict(default='digest', choices=['digest', 'sasl']), auth_credential=dict(type='str', no_log=True), use_tls=dict(default=False, type='bool'), ), supports_check_mode=False ) if not KAZOO_INSTALLED: module.fail_json(msg=missing_required_lib('kazoo >= 2.1'), exception=KAZOO_IMP_ERR) check = check_params(module.params) if not check['success']: module.fail_json(msg=check['msg']) zoo = KazooCommandProxy(module) try: zoo.start() except KazooTimeoutError: module.fail_json(msg='The connection to the ZooKeeper ensemble timed out.') command_dict = { 'op': { 'get': zoo.get, 'list': zoo.list, 'wait': zoo.wait }, 'state': { 'present': zoo.present, 'absent': zoo.absent } } command_type = 'op' if 'op' in module.params and module.params['op'] is not None else 'state' method = module.params[command_type] result, result_dict = command_dict[command_type][method]() zoo.shutdown() if result: module.exit_json(**result_dict) else: module.fail_json(**result_dict) def check_params(params): if not params['state'] and not params['op']: return {'success': False, 'msg': 'Please define an operation (op) or a state.'} if params['state'] and params['op']: return {'success': False, 'msg': 'Please choose an operation (op) or a state, but not both.'} return {'success': True} class KazooCommandProxy(): def __init__(self, module): self.module = module self.zk = KazooClient(module.params['hosts'], use_ssl=module.params['use_tls']) def absent(self): return self._absent(self.module.params['name']) def exists(self, znode): return self.zk.exists(znode) def list(self): children = self.zk.get_children(self.module.params['name']) return True, {'count': len(children), 'items': children, 'msg': 'Retrieved znodes in path.', 'znode': self.module.params['name']} def present(self): return self._present(self.module.params['name'], self.module.params['value']) def get(self): return self._get(self.module.params['name']) def shutdown(self): self.zk.stop() self.zk.close() def start(self): self.zk.start() if self.module.params['auth_credential']: self.zk.add_auth(self.module.params['auth_scheme'], self.module.params['auth_credential']) def wait(self): return self._wait(self.module.params['name'], self.module.params['timeout']) def _absent(self, znode): if self.exists(znode): self.zk.delete(znode, recursive=self.module.params['recursive']) return True, {'changed': True, 'msg': 'The znode was deleted.'} else: return True, {'changed': False, 'msg': 'The znode does not exist.'} def _get(self, path): if self.exists(path): value, zstat = self.zk.get(path) stat_dict = {} for i in dir(zstat): if not i.startswith('_'): attr = getattr(zstat, i) if isinstance(attr, (int, str)): stat_dict[i] = attr result = True, {'msg': 'The node was retrieved.', 'znode': path, 'value': value, 'stat': stat_dict} else: result = False, {'msg': 'The requested node does not exist.'} return result def _present(self, path, value): if self.exists(path): (current_value, zstat) = self.zk.get(path) if value != current_value: self.zk.set(path, to_bytes(value)) return True, {'changed': True, 'msg': 'Updated the znode value.', 'znode': path, 'value': value} else: return True, {'changed': False, 'msg': 'No changes were necessary.', 'znode': path, 'value': value} else: self.zk.create(path, to_bytes(value), makepath=True) return True, {'changed': True, 'msg': 'Created a new znode.', 'znode': path, 'value': value} def _wait(self, path, timeout, interval=5): lim = time.time() + timeout while time.time() < lim: if self.exists(path): return True, {'msg': 'The node appeared before the configured timeout.', 'znode': path, 'timeout': timeout} else: time.sleep(interval) return False, {'msg': 'The node did not appear before the operation timed out.', 'timeout': timeout, 'znode': path} if __name__ == '__main__': main()