mirror of
https://github.com/fusion44/blitz_api.git
synced 2026-08-13 11:52:45 +02:00
Make /system/health unauthenticated, compute real readiness from the shared startup state via build_health_info, and return 503 (body still a SystemHealthInfo) when a subsystem is not ready. Drop the now-dead per-backend get_system_health delegation. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
196 lines
7.2 KiB
Python
196 lines
7.2 KiB
Python
import secrets
|
|
from typing import Dict
|
|
|
|
import psutil
|
|
from fastapi import HTTPException, status
|
|
from loguru import logger
|
|
|
|
from app.api.config import config
|
|
from app.api.constants import API_VERSION
|
|
from app.api.error_report.report import Report
|
|
from app.auth.auth_handler import sign_jwt
|
|
from app.external.result_type.src.result.result import Err, Ok, Result
|
|
from app.lightning.service import get_ln_info
|
|
from app.system.impl.system_base import SystemBase
|
|
from app.system.models import (
|
|
APIPlatform,
|
|
ConnectionInfo,
|
|
LoginInput,
|
|
RawDebugLogData,
|
|
SystemInfo,
|
|
)
|
|
|
|
_SLEEP_TIME = config("BAPI_GATHER_HW_INFO_INTERVAL", default=2, cast=float)
|
|
_CPU_AVG_PERIOD = config("BAPI_CPU_USAGE_AVERAGING_PERIOD", default=0.5, cast=float)
|
|
_HW_INFO_YIELD_TIME = _SLEEP_TIME + _CPU_AVG_PERIOD
|
|
|
|
|
|
class NativePythonSystem(SystemBase):
|
|
@logger.catch(exclude=(HTTPException,))
|
|
async def get_system_info(self) -> SystemInfo:
|
|
lninfo = await get_ln_info()
|
|
|
|
version = config("BAPI_NP_VERSION", default="")
|
|
|
|
tor_api = config("BAPI_NP_TOR_ADDRESS_API_ENDPOINT", default="")
|
|
tor_api_docs = config("BAPI_NP_TOR_ADDRESS_API_DOCS", default="")
|
|
|
|
lan_api = config("BAPI_NP_LOCAL_ADDRESS_API_ENDPOINT", default="")
|
|
lan_api_docs = config("BAPI_NP_LOCAL_ADDRESS_API_DOCS", default="")
|
|
|
|
ssh_address = config("BAPI_NP_SSH_ADDRESS", default="")
|
|
|
|
return SystemInfo(
|
|
alias=lninfo.alias,
|
|
color=lninfo.color,
|
|
platform=APIPlatform.NATIVE_PYTHON,
|
|
platform_version=version,
|
|
api_version=API_VERSION,
|
|
tor_web_ui=tor_api_docs,
|
|
tor_api=tor_api,
|
|
lan_web_ui=lan_api_docs,
|
|
lan_api=lan_api,
|
|
ssh_address=ssh_address,
|
|
chain=lninfo.chains[0].network,
|
|
)
|
|
|
|
@logger.catch(exclude=(HTTPException,))
|
|
async def shutdown(self, reboot: bool) -> bool:
|
|
logger.info("Shutdown / reboot not supported in native_python mode.")
|
|
return False
|
|
|
|
@logger.catch(exclude=(HTTPException,))
|
|
async def get_connection_info(self) -> Result[ConnectionInfo, Report]:
|
|
# return an empty connection info object for now
|
|
return Ok(ConnectionInfo())
|
|
|
|
@logger.catch(exclude=(HTTPException,), reraise=True)
|
|
async def login(self, i: LoginInput) -> Result[Dict[str, str], Report]:
|
|
# https://github.com/fusion44/blitz_api/issues/255
|
|
pw = config("BAPI_NATIVE_LOGIN_PASSWORD", cast=str)
|
|
if not isinstance(pw, str):
|
|
return Err(
|
|
Report(
|
|
"unable to convert the .env password to a string",
|
|
error=HTTPException(
|
|
status.HTTP_500_INTERNAL_SERVER_ERROR,
|
|
detail="Password is incorrect",
|
|
),
|
|
)
|
|
)
|
|
|
|
if len(pw) < 8:
|
|
return Err(
|
|
Report(
|
|
"given password is less than 8 characters",
|
|
error=HTTPException(
|
|
status.HTTP_401_UNAUTHORIZED,
|
|
detail="given password is less than 8 characters",
|
|
),
|
|
)
|
|
)
|
|
|
|
matches = secrets.compare_digest(i.password, pw)
|
|
if matches:
|
|
return Ok(sign_jwt())
|
|
|
|
return Err(
|
|
Report(
|
|
"password is incorrect",
|
|
error=HTTPException(
|
|
status.HTTP_401_UNAUTHORIZED, detail="password is incorrect"
|
|
),
|
|
)
|
|
)
|
|
|
|
async def change_password(self, type: str, old_password: str, new_password: str):
|
|
# no @logger.catch: NotImplementedError must propagate to the service
|
|
# layer (which turns it into a 501), not be swallowed into a None return
|
|
raise NotImplementedError()
|
|
|
|
async def get_debug_logs_raw(self) -> RawDebugLogData:
|
|
raise NotImplementedError()
|
|
|
|
@logger.catch(exclude=(HTTPException,))
|
|
async def get_hardware_info(self) -> map:
|
|
info = {}
|
|
|
|
try:
|
|
info["cpu_overall_percent"] = psutil.cpu_percent(interval=_CPU_AVG_PERIOD)
|
|
info["cpu_per_cpu_percent"] = psutil.cpu_percent(
|
|
interval=_CPU_AVG_PERIOD, percpu=True
|
|
)
|
|
|
|
v = psutil.virtual_memory()
|
|
info["vram_total_bytes"] = v.total
|
|
info["vram_available_bytes"] = v.available
|
|
info["vram_used_bytes"] = v.used
|
|
info["vram_usage_percent"] = v.percent
|
|
|
|
s = psutil.swap_memory()
|
|
info["swap_ram_total_bytes"] = s.total
|
|
info["swap_used_bytes"] = s.used
|
|
info["swap_usage_bytes"] = s.percent
|
|
|
|
info["temperatures_celsius"] = psutil.sensors_temperatures()
|
|
info["boot_time_timestamp"] = psutil.boot_time()
|
|
|
|
disk_io = psutil.disk_io_counters()
|
|
info["disk_io_read_count"] = disk_io.read_count
|
|
info["disk_io_write_count"] = disk_io.write_count
|
|
info["disk_io_read_bytes"] = disk_io.read_bytes
|
|
info["disk_io_write_bytes"] = disk_io.write_bytes
|
|
|
|
disks = []
|
|
partitions = psutil.disk_partitions()
|
|
for partition in partitions:
|
|
p = {}
|
|
p["device"] = partition.device
|
|
p["mountpoint"] = partition.mountpoint
|
|
p["filesystem_type"] = partition.fstype
|
|
|
|
try:
|
|
usage = psutil.disk_usage(partition.mountpoint)
|
|
p["partition_total_bytes"] = usage.total
|
|
p["partition_used_bytes"] = usage.used
|
|
p["partition_free_bytes"] = usage.free
|
|
p["partition_percent"] = usage.percent
|
|
except PermissionError:
|
|
continue
|
|
disks.append(p)
|
|
info["disks"] = disks
|
|
|
|
nets = []
|
|
addresses = psutil.net_if_addrs()
|
|
for name, address in addresses.items():
|
|
net = {}
|
|
nets.append(net)
|
|
net["interface_name"] = name
|
|
for a in address:
|
|
if str(a.family) == "AddressFamily.AF_INET":
|
|
net["address"] = a.address
|
|
elif str(a.family) == "AddressFamily.AF_PACKET":
|
|
net["mac_address"] = a.address
|
|
|
|
net_io = psutil.net_io_counters()
|
|
info["networks"] = nets
|
|
info["networks_bytes_sent"] = net_io.bytes_sent
|
|
info["networks_bytes_received"] = net_io.bytes_recv
|
|
|
|
except FileNotFoundError:
|
|
logger.warning("Unable to access /proc/stat to get CPU stats")
|
|
except OSError as e:
|
|
logger.warning(
|
|
f"""Unable to query system: {e}
|
|
Check if the system is hardened against such calls.
|
|
For example in Nix you must not harden the following:
|
|
ProtectProc = "invisible"; // to get HW info
|
|
ProcSubset = "pid"; // to get HW info
|
|
RestrictAddressFamilies = "AF_UNIX AF_INET AF_INET6"; // to get network info
|
|
"""
|
|
)
|
|
|
|
return info
|
|
|
|
def get_hardware_info_yield_time(self) -> float:
|
|
return _HW_INFO_YIELD_TIME
|