fix: install JD01 GC growth policy

This commit is contained in:
Codex
2026-07-05 08:08:59 +00:00
parent 496b5fc729
commit 345b1cb7bb
5 changed files with 646 additions and 59 deletions
+139 -57
View File
@@ -22,6 +22,7 @@ CONTAINERD_CONFIG = REMOTE_TARGET.get("containerdImageCache") if isinstance(REMO
HOST_CONTAINERD_CONFIG = REMOTE_TARGET.get("hostContainerdCache") if isinstance(REMOTE_TARGET.get("hostContainerdCache"), dict) else {}
LOCAL_PATH_CONFIG = REMOTE_TARGET.get("localPathStorage") if isinstance(REMOTE_TARGET.get("localPathStorage"), dict) else {}
POLICY_TIMER_CONFIG = REMOTE_TARGET.get("policyTimer") if isinstance(REMOTE_TARGET.get("policyTimer"), dict) else {}
POLICY_RUNNER_SOURCE = base64.b64decode("__UNIDESK_GC_REMOTE_POLICY_RUNNER_BASE64__").decode("utf-8")
TMP_PREFIX_ALLOWLIST = [
"hwlab-agent-",
@@ -1523,61 +1524,52 @@ def render_remote_policy():
tmp_min_age_hours = config_float(POLICY_TIMER_CONFIG, "tmpMinAgeHours", float(OPTIONS.get("tmpMinAgeHours") or 24), minimum=0.0)
include_apt_cache = config_bool(POLICY_TIMER_CONFIG, "includeAptCache", bool(OPTIONS.get("aptCache", True)))
include_tool_caches = config_bool(POLICY_TIMER_CONFIG, "includeToolCaches", False)
script_path = "/usr/local/sbin/%s.sh" % unit_name
include_web_observe = config_bool(POLICY_TIMER_CONFIG, "includeWebObserveArtifacts", False)
include_k3s_images = config_bool(POLICY_TIMER_CONFIG, "includeK3sImageCache", False)
include_host_containerd = config_bool(POLICY_TIMER_CONFIG, "includeHostContainerdCache", False)
include_local_path_orphans = config_bool(POLICY_TIMER_CONFIG, "includeLocalPathOrphans", False)
state_dir = config_str(POLICY_TIMER_CONFIG, "stateDir", "/var/lib/unidesk-gc")
config_dir = config_str(POLICY_TIMER_CONFIG, "configDir", "/etc/unidesk-gc")
script_path = "/usr/local/sbin/%s.py" % unit_name
config_path = os.path.join(config_dir, "%s.json" % unit_name)
state_path = os.path.join(state_dir, "%s.last.json" % unit_name)
service_path = "/etc/systemd/system/%s.service" % unit_name
timer_path = "/etc/systemd/system/%s.timer" % unit_name
tool_paths = [item["path"] for item in TOOL_CACHE_ALLOWLIST] if include_tool_caches else []
script = "\n".join([
"#!/bin/sh",
"set -eu",
"umask 077",
"journalctl --vacuum-size=%s >/dev/null 2>&1 || true" % int(journal_target),
"apt-get clean >/dev/null 2>&1 || true" if include_apt_cache else ": apt cache disabled by YAML",
"python3 - <<'PY'",
"import json, os, shutil, time",
"prefixes = json.loads(%r)" % json.dumps(TMP_PREFIX_ALLOWLIST),
"protected = set(json.loads(%r))" % json.dumps(sorted(TMP_EXACT_PROTECT)),
"tool_paths = json.loads(%r)" % json.dumps(tool_paths),
"cutoff = time.time() - float(%r) * 3600.0" % tmp_min_age_hours,
"for name in os.listdir('/tmp'):",
" path = os.path.join('/tmp', name)",
" if path in protected or not any(name.startswith(prefix) for prefix in prefixes):",
" continue",
" try:",
" stat = os.lstat(path)",
" except OSError:",
" continue",
" if stat.st_mtime >= cutoff:",
" continue",
" if os.path.isdir(path) and not os.path.islink(path):",
" shutil.rmtree(path, ignore_errors=True)",
" elif os.path.exists(path):",
" try:",
" os.unlink(path)",
" except FileNotFoundError:",
" pass",
"for path in tool_paths:",
" resolved = os.path.abspath(path)",
" if resolved != path or os.path.islink(resolved) or resolved in ['/', '/root', '/root/.npm', '/root/.bun']:",
" continue",
" if os.path.isdir(resolved):",
" shutil.rmtree(resolved, ignore_errors=True)",
" elif os.path.exists(resolved):",
" try:",
" os.unlink(resolved)",
" except FileNotFoundError:",
" pass",
"PY",
"",
])
policy_config = {
"providerId": PROVIDER_ID,
"unitName": unit_name,
"statePath": state_path,
"journalTargetBytes": int(journal_target),
"tmpMinAgeHours": tmp_min_age_hours,
"tmpPrefixAllowlist": TMP_PREFIX_ALLOWLIST,
"tmpExactProtect": sorted(TMP_EXACT_PROTECT),
"includeAptCache": include_apt_cache,
"toolCachePaths": [item["path"] for item in TOOL_CACHE_ALLOWLIST] if include_tool_caches else [],
"webObserve": {"enabled": include_web_observe, **MEMORY_CONFIG},
"k3sImageCache": {"enabled": include_k3s_images, **CONTAINERD_CONFIG},
"hostContainerdCache": {"enabled": include_host_containerd, **HOST_CONTAINERD_CONFIG},
"localPathStorage": {"enabled": include_local_path_orphans, **LOCAL_PATH_CONFIG},
"agentrunSessionPvcs": POLICY_TIMER_CONFIG.get("agentrunSessionPvcs") if isinstance(POLICY_TIMER_CONFIG.get("agentrunSessionPvcs"), dict) else {"enabled": False},
}
stages = [
{"id": "journal", "enabled": True, "risk": "low", "mode": "journalctl-vacuum"},
{"id": "apt-cache", "enabled": include_apt_cache, "risk": "low", "mode": "apt-get-clean"},
{"id": "tmp-allowlist", "enabled": True, "risk": "low", "mode": "direct-child-prefix-retention"},
{"id": "tool-caches", "enabled": include_tool_caches, "risk": "medium", "mode": "fixed-path-rebuildable-cache"},
{"id": "web-observe-artifacts", "enabled": include_web_observe, "risk": "medium", "mode": "manifest-heartbeat-pid-openfd-stale-run"},
{"id": "agentrun-session-pvcs", "enabled": bool(policy_config["agentrunSessionPvcs"].get("enabled")), "risk": "medium", "mode": "kubectl-delete-pvc-wait-false-owner-aware"},
{"id": "k3s-image-cache", "enabled": include_k3s_images, "risk": "medium", "mode": "crictl-rmi-prune-ci-idle"},
{"id": "host-containerd-orphans", "enabled": include_host_containerd, "risk": "medium", "mode": "ctr-metadata-empty-yaml-orphan-state"},
{"id": "local-path-orphans", "enabled": include_local_path_orphans, "risk": "medium", "mode": "pv-unreferenced-direct-child"},
]
service = "\n".join([
"[Unit]",
"Description=UniDesk remote low-risk GC for %s" % PROVIDER_ID,
"Description=UniDesk remote growth-slowdown GC for %s" % PROVIDER_ID,
"Documentation=config/unidesk-cli.yaml#gc.remote.targets.%s.policyTimer" % PROVIDER_ID,
"",
"[Service]",
"Type=oneshot",
"ExecStart=%s" % script_path,
"ExecStart=/usr/bin/python3 %s %s" % (script_path, config_path),
"Nice=10",
"IOSchedulingClass=best-effort",
"IOSchedulingPriority=7",
@@ -1599,6 +1591,10 @@ def render_remote_policy():
return {
"unitName": unit_name,
"scriptPath": script_path,
"configPath": config_path,
"statePath": state_path,
"configDir": config_dir,
"stateDir": state_dir,
"servicePath": service_path,
"timerPath": timer_path,
"onCalendar": on_calendar,
@@ -1608,7 +1604,13 @@ def render_remote_policy():
"tmpMinAgeHours": tmp_min_age_hours,
"includeAptCache": include_apt_cache,
"includeToolCaches": include_tool_caches,
"script": script,
"includeWebObserveArtifacts": include_web_observe,
"includeK3sImageCache": include_k3s_images,
"includeHostContainerdCache": include_host_containerd,
"includeLocalPathOrphans": include_local_path_orphans,
"stages": stages,
"policyConfig": policy_config,
"script": POLICY_RUNNER_SOURCE,
"service": service,
"timer": timer,
}
@@ -1624,30 +1626,106 @@ def remote_policy_plan_payload(observed_at):
"observedAt": observed_at,
"configSource": "config/unidesk-cli.yaml#gc.remote.targets.%s.policyTimer" % PROVIDER_ID,
"enabled": config_bool(POLICY_TIMER_CONFIG, "enabled", False),
"timer": {key: rendered.get(key) for key in ["unitName", "scriptPath", "servicePath", "timerPath", "onCalendar", "randomizedDelaySec", "journalTargetBytes", "journalTarget", "tmpMinAgeHours", "includeAptCache", "includeToolCaches"]},
"scriptPreview": "\n".join(rendered["script"].splitlines()[:20]),
"servicePreview": rendered["service"],
"timerPreview": rendered["timer"],
"timer": {key: rendered.get(key) for key in ["unitName", "scriptPath", "configPath", "statePath", "servicePath", "timerPath", "onCalendar", "randomizedDelaySec", "journalTargetBytes", "journalTarget", "tmpMinAgeHours", "includeAptCache", "includeToolCaches", "includeWebObserveArtifacts", "includeK3sImageCache", "includeHostContainerdCache", "includeLocalPathOrphans"]},
"stages": rendered.get("stages"),
"servicePreview": rendered["service"] if bool(OPTIONS.get("full")) else None,
"timerPreview": rendered["timer"] if bool(OPTIONS.get("full")) else None,
"installCommand": "bun scripts/cli.ts gc remote %s policy install --confirm" % PROVIDER_ID,
"statusCommand": "bun scripts/cli.ts gc remote %s policy status" % PROVIDER_ID,
"policy": {
"risk": "low",
"risk": "low-to-medium-owner-aware",
"neverTouches": [
"k3s runtime directories",
"PVC/PV/local-path data",
"k3s runtime metadata unless a dedicated CRI prune stage is enabled",
"active PVC/PV/local-path data",
"Docker images, containers, volumes or Docker build cache",
"Secret/auth/config state",
"active Web observe runners or Chrome processes",
],
"toolCaches": "disabled unless config/unidesk-cli.yaml enables includeToolCaches for this remote target",
"sourceOfTruth": "config/unidesk-cli.yaml#gc.remote.targets.%s.policyTimer" % PROVIDER_ID,
},
}
def remote_policy_status_payload(observed_at):
rendered = render_remote_policy()
timer = command(["systemctl", "show", "%s.timer" % rendered["unitName"], "--property=LoadState,ActiveState,SubState,NextElapseUSecRealtime,LastTriggerUSec"], 10)
service = command(["systemctl", "show", "%s.service" % rendered["unitName"], "--property=LoadState,ActiveState,SubState,Result,ExecMainStatus"], 10)
last = None
try:
with open(rendered["statePath"], "r", encoding="utf-8") as handle:
last = json.load(handle)
except Exception:
last = None
if last is not None and not bool(OPTIONS.get("full")):
last = compact_policy_last_run(last)
return {
"ok": True,
"action": "gc remote policy status",
"providerId": PROVIDER_ID,
"dryRun": True,
"mutation": False,
"observedAt": observed_at,
"timer": {key: rendered.get(key) for key in ["unitName", "scriptPath", "configPath", "statePath", "servicePath", "timerPath", "onCalendar", "randomizedDelaySec"]},
"systemd": {
"timer": bounded(timer),
"service": bounded(service),
},
"lastRun": last,
"next": {
"plan": "bun scripts/cli.ts gc remote %s policy plan" % PROVIDER_ID,
"install": "bun scripts/cli.ts gc remote %s policy install --confirm" % PROVIDER_ID,
"disk": "bun scripts/cli.ts gc remote %s status --limit 20" % PROVIDER_ID,
},
}
def compact_policy_last_run(last):
results = []
for item in last.get("results") or []:
if not isinstance(item, dict):
continue
compact = {
key: item.get(key)
for key in [
"id",
"ok",
"skipped",
"reason",
"error",
"deletedCount",
"deletedPvcCount",
"protectedCount",
"candidateCount",
"reclaimedBytes",
]
if item.get(key) is not None
}
command_payload = item.get("command") if isinstance(item.get("command"), dict) else None
if command_payload is not None:
compact["command"] = {
key: command_payload.get(key)
for key in ["exitCode", "timedOut", "elapsedMs"]
if command_payload.get(key) is not None
}
results.append(compact)
payload = {
key: last.get(key)
for key in ["ok", "observedAt", "providerId", "unitName", "resultCount", "failedCount", "reclaimedBytes"]
if last.get(key) is not None
}
payload["results"] = results
return payload
def remote_policy_install_payload(observed_at):
rendered = render_remote_policy()
try:
os.makedirs(rendered["configDir"], mode=0o755, exist_ok=True)
os.makedirs(rendered["stateDir"], mode=0o755, exist_ok=True)
with open(rendered["scriptPath"], "w", encoding="utf-8") as handle:
handle.write(rendered["script"])
os.chmod(rendered["scriptPath"], 0o755)
with open(rendered["configPath"], "w", encoding="utf-8") as handle:
json.dump(rendered["policyConfig"], handle, sort_keys=True, separators=(",", ":"))
handle.write("\n")
os.chmod(rendered["configPath"], 0o644)
with open(rendered["servicePath"], "w", encoding="utf-8") as handle:
handle.write(rendered["service"])
with open(rendered["timerPath"], "w", encoding="utf-8") as handle:
@@ -1675,7 +1753,8 @@ def remote_policy_install_payload(observed_at):
"mutation": True,
"observedAt": observed_at,
"configSource": "config/unidesk-cli.yaml#gc.remote.targets.%s.policyTimer" % PROVIDER_ID,
"timer": {key: rendered.get(key) for key in ["unitName", "scriptPath", "servicePath", "timerPath", "onCalendar", "randomizedDelaySec", "journalTargetBytes", "journalTarget", "tmpMinAgeHours", "includeAptCache", "includeToolCaches"]},
"timer": {key: rendered.get(key) for key in ["unitName", "scriptPath", "configPath", "statePath", "servicePath", "timerPath", "onCalendar", "randomizedDelaySec", "journalTargetBytes", "journalTarget", "tmpMinAgeHours", "includeAptCache", "includeToolCaches", "includeWebObserveArtifacts", "includeK3sImageCache", "includeHostContainerdCache", "includeLocalPathOrphans"]},
"stages": rendered.get("stages"),
"systemd": {
"daemonReload": bounded(daemon),
"enableNow": bounded(enable),
@@ -1692,6 +1771,9 @@ def main():
if ACTION == "policy-install":
emit_json(remote_policy_install_payload(observed_at), persist_large=False)
return 0
if ACTION == "policy-status":
emit_json(remote_policy_status_payload(observed_at), persist_large=False)
return 0
if ACTION == "trend":
history_limit = int(OPTIONS.get("historyLimit") or 12)
history = read_growth_snapshots(history_limit)