Files
felhom.eu/documentation/audits/sizing-2026-10-13/tools/catalog_check.py
T
admin c885b5acf4 Audit: what a Felhom box needs — floor, 64 apps' memory and CPU, power (N100 bench); R-993..R-995; R-231 due check read green
Measured on demo-felhom (N100, 16 GB): 61 apps deployed and removed through the
product, sampled every 5 s; heavy apps with one real action (Immich 200 photos,
Jellyfin transcode, Paperless OCR, Nextcloud 500 MB, ...). sizing.json for the
estimator (tools/build_sizing.py), SIZING.md with the proposed minimum machine,
METHOD.md. Power is RAPL CPU-package only (no meter). R-993 asks the operator to
approve the minimum machine; R-994 lists catalog limits/requests that are too low;
R-995 records leftover unnamed volumes after removal. R-231's dated check measured:
dooplex-backup.service success, journal green.
2026-10-13 13:23:51 +02:00

37 lines
2.1 KiB
Python

#!/usr/bin/env python3
"""catalog_check.py — §7 C: measured memory against each template's mem_request and mem_limit.
Writes ../evidence/catalog-check.json and prints two lists:
RISK the app's peak > 80 % of its mem_limit, OR any one container's bench peak > 80 % of that
container's own compose limit (the limit Docker enforces is per container)
OVER the app's typical use > its mem_request (the catalog under-states what it needs)
Read only: nothing in the catalog is changed.
"""
import json, os
HERE = os.path.dirname(os.path.abspath(__file__))
EV = os.path.join(os.path.dirname(HERE), "evidence")
t = json.load(open(os.path.join(EV, "apps-table.json")))
risk, over = [], []
for a in sorted(t, key=lambda a: a["slug"]):
if a["source"] == "unmeasured":
continue
lim, req = a["catalog_limit_mb"], a["catalog_request_mb"]
why = []
if lim and a["mem_peak_mb"] and a["mem_peak_mb"] > 0.8 * lim:
why.append(f"app peak {a['mem_peak_mb']} MB = {100 * a['mem_peak_mb'] / lim:.0f} % of mem_limit {lim} MB")
for cname, c in ((a.get("bench") or {}).get("containers") or {}).items():
if c.get("limit_mb") and c["peak_used_mb"] > 0.8 * c["limit_mb"]:
why.append(f"container {cname} peak {c['peak_used_mb']} MB = {100 * c['peak_used_mb'] / c['limit_mb']:.0f} % of its {c['limit_mb']} MB limit")
if why:
risk.append(dict(slug=a["slug"], why=why, source=a["source"]))
if req and a["mem_typical_mb"] and a["mem_typical_mb"] > req:
over.append(dict(slug=a["slug"], typical_mb=a["mem_typical_mb"], mem_request_mb=req,
ratio=round(a["mem_typical_mb"] / req, 1), source=a["source"]))
json.dump(dict(risk=risk, over_request=over), open(os.path.join(EV, "catalog-check.json"), "w"), indent=1)
print("RISK (peak > 80 % of a limit):", len(risk))
for r in risk:
print(f" {r['slug']:16} {r['source']:12} " + "; ".join(r["why"]))
print("OVER (typical > mem_request):", len(over))
for o in sorted(over, key=lambda o: -o["ratio"]):
print(f" {o['slug']:16} typical {o['typical_mb']:>5} > request {o['mem_request_mb']:>5} x{o['ratio']} ({o['source']})")