fix: install JD01 GC growth policy
This commit is contained in:
+139
-57
@@ -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)
|
||||
|
||||
Reference in New Issue
Block a user