====== Update Script für lokale Server unter Linux ====== ===== Vorbereitung ===== Script erstellen (/usr/local/bin/Pruefe-installationen.py) #!/usr/bin/env python3 import os import time import datetime import paramiko import requests from concurrent.futures import ThreadPoolExecutor, as_completed # ========================= # Optional: .env / env-file laden (ohne python-dotenv) # - systemd EnvironmentFile gewinnt (os.environ bereits gesetzt) # - nur fehlende Variablen werden aus Dateien ergänzt # ========================= def load_env_file(path: str) -> bool: try: with open(path, "r") as f: for line in f: line = line.strip() if not line or line.startswith("#") or "=" not in line: continue k, v = line.split("=", 1) k = k.strip() v = v.strip().strip('"').strip("'") os.environ.setdefault(k, v) return True except FileNotFoundError: return False # zuerst systemd-typische Datei, dann lokale .env neben Script load_env_file("/etc/update-checker.env") load_env_file("/usr/local/bin/.env") # ========================= # KONFIGURATION (ENV) # ========================= TG_BOT_TOKEN = os.getenv("TG_BOT_TOKEN", "").strip() TG_CHAT_ID = os.getenv("TG_CHAT_ID", "").strip() TG_THREAD_ID = os.getenv("TG_THREAD_ID", "").strip() TG_THREAD_ID = int(TG_THREAD_ID) if TG_THREAD_ID else None SSH_KEY_PATH = os.getenv("SSH_KEY_PATH", "/root/.ssh/id_ed25519") LOG_FILE = os.getenv("LOG_FILE", "/var/log/update_checker.log") AUTO_UPDATE = os.getenv("AUTO_UPDATE", "true").lower() in ("1", "true", "yes", "on") ONLY_REPORT_CHANGES = os.getenv("ONLY_REPORT_CHANGES", "false").lower() in ("1", "true", "yes", "on") MAX_WORKERS = int(os.getenv("MAX_WORKERS", "5")) SERVERS = [ {"host": "172.20.50.52", "user": "root"}, {"host": "172.20.60.1", "user": "root"}, {"host": "172.20.70.2", "user": "root"}, {"host": "172.20.70.3", "user": "root"}, {"host": "172.20.70.4", "user": "root"}, {"host": "172.20.70.5", "user": "root"}, {"host": "172.20.90.1", "user": "root"}, {"host": "172.20.90.2", "user": "root"}, {"host": "172.20.90.3", "user": "root"}, {"host": "172.20.90.4", "user": "root"}, {"host": "172.20.90.6", "user": "root"}, {"host": "172.20.90.7", "user": "root"}, {"host": "172.20.90.8", "user": "root"}, {"host": "172.20.90.9", "user": "root"}, {"host": "172.20.90.12", "user": "root"}, {"host": "172.20.90.13", "user": "root"}, {"host": "172.20.90.14", "user": "root"}, {"host": "172.20.90.15", "user": "root"}, {"host": "172.20.90.17", "user": "root"}, {"host": "172.20.90.18", "user": "root"}, ] # ========================= # SSH Helper # ========================= def ssh_run(ssh: paramiko.SSHClient, cmd: str, timeout: int = 1200): stdin, stdout, stderr = ssh.exec_command(cmd, timeout=timeout) exit_code = stdout.channel.recv_exit_status() out = stdout.read().decode(errors="ignore").strip() err = stderr.read().decode(errors="ignore").strip() return exit_code, out, err def detect_pkg_manager(ssh): # apt bevorzugen, dann dnf, dann yum _, out, _ = ssh_run(ssh, "command -v apt-get || true; command -v dnf || true; command -v yum || true", timeout=30) joined = "\n".join([l.strip() for l in out.splitlines() if l.strip()]) if "apt-get" in joined: return "apt" if "dnf" in joined: return "dnf" if "yum" in joined: return "yum" return None def get_hostname(ssh): _, out, _ = ssh_run(ssh, "hostname -f 2>/dev/null || hostname 2>/dev/null || echo unknown", timeout=30) return out.strip() if out.strip() else "unknown" # ========================= # Core check/update # ========================= def check_and_update(server): host = server["host"] user = server["user"] start = time.time() try: ssh = paramiko.SSHClient() ssh.set_missing_host_key_policy(paramiko.AutoAddPolicy()) ssh.connect(host, username=user, key_filename=SSH_KEY_PATH, timeout=15) hostname = get_hostname(ssh) pm = detect_pkg_manager(ssh) if not pm: ssh.close() return { "host": host, "name": hostname, "status": "error", "msg": "❌ Paketmanager nicht erkannt", "reboot": False, "duration": time.time() - start, } reboot_needed = False updates_available = False updated = False if pm == "apt": # Update-Check via Simulation (verlässlicher als 'apt list --upgradable') rc, out, err = ssh_run( ssh, "DEBIAN_FRONTEND=noninteractive apt-get -s upgrade | awk '/^Inst /{c++} END{print c+0}'", timeout=120 ) if rc != 0: raise RuntimeError(err or out or "apt-get -s upgrade fehlgeschlagen") count = int(out.strip() or "0") updates_available = count > 0 if updates_available and AUTO_UPDATE: rc, out, err = ssh_run( ssh, "DEBIAN_FRONTEND=noninteractive apt-get update && DEBIAN_FRONTEND=noninteractive apt-get -y upgrade", timeout=2400 ) if rc != 0: raise RuntimeError(err or "apt upgrade fehlgeschlagen") updated = True rc, out, _ = ssh_run(ssh, "test -f /var/run/reboot-required && echo yes || echo no", timeout=30) reboot_needed = (out.strip() == "yes") elif pm in ("dnf", "yum"): # check-update Exit 100 => Updates vorhanden, 0 => keine, sonst Fehler check_cmd = "dnf -q check-update" if pm == "yum": check_cmd = "yum -q check-update" rc, out, err = ssh_run(ssh, f"{check_cmd} >/dev/null 2>&1; echo $?", timeout=180) code_line = out.strip().splitlines()[-1] if out.strip() else "1" code = int(code_line) if code == 100: updates_available = True elif code == 0: updates_available = False else: raise RuntimeError(err or f"{pm} check-update Fehler (Exit {code})") if updates_available and AUTO_UPDATE: up_cmd = "dnf -y upgrade" if pm == "yum": up_cmd = "yum -y update" rc, out, err = ssh_run(ssh, up_cmd, timeout=3600) if rc != 0: raise RuntimeError(err or f"{pm} upgrade fehlgeschlagen") updated = True rc, out, _ = ssh_run( ssh, "command -v needs-restarting >/dev/null 2>&1 && needs-restarting -r; echo $?", timeout=120 ) reboot_needed = ("reboot" in out.lower()) or out.strip().endswith("1") ssh.close() if updates_available and updated: msg = "⬆️ Updates installiert" status = "updated" elif updates_available and not AUTO_UPDATE: msg = "⬆️ Updates verfügbar" status = "updates" else: msg = "✅ Keine Updates" status = "ok" return { "host": host, "name": hostname, "status": status, "msg": msg, "reboot": reboot_needed, "duration": time.time() - start, } except Exception as e: return { "host": host, "name": "unknown", "status": "error", "msg": f"❌ Fehler: {e}", "reboot": False, "duration": time.time() - start, } # ========================= # Telegram formatting (2 lines per host + runtime in line 1) # ========================= def format_telegram(results): ts = datetime.datetime.now().strftime("%d.%m.%Y %H:%M") header = f"*Update-Status* ({ts})\n" # Optional: nur Änderungen/Probleme melden if ONLY_REPORT_CHANGES: results = [r for r in results if r["status"] in ("updated", "updates", "error") or r.get("reboot")] if not results: return header + "\nAlles aktuell ✅" lines = [header] for r in sorted(results, key=lambda x: x["host"]): host = r["host"] name = r["name"] dur = f"{r['duration']:.1f}s" # Zeile 1: IP + Host + Laufzeit lines.append(f"🖥️ `{host}` ({name}) [{dur}]") # Zeile 2: Status lines.append(f" {r['msg']}") # optional dritte Zeile: reboot if r.get("reboot"): lines.append(" ⚠️ Neustart erforderlich") lines.append("") # Leerzeile return "\n".join(lines).rstrip() def send_telegram(message: str): if not TG_BOT_TOKEN or not TG_CHAT_ID: raise RuntimeError("TG_BOT_TOKEN oder TG_CHAT_ID fehlt (ENV prüfen).") url = f"https://api.telegram.org/bot{TG_BOT_TOKEN}/sendMessage" data = { "chat_id": TG_CHAT_ID, "text": message, "parse_mode": "Markdown", "disable_web_page_preview": True, } if TG_THREAD_ID is not None: data["message_thread_id"] = TG_THREAD_ID r = requests.post(url, data=data, timeout=20) r.raise_for_status() def log_results(results): try: with open(LOG_FILE, "a") as f: f.write(f"\n[{datetime.datetime.now()}]\n") for r in sorted(results, key=lambda x: x["host"]): line = f"{r['host']} ({r['name']}): {r['msg']}" if r.get("reboot"): line += " [REBOOT]" line += f" ({r['duration']:.1f}s)\n" f.write(line) except Exception: pass # Logging darf nicht killen def main(): results = [] with ThreadPoolExecutor(max_workers=MAX_WORKERS) as executor: futures = [executor.submit(check_and_update, s) for s in SERVERS] for fut in as_completed(futures): results.append(fut.result()) msg = format_telegram(results) send_telegram(msg) log_results(results) if __name__ == "__main__": main() Lege dann eine .env-Datei in /etc/update-checker.env an TG_BOT_TOKEN=DEIN_NEUER_TOKEN TG_CHAT_ID=-1002414259924 TG_THREAD_ID=21266 SSH_KEY_PATH=/root/.ssh/id_ed25519 LOG_FILE=/var/log/update_checker.log AUTO_UPDATE=true ONLY_REPORT_CHANGES=false MAX_WORKERS=5 Danach folgenden Befehl eingeben: install -m 750 /usr/local/bin/pruefe-installation.py /usr/local/bin/update-checker.py ==== Anlegen des systemd Services ==== Service Datei anlegen: nano /etc/systemd/system/update-checker.service Inhalt: [Unit] Description=Update Checker (SSH) + Telegram After=network-online.target Wants=network-online.target [Service] Type=oneshot User=root EnvironmentFile=/etc/update-checker.env ExecStart=/usr/bin/python3 /usr/local/bin/update-checker.py # optional: Sicherheit/Isolation NoNewPrivileges=true PrivateTmp=true [Install] WantedBy=multi-user.target ==== systemd Timer anlegen ==== Datei anlegen: nano /etc/systemd/system/update-checker.timer Inhalt: [Unit] Description=Run Update Checker daily [Timer] OnCalendar=*-*-* 06:30:00 Persistent=true Unit=update-checker.service [Install] WantedBy=timers.target ==== systemd neu laden & Timer aktivieren ==== systemctl daemon-reload systemctl enable --now update-checker.timer ==== Testen (Manuell) ==== systemctl start update-checker.service systemctl status update-checker.service --no-pager