Files
RelayDock/app.py
T
2026-05-21 23:14:36 +02:00

176 lines
5.9 KiB
Python

from flask import Flask, render_template, request, redirect, url_for, flash
import json
import os
import re
import subprocess
from pathlib import Path
APP_TITLE = "RelayDock"
CONFIG_PATH = Path(os.environ.get("RELAYDOCK_CONFIG", "./config.json"))
RECONCILE_CMD = os.environ.get("RELAYDOCK_RECONCILE", "./relaydock-reconcile.sh")
SERVICE_PREFIX = os.environ.get("RELAYDOCK_PREFIX", "relaydock")
app = Flask(__name__)
app.secret_key = os.environ.get("FLASK_SECRET_KEY", "change-me")
NAME_RE = re.compile(r"^[a-zA-Z0-9._-]+$")
PROTO_SET = {"udp", "tcp"}
ROLE_SET = {"oracle", "pi"}
def default_config():
return {"role": "oracle", "rules": []}
def load_config():
if not CONFIG_PATH.exists():
return default_config()
with CONFIG_PATH.open("r", encoding="utf-8") as f:
return json.load(f)
def save_config(data):
CONFIG_PATH.parent.mkdir(parents=True, exist_ok=True)
with CONFIG_PATH.open("w", encoding="utf-8") as f:
json.dump(data, f, indent=2)
f.write("\n")
def validate_rule(rule, config):
errors = []
name = rule.get("name", "").strip()
proto = rule.get("proto", "").strip().lower()
target_host = rule.get("target_host", "").strip()
try:
listen_port = int(rule.get("listen_port", 0))
target_port = int(rule.get("target_port", 0))
except ValueError:
errors.append("Ports müssen Zahlen sein.")
return errors
if not NAME_RE.match(name):
errors.append("Name darf nur Buchstaben, Zahlen, Punkt, Unterstrich und Bindestrich enthalten.")
if proto not in PROTO_SET:
errors.append("Proto muss udp oder tcp sein.")
if not (1 <= listen_port <= 65535 and 1 <= target_port <= 65535):
errors.append("Ports müssen zwischen 1 und 65535 liegen.")
if not target_host:
errors.append("Target Host darf nicht leer sein.")
existing = [r["name"] for r in config.get("rules", [])]
if name in existing:
errors.append("Name existiert bereits.")
return errors
def run_cmd(cmd):
return subprocess.run(cmd, text=True, capture_output=True)
def unit_name(name):
return f"{SERVICE_PREFIX}@{name}.service"
@app.route("/")
def index():
config = load_config()
rules = []
for rule in config.get("rules", []):
svc = unit_name(rule["name"])
state = run_cmd(["systemctl", "is-active", svc])
enabled = run_cmd(["systemctl", "is-enabled", svc])
rules.append({
**rule,
"service": svc,
"active": state.stdout.strip() if state.returncode == 0 else state.stdout.strip() or "inactive",
"enabled": enabled.stdout.strip() if enabled.returncode == 0 else enabled.stdout.strip() or "disabled",
})
return render_template("index.html", title=APP_TITLE, config=config, rules=rules)
@app.post("/role")
def set_role():
config = load_config()
role = request.form.get("role", "").strip().lower()
if role not in ROLE_SET:
flash("Ungültige Rolle.", "error")
return redirect(url_for("index"))
config["role"] = role
save_config(config)
flash("Rolle gespeichert.", "success")
return redirect(url_for("index"))
@app.post("/rules/add")
def add_rule():
config = load_config()
rule = {
"name": request.form.get("name", ""),
"proto": request.form.get("proto", ""),
"listen_port": request.form.get("listen_port", "0"),
"target_host": request.form.get("target_host", ""),
"target_port": request.form.get("target_port", "0"),
}
errors = validate_rule(rule, config)
if errors:
for err in errors:
flash(err, "error")
return redirect(url_for("index"))
rule["listen_port"] = int(rule["listen_port"])
rule["target_port"] = int(rule["target_port"])
config.setdefault("rules", []).append(rule)
save_config(config)
flash("Regel hinzugefügt.", "success")
return redirect(url_for("index"))
@app.post("/rules/<name>/delete")
def delete_rule(name):
config = load_config()
before = len(config.get("rules", []))
config["rules"] = [r for r in config.get("rules", []) if r["name"] != name]
save_config(config)
if len(config["rules"]) < before:
flash(f"Regel {name} gelöscht.", "success")
else:
flash("Regel nicht gefunden.", "error")
return redirect(url_for("index"))
@app.post("/rules/<name>/toggle")
def toggle_rule(name):
svc = unit_name(name)
state = run_cmd(["systemctl", "is-active", svc])
if state.returncode == 0 and state.stdout.strip() == "active":
result = run_cmd(["sudo", "systemctl", "stop", svc])
flash(result.stderr.strip() or result.stdout.strip() or f"{svc} gestoppt.", "success" if result.returncode == 0 else "error")
else:
result = run_cmd(["sudo", "systemctl", "start", svc])
flash(result.stderr.strip() or result.stdout.strip() or f"{svc} gestartet.", "success" if result.returncode == 0 else "error")
return redirect(url_for("index"))
@app.post("/rules/<name>/restart")
def restart_rule(name):
svc = unit_name(name)
result = run_cmd(["sudo", "systemctl", "restart", svc])
flash(result.stderr.strip() or result.stdout.strip() or f"{svc} neugestartet.", "success" if result.returncode == 0 else "error")
return redirect(url_for("index"))
@app.post("/reconcile")
def reconcile():
result = run_cmd(["sudo", RECONCILE_CMD, str(CONFIG_PATH)])
flash(result.stderr.strip() or result.stdout.strip() or "Synchronisierung ausgeführt.", "success" if result.returncode == 0 else "error")
return redirect(url_for("index"))
@app.post("/daemon-reload")
def daemon_reload():
result = run_cmd(["sudo", "systemctl", "daemon-reload"])
flash(result.stderr.strip() or result.stdout.strip() or "systemd daemon-reload ausgeführt.", "success" if result.returncode == 0 else "error")
return redirect(url_for("index"))
if __name__ == "__main__":
app.run(host="0.0.0.0", port=8080, debug=True)