feat(daily_check): systemd unit drift 自动检测
work #04 (2026-07-03):user 重申 sync 目标强调 timer/service/依赖/编码格式 全链路审计,发现 5 处 systemd unit drift:work #01 (4h timeout) 和 work #02 (morning.timer) 代码改了但没 cp 到 /etc/systemd/system/,等于没修。 根因:deployment 与 repo 各自演进无任何告警机制。 修复:在 daily_sync_check 加 _check_systemd_drift() 段,对比 bin/systemd/ 与 /etc/systemd/system/ 下所有 market-sync* unit: - missing_in_etc (未部署) → status=not_deployed - drifted (两边有但内容不一致, SHA256 比对) → status=drifted - orphan_in_etc (/etc 有 repo 没维护, 捕获僵尸 unit) → status=orphan drift 合入 alerts 让 webhook 也带;report schema_version 1→2。 立即验证:跑 daily_check 抓到 5 处 drift(4 未部署 + 1 内容不一致), status=warning 让 webhook 自动发出——以后任何 deployment drift 都逃不掉。 Co-Authored-By: Claude <noreply@anthropic.com>
This commit is contained in:
+111
-2
@@ -221,6 +221,70 @@ def _classify_task(
|
||||
}
|
||||
|
||||
|
||||
# ── systemd unit drift 检测 ────────────────────────────────────────────────
|
||||
SYSTEMD_REPO_DIR = PROJECT_ROOT / "bin" / "systemd"
|
||||
SYSTEMD_ETC_DIR = Path("/etc/systemd/system")
|
||||
|
||||
|
||||
def _check_systemd_drift() -> dict[str, Any]:
|
||||
"""对比 bin/systemd/ 与 /etc/systemd/system/ 下的 market-sync* unit。
|
||||
|
||||
返回:
|
||||
missing_in_etc: repo 有但 /etc 没装(drift 1:未部署)
|
||||
drifted: 两边都有但内容不一致(drift 2:部署的版本过期)
|
||||
orphan_in_etc: /etc 有但 repo 没有(drift 3:临时/僵尸 unit)
|
||||
"""
|
||||
missing_in_etc: list[str] = []
|
||||
drifted: list[dict[str, str]] = []
|
||||
orphan_in_etc: list[str] = []
|
||||
|
||||
repo_units: set[str] = set()
|
||||
if SYSTEMD_REPO_DIR.exists():
|
||||
for f in SYSTEMD_REPO_DIR.iterdir():
|
||||
if f.suffix in {".service", ".timer"} and f.name.startswith("market-sync"):
|
||||
repo_units.add(f.name)
|
||||
|
||||
etc_units: set[str] = set()
|
||||
if SYSTEMD_ETC_DIR.exists():
|
||||
for f in SYSTEMD_ETC_DIR.iterdir():
|
||||
if f.name.startswith("market-sync") and f.suffix in {".service", ".timer"}:
|
||||
etc_units.add(f.name)
|
||||
|
||||
# drift 1: repo 有 /etc 没
|
||||
for name in sorted(repo_units - etc_units):
|
||||
missing_in_etc.append(name)
|
||||
|
||||
# drift 2: 两边都有但内容不一致
|
||||
for name in sorted(repo_units & etc_units):
|
||||
repo_path = SYSTEMD_REPO_DIR / name
|
||||
etc_path = SYSTEMD_ETC_DIR / name
|
||||
try:
|
||||
if repo_path.read_bytes() != etc_path.read_bytes():
|
||||
drifted.append({
|
||||
"unit": name,
|
||||
"repo_sha": hashlib.sha256(repo_path.read_bytes()).hexdigest()[:12],
|
||||
"etc_sha": hashlib.sha256(etc_path.read_bytes()).hexdigest()[:12],
|
||||
})
|
||||
except OSError:
|
||||
drifted.append({"unit": name, "repo_sha": "?", "etc_sha": "?"})
|
||||
|
||||
# drift 3: /etc 有 repo 没(孤儿)
|
||||
for name in sorted(etc_units - repo_units):
|
||||
orphan_in_etc.append(name)
|
||||
|
||||
has_drift = bool(missing_in_etc or drifted or orphan_in_etc)
|
||||
return {
|
||||
"schema_version": 1,
|
||||
"checked_at": datetime.now().strftime("%Y-%m-%d %H:%M:%S"),
|
||||
"repo_units": sorted(repo_units),
|
||||
"etc_units": sorted(etc_units),
|
||||
"missing_in_etc": missing_in_etc,
|
||||
"drifted": drifted,
|
||||
"orphan_in_etc": orphan_in_etc,
|
||||
"has_drift": has_drift,
|
||||
}
|
||||
|
||||
|
||||
# ── 报告生成 ───────────────────────────────────────────────────────────────
|
||||
def build_report(check_date: date) -> dict[str, Any]:
|
||||
"""生成当日巡检报告。"""
|
||||
@@ -242,18 +306,45 @@ def build_report(check_date: date) -> dict[str, Any]:
|
||||
if c["status"] in {"missed", "failed", "stuck", "never_run"}
|
||||
]
|
||||
|
||||
# systemd drift 单独段(不是 dataset 状态,但是 sync 完整性的关键)
|
||||
drift = _check_systemd_drift()
|
||||
if drift["has_drift"]:
|
||||
# drift 也算告警(影响同步运行)
|
||||
if drift["missing_in_etc"]:
|
||||
for u in drift["missing_in_etc"]:
|
||||
alerts.append({
|
||||
"dataset_id": f"systemd/{u}",
|
||||
"status": "not_deployed",
|
||||
"reason": f"unit {u} 在 bin/systemd/ 有但 /etc/systemd/system/ 没装",
|
||||
})
|
||||
if drift["drifted"]:
|
||||
for d in drift["drifted"]:
|
||||
alerts.append({
|
||||
"dataset_id": f"systemd/{d['unit']}",
|
||||
"status": "drifted",
|
||||
"reason": f"unit {d['unit']} /etc 版过期(repo={d['repo_sha']}, etc={d['etc_sha']})",
|
||||
})
|
||||
if drift["orphan_in_etc"]:
|
||||
for u in drift["orphan_in_etc"]:
|
||||
alerts.append({
|
||||
"dataset_id": f"systemd/{u}",
|
||||
"status": "orphan",
|
||||
"reason": f"unit {u} 在 /etc/systemd/system/ 但 repo 没维护",
|
||||
})
|
||||
|
||||
overall = "ok" if not alerts else (
|
||||
"warning" if any(a["status"] in {"missed", "never_run"} for a in alerts) else "error"
|
||||
"warning" if any(a["status"] in {"missed", "never_run", "not_deployed", "drifted"} for a in alerts) else "error"
|
||||
)
|
||||
|
||||
return {
|
||||
"schema_version": 1,
|
||||
"schema_version": 2,
|
||||
"generated_at": now.strftime("%Y-%m-%d %H:%M:%S"),
|
||||
"check_date": check_date.isoformat(),
|
||||
"overall": overall,
|
||||
"counts": counts,
|
||||
"alerts": alerts,
|
||||
"tasks": classified,
|
||||
"systemd_drift": drift,
|
||||
}
|
||||
|
||||
|
||||
@@ -278,6 +369,24 @@ def print_summary(report: dict[str, Any]) -> None:
|
||||
print(f" - [{a['status']:>9}] {a['dataset_id']:<20} {a['reason']}")
|
||||
else:
|
||||
print("\n✅ 所有任务今日已成功")
|
||||
|
||||
# systemd drift 段
|
||||
drift = report.get("systemd_drift", {})
|
||||
if drift:
|
||||
print(f"\nsystemd unit 漂移:")
|
||||
print(f" repo units ({len(drift['repo_units'])}): {drift['repo_units']}")
|
||||
print(f" /etc units ({len(drift['etc_units'])}): {drift['etc_units']}")
|
||||
if drift["missing_in_etc"]:
|
||||
print(f" ❌ 未部署 ({len(drift['missing_in_etc'])}): {drift['missing_in_etc']}")
|
||||
if drift["drifted"]:
|
||||
print(f" ⚠️ 内容不一致 ({len(drift['drifted'])}):")
|
||||
for d in drift["drifted"]:
|
||||
print(f" - {d['unit']} repo={d['repo_sha']} etc={d['etc_sha']}")
|
||||
if drift["orphan_in_etc"]:
|
||||
print(f" 👻 /etc 孤儿 ({len(drift['orphan_in_etc'])}): {drift['orphan_in_etc']}")
|
||||
if not drift["has_drift"]:
|
||||
print(f" ✅ 无漂移")
|
||||
|
||||
# 完整列表
|
||||
print(f"\n全部 {len(report['tasks'])} 个任务:")
|
||||
for t in report["tasks"]:
|
||||
|
||||
Reference in New Issue
Block a user