updated logging
This commit is contained in:
@@ -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,
|
||||
}
|
||||
Reference in New Issue
Block a user