Skip to content

Commit 61dadc0

Browse files
committed
charm: rework everything to only have one single charm with more configuration
1 parent 7c714c1 commit 61dadc0

26 files changed

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

charm/errortracker.py

Lines changed: 223 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,223 @@
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+
53+
def install(self):
54+
self._install_deps()
55+
self._install_et()
56+
57+
def _install_et(self):
58+
shutil.copytree(".", REPO_LOCATION)
59+
check_call(["chown", "-R", "ubuntu:ubuntu", str(REPO_LOCATION)])
60+
61+
def get_version(self):
62+
"""Get the retracer version"""
63+
try:
64+
version = check_output(
65+
[
66+
"sudo",
67+
"-u",
68+
"ubuntu",
69+
"python3",
70+
"-c",
71+
"import errortracker; print(errortracker.__version__)",
72+
],
73+
cwd=REPO_LOCATION / "src",
74+
)
75+
return version.decode()
76+
except CalledProcessError as e:
77+
logger.error("Unable to get version (%d, %s)", e.returncode, e.stderr)
78+
return "unknown"
79+
80+
def _install_deps(self):
81+
try:
82+
check_call(["apt-get", "update", "-y"])
83+
check_call(
84+
[
85+
"apt-get",
86+
"install",
87+
"-y",
88+
"git",
89+
"python3-amqp",
90+
"python3-apport",
91+
"python3-apt",
92+
"python3-bson",
93+
"python3-cassandra",
94+
"python3-flask",
95+
"python3-swiftclient",
96+
]
97+
)
98+
except CalledProcessError as e:
99+
logger.debug("Package install failed with return code %d", e.returncode)
100+
return
101+
102+
def configure(self, config: str):
103+
config_location = REPO_LOCATION / "src"
104+
(config_location / "local_config.py").write_text(config)
105+
106+
def configure_daisy(self):
107+
logger.info("Configuring daisy")
108+
logger.info("Installing additional daisy dependencies")
109+
check_call(["apt-get", "install", "-y", "gunicorn"])
110+
self.daisy_port = 5000
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+
118+
[Service]
119+
User=ubuntu
120+
Group=ubuntu
121+
Environment=PYTHONPATH={REPO_LOCATION}/src
122+
ExecStart=python3 {REPO_LOCATION}/src/daisy/app.py
123+
Restart=on-failure
124+
125+
[Install]
126+
WantedBy=multi-user.target
127+
"""
128+
)
129+
130+
check_call(["systemctl", "daemon-reload"])
131+
132+
logger.info("enabling systemd units")
133+
check_call(["systemctl", "enable", "daisy"])
134+
135+
logger.info("restarting systemd units")
136+
check_call(["systemctl", "restart", "daisy"])
137+
138+
def configure_retracer(self, retracer_failed_queue: bool):
139+
logger.info("Configuring retracer")
140+
failed = "--failed" if retracer_failed_queue else ""
141+
# Work around https://bugs.launchpad.net/ubuntu/+source/gdb/+bug/1818918
142+
# Apport will not be run as root, thus the included workaround here will hit ENOPERM
143+
(Path("/") / "usr" / "lib" / "debug" / ".dwz").mkdir(parents=True, exist_ok=True)
144+
logger.info("Installing additional retracer dependencies")
145+
check_call(
146+
[
147+
"apt-get",
148+
"install",
149+
"-y",
150+
"apport-retrace",
151+
"ubuntu-dbgsym-keyring",
152+
]
153+
)
154+
155+
logger.info("Configuring retracer systemd units")
156+
systemd_unit_location = Path("/") / "etc" / "systemd" / "system"
157+
systemd_unit_location.mkdir(parents=True, exist_ok=True)
158+
(systemd_unit_location / "[email protected]").write_text(
159+
f"""
160+
[Unit]
161+
Description=Retracer
162+
163+
[Service]
164+
User=ubuntu
165+
Group=ubuntu
166+
Environment=PYTHONPATH={REPO_LOCATION}/src
167+
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}
168+
Restart=on-failure
169+
170+
[Install]
171+
WantedBy=multi-user.target
172+
"""
173+
)
174+
175+
check_call(["systemctl", "daemon-reload"])
176+
177+
logger.info("enabling systemd units")
178+
check_call(["systemctl", "enable", "retracer@amd64"])
179+
check_call(["systemctl", "enable", "retracer@arm64"])
180+
check_call(["systemctl", "enable", "retracer@armhf"])
181+
check_call(["systemctl", "enable", "retracer@i386"])
182+
183+
logger.info("restarting systemd units")
184+
check_call(["systemctl", "restart", "retracer@amd64"])
185+
check_call(["systemctl", "restart", "retracer@arm64"])
186+
check_call(["systemctl", "restart", "retracer@armhf"])
187+
check_call(["systemctl", "restart", "retracer@i386"])
188+
189+
def configure_timers(self):
190+
logger.info("Configuring timers")
191+
setup_systemd_timer(
192+
"et-unique-users-daily-update",
193+
"Error Tracker - Unique users daily update",
194+
f"{REPO_LOCATION}/src/tools/unique_users_daily_update.py",
195+
"*-*-* 00:30:00", # every day at 00:30
196+
)
197+
setup_systemd_timer(
198+
"et-import-bugs",
199+
"Error Tracker - Import bugs",
200+
f"{REPO_LOCATION}/src/tools/import_bugs.py",
201+
"*-*-* 01,04,07,10,13,16,19,22:00:00", # every three hours
202+
)
203+
setup_systemd_timer(
204+
"et-import-team-packages",
205+
"Error Tracker - Import team packages",
206+
f"{REPO_LOCATION}/src/tools/import_team_packages.py",
207+
"*-*-* 02:30:00", # every day at 02:30
208+
)
209+
setup_systemd_timer(
210+
"et-swift-corrupt-core-check",
211+
"Error Tracker - Swift - Check for corrupt cores",
212+
f"{REPO_LOCATION}/src/tools/swift_corrupt_core_check.py",
213+
"*-*-* 04:30:00", # every day at 04:30
214+
)
215+
setup_systemd_timer(
216+
"et-swift-handle-old-cores",
217+
"Error Tracker - Swift - Handle old cores",
218+
f"{REPO_LOCATION}/src/tools/swift_handle_old_cores.py",
219+
"*-*-* *:45:00", # every hour at minute 45
220+
)
221+
222+
def configure_web(self):
223+
logger.info("Configuring web")

0 commit comments

Comments
 (0)