356 lines
12 KiB
Python
356 lines
12 KiB
Python
#!/usr/bin/env python3
|
|
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_bcrypt import Bcrypt
|
|
from werkzeug.security import generate_password_hash
|
|
from cryptography.fernet import Fernet
|
|
import sqlite3
|
|
import glob, os, re
|
|
|
|
app = Flask(__name__)
|
|
app.secret_key = "309cc4d5ce1fe7486ae25cbd232bbdfe6a72539c03f0127d372186dbdc0fc928"
|
|
bcrypt = Bcrypt(app)
|
|
|
|
login_manager = LoginManager()
|
|
login_manager.login_view = "login"
|
|
login_manager.init_app(app)
|
|
|
|
DB_PATH = "/srv/poe_manager/sqlite.db"
|
|
|
|
class User(UserMixin):
|
|
def __init__(self, id_, username, is_admin):
|
|
self.id = id_
|
|
self.username = username
|
|
self.is_admin = is_admin
|
|
|
|
def get_interval_seconds():
|
|
conn = get_db_connection()
|
|
row = conn.execute("SELECT value FROM settings WHERE key='check_interval'").fetchone()
|
|
conn.close()
|
|
return int(row['value']) if row else 300
|
|
|
|
with open("/srv/poe_manager/fernet.key", "rb") as f:
|
|
fernet = Fernet(f.read())
|
|
|
|
def encrypt_password(password: str) -> str:
|
|
return fernet.encrypt(password.encode()).decode()
|
|
|
|
def decrypt_password(token: str) -> str:
|
|
return fernet.decrypt(token.encode()).decode()
|
|
|
|
def get_db_connection():
|
|
conn = sqlite3.connect(DB_PATH)
|
|
conn.row_factory = sqlite3.Row
|
|
return conn
|
|
|
|
def get_devices():
|
|
"""
|
|
Liefert eine Liste aller Devices aus der Datenbank als Dictionaries.
|
|
"""
|
|
conn = get_db_connection()
|
|
devices = conn.execute("SELECT mac, rpi_ip, port, name, switch_hostname FROM devices").fetchall()
|
|
conn.close()
|
|
return devices
|
|
|
|
@login_manager.user_loader
|
|
def load_user(user_id):
|
|
conn = get_db_connection()
|
|
user = conn.execute("SELECT * FROM users WHERE id = ?", (user_id,)).fetchone()
|
|
conn.close()
|
|
if user:
|
|
return User(user['id'], user['username'], user['is_admin'])
|
|
return None
|
|
|
|
@app.route('/login', methods=['GET', 'POST'])
|
|
def login():
|
|
if request.method == 'POST':
|
|
username = request.form['username']
|
|
password = request.form['password']
|
|
conn = get_db_connection()
|
|
user = conn.execute("SELECT * FROM users WHERE username = ?", (username,)).fetchone()
|
|
conn.close()
|
|
if user and bcrypt.check_password_hash(user['password'], password):
|
|
login_user(User(user['id'], user['username'], user['is_admin']))
|
|
return redirect(url_for('index'))
|
|
else:
|
|
flash("Ungültiger Benutzername oder Passwort")
|
|
return render_template('login.html')
|
|
|
|
@app.route('/logout')
|
|
@login_required
|
|
def logout():
|
|
logout_user()
|
|
return redirect(url_for('login'))
|
|
|
|
@app.route('/')
|
|
@login_required
|
|
def index():
|
|
devices = get_devices() # alle Devices aus der DB
|
|
status = load_device_status()
|
|
|
|
# Standardstatus, falls Gerät im Log noch nicht auftaucht
|
|
for d in devices:
|
|
if d['name'] not in status:
|
|
status[d['name']] = 'offline'
|
|
|
|
interval_min = get_interval_seconds() // 60
|
|
return render_template('index.html', devices=devices, status=status, interval=interval_min)
|
|
|
|
@app.route('/settings', methods=['GET', 'POST'])
|
|
@login_required
|
|
def settings():
|
|
if not current_user.is_admin:
|
|
flash("Zugriff verweigert!")
|
|
return redirect(url_for('index'))
|
|
|
|
conn = get_db_connection()
|
|
interval_row = conn.execute("SELECT value FROM settings WHERE key='check_interval'").fetchone()
|
|
interval_min = int(interval_row['value']) // 60 if interval_row else 5 # Standard 5 Minuten
|
|
|
|
if request.method == 'POST':
|
|
new_interval_min = int(request.form['interval'])
|
|
conn.execute("INSERT OR REPLACE INTO settings (key, value) VALUES ('check_interval', ?)", (new_interval_min*60,))
|
|
conn.commit()
|
|
flash(f"Intervall auf {new_interval_min} Minuten gesetzt.")
|
|
interval_min = new_interval_min
|
|
|
|
conn.close()
|
|
return render_template('settings.html', interval=interval_min)
|
|
|
|
@app.route('/devices', methods=['GET', 'POST'])
|
|
@login_required
|
|
def devices():
|
|
conn = get_db_connection()
|
|
switches = conn.execute("SELECT hostname FROM switches").fetchall()
|
|
|
|
# Inline-Add
|
|
if request.method == 'POST' and 'add_device' in request.form:
|
|
if not current_user.is_admin:
|
|
flash("Zugriff verweigert!")
|
|
return redirect(url_for('devices'))
|
|
mac = request.form['mac']
|
|
rpi_ip = request.form['rpi_ip']
|
|
port = request.form['port']
|
|
name = request.form['name']
|
|
switch_hostname = request.form['switch_hostname']
|
|
try:
|
|
conn.execute("INSERT INTO devices (mac, rpi_ip, port, name, switch_hostname) VALUES (?, ?, ?, ?, ?)",
|
|
(mac, rpi_ip, port, name, switch_hostname))
|
|
conn.commit()
|
|
flash(f"Gerät {name} hinzugefügt.")
|
|
except sqlite3.IntegrityError:
|
|
flash("MAC existiert bereits oder Eingabefehler!")
|
|
|
|
# Inline-Edit
|
|
if request.method == 'POST' and 'edit_device' in request.form:
|
|
if not current_user.is_admin:
|
|
flash("Zugriff verweigert!")
|
|
return redirect(url_for('devices'))
|
|
old_mac = request.form['old_mac']
|
|
mac = request.form['mac']
|
|
rpi_ip = request.form['rpi_ip']
|
|
port = request.form['port']
|
|
name = request.form['name']
|
|
switch_hostname = request.form['switch_hostname']
|
|
try:
|
|
conn.execute("""
|
|
UPDATE devices
|
|
SET mac=?, rpi_ip=?, port=?, name=?, switch_hostname=?
|
|
WHERE mac=?
|
|
""", (mac, rpi_ip, port, name, switch_hostname, old_mac))
|
|
conn.commit()
|
|
flash(f"Gerät {name} aktualisiert.")
|
|
except sqlite3.IntegrityError:
|
|
flash("MAC existiert bereits oder Eingabefehler!")
|
|
|
|
# Inline-Delete
|
|
if request.method == 'POST' and 'delete_device' in request.form:
|
|
if not current_user.is_admin:
|
|
flash("Zugriff verweigert!")
|
|
return redirect(url_for('devices'))
|
|
del_mac = request.form['delete_device']
|
|
conn.execute("DELETE FROM devices WHERE mac=?", (del_mac,))
|
|
conn.commit()
|
|
flash(f"Gerät {del_mac} gelöscht.")
|
|
|
|
devices = conn.execute("""
|
|
SELECT devices.mac, devices.rpi_ip, devices.port, devices.name, switches.hostname AS switch_hostname
|
|
FROM devices
|
|
JOIN switches ON devices.switch_hostname = switches.hostname
|
|
""").fetchall()
|
|
conn.close()
|
|
interval_min = get_interval_seconds() // 60
|
|
return render_template('devices.html', devices=devices, switches=switches)
|
|
|
|
@app.route('/switches', methods=['GET', 'POST'])
|
|
@login_required
|
|
def switches():
|
|
conn = get_db_connection()
|
|
|
|
# Inline-Add
|
|
if request.method == 'POST' and 'add_switch' in request.form:
|
|
if not current_user.is_admin:
|
|
flash("Zugriff verweigert!")
|
|
return redirect(url_for('switches'))
|
|
hostname = request.form['hostname']
|
|
ip = request.form['ip']
|
|
username = request.form['username']
|
|
password = encrypt_password(request.form['password'])
|
|
try:
|
|
conn.execute("INSERT INTO switches (hostname, ip, username, password) VALUES (?, ?, ?, ?)",
|
|
(hostname, ip, username, password))
|
|
conn.commit()
|
|
flash(f"Switch {hostname} hinzugefügt.")
|
|
except sqlite3.IntegrityError:
|
|
flash("Hostname existiert bereits oder Eingabefehler!")
|
|
|
|
# Inline-Edit
|
|
if request.method == 'POST' and 'edit_switch' in request.form:
|
|
if not current_user.is_admin:
|
|
flash("Zugriff verweigert!")
|
|
return redirect(url_for('switches'))
|
|
old_hostname = request.form['old_hostname']
|
|
hostname = request.form['hostname']
|
|
ip = request.form['ip']
|
|
username = request.form['username']
|
|
password = encrypt_password(request.form['password'])
|
|
try:
|
|
conn.execute("""
|
|
UPDATE switches
|
|
SET hostname=?, ip=?, username=?, password=?
|
|
WHERE hostname=?
|
|
""", (hostname, ip, username, password, old_hostname))
|
|
conn.commit()
|
|
flash(f"Switch {hostname} aktualisiert.")
|
|
except sqlite3.IntegrityError:
|
|
flash("Hostname existiert bereits oder Eingabefehler!")
|
|
|
|
# Inline-Delete
|
|
if request.method == 'POST' and 'delete_switch' in request.form:
|
|
if not current_user.is_admin:
|
|
flash("Zugriff verweigert!")
|
|
return redirect(url_for('switches'))
|
|
del_hostname = request.form['delete_switch']
|
|
conn.execute("DELETE FROM switches WHERE hostname=?", (del_hostname,))
|
|
conn.commit()
|
|
flash(f"Switch {del_hostname} gelöscht.")
|
|
|
|
switches = conn.execute("SELECT hostname, ip, username FROM switches").fetchall()
|
|
conn.close()
|
|
return render_template('switche.html', switches=switches)
|
|
|
|
@app.route("/get_log")
|
|
@login_required
|
|
def get_log():
|
|
log_files = glob.glob("/var/log/rpi-*.log")
|
|
if not log_files:
|
|
return "Keine Logfiles gefunden."
|
|
|
|
latest_log = max(log_files, key=os.path.getctime)
|
|
|
|
try:
|
|
with open(latest_log, "r") as f:
|
|
return f.read()
|
|
except Exception as e:
|
|
return f"Fehler beim Lesen des Logs: {e}"
|
|
|
|
@app.route('/logs')
|
|
@login_required
|
|
def logs():
|
|
# alle Logfiles mit Muster rpi-YYYYMMDDHHMMSS.log
|
|
log_files = glob.glob("/var/log/rpi-*.log")
|
|
if not log_files:
|
|
return render_template('logs.html', log_content="Keine Logfiles gefunden.")
|
|
|
|
# das neuste Logfile auswählen
|
|
latest_log = max(log_files, key=os.path.getctime)
|
|
|
|
try:
|
|
with open(latest_log, "r") as f:
|
|
log_content = f.read()
|
|
except Exception as e:
|
|
log_content = f"Fehler beim Lesen des Logs: {e}"
|
|
|
|
return render_template('logs.html', log_content=log_content, log_name=os.path.basename(latest_log))
|
|
|
|
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 = {}
|
|
log_files = glob.glob("/var/log/rpi-*.log")
|
|
if not log_files:
|
|
return status
|
|
|
|
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!")
|
|
offline_re = re.compile(r"(\S+) ist nicht erreichbar!")
|
|
|
|
for line in lines:
|
|
line = line.strip()
|
|
m_online = online_re.search(line)
|
|
m_offline = offline_re.search(line)
|
|
if m_online:
|
|
status[m_online.group(1)] = 'online'
|
|
elif m_offline:
|
|
status[m_offline.group(1)] = 'offline'
|
|
|
|
return status
|
|
|
|
@app.route("/users", methods=["GET", "POST"])
|
|
@login_required
|
|
def users():
|
|
if not current_user.is_admin:
|
|
flash("Nur Admins dürfen Benutzer verwalten!")
|
|
return redirect(url_for("index"))
|
|
|
|
conn = sqlite3.connect("sqlite.db")
|
|
conn.row_factory = sqlite3.Row # wichtig für Template
|
|
c = conn.cursor()
|
|
|
|
# Neues Benutzer hinzufügen
|
|
if request.method == "POST":
|
|
if "add_user" in request.form:
|
|
username = request.form["username"]
|
|
password = generate_password_hash(request.form["password"])
|
|
is_admin = 1 if "is_admin" in request.form else 0
|
|
try:
|
|
c.execute("INSERT INTO users (username, password, is_admin) VALUES (?, ?, ?)",
|
|
(username, password, is_admin))
|
|
conn.commit()
|
|
flash(f"Benutzer {username} hinzugefügt!")
|
|
except sqlite3.IntegrityError:
|
|
flash("Benutzername existiert bereits!")
|
|
|
|
if "delete_user" in request.form:
|
|
user_id = request.form["delete_user"]
|
|
c.execute("DELETE FROM users WHERE id=?", (user_id,))
|
|
conn.commit()
|
|
flash("Benutzer gelöscht!")
|
|
|
|
if "edit_user" in request.form:
|
|
user_id = request.form["user_id"]
|
|
username = request.form["username"]
|
|
is_admin = 1 if "is_admin" in request.form else 0
|
|
c.execute("UPDATE users SET username=?, is_admin=? WHERE id=?",
|
|
(username, is_admin, user_id))
|
|
conn.commit()
|
|
flash("Benutzer geändert!")
|
|
|
|
# Alle Benutzer laden
|
|
c.execute("SELECT id, username, is_admin FROM users")
|
|
users_list = c.fetchall()
|
|
conn.close()
|
|
|
|
return render_template("users.html", users=users_list)
|
|
|
|
if __name__ == "__main__":
|
|
app.run(host='0.0.0.0', port=5000, debug=True)
|