Skip to content

Commit 4b74fd5

Browse files
committed
charm: rework everything to only have one single charm with more configuration
1 parent 4b866d1 commit 4b74fd5

28 files changed

+879
-1209
lines changed

.github/workflows/build-and-test-charms.yaml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -105,7 +105,7 @@ jobs:
105105
working-directory: charms/${{ matrix.charm }}
106106

107107
- name: Test charm
108-
run: uv run pytest tests/ -vv --log-level=INFO
108+
run: uv run --all-extras pytest tests/ -vv --log-level=INFO
109109
working-directory: charms/${{ matrix.charm }}
110110

111111
- name: Upload charm artifact

.gitignore

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,9 @@
1+
# Charm
2+
*.charm
3+
14
# Spread
25
.spread*
6+
37
# Byte-compiled / optimized / DLL files
48
__pycache__/
59
*.py[cod]

charm/charm.py

Lines changed: 79 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,79 @@
1+
#!/usr/bin/env python3
2+
# Copyright 2025 Skia
3+
# See LICENSE file for licensing details.
4+
5+
"""Charm the Error Tracker."""
6+
7+
import logging
8+
9+
import ops
10+
from charms.haproxy.v1.haproxy_route import HaproxyRouteRequirer
11+
12+
from errortracker import ErrorTracker
13+
14+
logger = logging.getLogger(__name__)
15+
16+
17+
class ErrorTrackerCharm(ops.CharmBase):
18+
"""Charm the application."""
19+
20+
def __init__(self, *args):
21+
super().__init__(*args)
22+
self._error_tracker = ErrorTracker()
23+
self.route_daisy = HaproxyRouteRequirer(
24+
self,
25+
service="daisy",
26+
ports=[self._error_tracker.daisy_port],
27+
relation_name="route_daisy",
28+
)
29+
30+
self.framework.observe(self.on.start, self._on_start)
31+
self.framework.observe(self.on.install, self._on_install)
32+
self.framework.observe(self.on.config_changed, self._on_config_changed)
33+
34+
def _on_start(self, event: ops.StartEvent):
35+
"""Handle start event."""
36+
self.unit.status = ops.ActiveStatus()
37+
38+
def _on_install(self, event: ops.InstallEvent):
39+
"""Handle install event."""
40+
self.unit.status = ops.MaintenanceStatus("Installing the error tracker")
41+
try:
42+
self._error_tracker.install()
43+
except Exception as e:
44+
logger.error("Failed to install the Error Tracker: %s", str(e))
45+
self.unit.status = ops.BlockedStatus("Failed installing the Error Tracker")
46+
return
47+
48+
self.unit.status = ops.ActiveStatus("Ready")
49+
50+
def _on_config_changed(self, event: ops.ConfigChangedEvent):
51+
enable_daisy = self.config.get("enable_daisy")
52+
enable_retracer = self.config.get("enable_retracer")
53+
enable_timers = self.config.get("enable_timers")
54+
enable_web = self.config.get("enable_web")
55+
56+
config = self.config.get("configuration")
57+
58+
self._error_tracker.configure(config)
59+
60+
# TODO: the charms know how to enable components, but not disable them.
61+
# This is a bit annoying, but also doesn't have a very big impact in
62+
# practice. This charm has no configuration where it's supposed to store
63+
# data, so it's always very easy to remove a unit and recreate.
64+
if enable_daisy:
65+
self._error_tracker.configure_daisy()
66+
self.unit.set_ports(self._error_tracker.daisy_port)
67+
if enable_retracer:
68+
self._error_tracker.configure_retracer(self.config.get("retracer_failed_queue"))
69+
if enable_timers:
70+
self._error_tracker.configure_timers()
71+
if enable_web:
72+
self._error_tracker.configure_web()
73+
74+
self.unit.set_workload_version(self._error_tracker.get_version())
75+
self.unit.status = ops.ActiveStatus("Ready")
76+
77+
78+
if __name__ == "__main__": # pragma: nocover
79+
ops.main(ErrorTrackerCharm) # type: ignore

charm/errortracker.py

Lines changed: 224 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,224 @@
1+
import logging
2+
import shutil
3+
from pathlib import Path
4+
from subprocess import CalledProcessError, check_call, check_output
5+
6+
logger = logging.getLogger(__name__)
7+
8+
HOME = Path("~ubuntu").expanduser()
9+
REPO_LOCATION = HOME / "error-tracker"
10+
11+
12+
def setup_systemd_timer(unit_name, description, command, calendar):
13+
systemd_unit_location = Path("/") / "etc" / "systemd" / "system"
14+
systemd_unit_location.mkdir(parents=True, exist_ok=True)
15+
16+
(systemd_unit_location / f"{unit_name}.service").write_text(
17+
f"""
18+
[Unit]
19+
Description={description}
20+
21+
[Service]
22+
Type=oneshot
23+
User=ubuntu
24+
Environment=PYTHONPATH={HOME}/config
25+
ExecStart={command}
26+
"""
27+
)
28+
(systemd_unit_location / f"{unit_name}.timer").write_text(
29+
f"""
30+
[Unit]
31+
Description={description}
32+
33+
[Timer]
34+
OnCalendar={calendar}
35+
Persistent=true
36+
37+
[Install]
38+
WantedBy=timers.target
39+
"""
40+
)
41+
42+
check_call(["systemctl", "daemon-reload"])
43+
check_call(["systemctl", "enable", "--now", f"{unit_name}.timer"])
44+
45+
46+
class ErrorTracker:
47+
def __init__(self):
48+
self.enable_retracer = True
49+
self.enable_timers = True
50+
self.enable_daisy = True
51+
self.enable_web = True
52+
self.daisy_port = 8000
53+
54+
def install(self):
55+
self._install_deps()
56+
self._install_et()
57+
58+
def _install_et(self):
59+
shutil.copytree(".", REPO_LOCATION)
60+
check_call(["chown", "-R", "ubuntu:ubuntu", str(REPO_LOCATION)])
61+
62+
def get_version(self):
63+
"""Get the retracer version"""
64+
try:
65+
version = check_output(
66+
[
67+
"sudo",
68+
"-u",
69+
"ubuntu",
70+
"python3",
71+
"-c",
72+
"import errortracker; print(errortracker.__version__)",
73+
],
74+
cwd=REPO_LOCATION / "src",
75+
)
76+
return version.decode()
77+
except CalledProcessError as e:
78+
logger.error("Unable to get version (%d, %s)", e.returncode, e.stderr)
79+
return "unknown"
80+
81+
def _install_deps(self):
82+
try:
83+
check_call(["apt-get", "update", "-y"])
84+
check_call(
85+
[
86+
"apt-get",
87+
"install",
88+
"-y",
89+
"git",
90+
"python3-amqp",
91+
"python3-apport",
92+
"python3-apt",
93+
"python3-bson",
94+
"python3-cassandra",
95+
"python3-flask",
96+
"python3-swiftclient",
97+
]
98+
)
99+
except CalledProcessError as e:
100+
logger.debug("Package install failed with return code %d", e.returncode)
101+
return
102+
103+
def configure(self, config: str):
104+
config_location = REPO_LOCATION / "src"
105+
(config_location / "local_config.py").write_text(config)
106+
107+
def configure_daisy(self):
108+
logger.info("Configuring daisy")
109+
logger.info("Installing additional daisy dependencies")
110+
check_call(["apt-get", "install", "-y", "gunicorn"])
111+
systemd_unit_location = Path("/") / "etc" / "systemd" / "system"
112+
systemd_unit_location.mkdir(parents=True, exist_ok=True)
113+
(systemd_unit_location / "daisy.service").write_text(
114+
f"""
115+
[Unit]
116+
Description=Daisy
117+
After=network.target
118+
119+
[Service]
120+
User=ubuntu
121+
Group=ubuntu
122+
WorkingDirectory={REPO_LOCATION}/src
123+
ExecStart=gunicorn -c {REPO_LOCATION}/src/daisy/gunicorn_config.py 'daisy.app:app'
124+
Restart=always
125+
126+
[Install]
127+
WantedBy=multi-user.target
128+
"""
129+
)
130+
131+
check_call(["systemctl", "daemon-reload"])
132+
133+
logger.info("enabling systemd units")
134+
check_call(["systemctl", "enable", "daisy"])
135+
136+
logger.info("restarting systemd units")
137+
check_call(["systemctl", "restart", "daisy"])
138+
139+
def configure_retracer(self, retracer_failed_queue: bool):
140+
logger.info("Configuring retracer")
141+
failed = "--failed" if retracer_failed_queue else ""
142+
# Work around https://bugs.launchpad.net/ubuntu/+source/gdb/+bug/1818918
143+
# Apport will not be run as root, thus the included workaround here will hit ENOPERM
144+
(Path("/") / "usr" / "lib" / "debug" / ".dwz").mkdir(parents=True, exist_ok=True)
145+
logger.info("Installing additional retracer dependencies")
146+
check_call(
147+
[
148+
"apt-get",
149+
"install",
150+
"-y",
151+
"apport-retrace",
152+
"ubuntu-dbgsym-keyring",
153+
]
154+
)
155+
156+
logger.info("Configuring retracer systemd units")
157+
systemd_unit_location = Path("/") / "etc" / "systemd" / "system"
158+
systemd_unit_location.mkdir(parents=True, exist_ok=True)
159+
(systemd_unit_location / "[email protected]").write_text(
160+
f"""
161+
[Unit]
162+
Description=Retracer
163+
164+
[Service]
165+
User=ubuntu
166+
Group=ubuntu
167+
Environment=PYTHONPATH={REPO_LOCATION}/src
168+
ExecStart=python3 {REPO_LOCATION}/src/retracer.py --config-dir {REPO_LOCATION}/src/retracer/config --sandbox-dir {HOME}/cache --cleanup-debs --cleanup-sandbox --architecture %i --core-storage {HOME}/var --verbose {failed}
169+
Restart=on-failure
170+
171+
[Install]
172+
WantedBy=multi-user.target
173+
"""
174+
)
175+
176+
check_call(["systemctl", "daemon-reload"])
177+
178+
logger.info("enabling systemd units")
179+
check_call(["systemctl", "enable", "retracer@amd64"])
180+
check_call(["systemctl", "enable", "retracer@arm64"])
181+
check_call(["systemctl", "enable", "retracer@armhf"])
182+
check_call(["systemctl", "enable", "retracer@i386"])
183+
184+
logger.info("restarting systemd units")
185+
check_call(["systemctl", "restart", "retracer@amd64"])
186+
check_call(["systemctl", "restart", "retracer@arm64"])
187+
check_call(["systemctl", "restart", "retracer@armhf"])
188+
check_call(["systemctl", "restart", "retracer@i386"])
189+
190+
def configure_timers(self):
191+
logger.info("Configuring timers")
192+
setup_systemd_timer(
193+
"et-unique-users-daily-update",
194+
"Error Tracker - Unique users daily update",
195+
f"{REPO_LOCATION}/src/tools/unique_users_daily_update.py",
196+
"*-*-* 00:30:00", # every day at 00:30
197+
)
198+
setup_systemd_timer(
199+
"et-import-bugs",
200+
"Error Tracker - Import bugs",
201+
f"{REPO_LOCATION}/src/tools/import_bugs.py",
202+
"*-*-* 01,04,07,10,13,16,19,22:00:00", # every three hours
203+
)
204+
setup_systemd_timer(
205+
"et-import-team-packages",
206+
"Error Tracker - Import team packages",
207+
f"{REPO_LOCATION}/src/tools/import_team_packages.py",
208+
"*-*-* 02:30:00", # every day at 02:30
209+
)
210+
setup_systemd_timer(
211+
"et-swift-corrupt-core-check",
212+
"Error Tracker - Swift - Check for corrupt cores",
213+
f"{REPO_LOCATION}/src/tools/swift_corrupt_core_check.py",
214+
"*-*-* 04:30:00", # every day at 04:30
215+
)
216+
setup_systemd_timer(
217+
"et-swift-handle-old-cores",
218+
"Error Tracker - Swift - Handle old cores",
219+
f"{REPO_LOCATION}/src/tools/swift_handle_old_cores.py",
220+
"*-*-* *:45:00", # every hour at minute 45
221+
)
222+
223+
def configure_web(self):
224+
logger.info("Configuring web")

0 commit comments

Comments
 (0)