|
7 | 7 | import stat |
8 | 8 | import subprocess |
9 | 9 | import tempfile |
10 | | -import time |
11 | 10 | import typing as t |
12 | 11 | from os import geteuid |
13 | 12 | from pathlib import Path |
@@ -202,135 +201,6 @@ def get_runtime_cache_database_paths(run_pioreactor_cache: Path) -> list[Path]: |
202 | 201 | ] |
203 | 202 |
|
204 | 203 |
|
205 | | -def format_repair_sample(paths: list[Path], limit: int = 5) -> str: |
206 | | - sample = ", ".join(str(path) for path in paths[:limit]) |
207 | | - remaining_count = len(paths) - limit |
208 | | - if remaining_count > 0: |
209 | | - return f"{sample}, and {remaining_count} more" |
210 | | - return sample |
211 | | - |
212 | | - |
213 | | -def collect_dot_pioreactor_repair_changes(dot_pioreactor_root: Path) -> dict[str, list[Path]]: |
214 | | - expected_uid, expected_gid, _expected_owner = get_expected_dot_pioreactor_uid_gid() |
215 | | - changes: dict[str, list[Path]] = { |
216 | | - "ownership": [], |
217 | | - "group_writable": [], |
218 | | - "setgid": [], |
219 | | - } |
220 | | - |
221 | | - for path in [dot_pioreactor_root, *dot_pioreactor_root.rglob("*")]: |
222 | | - path_stat = path.lstat() |
223 | | - mode = path_stat.st_mode |
224 | | - |
225 | | - if expected_uid is not None and expected_gid is not None: |
226 | | - if path_stat.st_uid != expected_uid or path_stat.st_gid != expected_gid: |
227 | | - changes["ownership"].append(path) |
228 | | - |
229 | | - if path == dot_pioreactor_root or stat.S_ISDIR(mode) or stat.S_ISREG(mode): |
230 | | - if not mode & stat.S_IWGRP: |
231 | | - changes["group_writable"].append(path) |
232 | | - |
233 | | - if stat.S_ISDIR(mode) and not mode & stat.S_ISGID: |
234 | | - changes["setgid"].append(path) |
235 | | - |
236 | | - return changes |
237 | | - |
238 | | - |
239 | | -def collect_runtime_repair_changes() -> dict[str, list[Path]]: |
240 | | - expected_uid, expected_gid, _expected_owner = get_expected_dot_pioreactor_uid_gid() |
241 | | - run_pioreactor_root = Path("/run/pioreactor") |
242 | | - run_pioreactor_exports = run_pioreactor_root / "exports" |
243 | | - run_pioreactor_cache = run_pioreactor_root / "cache" |
244 | | - runtime_cache_databases = get_runtime_cache_database_paths(run_pioreactor_cache) |
245 | | - now = time.time() |
246 | | - |
247 | | - changes: dict[str, list[Path]] = { |
248 | | - "directories": [], |
249 | | - "stale_exports": [], |
250 | | - "cache_databases": [], |
251 | | - "cache_sidecars": [], |
252 | | - } |
253 | | - |
254 | | - for path, expected_mode in ( |
255 | | - (run_pioreactor_root, 0o2775), |
256 | | - (run_pioreactor_exports, 0o2775), |
257 | | - (run_pioreactor_cache, 0o2770), |
258 | | - ): |
259 | | - if not path.exists(): |
260 | | - changes["directories"].append(path) |
261 | | - continue |
262 | | - |
263 | | - path_stat = path.lstat() |
264 | | - current_mode = stat.S_IMODE(path_stat.st_mode) |
265 | | - owner_or_group_differs = ( |
266 | | - expected_uid is not None |
267 | | - and expected_gid is not None |
268 | | - and (path_stat.st_uid != expected_uid or path_stat.st_gid != expected_gid) |
269 | | - ) |
270 | | - if current_mode != expected_mode or owner_or_group_differs: |
271 | | - changes["directories"].append(path) |
272 | | - |
273 | | - if run_pioreactor_exports.exists(): |
274 | | - for path in run_pioreactor_exports.iterdir(): |
275 | | - path_stat = path.lstat() |
276 | | - if not stat.S_ISREG(path_stat.st_mode): |
277 | | - continue |
278 | | - |
279 | | - age_minutes = (now - path_stat.st_mtime) / 60 |
280 | | - if (path.name.endswith((".tmp", ".csv")) and age_minutes > 30) or ( |
281 | | - path.name.startswith("export_") and path.name.endswith(".zip") and age_minutes > 360 |
282 | | - ): |
283 | | - changes["stale_exports"].append(path) |
284 | | - |
285 | | - for path in runtime_cache_databases: |
286 | | - if not path.exists(): |
287 | | - changes["cache_databases"].append(path) |
288 | | - continue |
289 | | - |
290 | | - path_stat = path.lstat() |
291 | | - current_mode = stat.S_IMODE(path_stat.st_mode) |
292 | | - owner_or_group_differs = ( |
293 | | - expected_uid is not None |
294 | | - and expected_gid is not None |
295 | | - and (path_stat.st_uid != expected_uid or path_stat.st_gid != expected_gid) |
296 | | - ) |
297 | | - if current_mode != 0o660 or owner_or_group_differs: |
298 | | - changes["cache_databases"].append(path) |
299 | | - |
300 | | - if run_pioreactor_cache.exists(): |
301 | | - for path in run_pioreactor_cache.iterdir(): |
302 | | - path_stat = path.lstat() |
303 | | - if not stat.S_ISREG(path_stat.st_mode) or not path.name.endswith(("-wal", "-shm")): |
304 | | - continue |
305 | | - |
306 | | - current_mode = stat.S_IMODE(path_stat.st_mode) |
307 | | - owner_or_group_differs = ( |
308 | | - expected_uid is not None |
309 | | - and expected_gid is not None |
310 | | - and (path_stat.st_uid != expected_uid or path_stat.st_gid != expected_gid) |
311 | | - ) |
312 | | - if current_mode != 0o660 or owner_or_group_differs: |
313 | | - changes["cache_sidecars"].append(path) |
314 | | - |
315 | | - return changes |
316 | | - |
317 | | - |
318 | | -def echo_repair_changes(title: str, changes: dict[str, list[Path]], labels: dict[str, str]) -> None: |
319 | | - changed_anything = False |
320 | | - for key, label in labels.items(): |
321 | | - paths = changes[key] |
322 | | - if not paths: |
323 | | - continue |
324 | | - |
325 | | - if not changed_anything: |
326 | | - click.echo(f"{title}:") |
327 | | - changed_anything = True |
328 | | - click.echo(f" - {label}: {len(paths)} path(s): {format_repair_sample(paths)}") |
329 | | - |
330 | | - if not changed_anything: |
331 | | - click.echo(f"{title}: no changes needed.") |
332 | | - |
333 | | - |
334 | 204 | def build_runtime_repair_commands(tools: dict[str, str]) -> list[list[str]]: |
335 | 205 | run_pioreactor_root = Path("/run/pioreactor") |
336 | 206 | run_pioreactor_exports = run_pioreactor_root / "exports" |
@@ -1694,40 +1564,19 @@ def repair() -> None: |
1694 | 1564 | raise click.ClickException(f"{dot_pioreactor_root} does not exist.") |
1695 | 1565 |
|
1696 | 1566 | tools = require_repair_command_paths("sudo", "find", "chown", "chmod", "install", "touch", "systemctl") |
1697 | | - dot_pioreactor_changes = collect_dot_pioreactor_repair_changes(dot_pioreactor_root) |
1698 | | - runtime_changes = collect_runtime_repair_changes() |
1699 | 1567 | dot_pioreactor_commands = build_dot_pioreactor_repair_commands(dot_pioreactor_root, tools) |
1700 | 1568 | runtime_commands = build_runtime_repair_commands(tools) |
1701 | 1569 | command_groups = [ |
1702 | | - dot_pioreactor_commands, |
1703 | | - runtime_commands[:2], |
1704 | | - runtime_commands[2:3], |
1705 | | - runtime_commands[3:], |
| 1570 | + (dot_pioreactor_commands, f"Repaired ownership and group permissions for {dot_pioreactor_root}."), |
| 1571 | + (runtime_commands[:2], "Repaired runtime directories under /run/pioreactor."), |
| 1572 | + (runtime_commands[2:3], "Cleared stale runtime export artifacts from /run/pioreactor/exports."), |
| 1573 | + (runtime_commands[3:], "Repaired runtime cache files under /run/pioreactor/cache."), |
1706 | 1574 | ] |
1707 | 1575 |
|
1708 | | - for commands in command_groups: |
| 1576 | + for commands, message in command_groups: |
1709 | 1577 | for command in commands: |
1710 | 1578 | subprocess.run(command, check=True) |
1711 | | - |
1712 | | - echo_repair_changes( |
1713 | | - f"Ownership and group permissions for {dot_pioreactor_root}", |
1714 | | - dot_pioreactor_changes, |
1715 | | - { |
1716 | | - "ownership": "fixed owner/group", |
1717 | | - "group_writable": "added group-write permission", |
1718 | | - "setgid": "added setgid permission", |
1719 | | - }, |
1720 | | - ) |
1721 | | - echo_repair_changes( |
1722 | | - "Runtime files under /run/pioreactor", |
1723 | | - runtime_changes, |
1724 | | - { |
1725 | | - "directories": "created or repaired runtime directories", |
1726 | | - "stale_exports": "removed stale export artifacts", |
1727 | | - "cache_databases": "created or repaired cache databases", |
1728 | | - "cache_sidecars": "repaired cache sidecar files", |
1729 | | - }, |
1730 | | - ) |
| 1579 | + click.echo(message) |
1731 | 1580 |
|
1732 | 1581 | restarted_services = restart_inactive_pioreactor_web_services(tools) |
1733 | 1582 | if restarted_services: |
|
0 commit comments