From 1b4688eaccbb54bc7efc48ac2bcd6394cc697bf4 Mon Sep 17 00:00:00 2001 From: malmert Date: Wed, 28 Jan 2026 19:10:00 +0100 Subject: [PATCH] add delete script --- backend/src/api/packet_scripting_api.py | 163 ++++++++++++++++++------ 1 file changed, 121 insertions(+), 42 deletions(-) diff --git a/backend/src/api/packet_scripting_api.py b/backend/src/api/packet_scripting_api.py index ec3f460..f4f5856 100644 --- a/backend/src/api/packet_scripting_api.py +++ b/backend/src/api/packet_scripting_api.py @@ -1,36 +1,31 @@ -# script_router_named.py +# script_router_named_with_delete.py """ APIRouter: upload scripts with a supplied name, optional requirements -> create per-script venv. If venv install fails, response includes pip output and the router deletes the uploaded files and venv. +Added: DELETE /scripts/{name} to disable any enabled services for that script and remove files/venv. + Endpoints: - POST /scripts -> upload script (multipart): script file, optional requirements file, required 'name' form field - GET /scripts -> list scripts - GET /scripts/{name} -> download script - POST /scripts/{name}/enable -> enable systemd service for script on given qnum - POST /scripts/{name}/disable -> disable service for script on qnum + - DELETE /scripts/{name} -> disable all or a specific qnum service(s) and delete script + venv + requirements - GET /scripts/status -> status of all fw-script units - GET /scripts/{name}/status -> status of units for that script - -Notes: - - This relies on systemd and writes units to /etc/systemd/system - - Script files are stored at SCRIPT_DIR/.py - - Venv stored at VENV_BASE/ (if requirements provided) - - 'name' must match regex [A-Za-z0-9_.-]+ (no path separators) """ import os import sys import re -import uuid -import json import shutil import subprocess import time import logging from typing import Optional, List, Dict -from fastapi import APIRouter, UploadFile, File, Form, HTTPException +from fastapi import APIRouter, UploadFile, File, Form, HTTPException, Query from fastapi.responses import FileResponse from pydantic import BaseModel @@ -46,13 +41,12 @@ os.makedirs(VENV_BASE, exist_ok=True) # ---------- Logging ---------- logging.basicConfig(level=logging.INFO, format="%(asctime)s %(levelname)s [%(name)s] %(message)s") -logger = logging.getLogger("script-router-named") +logger = logging.getLogger("script-router-named-delete") # ---------- Router ---------- router = APIRouter(prefix="/scripts", tags=["scripts"]) # ---------- Name validation ---------- -# Accept only safe file-name characters to avoid path traversal: letters, digits, dot, underscore, hyphen _NAME_RE = re.compile(r'^[A-Za-z0-9_.-]+$') def validate_name(name: str) -> None: @@ -60,7 +54,6 @@ def validate_name(name: str) -> None: raise ValueError("name must be provided") if not _NAME_RE.match(name): raise ValueError("invalid name; allowed characters: letters, digits, dot, underscore, hyphen") - # prevent reserved names or dots-only if name in (".", ".."): raise ValueError("invalid name") @@ -105,18 +98,12 @@ def create_venv(name: str, timeout: int = 60) -> str: return venv_dir def pip_install_requirements(name: str, requirements_path: str, timeout: int = 600) -> Dict[str, str]: - """ - Install requirements into the venv for name from requirements_path. - Returns dict with stdout/stderr. Raises RuntimeError on failure including outputs. - """ venv_dir = create_venv(name) pip_path = os.path.join(venv_dir, "bin", "pip") - # ensure pip exists and attempt to upgrade try: subprocess.run([pip_path, "install", "--upgrade", "pip"], check=True, capture_output=True, text=True, timeout=300) except subprocess.CalledProcessError as e: logger.warning("pip upgrade warning for %s: %s", name, getattr(e, "stderr", str(e))) - # run install try: p = subprocess.run([pip_path, "install", "-r", requirements_path, "--no-cache-dir"], check=True, capture_output=True, text=True, timeout=timeout) @@ -168,7 +155,7 @@ WantedBy=multi-user.target logger.warning("Failed to enable %s at boot", service_name) return unit_path -def remove_unit(service_name: str): +def remove_unit(service_name: str) -> None: unit_path = unit_path_for(service_name) try: subprocess.run(["systemctl", "stop", service_name], check=False) @@ -183,11 +170,11 @@ def remove_unit(service_name: str): subprocess.run(["systemctl", "daemon-reload"], check=True) logger.info("Removed unit %s", unit_path) -def start_unit(service_name: str): +def start_unit(service_name: str) -> None: subprocess.run(["systemctl", "start", service_name], check=True) logger.info("Started service %s", service_name) -def stop_unit(service_name: str): +def stop_unit(service_name: str) -> None: subprocess.run(["systemctl", "stop", service_name], check=True) logger.info("Stopped service %s", service_name) @@ -196,7 +183,6 @@ def is_unit_active(service_name: str) -> bool: return p.returncode == 0 def list_fw_units() -> List[str]: - """List our fw-script units (without .service suffix).""" units = [] try: for fn in os.listdir(UNIT_DIR): @@ -206,7 +192,6 @@ def list_fw_units() -> List[str]: logger.warning("Unit dir %s not found", UNIT_DIR) return units -# ExecStart parse pattern: python + /srv/fw-scripts/.py + qnum + optional extra _RE_EXECSTART = re.compile(r'(?P/\S*python\S*)\s+(?P