summaryrefslogtreecommitdiff
path: root/hetzner-admin
diff options
context:
space:
mode:
authoryyamashita <yyamashita@hetzner.yyamashita.com>2026-11-01 04:24:31 +0900
committeryyamashita <yyamashita@hetzner.yyamashita.com>2026-11-01 04:24:31 +0900
commitbe8565934a15c49eeeacb84dd30c0a1eb9ea8770 (patch)
tree00d237c9f20ed13a5d3a7d2758e703641a34510e /hetzner-admin
parentde58a03a219fe2e93da8fef32ba3484149befb0f (diff)
hetzner-admin: show CPU load / memory / disk / uptime, add Container Stats command
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01N5eS3SG9dxKszFC7t3QZn9
Diffstat (limited to 'hetzner-admin')
-rw-r--r--hetzner-admin/app.py44
-rwxr-xr-xhetzner-admin/run-command.sh11
2 files changed, 55 insertions, 0 deletions
diff --git a/hetzner-admin/app.py b/hetzner-admin/app.py
index a9377a8..17895d9 100644
--- a/hetzner-admin/app.py
+++ b/hetzner-admin/app.py
@@ -55,6 +55,7 @@ app.config.update(
COMMANDS = [
("Docker Status", "docker ps"),
+ ("Container Stats", "docker stats"),
("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"),
@@ -110,6 +111,20 @@ HTML = """<!DOCTYPE html>
<body>
<h3>Server Admin</h3>
<p><a href="https://status.yyamashita.com">Status</a></p>
+<h4>Resources</h4>
+{% if resources is string %}
+<p style="color:red">{{ resources }}</p>
+{% else %}
+<table>
+{% for label, pct, text in resources %}
+<tr>
+ <td>{{ label }}</td>
+ <td>{% if pct is not none %}<meter min="0" max="100" low="70" high="90" optimum="0" value="{{ pct }}"></meter>{% endif %}</td>
+ <td>{{ text }}</td>
+</tr>
+{% endfor %}
+</table>
+{% endif %}
<h4>Services</h4>
{% if services is string %}
<p style="color:red">{{ services }}</p>
@@ -214,10 +229,39 @@ def load_services():
return [(name, states.get(name, "unknown")) for name in SERVICES]
+def fmt_bytes(n):
+ for unit in ("B", "KiB", "MiB", "GiB", "TiB"):
+ if n < 1024 or unit == "TiB":
+ return f"{n:.1f} {unit}"
+ n /= 1024
+
+
+def load_resources():
+ """[(label, percent|None, text), ...]。取得できなければエラーメッセージ(str)"""
+ try:
+ out = run_ssh("resources")
+ v = {k: rest.split() for k, _, rest in (line.partition(" ") for line in out.splitlines())}
+ cpus = int(v["cpus"][0])
+ load = [float(x) for x in v["load"]]
+ mem_total, mem_used = (int(x) for x in v["mem"])
+ disk_total, disk_used = (int(x) for x in v["disk"])
+ up = int(v["uptime"][0])
+ except Exception as e:
+ return f"Error loading resources: {e}"
+ return [
+ ("CPU", min(100, load[0] / cpus * 100),
+ f"load {' / '.join(f'{x:.2f}' for x in load)} ({cpus} cores)"),
+ ("Memory", mem_used / mem_total * 100, f"{fmt_bytes(mem_used)} / {fmt_bytes(mem_total)}"),
+ ("Disk /", disk_used / disk_total * 100, f"{fmt_bytes(disk_used)} / {fmt_bytes(disk_total)}"),
+ ("Uptime", None, f"{up // 86400}d {up % 86400 // 3600}h {up % 3600 // 60}m"),
+ ]
+
+
def render_index(output=None):
return render_template_string(
HTML,
commands=COMMANDS,
+ resources=load_resources(),
services=load_services(),
output=output,
passkey_count=len(load_passkeys()),
diff --git a/hetzner-admin/run-command.sh b/hetzner-admin/run-command.sh
index d1f68f8..b473ce4 100755
--- a/hetzner-admin/run-command.sh
+++ b/hetzner-admin/run-command.sh
@@ -56,9 +56,20 @@ case "$SSH_ORIGINAL_COMMAND" in
if [ -n "$unit" ]; then systemctl enable --now "$unit" 2>&1; else docker compose -f "$compose" up -d 2>&1; fi
fi
;;
+ "resources")
+ # 管理画面のリソース表示用。"key value..." 形式で出力し app.py がパースする
+ echo "cpus $(nproc)"
+ echo "load $(cut -d' ' -f1-3 /proc/loadavg)"
+ awk '/^MemTotal:/{t=$2} /^MemAvailable:/{a=$2} END{print "mem", t*1024, (t-a)*1024}' /proc/meminfo
+ df -B1 --output=size,used / | awk 'NR==2{print "disk", $1, $2}'
+ echo "uptime $(cut -d. -f1 /proc/uptime)"
+ ;;
"docker ps")
exec docker ps
;;
+ "docker stats")
+ exec docker stats --no-stream --format 'table {{.Name}}\t{{.CPUPerc}}\t{{.MemUsage}}\t{{.MemPerc}}'
+ ;;
"docker compose -f /app/infra/caddy/docker-compose.yml restart")
exec docker compose -f /app/infra/caddy/docker-compose.yml restart
;;