图片解析应用
You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.

1792 lines
60 KiB

  1. """Kazoo Zookeeper Client"""
  2. from collections import defaultdict, deque
  3. from functools import partial
  4. import inspect
  5. import logging
  6. from os.path import split
  7. import re
  8. import warnings
  9. from kazoo.exceptions import (
  10. AuthFailedError,
  11. ConfigurationError,
  12. ConnectionClosedError,
  13. ConnectionLoss,
  14. KazooException,
  15. NoNodeError,
  16. NodeExistsError,
  17. SessionExpiredError,
  18. WriterNotClosedException,
  19. )
  20. from kazoo.handlers.threading import SequentialThreadingHandler
  21. from kazoo.handlers.utils import capture_exceptions, wrap
  22. from kazoo.hosts import collect_hosts
  23. from kazoo.loggingsupport import BLATHER
  24. from kazoo.protocol.connection import ConnectionHandler
  25. from kazoo.protocol.paths import _prefix_root, normpath
  26. from kazoo.protocol.serialization import (
  27. Auth,
  28. CheckVersion,
  29. CloseInstance,
  30. Create,
  31. Create2,
  32. Delete,
  33. Exists,
  34. GetChildren,
  35. GetChildren2,
  36. GetACL,
  37. SetACL,
  38. GetData,
  39. Reconfig,
  40. SetData,
  41. Sync,
  42. Transaction,
  43. )
  44. from kazoo.protocol.states import (
  45. Callback,
  46. EventType,
  47. KazooState,
  48. KeeperState,
  49. WatchedEvent,
  50. )
  51. from kazoo.retry import KazooRetry
  52. from kazoo.security import ACL, OPEN_ACL_UNSAFE
  53. # convenience API
  54. from kazoo.recipe.barrier import Barrier, DoubleBarrier
  55. from kazoo.recipe.counter import Counter
  56. from kazoo.recipe.election import Election
  57. from kazoo.recipe.lease import NonBlockingLease, MultiNonBlockingLease
  58. from kazoo.recipe.lock import Lock, ReadLock, WriteLock, Semaphore
  59. from kazoo.recipe.partitioner import SetPartitioner
  60. from kazoo.recipe.party import Party, ShallowParty
  61. from kazoo.recipe.queue import Queue, LockingQueue
  62. from kazoo.recipe.watchers import ChildrenWatch, DataWatch
  63. CLOSED_STATES = (
  64. KeeperState.EXPIRED_SESSION,
  65. KeeperState.AUTH_FAILED,
  66. KeeperState.CLOSED,
  67. )
  68. ENVI_VERSION = re.compile(r"([\d\.]*).*", re.DOTALL)
  69. ENVI_VERSION_KEY = "zookeeper.version"
  70. log = logging.getLogger(__name__)
  71. _RETRY_COMPAT_DEFAULTS = dict(
  72. max_retries=None,
  73. retry_delay=0.1,
  74. retry_backoff=2,
  75. retry_max_delay=3600,
  76. )
  77. _RETRY_COMPAT_MAPPING = dict(
  78. max_retries="max_tries",
  79. retry_delay="delay",
  80. retry_backoff="backoff",
  81. retry_max_delay="max_delay",
  82. )
  83. class KazooClient(object):
  84. """An Apache Zookeeper Python client supporting alternate callback
  85. handlers and high-level functionality.
  86. Watch functions registered with this class will not get session
  87. events, unlike the default Zookeeper watches. They will also be
  88. called with a single argument, a
  89. :class:`~kazoo.protocol.states.WatchedEvent` instance.
  90. """
  91. def __init__(
  92. self,
  93. hosts="127.0.0.1:2181",
  94. timeout=10.0,
  95. client_id=None,
  96. handler=None,
  97. default_acl=None,
  98. auth_data=None,
  99. sasl_options=None,
  100. read_only=None,
  101. randomize_hosts=True,
  102. connection_retry=None,
  103. command_retry=None,
  104. logger=None,
  105. keyfile=None,
  106. keyfile_password=None,
  107. certfile=None,
  108. ca=None,
  109. use_ssl=False,
  110. verify_certs=True,
  111. **kwargs,
  112. ):
  113. """Create a :class:`KazooClient` instance. All time arguments
  114. are in seconds.
  115. :param hosts: Comma-separated list of hosts to connect to
  116. (e.g. 127.0.0.1:2181,127.0.0.1:2182,[::1]:2183).
  117. :param timeout: The longest to wait for a Zookeeper connection.
  118. :param client_id: A Zookeeper client id, used when
  119. re-establishing a prior session connection.
  120. :param handler: An instance of a class implementing the
  121. :class:`~kazoo.interfaces.IHandler` interface
  122. for callback handling.
  123. :param default_acl: A default ACL used on node creation.
  124. :param auth_data:
  125. A list of authentication credentials to use for the
  126. connection. Should be a list of (scheme, credential)
  127. tuples as :meth:`add_auth` takes.
  128. :param sasl_options:
  129. SASL options for the connection, if SASL support is to be used.
  130. Should be a dict of SASL options passed to the underlying
  131. `pure-sasl <https://pypi.org/project/pure-sasl>`_ library.
  132. For example using the DIGEST-MD5 mechnism:
  133. .. code-block:: python
  134. sasl_options = {
  135. 'mechanism': 'DIGEST-MD5',
  136. 'username': 'myusername',
  137. 'password': 'mypassword'
  138. }
  139. For GSSAPI, using the running process' ticket cache:
  140. .. code-block:: python
  141. sasl_options = {
  142. 'mechanism': 'GSSAPI',
  143. 'service': 'myzk', # optional
  144. 'principal': 'client@EXAMPLE.COM' # optional
  145. }
  146. :param read_only: Allow connections to read only servers.
  147. :param randomize_hosts: By default randomize host selection.
  148. :param connection_retry:
  149. A :class:`kazoo.retry.KazooRetry` object to use for
  150. retrying the connection to Zookeeper. Also can be a dict of
  151. options which will be used for creating one.
  152. :param command_retry:
  153. A :class:`kazoo.retry.KazooRetry` object to use for
  154. the :meth:`KazooClient.retry` method. Also can be a dict of
  155. options which will be used for creating one.
  156. :param logger: A custom logger to use instead of the module
  157. global `log` instance.
  158. :param keyfile: SSL keyfile to use for authentication
  159. :param keyfile_password: SSL keyfile password
  160. :param certfile: SSL certfile to use for authentication
  161. :param ca: SSL CA file to use for authentication
  162. :param use_ssl: argument to control whether SSL is used or not
  163. :param verify_certs: when using SSL, argument to bypass
  164. certs verification
  165. Basic Example:
  166. .. code-block:: python
  167. zk = KazooClient()
  168. zk.start()
  169. children = zk.get_children('/')
  170. zk.stop()
  171. As a convenience all recipe classes are available as attributes
  172. and get automatically bound to the client. For example::
  173. zk = KazooClient()
  174. zk.start()
  175. lock = zk.Lock('/lock_path')
  176. .. versionadded:: 0.6
  177. The read_only option. Requires Zookeeper 3.4+
  178. .. versionadded:: 0.6
  179. The retry_max_delay option.
  180. .. versionadded:: 0.6
  181. The randomize_hosts option.
  182. .. versionchanged:: 0.8
  183. Removed the unused watcher argument (was second argument).
  184. .. versionadded:: 1.2
  185. The connection_retry, command_retry and logger options.
  186. .. versionadded:: 2.7
  187. The sasl_options option.
  188. """
  189. self.logger = logger or log
  190. # Record the handler strategy used
  191. self.handler = handler if handler else SequentialThreadingHandler()
  192. if inspect.isclass(self.handler):
  193. raise ConfigurationError(
  194. "Handler must be an instance of a class, "
  195. "not the class: %s" % self.handler
  196. )
  197. self.auth_data = auth_data if auth_data else set([])
  198. self.default_acl = default_acl
  199. self.randomize_hosts = randomize_hosts
  200. self.hosts = None
  201. self.chroot = None
  202. self.set_hosts(hosts)
  203. self.use_ssl = use_ssl
  204. self.verify_certs = verify_certs
  205. self.certfile = certfile
  206. self.keyfile = keyfile
  207. self.keyfile_password = keyfile_password
  208. self.ca = ca
  209. # Curator like simplified state tracking, and listeners for
  210. # state transitions
  211. self._state = KeeperState.CLOSED
  212. self.state = KazooState.LOST
  213. self.state_listeners = set()
  214. self._child_watchers = defaultdict(set)
  215. self._data_watchers = defaultdict(set)
  216. self._reset()
  217. self.read_only = read_only
  218. if client_id:
  219. self._session_id = client_id[0]
  220. self._session_passwd = client_id[1]
  221. else:
  222. self._reset_session()
  223. # ZK uses milliseconds
  224. self._session_timeout = int(timeout * 1000)
  225. # We use events like twitter's client to track current and
  226. # desired state (connected, and whether to shutdown)
  227. self._live = self.handler.event_object()
  228. self._writer_stopped = self.handler.event_object()
  229. self._stopped = self.handler.event_object()
  230. self._stopped.set()
  231. self._writer_stopped.set()
  232. self.retry = self._conn_retry = None
  233. if type(connection_retry) is dict:
  234. self._conn_retry = KazooRetry(**connection_retry)
  235. elif type(connection_retry) is KazooRetry:
  236. self._conn_retry = connection_retry
  237. if type(command_retry) is dict:
  238. self.retry = KazooRetry(**command_retry)
  239. elif type(command_retry) is KazooRetry:
  240. self.retry = command_retry
  241. if type(self._conn_retry) is KazooRetry:
  242. if self.handler.sleep_func != self._conn_retry.sleep_func:
  243. raise ConfigurationError(
  244. "Retry handler and event handler "
  245. " must use the same sleep func"
  246. )
  247. if type(self.retry) is KazooRetry:
  248. if self.handler.sleep_func != self.retry.sleep_func:
  249. raise ConfigurationError(
  250. "Command retry handler and event handler "
  251. "must use the same sleep func"
  252. )
  253. if self.retry is None or self._conn_retry is None:
  254. old_retry_keys = dict(_RETRY_COMPAT_DEFAULTS)
  255. for key in old_retry_keys:
  256. try:
  257. old_retry_keys[key] = kwargs.pop(key)
  258. warnings.warn(
  259. "Passing retry configuration param %s to the "
  260. "client directly is deprecated, please pass a "
  261. "configured retry object (using param %s)"
  262. % (key, _RETRY_COMPAT_MAPPING[key]),
  263. DeprecationWarning,
  264. stacklevel=2,
  265. )
  266. except KeyError:
  267. pass
  268. retry_keys = {}
  269. for oldname, value in old_retry_keys.items():
  270. retry_keys[_RETRY_COMPAT_MAPPING[oldname]] = value
  271. if self._conn_retry is None:
  272. self._conn_retry = KazooRetry(
  273. sleep_func=self.handler.sleep_func, **retry_keys
  274. )
  275. if self.retry is None:
  276. self.retry = KazooRetry(
  277. sleep_func=self.handler.sleep_func, **retry_keys
  278. )
  279. # Managing legacy SASL options
  280. for scheme, auth in self.auth_data:
  281. if scheme != "sasl":
  282. continue
  283. if sasl_options:
  284. raise ConfigurationError(
  285. "Multiple SASL configurations provided"
  286. )
  287. warnings.warn(
  288. "Passing SASL configuration as part of the auth_data is "
  289. "deprecated, please use the sasl_options configuration "
  290. "instead",
  291. DeprecationWarning,
  292. stacklevel=2,
  293. )
  294. username, password = auth.split(":")
  295. # Generate an equivalent SASL configuration
  296. sasl_options = {
  297. "username": username,
  298. "password": password,
  299. "mechanism": "DIGEST-MD5",
  300. "service": "zookeeper",
  301. "principal": "zk-sasl-md5",
  302. }
  303. # Cleanup
  304. self.auth_data = set(
  305. [
  306. (scheme, auth)
  307. for scheme, auth in self.auth_data
  308. if scheme != "sasl"
  309. ]
  310. )
  311. self._conn_retry.interrupt = lambda: self._stopped.is_set()
  312. self._connection = ConnectionHandler(
  313. self,
  314. self._conn_retry.copy(),
  315. logger=self.logger,
  316. sasl_options=sasl_options,
  317. )
  318. # Every retry call should have its own copy of the retry helper
  319. # to avoid shared retry counts
  320. self._retry = self.retry
  321. def _retry(*args, **kwargs):
  322. return self._retry.copy()(*args, **kwargs)
  323. self.retry = _retry
  324. self.Barrier = partial(Barrier, self)
  325. self.Counter = partial(Counter, self)
  326. self.DoubleBarrier = partial(DoubleBarrier, self)
  327. self.ChildrenWatch = partial(ChildrenWatch, self)
  328. self.DataWatch = partial(DataWatch, self)
  329. self.Election = partial(Election, self)
  330. self.NonBlockingLease = partial(NonBlockingLease, self)
  331. self.MultiNonBlockingLease = partial(MultiNonBlockingLease, self)
  332. self.Lock = partial(Lock, self)
  333. self.ReadLock = partial(ReadLock, self)
  334. self.WriteLock = partial(WriteLock, self)
  335. self.Party = partial(Party, self)
  336. self.Queue = partial(Queue, self)
  337. self.LockingQueue = partial(LockingQueue, self)
  338. self.SetPartitioner = partial(SetPartitioner, self)
  339. self.Semaphore = partial(Semaphore, self)
  340. self.ShallowParty = partial(ShallowParty, self)
  341. # If we got any unhandled keywords, complain like Python would
  342. if kwargs:
  343. raise TypeError(
  344. "__init__() got unexpected keyword arguments: %s"
  345. % (kwargs.keys(),)
  346. )
  347. def _reset(self):
  348. """Resets a variety of client states for a new connection."""
  349. self._queue = deque()
  350. self._pending = deque()
  351. self._reset_watchers()
  352. self._reset_session()
  353. self.last_zxid = 0
  354. self._protocol_version = None
  355. def _reset_watchers(self):
  356. watchers = []
  357. for child_watchers in self._child_watchers.values():
  358. watchers.extend(child_watchers)
  359. for data_watchers in self._data_watchers.values():
  360. watchers.extend(data_watchers)
  361. self._child_watchers = defaultdict(set)
  362. self._data_watchers = defaultdict(set)
  363. ev = WatchedEvent(EventType.NONE, self._state, None)
  364. for watch in watchers:
  365. self.handler.dispatch_callback(Callback("watch", watch, (ev,)))
  366. def _reset_session(self):
  367. self._session_id = None
  368. self._session_passwd = b"\x00" * 16
  369. @property
  370. def client_state(self):
  371. """Returns the last Zookeeper client state
  372. This is the non-simplified state information and is generally
  373. not as useful as the simplified KazooState information.
  374. """
  375. return self._state
  376. @property
  377. def client_id(self):
  378. """Returns the client id for this Zookeeper session if
  379. connected.
  380. :returns: client id which consists of the session id and
  381. password.
  382. :rtype: tuple
  383. """
  384. if self._live.is_set():
  385. return (self._session_id, self._session_passwd)
  386. return None
  387. @property
  388. def connected(self):
  389. """Returns whether the Zookeeper connection has been
  390. established."""
  391. return self._live.is_set()
  392. def set_hosts(self, hosts, randomize_hosts=None):
  393. """sets the list of hosts used by this client.
  394. This function accepts the same format hosts parameter as the init
  395. function and sets the client to use the new hosts the next time it
  396. needs to look up a set of hosts. This function does not affect the
  397. current connected status.
  398. It is not currently possible to change the chroot with this function,
  399. setting a host list with a new chroot will raise a ConfigurationError.
  400. :param hosts: see description in :meth:`KazooClient.__init__`
  401. :param randomize_hosts: override client default for host randomization
  402. :raises:
  403. :exc:`ConfigurationError` if the hosts argument changes the chroot
  404. .. versionadded:: 1.4
  405. .. warning::
  406. Using this function to point a client to a completely disparate
  407. zookeeper server cluster has undefined behavior.
  408. """
  409. # Change the client setting for randomization if specified
  410. if randomize_hosts is not None:
  411. self.randomize_hosts = randomize_hosts
  412. # Randomizing the list will be done at connect time
  413. self.hosts, chroot = collect_hosts(hosts)
  414. if chroot:
  415. new_chroot = normpath(chroot)
  416. else:
  417. new_chroot = ""
  418. if self.chroot is not None and new_chroot != self.chroot:
  419. raise ConfigurationError(
  420. "Changing chroot at runtime is not " "currently supported"
  421. )
  422. self.chroot = new_chroot
  423. def add_listener(self, listener):
  424. """Add a function to be called for connection state changes.
  425. This function will be called with a
  426. :class:`~kazoo.protocol.states.KazooState` instance indicating
  427. the new connection state on state transitions.
  428. .. warning::
  429. This function must not block. If its at all likely that it
  430. might need data or a value that could result in blocking
  431. than the :meth:`~kazoo.interfaces.IHandler.spawn` method
  432. should be used so that the listener can return immediately.
  433. """
  434. if not (listener and callable(listener)):
  435. raise ConfigurationError("listener must be callable")
  436. self.state_listeners.add(listener)
  437. def remove_listener(self, listener):
  438. """Remove a listener function"""
  439. self.state_listeners.discard(listener)
  440. def _make_state_change(self, state):
  441. # skip if state is current
  442. if self.state == state:
  443. return
  444. self.state = state
  445. # Create copy of listeners for iteration in case one needs to
  446. # remove itself
  447. for listener in list(self.state_listeners):
  448. try:
  449. remove = listener(state)
  450. if remove is True:
  451. self.remove_listener(listener)
  452. except Exception:
  453. self.logger.exception("Error in connection state listener")
  454. def _session_callback(self, state):
  455. if state == self._state:
  456. return
  457. # Note that we don't check self.state == LOST since that's also
  458. # the client's initial state
  459. closed_state = self._state in CLOSED_STATES
  460. self._state = state
  461. # If we were previously closed or had an expired session, and
  462. # are now connecting, don't bother with the rest of the
  463. # transitions since they only apply after
  464. # we've established a connection
  465. if closed_state and state == KeeperState.CONNECTING:
  466. self.logger.log(BLATHER, "Skipping state change")
  467. return
  468. if state in (KeeperState.CONNECTED, KeeperState.CONNECTED_RO):
  469. self.logger.info(
  470. "Zookeeper connection established, " "state: %s", state
  471. )
  472. self._live.set()
  473. self._make_state_change(KazooState.CONNECTED)
  474. elif state in CLOSED_STATES:
  475. self.logger.info("Zookeeper session closed, state: %s", state)
  476. self._live.clear()
  477. self._make_state_change(KazooState.LOST)
  478. self._notify_pending(state)
  479. self._reset()
  480. else:
  481. self.logger.info("Zookeeper connection lost")
  482. # Connection lost
  483. self._live.clear()
  484. self._notify_pending(state)
  485. self._make_state_change(KazooState.SUSPENDED)
  486. self._reset_watchers()
  487. def _notify_pending(self, state):
  488. """Used to clear a pending response queue and request queue
  489. during connection drops."""
  490. if state == KeeperState.AUTH_FAILED:
  491. exc = AuthFailedError()
  492. elif state == KeeperState.EXPIRED_SESSION:
  493. exc = SessionExpiredError()
  494. else:
  495. exc = ConnectionLoss()
  496. while True:
  497. try:
  498. request, async_object, xid = self._pending.popleft()
  499. if async_object:
  500. async_object.set_exception(exc)
  501. except IndexError:
  502. break
  503. while True:
  504. try:
  505. request, async_object = self._queue.popleft()
  506. if async_object:
  507. async_object.set_exception(exc)
  508. except IndexError:
  509. break
  510. def _safe_close(self):
  511. self.handler.stop()
  512. timeout = self._session_timeout // 1000
  513. if timeout < 10:
  514. timeout = 10
  515. if not self._connection.stop(timeout):
  516. raise WriterNotClosedException(
  517. "Writer still open from prior connection "
  518. "and wouldn't close after %s seconds" % timeout
  519. )
  520. def _call(self, request, async_object):
  521. """Ensure the client is in CONNECTED or SUSPENDED state and put the
  522. request in the queue if it is.
  523. Returns False if the call short circuits due to AUTH_FAILED,
  524. CLOSED, or EXPIRED_SESSION state.
  525. """
  526. if self._state == KeeperState.AUTH_FAILED:
  527. async_object.set_exception(AuthFailedError())
  528. return False
  529. elif self._state == KeeperState.CLOSED:
  530. async_object.set_exception(
  531. ConnectionClosedError("Connection has been closed")
  532. )
  533. return False
  534. elif self._state == KeeperState.EXPIRED_SESSION:
  535. async_object.set_exception(SessionExpiredError())
  536. return False
  537. self._queue.append((request, async_object))
  538. # wake the connection, guarding against a race with close()
  539. write_sock = self._connection._write_sock
  540. if write_sock is None:
  541. async_object.set_exception(
  542. ConnectionClosedError("Connection has been closed")
  543. )
  544. try:
  545. write_sock.send(b"\0")
  546. except: # NOQA
  547. async_object.set_exception(
  548. ConnectionClosedError("Connection has been closed")
  549. )
  550. def start(self, timeout=15):
  551. """Initiate connection to ZK.
  552. :param timeout: Time in seconds to wait for connection to
  553. succeed.
  554. :raises: :attr:`~kazoo.interfaces.IHandler.timeout_exception`
  555. if the connection wasn't established within `timeout`
  556. seconds.
  557. """
  558. event = self.start_async()
  559. event.wait(timeout=timeout)
  560. if not self.connected:
  561. # We time-out, ensure we are disconnected
  562. self.stop()
  563. self.close()
  564. raise self.handler.timeout_exception("Connection time-out")
  565. if self.chroot and not self.exists("/"):
  566. warnings.warn(
  567. "No chroot path exists, the chroot path "
  568. "should be created before normal use."
  569. )
  570. def start_async(self):
  571. """Asynchronously initiate connection to ZK.
  572. :returns: An event object that can be checked to see if the
  573. connection is alive.
  574. :rtype: :class:`~threading.Event` compatible object.
  575. """
  576. # If we're already connected, ignore
  577. if self._live.is_set():
  578. return self._live
  579. # Make sure we're safely closed
  580. self._safe_close()
  581. # We've been asked to connect, clear the stop and our writer
  582. # thread indicator
  583. self._stopped.clear()
  584. self._writer_stopped.clear()
  585. # Start the handler
  586. self.handler.start()
  587. # Start the connection
  588. self._connection.start()
  589. return self._live
  590. def stop(self):
  591. """Gracefully stop this Zookeeper session.
  592. This method can be called while a reconnection attempt is in
  593. progress, which will then be halted.
  594. Once the connection is closed, its session becomes invalid. All
  595. the ephemeral nodes in the ZooKeeper server associated with the
  596. session will be removed. The watches left on those nodes (and
  597. on their parents) will be triggered.
  598. """
  599. if self._stopped.is_set():
  600. return
  601. self._stopped.set()
  602. self._queue.append((CloseInstance, None))
  603. try:
  604. self._connection._write_sock.send(b"\0")
  605. finally:
  606. self._safe_close()
  607. def restart(self):
  608. """Stop and restart the Zookeeper session."""
  609. self.stop()
  610. self.start()
  611. def close(self):
  612. """Free any resources held by the client.
  613. This method should be called on a stopped client before it is
  614. discarded. Not doing so may result in filehandles being leaked.
  615. .. versionadded:: 1.0
  616. """
  617. self._connection.close()
  618. def command(self, cmd=b"ruok"):
  619. """Sent a management command to the current ZK server.
  620. Examples are `ruok`, `envi` or `stat`.
  621. :returns: An unstructured textual response.
  622. :rtype: str
  623. :raises:
  624. :exc:`ConnectionLoss` if there is no connection open, or
  625. possibly a :exc:`socket.error` if there's a problem with
  626. the connection used just for this command.
  627. .. versionadded:: 0.5
  628. """
  629. if not self._live.is_set():
  630. raise ConnectionLoss("No connection to server")
  631. peer = self._connection._socket.getpeername()[:2]
  632. sock = self.handler.create_connection(
  633. peer,
  634. timeout=self._session_timeout / 1000.0,
  635. use_ssl=self.use_ssl,
  636. ca=self.ca,
  637. certfile=self.certfile,
  638. keyfile=self.keyfile,
  639. keyfile_password=self.keyfile_password,
  640. verify_certs=self.verify_certs,
  641. )
  642. sock.sendall(cmd)
  643. result = sock.recv(8192)
  644. sock.close()
  645. return result.decode("utf-8", "replace")
  646. def server_version(self, retries=3):
  647. """Get the version of the currently connected ZK server.
  648. :returns: The server version, for example (3, 4, 3).
  649. :rtype: tuple
  650. .. versionadded:: 0.5
  651. """
  652. def _try_fetch():
  653. data = self.command(b"envi")
  654. data_parsed = {}
  655. for line in data.splitlines():
  656. try:
  657. k, v = line.split("=", 1)
  658. k = k.strip()
  659. v = v.strip()
  660. except ValueError:
  661. pass
  662. else:
  663. if k:
  664. data_parsed[k] = v
  665. version = data_parsed.get(ENVI_VERSION_KEY, "")
  666. version_digits = ENVI_VERSION.match(version).group(1)
  667. try:
  668. return tuple([int(d) for d in version_digits.split(".")])
  669. except ValueError:
  670. return None
  671. def _is_valid(version):
  672. # All zookeeper versions should have at least major.minor
  673. # version numbers; if we get one that doesn't it is likely not
  674. # correct and was truncated...
  675. if version and len(version) > 1:
  676. return True
  677. return False
  678. # Try 1 + retries amount of times to get a version that we know
  679. # will likely be acceptable...
  680. version = _try_fetch()
  681. if _is_valid(version):
  682. return version
  683. for _i in range(0, retries):
  684. version = _try_fetch()
  685. if _is_valid(version):
  686. return version
  687. raise KazooException(
  688. "Unable to fetch useable server"
  689. " version after trying %s times" % (1 + max(0, retries))
  690. )
  691. def add_auth(self, scheme, credential):
  692. """Send credentials to server.
  693. :param scheme: authentication scheme (default supported:
  694. "digest").
  695. :param credential: the credential -- value depends on scheme.
  696. :returns: True if it was successful.
  697. :rtype: bool
  698. :raises:
  699. :exc:`~kazoo.exceptions.AuthFailedError` if it failed though
  700. the session state will be set to AUTH_FAILED as well.
  701. """
  702. return self.add_auth_async(scheme, credential).get()
  703. def add_auth_async(self, scheme, credential):
  704. """Asynchronously send credentials to server. Takes the same
  705. arguments as :meth:`add_auth`.
  706. :rtype: :class:`~kazoo.interfaces.IAsyncResult`
  707. """
  708. if not isinstance(scheme, str):
  709. raise TypeError("Invalid type for 'scheme' (string expected)")
  710. if not isinstance(credential, str):
  711. raise TypeError("Invalid type for 'credential' (string expected)")
  712. # we need this auth data to re-authenticate on reconnect
  713. self.auth_data.add((scheme, credential))
  714. async_result = self.handler.async_result()
  715. self._call(Auth(0, scheme, credential), async_result)
  716. return async_result
  717. def unchroot(self, path):
  718. """Strip the chroot if applicable from the path."""
  719. if not self.chroot:
  720. return path
  721. if self.chroot == path:
  722. return "/"
  723. if path.startswith(self.chroot):
  724. return path[len(self.chroot) :]
  725. else:
  726. return path
  727. def sync_async(self, path):
  728. """Asynchronous sync.
  729. :rtype: :class:`~kazoo.interfaces.IAsyncResult`
  730. """
  731. async_result = self.handler.async_result()
  732. @wrap(async_result)
  733. def _sync_completion(result):
  734. return self.unchroot(result.get())
  735. def _do_sync():
  736. result = self.handler.async_result()
  737. self._call(Sync(_prefix_root(self.chroot, path)), result)
  738. result.rawlink(_sync_completion)
  739. _do_sync()
  740. return async_result
  741. def sync(self, path):
  742. """Sync, blocks until response is acknowledged.
  743. Flushes channel between process and leader.
  744. :param path: path of node.
  745. :returns: The node path that was synced.
  746. :raises:
  747. :exc:`~kazoo.exceptions.ZookeeperError` if the server
  748. returns a non-zero error code.
  749. .. versionadded:: 0.5
  750. """
  751. return self.sync_async(path).get()
  752. def create(
  753. self,
  754. path,
  755. value=b"",
  756. acl=None,
  757. ephemeral=False,
  758. sequence=False,
  759. makepath=False,
  760. include_data=False,
  761. ):
  762. """Create a node with the given value as its data. Optionally
  763. set an ACL on the node.
  764. The ephemeral and sequence arguments determine the type of the
  765. node.
  766. An ephemeral node will be automatically removed by ZooKeeper
  767. when the session associated with the creation of the node
  768. expires.
  769. A sequential node will be given the specified path plus a
  770. suffix `i` where i is the current sequential number of the
  771. node. The sequence number is always fixed length of 10 digits,
  772. 0 padded. Once such a node is created, the sequential number
  773. will be incremented by one.
  774. If a node with the same actual path already exists in
  775. ZooKeeper, a NodeExistsError will be raised. Note that since a
  776. different actual path is used for each invocation of creating
  777. sequential nodes with the same path argument, the call will
  778. never raise NodeExistsError.
  779. If the parent node does not exist in ZooKeeper, a NoNodeError
  780. will be raised. Setting the optional `makepath` argument to
  781. `True` will create all missing parent nodes instead.
  782. An ephemeral node cannot have children. If the parent node of
  783. the given path is ephemeral, a NoChildrenForEphemeralsError
  784. will be raised.
  785. This operation, if successful, will trigger all the watches
  786. left on the node of the given path by :meth:`exists` and
  787. :meth:`get` API calls, and the watches left on the parent node
  788. by :meth:`get_children` API calls.
  789. The maximum allowable size of the node value is 1 MB. Values
  790. larger than this will cause a ZookeeperError to be raised.
  791. :param path: Path of node.
  792. :param value: Initial bytes value of node.
  793. :param acl: :class:`~kazoo.security.ACL` list.
  794. :param ephemeral: Boolean indicating whether node is ephemeral
  795. (tied to this session).
  796. :param sequence: Boolean indicating whether path is suffixed
  797. with a unique index.
  798. :param makepath: Whether the path should be created if it
  799. doesn't exist.
  800. :param include_data:
  801. Include the :class:`~kazoo.protocol.states.ZnodeStat` of
  802. the node in addition to its real path. This option changes
  803. the return value to be a tuple of (path, stat).
  804. :returns: Real path of the new node, or tuple if `include_data`
  805. is `True`
  806. :rtype: str
  807. :raises:
  808. :exc:`~kazoo.exceptions.NodeExistsError` if the node
  809. already exists.
  810. :exc:`~kazoo.exceptions.NoNodeError` if parent nodes are
  811. missing.
  812. :exc:`~kazoo.exceptions.NoChildrenForEphemeralsError` if
  813. the parent node is an ephemeral node.
  814. :exc:`~kazoo.exceptions.ZookeeperError` if the provided
  815. value is too large.
  816. :exc:`~kazoo.exceptions.ZookeeperError` if the server
  817. returns a non-zero error code.
  818. .. versionadded:: 1.1
  819. The `makepath` option.
  820. .. versionadded:: 2.7
  821. The `include_data` option.
  822. """
  823. acl = acl or self.default_acl
  824. return self.create_async(
  825. path,
  826. value,
  827. acl=acl,
  828. ephemeral=ephemeral,
  829. sequence=sequence,
  830. makepath=makepath,
  831. include_data=include_data,
  832. ).get()
  833. def create_async(
  834. self,
  835. path,
  836. value=b"",
  837. acl=None,
  838. ephemeral=False,
  839. sequence=False,
  840. makepath=False,
  841. include_data=False,
  842. ):
  843. """Asynchronously create a ZNode. Takes the same arguments as
  844. :meth:`create`.
  845. :rtype: :class:`~kazoo.interfaces.IAsyncResult`
  846. .. versionadded:: 1.1
  847. The makepath option.
  848. .. versionadded:: 2.7
  849. The `include_data` option.
  850. """
  851. if acl is None and self.default_acl:
  852. acl = self.default_acl
  853. if not isinstance(path, str):
  854. raise TypeError("Invalid type for 'path' (string expected)")
  855. if acl and (
  856. isinstance(acl, ACL) or not isinstance(acl, (tuple, list))
  857. ):
  858. raise TypeError(
  859. "Invalid type for 'acl' (acl must be a tuple/list" " of ACL's"
  860. )
  861. if value is not None and not isinstance(value, bytes):
  862. raise TypeError("Invalid type for 'value' (must be a byte string)")
  863. if not isinstance(ephemeral, bool):
  864. raise TypeError("Invalid type for 'ephemeral' (bool expected)")
  865. if not isinstance(sequence, bool):
  866. raise TypeError("Invalid type for 'sequence' (bool expected)")
  867. if not isinstance(makepath, bool):
  868. raise TypeError("Invalid type for 'makepath' (bool expected)")
  869. if not isinstance(include_data, bool):
  870. raise TypeError("Invalid type for 'include_data' (bool expected)")
  871. flags = 0
  872. if ephemeral:
  873. flags |= 1
  874. if sequence:
  875. flags |= 2
  876. if acl is None:
  877. acl = OPEN_ACL_UNSAFE
  878. async_result = self.handler.async_result()
  879. @capture_exceptions(async_result)
  880. def do_create():
  881. result = self._create_async_inner(
  882. path,
  883. value,
  884. acl,
  885. flags,
  886. trailing=sequence,
  887. include_data=include_data,
  888. )
  889. result.rawlink(create_completion)
  890. @capture_exceptions(async_result)
  891. def retry_completion(result):
  892. result.get()
  893. do_create()
  894. @wrap(async_result)
  895. def create_completion(result):
  896. try:
  897. if include_data:
  898. new_path, stat = result.get()
  899. return self.unchroot(new_path), stat
  900. else:
  901. return self.unchroot(result.get())
  902. except NoNodeError:
  903. if not makepath:
  904. raise
  905. if sequence and path.endswith("/"):
  906. parent = path.rstrip("/")
  907. else:
  908. parent, _ = split(path)
  909. self.ensure_path_async(parent, acl).rawlink(retry_completion)
  910. do_create()
  911. return async_result
  912. def _create_async_inner(
  913. self, path, value, acl, flags, trailing=False, include_data=False
  914. ):
  915. async_result = self.handler.async_result()
  916. if include_data:
  917. opcode = Create2
  918. else:
  919. opcode = Create
  920. call_result = self._call(
  921. opcode(
  922. _prefix_root(self.chroot, path, trailing=trailing),
  923. value,
  924. acl,
  925. flags,
  926. ),
  927. async_result,
  928. )
  929. if call_result is False:
  930. # We hit a short-circuit exit on the _call. Because we are
  931. # not using the original async_result here, we bubble the
  932. # exception upwards to the do_create function in
  933. # KazooClient.create so that it gets set on the correct
  934. # async_result object
  935. raise async_result.exception
  936. return async_result
  937. def ensure_path(self, path, acl=None):
  938. """Recursively create a path if it doesn't exist.
  939. :param path: Path of node.
  940. :param acl: Permissions for node.
  941. """
  942. return self.ensure_path_async(path, acl).get()
  943. def ensure_path_async(self, path, acl=None):
  944. """Recursively create a path asynchronously if it doesn't
  945. exist. Takes the same arguments as :meth:`ensure_path`.
  946. :rtype: :class:`~kazoo.interfaces.IAsyncResult`
  947. .. versionadded:: 1.1
  948. """
  949. acl = acl or self.default_acl
  950. async_result = self.handler.async_result()
  951. @wrap(async_result)
  952. def create_completion(result):
  953. try:
  954. return result.get()
  955. except NodeExistsError:
  956. return True
  957. @capture_exceptions(async_result)
  958. def prepare_completion(next_path, result):
  959. result.get()
  960. self.create_async(next_path, acl=acl).rawlink(create_completion)
  961. @wrap(async_result)
  962. def exists_completion(path, result):
  963. if result.get():
  964. return True
  965. parent, node = split(path)
  966. if node:
  967. self.ensure_path_async(parent, acl=acl).rawlink(
  968. partial(prepare_completion, path)
  969. )
  970. else:
  971. self.create_async(path, acl=acl).rawlink(create_completion)
  972. self.exists_async(path).rawlink(partial(exists_completion, path))
  973. return async_result
  974. def exists(self, path, watch=None):
  975. """Check if a node exists.
  976. If a watch is provided, it will be left on the node with the
  977. given path. The watch will be triggered by a successful
  978. operation that creates/deletes the node or sets the data on the
  979. node.
  980. :param path: Path of node.
  981. :param watch: Optional watch callback to set for future changes
  982. to this path.
  983. :returns: ZnodeStat of the node if it exists, else None if the
  984. node does not exist.
  985. :rtype: :class:`~kazoo.protocol.states.ZnodeStat` or `None`.
  986. :raises:
  987. :exc:`~kazoo.exceptions.ZookeeperError` if the server
  988. returns a non-zero error code.
  989. """
  990. return self.exists_async(path, watch=watch).get()
  991. def exists_async(self, path, watch=None):
  992. """Asynchronously check if a node exists. Takes the same
  993. arguments as :meth:`exists`.
  994. :rtype: :class:`~kazoo.interfaces.IAsyncResult`
  995. """
  996. if not isinstance(path, str):
  997. raise TypeError("Invalid type for 'path' (string expected)")
  998. if watch and not callable(watch):
  999. raise TypeError("Invalid type for 'watch' (must be a callable)")
  1000. async_result = self.handler.async_result()
  1001. self._call(
  1002. Exists(_prefix_root(self.chroot, path), watch), async_result
  1003. )
  1004. return async_result
  1005. def get(self, path, watch=None):
  1006. """Get the value of a node.
  1007. If a watch is provided, it will be left on the node with the
  1008. given path. The watch will be triggered by a successful
  1009. operation that sets data on the node, or deletes the node.
  1010. :param path: Path of node.
  1011. :param watch: Optional watch callback to set for future changes
  1012. to this path.
  1013. :returns:
  1014. Tuple (value, :class:`~kazoo.protocol.states.ZnodeStat`) of
  1015. node.
  1016. :rtype: tuple
  1017. :raises:
  1018. :exc:`~kazoo.exceptions.NoNodeError` if the node doesn't
  1019. exist
  1020. :exc:`~kazoo.exceptions.ZookeeperError` if the server
  1021. returns a non-zero error code
  1022. """
  1023. return self.get_async(path, watch=watch).get()
  1024. def get_async(self, path, watch=None):
  1025. """Asynchronously get the value of a node. Takes the same
  1026. arguments as :meth:`get`.
  1027. :rtype: :class:`~kazoo.interfaces.IAsyncResult`
  1028. """
  1029. if not isinstance(path, str):
  1030. raise TypeError("Invalid type for 'path' (string expected)")
  1031. if watch and not callable(watch):
  1032. raise TypeError("Invalid type for 'watch' (must be a callable)")
  1033. async_result = self.handler.async_result()
  1034. self._call(
  1035. GetData(_prefix_root(self.chroot, path), watch), async_result
  1036. )
  1037. return async_result
  1038. def get_children(self, path, watch=None, include_data=False):
  1039. """Get a list of child nodes of a path.
  1040. If a watch is provided it will be left on the node with the
  1041. given path. The watch will be triggered by a successful
  1042. operation that deletes the node of the given path or
  1043. creates/deletes a child under the node.
  1044. The list of children returned is not sorted and no guarantee is
  1045. provided as to its natural or lexical order.
  1046. :param path: Path of node to list.
  1047. :param watch: Optional watch callback to set for future changes
  1048. to this path.
  1049. :param include_data:
  1050. Include the :class:`~kazoo.protocol.states.ZnodeStat` of
  1051. the node in addition to the children. This option changes
  1052. the return value to be a tuple of (children, stat).
  1053. :returns: List of child node names, or tuple if `include_data`
  1054. is `True`.
  1055. :rtype: list
  1056. :raises:
  1057. :exc:`~kazoo.exceptions.NoNodeError` if the node doesn't
  1058. exist.
  1059. :exc:`~kazoo.exceptions.ZookeeperError` if the server
  1060. returns a non-zero error code.
  1061. .. versionadded:: 0.5
  1062. The `include_data` option.
  1063. """
  1064. return self.get_children_async(
  1065. path, watch=watch, include_data=include_data
  1066. ).get()
  1067. def get_children_async(self, path, watch=None, include_data=False):
  1068. """Asynchronously get a list of child nodes of a path. Takes
  1069. the same arguments as :meth:`get_children`.
  1070. :rtype: :class:`~kazoo.interfaces.IAsyncResult`
  1071. """
  1072. if not isinstance(path, str):
  1073. raise TypeError("Invalid type for 'path' (string expected)")
  1074. if watch and not callable(watch):
  1075. raise TypeError("Invalid type for 'watch' (must be a callable)")
  1076. if not isinstance(include_data, bool):
  1077. raise TypeError("Invalid type for 'include_data' (bool expected)")
  1078. async_result = self.handler.async_result()
  1079. if include_data:
  1080. req = GetChildren2(_prefix_root(self.chroot, path), watch)
  1081. else:
  1082. req = GetChildren(_prefix_root(self.chroot, path), watch)
  1083. self._call(req, async_result)
  1084. return async_result
  1085. def get_acls(self, path):
  1086. """Return the ACL and stat of the node of the given path.
  1087. :param path: Path of the node.
  1088. :returns: The ACL array of the given node and its
  1089. :class:`~kazoo.protocol.states.ZnodeStat`.
  1090. :rtype: tuple of (:class:`~kazoo.security.ACL` list,
  1091. :class:`~kazoo.protocol.states.ZnodeStat`)
  1092. :raises:
  1093. :exc:`~kazoo.exceptions.NoNodeError` if the node doesn't
  1094. exist.
  1095. :exc:`~kazoo.exceptions.ZookeeperError` if the server
  1096. returns a non-zero error code
  1097. .. versionadded:: 0.5
  1098. """
  1099. return self.get_acls_async(path).get()
  1100. def get_acls_async(self, path):
  1101. """Return the ACL and stat of the node of the given path. Takes
  1102. the same arguments as :meth:`get_acls`.
  1103. :rtype: :class:`~kazoo.interfaces.IAsyncResult`
  1104. """
  1105. if not isinstance(path, str):
  1106. raise TypeError("Invalid type for 'path' (string expected)")
  1107. async_result = self.handler.async_result()
  1108. self._call(GetACL(_prefix_root(self.chroot, path)), async_result)
  1109. return async_result
  1110. def set_acls(self, path, acls, version=-1):
  1111. """Set the ACL for the node of the given path.
  1112. Set the ACL for the node of the given path if such a node
  1113. exists and the given version matches the version of the node.
  1114. :param path: Path for the node.
  1115. :param acls: List of :class:`~kazoo.security.ACL` objects to
  1116. set.
  1117. :param version: The expected node version that must match.
  1118. :returns: The stat of the node.
  1119. :raises:
  1120. :exc:`~kazoo.exceptions.BadVersionError` if version doesn't
  1121. match.
  1122. :exc:`~kazoo.exceptions.NoNodeError` if the node doesn't
  1123. exist.
  1124. :exc:`~kazoo.exceptions.InvalidACLError` if the ACL is
  1125. invalid.
  1126. :exc:`~kazoo.exceptions.ZookeeperError` if the server
  1127. returns a non-zero error code.
  1128. .. versionadded:: 0.5
  1129. """
  1130. return self.set_acls_async(path, acls, version).get()
  1131. def set_acls_async(self, path, acls, version=-1):
  1132. """Set the ACL for the node of the given path. Takes the same
  1133. arguments as :meth:`set_acls`.
  1134. :rtype: :class:`~kazoo.interfaces.IAsyncResult`
  1135. """
  1136. if not isinstance(path, str):
  1137. raise TypeError("Invalid type for 'path' (string expected)")
  1138. if isinstance(acls, ACL) or not isinstance(acls, (tuple, list)):
  1139. raise TypeError(
  1140. "Invalid type for 'acl' (acl must be a tuple/list" " of ACL's)"
  1141. )
  1142. if not isinstance(version, int):
  1143. raise TypeError("Invalid type for 'version' (int expected)")
  1144. async_result = self.handler.async_result()
  1145. self._call(
  1146. SetACL(_prefix_root(self.chroot, path), acls, version),
  1147. async_result,
  1148. )
  1149. return async_result
  1150. def set(self, path, value, version=-1):
  1151. """Set the value of a node.
  1152. If the version of the node being updated is newer than the
  1153. supplied version (and the supplied version is not -1), a
  1154. BadVersionError will be raised.
  1155. This operation, if successful, will trigger all the watches on
  1156. the node of the given path left by :meth:`get` API calls.
  1157. The maximum allowable size of the value is 1 MB. Values larger
  1158. than this will cause a ZookeeperError to be raised.
  1159. :param path: Path of node.
  1160. :param value: New data value.
  1161. :param version: Version of node being updated, or -1.
  1162. :returns: Updated :class:`~kazoo.protocol.states.ZnodeStat` of
  1163. the node.
  1164. :raises:
  1165. :exc:`~kazoo.exceptions.BadVersionError` if version doesn't
  1166. match.
  1167. :exc:`~kazoo.exceptions.NoNodeError` if the node doesn't
  1168. exist.
  1169. :exc:`~kazoo.exceptions.ZookeeperError` if the provided
  1170. value is too large.
  1171. :exc:`~kazoo.exceptions.ZookeeperError` if the server
  1172. returns a non-zero error code.
  1173. """
  1174. return self.set_async(path, value, version).get()
  1175. def set_async(self, path, value, version=-1):
  1176. """Set the value of a node. Takes the same arguments as
  1177. :meth:`set`.
  1178. :rtype: :class:`~kazoo.interfaces.IAsyncResult`
  1179. """
  1180. if not isinstance(path, str):
  1181. raise TypeError("Invalid type for 'path' (string expected)")
  1182. if value is not None and not isinstance(value, bytes):
  1183. raise TypeError("Invalid type for 'value' (must be a byte string)")
  1184. if not isinstance(version, int):
  1185. raise TypeError("Invalid type for 'version' (int expected)")
  1186. async_result = self.handler.async_result()
  1187. self._call(
  1188. SetData(_prefix_root(self.chroot, path), value, version),
  1189. async_result,
  1190. )
  1191. return async_result
  1192. def transaction(self):
  1193. """Create and return a :class:`TransactionRequest` object
  1194. Creates a :class:`TransactionRequest` object. A Transaction can
  1195. consist of multiple operations which can be committed as a
  1196. single atomic unit. Either all of the operations will succeed
  1197. or none of them.
  1198. :returns: A TransactionRequest.
  1199. :rtype: :class:`TransactionRequest`
  1200. .. versionadded:: 0.6
  1201. Requires Zookeeper 3.4+
  1202. """
  1203. return TransactionRequest(self)
  1204. def delete(self, path, version=-1, recursive=False):
  1205. """Delete a node.
  1206. The call will succeed if such a node exists, and the given
  1207. version matches the node's version (if the given version is -1,
  1208. the default, it matches any node's versions).
  1209. This operation, if successful, will trigger all the watches on
  1210. the node of the given path left by `exists` API calls, and the
  1211. watches on the parent node left by `get_children` API calls.
  1212. :param path: Path of node to delete.
  1213. :param version: Version of node to delete, or -1 for any.
  1214. :param recursive: Recursively delete node and all its children,
  1215. defaults to False.
  1216. :type recursive: bool
  1217. :raises:
  1218. :exc:`~kazoo.exceptions.BadVersionError` if version doesn't
  1219. match.
  1220. :exc:`~kazoo.exceptions.NoNodeError` if the node doesn't
  1221. exist.
  1222. :exc:`~kazoo.exceptions.NotEmptyError` if the node has
  1223. children.
  1224. :exc:`~kazoo.exceptions.ZookeeperError` if the server
  1225. returns a non-zero error code.
  1226. """
  1227. if not isinstance(recursive, bool):
  1228. raise TypeError("Invalid type for 'recursive' (bool expected)")
  1229. if recursive:
  1230. return self._delete_recursive(path)
  1231. else:
  1232. return self.delete_async(path, version).get()
  1233. def delete_async(self, path, version=-1):
  1234. """Asynchronously delete a node. Takes the same arguments as
  1235. :meth:`delete`, with the exception of `recursive`.
  1236. :rtype: :class:`~kazoo.interfaces.IAsyncResult`
  1237. """
  1238. if not isinstance(path, str):
  1239. raise TypeError("Invalid type for 'path' (string expected)")
  1240. if not isinstance(version, int):
  1241. raise TypeError("Invalid type for 'version' (int expected)")
  1242. async_result = self.handler.async_result()
  1243. self._call(
  1244. Delete(_prefix_root(self.chroot, path), version), async_result
  1245. )
  1246. return async_result
  1247. def _delete_recursive(self, path):
  1248. try:
  1249. children = self.get_children(path)
  1250. except NoNodeError:
  1251. return True
  1252. if children:
  1253. for child in children:
  1254. if path == "/":
  1255. child_path = path + child
  1256. else:
  1257. child_path = path + "/" + child
  1258. self._delete_recursive(child_path)
  1259. try:
  1260. self.delete(path)
  1261. except NoNodeError: # pragma: nocover
  1262. pass
  1263. def reconfig(self, joining, leaving, new_members, from_config=-1):
  1264. """Reconfig a cluster.
  1265. This call will succeed if the cluster was reconfigured accordingly.
  1266. :param joining: a comma separated list of servers being added
  1267. (see example for format) (incremental reconfiguration)
  1268. :param leaving: a comma separated list of servers being removed
  1269. (see example for format) (incremental reconfiguration)
  1270. :param new_members: a comma separated list of new membership
  1271. (non-incremental reconfiguration)
  1272. :param from_config: version of the current configuration (optional -
  1273. causes reconfiguration to throw an exception if
  1274. configuration is no longer current)
  1275. :type from_config: int
  1276. :returns:
  1277. Tuple (value, :class:`~kazoo.protocol.states.ZnodeStat`) of
  1278. node.
  1279. :rtype: tuple
  1280. Basic Example:
  1281. .. code-block:: python
  1282. zk = KazooClient()
  1283. zk.start()
  1284. # first add an observer (incremental reconfiguration)
  1285. joining = 'server.100=10.0.0.10:2889:3888:observer;0.0.0.0:2181'
  1286. data, _ = zk.reconfig(
  1287. joining=joining, leaving=None, new_members=None)
  1288. # wait and then remove it (just by using its id) (incremental)
  1289. data, _ = zk.reconfig(joining=None, leaving='100',
  1290. new_members=None)
  1291. # now do a full change of the cluster (non-incremental)
  1292. new = [
  1293. 'server.100=10.0.0.10:2889:3888:observer;0.0.0.0:2181',
  1294. 'server.100=10.0.0.11:2889:3888:observer;0.0.0.0:2181',
  1295. 'server.100=10.0.0.12:2889:3888:observer;0.0.0.0:2181',
  1296. ]
  1297. data, _ = zk.reconfig(
  1298. joining=None, leaving=None, new_members=','.join(new))
  1299. zk.stop()
  1300. :raises:
  1301. :exc:`~kazoo.exceptions.UnimplementedError` if not supported.
  1302. :exc:`~kazoo.exceptions.NewConfigNoQuorumError` if no quorum of new
  1303. config is connected and up-to-date with the leader of last
  1304. commmitted config - try invoking reconfiguration after new servers
  1305. are connected and synced.
  1306. :exc:`~kazoo.exceptions.ReconfigInProcessError` if another
  1307. reconfiguration is in progress.
  1308. :exc:`~kazoo.exceptions.BadVersionError` if version doesn't
  1309. match.
  1310. :exc:`~kazoo.exceptions.BadArgumentsError` if any of the given
  1311. lists of servers has a bad format.
  1312. :exc:`~kazoo.exceptions.ZookeeperError` if the server
  1313. returns a non-zero error code.
  1314. """
  1315. result = self.reconfig_async(
  1316. joining, leaving, new_members, from_config
  1317. )
  1318. return result.get()
  1319. def reconfig_async(self, joining, leaving, new_members, from_config):
  1320. """Asynchronously reconfig a cluster. Takes the same arguments as
  1321. :meth:`reconfig`.
  1322. :rtype: :class:`~kazoo.interfaces.IAsyncResult`
  1323. """
  1324. if joining and not isinstance(joining, str):
  1325. raise TypeError("Invalid type for 'joining' (string expected)")
  1326. if leaving and not isinstance(leaving, str):
  1327. raise TypeError("Invalid type for 'leaving' (string expected)")
  1328. if new_members and not isinstance(new_members, str):
  1329. raise TypeError(
  1330. "Invalid type for 'new_members' (string " "expected)"
  1331. )
  1332. if not isinstance(from_config, int):
  1333. raise TypeError("Invalid type for 'from_config' (int expected)")
  1334. async_result = self.handler.async_result()
  1335. reconfig = Reconfig(joining, leaving, new_members, from_config)
  1336. self._call(reconfig, async_result)
  1337. return async_result
  1338. class TransactionRequest(object):
  1339. """A Zookeeper Transaction Request
  1340. A Transaction provides a builder object that can be used to
  1341. construct and commit an atomic set of operations. The transaction
  1342. must be committed before its sent.
  1343. Transactions are not thread-safe and should not be accessed from
  1344. multiple threads at once.
  1345. .. note::
  1346. The ``committed`` attribute only indicates whether this
  1347. transaction has been sent to Zookeeper and is used to prevent
  1348. duplicate commits of the same transaction. The result should be
  1349. checked to determine if the transaction executed as desired.
  1350. .. versionadded:: 0.6
  1351. Requires Zookeeper 3.4+
  1352. """
  1353. def __init__(self, client):
  1354. self.client = client
  1355. self.operations = []
  1356. self.committed = False
  1357. def create(
  1358. self, path, value=b"", acl=None, ephemeral=False, sequence=False
  1359. ):
  1360. """Add a create ZNode to the transaction. Takes the same
  1361. arguments as :meth:`KazooClient.create`, with the exception
  1362. of `makepath`.
  1363. :returns: None
  1364. """
  1365. if acl is None and self.client.default_acl:
  1366. acl = self.client.default_acl
  1367. if not isinstance(path, str):
  1368. raise TypeError("Invalid type for 'path' (string expected)")
  1369. if acl and not isinstance(acl, (tuple, list)):
  1370. raise TypeError(
  1371. "Invalid type for 'acl' (acl must be a tuple/list" " of ACL's"
  1372. )
  1373. if not isinstance(value, bytes):
  1374. raise TypeError("Invalid type for 'value' (must be a byte string)")
  1375. if not isinstance(ephemeral, bool):
  1376. raise TypeError("Invalid type for 'ephemeral' (bool expected)")
  1377. if not isinstance(sequence, bool):
  1378. raise TypeError("Invalid type for 'sequence' (bool expected)")
  1379. flags = 0
  1380. if ephemeral:
  1381. flags |= 1
  1382. if sequence:
  1383. flags |= 2
  1384. if acl is None:
  1385. acl = OPEN_ACL_UNSAFE
  1386. self._add(
  1387. Create(_prefix_root(self.client.chroot, path), value, acl, flags),
  1388. None,
  1389. )
  1390. def delete(self, path, version=-1):
  1391. """Add a delete ZNode to the transaction. Takes the same
  1392. arguments as :meth:`KazooClient.delete`, with the exception of
  1393. `recursive`.
  1394. """
  1395. if not isinstance(path, str):
  1396. raise TypeError("Invalid type for 'path' (string expected)")
  1397. if not isinstance(version, int):
  1398. raise TypeError("Invalid type for 'version' (int expected)")
  1399. self._add(Delete(_prefix_root(self.client.chroot, path), version))
  1400. def set_data(self, path, value, version=-1):
  1401. """Add a set ZNode value to the transaction. Takes the same
  1402. arguments as :meth:`KazooClient.set`.
  1403. """
  1404. if not isinstance(path, str):
  1405. raise TypeError("Invalid type for 'path' (string expected)")
  1406. if not isinstance(value, bytes):
  1407. raise TypeError("Invalid type for 'value' (must be a byte string)")
  1408. if not isinstance(version, int):
  1409. raise TypeError("Invalid type for 'version' (int expected)")
  1410. self._add(
  1411. SetData(_prefix_root(self.client.chroot, path), value, version)
  1412. )
  1413. def check(self, path, version):
  1414. """Add a Check Version to the transaction.
  1415. This command will fail and abort a transaction if the path
  1416. does not match the specified version.
  1417. """
  1418. if not isinstance(path, str):
  1419. raise TypeError("Invalid type for 'path' (string expected)")
  1420. if not isinstance(version, int):
  1421. raise TypeError("Invalid type for 'version' (int expected)")
  1422. self._add(
  1423. CheckVersion(_prefix_root(self.client.chroot, path), version)
  1424. )
  1425. def commit_async(self):
  1426. """Commit the transaction asynchronously.
  1427. :rtype: :class:`~kazoo.interfaces.IAsyncResult`
  1428. """
  1429. self._check_tx_state()
  1430. self.committed = True
  1431. async_object = self.client.handler.async_result()
  1432. self.client._call(Transaction(self.operations), async_object)
  1433. return async_object
  1434. def commit(self):
  1435. """Commit the transaction.
  1436. :returns: A list of the results for each operation in the
  1437. transaction.
  1438. """
  1439. return self.commit_async().get()
  1440. def __enter__(self):
  1441. return self
  1442. def __exit__(self, exc_type, exc_value, exc_tb):
  1443. """Commit and cleanup accumulated transaction data."""
  1444. if not exc_type:
  1445. self.commit()
  1446. def _check_tx_state(self):
  1447. if self.committed:
  1448. raise ValueError("Transaction already committed")
  1449. def _add(self, request, post_processor=None):
  1450. self._check_tx_state()
  1451. self.client.logger.log(BLATHER, "Added %r to %r", request, self)
  1452. self.operations.append(request)