"""The cutoffs a host's readings are graded by, as Settings → Thresholds sets them: amber past `warn`, red past `crit`.""" from __future__ import annotations from steward.core.settings import DEFAULTS, get_all_settings, set_setting, to_thresholds_cfg from steward.mcp._context import mcp_app from steward.models.users import UserRole from ._audit import audit_mcp from ._shape import db_session # Each graded reading, its unit, and what it measures. READINGS = { "cpu": ("%", "CPU use"), "mem": ("%", "memory used"), "disk": ("%", "fullest mount's space used"), "load": ("%", "1-minute load per core"), "temp": ("°C", "hottest real thermometer"), } def _view(settings: dict) -> dict: cfg = to_thresholds_cfg(settings) return {kind: {"warn": cfg[kind]["warn"], "crit": cfg[kind]["crit"], "unit": unit, "measures": what, "default": {"warn": DEFAULTS[f"thresholds.{kind}_warn"], "crit": DEFAULTS[f"thresholds.{kind}_crit"]}} for kind, (unit, what) in READINGS.items()} async def get_thresholds() -> dict: """The cutoffs host readings are graded by, as Settings → Thresholds sets them: per reading (cpu, mem, disk, load, temp), `warn` (amber past it) and `crit` (red past it) in `unit`, with Steward's default for each. The host pages, the Overview and get_overview grade by these. Alert rules have their own cutoffs (list_alert_rules); these don't fire anything. set_thresholds (admin) changes them. """ async with db_session() as db: return _view(await get_all_settings(db)) async def set_thresholds(cutoffs: dict[str, dict[str, float]]) -> dict: """Change the cutoffs host readings are graded by, as Settings → Thresholds does. Takes effect on the next page load; audited. cutoffs: {reading: {"warn": n, "crit": n}} for any of get_thresholds' readings, e.g. {"temp": {"crit": 90}}. Either of warn and crit may be left out and keeps its value; warn must stay below crit. A reading not named is left as it is. Returns the cutoffs as they now stand. """ unknown = sorted(set(cutoffs or {}) - set(READINGS)) if unknown: raise ValueError(f"No reading named {', '.join(unknown)}; they are {', '.join(READINGS)}.") if not cutoffs: raise ValueError("Nothing to change: pass cutoffs, e.g. {\"temp\": {\"crit\": 90}}.") async with db_session() as db: current = _view(await get_all_settings(db)) changes: dict = {} for kind, given in cutoffs.items(): extra = sorted(set(given or {}) - {"warn", "crit"}) if extra or not given: raise ValueError(f"{kind} takes warn and/or crit, not {', '.join(extra) or 'nothing'}.") new = {} for edge in ("warn", "crit"): if edge not in given: new[edge] = current[kind][edge] continue try: value = float(given[edge]) except (TypeError, ValueError): raise ValueError(f"{kind} {edge} must be a number.") from None if not value > 0: raise ValueError(f"{kind} {edge} must be more than 0.") new[edge] = int(value) if value.is_integer() else value changes[f"thresholds.{kind}_{edge}"] = new[edge] if new["warn"] >= new["crit"]: raise ValueError(f"{kind} warn ({new['warn']}) must be below its crit ({new['crit']}).") app = mcp_app() async with db_session() as db: async with db.begin(): before = await get_all_settings(db) for key, value in changes.items(): await set_setting(db, key, value) fresh = await get_all_settings(db) app.config["THRESHOLDS"] = to_thresholds_cfg(fresh) await audit_mcp("settings.saved", "settings", "thresholds", {"section": "thresholds", "changes": {k: {"from": before.get(k), "to": v} for k, v in changes.items()}}) return _view(fresh) def register(register_tool) -> None: register_tool(get_thresholds, role=UserRole.viewer, read_only=True) register_tool(set_thresholds, role=UserRole.admin, read_only=False)