Skip to content

Commit 94770dd

Browse files
more
1 parent 27bc30a commit 94770dd

6 files changed

Lines changed: 90 additions & 264 deletions

File tree

core/pioreactor/cli/pio.py

Lines changed: 6 additions & 157 deletions
Original file line numberDiff line numberDiff line change
@@ -7,7 +7,6 @@
77
import stat
88
import subprocess
99
import tempfile
10-
import time
1110
import typing as t
1211
from os import geteuid
1312
from pathlib import Path
@@ -202,135 +201,6 @@ def get_runtime_cache_database_paths(run_pioreactor_cache: Path) -> list[Path]:
202201
]
203202

204203

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-
334204
def build_runtime_repair_commands(tools: dict[str, str]) -> list[list[str]]:
335205
run_pioreactor_root = Path("/run/pioreactor")
336206
run_pioreactor_exports = run_pioreactor_root / "exports"
@@ -1694,40 +1564,19 @@ def repair() -> None:
16941564
raise click.ClickException(f"{dot_pioreactor_root} does not exist.")
16951565

16961566
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()
16991567
dot_pioreactor_commands = build_dot_pioreactor_repair_commands(dot_pioreactor_root, tools)
17001568
runtime_commands = build_runtime_repair_commands(tools)
17011569
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."),
17061574
]
17071575

1708-
for commands in command_groups:
1576+
for commands, message in command_groups:
17091577
for command in commands:
17101578
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)
17311580

17321581
restarted_services = restart_inactive_pioreactor_web_services(tools)
17331582
if restarted_services:

core/pioreactor/web/api.py

Lines changed: 9 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -4073,16 +4073,22 @@ def get_worker_model_and_metadata(pioreactor_unit: str) -> ResponseReturnValue:
40734073
one=True,
40744074
)
40754075
if result is None:
4076-
# If the worker is not found, return an error
40774076
return abort_with(
40784077
404,
40794078
"Worker not found",
40804079
cause="Worker name not found in database.",
40814080
remediation="Check the unit name or add the worker to the inventory.",
40824081
)
4082+
4083+
assert isinstance(result, dict)
4084+
if result["model_version"] or not result["model_name"]:
4085+
return abort_with(
4086+
404,
4087+
f"Model not set for worker {pioreactor_unit}.",
4088+
cause="Model not set in database.",
4089+
remediation="Set the model of this worker.",
4090+
)
40834091
else:
4084-
assert isinstance(result, dict)
4085-
# If the worker is found, return the model and metadata
40864092
return attach_cache_control(
40874093
jsonify(
40884094
{

core/pioreactor/web/utils.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -225,7 +225,7 @@ def load_background_job_descriptors(
225225
for file in files:
226226
try:
227227
decoded_yaml = yaml_decode(file.read_bytes(), type=structs.BackgroundJobDescriptor)
228-
validate_background_job_descriptor(decoded_yaml)
228+
# validate_background_job_descriptor(decoded_yaml) TODO: uncomment me for next update.sh release.
229229
if decoded_yaml.job_name in parsed_yaml and report_error is not None:
230230
report_error(f"Descriptor {file.name} overrides job {decoded_yaml.job_name}.")
231231
parsed_yaml[decoded_yaml.job_name] = decoded_yaml

core/tests/test_cli.py

Lines changed: 4 additions & 67 deletions
Original file line numberDiff line numberDiff line change
@@ -576,23 +576,6 @@ def test_pio_repair_runs_dot_pioreactor_and_runtime_permission_commands(
576576

577577
monkeypatch.setenv("DOT_PIOREACTOR", str(dot_pioreactor))
578578
monkeypatch.setattr("shutil.which", lambda name: f"/usr/bin/{name}")
579-
monkeypatch.setattr(
580-
"pioreactor.cli.pio.collect_dot_pioreactor_repair_changes",
581-
lambda _root: {
582-
"ownership": [dot_pioreactor / "config.ini"],
583-
"group_writable": [dot_pioreactor, dot_pioreactor / "storage"],
584-
"setgid": [dot_pioreactor / "storage"],
585-
},
586-
)
587-
monkeypatch.setattr(
588-
"pioreactor.cli.pio.collect_runtime_repair_changes",
589-
lambda: {
590-
"directories": [Path("/run/pioreactor/cache")],
591-
"stale_exports": [Path("/run/pioreactor/exports/export_old.zip")],
592-
"cache_databases": [Path("/run/pioreactor/cache/huey.db")],
593-
"cache_sidecars": [Path("/run/pioreactor/cache/huey.db-wal")],
594-
},
595-
)
596579

597580
class DummyResult:
598581
def __init__(self, returncode: int = 0) -> None:
@@ -763,20 +746,10 @@ def record_run(command: list[str], check: bool) -> DummyResult:
763746
"restart",
764747
"pioreactor_startup_run@mqtt_to_db_streaming.service",
765748
]
766-
assert f"Ownership and group permissions for {dot_pioreactor}:" in result.output
767-
assert f"fixed owner/group: 1 path(s): {dot_pioreactor / 'config.ini'}" in result.output
768-
assert (
769-
f"added group-write permission: 2 path(s): {dot_pioreactor}, {dot_pioreactor / 'storage'}"
770-
in result.output
771-
)
772-
assert f"added setgid permission: 1 path(s): {dot_pioreactor / 'storage'}" in result.output
773-
assert "Runtime files under /run/pioreactor:" in result.output
774-
assert "created or repaired runtime directories: 1 path(s): /run/pioreactor/cache" in result.output
775-
assert (
776-
"removed stale export artifacts: 1 path(s): /run/pioreactor/exports/export_old.zip" in result.output
777-
)
778-
assert "created or repaired cache databases: 1 path(s): /run/pioreactor/cache/huey.db" in result.output
779-
assert "repaired cache sidecar files: 1 path(s): /run/pioreactor/cache/huey.db-wal" in result.output
749+
assert f"Repaired ownership and group permissions for {dot_pioreactor}." in result.output
750+
assert "Repaired runtime directories under /run/pioreactor." in result.output
751+
assert "Cleared stale runtime export artifacts from /run/pioreactor/exports." in result.output
752+
assert "Repaired runtime cache files under /run/pioreactor/cache." in result.output
780753
assert "Restarted inactive pioreactor-web.target services: huey.service." in result.output
781754
assert (
782755
"Restarted inactive pioreactor startup services: "
@@ -785,42 +758,6 @@ def record_run(command: list[str], check: bool) -> DummyResult:
785758
assert "Repair complete." in result.output
786759

787760

788-
def test_pio_repair_reports_no_changes_when_preflight_is_clean(
789-
monkeypatch: pytest.MonkeyPatch, tmp_path: Path
790-
) -> None:
791-
dot_pioreactor = tmp_path / ".pioreactor"
792-
dot_pioreactor.mkdir()
793-
794-
monkeypatch.setenv("DOT_PIOREACTOR", str(dot_pioreactor))
795-
monkeypatch.setattr("shutil.which", lambda name: f"/usr/bin/{name}")
796-
monkeypatch.setattr(
797-
"pioreactor.cli.pio.collect_dot_pioreactor_repair_changes",
798-
lambda _root: {"ownership": [], "group_writable": [], "setgid": []},
799-
)
800-
monkeypatch.setattr(
801-
"pioreactor.cli.pio.collect_runtime_repair_changes",
802-
lambda: {"directories": [], "stale_exports": [], "cache_databases": [], "cache_sidecars": []},
803-
)
804-
805-
class DummyResult:
806-
def __init__(self, returncode: int = 0) -> None:
807-
self.returncode = returncode
808-
809-
def record_run(command: list[str], check: bool) -> DummyResult:
810-
assert check is True or command[:3] == ["/usr/bin/systemctl", "is-active", "--quiet"]
811-
return DummyResult()
812-
813-
monkeypatch.setattr("subprocess.run", record_run)
814-
monkeypatch.setattr("pioreactor.cli.pio.whoami.am_I_leader", lambda: True)
815-
816-
runner = CliRunner()
817-
result = runner.invoke(pio, ["repair"])
818-
819-
assert result.exit_code == 0
820-
assert f"Ownership and group permissions for {dot_pioreactor}: no changes needed." in result.output
821-
assert "Runtime files under /run/pioreactor: no changes needed." in result.output
822-
823-
824761
def test_pio_repair_does_not_check_mqtt_to_db_streaming_on_workers(
825762
monkeypatch: pytest.MonkeyPatch, tmp_path: Path
826763
) -> None:

0 commit comments

Comments
 (0)