diff options
| author | yyamashita <yyamashita@hetzner.yyamashita.com> | 2026-11-01 04:24:31 +0900 |
|---|---|---|
| committer | yyamashita <yyamashita@hetzner.yyamashita.com> | 2026-11-01 04:24:31 +0900 |
| commit | be8565934a15c49eeeacb84dd30c0a1eb9ea8770 (patch) | |
| tree | 00d237c9f20ed13a5d3a7d2758e703641a34510e /hetzner-admin/app.py | |
| parent | de58a03a219fe2e93da8fef32ba3484149befb0f (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/app.py')
| -rw-r--r-- | hetzner-admin/app.py | 44 |
1 files changed, 44 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()), |
