"""Probe OS facts on first connect; cache in-memory so we only run uname once.""" from __future__ import annotations from .models import Host, OSFamily # In-memory cache. For production, persist to DB so facts survive restarts. _FACTS_CACHE: dict[str, dict] = {} def get_cached_facts(host: Host) -> dict | None: return _FACTS_CACHE.get(host.name) def set_cached_facts(host: Host, facts: dict) -> None: _FACTS_CACHE[host.name] = facts async def probe_facts(host: Host, connector) -> dict: """Run a tiny shell snippet to discover OS family + distribution. `connector` is an already-open connector instance for this host. Returns a facts dict and caches it. """ cached = get_cached_facts(host) if cached: return cached facts: dict = {"os_family": host.os_family.value} # If we already know it from inventory, trust it. if host.os_family != OSFamily.UNKNOWN: set_cached_facts(host, facts) return facts # Heuristic: try Linux/Unix first, then Windows. # Note: on Windows hosts with Git-Bash installed, `uname -s` may return # MINGW/MSYS/CYGWIN — those still mean the underlying OS is Windows. try: res = await connector.run("uname -s", timeout=5) if res.exit_code == 0 and res.stdout.strip(): kernel = res.stdout.strip().lower() if any(tok in kernel for tok in ("mingw", "msys", "cygwin")): facts["os_family"] = OSFamily.WINDOWS.value elif "linux" in kernel: facts["os_family"] = OSFamily.LINUX.value elif "darwin" in kernel: facts["os_family"] = OSFamily.DARWIN.value elif "aix" in kernel: facts["os_family"] = OSFamily.AIX.value if facts["os_family"] == OSFamily.UNKNOWN.value: # Try Windows PowerShell as last resort. res = await connector.run( "(Get-CimInstance Win32_OperatingSystem).Caption", timeout=5 ) if res.exit_code == 0 and res.stdout.strip(): facts["os_family"] = OSFamily.WINDOWS.value facts["os_caption"] = res.stdout.strip() except Exception as e: # noqa: BLE001 — probing is best-effort facts["probe_error"] = str(e) set_cached_facts(host, facts) return facts