Compare commits
230 Commits
latest
...
579f3f70b9
| Author | SHA1 | Date | |
|---|---|---|---|
| 579f3f70b9 | |||
| 9344690a31 | |||
| 1b74609d1b | |||
| 7f0871fa64 | |||
| 5ae38a9e20 | |||
| de23b132ed | |||
| 746972e321 | |||
| dfba8789a7 | |||
| 194e3cfbf9 | |||
| 5b33e2d656 | |||
| 3b7acfbac1 | |||
| 4e10fe8ac0 | |||
| 7ae131b5f4 | |||
| 09ee8c245e | |||
| bb07a598d7 | |||
| 4ef10fc39d | |||
| d73fda1935 | |||
| ad9dfcee7a | |||
| 4ae255b67f | |||
| a09454211b | |||
| aa2895ccf5 | |||
| 7c81f3a299 | |||
| cef96cd7cd | |||
| b4cc41c272 | |||
| 0d784a0530 | |||
| 34ef2c4e2a | |||
| a26a91ec5a | |||
| ef548b9532 | |||
| 466f3615ed | |||
| 3dc554e165 | |||
| 78ed058d69 | |||
| d3f209941b | |||
| 9f98d240ca | |||
| d88135fc12 | |||
| 34eae3ca9d | |||
| f05936226d | |||
| d6c15d3d49 | |||
| 7a1e9d88e5 | |||
| 2f1c7b5653 | |||
| d1e43a1220 | |||
| fe921254c8 | |||
| f27e73ae72 | |||
| 63b8e33434 | |||
| ad09b4f6a2 | |||
| ba11861336 | |||
| 59a9000de5 | |||
| 20b38194a5 | |||
| 772d190369 | |||
| 41f244cc59 | |||
| f6ce3763a5 | |||
| 5df7a13780 | |||
| cbce015edd | |||
| 9965d8d4fb | |||
| 1e94ac6dde | |||
| 25d778086d | |||
| b1eaecf3f2 | |||
| 56ed90f560 | |||
| 861c019324 | |||
| 459aeb22cf | |||
| fbfafd0055 | |||
| 91885932c8 | |||
| 4258ff07eb | |||
| af689c20b8 | |||
| 57ce7d0c6f | |||
| cea081585f | |||
| b2f9b701fe | |||
| 1a67dca394 | |||
| 349710e0d3 | |||
| 45ba78c374 | |||
| a444185933 | |||
| 8a5745e337 | |||
| e244092667 | |||
| ad0e48b979 | |||
| 3544675975 | |||
| 580cda1acc | |||
| 61b0137233 | |||
| 2b92c328e1 | |||
| 831da1479e | |||
| 122a4bd4a4 | |||
| cd654cf9a0 | |||
| 8ca32bf7a9 | |||
| 3e49f433aa | |||
| c2a74e54cb | |||
| 847c720055 | |||
| 250a131542 | |||
| e7458aca5f | |||
| 64e9ebc3ea | |||
| 3ab5e023fe | |||
| f479de8508 | |||
| 2d735fd4b4 | |||
| 9d50113c20 | |||
| 18a2b8facf | |||
| f8042669b4 | |||
| 0cc9d2ea0b | |||
| b7c6264082 | |||
| 8ff4515a17 | |||
| 225750f6bc | |||
| de6b5c7fe9 | |||
| 2c9a715d37 | |||
| c957a32a86 | |||
| 4fd22bd3ec | |||
| 44b7a9e0b0 | |||
| 9c25d532ec | |||
| 207eed1f00 | |||
| a2c2f6464f | |||
| 404f12e5fe | |||
| 7eec728796 | |||
| 93a93e2289 | |||
| d8a4b17b28 | |||
| f6c539ac8d | |||
| 5f0d024a15 | |||
| c4708bbb19 | |||
| cc40ae6276 | |||
| ca86166c5d | |||
| 6529fd4643 | |||
| 738b7f9cb0 | |||
| 0e80925a11 | |||
| a644de01ad | |||
| 8d0bdeb936 | |||
| aa8facfe28 | |||
| 1a5ba7b0ae | |||
| cce1de034a | |||
| 0fd53990e6 | |||
| d33c567769 | |||
| 02f9445f42 | |||
| 2d74cbce10 | |||
| 11857b224a | |||
| 4b25d4dcf6 | |||
| 5cdb336e4a | |||
| 38ad402048 | |||
| a29c46345d | |||
| 1e03dad68f | |||
| b00d55e1ad | |||
| 0d569dcd1b | |||
| 81afc82190 | |||
| 43d30dd822 | |||
| df000eace9 | |||
| 74617a1e0f | |||
| 98950d706c | |||
| ae3caefd18 | |||
| 0f6b87ba1f | |||
| bbbe53f0db | |||
| 290584e0d9 | |||
| 51594cbd2f | |||
| 79b980b069 | |||
| 53b966307a | |||
| 9b3d78ad79 | |||
| 72392c97b1 | |||
| 7a5de70fc1 | |||
| add19eeb4d | |||
| d3bdc46314 | |||
| 4b5f6abe99 | |||
| 178ea8ff58 | |||
| 119dbccf5d | |||
| a5ec09297f | |||
| 43a77700db | |||
| dd4f4106a5 | |||
| 202d13e552 | |||
| ecbba08074 | |||
| ffd2a03e6d | |||
| 83b4b5a32c | |||
| e9cfbe4678 | |||
| c9449dc756 | |||
| 02122f966a | |||
| a24b09e18d | |||
| b526f9d0a0 | |||
| c3186813d4 | |||
| 9af5740ed3 | |||
| ecad3cda88 | |||
| 022050400e | |||
| d248087326 | |||
| 183725b03f | |||
| 1fcdd27f6a | |||
| 99395089f9 | |||
| bcd2c30c70 | |||
| be43c85961 | |||
| ffcde260c4 | |||
| 8b130a072e | |||
| 9c895514e4 | |||
| 7af700119e | |||
| 9be5fdbeb4 | |||
| 862577632a | |||
| 1e37b9551b | |||
| d116d687b4 | |||
| c367531860 | |||
| bc80b5fcf1 | |||
| 96a93188f7 | |||
| 0c82ce6100 | |||
| c5f6e1c708 | |||
| 55513c7fae | |||
| 1136c97ce2 | |||
| 28a436df16 | |||
| a10343d37e | |||
| b465bd9c54 | |||
| 4a093c1317 | |||
| ca389f488c | |||
| c39289c584 | |||
| a25462c78d | |||
| fe936ceca9 | |||
| ca7620abaf | |||
| 57005486cb | |||
| 80d596b94c | |||
| 2f50633eff | |||
| 84a24c811b | |||
| d3cdc5d3bd | |||
| 3a454780a2 | |||
| 0f090d594e | |||
| b602ebe089 | |||
| c0c3ed4dd7 | |||
| 85d7872934 | |||
| f057dc65c5 | |||
| d948096494 | |||
| 8b39425b38 | |||
| 12564647a2 | |||
| b7256868ee | |||
| dc9e42fadf | |||
| e7f6ff6ee1 | |||
| d38b8e4873 | |||
| 70d30e95fb | |||
| e99e17be3b | |||
| 26fb560aac | |||
| b95b1fbb9f | |||
| 02e7adf6b9 | |||
| 649d0598f9 | |||
| c067d26456 | |||
| 02efd0e40a | |||
| 58d1660073 | |||
| d60a525bfb | |||
| f45a346601 | |||
| 1e0df286db |
47
.gitignore
vendored
Normal file
47
.gitignore
vendored
Normal file
@@ -0,0 +1,47 @@
|
|||||||
|
# Alles ignorieren
|
||||||
|
*
|
||||||
|
|
||||||
|
# .gitignore selbst tracken
|
||||||
|
!.gitignore
|
||||||
|
|
||||||
|
# Virtuelle Umgebung ignorieren
|
||||||
|
/srv/poe_manager/venv/
|
||||||
|
__pycache__/
|
||||||
|
|
||||||
|
# Logfiles ignorieren
|
||||||
|
*.log
|
||||||
|
|
||||||
|
# Systemd Services tracken
|
||||||
|
!etc/
|
||||||
|
!etc/systemd/
|
||||||
|
!etc/systemd/system/
|
||||||
|
!etc/systemd/system/rpi*.service
|
||||||
|
!etc/systemd/system/rpi*.timer
|
||||||
|
!etc/systemd/system/poe*.service
|
||||||
|
!etc/nginx/
|
||||||
|
!etc/nginx/sites-enabled/
|
||||||
|
!etc/nginx/sites-enabled/default
|
||||||
|
|
||||||
|
# Custom Scripts tracken
|
||||||
|
!usr/
|
||||||
|
!usr/local/
|
||||||
|
!usr/local/bin/
|
||||||
|
!usr/local/bin/custom/
|
||||||
|
!usr/local/bin/custom/*
|
||||||
|
|
||||||
|
# Web-App Dateien im poe_manager tracken
|
||||||
|
!/srv/
|
||||||
|
!/srv/poe_manager/
|
||||||
|
!/srv/poe_manager/*.*
|
||||||
|
!/srv/poe_manager/templates/
|
||||||
|
!/srv/poe_manager/templates/*.*
|
||||||
|
!/srv/poe_manager/static/
|
||||||
|
!/srv/poe_manager/static/css/
|
||||||
|
!/srv/poe_manager/static/css/*
|
||||||
|
!/srv/poe_manager/static/js/
|
||||||
|
!/srv/poe_manager/static/js/*
|
||||||
|
!/srv/poe_manager/static/images/
|
||||||
|
!/srv/poe_manager/static/images/*
|
||||||
|
|
||||||
|
# Optional: SQLite DB ignorieren (falls du nicht willst, dass Passwörter im Repo landen)
|
||||||
|
!/srv/poe_manager/sqlite.db
|
||||||
45
README.md
45
README.md
@@ -1,15 +1,42 @@
|
|||||||
# Aruba PoE
|
# PoE Manager Web-App
|
||||||
|
|
||||||
```
|
Webbasierte Verwaltung und Monitoring von PoE-Devices und Switches.
|
||||||
Dieses Repo konfiguriert auf Ubuntu/Debian einen Service,
|
Die App ermöglicht:
|
||||||
dieser Service ist in der Lage IPs zu Pingen und entsprechend daran POE auf vorgegebenen Ports an einem Aruba Switch
|
|
||||||
zu Aktivieren bzw. Deaktivieren.
|
|
||||||
```
|
|
||||||
|
|
||||||
Download:
|
- Anzeige von Device-Status (Online/Offline)
|
||||||
|
- Verwaltung von Devices und Switches
|
||||||
|
- Einstellung des Prüfintervalls
|
||||||
|
- Live-Log-Ansicht
|
||||||
|
- Benutzerverwaltung mit Adminrechten
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## **Installation (nach einem frischen Clone)**
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
wget -qO- --header 'Authorization:token 9031f8d227dd83ba601680bf3a9f6c2d26c1a970' https://gitea.int.eertmoed.net/WiS/Aruba-PoE/archive/latest.tar.gz | tar xvz ; bash /root/aruba-poe/install.sh ;
|
# Pakete installieren
|
||||||
|
sudo apt update
|
||||||
|
sudo apt install python3 python3-venv python3-pip nginx sqlite3 git nano -y
|
||||||
|
|
||||||
|
# Repo klonen
|
||||||
|
git clone https://gitea.int.eertmoed.net/WiS/Aruba-PoE.git /srv/poe_manager
|
||||||
|
cd /srv/poe_manager
|
||||||
|
|
||||||
|
# Virtuelle Umgebung erstellen
|
||||||
|
python3 -m venv venv
|
||||||
|
source venv/bin/activate
|
||||||
|
|
||||||
|
# Abhängigkeiten installieren
|
||||||
|
pip install --upgrade pip
|
||||||
|
pip install -r requirements.txt
|
||||||
|
|
||||||
|
# Datenbank initialisieren
|
||||||
|
python create_db.py
|
||||||
|
|
||||||
|
# Admin-Benutzer erstellen
|
||||||
|
python create_admin.py
|
||||||
|
|
||||||
|
# Web-App starten
|
||||||
|
python app.py --host=0.0.0.0 --port=5000
|
||||||
```
|
```
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
1
etc/nginx/sites-enabled/default
Symbolic link
1
etc/nginx/sites-enabled/default
Symbolic link
@@ -0,0 +1 @@
|
|||||||
|
/etc/nginx/sites-available/default
|
||||||
16
etc/systemd/system/poe_web.service
Normal file
16
etc/systemd/system/poe_web.service
Normal file
@@ -0,0 +1,16 @@
|
|||||||
|
[Unit]
|
||||||
|
Description=PoE Manager Web App
|
||||||
|
After=network.target
|
||||||
|
|
||||||
|
[Service]
|
||||||
|
Type=simple
|
||||||
|
User=root
|
||||||
|
WorkingDirectory=/srv/poe_manager
|
||||||
|
# Nutze die virtuelle Umgebung
|
||||||
|
ExecStart=/srv/poe_manager/venv/bin/python3 /srv/poe_manager/app.py
|
||||||
|
Restart=always
|
||||||
|
RestartSec=5
|
||||||
|
|
||||||
|
[Install]
|
||||||
|
WantedBy=multi-user.target
|
||||||
|
|
||||||
@@ -3,9 +3,12 @@ Description=RPI Ping Check
|
|||||||
After=network.target
|
After=network.target
|
||||||
|
|
||||||
[Service]
|
[Service]
|
||||||
ExecStart=/usr/local/bin/custom/poe.sh
|
Type=simple
|
||||||
|
User=root
|
||||||
|
WorkingDirectory=/srv/poe_manager
|
||||||
|
ExecStart=/srv/poe_manager/venv/bin/python3 /srv/poe_manager/poe_wrapper.py
|
||||||
Restart=always
|
Restart=always
|
||||||
RestartSec=5
|
RestartSec=5
|
||||||
|
|
||||||
[Install]
|
[Install]
|
||||||
WantedBy=multi-user.target
|
WantedBy=multi-user.target
|
||||||
|
|||||||
605
srv/poe_manager/app.py
Normal file
605
srv/poe_manager/app.py
Normal file
@@ -0,0 +1,605 @@
|
|||||||
|
#u!/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 cryptography.fernet import Fernet
|
||||||
|
from datetime import datetime
|
||||||
|
import sqlite3, 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, is_active FROM devices ORDER BY name ASC").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'))
|
||||||
|
|
||||||
|
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("/")
|
||||||
|
@login_required
|
||||||
|
def index():
|
||||||
|
# Geräte aus DB laden
|
||||||
|
conn = sqlite3.connect("sqlite.db")
|
||||||
|
c = conn.cursor()
|
||||||
|
c.execute("SELECT mac, name, is_active FROM devices ORDER BY name ASC")
|
||||||
|
devices = c.fetchall()
|
||||||
|
|
||||||
|
# Intervall aus DB laden
|
||||||
|
c.execute("SELECT value FROM settings WHERE key='interval'")
|
||||||
|
row = c.fetchone()
|
||||||
|
interval = int(row[0]) if row else 5
|
||||||
|
conn.close()
|
||||||
|
|
||||||
|
# Status aus Logdateien ermitteln
|
||||||
|
log_files = glob.glob("/var/log/rpi-*.log")
|
||||||
|
status_dict = {}
|
||||||
|
last_seen_dict = {}
|
||||||
|
|
||||||
|
if log_files:
|
||||||
|
latest_log = max(log_files, key=os.path.getctime)
|
||||||
|
with open(latest_log, "r") as f:
|
||||||
|
lines = f.readlines()
|
||||||
|
|
||||||
|
for dev in devices:
|
||||||
|
last_status = None
|
||||||
|
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", devices=devices, status=status_dict, last_seen=last_seen_dict, interval=interval)
|
||||||
|
|
||||||
|
@app.route("/settings", methods=["GET", "POST"])
|
||||||
|
@login_required
|
||||||
|
def settings():
|
||||||
|
if not current_user.is_admin:
|
||||||
|
flash("Nur Admins dürfen die Einstellungen ändern!")
|
||||||
|
return redirect(url_for("index"))
|
||||||
|
|
||||||
|
# Aktuellen Prüfintervall laden
|
||||||
|
conn = sqlite3.connect("sqlite.db")
|
||||||
|
c = conn.cursor()
|
||||||
|
c.execute("SELECT value FROM settings WHERE key='interval'")
|
||||||
|
row = c.fetchone()
|
||||||
|
interval = int(row[0]) if row else 5 # Default 5 Minuten
|
||||||
|
conn.close()
|
||||||
|
|
||||||
|
if request.method == "POST":
|
||||||
|
new_interval = int(request.form["interval"])
|
||||||
|
|
||||||
|
# Minuten und Sekunden berechnen
|
||||||
|
interval_minutes = new_interval
|
||||||
|
check_interval_seconds = new_interval * 60
|
||||||
|
|
||||||
|
conn = sqlite3.connect("sqlite.db")
|
||||||
|
c = conn.cursor()
|
||||||
|
# interval (Minuten)
|
||||||
|
c.execute("""
|
||||||
|
INSERT INTO settings (key, value) VALUES (?, ?)
|
||||||
|
ON CONFLICT(key) DO UPDATE SET value=excluded.value
|
||||||
|
""", ("interval", interval_minutes))
|
||||||
|
# check_interval (Sekunden)
|
||||||
|
c.execute("""
|
||||||
|
INSERT INTO settings (key, value) VALUES (?, ?)
|
||||||
|
ON CONFLICT(key) DO UPDATE SET value=excluded.value
|
||||||
|
""", ("check_interval", check_interval_seconds))
|
||||||
|
conn.commit()
|
||||||
|
conn.close()
|
||||||
|
|
||||||
|
# rpi-check.service neu starten
|
||||||
|
import subprocess
|
||||||
|
subprocess.run(["systemctl", "restart", "rpi-check.service"])
|
||||||
|
|
||||||
|
flash(f"Intervall auf {interval_minutes} Minuten gesetzt und Service neu gestartet!")
|
||||||
|
return redirect(url_for("settings"))
|
||||||
|
|
||||||
|
return render_template("settings.html", interval=interval)
|
||||||
|
|
||||||
|
@app.route('/devices', methods=['GET', 'POST'])
|
||||||
|
@login_required
|
||||||
|
def devices():
|
||||||
|
conn = get_db_connection()
|
||||||
|
switches = conn.execute("SELECT hostname FROM switches").fetchall()
|
||||||
|
|
||||||
|
if request.method == 'POST':
|
||||||
|
# -----------------------
|
||||||
|
# Gerät hinzufügen
|
||||||
|
# -----------------------
|
||||||
|
if 'add_device' in request.form:
|
||||||
|
if not current_user.is_admin:
|
||||||
|
flash("Zugriff verweigert!")
|
||||||
|
return redirect(url_for('devices'))
|
||||||
|
|
||||||
|
mac = request.form.get('mac')
|
||||||
|
rpi_ip = request.form.get('rpi_ip')
|
||||||
|
port = request.form.get('port')
|
||||||
|
name = request.form.get('name')
|
||||||
|
switch_hostname = request.form.get('switch_hostname')
|
||||||
|
is_active = 1 if 'is_active' in request.form else 0
|
||||||
|
|
||||||
|
if not all([mac, rpi_ip, name]):
|
||||||
|
flash("Alle Felder müssen ausgefüllt sein!")
|
||||||
|
return redirect(url_for('devices'))
|
||||||
|
|
||||||
|
# Prüfen auf doppelte IP
|
||||||
|
ip_device = conn.execute("SELECT name FROM devices WHERE rpi_ip=?", (rpi_ip,)).fetchone()
|
||||||
|
if ip_device:
|
||||||
|
flash(f"IP-Adresse existiert bereits für Gerät '{ip_device['name']}'!")
|
||||||
|
return redirect(url_for('devices'))
|
||||||
|
|
||||||
|
# Prüfen auf doppelte MAC
|
||||||
|
mac_device = conn.execute("SELECT name FROM devices WHERE mac=?", (mac,)).fetchone()
|
||||||
|
if mac_device:
|
||||||
|
flash(f"MAC-Adresse existiert bereits für Gerät '{mac_device['name']}'!")
|
||||||
|
return redirect(url_for('devices'))
|
||||||
|
|
||||||
|
try:
|
||||||
|
conn.execute("""
|
||||||
|
INSERT INTO devices (mac, rpi_ip, port, name, switch_hostname, is_active)
|
||||||
|
VALUES (?, ?, ?, ?, ?, ?)
|
||||||
|
""", (mac, rpi_ip, port, name, switch_hostname, is_active))
|
||||||
|
conn.commit()
|
||||||
|
flash(f"Gerät {name} hinzugefügt.")
|
||||||
|
except sqlite3.IntegrityError:
|
||||||
|
flash("Fehler beim Hinzufügen des Geräts!")
|
||||||
|
|
||||||
|
# -----------------------
|
||||||
|
# Gerät bearbeiten
|
||||||
|
# -----------------------
|
||||||
|
elif 'edit_device' in request.form:
|
||||||
|
if not current_user.is_admin:
|
||||||
|
flash("Zugriff verweigert!")
|
||||||
|
return redirect(url_for('devices'))
|
||||||
|
|
||||||
|
old_mac = request.form.get('old_mac')
|
||||||
|
mac = request.form.get('mac')
|
||||||
|
rpi_ip = request.form.get('rpi_ip')
|
||||||
|
port = request.form.get('port') or None
|
||||||
|
name = request.form.get('name')
|
||||||
|
switch_hostname = request.form.get('switch_hostname') or None
|
||||||
|
is_active = 1 if 'is_active' in request.form else 0
|
||||||
|
|
||||||
|
# --- Nur Switch ändern ---
|
||||||
|
# Prüfen, ob nur das Switch-Feld gesendet wurde und die anderen Felder leer sind
|
||||||
|
if 'switch_hostname' in request.form and all(not f for f in [mac, rpi_ip, name]):
|
||||||
|
device = conn.execute("SELECT name, switch_hostname FROM devices WHERE mac=?", (old_mac,)).fetchone()
|
||||||
|
if not device:
|
||||||
|
flash("Gerät nicht gefunden!")
|
||||||
|
return redirect(url_for('devices'))
|
||||||
|
|
||||||
|
old_switch = device['switch_hostname'] or "unbekannt"
|
||||||
|
device_name = device['name']
|
||||||
|
switch_hostname = request.form.get('switch_hostname') or ""
|
||||||
|
|
||||||
|
try:
|
||||||
|
conn.execute("""
|
||||||
|
UPDATE devices
|
||||||
|
SET switch_hostname=?
|
||||||
|
WHERE mac=?
|
||||||
|
""", (switch_hostname, old_mac))
|
||||||
|
conn.commit()
|
||||||
|
flash(f"Switch von {device_name} geändert: {old_switch} → {switch_hostname or 'Kein Switch'}")
|
||||||
|
except sqlite3.IntegrityError:
|
||||||
|
flash("Fehler beim Ändern des Switch!")
|
||||||
|
return redirect(url_for('devices'))
|
||||||
|
|
||||||
|
# --- Normales Gerät bearbeiten ---
|
||||||
|
# Pflichtfelder prüfen
|
||||||
|
if not all([old_mac, mac, rpi_ip, name]):
|
||||||
|
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
|
||||||
|
ip_device = conn.execute(
|
||||||
|
"SELECT name FROM devices WHERE rpi_ip=? AND mac<>?",
|
||||||
|
(rpi_ip, old_mac)
|
||||||
|
).fetchone()
|
||||||
|
if ip_device:
|
||||||
|
flash(f"IP-Adresse existiert bereits für Gerät '{ip_device['name']}'!")
|
||||||
|
return redirect(url_for('devices'))
|
||||||
|
|
||||||
|
# Prüfen auf doppelte MAC außer das aktuelle Gerät
|
||||||
|
mac_device = conn.execute(
|
||||||
|
"SELECT name FROM devices WHERE mac=? AND mac<>?",
|
||||||
|
(mac, old_mac)
|
||||||
|
).fetchone()
|
||||||
|
if mac_device:
|
||||||
|
flash(f"MAC-Adresse existiert bereits für Gerät '{mac_device['name']}'!")
|
||||||
|
return redirect(url_for('devices'))
|
||||||
|
|
||||||
|
# Update durchführen
|
||||||
|
try:
|
||||||
|
conn.execute("""
|
||||||
|
UPDATE devices
|
||||||
|
SET mac=?, rpi_ip=?, port=?, name=?, is_active=?
|
||||||
|
WHERE mac=?
|
||||||
|
""", (mac, rpi_ip, port, name, is_active, old_mac))
|
||||||
|
conn.commit()
|
||||||
|
flash(f"Gerät {name} aktualisiert.")
|
||||||
|
except sqlite3.IntegrityError:
|
||||||
|
flash("Fehler beim Aktualisieren des Geräts!")
|
||||||
|
|
||||||
|
|
||||||
|
# -----------------------
|
||||||
|
# Gerät löschen
|
||||||
|
# -----------------------
|
||||||
|
elif 'delete_device' in request.form:
|
||||||
|
if not current_user.is_admin:
|
||||||
|
flash("Zugriff verweigert!")
|
||||||
|
return redirect(url_for('devices'))
|
||||||
|
|
||||||
|
del_mac = request.form.get('delete_device')
|
||||||
|
if del_mac:
|
||||||
|
device = conn.execute("SELECT name FROM devices WHERE mac=?", (del_mac,)).fetchone()
|
||||||
|
hostname = device['name'] if device else del_mac
|
||||||
|
conn.execute("DELETE FROM devices WHERE mac=?", (del_mac,))
|
||||||
|
conn.commit()
|
||||||
|
flash(f"Gerät {hostname} gelöscht.")
|
||||||
|
else:
|
||||||
|
flash("Keine MAC-Adresse übermittelt!")
|
||||||
|
|
||||||
|
return redirect(url_for('devices'))
|
||||||
|
|
||||||
|
# -----------------------
|
||||||
|
# Devices für Anzeige
|
||||||
|
# -----------------------
|
||||||
|
devices = conn.execute("""
|
||||||
|
SELECT devices.mac, devices.rpi_ip, devices.port, devices.name, devices.is_active,
|
||||||
|
switches.hostname AS switch_hostname
|
||||||
|
FROM devices
|
||||||
|
LEFT JOIN switches ON devices.switch_hostname = switches.hostname
|
||||||
|
ORDER BY switches.hostname ASC, devices.name ASC
|
||||||
|
""").fetchall()
|
||||||
|
|
||||||
|
conn.close()
|
||||||
|
return render_template('devices.html', devices=devices, switches=switches)
|
||||||
|
|
||||||
|
@app.route('/devices/toggle/<mac>', methods=['POST'])
|
||||||
|
@login_required
|
||||||
|
def toggle_device(mac):
|
||||||
|
if not current_user.is_admin:
|
||||||
|
return {"success": False, "msg": "Zugriff verweigert!"}, 403
|
||||||
|
|
||||||
|
conn = get_db_connection()
|
||||||
|
device = conn.execute("SELECT is_active, name FROM devices WHERE mac=?", (mac,)).fetchone()
|
||||||
|
|
||||||
|
if not device:
|
||||||
|
conn.close()
|
||||||
|
return {"success": False, "msg": "Gerät nicht gefunden!"}, 404
|
||||||
|
|
||||||
|
new_status = 0 if device['is_active'] else 1
|
||||||
|
conn.execute("UPDATE devices SET is_active=? WHERE mac=?", (new_status, mac))
|
||||||
|
conn.commit()
|
||||||
|
conn.close()
|
||||||
|
|
||||||
|
status_text = "deaktiviert" if new_status == 0 else "aktiviert"
|
||||||
|
return {"success": True, "msg": f"Gerät {device['name']} wurde {status_text}.", "new_status": new_status}
|
||||||
|
|
||||||
|
@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!")
|
||||||
|
|
||||||
|
switches = conn.execute("SELECT hostname, ip, username FROM switches").fetchall()
|
||||||
|
conn.close()
|
||||||
|
return render_template('switche.html', switches=switches)
|
||||||
|
|
||||||
|
@app.route('/switches/delete/<hostname>', methods=['POST'])
|
||||||
|
@login_required
|
||||||
|
def delete_switch(hostname):
|
||||||
|
conn = get_db_connection()
|
||||||
|
devices = conn.execute("SELECT name FROM devices WHERE switch_hostname=?", (hostname,)).fetchall()
|
||||||
|
if devices:
|
||||||
|
device_names = [d['name'] for d in devices]
|
||||||
|
flash(f"Folgende Geräte sind noch dem Switch {hostname} zugewiesen: {', '.join(device_names)}", "danger")
|
||||||
|
conn.close()
|
||||||
|
return redirect(url_for('switches'))
|
||||||
|
|
||||||
|
conn.execute("DELETE FROM switches WHERE hostname=?", (hostname,))
|
||||||
|
conn.commit()
|
||||||
|
conn.close()
|
||||||
|
flash(f"Switch '{hostname}' gelöscht.", "success")
|
||||||
|
return redirect(url_for('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():
|
||||||
|
# Intervall aus DB laden
|
||||||
|
conn = sqlite3.connect("sqlite.db")
|
||||||
|
c = conn.cursor()
|
||||||
|
c.execute("SELECT value FROM settings WHERE key='interval'")
|
||||||
|
row = c.fetchone()
|
||||||
|
interval = int(row[0]) if row else 5 # Default 5 Minuten
|
||||||
|
conn.close()
|
||||||
|
|
||||||
|
# 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), interval=interval)
|
||||||
|
|
||||||
|
def load_device_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")
|
||||||
|
if not log_files:
|
||||||
|
return status
|
||||||
|
latest_log = max(log_files, key=os.path.getctime)
|
||||||
|
|
||||||
|
online_re = re.compile(r"(\S+) ist erreichbar!")
|
||||||
|
offline_re = re.compile(r"(\S+) ist nicht erreichbar!")
|
||||||
|
|
||||||
|
with open(latest_log, "r") as f:
|
||||||
|
for line in f:
|
||||||
|
line = line.strip()
|
||||||
|
m_online = online_re.search(line)
|
||||||
|
m_offline = offline_re.search(line)
|
||||||
|
if m_online:
|
||||||
|
name = m_online.group(1)
|
||||||
|
mac = name_to_mac.get(name)
|
||||||
|
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
|
||||||
|
|
||||||
|
|
||||||
|
@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
|
||||||
|
c = conn.cursor()
|
||||||
|
|
||||||
|
if request.method == "POST":
|
||||||
|
|
||||||
|
# Neuen Benutzer hinzufügen
|
||||||
|
if "add_user" in request.form:
|
||||||
|
username = request.form["username"].strip()
|
||||||
|
password = request.form["password"].strip()
|
||||||
|
is_admin = int(request.form.get("is_admin", 0))
|
||||||
|
|
||||||
|
if username and password:
|
||||||
|
pw_hash = bcrypt.generate_password_hash(password).decode("utf-8")
|
||||||
|
try:
|
||||||
|
c.execute(
|
||||||
|
"INSERT INTO users (username, password, is_admin) VALUES (?, ?, ?)",
|
||||||
|
(username, pw_hash, is_admin)
|
||||||
|
)
|
||||||
|
conn.commit()
|
||||||
|
flash(f"Benutzer '{username}' erfolgreich angelegt!")
|
||||||
|
except sqlite3.IntegrityError:
|
||||||
|
flash("Benutzername existiert bereits!")
|
||||||
|
else:
|
||||||
|
flash("Username und Passwort dürfen nicht leer sein!")
|
||||||
|
|
||||||
|
# Rolle ändern
|
||||||
|
elif "change_role" in request.form:
|
||||||
|
user_id = request.form["user_id"]
|
||||||
|
username = request.form.get("username", "").strip()
|
||||||
|
is_admin = int(request.form.get("is_admin", 0))
|
||||||
|
if username:
|
||||||
|
c.execute(
|
||||||
|
"UPDATE users SET username=?, is_admin=? WHERE id=?",
|
||||||
|
(username, is_admin, user_id)
|
||||||
|
)
|
||||||
|
conn.commit()
|
||||||
|
flash("Rolle und Username geändert!")
|
||||||
|
else:
|
||||||
|
flash("Username darf nicht leer sein!")
|
||||||
|
|
||||||
|
# Passwort ändern
|
||||||
|
elif "change_password" in request.form:
|
||||||
|
user_id = request.form["user_id"]
|
||||||
|
new_password = request.form.get("new_password", "").strip()
|
||||||
|
if new_password:
|
||||||
|
pw_hash = bcrypt.generate_password_hash(new_password).decode("utf-8")
|
||||||
|
c.execute(
|
||||||
|
"UPDATE users SET password=? WHERE id=?",
|
||||||
|
(pw_hash, user_id)
|
||||||
|
)
|
||||||
|
conn.commit()
|
||||||
|
flash("Passwort erfolgreich geändert!")
|
||||||
|
else:
|
||||||
|
flash("Passwort darf nicht leer sein!")
|
||||||
|
|
||||||
|
# Benutzer löschen
|
||||||
|
elif "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!")
|
||||||
|
|
||||||
|
# Alle Benutzer laden (GET oder nach POST)
|
||||||
|
c.execute("SELECT id, username, is_admin FROM users")
|
||||||
|
users_list = c.fetchall()
|
||||||
|
conn.close()
|
||||||
|
|
||||||
|
# Direkt rendern, Flash-Messages werden angezeigt
|
||||||
|
return render_template("users.html", users=users_list)
|
||||||
|
|
||||||
|
if __name__ == "__main__":
|
||||||
|
app.run(host='0.0.0.0', port=5000, debug=True)
|
||||||
33
srv/poe_manager/create_admin.py
Executable file
33
srv/poe_manager/create_admin.py
Executable file
@@ -0,0 +1,33 @@
|
|||||||
|
#!/usr/bin/env python3
|
||||||
|
import sqlite3
|
||||||
|
from getpass import getpass
|
||||||
|
from flask_bcrypt import Bcrypt
|
||||||
|
|
||||||
|
DB_PATH = "/srv/poe_manager/sqlite.db"
|
||||||
|
bcrypt = Bcrypt()
|
||||||
|
|
||||||
|
def main():
|
||||||
|
username = input("Admin-Benutzername: ")
|
||||||
|
password = getpass("Passwort: ")
|
||||||
|
password_confirm = getpass("Passwort bestätigen: ")
|
||||||
|
|
||||||
|
if password != password_confirm:
|
||||||
|
print("Passwörter stimmen nicht überein!")
|
||||||
|
return
|
||||||
|
|
||||||
|
pw_hash = bcrypt.generate_password_hash(password).decode('utf-8')
|
||||||
|
|
||||||
|
conn = sqlite3.connect(DB_PATH)
|
||||||
|
cur = conn.cursor()
|
||||||
|
try:
|
||||||
|
cur.execute("INSERT INTO users (username, password, is_admin) VALUES (?, ?, ?)",
|
||||||
|
(username, pw_hash, 1))
|
||||||
|
conn.commit()
|
||||||
|
print(f"Admin-Benutzer '{username}' erfolgreich angelegt.")
|
||||||
|
except sqlite3.IntegrityError:
|
||||||
|
print("Benutzername existiert bereits!")
|
||||||
|
finally:
|
||||||
|
conn.close()
|
||||||
|
|
||||||
|
if __name__ == "__main__":
|
||||||
|
main()
|
||||||
52
srv/poe_manager/create_db.py
Normal file
52
srv/poe_manager/create_db.py
Normal file
@@ -0,0 +1,52 @@
|
|||||||
|
import sqlite3
|
||||||
|
|
||||||
|
conn = sqlite3.connect("sqlite.db")
|
||||||
|
c = conn.cursor()
|
||||||
|
|
||||||
|
# Switches
|
||||||
|
c.execute("""
|
||||||
|
CREATE TABLE IF NOT EXISTS switches (
|
||||||
|
hostname TEXT PRIMARY KEY,
|
||||||
|
ip TEXT NOT NULL,
|
||||||
|
username TEXT NOT NULL,
|
||||||
|
password TEXT NOT NULL
|
||||||
|
);
|
||||||
|
""")
|
||||||
|
|
||||||
|
# Devices
|
||||||
|
c.execute("""
|
||||||
|
CREATE TABLE devices (
|
||||||
|
mac TEXT PRIMARY KEY,
|
||||||
|
rpi_ip TEXT NOT NULL,
|
||||||
|
switch_hostname TEXT NOT NULL,
|
||||||
|
port TEXT,
|
||||||
|
name TEXT NOT NULL,
|
||||||
|
is_active INTEGER DEFAULT 0,
|
||||||
|
FOREIGN KEY (switch_hostname) REFERENCES switches(hostname)
|
||||||
|
);
|
||||||
|
""")
|
||||||
|
|
||||||
|
# Benutzer
|
||||||
|
c.execute("""
|
||||||
|
CREATE TABLE IF NOT EXISTS users (
|
||||||
|
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
||||||
|
username TEXT UNIQUE NOT NULL,
|
||||||
|
password TEXT NOT NULL,
|
||||||
|
is_admin INTEGER DEFAULT 0
|
||||||
|
);
|
||||||
|
""")
|
||||||
|
|
||||||
|
# Settings (z.B. Prüfintervall)
|
||||||
|
c.execute("""
|
||||||
|
CREATE TABLE IF NOT EXISTS settings (
|
||||||
|
key TEXT PRIMARY KEY,
|
||||||
|
value TEXT NOT NULL
|
||||||
|
);
|
||||||
|
""")
|
||||||
|
|
||||||
|
# Standard-Setting: Prüfintervall 5 Minuten
|
||||||
|
c.execute("INSERT OR IGNORE INTO settings (key, value) VALUES (?, ?)", ("interval_minutes", "5"))
|
||||||
|
|
||||||
|
conn.commit()
|
||||||
|
conn.close()
|
||||||
|
print("Datenbank sqlite.db wurde initialisiert inklusive Settings.")
|
||||||
33
srv/poe_manager/create_user.py
Executable file
33
srv/poe_manager/create_user.py
Executable file
@@ -0,0 +1,33 @@
|
|||||||
|
#!/usr/bin/env python3
|
||||||
|
import sqlite3
|
||||||
|
from getpass import getpass
|
||||||
|
from flask_bcrypt import Bcrypt
|
||||||
|
|
||||||
|
DB_PATH = "/srv/poe_manager/sqlite.db"
|
||||||
|
bcrypt = Bcrypt()
|
||||||
|
|
||||||
|
def main():
|
||||||
|
username = input("Benutzername: ")
|
||||||
|
password = getpass("Passwort: ")
|
||||||
|
password_confirm = getpass("Passwort bestätigen: ")
|
||||||
|
|
||||||
|
if password != password_confirm:
|
||||||
|
print("Passwörter stimmen nicht überein!")
|
||||||
|
return
|
||||||
|
|
||||||
|
pw_hash = bcrypt.generate_password_hash(password).decode('utf-8')
|
||||||
|
|
||||||
|
conn = sqlite3.connect(DB_PATH)
|
||||||
|
cur = conn.cursor()
|
||||||
|
try:
|
||||||
|
cur.execute("INSERT INTO users (username, password, is_admin) VALUES (?, ?, ?)",
|
||||||
|
(username, pw_hash, 0))
|
||||||
|
conn.commit()
|
||||||
|
print(f"Benutzer '{username}' erfolgreich angelegt.")
|
||||||
|
except sqlite3.IntegrityError:
|
||||||
|
print("Benutzername existiert bereits!")
|
||||||
|
finally:
|
||||||
|
conn.close()
|
||||||
|
|
||||||
|
if __name__ == "__main__":
|
||||||
|
main()
|
||||||
1
srv/poe_manager/fernet.key
Normal file
1
srv/poe_manager/fernet.key
Normal file
@@ -0,0 +1 @@
|
|||||||
|
lKahy0lgo1McpoZtUDBwqddKkkRg7EnRnK3zdCL4_dw=
|
||||||
35
srv/poe_manager/generate_ips.py
Normal file
35
srv/poe_manager/generate_ips.py
Normal file
@@ -0,0 +1,35 @@
|
|||||||
|
#!/usr/bin/env python3
|
||||||
|
import sqlite3
|
||||||
|
from app import decrypt_password, DB_PATH
|
||||||
|
|
||||||
|
def generate_ips_list():
|
||||||
|
conn = sqlite3.connect(DB_PATH)
|
||||||
|
conn.row_factory = sqlite3.Row
|
||||||
|
|
||||||
|
switches = {row['hostname']: row for row in conn.execute(
|
||||||
|
"SELECT hostname, ip, username, password FROM switches"
|
||||||
|
)}
|
||||||
|
|
||||||
|
devices = conn.execute("""
|
||||||
|
SELECT mac, rpi_ip, port, name, switch_hostname
|
||||||
|
FROM devices
|
||||||
|
WHERE is_active=1
|
||||||
|
""").fetchall()
|
||||||
|
conn.close()
|
||||||
|
|
||||||
|
for dev in devices:
|
||||||
|
switch = switches.get(dev['switch_hostname'])
|
||||||
|
if switch:
|
||||||
|
switch_ip = switch['ip']
|
||||||
|
switch_user = switch['username']
|
||||||
|
switch_pass = decrypt_password(switch['password'])
|
||||||
|
else:
|
||||||
|
switch_ip = ""
|
||||||
|
switch_user = ""
|
||||||
|
switch_pass = ""
|
||||||
|
|
||||||
|
port = dev['port'] or ""
|
||||||
|
print(f"{dev['rpi_ip']}:{dev['name']}:{switch_ip}:{dev['switch_hostname'] or 'kein Switch'}:{port}:{switch_user}:{switch_pass}")
|
||||||
|
|
||||||
|
if __name__ == "__main__":
|
||||||
|
generate_ips_list()
|
||||||
9
srv/poe_manager/poe_wrapper.py
Normal file
9
srv/poe_manager/poe_wrapper.py
Normal file
@@ -0,0 +1,9 @@
|
|||||||
|
#!/usr/bin/env python3
|
||||||
|
import subprocess
|
||||||
|
import os
|
||||||
|
|
||||||
|
# Virtuelle Umgebung aktivieren (Pfad zu deinem venv)
|
||||||
|
venv_python = "/srv/poe_manager/venv/bin/python3"
|
||||||
|
|
||||||
|
# Poe.sh über bash ausführen, innerhalb der venv
|
||||||
|
subprocess.run(["/bin/bash", "/usr/local/bin/custom/poe.sh"], env={"PATH": f"/srv/poe_manager/venv/bin:" + os.environ["PATH"]})
|
||||||
13
srv/poe_manager/requirements.txt
Normal file
13
srv/poe_manager/requirements.txt
Normal file
@@ -0,0 +1,13 @@
|
|||||||
|
bcrypt==5.0.0
|
||||||
|
blinker==1.9.0
|
||||||
|
cffi==2.0.0
|
||||||
|
click==8.3.0
|
||||||
|
cryptography==46.0.1
|
||||||
|
Flask==3.1.2
|
||||||
|
Flask-Bcrypt==1.0.1
|
||||||
|
Flask-Login==0.6.3
|
||||||
|
itsdangerous==2.2.0
|
||||||
|
Jinja2==3.1.6
|
||||||
|
MarkupSafe==3.0.2
|
||||||
|
pycparser==2.23
|
||||||
|
Werkzeug==3.1.3
|
||||||
BIN
srv/poe_manager/sqlite.db
Normal file
BIN
srv/poe_manager/sqlite.db
Normal file
Binary file not shown.
6
srv/poe_manager/static/css/bootstrap.min.css
vendored
Normal file
6
srv/poe_manager/static/css/bootstrap.min.css
vendored
Normal file
File diff suppressed because one or more lines are too long
169
srv/poe_manager/static/css/style.css
Normal file
169
srv/poe_manager/static/css/style.css
Normal file
@@ -0,0 +1,169 @@
|
|||||||
|
body {
|
||||||
|
padding: 20px;
|
||||||
|
background: #3c4346;
|
||||||
|
}
|
||||||
|
|
||||||
|
h1, h2, h3, h4, h5, h6 {
|
||||||
|
color: #fff;
|
||||||
|
}
|
||||||
|
|
||||||
|
.table-dark {
|
||||||
|
background-color: #343a40 !important;
|
||||||
|
color: #fff !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
.badge {
|
||||||
|
font-size: 0.9rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
pre {
|
||||||
|
background-color: #f8f9fa7c;
|
||||||
|
padding: 10px;
|
||||||
|
border-radius: 5px;
|
||||||
|
max-height: 600px;
|
||||||
|
overflow-y: scroll;
|
||||||
|
}
|
||||||
|
|
||||||
|
.mb-3 a.btn {
|
||||||
|
margin-right: 5px;
|
||||||
|
}
|
||||||
|
|
||||||
|
#dashboard-timer {
|
||||||
|
font-size: 0.9rem;
|
||||||
|
padding: 0.25rem 0.5rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.device-card {
|
||||||
|
border: 1px solid #ccc;
|
||||||
|
border-radius: 5px;
|
||||||
|
padding: 15px;
|
||||||
|
margin-bottom: 15px;
|
||||||
|
text-align: center;
|
||||||
|
min-width: 150px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.device-name {
|
||||||
|
margin-bottom: 0.5rem;
|
||||||
|
font-weight: bold;
|
||||||
|
}
|
||||||
|
|
||||||
|
.status-online {
|
||||||
|
color: green;
|
||||||
|
font-weight: bold;
|
||||||
|
}
|
||||||
|
|
||||||
|
.status-offline {
|
||||||
|
color: red;
|
||||||
|
font-weight: bold;
|
||||||
|
}
|
||||||
|
|
||||||
|
.content-wrapper {
|
||||||
|
max-width: 1080px;
|
||||||
|
margin-left: auto;
|
||||||
|
margin-right: auto;
|
||||||
|
}
|
||||||
|
|
||||||
|
.navbar {
|
||||||
|
display: flex;
|
||||||
|
justify-content: flex-end;
|
||||||
|
}
|
||||||
|
|
||||||
|
.navbar-container {
|
||||||
|
display: flex;
|
||||||
|
justify-content: space-between; /* Logo links, Navbar rechts */
|
||||||
|
align-items: center;
|
||||||
|
margin-bottom: 1rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.navbar-buttons .btn {
|
||||||
|
margin-right: 0.25rem; /* kleine Lücke zwischen Buttons */
|
||||||
|
}
|
||||||
|
|
||||||
|
.navbar-logo img {
|
||||||
|
display: flex;
|
||||||
|
justify-content: flex-start;
|
||||||
|
height: 83px; /* Logo Höhe, kann angepasst werden */
|
||||||
|
}
|
||||||
|
|
||||||
|
#log-container {
|
||||||
|
position: relative;
|
||||||
|
height: calc(100vh - 150px); /* Füllt die Seite minus Header */
|
||||||
|
}
|
||||||
|
|
||||||
|
#log-box {
|
||||||
|
height: 97%;
|
||||||
|
overflow: auto;
|
||||||
|
white-space: pre-wrap;
|
||||||
|
font-family: monospace;
|
||||||
|
border: 1px solid #dee2e6; /* Bootstrap-like border */
|
||||||
|
padding: 1rem;
|
||||||
|
background-color: #f8f9fa;
|
||||||
|
}
|
||||||
|
|
||||||
|
#refresh-timer {
|
||||||
|
position: absolute;
|
||||||
|
bottom: 10px;
|
||||||
|
right: 10px;
|
||||||
|
font-size: 0.9em;
|
||||||
|
color: gray;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Tabelle anpassen */
|
||||||
|
.custom-table input,
|
||||||
|
.custom-table select {
|
||||||
|
height: 28px;
|
||||||
|
padding: 0.25rem 0.5rem;
|
||||||
|
font-size: 0.9rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Spaltenbreiten */
|
||||||
|
.custom-table .col-small {
|
||||||
|
width: 140px; /* Hostname, Port */
|
||||||
|
}
|
||||||
|
|
||||||
|
.custom-table .col-ip {
|
||||||
|
width: 140px; /* IP-Adresse schmaler */
|
||||||
|
}
|
||||||
|
|
||||||
|
.custom-table .col-mac {
|
||||||
|
width: 140px; /* MAC-Adresse schmaler */
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
/* Checkbox sichtbar auch bei disabled */
|
||||||
|
.checkbox-visible:disabled {
|
||||||
|
opacity: 1; /* nicht ausgegraut */
|
||||||
|
cursor: not-allowed;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Aktiv-Label für Neues Gerät */
|
||||||
|
label.text-white {
|
||||||
|
color: white !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
.checkbox-visible:not(:checked) {
|
||||||
|
accent-color: #dc3545; /* rot wenn unchecked */
|
||||||
|
}
|
||||||
|
.checkbox-visible:disabled {
|
||||||
|
cursor: not-allowed;
|
||||||
|
opacity: 1; /* verhindert Ausgrauen */
|
||||||
|
}
|
||||||
|
|
||||||
|
.custom-actions {
|
||||||
|
margin: auto 0;
|
||||||
|
background: #fff;
|
||||||
|
}
|
||||||
|
|
||||||
|
.black {
|
||||||
|
color: #000000;
|
||||||
|
}
|
||||||
|
|
||||||
|
.white {
|
||||||
|
color: #ffffff;
|
||||||
|
}
|
||||||
|
|
||||||
|
button.login {
|
||||||
|
background-color: #ff7100;
|
||||||
|
border-color: #ff7300a9;;
|
||||||
|
}
|
||||||
BIN
srv/poe_manager/static/images/logo.png
Normal file
BIN
srv/poe_manager/static/images/logo.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 2.6 KiB |
7
srv/poe_manager/static/js/bootstrap.bundle.min.js
vendored
Normal file
7
srv/poe_manager/static/js/bootstrap.bundle.min.js
vendored
Normal file
File diff suppressed because one or more lines are too long
66
srv/poe_manager/templates/base.html
Normal file
66
srv/poe_manager/templates/base.html
Normal file
@@ -0,0 +1,66 @@
|
|||||||
|
<!DOCTYPE html>
|
||||||
|
<html>
|
||||||
|
<head>
|
||||||
|
<title>{{ title or "PoE Manager" }}</title>
|
||||||
|
<link rel="stylesheet" href="{{ url_for('static', filename='css/bootstrap.min.css') }}">
|
||||||
|
<link rel="stylesheet" href="{{ url_for('static', filename='css/style.css') }}">
|
||||||
|
</head>
|
||||||
|
<body class="p-4">
|
||||||
|
|
||||||
|
<div class="content-wrapper">
|
||||||
|
|
||||||
|
<!-- Logo + Navbar -->
|
||||||
|
<div class="navbar-container">
|
||||||
|
<div class="navbar-logo">
|
||||||
|
<img src="{{ url_for('static', filename='images/logo.png') }}" alt="Logo">
|
||||||
|
</div>
|
||||||
|
<div class="navbar">
|
||||||
|
<div class="navbar-buttons btn-group" role="group">
|
||||||
|
<a href="{{ url_for('index') }}" class="btn btn-secondary">Dashboard</a>
|
||||||
|
<a href="{{ url_for('devices') }}" class="btn btn-secondary">Devices</a>
|
||||||
|
{% if current_user.is_admin %}
|
||||||
|
<a href="{{ url_for('switches') }}" class="btn btn-secondary">Switche</a>
|
||||||
|
<a href="{{ url_for('users') }}" class="btn btn-secondary">Users</a>
|
||||||
|
<a href="{{ url_for('logs') }}" class="btn btn-secondary">Live-Log</a>
|
||||||
|
<a href="{{ url_for('settings') }}" class="btn btn-secondary">Settings</a>
|
||||||
|
{% endif %}
|
||||||
|
<a href="{{ url_for('logout') }}" class="btn btn-danger">Logout</a>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Flash Messages -->
|
||||||
|
{% with messages = get_flashed_messages() %}
|
||||||
|
{% if messages %}
|
||||||
|
<div id="flash-messages" class="mt-2">
|
||||||
|
{% for message in messages %}
|
||||||
|
<div class="alert alert-info alert-dismissible fade show" role="alert">
|
||||||
|
{{ message }}
|
||||||
|
<button type="button" class="btn-close" data-bs-dismiss="alert" aria-label="Close"></button>
|
||||||
|
</div>
|
||||||
|
{% endfor %}
|
||||||
|
</div>
|
||||||
|
{% endif %}
|
||||||
|
{% endwith %}
|
||||||
|
|
||||||
|
<!-- Content -->
|
||||||
|
<div>
|
||||||
|
{% block content %}{% endblock %}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<script src="{{ url_for('static', filename='js/bootstrap.bundle.min.js') }}"></script>
|
||||||
|
<script>
|
||||||
|
// Automatisch alle Alerts nach 5 Sekunden ausblenden
|
||||||
|
document.addEventListener("DOMContentLoaded", function() {
|
||||||
|
setTimeout(() => {
|
||||||
|
const alerts = document.querySelectorAll('#flash-messages .alert');
|
||||||
|
alerts.forEach(alert => {
|
||||||
|
bootstrap.Alert.getOrCreateInstance(alert).close();
|
||||||
|
});
|
||||||
|
}, 5000);
|
||||||
|
});
|
||||||
|
</script>
|
||||||
|
</body>
|
||||||
|
</html>
|
||||||
296
srv/poe_manager/templates/devices.html
Normal file
296
srv/poe_manager/templates/devices.html
Normal file
@@ -0,0 +1,296 @@
|
|||||||
|
{% extends "base.html" %}
|
||||||
|
{% block content %}
|
||||||
|
|
||||||
|
<h2>Devices</h2>
|
||||||
|
|
||||||
|
{% if current_user.is_admin %}
|
||||||
|
<!-- Neues Gerät hinzufügen -->
|
||||||
|
<button class="btn btn-success mb-3" data-bs-toggle="modal" data-bs-target="#deviceModal" onclick="openDeviceModal()">Neues Gerät hinzufügen</button>
|
||||||
|
{% endif %}
|
||||||
|
|
||||||
|
<table class="table table-bordered">
|
||||||
|
<thead>
|
||||||
|
<tr>
|
||||||
|
<th>Hostname</th>
|
||||||
|
<th>IP-Adresse</th>
|
||||||
|
<th>MAC-Adresse</th>
|
||||||
|
<th>Switch</th>
|
||||||
|
<th>Port</th>
|
||||||
|
{% if current_user.is_admin %}<th>Status</th>{% endif %}
|
||||||
|
{% if current_user.is_admin %}<th>Aktionen</th>{% endif %}
|
||||||
|
</tr>
|
||||||
|
</thead>
|
||||||
|
<tbody>
|
||||||
|
{% for d in devices %}
|
||||||
|
<tr>
|
||||||
|
<td>{{ d['name'] }}</td>
|
||||||
|
<td>{{ d['rpi_ip'] }}</td>
|
||||||
|
<td>{{ d['mac'] }}</td>
|
||||||
|
<td>{{ d['switch_hostname'] or '-' }}</td>
|
||||||
|
<td>{{ d['port'] }}</td>
|
||||||
|
{% if current_user.is_admin %}
|
||||||
|
<td>
|
||||||
|
<button class="btn btn-sm {% if d['is_active'] %}btn-success{% else %}btn-secondary{% endif %}"
|
||||||
|
onclick="toggleDevice('{{ d['mac'] }}', this)">
|
||||||
|
{% if d['is_active'] %}Deaktivieren{% else %}Aktivieren{% endif %}
|
||||||
|
</button>
|
||||||
|
</td>
|
||||||
|
<td>
|
||||||
|
<!-- Bearbeiten Modal -->
|
||||||
|
<button class="btn btn-secondary btn-sm button login white" data-bs-toggle="modal" data-bs-target="#editDeviceModal"
|
||||||
|
onclick="openEditDeviceModal('{{ d['mac'] }}','{{ d['name'] }}','{{ d['rpi_ip'] }}','{{ d['port'] }}')">
|
||||||
|
Bearbeiten
|
||||||
|
</button>
|
||||||
|
|
||||||
|
<!-- Switch ändern Modal -->
|
||||||
|
<button class="btn btn-secondary btn-sm button login white" data-bs-toggle="modal" data-bs-target="#switchModal"
|
||||||
|
onclick="openSwitchModal('{{ d['mac'] }}','{{ d['switch_hostname'] }}')">
|
||||||
|
Switch ändern
|
||||||
|
</button>
|
||||||
|
|
||||||
|
<!-- Löschen -->
|
||||||
|
<form method="post" style="display:inline;">
|
||||||
|
<input type="hidden" name="delete_device" value="{{ d['mac'] }}">
|
||||||
|
<button class="btn btn-danger btn-sm" onclick="return confirm('Willst du das Gerät wirklich löschen?');">
|
||||||
|
Löschen
|
||||||
|
</button>
|
||||||
|
</form>
|
||||||
|
</td>
|
||||||
|
{% endif %}
|
||||||
|
</tr>
|
||||||
|
{% endfor %}
|
||||||
|
</tbody>
|
||||||
|
</table>
|
||||||
|
|
||||||
|
<!-- Modal: Neues Gerät -->
|
||||||
|
<div class="modal fade" id="deviceModal" tabindex="-1" aria-hidden="true">
|
||||||
|
<div class="modal-dialog">
|
||||||
|
<div class="modal-content">
|
||||||
|
<form method="post" onsubmit="return validateDeviceForm(this);">
|
||||||
|
<div class="modal-header">
|
||||||
|
<h5 class="modal-title black">Neues Gerät hinzufügen</h5>
|
||||||
|
<button type="button" class="btn-close" data-bs-dismiss="modal"></button>
|
||||||
|
</div>
|
||||||
|
<div class="modal-body">
|
||||||
|
<input type="hidden" name="add_device" value="1">
|
||||||
|
<div class="mb-3">
|
||||||
|
<label>Hostname</label>
|
||||||
|
<input type="text" name="name" class="form-control" required placeholder="z.B. Sensor01">
|
||||||
|
</div>
|
||||||
|
<div class="mb-3">
|
||||||
|
<label>IP-Adresse</label>
|
||||||
|
<input type="text" name="rpi_ip" class="form-control" required placeholder="z.B. 192.168.1.100">
|
||||||
|
<div class="invalid-feedback">Bitte eine gültige IP-Adresse eingeben (z. B. 192.168.1.100).</div>
|
||||||
|
</div>
|
||||||
|
<div class="mb-3">
|
||||||
|
<label>MAC-Adresse</label>
|
||||||
|
<input type="text" name="mac" class="form-control" required placeholder="z.B. AA:BB:CC:DD:EE:FF">
|
||||||
|
<div class="invalid-feedback">Bitte eine gültige MAC-Adresse eingeben (xx:xx:xx:xx:xx:xx).</div>
|
||||||
|
</div>
|
||||||
|
<div class="mb-3">
|
||||||
|
<label>Port</label>
|
||||||
|
<input type="text" name="port" class="form-control" placeholder="z.B. 3">
|
||||||
|
</div>
|
||||||
|
<div class="mb-3">
|
||||||
|
<label>Switch (optional)</label>
|
||||||
|
<select name="switch_hostname" class="form-select">
|
||||||
|
<option value="">Kein Switch</option>
|
||||||
|
{% for sw in switches %}
|
||||||
|
<option value="{{ sw['hostname'] }}">{{ sw['hostname'] }}</option>
|
||||||
|
{% endfor %}
|
||||||
|
</select>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="modal-footer">
|
||||||
|
<button class="btn btn-success button login" type="submit">Hinzufügen</button>
|
||||||
|
<button type="button" class="btn btn-secondary" data-bs-dismiss="modal">Abbrechen</button>
|
||||||
|
</div>
|
||||||
|
</form>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Modal: Bearbeiten -->
|
||||||
|
<div class="modal fade" id="editDeviceModal" tabindex="-1" aria-hidden="true">
|
||||||
|
<div class="modal-dialog">
|
||||||
|
<div class="modal-content">
|
||||||
|
<form method="post" onsubmit="return validateDeviceForm(this);">
|
||||||
|
<input type="hidden" name="edit_device" value="1">
|
||||||
|
<input type="hidden" name="old_mac" id="edit_old_mac">
|
||||||
|
<div class="modal-header">
|
||||||
|
<h5 class="modal-title black">Gerät bearbeiten</h5>
|
||||||
|
<button type="button" class="btn-close" data-bs-dismiss="modal"></button>
|
||||||
|
</div>
|
||||||
|
<div class="modal-body">
|
||||||
|
<div class="mb-3">
|
||||||
|
<label>Hostname</label>
|
||||||
|
<input type="text" name="name" id="edit_name" class="form-control" required placeholder="z.B. Sensor01">
|
||||||
|
</div>
|
||||||
|
<div class="mb-3">
|
||||||
|
<label>IP-Adresse</label>
|
||||||
|
<input type="text" name="rpi_ip" id="edit_ip" class="form-control" required placeholder="z.B. 192.168.1.100">
|
||||||
|
<div class="invalid-feedback">Bitte eine gültige IP-Adresse eingeben (z. B. 192.168.1.100).</div>
|
||||||
|
</div>
|
||||||
|
<div class="mb-3">
|
||||||
|
<label>MAC-Adresse</label>
|
||||||
|
<input type="text" name="mac" id="edit_mac" class="form-control" required placeholder="z.B. AA:BB:CC:DD:EE:FF">
|
||||||
|
<div class="invalid-feedback">Bitte eine gültige MAC-Adresse eingeben (xx:xx:xx:xx:xx:xx).</div>
|
||||||
|
</div>
|
||||||
|
<div class="mb-3">
|
||||||
|
<label>Port</label>
|
||||||
|
<input type="text" name="port" id="edit_port" class="form-control" placeholder="z.B. 3">
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="modal-footer">
|
||||||
|
<button class="btn btn-primary button login" type="submit">Speichern</button>
|
||||||
|
<button type="button" class="btn btn-secondary" data-bs-dismiss="modal">Abbrechen</button>
|
||||||
|
</div>
|
||||||
|
</form>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Modal: Switch ändern -->
|
||||||
|
<div class="modal fade" id="switchModal" tabindex="-1" aria-hidden="true">
|
||||||
|
<div class="modal-dialog">
|
||||||
|
<div class="modal-content">
|
||||||
|
<form method="post">
|
||||||
|
<input type="hidden" name="edit_device" value="1">
|
||||||
|
<input type="hidden" name="old_mac" id="switch_mac">
|
||||||
|
<div class="modal-header">
|
||||||
|
<h5 class="modal-title black">Switch ändern</h5>
|
||||||
|
<button type="button" class="btn-close" data-bs-dismiss="modal"></button>
|
||||||
|
</div>
|
||||||
|
<div class="modal-body">
|
||||||
|
<select name="switch_hostname" id="switch_select" class="form-select">
|
||||||
|
<option value="">Kein Switch</option>
|
||||||
|
{% for sw in switches %}
|
||||||
|
<option value="{{ sw['hostname'] }}">{{ sw['hostname'] }}</option>
|
||||||
|
{% endfor %}
|
||||||
|
</select>
|
||||||
|
</div>
|
||||||
|
<div class="modal-footer">
|
||||||
|
<button class="btn btn-primary button login" type="submit">Speichern</button>
|
||||||
|
<button type="button" class="btn btn-secondary" data-bs-dismiss="modal">Abbrechen</button>
|
||||||
|
</div>
|
||||||
|
</form>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<script>
|
||||||
|
function openDeviceModal() {
|
||||||
|
document.querySelector("#deviceModal form").reset();
|
||||||
|
}
|
||||||
|
|
||||||
|
function openEditDeviceModal(mac, name, ip, port) {
|
||||||
|
document.getElementById("edit_old_mac").value = mac;
|
||||||
|
document.getElementById("edit_name").value = name;
|
||||||
|
document.getElementById("edit_ip").value = ip;
|
||||||
|
document.getElementById("edit_mac").value = mac;
|
||||||
|
document.getElementById("edit_port").value = port;
|
||||||
|
}
|
||||||
|
|
||||||
|
function openSwitchModal(mac, switch_hostname) {
|
||||||
|
document.getElementById("switch_mac").value = mac;
|
||||||
|
document.getElementById("switch_select").value = switch_hostname;
|
||||||
|
}
|
||||||
|
|
||||||
|
// -------------------
|
||||||
|
// IP-Validierung
|
||||||
|
// -------------------
|
||||||
|
const ipPattern = /^(25[0-5]|2[0-4]\d|1\d\d|[1-9]?\d)(\.(25[0-5]|2[0-4]\d|1\d\d|[1-9]?\d)){3}$/;
|
||||||
|
|
||||||
|
// -------------------
|
||||||
|
// MAC-Validierung
|
||||||
|
// -------------------
|
||||||
|
const macPattern = /^([0-9A-Fa-f]{2}:){5}[0-9A-Fa-f]{2}$/;
|
||||||
|
|
||||||
|
function validateIP(input) {
|
||||||
|
if (!ipPattern.test(input.value)) {
|
||||||
|
input.classList.add("is-invalid");
|
||||||
|
return false;
|
||||||
|
} else {
|
||||||
|
input.classList.remove("is-invalid");
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function validateMAC(input) {
|
||||||
|
if (!macPattern.test(input.value)) {
|
||||||
|
input.classList.add("is-invalid");
|
||||||
|
return false;
|
||||||
|
} else {
|
||||||
|
input.classList.remove("is-invalid");
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function validateDeviceForm(form) {
|
||||||
|
const ipInput = form.querySelector("input[name='rpi_ip']");
|
||||||
|
const macInput = form.querySelector("input[name='mac']");
|
||||||
|
let valid = true;
|
||||||
|
if (ipInput) valid = validateIP(ipInput) && valid;
|
||||||
|
if (macInput) valid = validateMAC(macInput) && valid;
|
||||||
|
return valid;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Live-Feedback beim Tippen
|
||||||
|
document.addEventListener("input", function(e) {
|
||||||
|
if (e.target.name === "rpi_ip") validateIP(e.target);
|
||||||
|
if (e.target.name === "mac") validateMAC(e.target);
|
||||||
|
});
|
||||||
|
|
||||||
|
// Aktivieren/Deaktivieren
|
||||||
|
function toggleDevice(mac, btn) {
|
||||||
|
fetch(`/devices/toggle/${mac}`, { method: 'POST' })
|
||||||
|
.then(resp => resp.json())
|
||||||
|
.then(data => {
|
||||||
|
if (data.success) {
|
||||||
|
// Button aktualisieren
|
||||||
|
if (data.new_status === 1) {
|
||||||
|
btn.classList.remove('btn-secondary');
|
||||||
|
btn.classList.add('btn-success');
|
||||||
|
btn.innerText = 'Deaktivieren';
|
||||||
|
} else {
|
||||||
|
btn.classList.remove('btn-success');
|
||||||
|
btn.classList.add('btn-secondary');
|
||||||
|
btn.innerText = 'Aktivieren';
|
||||||
|
}
|
||||||
|
|
||||||
|
// Flash-Nachricht im Frontend aktualisieren
|
||||||
|
let flashContainer = document.getElementById('flash-messages');
|
||||||
|
if (!flashContainer) {
|
||||||
|
flashContainer = document.createElement('div');
|
||||||
|
flashContainer.id = 'flash-messages';
|
||||||
|
flashContainer.style.position = 'fixed';
|
||||||
|
flashContainer.style.top = '10px';
|
||||||
|
flashContainer.style.right = '10px';
|
||||||
|
flashContainer.style.zIndex = 1050;
|
||||||
|
document.body.appendChild(flashContainer);
|
||||||
|
}
|
||||||
|
|
||||||
|
const flash = document.createElement('div');
|
||||||
|
flash.className = 'alert alert-success alert-dismissible fade show';
|
||||||
|
flash.role = 'alert';
|
||||||
|
flash.innerHTML = `
|
||||||
|
${data.msg}
|
||||||
|
<button type="button" class="btn-close" data-bs-dismiss="alert"></button>
|
||||||
|
`;
|
||||||
|
flashContainer.appendChild(flash);
|
||||||
|
|
||||||
|
// Automatisch nach 3 Sekunden ausblenden
|
||||||
|
setTimeout(() => {
|
||||||
|
flash.classList.remove('show');
|
||||||
|
flash.classList.add('hide');
|
||||||
|
flash.addEventListener('transitionend', () => flash.remove());
|
||||||
|
}, 3000);
|
||||||
|
} else {
|
||||||
|
console.error(data.msg);
|
||||||
|
}
|
||||||
|
})
|
||||||
|
.catch(err => console.error(err));
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
|
||||||
|
{% endblock %}
|
||||||
93
srv/poe_manager/templates/index.html
Normal file
93
srv/poe_manager/templates/index.html
Normal file
@@ -0,0 +1,93 @@
|
|||||||
|
{% extends "base.html" %}
|
||||||
|
{% block content %}
|
||||||
|
|
||||||
|
<h2 class="d-flex justify-content-between align-items-center">
|
||||||
|
Dashboard
|
||||||
|
<span id="dashboard-timer" class="badge bg-success">
|
||||||
|
Nächste Prüfung in -- Sekunden
|
||||||
|
</span>
|
||||||
|
</h2>
|
||||||
|
<div class="row row-cols-1 row-cols-md-6 g-3">
|
||||||
|
{% for d in devices %}
|
||||||
|
<div class="col">
|
||||||
|
<div class="card text-center p-2"
|
||||||
|
{% if last_seen.get(d[0]) %}
|
||||||
|
title="{{ last_seen[d[0]] }}"
|
||||||
|
{% elif status[d[0]] == 'offline' %}
|
||||||
|
title="Noch nie online"
|
||||||
|
{% endif %}>
|
||||||
|
<div class="card-header">{{ d[1] }}</div>
|
||||||
|
<div class="card-body">
|
||||||
|
<span class="fw-bold" style="color:
|
||||||
|
{% if d[2] == 0 %}gray
|
||||||
|
{% elif status[d[0]] == 'online' %}green
|
||||||
|
{% else %}red
|
||||||
|
{% endif %};">
|
||||||
|
{% if d[2] == 0 %}
|
||||||
|
Deaktiviert
|
||||||
|
{% else %}
|
||||||
|
{% if status[d[0]] %}{{ status[d[0]]|capitalize }}{% else %}Unbekannt{% endif %}
|
||||||
|
{% endif %}
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
{% endfor %}
|
||||||
|
</div>
|
||||||
|
<script>
|
||||||
|
document.addEventListener("DOMContentLoaded", () => {
|
||||||
|
const intervalMinutes = {{ interval | int }}; // aus DB
|
||||||
|
const intervalMilliseconds = intervalMinutes * 60 * 1000;
|
||||||
|
let lastUpdateTime = Date.now();
|
||||||
|
|
||||||
|
function parseLogTimestamp(ts) {
|
||||||
|
const parts = ts.match(/^(\d{4})-(\d{2})-(\d{2}) (\d{2}):(\d{2}):(\d{2})$/);
|
||||||
|
if (!parts) return Date.now();
|
||||||
|
const [, year, month, day, hour, minute, second] = parts.map(Number);
|
||||||
|
return new Date(year, month - 1, day, hour, minute, second).getTime();
|
||||||
|
}
|
||||||
|
|
||||||
|
function updateTimer() {
|
||||||
|
const now = Date.now();
|
||||||
|
const elapsed = now - lastUpdateTime;
|
||||||
|
const remainingMs = intervalMilliseconds - (elapsed % intervalMilliseconds);
|
||||||
|
const remainingSec = Math.ceil(remainingMs / 1000);
|
||||||
|
document.getElementById("dashboard-timer").innerText =
|
||||||
|
`Nächste Prüfung in ${remainingSec} Sekunden`;
|
||||||
|
|
||||||
|
if (remainingSec <= 1) {
|
||||||
|
// Timer abgelaufen → Reload starten
|
||||||
|
window.location.reload();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function fetchLastLog() {
|
||||||
|
fetch("{{ url_for('get_log') }}")
|
||||||
|
.then(response => response.text())
|
||||||
|
.then(data => {
|
||||||
|
const lines = data.split("\n");
|
||||||
|
let lastSepIndex = -1;
|
||||||
|
for (let i = lines.length - 1; i >= 0; i--) {
|
||||||
|
if (lines[i].startsWith("--------------------------------------------------------------------")) {
|
||||||
|
lastSepIndex = i;
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (lastSepIndex >= 0 && lastSepIndex + 1 < lines.length) {
|
||||||
|
const firstLine = lines[lastSepIndex + 1];
|
||||||
|
const match = firstLine.match(/^(\d{4}-\d{2}-\d{2} \d{2}:\d{2}:\d{2})/);
|
||||||
|
if (match) lastUpdateTime = parseLogTimestamp(match[1]);
|
||||||
|
}
|
||||||
|
})
|
||||||
|
.catch(err => console.error("Fehler beim Laden der Logs:", err));
|
||||||
|
}
|
||||||
|
|
||||||
|
// Timer alle 1 Sekunde aktualisieren
|
||||||
|
setInterval(updateTimer, 1000);
|
||||||
|
|
||||||
|
// einmal beim Laden die letzte Log-Zeit setzen
|
||||||
|
fetchLastLog();
|
||||||
|
});
|
||||||
|
</script>
|
||||||
|
|
||||||
|
{% endblock %}
|
||||||
33
srv/poe_manager/templates/login.html
Normal file
33
srv/poe_manager/templates/login.html
Normal file
@@ -0,0 +1,33 @@
|
|||||||
|
<!DOCTYPE html>
|
||||||
|
<html>
|
||||||
|
<head>
|
||||||
|
<title>Login</title>
|
||||||
|
<link rel="stylesheet" href="{{ url_for('static', filename='css/bootstrap.min.css') }}">
|
||||||
|
<link rel="stylesheet" href="{{ url_for('static', filename='css/style.css') }}">
|
||||||
|
</head>
|
||||||
|
<body class="p-5">
|
||||||
|
|
||||||
|
<h2>Login</h2>
|
||||||
|
|
||||||
|
{% with messages = get_flashed_messages() %}
|
||||||
|
{% if messages %}
|
||||||
|
<div class="mt-2 alert alert-danger">
|
||||||
|
{% for message in messages %}{{ message }}<br>{% endfor %}
|
||||||
|
</div>
|
||||||
|
{% endif %}
|
||||||
|
{% endwith %}
|
||||||
|
|
||||||
|
<form method="post" class="w-25">
|
||||||
|
<div class="mb-3">
|
||||||
|
<label class="form-label label text-white">Username</label>
|
||||||
|
<input type="text" name="username" class="form-control" required>
|
||||||
|
</div>
|
||||||
|
<div class="mb-3">
|
||||||
|
<label class="form-label label text-white">Password</label>
|
||||||
|
<input type="password" name="password" class="form-control" required>
|
||||||
|
</div>
|
||||||
|
<button class="btn btn-primary button login">Login</button>
|
||||||
|
</form>
|
||||||
|
|
||||||
|
</body>
|
||||||
|
</html>
|
||||||
79
srv/poe_manager/templates/logs.html
Normal file
79
srv/poe_manager/templates/logs.html
Normal file
@@ -0,0 +1,79 @@
|
|||||||
|
{% extends "base.html" %}
|
||||||
|
{% block content %}
|
||||||
|
|
||||||
|
<h2>Live Log</h2>
|
||||||
|
|
||||||
|
<button id="refresh-btn" class="btn btn-success mb-3">
|
||||||
|
<span style="font-size: 1.2rem; color: white;">⟳</span> Logs aktualisieren
|
||||||
|
</button>
|
||||||
|
|
||||||
|
<div id="log-container">
|
||||||
|
<div id="log-box"></div>
|
||||||
|
<div id="refresh-timer">
|
||||||
|
Nächstes Update in <span id="timer"></span> Sekunden
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<script>
|
||||||
|
function parseLogTimestamp(ts) {
|
||||||
|
const parts = ts.match(/^(\d{4})-(\d{2})-(\d{2}) (\d{2}):(\d{2}):(\d{2})$/);
|
||||||
|
if (!parts) return Date.now();
|
||||||
|
const [, year, month, day, hour, minute, second] = parts.map(Number);
|
||||||
|
return new Date(year, month - 1, day, hour, minute, second).getTime();
|
||||||
|
}
|
||||||
|
|
||||||
|
document.addEventListener("DOMContentLoaded", () => {
|
||||||
|
const intervalMinutes = {{ interval | int }};
|
||||||
|
const intervalMilliseconds = intervalMinutes * 60 * 1000;
|
||||||
|
let lastUpdateTime = Date.now();
|
||||||
|
|
||||||
|
function fetchLog() {
|
||||||
|
fetch("{{ url_for('get_log') }}")
|
||||||
|
.then(res => res.text())
|
||||||
|
.then(data => {
|
||||||
|
const box = document.getElementById("log-box");
|
||||||
|
const filteredLines = data
|
||||||
|
.split("\n");
|
||||||
|
box.innerText = filteredLines.join("\n");
|
||||||
|
box.scrollTop = box.scrollHeight;
|
||||||
|
|
||||||
|
// letzte Separator-Linie
|
||||||
|
let lastSep = -1;
|
||||||
|
for (let i = filteredLines.length - 1; i >= 0; i--) {
|
||||||
|
if (filteredLines[i].startsWith("--------------------------------------------------------------------")) {
|
||||||
|
lastSep = i;
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (lastSep >= 0 && lastSep + 1 < filteredLines.length) {
|
||||||
|
const firstLine = filteredLines[lastSep + 1];
|
||||||
|
const match = firstLine.match(/^(\d{4}-\d{2}-\d{2} \d{2}:\d{2}:\d{2})/);
|
||||||
|
if (match) {
|
||||||
|
lastUpdateTime = parseLogTimestamp(match[1]);
|
||||||
|
} else {
|
||||||
|
lastUpdateTime = Date.now();
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
lastUpdateTime = Date.now();
|
||||||
|
}
|
||||||
|
})
|
||||||
|
.catch(err => console.error(err));
|
||||||
|
}
|
||||||
|
|
||||||
|
function updateTimer() {
|
||||||
|
const now = Date.now();
|
||||||
|
const elapsed = now - lastUpdateTime;
|
||||||
|
const remainingMs = intervalMilliseconds - (elapsed % intervalMilliseconds);
|
||||||
|
const remainingSec = Math.ceil(remainingMs / 1000);
|
||||||
|
document.getElementById("timer").innerText = remainingSec;
|
||||||
|
}
|
||||||
|
|
||||||
|
document.getElementById("refresh-btn").addEventListener("click", fetchLog);
|
||||||
|
|
||||||
|
fetchLog();
|
||||||
|
setInterval(fetchLog, intervalMilliseconds);
|
||||||
|
setInterval(updateTimer, 1000);
|
||||||
|
});
|
||||||
|
</script>
|
||||||
|
{% endblock %}
|
||||||
15
srv/poe_manager/templates/settings.html
Normal file
15
srv/poe_manager/templates/settings.html
Normal file
@@ -0,0 +1,15 @@
|
|||||||
|
{% extends "base.html" %}
|
||||||
|
{% block content %}
|
||||||
|
|
||||||
|
<h2>Settings</h2>
|
||||||
|
|
||||||
|
<form method="post" class="row g-2">
|
||||||
|
<div class="col-auto">
|
||||||
|
<label for="interval" class="form-label white">Prüfintervall (Minuten):</label>
|
||||||
|
<input type="number" name="interval" id="interval" class="form-control" value="{{ interval }}" min="1" required>
|
||||||
|
</div>
|
||||||
|
<div class="col-auto align-self-end">
|
||||||
|
<button type="submit" class="btn btn-success">Speichern & Service neustarten</button>
|
||||||
|
</div>
|
||||||
|
</form>
|
||||||
|
{% endblock %}
|
||||||
183
srv/poe_manager/templates/switche.html
Normal file
183
srv/poe_manager/templates/switche.html
Normal file
@@ -0,0 +1,183 @@
|
|||||||
|
{% extends "base.html" %}
|
||||||
|
{% block content %}
|
||||||
|
<h2>Switch Verwaltung</h2>
|
||||||
|
|
||||||
|
<!-- Button zum Hinzufügen -->
|
||||||
|
<button class="btn btn-success mb-3" data-bs-toggle="modal" data-bs-target="#addSwitchModal">Neuen Switch hinzufügen</button>
|
||||||
|
|
||||||
|
<table class="table table-bordered">
|
||||||
|
<thead>
|
||||||
|
<tr>
|
||||||
|
<th>Hostname</th>
|
||||||
|
<th>IP-Adresse</th>
|
||||||
|
<th>Username</th>
|
||||||
|
<th>Aktionen</th>
|
||||||
|
</tr>
|
||||||
|
</thead>
|
||||||
|
<tbody>
|
||||||
|
{% for s in switches %}
|
||||||
|
<tr>
|
||||||
|
<td>{{ s['hostname'] }}</td>
|
||||||
|
<td>{{ s['ip'] }}</td>
|
||||||
|
<td>{{ s['username'] }}</td>
|
||||||
|
<td>
|
||||||
|
<!-- Bearbeiten -->
|
||||||
|
<button class="btn btn-sm button login white" data-bs-toggle="modal" data-bs-target="#editSwitchModal{{ loop.index }}">Bearbeiten</button>
|
||||||
|
|
||||||
|
<!-- Löschen -->
|
||||||
|
<form method="post" action="{{ url_for('delete_switch', hostname=s['hostname']) }}" style="display:inline;">
|
||||||
|
<button class="btn btn-danger btn-sm" onclick="return confirm('Willst du den Switch wirklich löschen?');">
|
||||||
|
Löschen
|
||||||
|
</button>
|
||||||
|
</form>
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
|
||||||
|
<!-- Bearbeiten Modal -->
|
||||||
|
<div class="modal fade" id="editSwitchModal{{ loop.index }}" tabindex="-1">
|
||||||
|
<div class="modal-dialog">
|
||||||
|
<div class="modal-content">
|
||||||
|
<div class="modal-header">
|
||||||
|
<h5 class="modal-title text-dark">Switch bearbeiten</h5>
|
||||||
|
<button type="button" class="btn-close" data-bs-dismiss="modal"></button>
|
||||||
|
</div>
|
||||||
|
<form method="post" onsubmit="return validateSwitchForm(this, '{{ loop.index }}')">
|
||||||
|
<div class="modal-body">
|
||||||
|
<input type="hidden" name="edit_switch" value="1">
|
||||||
|
<input type="hidden" name="old_hostname" value="{{ s['hostname'] }}">
|
||||||
|
<div class="mb-3">
|
||||||
|
<label class="form-label">Hostname</label>
|
||||||
|
<input type="text" name="hostname" class="form-control" value="{{ s['hostname'] }}" required>
|
||||||
|
</div>
|
||||||
|
<div class="mb-3">
|
||||||
|
<label class="form-label">IP-Adresse</label>
|
||||||
|
<input type="text" name="ip" class="form-control" value="{{ s['ip'] }}" required placeholder="z.B. 192.168.1.100">
|
||||||
|
<div class="invalid-feedback">Bitte eine gültige IP-Adresse eingeben (z. B. 192.168.1.100).</div>
|
||||||
|
</div>
|
||||||
|
<div class="mb-3">
|
||||||
|
<label class="form-label">Username</label>
|
||||||
|
<input type="text" name="username" class="form-control" value="{{ s['username'] }}" required>
|
||||||
|
</div>
|
||||||
|
<div class="mb-3">
|
||||||
|
<label class="form-label">Neues Passwort</label>
|
||||||
|
<input type="password" id="password_edit_{{ loop.index }}" name="password" class="form-control" placeholder="Neues Passwort (optional)">
|
||||||
|
</div>
|
||||||
|
<div class="mb-3">
|
||||||
|
<label class="form-label">Passwort bestätigen</label>
|
||||||
|
<input type="password" id="password_confirm_edit_{{ loop.index }}" name="password_confirm" class="form-control" placeholder="Bestätigen">
|
||||||
|
<div class="invalid-feedback">Passwörter stimmen nicht überein!</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="modal-footer">
|
||||||
|
<button class="btn btn-secondary" data-bs-dismiss="modal">Abbrechen</button>
|
||||||
|
<button class="btn button login white" type="submit">Speichern</button>
|
||||||
|
</div>
|
||||||
|
</form>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
{% endfor %}
|
||||||
|
</tbody>
|
||||||
|
</table>
|
||||||
|
|
||||||
|
<!-- Hinzufügen Modal -->
|
||||||
|
<div class="modal fade" id="addSwitchModal" tabindex="-1">
|
||||||
|
<div class="modal-dialog">
|
||||||
|
<div class="modal-content">
|
||||||
|
<div class="modal-header">
|
||||||
|
<h5 class="modal-title text-dark">Neuen Switch hinzufügen</h5>
|
||||||
|
<button type="button" class="btn-close" data-bs-dismiss="modal"></button>
|
||||||
|
</div>
|
||||||
|
<form method="post" onsubmit="return validateSwitchForm(this, 'add')">
|
||||||
|
<div class="modal-body">
|
||||||
|
<input type="hidden" name="add_switch" value="1">
|
||||||
|
<div class="mb-3">
|
||||||
|
<label class="form-label">Hostname</label>
|
||||||
|
<input type="text" name="hostname" class="form-control" required placeholder="z.B. Switch01">
|
||||||
|
</div>
|
||||||
|
<div class="mb-3">
|
||||||
|
<label class="form-label">IP-Adresse</label>
|
||||||
|
<input type="text" name="ip" class="form-control" required placeholder="z.B. 192.168.1.100">
|
||||||
|
<div class="invalid-feedback">Bitte eine gültige IP-Adresse eingeben (z. B. 192.168.1.100).</div>
|
||||||
|
</div>
|
||||||
|
<div class="mb-3">
|
||||||
|
<label class="form-label">Username</label>
|
||||||
|
<input type="text" name="username" class="form-control" required placeholder="z.B. admin">
|
||||||
|
</div>
|
||||||
|
<div class="mb-3">
|
||||||
|
<label class="form-label">Passwort</label>
|
||||||
|
<input type="password" id="password_add" name="password" class="form-control" required placeholder="z.B. geheim123">
|
||||||
|
</div>
|
||||||
|
<div class="mb-3">
|
||||||
|
<label class="form-label">Passwort bestätigen</label>
|
||||||
|
<input type="password" id="password_confirm_add" name="password_confirm" class="form-control" required placeholder="Passwort wiederholen">
|
||||||
|
<div class="invalid-feedback">Passwörter stimmen nicht überein!</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="modal-footer">
|
||||||
|
<button class="btn btn-secondary" data-bs-dismiss="modal">Abbrechen</button>
|
||||||
|
<button class="btn button login white" type="submit">Hinzufügen</button>
|
||||||
|
</div>
|
||||||
|
</form>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- JS: Passwort + IP-Validierung -->
|
||||||
|
<script>
|
||||||
|
// Passwortcheck
|
||||||
|
function validatePassword(id, isEdit) {
|
||||||
|
let pass = document.getElementById(isEdit ? `password_edit_${id}` : `password_${id}`) || document.getElementById('password_add');
|
||||||
|
let confirm = document.getElementById(isEdit ? `password_confirm_edit_${id}` : `password_confirm_${id}`) || document.getElementById('password_confirm_add');
|
||||||
|
|
||||||
|
if (!pass || !confirm) return true;
|
||||||
|
if (pass.value !== confirm.value) {
|
||||||
|
confirm.classList.add("is-invalid");
|
||||||
|
return false;
|
||||||
|
} else {
|
||||||
|
confirm.classList.remove("is-invalid");
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// -------------------
|
||||||
|
// IP-Validierung
|
||||||
|
// -------------------
|
||||||
|
const ipPattern = /^(25[0-5]|2[0-4]\d|1\d\d|[1-9]?\d)(\.(25[0-5]|2[0-4]\d|1\d\d|[1-9]?\d)){3}$/;
|
||||||
|
|
||||||
|
function validateIP(input) {
|
||||||
|
if (!ipPattern.test(input.value)) {
|
||||||
|
input.classList.add("is-invalid");
|
||||||
|
return false;
|
||||||
|
} else {
|
||||||
|
input.classList.remove("is-invalid");
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function validateSwitchForm(form, id) {
|
||||||
|
const ipInput = form.querySelector("input[name='ip']");
|
||||||
|
let valid = true;
|
||||||
|
if (ipInput) valid = validateIP(ipInput) && valid;
|
||||||
|
|
||||||
|
// Passwortcheck
|
||||||
|
valid = validatePassword(id, id !== 'add') && valid;
|
||||||
|
|
||||||
|
return valid;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Live-Feedback IP & Passwort
|
||||||
|
document.addEventListener('input', e => {
|
||||||
|
if (e.target.name === "ip") validateIP(e.target);
|
||||||
|
if (e.target.id.startsWith('password_confirm')) {
|
||||||
|
const id = e.target.id.replace('password_confirm_', '');
|
||||||
|
const pass = document.getElementById('password_' + id);
|
||||||
|
if (pass && pass.value !== e.target.value) {
|
||||||
|
e.target.classList.add('is-invalid');
|
||||||
|
} else {
|
||||||
|
e.target.classList.remove('is-invalid');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
</script>
|
||||||
|
{% endblock %}
|
||||||
167
srv/poe_manager/templates/users.html
Normal file
167
srv/poe_manager/templates/users.html
Normal file
@@ -0,0 +1,167 @@
|
|||||||
|
{% extends "base.html" %}
|
||||||
|
{% block content %}
|
||||||
|
|
||||||
|
<h2>Users</h2>
|
||||||
|
|
||||||
|
{% if current_user.is_admin %}
|
||||||
|
<!-- Button Neues User-Popup -->
|
||||||
|
<button class="btn btn-success mb-3" data-bs-toggle="modal" data-bs-target="#userModal" onclick="openUserModal()">Neuen Benutzer</button>
|
||||||
|
{% endif %}
|
||||||
|
|
||||||
|
<table class="table table-bordered">
|
||||||
|
<thead>
|
||||||
|
<tr>
|
||||||
|
<th class="col-ip">Username</th>
|
||||||
|
<th class="col-ip">Rolle</th>
|
||||||
|
{% if current_user.is_admin %}<th>Aktionen</th>{% endif %}
|
||||||
|
</tr>
|
||||||
|
</thead>
|
||||||
|
<tbody>
|
||||||
|
{% for u in users %}
|
||||||
|
<tr>
|
||||||
|
<td class="col-small">{{ u['username'] }}</td>
|
||||||
|
<td class="col-small">{% if u['is_admin'] %}Admin{% else %}User{% endif %}</td>
|
||||||
|
{% if current_user.is_admin %}
|
||||||
|
<td class="custom-actions d-flex gap-1 align-items-center">
|
||||||
|
<!-- Rolle ändern -->
|
||||||
|
<button class="btn btn-sm button login white" data-bs-toggle="modal"
|
||||||
|
data-bs-target="#roleModal"
|
||||||
|
onclick="openRoleModal({{ u['id'] }}, '{{ u['username'] }}', {{ u['is_admin'] }})">
|
||||||
|
Rolle ändern
|
||||||
|
</button>
|
||||||
|
|
||||||
|
<!-- Passwort ändern -->
|
||||||
|
<button class="btn btn-sm button login white" data-bs-toggle="modal"
|
||||||
|
data-bs-target="#passwordModal"
|
||||||
|
onclick="openPasswordModal({{ u['id'] }}, '{{ u['username'] }}')">
|
||||||
|
Passwort ändern
|
||||||
|
</button>
|
||||||
|
|
||||||
|
<!-- Benutzer löschen -->
|
||||||
|
<form method="post" style="display:inline;">
|
||||||
|
<button name="delete_user" value="{{ u['id'] }}" class="btn btn-danger btn-sm"
|
||||||
|
onclick="return confirm('Willst du den Benutzer wirklich löschen?');">Löschen</button>
|
||||||
|
</form>
|
||||||
|
</td>
|
||||||
|
{% endif %}
|
||||||
|
</tr>
|
||||||
|
{% endfor %}
|
||||||
|
</tbody>
|
||||||
|
</table>
|
||||||
|
|
||||||
|
<!-- Modals bleiben unverändert -->
|
||||||
|
<!-- Modal für neuen Benutzer -->
|
||||||
|
<div class="modal fade" id="userModal" tabindex="-1" aria-hidden="true">
|
||||||
|
<div class="modal-dialog">
|
||||||
|
<div class="modal-content">
|
||||||
|
<form method="post" id="userForm">
|
||||||
|
<div class="modal-header">
|
||||||
|
<h5 class="modal-title black">Neuen Benutzer anlegen</h5>
|
||||||
|
<button type="button" class="btn-close" data-bs-dismiss="modal"></button>
|
||||||
|
</div>
|
||||||
|
<div class="modal-body">
|
||||||
|
<div class="mb-3">
|
||||||
|
<label>Username</label>
|
||||||
|
<input type="text" name="username" class="form-control" required>
|
||||||
|
</div>
|
||||||
|
<div class="mb-3">
|
||||||
|
<label>Passwort</label>
|
||||||
|
<input type="password" name="password" class="form-control" required>
|
||||||
|
</div>
|
||||||
|
<div class="mb-3">
|
||||||
|
<label>Rolle</label>
|
||||||
|
<select name="is_admin" class="form-control">
|
||||||
|
<option value="0">User</option>
|
||||||
|
<option value="1">Admin</option>
|
||||||
|
</select>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="modal-footer">
|
||||||
|
<button type="submit" name="add_user" value="1" class="btn btn-success">Benutzer anlegen</button>
|
||||||
|
<button type="button" class="btn btn-secondary" data-bs-dismiss="modal">Abbrechen</button>
|
||||||
|
</div>
|
||||||
|
</form>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Modal für Rolle ändern -->
|
||||||
|
<div class="modal fade" id="roleModal" tabindex="-1" aria-hidden="true">
|
||||||
|
<div class="modal-dialog">
|
||||||
|
<div class="modal-content">
|
||||||
|
<form method="post" id="roleForm">
|
||||||
|
<div class="modal-header">
|
||||||
|
<h5 class="modal-title black">Rolle ändern</h5>
|
||||||
|
<button type="button" class="btn-close" data-bs-dismiss="modal"></button>
|
||||||
|
</div>
|
||||||
|
<div class="modal-body">
|
||||||
|
<input type="hidden" name="user_id" id="role_user_id">
|
||||||
|
<div class="mb-3">
|
||||||
|
<label>Username</label>
|
||||||
|
<input type="text" name="username" id="role_username" class="form-control" required>
|
||||||
|
</div>
|
||||||
|
<div class="mb-3">
|
||||||
|
<label>Rolle</label>
|
||||||
|
<select name="is_admin" id="role_is_admin" class="form-control">
|
||||||
|
<option value="0">User</option>
|
||||||
|
<option value="1">Admin</option>
|
||||||
|
</select>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="modal-footer">
|
||||||
|
<button type="submit" name="change_role" value="1" class="btn btn-primary">Rolle ändern</button>
|
||||||
|
<button type="button" class="btn btn-secondary" data-bs-dismiss="modal">Abbrechen</button>
|
||||||
|
</div>
|
||||||
|
</form>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Modal für Passwort ändern -->
|
||||||
|
<div class="modal fade" id="passwordModal" tabindex="-1" aria-hidden="true">
|
||||||
|
<div class="modal-dialog">
|
||||||
|
<div class="modal-content">
|
||||||
|
<form method="post" id="passwordForm">
|
||||||
|
<div class="modal-header">
|
||||||
|
<h5 class="modal-title black">Passwort ändern</h5>
|
||||||
|
<button type="button" class="btn-close" data-bs-dismiss="modal"></button>
|
||||||
|
</div>
|
||||||
|
<div class="modal-body">
|
||||||
|
<input type="hidden" name="user_id" id="password_user_id">
|
||||||
|
<div class="mb-3">
|
||||||
|
<label>Username</label>
|
||||||
|
<input type="text" name="username" id ="password_username" class="form-control" readonly>
|
||||||
|
</div>
|
||||||
|
<div class="mb-3">
|
||||||
|
<label>Neues Passwort</label>
|
||||||
|
<input type="password" name="new_password" class="form-control" required>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="modal-footer">
|
||||||
|
<button type="submit" name="change_password" value="1" class="btn btn-warning">Passwort ändern</button>
|
||||||
|
<button type="button" class="btn btn-secondary" data-bs-dismiss="modal">Abbrechen</button>
|
||||||
|
</div>
|
||||||
|
</form>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<script>
|
||||||
|
function openUserModal() {
|
||||||
|
document.getElementById('userForm').reset();
|
||||||
|
}
|
||||||
|
|
||||||
|
function openRoleModal(user_id, username, is_admin) {
|
||||||
|
document.getElementById('role_user_id').value = user_id;
|
||||||
|
document.getElementById('role_username').value = username;
|
||||||
|
document.getElementById('role_is_admin').value = is_admin;
|
||||||
|
}
|
||||||
|
|
||||||
|
function openPasswordModal(user_id, username) {
|
||||||
|
document.getElementById('password_user_id').value = user_id;
|
||||||
|
document.getElementById('password_username').value = username;
|
||||||
|
document.querySelector("#passwordForm input[name='new_password']").value = "";
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
|
||||||
|
{% endblock %}
|
||||||
@@ -1,8 +0,0 @@
|
|||||||
#IP-RPI:IP-SWITCH:PORT:NAME
|
|
||||||
192.168.120.104:192.168.200.117:37:HAP04
|
|
||||||
192.168.120.110:192.168.200.116:1/14:HAP10
|
|
||||||
192.168.120.114:192.168.200.116:1/26:HAP14
|
|
||||||
192.168.120.115:192.168.200.116:1/23:HAP15
|
|
||||||
192.168.120.118:192.168.200.116:1/34:HAP18
|
|
||||||
192.168.120.123:192.168.200.117:36:HAP23
|
|
||||||
192.168.120.131:192.168.200.118:1/36:HAP31
|
|
||||||
@@ -1,35 +1,55 @@
|
|||||||
#!/bin/bash
|
#!/bin/bash
|
||||||
|
# ============================================================================
|
||||||
|
# PoE Device Check Script
|
||||||
|
# - prüft Erreichbarkeit von Geräten
|
||||||
|
# - startet PoE-Port bei Ausfall neu
|
||||||
|
# - loggt Ereignisse
|
||||||
|
# - löscht alte Logfiles (>30 Tage)
|
||||||
|
# ============================================================================
|
||||||
|
|
||||||
USER="admin"
|
LOG_DIR="/var/log"
|
||||||
PASS="Expl0rer#2022"
|
LOGFILE="$LOG_DIR/rpi-$(date '+%Y%m%d%H%M%S').log"
|
||||||
IP_FILE="/usr/local/bin/custom/ips.list"
|
|
||||||
LOGFILE="/var/log/rpi-$(date '+%Y%m%d%H%M%S').log"
|
# Alte Logfiles löschen (älter als 30 Tage)
|
||||||
|
find "$LOG_DIR" -type f -name "rpi-*.log" -mtime +30 -delete
|
||||||
|
|
||||||
|
# Intervall aus DB abrufen
|
||||||
|
SLEEP=$(python3 - <<'END'
|
||||||
|
import sqlite3
|
||||||
|
conn = sqlite3.connect("/srv/poe_manager/sqlite.db")
|
||||||
|
row = conn.execute("SELECT value FROM settings WHERE key='check_interval'").fetchone()
|
||||||
|
conn.close()
|
||||||
|
print(row[0] if row else 300)
|
||||||
|
END
|
||||||
|
)
|
||||||
|
|
||||||
|
SLEEP=${SLEEP:-300}
|
||||||
|
|
||||||
function disable_poe() {
|
function disable_poe() {
|
||||||
local switch_ip=$1
|
local switch_ip=$1
|
||||||
local port=$2
|
local switch_port=$2
|
||||||
|
local username=$3
|
||||||
|
local password=$4
|
||||||
expect <<EOF
|
expect <<EOF
|
||||||
set timeout 5
|
set timeout 5
|
||||||
spawn ssh $USER@$switch_ip
|
spawn ssh $username@$switch_ip
|
||||||
|
|
||||||
expect {
|
expect {
|
||||||
"assword:" { send "$PASS\r"; exp_continue }
|
"assword:" { send "$password\r"; exp_continue }
|
||||||
"Press any key" { send "\r"; exp_continue }
|
"Press any key" { send "\r"; exp_continue }
|
||||||
-re ".*> $" { }
|
-re ".*> $" { }
|
||||||
}
|
}
|
||||||
send "configure terminal\r"
|
send "configure terminal\r"
|
||||||
expect "(config)#"
|
expect "(config)#"
|
||||||
send "interface $port\r"
|
send "interface $switch_port\r"
|
||||||
expect "(eth-$port)#"
|
expect "(eth-$switch_port)#"
|
||||||
send "no power-over-ethernet\r"
|
send "no power-over-ethernet\r"
|
||||||
expect "(eth-$port)#"
|
expect "(eth-$switch_port)#"
|
||||||
send "exit\r"
|
send "exit\r"
|
||||||
expect "(config)#"
|
expect "(config)#"
|
||||||
send "exit\r"
|
send "exit\r"
|
||||||
expect "#"
|
expect "#"
|
||||||
send "exit\r"
|
send "exit\r"
|
||||||
expect ">"
|
expect ">"; send "exit\r"
|
||||||
send "exit\r"
|
|
||||||
expect "Do you want to log out (y/n)?" { send "y\r" }
|
expect "Do you want to log out (y/n)?" { send "y\r" }
|
||||||
expect eof
|
expect eof
|
||||||
EOF
|
EOF
|
||||||
@@ -37,49 +57,53 @@ EOF
|
|||||||
|
|
||||||
function enable_poe() {
|
function enable_poe() {
|
||||||
local switch_ip=$1
|
local switch_ip=$1
|
||||||
local port=$2
|
local switch_port=$2
|
||||||
|
local username=$3
|
||||||
|
local password=$4
|
||||||
expect <<EOF
|
expect <<EOF
|
||||||
set timeout 5
|
set timeout 5
|
||||||
spawn ssh $USER@$switch_ip
|
spawn ssh $username@$switch_ip
|
||||||
|
|
||||||
expect {
|
expect {
|
||||||
"assword:" { send "$PASS\r"; exp_continue }
|
"assword:" { send "$password\r"; exp_continue }
|
||||||
"Press any key" { send "\r"; exp_continue }
|
"Press any key" { send "\r"; exp_continue }
|
||||||
-re ".*> $" { }
|
-re ".*> $" { }
|
||||||
}
|
}
|
||||||
send "configure terminal\r"
|
send "configure terminal\r"
|
||||||
expect "(config)#"
|
expect "(config)#"
|
||||||
send "interface $port\r"
|
send "interface $switch_port\r"
|
||||||
expect "(eth-$port)#"
|
expect "(eth-$switch_port)#"
|
||||||
send "power-over-ethernet\r"
|
send "power-over-ethernet\r"
|
||||||
expect "(eth-$port)#"
|
expect "(eth-$switch_port)#"
|
||||||
send "exit\r"
|
send "exit\r"
|
||||||
expect "(config)#"
|
expect "(config)#"
|
||||||
send "exit\r"
|
send "exit\r"
|
||||||
expect "#"
|
expect "#"
|
||||||
send "exit\r"
|
send "exit\r"
|
||||||
expect ">"
|
expect ">"; send "exit\r"
|
||||||
send "exit\r"
|
|
||||||
expect "Do you want to log out (y/n)?" { send "y\r" }
|
expect "Do you want to log out (y/n)?" { send "y\r" }
|
||||||
expect eof
|
expect eof
|
||||||
EOF
|
EOF
|
||||||
}
|
}
|
||||||
|
|
||||||
echo "" > $LOGFILE
|
echo "" > "$LOGFILE"
|
||||||
|
|
||||||
while true; do
|
while true; do
|
||||||
echo "--------------------------------------------------------------------" >> $LOGFILE
|
echo "--------------------------------------------------------------------" >> "$LOGFILE"
|
||||||
while IFS=: read -r ip switch port hap; 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
|
||||||
ping -c 1 -W 2 $ip &> /dev/null
|
if ping -c 1 -W 2 "$rpi_ip" &> /dev/null; then
|
||||||
if [ $? -ne 0 ]; then
|
echo "$(date '+%Y-%m-%d %H:%M:%S') $dev_name ist erreichbar!" >> "$LOGFILE"
|
||||||
echo "$(date '+%Y-%m-%d %H:%M:%S') $hap ist nicht erreichbar!" >> $LOGFILE
|
else
|
||||||
disable_poe $switch $port
|
echo "$(date '+%Y-%m-%d %H:%M:%S') $dev_name ist nicht erreichbar!" >> "$LOGFILE"
|
||||||
echo "$(date '+%Y-%m-%d %H:%M:%S') $hap PoE auf Port $port für IP $ip am Switch $switch deaktiviert." >> $LOGFILE
|
|
||||||
sleep 2
|
# Nur PoE neu starten, wenn Port vorhanden ist
|
||||||
enable_poe $switch $port
|
if [ -n "$switch_port" ] && [ "$switch_port" != "None" ]; then
|
||||||
echo "$(date '+%Y-%m-%d %H:%M:%S') $hap PoE auf Port $port für IP $ip am Switch $switch aktiviert." >> $LOGFILE
|
disable_poe "$switch_ip" "$switch_port" "$switch_user" "$switch_pass"
|
||||||
else
|
echo "$(date '+%Y-%m-%d %H:%M:%S') $dev_name PoE auf Port $switch_port am Switch $switch_hostname deaktiviert." >> "$LOGFILE"
|
||||||
echo "$(date '+%Y-%m-%d %H:%M:%S') $hap ist erreichbar!" >> $LOGFILE
|
sleep 2
|
||||||
fi
|
enable_poe "$switch_ip" "$switch_port" "$switch_user" "$switch_pass"
|
||||||
done < "$IP_FILE"
|
echo "$(date '+%Y-%m-%d %H:%M:%S') $dev_name PoE auf Port $switch_port am Switch $switch_hostname aktiviert." >> "$LOGFILE"
|
||||||
sleep 300
|
fi
|
||||||
done
|
fi
|
||||||
|
done
|
||||||
|
sleep "$SLEEP"
|
||||||
|
done
|
||||||
|
|||||||
Reference in New Issue
Block a user