updated logging

This commit is contained in:
2026-04-02 13:15:19 -05:00
parent 38733daffc
commit 150666d7c3
3 changed files with 134 additions and 137 deletions

View File

@@ -8,10 +8,9 @@ import os
import socket
import subprocess
import urllib.request
from typing import AsyncIterator
from fastapi import FastAPI, HTTPException, Response
from fastapi.responses import HTMLResponse, StreamingResponse
from fastapi import FastAPI, HTTPException
from fastapi.responses import HTMLResponse
from fastapi.staticfiles import StaticFiles
from fastapi.templating import Jinja2Templates
from fastapi.requests import Request
@@ -26,10 +25,9 @@ FLAKE_INPUT_NAME = "Sovran_Systems"
GITEA_API_BASE = "https://git.sovransystems.com/api/v1/repos/Sovran_Systems/Sovran_SystemsOS/commits"
UPDATE_UNIT = "sovran-hub-update.service"
UPDATE_LOG = "/var/log/sovran-hub-update.log"
REBOOT_COMMAND = [
"reboot",
]
REBOOT_COMMAND = ["reboot"]
CATEGORY_ORDER = [
("infrastructure", "Infrastructure"),
@@ -58,7 +56,6 @@ app.mount(
name="static",
)
# Also serve icons from the app/icons directory (set via env or adjacent folder)
_ICONS_DIR = os.environ.get(
"SOVRAN_HUB_ICONS",
os.path.join(os.path.dirname(_BASE_DIR), "icons"),
@@ -141,7 +138,6 @@ def _get_internal_ip() -> str:
def _get_external_ip() -> str:
# Max length 46 covers the longest valid IPv6 address (45 chars) plus a newline
MAX_IP_LENGTH = 46
for url in [
"https://api.ipify.org",
@@ -184,44 +180,21 @@ def _update_result() -> str:
return "unknown"
def _get_update_invocation_id() -> str:
"""Get the current InvocationID of the update unit."""
r = subprocess.run(
["systemctl", "show", "-p", "InvocationID", "--value", UPDATE_UNIT],
capture_output=True, text=True,
)
return r.stdout.strip()
def _read_journal_logs(since_cursor: str = "") -> tuple[list[str], str]:
"""
Read journal logs for the update unit.
Returns (lines, last_cursor).
Uses cursors so we never miss lines even if the server restarts.
"""
cmd = [
"journalctl", "-u", UPDATE_UNIT,
"--no-pager", "-o", "cat",
"--show-cursor",
]
if since_cursor:
cmd += ["--after-cursor", since_cursor]
else:
# Only get logs from the most recent invocation
cmd += ["-n", "10000"]
r = subprocess.run(cmd, capture_output=True, text=True, timeout=10)
output = r.stdout
lines = []
cursor = since_cursor
for raw_line in output.split("\n"):
if raw_line.startswith("-- cursor: "):
cursor = raw_line[len("-- cursor: "):]
elif raw_line:
lines.append(raw_line)
return lines, cursor
def _read_log(offset: int = 0) -> tuple[str, int]:
"""Read the update log file from the given byte offset.
Returns (new_text, new_offset)."""
try:
with open(UPDATE_LOG, "rb") as f:
f.seek(0, 2) # seek to end
size = f.tell()
if offset > size:
# Log was truncated (new run), start over
offset = 0
f.seek(offset)
chunk = f.read()
return chunk.decode(errors="replace"), offset + len(chunk)
except FileNotFoundError:
return "", 0
# ── Routes ───────────────────────────────────────────────────────
@@ -275,7 +248,6 @@ async def api_services():
def _get_allowed_units() -> set[str]:
"""Return the set of unit names from the current config (whitelist)."""
cfg = load_config()
return {s.get("unit", "") for s in cfg.get("services", []) if s.get("unit")}
@@ -356,19 +328,19 @@ async def api_updates_run():
"""Kick off the detached update systemd unit."""
loop = asyncio.get_event_loop()
# Check if already running
running = await loop.run_in_executor(None, _update_is_active)
if running:
return {"ok": True, "status": "already_running"}
# Reset the failed state (if any) and start the unit
# Reset failed state if any
await asyncio.create_subprocess_exec(
"systemctl", "reset-failed", UPDATE_UNIT,
stdout=asyncio.subprocess.DEVNULL,
stderr=asyncio.subprocess.DEVNULL,
)
proc = await asyncio.create_subprocess_exec(
"systemctl", "start", UPDATE_UNIT,
"systemctl", "start", "--no-block", UPDATE_UNIT,
stdout=asyncio.subprocess.DEVNULL,
stderr=asyncio.subprocess.DEVNULL,
)
@@ -378,19 +350,17 @@ async def api_updates_run():
@app.get("/api/updates/status")
async def api_updates_status(cursor: str = ""):
"""Poll endpoint: returns running state, result, and new journal lines."""
async def api_updates_status(offset: int = 0):
"""Poll endpoint: returns running state, result, and new log content."""
loop = asyncio.get_event_loop()
running = await loop.run_in_executor(None, _update_is_active)
result = await loop.run_in_executor(None, _update_result)
lines, new_cursor = await loop.run_in_executor(
None, _read_journal_logs, cursor,
)
new_log, new_offset = await loop.run_in_executor(None, _read_log, offset)
return {
"running": running,
"result": result,
"lines": lines,
"cursor": new_cursor,
"log": new_log,
"offset": new_offset,
}