v1.1: Update-UI mit Verlauf, Referenzeditor für Pakete, Referenzskript (automatische Versionsupdates mit POST-Integration)

This commit is contained in:
chickenandrice02 2025-06-26 14:06:37 +02:00
parent 029caa8565
commit 06515b8d43
8 changed files with 1412 additions and 148 deletions

View file

@ -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

1143
curl.txt Normal file

File diff suppressed because it is too large Load diff

44
gen_ref_vers.py Normal file
View file

@ -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: <dd>VERSION</dd>
match = re.search(r'Current version:</dt>\s*<dd>([^<]+)</dd>', 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")

298
main.py
View file

@ -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("""
<!DOCTYPE html>
<html>
<head>
<meta charset="utf-8">
<title>UpdateLog Übersicht</title>
<style>
body { font-family: sans-serif; margin: 2rem; }
button { margin: 0.2rem 0; padding: 0.4rem 1rem; cursor: pointer; }
table { border-collapse: collapse; width: 100%%; margin-top: 0.5rem; }
th, td { border: 1px solid #ccc; padding: 6px 12px; text-align: left; }
th { background-color: #f4f4f4; }
.ok { background-color: #d4edda; }
.outdated { background-color: #f8d7da; }
.host-block { margin-bottom: 2rem; }
</style>
<script>
function toggle(hostId) {
const el = document.getElementById(hostId);
el.style.display = (el.style.display === "none") ? "block" : "none";
}
</script>
</head>
<body>
<h1>UpdateLog Übersicht</h1>
{% for server, content in data.items() %}
<div class="host-block">
<button onclick="toggle('host-{{ server }}')">
{{ server }} (Stand: {{ content.timestamp }})
</button>
<a href="/host/{{ server }}" style="margin-left: 1rem;">🔍 Verlauf anzeigen</a>
<div id="host-{{ server }}" style="display:none">
<table>
<tr><th>Paket</th><th>Installiert</th><th>Referenz</th><th>Status</th></tr>
{% for pkg, info in content.packages.items() %}
<tr class="{{ 'ok' if info.current == info.expected else 'outdated' }}">
<td>{{ pkg }}</td>
<td>{{ info.current }}</td>
<td>{{ info.expected }}</td>
<td>{{ '' if info.current == info.expected else '' }}</td>
</tr>
{% endfor %}
</table>
</div>
</div>
{% endfor %}
</body>
</html>
""")
HISTORY_TEMPLATE = Template("""
<!DOCTYPE html>
<html>
<head>
<meta charset="utf-8">
<title>Verlauf: {{ hostname }}</title>
<style>
body { font-family: sans-serif; margin: 2rem; }
table { border-collapse: collapse; width: 100%%; margin-top: 1rem; }
th, td { border: 1px solid #ccc; padding: 6px 12px; text-align: left; }
th { background-color: #f4f4f4; }
</style>
</head>
<body>
<h1>Update-Verlauf für {{ hostname }}</h1>
<a href="/"> Zurück zur Übersicht</a>
{% for entry in history %}
<h3>{{ entry.timestamp }}</h3>
<table>
<tr><th>Paket</th><th>Installiert</th></tr>
{% for pkg, ver in entry.packages.items() %}
<tr>
<td>{{ pkg }}</td>
<td>{{ ver }}</td>
</tr>
{% endfor %}
</table>
{% endfor %}
</body>
</html>
""")
# ================= 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("""
<!DOCTYPE html>
<html>
<head>
<meta charset="utf-8">
<title>Update Übersicht</title>
<style>
body { font-family: sans-serif; margin: 2rem; }
.ok { background: #d4edda; }
.outdated { background: #f8d7da; }
.missing { background: #fff3cd; }
table { border-collapse: collapse; width: 100%; margin-top: 1rem; }
th, td { border: 1px solid #ccc; padding: 4px 10px; }
#search { margin-bottom: 1rem; padding: 4px; width: 250px; }
</style>
<script>
function toggle(id) {
const el = document.getElementById(id);
el.style.display = el.style.display === "none" ? "block" : "none";
}
function filterTable() {
let q = document.getElementById("search").value.toLowerCase();
document.querySelectorAll(".host-block").forEach(div => {
div.style.display = div.innerText.toLowerCase().includes(q) ? "block" : "none";
});
}
</script>
</head>
<body>
<h1>Update Übersicht</h1>
<input id="search" placeholder="🔍 Host oder Paket..." onkeyup="filterTable()">
<a href="/edit"> Referenz bearbeiten</a>
{% for host, d in data.items() %}
<div class="host-block">
<button onclick="toggle('b-{{ host }}')">{{ host }} (Stand: {{ d.timestamp }})</button>
<a href="/host/{{ host }}">🔍 Verlauf</a>
<div id="b-{{ host }}" style="display:none">
<table>
<tr><th>Paket</th><th>Installiert</th><th>Referenz</th><th>Status</th></tr>
{% for p, info in d.packages.items() %}
<tr class="{% if not info.expected %}missing{% elif info.current != info.expected %}outdated{% else %}ok{% endif %}">
<td>{{ p }}</td>
<td>{{ info.current }}</td>
<td>{{ info.expected or '' }}</td>
<td>
{% if not info.expected %} fehlt
{% elif info.current != info.expected %}
{% else %}
{% endif %}
</td>
</tr>
{% endfor %}
</table>
</div>
</div>
{% endfor %}
</body>
</html>
""")
HOST_TEMPLATE = Template("""
<!DOCTYPE html>
<html>
<head><meta charset="utf-8"><title>{{ hostname }}</title>
<style>
body { font-family: sans-serif; margin: 2rem; }
table { border-collapse: collapse; width: 100%; margin-top: 1rem; }
th, td { border: 1px solid #ccc; padding: 4px 10px; }
</style></head>
<body>
<h1>Verlauf: {{ hostname }}</h1>
<a href="/"> Zurück</a>
{% for h in history %}
<h3>{{ h.timestamp }}</h3>
<table>
<tr><th>Paket</th><th>Version</th></tr>
{% for p, v in h.packages.items() %}
<tr><td>{{ p }}</td><td>{{ v }}</td></tr>
{% endfor %}
</table>
{% endfor %}
</body>
</html>
""")
EDIT_TEMPLATE = Template("""
<!DOCTYPE html>
<html>
<head><meta charset="utf-8"><title>Referenz bearbeiten</title>
<style>body { font-family: sans-serif; margin: 2rem; }</style>
</head><body>
<h1>Referenzdatei bearbeiten</h1>
<a href="/"> Zurück</a>
<form method="post">
<textarea name="content" style="width:100%;height:400px;">{{ content }}</textarea><br>
<button type="submit">💾 Speichern</button>
</form>
</body>
</html>
""")

View file

@ -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"
}

View file

@ -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"

41
send-update-log-win.ps1 Normal file
View file

@ -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: $_"
}

20
send-update.log.sh Normal file
View file

@ -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"