Newer
Older
hard-panel / panel / backend / app / shares_probe.py
"""Фоновый пробер сетевых хранилищ.

Панель сама опрашивает примонтированные к её машине шарды (os.statvfs):
раз в GHARD_SHARE_INTERVAL секунд пишет точку в share_samples. Путь не
читается (отвалился NFS/SMB, сеть лежит) — пишется ok=0, панель покажет
«offline». Старше 7 дней точки удаляются (политика хранения — этап 5).
"""

import asyncio
import os
from datetime import datetime, timedelta, timezone

from app.config import get_settings
from app.db import get_db

RETENTION_DAYS = 7


def _probe(path: str) -> tuple[int, int, int]:
    """(total, used, ok) в байтах; недоступный путь → (0, 0, 0)."""
    try:
        st = os.statvfs(path)
    except OSError:
        return 0, 0, 0
    total = st.f_blocks * st.f_frsize
    free = st.f_bfree * st.f_frsize
    return total, total - free, 1


async def sample_once() -> int:
    """Одна волна проб по всем шардам. Возвращает число точек."""
    db = get_db()
    cursor = await db.execute("SELECT id, path FROM shares")
    rows = await cursor.fetchall()
    if not rows:
        return 0
    now = datetime.now(timezone.utc).isoformat()
    points = 0
    for row in rows:
        total, used, ok = _probe(row["path"])
        await db.execute(
            "INSERT INTO share_samples (share_id, ts, total, used, ok) VALUES (?, ?, ?, ?, ?)",
            (row["id"], now, total, used, ok),
        )
        points += 1
    # политика хранения: точки шардов старше 7 дней не нужны
    cutoff = (datetime.now(timezone.utc) - timedelta(days=RETENTION_DAYS)).isoformat()
    await db.execute("DELETE FROM share_samples WHERE ts < ?", (cutoff,))
    await db.commit()
    return points


async def loop() -> None:
    interval = get_settings().share_interval
    while True:
        try:
            await sample_once()
        except Exception as exc:  # не роняем цикл из-за одной ошибки
            print(f"shares probe error: {exc}", flush=True)
        await asyncio.sleep(interval)