s
This commit is contained in:
@@ -479,6 +479,13 @@ def load_stack_env() -> dict[str, str]:
|
||||
return values
|
||||
|
||||
|
||||
def _env_flag(env: dict[str, str], key: str, default: bool = True) -> bool:
|
||||
raw = str(env.get(key, "1" if default else "0")).strip().lower()
|
||||
if raw in {"", "default"}:
|
||||
return default
|
||||
return raw not in {"0", "false", "no", "off", "disable", "disabled"}
|
||||
|
||||
|
||||
def _probe_tcp_port(host: str, port: int, timeout: float = 2.0) -> dict[str, Any]:
|
||||
t0 = time.monotonic()
|
||||
try:
|
||||
@@ -524,6 +531,207 @@ def _probe_http_get(url: str, timeout: float = 2.5) -> dict[str, Any]:
|
||||
}
|
||||
|
||||
|
||||
def _resolve_host_ipv4s(host: str) -> list[str]:
|
||||
ips: list[str] = []
|
||||
try:
|
||||
for info in socket.getaddrinfo(host, None, family=socket.AF_INET):
|
||||
ip = str(info[4][0]).strip()
|
||||
if ip and ip not in ips:
|
||||
ips.append(ip)
|
||||
except OSError:
|
||||
return []
|
||||
return ips
|
||||
|
||||
|
||||
def _deployment_file_check(check_id: str, label: str, path: Path, detail: str) -> dict[str, Any]:
|
||||
return {
|
||||
"id": check_id,
|
||||
"label": label,
|
||||
"ok": path.is_file(),
|
||||
"kind": "file",
|
||||
"target": str(path),
|
||||
"detail": detail,
|
||||
}
|
||||
|
||||
|
||||
def _deployment_http_check(
|
||||
check_id: str,
|
||||
label: str,
|
||||
url: str,
|
||||
*,
|
||||
expected_codes: tuple[int, ...] = (200,),
|
||||
timeout: float = 2.5,
|
||||
) -> dict[str, Any]:
|
||||
probe = _probe_http_get(url, timeout=timeout)
|
||||
code = int(probe.get("http_code") or 0)
|
||||
ok = bool(probe.get("reachable")) and code in expected_codes
|
||||
detail = f"HTTP {code}" if code else (probe.get("error") or "unreachable")
|
||||
if probe.get("latency_ms") is not None:
|
||||
detail = f"{detail} · {probe['latency_ms']} ms"
|
||||
return {
|
||||
"id": check_id,
|
||||
"label": label,
|
||||
"ok": ok,
|
||||
"kind": "http",
|
||||
"url": url,
|
||||
"http_code": code,
|
||||
"latency_ms": probe.get("latency_ms"),
|
||||
"reachable": bool(probe.get("reachable")),
|
||||
"detail": detail,
|
||||
"error": str(probe.get("error") or ""),
|
||||
}
|
||||
|
||||
|
||||
def deployment_report() -> dict[str, Any]:
|
||||
env = load_stack_env()
|
||||
summary = stack_summary()
|
||||
mdns_host = str(summary.get("mdns_host") or "live.local")
|
||||
web_port = str(env.get("PORT", os.environ.get("PORT", "8001")) or "8001")
|
||||
lan_ips = list(summary.get("ips") or [])
|
||||
primary_lan_ip = lan_ips[0] if lan_ips else ""
|
||||
live_edge_enabled = _env_flag(env, "ENABLE_LIVE_EDGE_PROXY", True)
|
||||
mdns_enabled = _env_flag(env, "ENABLE_MDNS", True)
|
||||
|
||||
control_assets = BASE_DIR / "web-console" / "out" / "index.html"
|
||||
checks: list[dict[str, Any]] = [
|
||||
_deployment_file_check(
|
||||
"stack_env",
|
||||
"system-stack.env",
|
||||
STACK_ENV,
|
||||
"Live stack environment file",
|
||||
),
|
||||
_deployment_file_check(
|
||||
"console_assets",
|
||||
"web-console/out/index.html",
|
||||
control_assets,
|
||||
"Exported web control panel assets",
|
||||
),
|
||||
_deployment_http_check(
|
||||
"api_loopback",
|
||||
"FastAPI loopback",
|
||||
f"http://127.0.0.1:{web_port}/health",
|
||||
),
|
||||
]
|
||||
|
||||
if primary_lan_ip:
|
||||
checks.append(
|
||||
_deployment_http_check(
|
||||
"api_lan",
|
||||
"FastAPI LAN bind",
|
||||
f"http://{primary_lan_ip}:{web_port}/health",
|
||||
)
|
||||
)
|
||||
|
||||
if live_edge_enabled:
|
||||
checks.append(
|
||||
_deployment_http_check(
|
||||
"edge_loopback",
|
||||
"live-edge loopback",
|
||||
"http://127.0.0.1/health",
|
||||
)
|
||||
)
|
||||
if primary_lan_ip:
|
||||
checks.append(
|
||||
_deployment_http_check(
|
||||
"edge_lan",
|
||||
"live-edge LAN bind",
|
||||
f"http://{primary_lan_ip}/health",
|
||||
)
|
||||
)
|
||||
|
||||
mdns_ips = _resolve_host_ipv4s(mdns_host) if mdns_enabled else []
|
||||
checks.append(
|
||||
{
|
||||
"id": "mdns_resolution",
|
||||
"label": "mDNS resolution",
|
||||
"ok": bool(mdns_ips) if mdns_enabled else True,
|
||||
"kind": "dns",
|
||||
"host": mdns_host,
|
||||
"ips": mdns_ips,
|
||||
"detail": ", ".join(mdns_ips) if mdns_ips else ("disabled" if not mdns_enabled else "unresolved"),
|
||||
}
|
||||
)
|
||||
|
||||
if mdns_enabled and mdns_ips:
|
||||
checks.append(
|
||||
_deployment_http_check(
|
||||
"mdns_api",
|
||||
"live.local direct API",
|
||||
f"http://{mdns_host}:{web_port}/health",
|
||||
)
|
||||
)
|
||||
if live_edge_enabled:
|
||||
checks.append(
|
||||
_deployment_http_check(
|
||||
"mdns_edge",
|
||||
"live.local edge proxy",
|
||||
f"http://{mdns_host}/health",
|
||||
)
|
||||
)
|
||||
|
||||
required_ids = {
|
||||
"stack_env",
|
||||
"console_assets",
|
||||
"api_loopback",
|
||||
}
|
||||
if primary_lan_ip:
|
||||
required_ids.add("api_lan")
|
||||
if live_edge_enabled:
|
||||
required_ids.add("edge_loopback")
|
||||
if primary_lan_ip:
|
||||
required_ids.add("edge_lan")
|
||||
if mdns_enabled:
|
||||
required_ids.add("mdns_resolution")
|
||||
if mdns_ips:
|
||||
required_ids.add("mdns_api")
|
||||
if live_edge_enabled:
|
||||
required_ids.add("mdns_edge")
|
||||
|
||||
pass_count = sum(1 for item in checks if item.get("ok"))
|
||||
total_count = len(checks)
|
||||
failing = [item for item in checks if item["id"] in required_ids and not item.get("ok")]
|
||||
hints: list[str] = []
|
||||
failed_ids = {item["id"] for item in failing}
|
||||
if "api_loopback" in failed_ids:
|
||||
hints.append("live-console service is unhealthy on 127.0.0.1; inspect systemd logs or scripts/launch.py.")
|
||||
if "api_lan" in failed_ids:
|
||||
hints.append("The control plane answers on loopback but not on the LAN IP; check HOST binding and local firewall rules.")
|
||||
if "edge_loopback" in failed_ids or "edge_lan" in failed_ids:
|
||||
hints.append("Port 80 reverse proxy is unhealthy; restart live-edge.service or rerun scripts/linux/reinstall_live_edge.sh.")
|
||||
if "mdns_resolution" in failed_ids:
|
||||
hints.append("live.local does not resolve locally; check avahi-daemon, libnss-mdns, and same-LAN mDNS visibility.")
|
||||
if "mdns_api" in failed_ids and "mdns_resolution" not in failed_ids:
|
||||
hints.append("live.local resolves but :PORT is not reachable through that hostname; verify mDNS points to the correct LAN IP.")
|
||||
if "mdns_edge" in failed_ids and "mdns_resolution" not in failed_ids:
|
||||
hints.append("live.local resolves but port 80 proxy is unhealthy; verify live-edge.service and the upstream PORT value.")
|
||||
if "console_assets" in failed_ids:
|
||||
hints.append("The exported web console is missing; run upgrade-live.sh or rebuild web-console before relying on live.local.")
|
||||
|
||||
urls = {
|
||||
"loopback_api": f"http://127.0.0.1:{web_port}/health",
|
||||
"mdns_control": f"http://{mdns_host}:{web_port}/",
|
||||
"mdns_root": f"http://{mdns_host}/",
|
||||
"lan_control": f"http://{primary_lan_ip}:{web_port}/" if primary_lan_ip else "",
|
||||
"lan_root": f"http://{primary_lan_ip}/" if primary_lan_ip else "",
|
||||
}
|
||||
return {
|
||||
"ready": not failing,
|
||||
"pass_count": pass_count,
|
||||
"total_count": total_count,
|
||||
"required_total": len(required_ids),
|
||||
"hostname": summary.get("hostname"),
|
||||
"mdns_host": mdns_host,
|
||||
"primary_lan_ip": primary_lan_ip,
|
||||
"port": web_port,
|
||||
"live_edge_enabled": live_edge_enabled,
|
||||
"mdns_enabled": mdns_enabled,
|
||||
"checks": checks,
|
||||
"failing_checks": failing,
|
||||
"hints": hints,
|
||||
"urls": urls,
|
||||
}
|
||||
|
||||
|
||||
def _iface_speed_mbps(name: str) -> int | None:
|
||||
path = Path("/sys/class/net") / name / "speed"
|
||||
if not path.is_file():
|
||||
|
||||
Reference in New Issue
Block a user