diff --git a/Dockerfile b/Dockerfile index 52ff427..2618499 100644 --- a/Dockerfile +++ b/Dockerfile @@ -2,7 +2,7 @@ FROM python:3.11-slim WORKDIR /app -RUN pip install fastapi jinja2 uvicorn requests python-dateutil +RUN pip install fastapi jinja2 uvicorn requests python-dateutil python-multipart COPY main.py . COPY reference_versions.json ./reference_versions.json diff --git a/gen_ref_vers.py b/gen_ref_vers.py new file mode 100644 index 0000000..88b59b2 --- /dev/null +++ b/gen_ref_vers.py @@ -0,0 +1,44 @@ +import requests +import re +import json + +# Logischer Name (wie im UI) → Launchpad-Quellpaketname +PACKAGES = { + "python3": "python3-defaults", + "nginx": "nginx", + "openssh-server": "openssh" +} + +# Ubuntu-Release (muss auf launchpad.net vorhanden sein) +UBUNTU_RELEASE = "noble" + +def get_version(pkg_source, release=UBUNTU_RELEASE): + try: + url = f"https://launchpad.net/ubuntu/{release}/+source/{pkg_source}" + r = requests.get(url, timeout=10) + if r.status_code != 200: + print(f"⚠️ Fehler {r.status_code} bei {url}") + return None + + # Suche nach: Current version:
VERSION
+ match = re.search(r'Current version:\s*
([^<]+)
', r.text) + return match.group(1).strip() if match else None + except Exception as e: + print(f"❌ Fehler bei {pkg_source}: {e}") + return None + +def generate(): + result = {} + for display_name, source_name in PACKAGES.items(): + version = get_version(source_name) + if version: + result[display_name] = version + else: + print(f"❌ Keine Version gefunden für {display_name}") + return result + +if __name__ == "__main__": + versions = generate() + with open("reference_versions.json", "w") as f: + json.dump(versions, f, indent=2) + print("✅ Referenz gespeichert in reference_versions.json") diff --git a/main.py b/main.py index 7c0f32d..aa88466 100644 --- a/main.py +++ b/main.py @@ -1,9 +1,10 @@ -from fastapi import FastAPI -from fastapi.responses import HTMLResponse +from fastapi import FastAPI, Request, Form +from fastapi.responses import HTMLResponse, RedirectResponse from jinja2 import Template import requests import json from dateutil import parser, tz +from pathlib import Path app = FastAPI() @@ -11,104 +12,6 @@ API_URL = "http://192.168.18.4:8000/results/updatecheck/" READ_API_KEY = "read_key_1" REFERENCE_FILE = "reference_versions.json" -# ================= Templates ================== - -MAIN_TEMPLATE = Template(""" - - - - - UpdateLog Übersicht - - - - -

UpdateLog Übersicht

- {% for server, content in data.items() %} -
- - 🔍 Verlauf anzeigen - -
- {% endfor %} - - -""") - -HISTORY_TEMPLATE = Template(""" - - - - - Verlauf: {{ hostname }} - - - -

Update-Verlauf für {{ hostname }}

- ⬅ Zurück zur Übersicht - {% for entry in history %} -

{{ entry.timestamp }}

- - - {% for pkg, ver in entry.packages.items() %} - - - - - {% endfor %} -
PaketInstalliert
{{ pkg }}{{ ver }}
- {% endfor %} - - -""") - -# ================= Routes ================== - -@app.get("/", response_class=HTMLResponse) -def index(): - data = fetch_latest_per_host() - return MAIN_TEMPLATE.render(data=data) - -@app.get("/host/{hostname}", response_class=HTMLResponse) -def host_history(hostname: str): - history = fetch_history_for_host(hostname) - return HISTORY_TEMPLATE.render(hostname=hostname, history=history) - -# ================= Datenfunktionen ================== - def fetch_raw_lines(): try: response = requests.get(API_URL, headers={"Authorization": f"Bearer {READ_API_KEY}"}, timeout=5) @@ -122,52 +25,46 @@ def fetch_reference(): try: with open(REFERENCE_FILE) as f: return json.load(f) - except Exception as e: - print(f"❌ Fehler beim Laden der Referenzversionen: {e}") + except Exception: return {} def fetch_latest_per_host(): lines = fetch_raw_lines() reference = fetch_reference() - latest_by_host = {} + latest = {} for line in lines: parts = line.strip().split(",", 3) if len(parts) < 4: continue timestamp, hostname, _, packages = parts - if hostname not in latest_by_host or timestamp > latest_by_host[hostname]["timestamp"]: - latest_by_host[hostname] = { + if hostname not in latest or timestamp > latest[hostname]["timestamp"]: + latest[hostname] = { "timestamp": timestamp, "raw": packages } result = {} - for hostname, entry in latest_by_host.items(): - # Zeitstempel umrechnen + for host, entry in latest.items(): try: - dt_utc = parser.isoparse(entry["timestamp"]) - dt_local = dt_utc.astimezone(tz.gettz("Europe/Berlin")) - display_time = dt_local.strftime("%Y-%m-%d %H:%M:%S") - except Exception: + dt = parser.isoparse(entry["timestamp"]).astimezone(tz.gettz("Europe/Berlin")) + display_time = dt.strftime("%Y-%m-%d %H:%M:%S") + except: display_time = entry["timestamp"] - package_str = entry["raw"] - result[hostname] = { - "timestamp": display_time, - "packages": {} - } - for item in package_str.strip(";").split(";"): - if "=" not in item: - continue - pkg, version = item.split("=", 1) - expected = reference.get(pkg) - if expected: - result[hostname]["packages"][pkg] = { - "current": version, - "expected": expected + pkgs = {} + for p in entry["raw"].strip(";").split(";"): + if "=" in p: + k, v = p.split("=", 1) + pkgs[k] = { + "current": v, + "expected": reference.get(k) } + result[host] = { + "timestamp": display_time, + "packages": pkgs + } return result def fetch_history_for_host(hostname): @@ -175,20 +72,143 @@ def fetch_history_for_host(hostname): history = [] for line in lines: parts = line.strip().split(",", 3) - if len(parts) < 4: + if len(parts) < 4 or parts[1] != hostname: continue - timestamp, host, _, packages = parts - if host != hostname: - continue - parsed = {} - for item in packages.strip(";").split(";"): - if "=" not in item: - continue - pkg, version = item.split("=", 1) - parsed[pkg] = version - history.append({ - "timestamp": timestamp, - "packages": parsed - }) + timestamp, _, _, raw = parts + pkgs = {} + for p in raw.strip(";").split(";"): + if "=" in p: + k, v = p.split("=", 1) + pkgs[k] = v + history.append({"timestamp": timestamp, "packages": pkgs}) history.sort(key=lambda x: x["timestamp"], reverse=True) return history + +@app.get("/", response_class=HTMLResponse) +def index(): + data = fetch_latest_per_host() + return MAIN_TEMPLATE.render(data=data) + +@app.get("/host/{hostname}", response_class=HTMLResponse) +def host_view(hostname: str): + return HOST_TEMPLATE.render(hostname=hostname, history=fetch_history_for_host(hostname)) + +@app.get("/edit", response_class=HTMLResponse) +def edit_view(): + try: + data = Path(REFERENCE_FILE).read_text() + except: + data = "{}" + return EDIT_TEMPLATE.render(content=data) + +@app.post("/edit", response_class=RedirectResponse) +def edit_post(content: str = Form(...)): + try: + json.loads(content) + Path(REFERENCE_FILE).write_text(content) + except Exception as e: + print(f"Fehler beim Schreiben der Referenzdatei: {e}") + return RedirectResponse(url="/edit", status_code=303) + +# ========== Templates ========== + +MAIN_TEMPLATE = Template(""" + + + + + Update Übersicht + + + + +

Update Übersicht

+ + ⚙️ Referenz bearbeiten + {% for host, d in data.items() %} +
+ + 🔍 Verlauf + +
+ {% endfor %} + + +""") + +HOST_TEMPLATE = Template(""" + + +{{ hostname }} + + +

Verlauf: {{ hostname }}

+ ⬅ Zurück + {% for h in history %} +

{{ h.timestamp }}

+ + + {% for p, v in h.packages.items() %} + + {% endfor %} +
PaketVersion
{{ p }}{{ v }}
+ {% endfor %} + + +""") + +EDIT_TEMPLATE = Template(""" + + +Referenz bearbeiten + + +

Referenzdatei bearbeiten

+⬅ Zurück +
+
+ +
+ + +""") diff --git a/reference_versions.json b/reference_versions.json index 987f8ef..7854f28 100644 --- a/reference_versions.json +++ b/reference_versions.json @@ -1,4 +1,5 @@ { "python3": "3.12.3-0ubuntu2", - "openssh-server": "1:9.6p1-3ubuntu13.11" -} + "nginx": "1.24.0-2ubuntu7.4", + "openssh-server": "1:9.6p1-3ubuntu13.12" +} \ No newline at end of file diff --git a/run_container.sh b/run_container.sh index ca61608..4d69bc0 100755 --- a/run_container.sh +++ b/run_container.sh @@ -13,10 +13,5 @@ docker build -t $IMAGE_NAME . docker run -d \ --name $CONTAINER_NAME \ -p $PORT:8080 \ - --read-only \ - --tmpfs /tmp \ - --cap-drop ALL \ - --security-opt no-new-privileges \ + -v "$(realpath ./reference_versions.json)":/app/reference_versions.json \ $IMAGE_NAME - -echo "✅ Update-UI läuft unter http://localhost:$PORT" diff --git a/send-update-log-win.ps1 b/send-update-log-win.ps1 new file mode 100644 index 0000000..7ef3731 --- /dev/null +++ b/send-update-log-win.ps1 @@ -0,0 +1,41 @@ +# Konfiguration +$apiUrl = "http://192.168.18.4:8000/api/updatecheck/" +$apiToken = "9d207bf0-10f5-4d8f-a479-22ff5aeff8d1" + +# Hostname und Zeit +$hostname = $env:COMPUTERNAME +$timestamp = (Get-Date).ToString("yyyy-MM-ddTHH:mm:sszzz") + +# OS- und Kernel-Info +$os = (Get-CimInstance Win32_OperatingSystem).Caption +$kernel = (Get-CimInstance Win32_OperatingSystem).Version + +# Installierte Pakete mit DisplayName +$raw = Get-ItemProperty HKLM:\Software\Microsoft\Windows\CurrentVersion\Uninstall\* | + Where-Object { $_.DisplayName -and ($_.DisplayName -match 'Python|OpenSSH|nginx') } | + ForEach-Object { "$($_.DisplayName)=unknown" } + +# Fallback +if (-not $raw) { + $raw = @("none=none") +} + +# Paketzeichenfolge zusammenbauen +$packageString = "os=$os;kernel=$kernel;" + ($raw -join ";") + +# Body zusammenbauen +$body = "$hostname,$timestamp,$packageString" + +# Header +$headers = @{ + "Authorization" = "Bearer $apiToken" +} + +# Abschicken +try { + Invoke-RestMethod -Uri $apiUrl -Method Post -Headers $headers -Body $body + Write-Host "✅ Update-Log gesendet von $hostname" +} +catch { + Write-Warning "❌ Fehler beim Senden: $_" +} diff --git a/send-update.log.sh b/send-update.log.sh new file mode 100644 index 0000000..687bd88 --- /dev/null +++ b/send-update.log.sh @@ -0,0 +1,20 @@ +#!/bin/bash + +API_URL="http://192.168.18.4:8000/api/updatecheck/" +API_TOKEN="9d207bf0-10f5-4d8f-a479-22ff5aeff8d1" + +HOSTNAME=$(hostname) +TIMESTAMP=$(date -Iseconds) +OS=$(lsb_release -ds 2>/dev/null || echo "Unknown") +KERNEL=$(uname -r) + +# Pakete +PACKAGES="python3|openssh-server|nginx" +PACKAGE_VERSIONS=$(dpkg-query -W -f='${binary:Package}=${Version}\n' 2>/dev/null | grep -E "$PACKAGES" | tr '\n' ';') + +# Zusammenbauen +BODY="$HOSTNAME,$TIMESTAMP,os=$OS;kernel=$KERNEL;$PACKAGE_VERSIONS" + +curl -s -X POST "$API_URL" \ + -H "Authorization: Bearer $API_TOKEN" \ + -d "$BODY"