Initial commit – Update UI
This commit is contained in:
commit
029caa8565
5 changed files with 235 additions and 0 deletions
5
.gitignore
vendored
Normal file
5
.gitignore
vendored
Normal file
|
@ -0,0 +1,5 @@
|
||||||
|
__pycache__/
|
||||||
|
*.pyc
|
||||||
|
*.log
|
||||||
|
input/
|
||||||
|
.env
|
10
Dockerfile
Normal file
10
Dockerfile
Normal file
|
@ -0,0 +1,10 @@
|
||||||
|
FROM python:3.11-slim
|
||||||
|
|
||||||
|
WORKDIR /app
|
||||||
|
|
||||||
|
RUN pip install fastapi jinja2 uvicorn requests python-dateutil
|
||||||
|
|
||||||
|
COPY main.py .
|
||||||
|
COPY reference_versions.json ./reference_versions.json
|
||||||
|
|
||||||
|
CMD ["uvicorn", "main:app", "--host", "0.0.0.0", "--port", "8080"]
|
194
main.py
Normal file
194
main.py
Normal file
|
@ -0,0 +1,194 @@
|
||||||
|
from fastapi import FastAPI
|
||||||
|
from fastapi.responses import HTMLResponse
|
||||||
|
from jinja2 import Template
|
||||||
|
import requests
|
||||||
|
import json
|
||||||
|
from dateutil import parser, tz
|
||||||
|
|
||||||
|
app = FastAPI()
|
||||||
|
|
||||||
|
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)
|
||||||
|
response.raise_for_status()
|
||||||
|
return response.json().get("lines", [])
|
||||||
|
except Exception as e:
|
||||||
|
print(f"❌ Fehler beim Abrufen der Logs: {e}")
|
||||||
|
return []
|
||||||
|
|
||||||
|
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}")
|
||||||
|
return {}
|
||||||
|
|
||||||
|
def fetch_latest_per_host():
|
||||||
|
lines = fetch_raw_lines()
|
||||||
|
reference = fetch_reference()
|
||||||
|
latest_by_host = {}
|
||||||
|
|
||||||
|
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] = {
|
||||||
|
"timestamp": timestamp,
|
||||||
|
"raw": packages
|
||||||
|
}
|
||||||
|
|
||||||
|
result = {}
|
||||||
|
for hostname, entry in latest_by_host.items():
|
||||||
|
# Zeitstempel umrechnen
|
||||||
|
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:
|
||||||
|
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
|
||||||
|
}
|
||||||
|
|
||||||
|
return result
|
||||||
|
|
||||||
|
def fetch_history_for_host(hostname):
|
||||||
|
lines = fetch_raw_lines()
|
||||||
|
history = []
|
||||||
|
for line in lines:
|
||||||
|
parts = line.strip().split(",", 3)
|
||||||
|
if len(parts) < 4:
|
||||||
|
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
|
||||||
|
})
|
||||||
|
history.sort(key=lambda x: x["timestamp"], reverse=True)
|
||||||
|
return history
|
4
reference_versions.json
Normal file
4
reference_versions.json
Normal file
|
@ -0,0 +1,4 @@
|
||||||
|
{
|
||||||
|
"python3": "3.12.3-0ubuntu2",
|
||||||
|
"openssh-server": "1:9.6p1-3ubuntu13.11"
|
||||||
|
}
|
22
run_container.sh
Executable file
22
run_container.sh
Executable file
|
@ -0,0 +1,22 @@
|
||||||
|
#!/bin/bash
|
||||||
|
set -e
|
||||||
|
|
||||||
|
CONTAINER_NAME="update-ui"
|
||||||
|
IMAGE_NAME="update-ui:latest"
|
||||||
|
PORT="8080"
|
||||||
|
|
||||||
|
docker stop $CONTAINER_NAME 2>/dev/null || true
|
||||||
|
docker rm $CONTAINER_NAME 2>/dev/null || true
|
||||||
|
|
||||||
|
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 \
|
||||||
|
$IMAGE_NAME
|
||||||
|
|
||||||
|
echo "✅ Update-UI läuft unter http://localhost:$PORT"
|
Loading…
Add table
Add a link
Reference in a new issue