Add service_watcher.py
This commit is contained in:
167
service_watcher.py
Normal file
167
service_watcher.py
Normal file
@@ -0,0 +1,167 @@
|
||||
#!/usr/bin/env python3
|
||||
# -*- coding: utf-8 -*-
|
||||
"""
|
||||
service_watcher.py — prosty watcher do crona
|
||||
|
||||
DZIAŁANIE
|
||||
- Oblicza sumę SHA256 monitorowanego pliku.
|
||||
- Porównuje z poprzednią wartością zapisaną w /dev/shm (tmpfs, RAM).
|
||||
- Gdy są zmiany → restartuje podane usługi (systemd) i aktualizuje sumę.
|
||||
|
||||
UŻYCIE
|
||||
python3 service_watcher.py -f /ścieżka/do/pliku -s nginx -s php-fpm
|
||||
python3 service_watcher.py -f /etc/nginx/nginx.conf -s nginx
|
||||
# lista po przecinku też działa:
|
||||
python3 service_watcher.py -f /etc/my.conf -s "nginx,php-fpm@1,redis"
|
||||
|
||||
CRON (przykład: co minutę)
|
||||
* * * * * /usr/bin/python3 /usr/local/bin/service_watcher.py -f /etc/nginx/nginx.conf -s nginx
|
||||
* * * * * /usr/bin/python3 /usr/local/bin/service_watcher.py -f /etc/hosts -s dnsmasq.service -s dnsdist -s unbound -q
|
||||
|
||||
|
||||
WYMAGANIA
|
||||
- systemd (systemctl), uprawnienia do restartu usług (root lub sudoers).
|
||||
- Python 3.6+
|
||||
|
||||
OPCJE
|
||||
-f/--file monitorowany plik (wymagane)
|
||||
-s/--service usługa; można podawać wiele razy lub jako CSV
|
||||
--state-dir katalog na stan (domyślnie: /dev/shm/service_watcher)
|
||||
--no-hash użyj tylko mtime+size (szybciej, mniej dokładnie)
|
||||
-q/--quiet bez logów (kody wyjścia nadal znaczące)
|
||||
|
||||
KODY WYJŚCIA
|
||||
0: brak zmian / restart(-y) OK
|
||||
1: błąd wykonania
|
||||
2: plik nie istnieje
|
||||
"""
|
||||
|
||||
import argparse
|
||||
import hashlib
|
||||
import os
|
||||
import sys
|
||||
import json
|
||||
import fcntl
|
||||
import subprocess
|
||||
from pathlib import Path
|
||||
from typing import List
|
||||
|
||||
def parse_args():
|
||||
p = argparse.ArgumentParser(description="Restartuje usługi po zmianie pliku.")
|
||||
p.add_argument("-f", "--file", required=True, help="Monitorowany plik.")
|
||||
p.add_argument("-s", "--service", action="append", default=[],
|
||||
help="Usługa (systemd). Można powtarzać lub podać CSV.")
|
||||
p.add_argument("--state-dir", default="/dev/shm/service_watcher",
|
||||
help="Katalog na stan (domyślnie: /dev/shm/service_watcher).")
|
||||
p.add_argument("--no-hash", action="store_true",
|
||||
help="Porównuj tylko mtime+size (bez SHA256).")
|
||||
p.add_argument("-q", "--quiet", action="store_true", help="Tryb cichy.")
|
||||
return p.parse_args()
|
||||
|
||||
def log(msg: str, quiet: bool):
|
||||
if not quiet:
|
||||
print(msg)
|
||||
|
||||
def split_services(svcs: List[str]) -> List[str]:
|
||||
out = []
|
||||
for s in svcs:
|
||||
if s:
|
||||
out.extend([x.strip() for x in s.split(",") if x.strip()])
|
||||
return list(dict.fromkeys(out)) # bez duplikatów, zachowaj kolejność
|
||||
|
||||
def file_signature(path: Path, use_hash: bool) -> str:
|
||||
st = path.stat()
|
||||
base = f"{st.st_mtime_ns}:{st.st_size}"
|
||||
if not use_hash:
|
||||
return base
|
||||
h = hashlib.sha256()
|
||||
with path.open("rb") as f:
|
||||
for chunk in iter(lambda: f.read(1024 * 1024), b""):
|
||||
h.update(chunk)
|
||||
return base + ":" + h.hexdigest()
|
||||
|
||||
def lockfile(path: Path):
|
||||
path.parent.mkdir(parents=True, exist_ok=True)
|
||||
f = path.open("a+")
|
||||
fcntl.flock(f.fileno(), fcntl.LOCK_EX)
|
||||
return f
|
||||
|
||||
def state_paths(state_dir: Path, target_file: Path):
|
||||
safe = target_file.as_posix().replace("/", "_")
|
||||
meta = state_dir / f"{safe}.json"
|
||||
lck = state_dir / f"{safe}.lock"
|
||||
return meta, lck
|
||||
|
||||
def load_state(p: Path) -> dict:
|
||||
if not p.exists():
|
||||
return {}
|
||||
try:
|
||||
return json.loads(p.read_text() or "{}")
|
||||
except Exception:
|
||||
return {}
|
||||
|
||||
def save_state(p: Path, data: dict):
|
||||
p.parent.mkdir(parents=True, exist_ok=True)
|
||||
tmp = p.with_suffix(".json.tmp")
|
||||
tmp.write_text(json.dumps(data, ensure_ascii=False))
|
||||
os.replace(tmp, p)
|
||||
|
||||
def restart_service(svc: str) -> subprocess.CompletedProcess:
|
||||
# pozwala działać z sudoers bez hasła dla systemctl
|
||||
return subprocess.run(
|
||||
["systemctl", "restart", svc],
|
||||
stdout=subprocess.PIPE, stderr=subprocess.PIPE, text=True, check=False
|
||||
)
|
||||
|
||||
def main():
|
||||
args = parse_args()
|
||||
file_path = Path(args.file)
|
||||
|
||||
if not file_path.exists():
|
||||
print(f"Plik nie istnieje: {file_path}", file=sys.stderr)
|
||||
sys.exit(2)
|
||||
|
||||
services = split_services(args.service)
|
||||
if not services:
|
||||
print("Podaj co najmniej jedną usługę (-s/--service).", file=sys.stderr)
|
||||
sys.exit(1)
|
||||
|
||||
state_dir = Path(args.state_dir)
|
||||
meta_path, lock_path = state_paths(state_dir, file_path)
|
||||
|
||||
# blokada współbieżnych uruchomień (np. overlap w cron)
|
||||
with lockfile(lock_path):
|
||||
sig = file_signature(file_path, use_hash=not args.no_hash)
|
||||
state = load_state(meta_path)
|
||||
prev = state.get("signature")
|
||||
|
||||
if prev == sig:
|
||||
log("Brak zmian — nic do zrobienia.", args.quiet)
|
||||
sys.exit(0)
|
||||
|
||||
log(f"Wykryto zmianę w {file_path}. Restart usług: {', '.join(services)}", args.quiet)
|
||||
|
||||
failures = []
|
||||
for svc in services:
|
||||
res = restart_service(svc)
|
||||
if res.returncode != 0:
|
||||
failures.append((svc, res.stderr.strip()))
|
||||
log(f"[FAIL] {svc}: {res.stderr.strip()}", args.quiet)
|
||||
else:
|
||||
log(f"[OK] {svc}", args.quiet)
|
||||
|
||||
if failures:
|
||||
# nie zapisuj nowej sygnatury, aby ponowić przy następnym uruchomieniu
|
||||
print("Co najmniej jeden restart nie powiódł się.", file=sys.stderr)
|
||||
for svc, err in failures:
|
||||
print(f"{svc}: {err}", file=sys.stderr)
|
||||
sys.exit(1)
|
||||
|
||||
# zapisz nową sygnaturę po udanych restartach
|
||||
state.update({"signature": sig})
|
||||
save_state(meta_path, state)
|
||||
log("Zakończono pomyślnie.", args.quiet)
|
||||
sys.exit(0)
|
||||
|
||||
if __name__ == "__main__":
|
||||
main()
|
Reference in New Issue
Block a user