Initial Commit
This commit is contained in:
@@ -0,0 +1,175 @@
|
||||
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)
|
||||
Reference in New Issue
Block a user