# script_router_stateless.py """ Stateless APIRouter that manages uploaded scripts via systemd only. - Upload/list/download scripts (stored in SCRIPT_DIR) - Enable script on qnum => write systemd unit fw-script--q.service and start it - Disable script on qnum => stop service and remove unit file - Status endpoints discover active services by scanning /etc/systemd/system for fw-script-*.service No DB or run-dir used — systemd is the source of truth. """ import os import uuid import subprocess import time import re import logging from typing import List, Dict, Optional from fastapi import APIRouter, UploadFile, File, HTTPException from fastapi.responses import FileResponse from pydantic import BaseModel # ---------- config ---------- SCRIPT_DIR = "/srv/fw-scripts" UNIT_DIR = "/etc/systemd/system" UNIT_PREFIX = "fw-script" # unit names like fw-script--q.service os.makedirs(SCRIPT_DIR, exist_ok=True) # ---------- logging ---------- logging.basicConfig(level=logging.INFO, format="%(asctime)s %(levelname)s [%(name)s] %(message)s") logger = logging.getLogger("script-router-stateless") # ---------- router ---------- router = APIRouter(prefix="/scripts", tags=["scripts"]) # ---------- models ---------- class ScriptInfo(BaseModel): id: str name: str path: str class EnableRequest(BaseModel): qnum: int service_name: Optional[str] = None extra_args: Optional[str] = None # ---------- helpers ---------- def make_service_name(sid: str, qnum: int) -> str: # keep name safe for systemd return f"{UNIT_PREFIX}-{sid}-q{qnum}" def unit_path_for(service_name: str) -> str: return os.path.join(UNIT_DIR, service_name + ".service") def write_unit(service_name: str, exec_start: str, description: str = "") -> str: """Write unit file and daemon-reload systemd.""" unit_path = unit_path_for(service_name) unit_text = f"""[Unit] Description={description} After=network.target [Service] Type=simple ExecStart={exec_start} Restart=always RestartSec=2 StandardOutput=syslog StandardError=syslog [Install] WantedBy=multi-user.target """ with open(unit_path, "w") as fh: fh.write(unit_text) # reload systemd to pick up new unit subprocess.run(["systemctl", "daemon-reload"], check=True) logger.info("Wrote unit %s", unit_path) return unit_path def remove_unit(service_name: str): """Stop and remove a unit file, then daemon-reload.""" try: subprocess.run(["systemctl", "stop", service_name], check=False) except Exception: logger.exception("Failed to stop %s", service_name) unit_path = unit_path_for(service_name) if os.path.exists(unit_path): os.remove(unit_path) subprocess.run(["systemctl", "daemon-reload"], check=True) logger.info("Removed unit %s", unit_path) def start_unit(service_name: str): subprocess.run(["systemctl", "start", service_name], check=True) logger.info("Started service %s", service_name) def is_unit_active(service_name: str) -> bool: p = subprocess.run(["systemctl", "is-active", "--quiet", service_name]) return p.returncode == 0 def list_fw_units() -> List[str]: """Return list of fw unit names without .service suffix.""" units = [] # scan UNIT_DIR for files matching UNIT_PREFIX-*.service try: for fn in os.listdir(UNIT_DIR): if fn.startswith(UNIT_PREFIX + "-") and fn.endswith(".service"): units.append(fn[:-8]) # strip .service except FileNotFoundError: logger.warning("Unit dir %s not found", UNIT_DIR) return units # ExecStart parser: we expect ExecStart like "/usr/bin/python3 /srv/fw-scripts/.py [extra]" _RE_EXECSTART = re.compile(r'(?P/\S*python\S*)\s+(?P