Skip to content

Commit

Permalink
[DPE-5126] Use admin server instead of the 4lw commands (#154)
Browse files Browse the repository at this point in the history
  • Loading branch information
Batalex authored Sep 16, 2024
1 parent a135ba9 commit 19b97eb
Show file tree
Hide file tree
Showing 7 changed files with 148 additions and 154 deletions.
137 changes: 70 additions & 67 deletions poetry.lock

Large diffs are not rendered by default.

2 changes: 1 addition & 1 deletion pyproject.toml
Original file line number Diff line number Diff line change
Expand Up @@ -118,7 +118,7 @@ mccabe.max-complexity = 10

[tool.pyright]
include = ["src"]
extraPaths = ["./lib", "src"]
extraPaths = ["./lib"]
pythonVersion = "3.10"
pythonPlatform = "All"
typeCheckingMode = "basic"
Expand Down
18 changes: 9 additions & 9 deletions requirements.txt
Original file line number Diff line number Diff line change
@@ -1,24 +1,24 @@
anyio==4.4.0 ; python_version >= "3.10" and python_version < "4.0"
boto3-stubs[s3]==1.35.15 ; python_version >= "3.10" and python_version < "4.0"
boto3==1.35.15 ; python_version >= "3.10" and python_version < "4.0"
botocore-stubs==1.35.15 ; python_version >= "3.10" and python_version < "4.0"
botocore==1.35.15 ; python_version >= "3.10" and python_version < "4.0"
boto3-stubs[s3]==1.35.19 ; python_version >= "3.10" and python_version < "4.0"
boto3==1.35.19 ; python_version >= "3.10" and python_version < "4.0"
botocore-stubs==1.35.19 ; python_version >= "3.10" and python_version < "4.0"
botocore==1.35.19 ; python_version >= "3.10" and python_version < "4.0"
certifi==2024.8.30 ; python_version >= "3.10" and python_version < "4.0"
cffi==1.17.1 ; python_version >= "3.10" and python_version < "4.0" and platform_python_implementation != "PyPy"
cosl==0.0.32 ; python_version >= "3.10" and python_version < "4.0"
cosl==0.0.33 ; python_version >= "3.10" and python_version < "4.0"
cryptography==43.0.1 ; python_version >= "3.10" and python_version < "4.0"
exceptiongroup==1.2.2 ; python_version >= "3.10" and python_version < "3.11"
h11==0.14.0 ; python_version >= "3.10" and python_version < "4.0"
httpcore==1.0.5 ; python_version >= "3.10" and python_version < "4.0"
httpx==0.27.2 ; python_version >= "3.10" and python_version < "4.0"
idna==3.8 ; python_version >= "3.10" and python_version < "4.0"
idna==3.10 ; python_version >= "3.10" and python_version < "4.0"
jmespath==1.0.1 ; python_version >= "3.10" and python_version < "4.0"
kazoo==2.9.0 ; python_version >= "3.10" and python_version < "4.0"
lightkube-models==1.30.0.8 ; python_version >= "3.10" and python_version < "4.0"
lightkube-models==1.31.1.8 ; python_version >= "3.10" and python_version < "4.0"
lightkube==0.15.4 ; python_version >= "3.10" and python_version < "4.0"
markdown-it-py==3.0.0 ; python_version >= "3.10" and python_version < "4.0"
mdurl==0.1.2 ; python_version >= "3.10" and python_version < "4.0"
mypy-boto3-s3==1.35.2 ; python_version >= "3.10" and python_version < "4.0"
mypy-boto3-s3==1.35.16 ; python_version >= "3.10" and python_version < "4.0"
ops==2.16.1 ; python_version >= "3.10" and python_version < "4.0"
pure-sasl==0.6.2 ; python_version >= "3.10" and python_version < "4.0"
pycparser==2.22 ; python_version >= "3.10" and python_version < "4.0" and platform_python_implementation != "PyPy"
Expand All @@ -36,5 +36,5 @@ tenacity==9.0.0 ; python_version >= "3.10" and python_version < "4.0"
types-awscrt==0.21.5 ; python_version >= "3.10" and python_version < "4.0"
types-s3transfer==0.10.2 ; python_version >= "3.10" and python_version < "4.0"
typing-extensions==4.12.2 ; python_version >= "3.10" and python_version < "4.0"
urllib3==2.2.2 ; python_version >= "3.10" and python_version < "4.0"
urllib3==2.2.3 ; python_version >= "3.10" and python_version < "4.0"
websocket-client==1.8.0 ; python_version >= "3.10" and python_version < "4.0"
54 changes: 16 additions & 38 deletions src/workload.py
Original file line number Diff line number Diff line change
Expand Up @@ -5,20 +5,18 @@
"""Implementation of WorkloadBase for running on VMs."""
import logging
import os
import re
import secrets
import shutil
import string
import subprocess
from subprocess import CalledProcessError

import httpx
from charms.operator_libs_linux.v1 import snap
from ops.pebble import ExecError
from tenacity import retry, retry_if_result, stop_after_attempt, wait_fixed
from typing_extensions import override

from core.workload import WorkloadBase
from literals import CHARMED_ZOOKEEPER_SNAP_REVISION, CLIENT_PORT
from literals import ADMIN_SERVER_PORT, CHARMED_ZOOKEEPER_SNAP_REVISION

logger = logging.getLogger(__name__)

Expand Down Expand Up @@ -109,27 +107,14 @@ def healthy(self) -> bool:
if not self.alive:
return False

# netcat isn't a default utility, so can't guarantee it's on the charm containers
# this ugly hack avoids needing netcat
bash_netcat = (
f"echo '4lw' | (exec 3<>/dev/tcp/localhost/{CLIENT_PORT}; cat >&3; cat <&3; exec 3<&-)"
)
ruok = [bash_netcat.replace("4lw", "ruok")]
srvr = [bash_netcat.replace("4lw", "srvr")]
try:
response = httpx.get(f"http://localhost:{ADMIN_SERVER_PORT}/commands/ruok", timeout=10)
response.raise_for_status()

# timeout needed as it can sometimes hang forever if there's a problem
# for example when the endpoint is unreachable
timeout = ["timeout", "10s", "bash", "-c"]
except httpx.HTTPError:
return False

try:
ruok_response = self.exec(command=timeout + ruok)
if not ruok_response or "imok" not in ruok_response:
return False

srvr_response = self.exec(command=timeout + srvr)
if not srvr_response or "not currently serving requests" in srvr_response:
return False
except (ExecError, CalledProcessError):
if response.json().get("error", None):
return False

return True
Expand All @@ -152,7 +137,7 @@ def install(self) -> bool:
self.zookeeper.hold()

return True
except (snap.SnapError) as e:
except snap.SnapError as e:
logger.error(str(e))
return False

Expand All @@ -170,21 +155,14 @@ def get_version(self) -> str:
if not self.healthy:
return ""

stat = [
"bash",
"-c",
f"echo 'stat' | (exec 3<>/dev/tcp/localhost/{CLIENT_PORT}; cat >&3; cat <&3; exec 3<&-; )",
]

try:
stat_response = self.exec(command=stat)
if not stat_response:
return ""
response = httpx.get(f"http://localhost:{ADMIN_SERVER_PORT}/commands/srvr", timeout=10)
response.raise_for_status()

matcher = re.search(r"(?P<version>\d\.\d\.\d)", stat_response)
version = matcher.group("version") if matcher else ""

except (ExecError, CalledProcessError):
except httpx.HTTPError:
return ""

return version
if not (full_version := response.json().get("version", "")):
return full_version
else:
return full_version.split("-")[0]
41 changes: 24 additions & 17 deletions tests/integration/ha/helpers.py
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,6 @@

import json
import logging
import re
import subprocess
from pathlib import Path
from typing import Dict, Optional
Expand All @@ -14,6 +13,8 @@
from pytest_operator.plugin import OpsTest
from tenacity import RetryError, Retrying, retry, stop_after_attempt, wait_fixed

from literals import ADMIN_SERVER_PORT

logger = logging.getLogger(__name__)

METADATA = yaml.safe_load(Path("./metadata.yaml").read_text())
Expand Down Expand Up @@ -50,28 +51,26 @@ async def wait_idle(ops_test, apps: list[str] = [APP_NAME], units: int = 3) -> N
stop=stop_after_attempt(60),
reraise=True,
)
def srvr(host: str) -> dict:
"""Calls srvr 4lw command to specified host.
def srvr(model_full_name: str, unit: str) -> dict:
"""Calls srvr 4lw command to specified unit.
Args:
host: ZooKeeper address and port to issue srvr 4lw command to
model_full_name: Current test model
unit: ZooKeeper unit to issue srvr 4lw command to
Returns:
Dict of srvr command output key/values
"""
response = subprocess.check_output(
f"echo srvr | nc {host} 2181", stderr=subprocess.PIPE, shell=True, universal_newlines=True
f"JUJU_MODEL={model_full_name} juju ssh {unit} sudo -i 'curl localhost:{ADMIN_SERVER_PORT}/commands/srvr -m 10'",
stderr=subprocess.PIPE,
shell=True,
universal_newlines=True,
)

assert response, "ZooKeeper not running"

result = {}
for item in response.splitlines():
k = re.split(": ", item)[0]
v = re.split(": ", item)[1]
result[k] = v

return result
return json.loads(response)


def get_hosts_from_status(
Expand Down Expand Up @@ -176,13 +175,17 @@ def get_leader_name(ops_test: OpsTest, hosts: str, app_name: str = APP_NAME) ->
String of unit name of the ZooKeeper quorum leader
"""
for host in hosts.split(","):
unit_name = get_unit_name_from_host(ops_test, host, app_name)
try:
mode = srvr(host.split(":")[0])["Mode"]
mode = (
srvr(ops_test.model_full_name, unit_name)
.get("server_stats", {})
.get("server_state", "")
)
except subprocess.CalledProcessError: # unit is down
continue
if mode == "leader":
leader_name = get_unit_name_from_host(ops_test, host, app_name)
return leader_name
return unit_name

return ""

Expand Down Expand Up @@ -481,8 +484,12 @@ def ping_servers(ops_test: OpsTest) -> bool:
True if all units are in quorum. Otherwise False
"""
for unit in ops_test.model.applications[APP_NAME].units:
host = unit.public_address
mode = srvr(host)["Mode"]
srvr_response = srvr(ops_test.model_full_name, unit.name)

if srvr_response.get("error", None):
return False

mode = srvr_response.get("server_stats", {}).get("server_state", "")
if mode not in ["leader", "follower"]:
return False

Expand Down
29 changes: 16 additions & 13 deletions tests/integration/helpers.py
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@
from pytest_operator.plugin import OpsTest

from core.workload import ZKPaths
from literals import ADMIN_SERVER_PORT

METADATA = yaml.safe_load(Path("./metadata.yaml").read_text())
APP_NAME = METADATA["name"]
Expand Down Expand Up @@ -131,31 +132,32 @@ def check_key(host: str, password: str, username: str = "super") -> None:
raise KeyError


def srvr(host: str) -> Dict:
def srvr(model_full_name: str, unit: str) -> dict:
"""Retrieves attributes returned from the 'srvr' 4lw command.
Specifically for this test, we are interested in the "Mode" of the ZK server,
which allows checking quorum leadership and follower active status.
"""
response = check_output(
f"echo srvr | nc {host} 2181", stderr=PIPE, shell=True, universal_newlines=True
f"JUJU_MODEL={model_full_name} juju ssh {unit} sudo -i 'curl localhost:{ADMIN_SERVER_PORT}/commands/srvr -m 10'",
stderr=PIPE,
shell=True,
universal_newlines=True,
)

assert response, "ZooKeeper not running"

result = {}
for item in response.splitlines():
k = re.split(": ", item)[0]
v = re.split(": ", item)[1]
result[k] = v

return result
return json.loads(response)


async def ping_servers(ops_test: OpsTest) -> bool:
for unit in ops_test.model.applications[APP_NAME].units:
host = unit.public_address
mode = srvr(host)["Mode"]
srvr_response = srvr(ops_test.model_full_name, unit.name)

if srvr_response.get("error", None):
return False

mode = srvr_response.get("server_stats", {}).get("server_state", "")
if mode not in ["leader", "follower"]:
return False

Expand All @@ -164,8 +166,9 @@ async def ping_servers(ops_test: OpsTest) -> bool:

async def correct_version_running(ops_test: OpsTest, expected_version: str) -> bool:
for unit in ops_test.model.applications[APP_NAME].units:
host = unit.public_address
if expected_version not in srvr(host)["Zookeeper version"]:
srvr_response = srvr(ops_test.model_full_name, unit.name)

if expected_version not in srvr_response.get("version", ""):
return False

return True
Expand Down
21 changes: 12 additions & 9 deletions tests/unit/test_charm.py
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@
from pathlib import Path
from unittest.mock import DEFAULT, Mock, PropertyMock, patch

import httpx
import pytest
import yaml
from charms.rolling_ops.v0.rollingops import WaitingStatus
Expand Down Expand Up @@ -1055,16 +1056,18 @@ def test_update_relation_data(harness):


def test_workload_version_is_setted(harness, monkeypatch):
output_install = (
"Zookeeper version: 3.8.1-ubuntu0-${mvngit.commit.id}, built on 2023-11-21 15:33 UTC"
)
output_changed = (
"Zookeeper version: 3.8.2-ubuntu0-${mvngit.commit.id}, built on 2023-11-21 15:33 UTC"
)
output_install = {
"version": "3.8.1-ubuntu0-${mvngit.commit.id}, built on 2023-11-21 15:33 UTC"
}
output_changed = {
"version": "3.8.2-ubuntu0-${mvngit.commit.id}, built on 2023-11-21 15:33 UTC"
}
response_mock = Mock()
response_mock.return_value.json.side_effect = [output_install, output_changed]
monkeypatch.setattr(
harness.charm.workload,
"exec",
Mock(side_effect=[output_install, output_changed]),
httpx,
"get",
response_mock,
)
monkeypatch.setattr(harness.charm.workload, "install", Mock(return_value=True))
monkeypatch.setattr(harness.charm.workload, "healthy", Mock(return_value=True))
Expand Down

0 comments on commit 19b97eb

Please sign in to comment.