atlas-iac/services/maintenance/node-ops/scripts/node_pod_log_cleanup.py

97 lines
3.4 KiB
Python
Raw Normal View History

#!/usr/bin/env python3
"""Remove only expired orphan pod-log directories; never read log contents."""
from __future__ import annotations
import argparse
import os
from pathlib import Path
import re
import shutil
import time
UID = re.compile(r"[0-9a-f]{8}(?:-[0-9a-f]{4}){3}-[0-9a-f]{12}")
def walk_error(error: OSError) -> None:
"""A failed directory scan cannot establish that all logs are expired."""
raise error
def newest_mtime(path: Path) -> float:
"""Inspect timestamps without following symlinks or reading file contents."""
newest = path.stat().st_mtime
for root, directories, files in os.walk(path, onerror=walk_error, followlinks=False):
for name in directories + files:
newest = max(newest, (Path(root) / name).lstat().st_mtime)
return newest
def cleanup(host_root: Path, retention_days: int, dry_run: bool = False) -> dict[str, int]:
"""Return counts after pruning inactive UID directories older than retention.
An unreadable or empty kubelet inventory cannot authorize any deletion.
Recent files preserve a directory even when its own timestamp is old.
"""
if retention_days < 1:
raise ValueError("Pod-log retention must be at least one day")
result = {"removed": 0, "eligible": 0, "skipped": 0, "inventory_unavailable": 0}
try:
active = {p.name for p in (host_root / "var/lib/kubelet/pods").iterdir()
if UID.fullmatch(p.name)}
except OSError:
active = set()
if not active:
result["inventory_unavailable"] = 1
return result
cutoff = time.time() - retention_days * 86400
for relative in ("var/log/pods", "var/log.hdd/pods"):
try:
candidates = list((host_root / relative).iterdir())
except OSError:
continue
for path in candidates:
uid = path.name.rsplit("_", 1)[-1]
if not UID.fullmatch(uid) or uid in active or path.is_symlink():
result["skipped"] += 1
continue
try:
if not path.is_dir():
continue
if newest_mtime(path) >= cutoff:
result["skipped"] += 1
continue
# Recheck the exact UID immediately before the destructive step.
current = {p.name for p in (host_root / "var/lib/kubelet/pods").iterdir()
if UID.fullmatch(p.name)}
if not current:
result["inventory_unavailable"] = 1
return result
if uid in current:
result["skipped"] += 1
continue
result["eligible"] += 1
if not dry_run:
shutil.rmtree(path)
result["removed"] += 1
except OSError:
result["skipped"] += 1
return result
def main() -> None:
"""Run the configured host-root cleanup and print counts, not log paths."""
import json
parser = argparse.ArgumentParser(description=__doc__)
parser.add_argument("--host-root", type=Path, default=Path("/host"))
parser.add_argument("--retention-days", type=int, default=3)
parser.add_argument("--dry-run", action="store_true")
args = parser.parse_args()
print(json.dumps(cleanup(args.host_root, args.retention_days, args.dry_run)))
if __name__ == "__main__":
main()