Compare commits

..

5 Commits

Author SHA1 Message Date
ba11861336 Merge pull request 'dev' (#1) from dev into main
Reviewed-on: #1
2025-10-12 13:44:14 +02:00
df000eace9 revert 74617a1e0f
revert srv/poe_manager/templates/switche.html aktualisiert
2025-10-07 18:19:42 +02:00
74617a1e0f srv/poe_manager/templates/switche.html aktualisiert 2025-10-07 18:17:58 +02:00
bc80b5fcf1 srv/poe_manager/create_db.py aktualisiert 2025-09-30 19:11:09 +02:00
0c82ce6100 srv/poe_manager/create_db.py aktualisiert 2025-09-30 19:10:13 +02:00
9 changed files with 93 additions and 209 deletions

View File

@@ -3,9 +3,8 @@ from flask import Flask, render_template, request, redirect, url_for, flash
from flask_login import LoginManager, login_user, login_required, logout_user, UserMixin, current_user from flask_login import LoginManager, login_user, login_required, logout_user, UserMixin, current_user
from flask_bcrypt import Bcrypt from flask_bcrypt import Bcrypt
from cryptography.fernet import Fernet from cryptography.fernet import Fernet
from datetime import datetime import sqlite3
from collections import defaultdict import glob, os, re
import sqlite3, glob, os, re
app = Flask(__name__) app = Flask(__name__)
app.secret_key = "309cc4d5ce1fe7486ae25cbd232bbdfe6a72539c03f0127d372186dbdc0fc928" app.secret_key = "309cc4d5ce1fe7486ae25cbd232bbdfe6a72539c03f0127d372186dbdc0fc928"
@@ -82,34 +81,6 @@ def logout():
logout_user() logout_user()
return redirect(url_for('login')) return redirect(url_for('login'))
def get_last_seen(dev_name: str):
"""Letztes Mal, dass ein Gerät erreichbar war."""
log_files = glob.glob("/var/log/rpi-*.log")
if not log_files:
return None
latest_time = None
# alle Logs durchgehen
for logfile in sorted(log_files):
with open(logfile, "r") as f:
for line in f:
line = line.strip()
if f"{dev_name} ist erreichbar!" in line:
try:
ts_str = line.split(" ")[0] + " " + line.split(" ")[1] # "YYYY-MM-DD HH:MM:SS"
ts = datetime.strptime(ts_str, "%Y-%m-%d %H:%M:%S")
if latest_time is None or ts > latest_time:
latest_time = ts
except Exception:
continue
if latest_time:
datetime_str = latest_time.strftime("Zuletzt Online am %d.%m.%Y um %H:%M Uhr")
return f"{datetime_str}"
return None
@app.route("/") @app.route("/")
@login_required @login_required
def index(): def index():
@@ -119,47 +90,26 @@ def index():
c.execute("SELECT mac, name, is_active FROM devices ORDER BY name ASC") c.execute("SELECT mac, name, is_active FROM devices ORDER BY name ASC")
devices = c.fetchall() devices = c.fetchall()
devices = sorted(devices, key=lambda d: d[1][0].upper()) # Intervall aus DB (Minuten) laden
grouped_devices = defaultdict(list)
for d in devices:
first_letter = d[1][:2].upper()
grouped_devices[first_letter].append(d)
# Intervall aus DB laden
c.execute("SELECT value FROM settings WHERE key='interval'") c.execute("SELECT value FROM settings WHERE key='interval'")
row = c.fetchone() row = c.fetchone()
interval = int(row[0]) if row else 5 interval = int(row[0]) if row else 5 # Default 5 Minuten
conn.close() conn.close()
# Status aus Logdateien ermitteln # Status aus letztem Log ermitteln
import glob, os
log_files = glob.glob("/var/log/rpi-*.log") log_files = glob.glob("/var/log/rpi-*.log")
status_dict = {} status_dict = {}
last_seen_dict = {}
if log_files: if log_files:
latest_log = max(log_files, key=os.path.getctime) latest_log = max(log_files, key=os.path.getctime)
with open(latest_log, "r") as f: with open(latest_log, "r") as f:
lines = f.readlines() for line in f:
for dev in devices:
if dev[1] in line:
status_dict[dev[0]] = "online" if "erreichbar" in line else "offline"
for dev in devices: # Template rendern mit Devices, Status und Intervall
last_status = None return render_template("index.html", devices=devices, status=status_dict, interval=interval)
for line in reversed(lines):
if f"{dev[1]} ist erreichbar!" in line:
last_status = "online"
break
elif f"{dev[1]} ist nicht erreichbar!" in line:
last_status = "offline"
break
if last_status:
status_dict[dev[0]] = last_status
if last_status == "offline":
last_seen_dict[dev[0]] = get_last_seen(dev[1])
else:
status_dict[dev[0]] = "unbekannt"
return render_template("index.html", grouped_devices=grouped_devices, devices=devices, status=status_dict, last_seen=last_seen_dict, interval=interval)
@app.route("/settings", methods=["GET", "POST"]) @app.route("/settings", methods=["GET", "POST"])
@login_required @login_required
@@ -229,7 +179,7 @@ def devices():
switch_hostname = request.form.get('switch_hostname') switch_hostname = request.form.get('switch_hostname')
is_active = 1 if 'is_active' in request.form else 0 is_active = 1 if 'is_active' in request.form else 0
if not all([mac, rpi_ip, name]): if not all([mac, rpi_ip, port, name, switch_hostname]):
flash("Alle Felder müssen ausgefüllt sein!") flash("Alle Felder müssen ausgefüllt sein!")
return redirect(url_for('devices')) return redirect(url_for('devices'))
@@ -266,22 +216,22 @@ def devices():
old_mac = request.form.get('old_mac') old_mac = request.form.get('old_mac')
mac = request.form.get('mac') mac = request.form.get('mac')
rpi_ip = request.form.get('rpi_ip') rpi_ip = request.form.get('rpi_ip')
port = request.form.get('port') or None port = request.form.get('port')
name = request.form.get('name') name = request.form.get('name')
switch_hostname = request.form.get('switch_hostname') or None switch_hostname = request.form.get('switch_hostname') or None
is_active = 1 if 'is_active' in request.form else 0 is_active = 1 if 'is_active' in request.form else 0
# --- Nur Switch ändern --- # --- Prüfen, ob es sich um eine Switchnderung handelt ---
# Prüfen, ob nur das Switch-Feld gesendet wurde und die anderen Felder leer sind if mac is None and rpi_ip is None and port is None and name is None and switch_hostname:
if 'switch_hostname' in request.form and all(not f for f in [mac, rpi_ip, name]): # Nur den Switch ändern
device = conn.execute("SELECT name, switch_hostname FROM devices WHERE mac=?", (old_mac,)).fetchone() device = conn.execute("SELECT name, switch_hostname FROM devices WHERE mac=?", (old_mac,)).fetchone()
if not device: if not device:
flash("Gerät nicht gefunden!") flash("Gerät nicht gefunden!")
return redirect(url_for('devices')) return redirect(url_for('devices'))
old_switch = device['switch_hostname'] or "unbekannt" old_switch = device['switch_hostname'] or "unbekannt"
device_name = device['name'] device_name = device['name']
switch_hostname = request.form.get('switch_hostname') or ""
try: try:
conn.execute(""" conn.execute("""
@@ -290,36 +240,27 @@ def devices():
WHERE mac=? WHERE mac=?
""", (switch_hostname, old_mac)) """, (switch_hostname, old_mac))
conn.commit() conn.commit()
flash(f"Switch von {device_name} geändert: {old_switch}{switch_hostname or 'Kein Switch'}") flash(f"Switch von {device_name} geändert: {old_switch}{switch_hostname}")
except sqlite3.IntegrityError: except sqlite3.IntegrityError:
flash("Fehler beim Ändern des Switch!") flash("Fehler beim Ändern des Switch!")
return redirect(url_for('devices')) return redirect(url_for('devices'))
# --- Normales Gerät bearbeiten --- if not all([old_mac, mac, rpi_ip, port, name]):
# Pflichtfelder prüfen flash("Alle Felder müssen ausgefüllt sein!")
if not all([old_mac, mac, rpi_ip, name]): return redirect(url_for('devices'))
flash("Felder 'MAC', 'IP' und 'Name' müssen ausgefüllt sein!")
return redirect(url_for('devices'))
# Prüfen auf doppelte IP außer das aktuelle Gerät # Prüfen auf doppelte IP außer das aktuelle Gerät
ip_device = conn.execute( ip_device = conn.execute("SELECT name FROM devices WHERE rpi_ip=? AND mac<>?", (rpi_ip, old_mac)).fetchone()
"SELECT name FROM devices WHERE rpi_ip=? AND mac<>?",
(rpi_ip, old_mac)
).fetchone()
if ip_device: if ip_device:
flash(f"IP-Adresse existiert bereits für Gerät '{ip_device['name']}'!") flash(f"IP-Adresse existiert bereits für Gerät '{ip_device['name']}'!")
return redirect(url_for('devices')) return redirect(url_for('devices'))
# Prüfen auf doppelte MAC außer das aktuelle Gerät # Prüfen auf doppelte MAC außer das aktuelle Gerät
mac_device = conn.execute( mac_device = conn.execute("SELECT name FROM devices WHERE mac=? AND mac<>?", (mac, old_mac)).fetchone()
"SELECT name FROM devices WHERE mac=? AND mac<>?",
(mac, old_mac)
).fetchone()
if mac_device: if mac_device:
flash(f"MAC-Adresse existiert bereits für Gerät '{mac_device['name']}'!") flash(f"MAC-Adresse existiert bereits für Gerät '{mac_device['name']}'!")
return redirect(url_for('devices')) return redirect(url_for('devices'))
# Update durchführen
try: try:
conn.execute(""" conn.execute("""
UPDATE devices UPDATE devices
@@ -331,7 +272,6 @@ def devices():
except sqlite3.IntegrityError: except sqlite3.IntegrityError:
flash("Fehler beim Aktualisieren des Geräts!") flash("Fehler beim Aktualisieren des Geräts!")
# ----------------------- # -----------------------
# Gerät löschen # Gerät löschen
# ----------------------- # -----------------------
@@ -360,7 +300,7 @@ def devices():
switches.hostname AS switch_hostname switches.hostname AS switch_hostname
FROM devices FROM devices
LEFT JOIN switches ON devices.switch_hostname = switches.hostname LEFT JOIN switches ON devices.switch_hostname = switches.hostname
ORDER BY switches.hostname ASC, devices.name ASC ORDER BY switches.hostname ASC
""").fetchall() """).fetchall()
conn.close() conn.close()
@@ -494,43 +434,36 @@ def logs():
return render_template('logs.html', log_content=log_content, log_name=os.path.basename(latest_log), interval=interval) return render_template('logs.html', log_content=log_content, log_name=os.path.basename(latest_log), interval=interval)
def load_device_status(): def load_device_status():
"""
Liest das aktuellste rpi-Logfile und extrahiert den letzten Status jedes Devices.
Gibt ein Dictionary zurück: {Device-Name: 'online'/'offline'}
"""
status = {} status = {}
# Devices aus DB laden (Name → MAC)
conn = sqlite3.connect("sqlite.db")
conn.row_factory = sqlite3.Row
devices = conn.execute("SELECT mac, name FROM devices").fetchall()
name_to_mac = {d['name']: d['mac'] for d in devices}
conn.close()
# Logfile
log_files = glob.glob("/var/log/rpi-*.log") log_files = glob.glob("/var/log/rpi-*.log")
if not log_files: if not log_files:
return status return status
latest_log = max(log_files, key=os.path.getctime) latest_log = max(log_files, key=os.path.getctime)
# Jede Zeile des Logs lesen
with open(latest_log, "r") as f:
lines = f.readlines()
# Regex für Ping-Ergebnisse
online_re = re.compile(r"(\S+) ist erreichbar!") online_re = re.compile(r"(\S+) ist erreichbar!")
offline_re = re.compile(r"(\S+) ist nicht erreichbar!") offline_re = re.compile(r"(\S+) ist nicht erreichbar!")
with open(latest_log, "r") as f: for line in lines:
for line in f: line = line.strip()
line = line.strip() m_online = online_re.search(line)
m_online = online_re.search(line) m_offline = offline_re.search(line)
m_offline = offline_re.search(line) if m_online:
if m_online: status[m_online.group(1)] = 'online'
name = m_online.group(1) elif m_offline:
mac = name_to_mac.get(name) status[m_offline.group(1)] = 'offline'
if mac:
status[mac] = 'online'
elif m_offline:
name = m_offline.group(1)
mac = name_to_mac.get(name)
if mac:
status[mac] = 'offline'
return status return status
@app.route("/users", methods=["GET", "POST"]) @app.route("/users", methods=["GET", "POST"])
@login_required @login_required
def users(): def users():

View File

@@ -6,10 +6,7 @@ def generate_ips_list():
conn = sqlite3.connect(DB_PATH) conn = sqlite3.connect(DB_PATH)
conn.row_factory = sqlite3.Row conn.row_factory = sqlite3.Row
switches = {row['hostname']: row for row in conn.execute( switches = {row['hostname']: row for row in conn.execute("SELECT hostname, ip, username, password FROM switches")}
"SELECT hostname, ip, username, password FROM switches"
)}
devices = conn.execute(""" devices = conn.execute("""
SELECT mac, rpi_ip, port, name, switch_hostname SELECT mac, rpi_ip, port, name, switch_hostname
FROM devices FROM devices
@@ -19,17 +16,11 @@ def generate_ips_list():
for dev in devices: for dev in devices:
switch = switches.get(dev['switch_hostname']) switch = switches.get(dev['switch_hostname'])
if switch: if not switch:
switch_ip = switch['ip'] continue
switch_user = switch['username'] password = decrypt_password(switch['password'])
switch_pass = decrypt_password(switch['password'])
else:
switch_ip = ""
switch_user = ""
switch_pass = ""
port = dev['port'] or "" port = dev['port'] or ""
print(f"{dev['rpi_ip']}:{dev['name']}:{switch_ip}:{dev['switch_hostname'] or 'kein Switch'}:{port}:{switch_user}:{switch_pass}") print(f"{dev['rpi_ip']}:{dev['name']}:{switch['ip']}:{switch['hostname']}:{port}:{switch['username']}:{password}")
if __name__ == "__main__": if __name__ == "__main__":
generate_ips_list() generate_ips_list()

Binary file not shown.

View File

@@ -87,33 +87,25 @@ pre {
#log-container { #log-container {
position: relative; position: relative;
height: calc(100vh - 252px); /* Füllt die Seite minus Header */ height: calc(100vh - 150px); /* Füllt die Seite minus Header */
padding: 1rem; /* optional, Abstand innen */
box-sizing: border-box; /* damit Padding nicht die Höhe sprengt */
display: flex;
flex-direction: column;
} }
#log-box { #log-box {
flex: 1 1 auto; /* füllt den Container, berücksichtigt Header/Padding */ height: 97%;
overflow: auto; overflow: auto;
white-space: pre-wrap; white-space: pre-wrap;
font-family: monospace; font-family: monospace;
border: 1px solid #dee2e6; border: 1px solid #dee2e6; /* Bootstrap-like border */
padding: 1rem;
background-color: #f8f9fa; background-color: #f8f9fa;
padding: 0.5rem;
} }
#refresh-timer { #refresh-timer {
position: absolute; position: absolute;
bottom: 18px; bottom: 10px;
right: 30px; right: 10px;
font-size: 0.9em; font-size: 0.9em;
color: gray; color: gray;
background-color: rgba(255,255,255,0.7); /* optional, besser lesbar */
padding: 2px 4px;
border-radius: 3px;
pointer-events: none; /* damit Scrollbar nicht blockiert wird */
} }
/* Tabelle anpassen */ /* Tabelle anpassen */

View File

@@ -8,7 +8,7 @@
<button class="btn btn-success mb-3" data-bs-toggle="modal" data-bs-target="#deviceModal" onclick="openDeviceModal()">Neues Gerät hinzufügen</button> <button class="btn btn-success mb-3" data-bs-toggle="modal" data-bs-target="#deviceModal" onclick="openDeviceModal()">Neues Gerät hinzufügen</button>
{% endif %} {% endif %}
<table class="table table-striped"> <table class="table table-bordered">
<thead> <thead>
<tr> <tr>
<th>Hostname</th> <th>Hostname</th>
@@ -89,12 +89,11 @@
</div> </div>
<div class="mb-3"> <div class="mb-3">
<label>Port</label> <label>Port</label>
<input type="text" name="port" class="form-control" placeholder="z.B. 3"> <input type="text" name="port" class="form-control" required placeholder="z.B. 3">
</div> </div>
<div class="mb-3"> <div class="mb-3">
<label>Switch (optional)</label> <label>Switch</label>
<select name="switch_hostname" class="form-select"> <select name="switch_hostname" class="form-select" required>
<option value="">Kein Switch</option>
{% for sw in switches %} {% for sw in switches %}
<option value="{{ sw['hostname'] }}">{{ sw['hostname'] }}</option> <option value="{{ sw['hostname'] }}">{{ sw['hostname'] }}</option>
{% endfor %} {% endfor %}
@@ -138,7 +137,7 @@
</div> </div>
<div class="mb-3"> <div class="mb-3">
<label>Port</label> <label>Port</label>
<input type="text" name="port" id="edit_port" class="form-control" placeholder="z.B. 3"> <input type="text" name="port" id="edit_port" class="form-control" required placeholder="z.B. 3">
</div> </div>
</div> </div>
<div class="modal-footer"> <div class="modal-footer">
@@ -162,8 +161,7 @@
<button type="button" class="btn-close" data-bs-dismiss="modal"></button> <button type="button" class="btn-close" data-bs-dismiss="modal"></button>
</div> </div>
<div class="modal-body"> <div class="modal-body">
<select name="switch_hostname" id="switch_select" class="form-select"> <select name="switch_hostname" id="switch_select" class="form-select" required>
<option value="">Kein Switch</option>
{% for sw in switches %} {% for sw in switches %}
<option value="{{ sw['hostname'] }}">{{ sw['hostname'] }}</option> <option value="{{ sw['hostname'] }}">{{ sw['hostname'] }}</option>
{% endfor %} {% endfor %}

View File

@@ -7,37 +7,29 @@
Nächste Prüfung in -- Sekunden Nächste Prüfung in -- Sekunden
</span> </span>
</h2> </h2>
<div class="row g-3"> <div class="row row-cols-1 row-cols-md-6 g-3">
{% for letter, group in grouped_devices.items() %} {% for d in devices %}
<div class="row g-3"> <div class="col">
{% for d in group %} <div class="card text-center p-2">
<div class="col-6 col-md-4 col-lg-3 col-xl-2"> <div class="card-header">{{ d[1] }}</div>
<div class="card text-center p-2" <div class="card-body">
{% if last_seen.get(d[0]) %} <span class="fw-bold" style="color:
title="{{ last_seen[d[0]] }}" {% if d[2] == 0 %}gray
{% elif status[d[0]] == 'offline' %} {% elif status[d[0]]=='online' %}green
title="Noch nie online" {% else %}red
{% endif %}> {% endif %};">
<div class="card-header">{{ d[1] }}</div> {% if d[2] == 0 %}
<div class="card-body"> Deaktiviert
<span class="fw-bold" style="color: {% else %}
{% if d[2] == 0 %}gray {% if status[d[0]] %}{{ status[d[0]]|capitalize }}{% else %}Unbekannt{% endif %}
{% elif status[d[0]] == 'online' %}green {% endif %}
{% else %}red </span>
{% endif %};">
{% if d[2] == 0 %}
Deaktiviert
{% else %}
{% if status[d[0]] %}{{ status[d[0]]|capitalize }}{% else %}Unbekannt{% endif %}
{% endif %}
</span>
</div>
</div> </div>
</div> </div>
{% endfor %}
</div> </div>
{% endfor %} {% endfor %}
</div> </div>
<script> <script>
document.addEventListener("DOMContentLoaded", () => { document.addEventListener("DOMContentLoaded", () => {
const intervalMinutes = {{ interval | int }}; // aus DB const intervalMinutes = {{ interval | int }}; // aus DB

View File

@@ -5,7 +5,7 @@
<!-- Button zum Hinzufügen --> <!-- Button zum Hinzufügen -->
<button class="btn btn-success mb-3" data-bs-toggle="modal" data-bs-target="#addSwitchModal">Neuen Switch hinzufügen</button> <button class="btn btn-success mb-3" data-bs-toggle="modal" data-bs-target="#addSwitchModal">Neuen Switch hinzufügen</button>
<table class="table table-striped"> <table class="table table-bordered">
<thead> <thead>
<tr> <tr>
<th>Hostname</th> <th>Hostname</th>

View File

@@ -8,7 +8,7 @@
<button class="btn btn-success mb-3" data-bs-toggle="modal" data-bs-target="#userModal" onclick="openUserModal()">Neuen Benutzer</button> <button class="btn btn-success mb-3" data-bs-toggle="modal" data-bs-target="#userModal" onclick="openUserModal()">Neuen Benutzer</button>
{% endif %} {% endif %}
<table class="table table-striped"> <table class="table table-bordered">
<thead> <thead>
<tr> <tr>
<th class="col-ip">Username</th> <th class="col-ip">Username</th>

View File

@@ -87,45 +87,23 @@ EOF
echo "" > "$LOGFILE" echo "" > "$LOGFILE"
MAX_PARALLEL=10 # maximal gleichzeitig laufende Geräte
while true; do while true; do
echo "--------------------------------------------------------------------" >> "$LOGFILE" echo "--------------------------------------------------------------------" >> "$LOGFILE"
python3 /srv/poe_manager/generate_ips.py | while IFS=: read -r rpi_ip dev_name switch_ip switch_hostname switch_port switch_user switch_pass; do python3 /srv/poe_manager/generate_ips.py | while IFS=: read -r rpi_ip dev_name switch_ip switch_hostname switch_port switch_user switch_pass; do
# Funktion für ein Gerät if ping -c 1 -W 2 "$rpi_ip" &> /dev/null; then
check_device() { echo "$(date '+%Y-%m-%d %H:%M:%S') $dev_name ist erreichbar!" >> "$LOGFILE"
local rpi_ip="$1" else
local dev_name="$2" echo "$(date '+%Y-%m-%d %H:%M:%S') $dev_name ist nicht erreichbar!" >> "$LOGFILE"
local switch_ip="$3"
local switch_hostname="$4"
local switch_port="$5"
local switch_user="$6"
local switch_pass="$7"
if ping -c 4 -W 1 "$rpi_ip" &> /dev/null; then # Nur PoE neu starten, wenn Port vorhanden ist
echo "$(date '+%Y-%m-%d %H:%M:%S') $dev_name ist erreichbar!" >> "$LOGFILE" if [ -n "$switch_port" ]; then
else disable_poe "$switch_ip" "$switch_port" "$switch_user" "$switch_pass"
echo "$(date '+%Y-%m-%d %H:%M:%S') $dev_name ist nicht erreichbar!" >> "$LOGFILE" echo "$(date '+%Y-%m-%d %H:%M:%S') $dev_name PoE auf Port $switch_port am Switch $switch_hostname deaktiviert." >> "$LOGFILE"
sleep 2
if [ -n "$switch_port" ] && [ "$switch_port" != "None" ]; then enable_poe "$switch_ip" "$switch_port" "$switch_user" "$switch_pass"
disable_poe "$switch_ip" "$switch_port" "$switch_user" "$switch_pass" echo "$(date '+%Y-%m-%d %H:%M:%S') $dev_name PoE auf Port $switch_port am Switch $switch_hostname aktiviert." >> "$LOGFILE"
echo "$(date '+%Y-%m-%d %H:%M:%S') $dev_name PoE auf Port $switch_port am Switch $switch_hostname deaktiviert." >> "$LOGFILE"
sleep 2
enable_poe "$switch_ip" "$switch_port" "$switch_user" "$switch_pass"
echo "$(date '+%Y-%m-%d %H:%M:%S') $dev_name PoE auf Port $switch_port am Switch $switch_hostname aktiviert." >> "$LOGFILE"
fi
fi fi
} fi
# Job in Hintergrund starten
check_device "$rpi_ip" "$dev_name" "$switch_ip" "$switch_hostname" "$switch_port" "$switch_user" "$switch_pass" &
# Limit auf MAX_PARALLEL Jobs
while [ "$(jobs -rp | wc -l)" -ge "$MAX_PARALLEL" ]; do
sleep 0.2
done
done done
wait # alle Hintergrundjobs beenden, bevor sleep
sleep "$SLEEP" sleep "$SLEEP"
done done