Skip to content

Commit 19b97eb

Browse files
authored
[DPE-5126] Use admin server instead of the 4lw commands (#154)
1 parent a135ba9 commit 19b97eb

7 files changed

Lines changed: 148 additions & 154 deletions

File tree

poetry.lock

Lines changed: 70 additions & 67 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

pyproject.toml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -118,7 +118,7 @@ mccabe.max-complexity = 10
118118

119119
[tool.pyright]
120120
include = ["src"]
121-
extraPaths = ["./lib", "src"]
121+
extraPaths = ["./lib"]
122122
pythonVersion = "3.10"
123123
pythonPlatform = "All"
124124
typeCheckingMode = "basic"

requirements.txt

Lines changed: 9 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -1,24 +1,24 @@
11
anyio==4.4.0 ; python_version >= "3.10" and python_version < "4.0"
2-
boto3-stubs[s3]==1.35.15 ; python_version >= "3.10" and python_version < "4.0"
3-
boto3==1.35.15 ; python_version >= "3.10" and python_version < "4.0"
4-
botocore-stubs==1.35.15 ; python_version >= "3.10" and python_version < "4.0"
5-
botocore==1.35.15 ; python_version >= "3.10" and python_version < "4.0"
2+
boto3-stubs[s3]==1.35.19 ; python_version >= "3.10" and python_version < "4.0"
3+
boto3==1.35.19 ; python_version >= "3.10" and python_version < "4.0"
4+
botocore-stubs==1.35.19 ; python_version >= "3.10" and python_version < "4.0"
5+
botocore==1.35.19 ; python_version >= "3.10" and python_version < "4.0"
66
certifi==2024.8.30 ; python_version >= "3.10" and python_version < "4.0"
77
cffi==1.17.1 ; python_version >= "3.10" and python_version < "4.0" and platform_python_implementation != "PyPy"
8-
cosl==0.0.32 ; python_version >= "3.10" and python_version < "4.0"
8+
cosl==0.0.33 ; python_version >= "3.10" and python_version < "4.0"
99
cryptography==43.0.1 ; python_version >= "3.10" and python_version < "4.0"
1010
exceptiongroup==1.2.2 ; python_version >= "3.10" and python_version < "3.11"
1111
h11==0.14.0 ; python_version >= "3.10" and python_version < "4.0"
1212
httpcore==1.0.5 ; python_version >= "3.10" and python_version < "4.0"
1313
httpx==0.27.2 ; python_version >= "3.10" and python_version < "4.0"
14-
idna==3.8 ; python_version >= "3.10" and python_version < "4.0"
14+
idna==3.10 ; python_version >= "3.10" and python_version < "4.0"
1515
jmespath==1.0.1 ; python_version >= "3.10" and python_version < "4.0"
1616
kazoo==2.9.0 ; python_version >= "3.10" and python_version < "4.0"
17-
lightkube-models==1.30.0.8 ; python_version >= "3.10" and python_version < "4.0"
17+
lightkube-models==1.31.1.8 ; python_version >= "3.10" and python_version < "4.0"
1818
lightkube==0.15.4 ; python_version >= "3.10" and python_version < "4.0"
1919
markdown-it-py==3.0.0 ; python_version >= "3.10" and python_version < "4.0"
2020
mdurl==0.1.2 ; python_version >= "3.10" and python_version < "4.0"
21-
mypy-boto3-s3==1.35.2 ; python_version >= "3.10" and python_version < "4.0"
21+
mypy-boto3-s3==1.35.16 ; python_version >= "3.10" and python_version < "4.0"
2222
ops==2.16.1 ; python_version >= "3.10" and python_version < "4.0"
2323
pure-sasl==0.6.2 ; python_version >= "3.10" and python_version < "4.0"
2424
pycparser==2.22 ; python_version >= "3.10" and python_version < "4.0" and platform_python_implementation != "PyPy"
@@ -36,5 +36,5 @@ tenacity==9.0.0 ; python_version >= "3.10" and python_version < "4.0"
3636
types-awscrt==0.21.5 ; python_version >= "3.10" and python_version < "4.0"
3737
types-s3transfer==0.10.2 ; python_version >= "3.10" and python_version < "4.0"
3838
typing-extensions==4.12.2 ; python_version >= "3.10" and python_version < "4.0"
39-
urllib3==2.2.2 ; python_version >= "3.10" and python_version < "4.0"
39+
urllib3==2.2.3 ; python_version >= "3.10" and python_version < "4.0"
4040
websocket-client==1.8.0 ; python_version >= "3.10" and python_version < "4.0"

src/workload.py

Lines changed: 16 additions & 38 deletions
Original file line numberDiff line numberDiff line change
@@ -5,20 +5,18 @@
55
"""Implementation of WorkloadBase for running on VMs."""
66
import logging
77
import os
8-
import re
98
import secrets
109
import shutil
1110
import string
1211
import subprocess
13-
from subprocess import CalledProcessError
1412

13+
import httpx
1514
from charms.operator_libs_linux.v1 import snap
16-
from ops.pebble import ExecError
1715
from tenacity import retry, retry_if_result, stop_after_attempt, wait_fixed
1816
from typing_extensions import override
1917

2018
from core.workload import WorkloadBase
21-
from literals import CHARMED_ZOOKEEPER_SNAP_REVISION, CLIENT_PORT
19+
from literals import ADMIN_SERVER_PORT, CHARMED_ZOOKEEPER_SNAP_REVISION
2220

2321
logger = logging.getLogger(__name__)
2422

@@ -109,27 +107,14 @@ def healthy(self) -> bool:
109107
if not self.alive:
110108
return False
111109

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

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

124-
try:
125-
ruok_response = self.exec(command=timeout + ruok)
126-
if not ruok_response or "imok" not in ruok_response:
127-
return False
128-
129-
srvr_response = self.exec(command=timeout + srvr)
130-
if not srvr_response or "not currently serving requests" in srvr_response:
131-
return False
132-
except (ExecError, CalledProcessError):
117+
if response.json().get("error", None):
133118
return False
134119

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

154139
return True
155-
except (snap.SnapError) as e:
140+
except snap.SnapError as e:
156141
logger.error(str(e))
157142
return False
158143

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

173-
stat = [
174-
"bash",
175-
"-c",
176-
f"echo 'stat' | (exec 3<>/dev/tcp/localhost/{CLIENT_PORT}; cat >&3; cat <&3; exec 3<&-; )",
177-
]
178-
179158
try:
180-
stat_response = self.exec(command=stat)
181-
if not stat_response:
182-
return ""
159+
response = httpx.get(f"http://localhost:{ADMIN_SERVER_PORT}/commands/srvr", timeout=10)
160+
response.raise_for_status()
183161

184-
matcher = re.search(r"(?P<version>\d\.\d\.\d)", stat_response)
185-
version = matcher.group("version") if matcher else ""
186-
187-
except (ExecError, CalledProcessError):
162+
except httpx.HTTPError:
188163
return ""
189164

190-
return version
165+
if not (full_version := response.json().get("version", "")):
166+
return full_version
167+
else:
168+
return full_version.split("-")[0]

tests/integration/ha/helpers.py

Lines changed: 24 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,6 @@
44

55
import json
66
import logging
7-
import re
87
import subprocess
98
from pathlib import Path
109
from typing import Dict, Optional
@@ -14,6 +13,8 @@
1413
from pytest_operator.plugin import OpsTest
1514
from tenacity import RetryError, Retrying, retry, stop_after_attempt, wait_fixed
1615

16+
from literals import ADMIN_SERVER_PORT
17+
1718
logger = logging.getLogger(__name__)
1819

1920
METADATA = yaml.safe_load(Path("./metadata.yaml").read_text())
@@ -50,28 +51,26 @@ async def wait_idle(ops_test, apps: list[str] = [APP_NAME], units: int = 3) -> N
5051
stop=stop_after_attempt(60),
5152
reraise=True,
5253
)
53-
def srvr(host: str) -> dict:
54-
"""Calls srvr 4lw command to specified host.
54+
def srvr(model_full_name: str, unit: str) -> dict:
55+
"""Calls srvr 4lw command to specified unit.
5556
5657
Args:
57-
host: ZooKeeper address and port to issue srvr 4lw command to
58+
model_full_name: Current test model
59+
unit: ZooKeeper unit to issue srvr 4lw command to
5860
5961
Returns:
6062
Dict of srvr command output key/values
6163
"""
6264
response = subprocess.check_output(
63-
f"echo srvr | nc {host} 2181", stderr=subprocess.PIPE, shell=True, universal_newlines=True
65+
f"JUJU_MODEL={model_full_name} juju ssh {unit} sudo -i 'curl localhost:{ADMIN_SERVER_PORT}/commands/srvr -m 10'",
66+
stderr=subprocess.PIPE,
67+
shell=True,
68+
universal_newlines=True,
6469
)
6570

6671
assert response, "ZooKeeper not running"
6772

68-
result = {}
69-
for item in response.splitlines():
70-
k = re.split(": ", item)[0]
71-
v = re.split(": ", item)[1]
72-
result[k] = v
73-
74-
return result
73+
return json.loads(response)
7574

7675

7776
def get_hosts_from_status(
@@ -176,13 +175,17 @@ def get_leader_name(ops_test: OpsTest, hosts: str, app_name: str = APP_NAME) ->
176175
String of unit name of the ZooKeeper quorum leader
177176
"""
178177
for host in hosts.split(","):
178+
unit_name = get_unit_name_from_host(ops_test, host, app_name)
179179
try:
180-
mode = srvr(host.split(":")[0])["Mode"]
180+
mode = (
181+
srvr(ops_test.model_full_name, unit_name)
182+
.get("server_stats", {})
183+
.get("server_state", "")
184+
)
181185
except subprocess.CalledProcessError: # unit is down
182186
continue
183187
if mode == "leader":
184-
leader_name = get_unit_name_from_host(ops_test, host, app_name)
185-
return leader_name
188+
return unit_name
186189

187190
return ""
188191

@@ -481,8 +484,12 @@ def ping_servers(ops_test: OpsTest) -> bool:
481484
True if all units are in quorum. Otherwise False
482485
"""
483486
for unit in ops_test.model.applications[APP_NAME].units:
484-
host = unit.public_address
485-
mode = srvr(host)["Mode"]
487+
srvr_response = srvr(ops_test.model_full_name, unit.name)
488+
489+
if srvr_response.get("error", None):
490+
return False
491+
492+
mode = srvr_response.get("server_stats", {}).get("server_state", "")
486493
if mode not in ["leader", "follower"]:
487494
return False
488495

tests/integration/helpers.py

Lines changed: 16 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,7 @@
1414
from pytest_operator.plugin import OpsTest
1515

1616
from core.workload import ZKPaths
17+
from literals import ADMIN_SERVER_PORT
1718

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

133134

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

144148
assert response, "ZooKeeper not running"
145149

146-
result = {}
147-
for item in response.splitlines():
148-
k = re.split(": ", item)[0]
149-
v = re.split(": ", item)[1]
150-
result[k] = v
151-
152-
return result
150+
return json.loads(response)
153151

154152

155153
async def ping_servers(ops_test: OpsTest) -> bool:
156154
for unit in ops_test.model.applications[APP_NAME].units:
157-
host = unit.public_address
158-
mode = srvr(host)["Mode"]
155+
srvr_response = srvr(ops_test.model_full_name, unit.name)
156+
157+
if srvr_response.get("error", None):
158+
return False
159+
160+
mode = srvr_response.get("server_stats", {}).get("server_state", "")
159161
if mode not in ["leader", "follower"]:
160162
return False
161163

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

165167
async def correct_version_running(ops_test: OpsTest, expected_version: str) -> bool:
166168
for unit in ops_test.model.applications[APP_NAME].units:
167-
host = unit.public_address
168-
if expected_version not in srvr(host)["Zookeeper version"]:
169+
srvr_response = srvr(ops_test.model_full_name, unit.name)
170+
171+
if expected_version not in srvr_response.get("version", ""):
169172
return False
170173

171174
return True

tests/unit/test_charm.py

Lines changed: 12 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,7 @@
88
from pathlib import Path
99
from unittest.mock import DEFAULT, Mock, PropertyMock, patch
1010

11+
import httpx
1112
import pytest
1213
import yaml
1314
from charms.rolling_ops.v0.rollingops import WaitingStatus
@@ -1055,16 +1056,18 @@ def test_update_relation_data(harness):
10551056

10561057

10571058
def test_workload_version_is_setted(harness, monkeypatch):
1058-
output_install = (
1059-
"Zookeeper version: 3.8.1-ubuntu0-${mvngit.commit.id}, built on 2023-11-21 15:33 UTC"
1060-
)
1061-
output_changed = (
1062-
"Zookeeper version: 3.8.2-ubuntu0-${mvngit.commit.id}, built on 2023-11-21 15:33 UTC"
1063-
)
1059+
output_install = {
1060+
"version": "3.8.1-ubuntu0-${mvngit.commit.id}, built on 2023-11-21 15:33 UTC"
1061+
}
1062+
output_changed = {
1063+
"version": "3.8.2-ubuntu0-${mvngit.commit.id}, built on 2023-11-21 15:33 UTC"
1064+
}
1065+
response_mock = Mock()
1066+
response_mock.return_value.json.side_effect = [output_install, output_changed]
10641067
monkeypatch.setattr(
1065-
harness.charm.workload,
1066-
"exec",
1067-
Mock(side_effect=[output_install, output_changed]),
1068+
httpx,
1069+
"get",
1070+
response_mock,
10681071
)
10691072
monkeypatch.setattr(harness.charm.workload, "install", Mock(return_value=True))
10701073
monkeypatch.setattr(harness.charm.workload, "healthy", Mock(return_value=True))

0 commit comments

Comments
 (0)