Logo

Personal Ops Runbook

Personal runbook covering infrastructure operations for Cloud, Kubernetes, OpenStack, and Ceph environments. Includes deployment and teardown procedures, node management, cluster monitoring setup, and incident response workflows compiled from day-to-day operational work. Intended strictly for personal reference — configurations and scripts are environment-specific and not guaranteed to work as-is elsewhere.

Chạy trên node OSD

Lưu ý:

  • SLEEP: mặc định là 0.2, tăng 0.5-1.0 nếu node đang tải cao
  • POOLID: giúp tính OSD đang dùng bao nhiêu cho riêng pool này
#!/usr/bin/env python3
import json, re, subprocess, time, socket, datetime
import urllib.request

PG_API_BASE = "http://10.166.11.11:1024"
SLEEP = 0.2

def sh(cmd):
    return subprocess.check_output(cmd, text=True, stderr=subprocess.STDOUT)

def j(cmd):
    return json.loads(sh(cmd))

def GiB(x): return x / (1024**3)
def TiB(x): return x / (1024**4)

def discover_osd_containers():
    out = sh(["podman", "ps", "--format", "{{.Names}}"])
    names = [x.strip() for x in out.splitlines() if x.strip()]
    res = []
    pat = re.compile(r"^ceph-([0-9a-fA-F\-]{8,})-osd-(\d+)$")
    for n in names:
        low = n.lower()
        if any(x in low for x in ["mon","mgr","mds","rgw","crash","exporter","node-exporter","prometheus"]):
            continue
        m = pat.match(n)
        if m:
            res.append((n, m.group(1), int(m.group(2))))
    return sorted(res, key=lambda x: x[2])

def api_get_pools(osd_id: int):
    url = f"{PG_API_BASE}/pg-by-osd/{osd_id}"
    with urllib.request.urlopen(url, timeout=10) as resp:
        data = json.loads(resp.read().decode("utf-8"))
    if not data.get("ok"):
        raise RuntimeError(f"API error: {data}")
    return data.get("pools", [])

def daemon_json(container, osd_id, args):
    return j(["podman", "exec", container, "ceph", "daemon", f"osd.{osd_id}"] + args)

def main():
    hostname = socket.gethostname()
    run_ts = datetime.datetime.now(datetime.timezone.utc).isoformat()

    rows = []
    for cname, fsid, oid in discover_osd_containers():
        row = {
            "ok": False,
            "timestamp_utc": run_ts,
            "hostname": hostname,
            "fsid": fsid,
            "container": cname,
            "osd": f"osd.{oid}",
        }

        try:
            pools = api_get_pools(oid)
            row["pools_from_api"] = pools

            perf = daemon_json(cname, oid, ["perf", "dump"])
            osd_total = perf["osd"]["stat_bytes"]
            osd_used  = perf["osd"]["stat_bytes_used"]
            osd_avail = perf["osd"].get("stat_bytes_avail", None)
            db_used   = perf.get("bluefs", {}).get("db_used_bytes", 0)

            # bluefs device info
            bdi = daemon_json(cname, oid, ["bluestore", "bluefs", "device", "info"])
            dev = bdi.get("dev", {})
            # note: key "bluefs max available" có space
            bluefs_max_avail = dev.get("bluefs max available", None)

            # frag score
            fragj = daemon_json(cname, oid, ["bluestore", "allocator", "score", "block"])
            frag = fragj.get("fragmentation_rating", None)

            pool_alloc_sum = 0
            pool_data_stored_sum = 0
            per_pool = {}

            for p in pools:
                ps = daemon_json(cname, oid, ["dump_pool_statfs", str(p)])
                alloc = ps.get("allocated", 0)
                stored = ps.get("data_stored", 0)
                pool_alloc_sum += alloc
                pool_data_stored_sum += stored
                per_pool[str(p)] = {
                    "allocated_bytes": alloc,
                    "data_stored_bytes": stored
                }

            unacc_bytes = osd_used - pool_alloc_sum - db_used

            row.update({
                "ok": True,

                "osd_total_bytes": osd_total,
                "osd_used_bytes": osd_used,
                "osd_avail_bytes": osd_avail,
                "osd_used_pct": (osd_used * 100.0 / osd_total) if osd_total else None,

                "bluefs_db_used_bytes": db_used,

                "bluefs_device": dev.get("device", None),
                "bluefs_device_total_bytes": dev.get("total", None),
                "bluefs_device_free_bytes": dev.get("free", None),
                "bluefs_used_bytes": dev.get("bluefs_used", None),
                "bluefs_max_avail_bytes": bluefs_max_avail,

                "frag": frag,

                "pool_alloc_sum_bytes": pool_alloc_sum,
                "pool_data_stored_sum_bytes": pool_data_stored_sum,
                "per_pool": per_pool,

                "unaccounted_bytes": unacc_bytes,

                "osd_used_TiB": TiB(osd_used),
                "pool_alloc_sum_TiB": TiB(pool_alloc_sum),
                "unaccounted_GiB": GiB(unacc_bytes),
                "db_used_GiB": GiB(db_used),

                "bluefs_device_free_GiB": GiB(dev["free"]) if isinstance(dev.get("free"), (int, float)) else None,
                "bluefs_device_total_TiB": TiB(dev["total"]) if isinstance(dev.get("total"), (int, float)) else None,
            })

        except Exception as e:
            row["error"] = str(e).strip()

        rows.append(row)
        time.sleep(SLEEP)

    print(json.dumps(rows, ensure_ascii=False, indent=2))

if __name__ == "__main__":
    main()

Chạy

python3 osd_unaccounted_autopools_plus.py > /home/hoanghd3/osd_autopools_plus.json

Check nhanh

jq '.[0]' /home/hoanghd3/osd_autopools_plus.json
jq '[.[] | select(.ok==true) | .unaccounted_bytes] | add / 1099511627776' /home/hoanghd3/osd_autopools_plus.json

Output là JSON list với:

  • tự lấy hostname
  • tự detect FSID từ tên container ceph-<FSID>-osd-<ID> (khỏi hardcode)
  • timestamp (ISO8601) cho mỗi lần chạy
  • vẫn lọc chỉ OSD containers, không dính crash/mon/mgr/exporter
  • output là một mảng JSON (list), mỗi OSD = 1 object

Ví dụ output:

root@SOC-CEPH-PUB-C06-OSD-050:/home/hoanghd3# jq '.[0]' /home/hoanghd3/osd_autopools_plus.json
{
  "ok": true,
  "timestamp_utc": "2026-01-13T17:45:59.565834+00:00",
  "hostname": "SOC-CEPH-PUB-C06-OSD-050",
  "fsid": "fe23f650-2b8f-11ee-9575-17906427caf5",
  "container": "ceph-fe23f650-2b8f-11ee-9575-17906427caf5-osd-608",
  "osd": "osd.608",
  "pools_from_api": [
    4
  ],
  "osd_total_bytes": 1600315719680,
  "osd_used_bytes": 1016167370752,
  "osd_avail_bytes": 584148348928,
  "osd_used_pct": 63.4979309554738,
  "bluefs_db_used_bytes": 4420141056,
  "bluefs_device": "BDEV_DB",
  "bluefs_device_total_bytes": 1600315719680,
  "bluefs_device_free_bytes": 584146690048,
  "bluefs_used_bytes": 4420206592,
  "bluefs_max_avail_bytes": 297222275072,
  "frag": 0.8989855746934527,
  "pool_alloc_sum_bytes": 1011458252800,
  "pool_data_stored_sum_bytes": 1073563047769,
  "per_pool": {
    "4": {
      "allocated_bytes": 1011458252800,
      "data_stored_bytes": 1073563047769
    }
  },
  "unaccounted_bytes": 288976896,
  "osd_used_TiB": 0.9241988398134708,
  "pool_alloc_sum_TiB": 0.9199159219861031,
  "unaccounted_GiB": 0.2691307067871094,
  "db_used_GiB": 4.1165771484375,
  "bluefs_device_free_GiB": 544.0289993286133,
  "bluefs_device_total_TiB": 1.4554786682128906
}

root@SOC-CEPH-PUB-C06-OSD-050:/home/hoanghd3# jq '[.[] | select(.ok==true) | .unaccounted_bytes] | add / 1099511627776' /home/hoanghd3/osd_autopools_plus.json
0.0042819008231163025

MON chạy Flask API, còn script trên OSD node gọi API để lấy list pool-id đang có PG trên OSD đó, rồi script sẽ tự dump_pool_statfs theo đúng pool-id thay vì hardcode POOLID=3.

Mình đưa bạn 2 file:

  1. Flask API (chạy trên MON) → trả JSON từ ceph pg ls-by-osd <id>
  2. OSD-side script (chạy trên OSD node) → gọi API để lấy pool list, rồi tính unaccounted chính xác hơn

Lưu ý bảo mật: API này chỉ nên mở nội bộ, có token (header) và allowlist IP nếu được.


1) Flask API trên MON node

File: /opt/ceph_pg_api/app.py

#!/usr/bin/env python3
import os
import re
import json
import subprocess
from flask import Flask, request, jsonify

app = Flask(__name__)

# Token đơn giản để tránh ai gọi bậy
API_TOKEN = os.getenv("CEPH_PG_API_TOKEN", "")

def require_token():
    if not API_TOKEN:
        return True  # nếu bạn không set token thì bỏ qua check
    return request.headers.get("X-Api-Token", "") == API_TOKEN

def run(cmd):
    # timeout để tránh treo
    out = subprocess.check_output(cmd, text=True, stderr=subprocess.STDOUT, timeout=20)
    return out

def parse_pools_from_text(output: str):
    """
    Parse pool id từ output dạng text của `ceph pg ls-by-osd <id>`
    PGID thường có dạng: <poolid>.<something>
    """
    pools = set()
    pgids = []
    for line in output.splitlines():
        line = line.strip()
        if not line:
            continue
        # skip header lines kiểu "PG_STAT" hoặc "pg_stat"
        if line.lower().startswith("pg_stat") or line.lower().startswith("pgid"):
            continue
        # lấy token đầu tiên có dạng N.xxx
        m = re.search(r"\b(\d+)\.[0-9a-fA-F]+\b", line)
        if m:
            pool = int(m.group(1))
            pools.add(pool)
            pgids.append(m.group(0))
    return sorted(pools), pgids

@app.get("/health")
def health():
    return jsonify({"ok": True})

@app.get("/pg-by-osd/<int:osd_id>")
def pg_by_osd(osd_id: int):
    if not require_token():
        return jsonify({"ok": False, "error": "unauthorized"}), 401

    # ưu tiên json output nếu ceph hỗ trợ
    # nhiều cluster hỗ trợ -f json cho subcommand này; nếu fail thì fallback text parsing
    try:
        out = run(["ceph", "pg", "ls-by-osd", str(osd_id), "-f", "json"])
        data = json.loads(out)

        # cấu trúc json tùy version; mình làm mềm:
        # cố gắng lấy danh sách pgid ở nhiều chỗ khác nhau
        pgids = []
        pools = set()

        # case: {"pg_stats":[{"pgid":"3.abc",...},...]}
        if isinstance(data, dict) and "pg_stats" in data and isinstance(data["pg_stats"], list):
            for x in data["pg_stats"]:
                pgid = x.get("pgid") or x.get("pgid_str") or x.get("pg")
                if isinstance(pgid, str) and "." in pgid:
                    pgids.append(pgid)
                    pools.add(int(pgid.split(".", 1)[0]))

        # case khác: list trực tiếp
        elif isinstance(data, list):
            for x in data:
                pgid = x.get("pgid") if isinstance(x, dict) else None
                if isinstance(pgid, str) and "." in pgid:
                    pgids.append(pgid)
                    pools.add(int(pgid.split(".", 1)[0]))

        # nếu json parse không ra gì thì fallback text
        if not pools:
            raise ValueError("json parsed but no pgids found")

        return jsonify({
            "ok": True,
            "osd_id": osd_id,
            "pools": sorted(pools),
            "pg_count": len(pgids),
            "pgids_sample": pgids[:50],  # sample để response không quá to
            "source": "json"
        })

    except Exception:
        # fallback: text
        out = run(["ceph", "pg", "ls-by-osd", str(osd_id)])
        pools, pgids = parse_pools_from_text(out)
        return jsonify({
            "ok": True,
            "osd_id": osd_id,
            "pools": pools,
            "pg_count": len(pgids),
            "pgids_sample": pgids[:50],
            "source": "text"
        })

if __name__ == "__main__":
    # dev only; production dùng gunicorn
    app.run(host="0.0.0.0", port=5001)

Cài & chạy nhanh (test)

# trên MON
python3 -m venv /opt/ceph_pg_api/venv
/opt/ceph_pg_api/venv/bin/pip install flask

export CEPH_PG_API_TOKEN="your-secret-token"
/opt/ceph_pg_api/venv/bin/python /opt/ceph_pg_api/app.py

Test:

curl -s -H 'X-Api-Token: your-secret-token' http://127.0.0.1:5001/pg-by-osd/229 | jq .

Chạy production bằng systemd + gunicorn (khuyên dùng)

/opt/ceph_pg_api/venv/bin/pip install gunicorn

/etc/systemd/system/ceph-pg-api.service:

[Unit]
Description=Ceph PG API (ls-by-osd)
After=network.target

[Service]
User=root
WorkingDirectory=/opt/ceph_pg_api
Environment=CEPH_PG_API_TOKEN=your-secret-token
ExecStart=/opt/ceph_pg_api/venv/bin/gunicorn -w 2 -b 0.0.0.0:5001 app:app
Restart=always
RestartSec=2

[Install]
WantedBy=multi-user.target

Enable:

systemctl daemon-reload
systemctl enable --now ceph-pg-api
systemctl status ceph-pg-api

2) Script trên OSD node: gọi API để lấy pool list rồi tính “unaccounted” chuẩn

Bạn dùng version “JSON list output” và thêm phần gọi API như sau.

File: osd_unaccounted_autopools.py (chạy trên OSD node)


Chạy:

python3 osd_unaccounted_autopools.py > /tmp/osd_autopools.json
jq '.[0]' /tmp/osd_autopools.json
root@SOC-CEPH-PUB-C06-OSD-050:/home/hoanghd3# curl  http://10.166.21.11:1024/pg-by-osd/229
{"ok":true,"osd_id":229,"pg_count":84,"pgids_sample":["3.ad","3.ca","3.d4","3.18a","3.1a3","3.33c","3.3e7","3.506","3.668","3.6e3","3.6e8","3.7df","3.7e7","3.839","3.87d","3.936","3.938","3.960","3.a6b","3.bbb","3.bfc","3.c63","3.c94","3.db1","3.f6a","3.ff7","3.1050","3.1299","3.131c","3.1371","3.1687","3.1771","3.184d","3.191b","3.1921","3.192c","3.19a6","3.1b00","3.1b80","3.1c35","3.1c6c","3.1c99","3.1cce","3.1cf5","3.1eae","3.1f53","3.1f90","3.1ff3","3.23a5","3.2411"],"pools":[3],"source":"json"}