Benutzer-Werkzeuge

Webseiten-Werkzeuge


wiki:update_script_auf_proxmox_server_fuer_alle_server

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
wiki/update_script_auf_proxmox_server_fuer_alle_server.txt · Zuletzt geändert: von Daniel Kaspar

DokuWiki Appliance - Powered by TurnKey Linux