diff --git a/lib/charms/zookeeper/v0/client.py b/lib/charms/zookeeper/v0/client.py index e7f03cc5..d0b2d78a 100644 --- a/lib/charms/zookeeper/v0/client.py +++ b/lib/charms/zookeeper/v0/client.py @@ -74,7 +74,7 @@ def update_cluster(new_members: List[str], event: EventBase) -> None: # Increment this PATCH version before using `charmcraft publish-lib` or reset # to 0 if you are raising the major API version -LIBPATCH = 6 +LIBPATCH = 8 logger = logging.getLogger(__name__) @@ -101,6 +101,12 @@ class QuorumLeaderNotFoundError(Exception): pass +class NoUnitFoundError(Exception): + """Generic exception for when there are no running zk unit in the app.""" + + pass + + class ZooKeeperManager: """Handler for performing ZK commands.""" @@ -114,6 +120,7 @@ def __init__( keyfile_path: Optional[str] = "", keyfile_password: Optional[str] = "", certfile_path: Optional[str] = "", + read_only: bool = True, ): self.hosts = hosts self.username = username @@ -123,12 +130,21 @@ def __init__( self.keyfile_path = keyfile_path self.keyfile_password = keyfile_password self.certfile_path = certfile_path - self.leader = "" + self.zk_host = "" + self.read_only = read_only - try: - self.leader = self.get_leader() - except RetryError: - raise QuorumLeaderNotFoundError("quorum leader not found") + if not read_only: + try: + self.zk_host = self.get_leader() + except RetryError: + raise QuorumLeaderNotFoundError("quorum leader not found") + + else: + try: + self.zk_host = self.get_any_unit() + + except RetryError: + raise NoUnitFoundError @retry( wait=wait_fixed(3), @@ -170,6 +186,35 @@ def get_leader(self) -> str: return leader or "" + @retry( + wait=wait_fixed(3), + stop=stop_after_attempt(2), + retry=retry_if_not_result(lambda result: True if result else False), + ) + def get_any_unit(self) -> str: + any_host = None + for host in self.hosts: + try: + with ZooKeeperClient( + host=host, + client_port=self.client_port, + username=self.username, + password=self.password, + use_ssl=self.use_ssl, + keyfile_path=self.keyfile_path, + keyfile_password=self.keyfile_password, + certfile_path=self.certfile_path, + ) as zk: + response = zk.srvr + if response: + any_host = host + break + except KazooTimeoutError: # in the case of having a dead unit in relation data + logger.debug(f"TIMEOUT - {host}") + continue + + return any_host or "" + @property def server_members(self) -> Set[str]: """The current members within the ZooKeeper quorum. @@ -179,7 +224,7 @@ def server_members(self) -> Set[str]: e.g {"server.1=10.141.78.207:2888:3888:participant;0.0.0.0:2181"} """ with ZooKeeperClient( - host=self.leader, + host=self.zk_host, client_port=self.client_port, username=self.username, password=self.password, @@ -200,7 +245,7 @@ def config_version(self) -> int: The zookeeper config version decoded from base16 """ with ZooKeeperClient( - host=self.leader, + host=self.zk_host, client_port=self.client_port, username=self.username, password=self.password, @@ -221,7 +266,7 @@ def members_syncing(self) -> bool: True if any members are syncing. Otherwise False. """ with ZooKeeperClient( - host=self.leader, + host=self.zk_host, client_port=self.client_port, username=self.username, password=self.password, @@ -305,7 +350,7 @@ def add_members(self, members: Iterable[str]) -> None: # specific connection to leader with ZooKeeperClient( - host=self.leader, + host=self.zk_host, client_port=self.client_port, username=self.username, password=self.password, @@ -330,7 +375,7 @@ def remove_members(self, members: Iterable[str]) -> None: for member in members: member_id = re.findall(r"server.([0-9]+)", member)[0] with ZooKeeperClient( - host=self.leader, + host=self.zk_host, client_port=self.client_port, username=self.username, password=self.password, @@ -356,7 +401,7 @@ def leader_znodes(self, path: str) -> Set[str]: Set of all nested child zNodes """ with ZooKeeperClient( - host=self.leader, + host=self.zk_host, client_port=self.client_port, username=self.username, password=self.password, @@ -369,7 +414,7 @@ def leader_znodes(self, path: str) -> Set[str]: return all_znode_children - def create_znode_leader(self, path: str, acls: List[ACL]) -> None: + def create_znode_leader(self, path: str, acls: List[ACL] | None = None) -> None: """Creates a new zNode on the current quorum leader with given ACLs. Args: @@ -377,7 +422,7 @@ def create_znode_leader(self, path: str, acls: List[ACL]) -> None: acls: the ACLs to be set on that path """ with ZooKeeperClient( - host=self.leader, + host=self.zk_host, client_port=self.client_port, username=self.username, password=self.password, @@ -388,7 +433,7 @@ def create_znode_leader(self, path: str, acls: List[ACL]) -> None: ) as zk: zk.create_znode(path=path, acls=acls) - def set_acls_znode_leader(self, path: str, acls: List[ACL]) -> None: + def set_acls_znode_leader(self, path: str, acls: List[ACL] | None = None) -> None: """Updates ACLs for an existing zNode on the current quorum leader. Args: @@ -396,7 +441,7 @@ def set_acls_znode_leader(self, path: str, acls: List[ACL]) -> None: acls: the new ACLs to be set on that path """ with ZooKeeperClient( - host=self.leader, + host=self.zk_host, client_port=self.client_port, username=self.username, password=self.password, @@ -414,7 +459,7 @@ def delete_znode_leader(self, path: str) -> None: path: the zNode path to delete """ with ZooKeeperClient( - host=self.leader, + host=self.zk_host, client_port=self.client_port, username=self.username, password=self.password, @@ -432,7 +477,7 @@ def get_version(self) -> str: String of ZooKeeper service version """ with ZooKeeperClient( - host=self.leader, + host=self.zk_host, client_port=self.client_port, username=self.username, password=self.password, @@ -577,7 +622,7 @@ def delete_znode(self, path: str) -> None: return self.client.delete(path, recursive=True) - def create_znode(self, path: str, acls: List[ACL]) -> None: + def create_znode(self, path: str, acls: List[ACL] | None = None) -> None: """Create new znode. Args: @@ -599,7 +644,7 @@ def get_acls(self, path: str) -> List[ACL]: return acl_list if acl_list else [] - def set_acls(self, path: str, acls: List[ACL]) -> None: + def set_acls(self, path: str, acls: List[ACL] | None = None) -> None: """Sets acls for a desired znode path. Args: diff --git a/src/core/models.py b/src/core/models.py index bd6eacf1..bcfc3cd4 100644 --- a/src/core/models.py +++ b/src/core/models.py @@ -740,8 +740,15 @@ def zookeeper_connected(self) -> bool: @property def zookeeper_version(self) -> str: """Get running zookeeper version.""" - hosts = self.endpoints.split(",") - zk = ZooKeeperManager(hosts=hosts, username=self.username, password=self.password) + hosts = [host.split(":")[0] for host in self.endpoints.split(",")] + port = next(iter([int(host.split(":")[1]) for host in self.endpoints.split(",")]), 2181) + zk = ZooKeeperManager( + hosts=hosts, + client_port=port, + username=self.username, + password=self.password, + use_ssl=self.tls, + ) return zk.get_version() @@ -755,10 +762,17 @@ def zookeeper_version(self) -> str: def broker_active(self) -> bool: """Checks if broker id is recognised as active by ZooKeeper.""" broker_id = self.data_interface.local_unit.name.split("/")[1] - hosts = self.endpoints.split(",") + hosts = [host.split(":")[0] for host in self.endpoints.split(",")] + port = next(iter([int(host.split(":")[1]) for host in self.endpoints.split(",")]), 2181) path = f"{self.database}/brokers/ids/" - zk = ZooKeeperManager(hosts=hosts, username=self.username, password=self.password) + zk = ZooKeeperManager( + hosts=hosts, + client_port=port, + username=self.username, + password=self.password, + use_ssl=self.tls, + ) try: brokers = zk.leader_znodes(path=path) except (