summaryrefslogtreecommitdiff
path: root/hetzner-admin/app.py
diff options
context:
space:
mode:
Diffstat (limited to 'hetzner-admin/app.py')
-rw-r--r--hetzner-admin/app.py95
1 files changed, 95 insertions, 0 deletions
diff --git a/hetzner-admin/app.py b/hetzner-admin/app.py
new file mode 100644
index 0000000..ccaadce
--- /dev/null
+++ b/hetzner-admin/app.py
@@ -0,0 +1,95 @@
+import functools
+import os
+
+import paramiko
+from flask import Flask, Response, render_template_string, request
+
+app = Flask(__name__)
+
+ADMIN_USER = os.environ["ADMIN_USER"]
+ADMIN_PASS = os.environ["ADMIN_PASS"]
+SSH_HOST = os.environ.get("SSH_HOST", "host.docker.internal")
+SSH_PORT = int(os.environ.get("SSH_PORT", "22"))
+SSH_USER = os.environ.get("SSH_USER", "root")
+SSH_KEY_PATH = os.environ.get("SSH_KEY_PATH", "/app/id_rsa")
+
+COMMANDS = [
+ ("Docker Status", "docker ps"),
+ ("Restart Caddy", "docker compose -f /app/infra/caddy/docker-compose.yml restart"),
+ ("Restart Mail", "docker compose -f /app/infra/mail/docker-compose.yml restart"),
+ ("Restart cgit", "docker compose -f /app/infra/cgit/docker-compose.yml restart"),
+ ("Restart Admin", "docker compose -f /app/infra/hetzner-admin/docker-compose.yml restart"),
+ ("Reboot Server", "reboot"),
+]
+
+COMMAND_MAP = {label: cmd for label, cmd in COMMANDS}
+
+HTML = """<!DOCTYPE html>
+<html>
+<head><title>Server Admin</title></head>
+<body>
+<h3>Server Admin</h3>
+{% for label, _ in commands %}
+<form method="post" action="/run" style="display:inline;margin-right:4px">
+ <input type="hidden" name="cmd" value="{{ label }}">
+ {% if label == "Reboot Server" %}
+ <button onclick="return confirm('Really reboot?')">{{ label }}</button>
+ {% else %}
+ <button>{{ label }}</button>
+ {% endif %}
+</form>
+{% endfor %}
+{% if output is not none %}
+<h4>Output:</h4>
+<pre>{{ output }}</pre>
+{% endif %}
+</body>
+</html>"""
+
+
+def requires_auth(f):
+ @functools.wraps(f)
+ def decorated(*args, **kwargs):
+ auth = request.authorization
+ if not auth or auth.username != ADMIN_USER or auth.password != ADMIN_PASS:
+ return Response(
+ "Unauthorized", 401, {"WWW-Authenticate": 'Basic realm="Admin"'}
+ )
+ return f(*args, **kwargs)
+
+ return decorated
+
+
+def run_ssh(command):
+ key = paramiko.RSAKey.from_private_key_file(SSH_KEY_PATH)
+ client = paramiko.SSHClient()
+ client.set_missing_host_key_policy(paramiko.AutoAddPolicy())
+ client.connect(SSH_HOST, port=SSH_PORT, username=SSH_USER, pkey=key, timeout=10)
+ try:
+ _, stdout, stderr = client.exec_command(command, timeout=30)
+ out = stdout.read().decode(errors="replace")
+ err = stderr.read().decode(errors="replace")
+ return (out + err).strip() or "(no output)"
+ finally:
+ client.close()
+
+
+@app.route("/")
+@requires_auth
+def index():
+ return render_template_string(HTML, commands=COMMANDS, output=None)
+
+
+@app.route("/run", methods=["POST"])
+@requires_auth
+def run():
+ label = request.form.get("cmd", "")
+ command = COMMAND_MAP.get(label)
+ if not command:
+ output = f"Unknown command: {label}"
+ else:
+ try:
+ output = run_ssh(command)
+ except Exception as e:
+ output = f"Error: {e}"
+ return render_template_string(HTML, commands=COMMANDS, output=output)