diff --git a/backend/src/api/packet_scripting_api.py b/backend/src/api/packet_scripting_api.py new file mode 100644 index 0000000..4769fbd --- /dev/null +++ b/backend/src/api/packet_scripting_api.py @@ -0,0 +1,257 @@ +# 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