test new file guard and auto deploy
This commit is contained in:
23
backend/example_scripts/README.md
Normal file
23
backend/example_scripts/README.md
Normal file
@@ -0,0 +1,23 @@
|
|||||||
|
# Example NFQUEUE Scripts
|
||||||
|
|
||||||
|
Files in this folder are treated as protected example scripts by the API:
|
||||||
|
|
||||||
|
- `*.py`: script source
|
||||||
|
- `*-requirements.txt`: optional pip requirements copied and installed into the script venv
|
||||||
|
- `*.deploy.json`: optional deployment settings for startup auto-deploy
|
||||||
|
|
||||||
|
Protected behavior:
|
||||||
|
|
||||||
|
- scripts are synced from this folder into `/srv/fw-scripts` on backend startup
|
||||||
|
- scripts in this folder cannot be overwritten, disabled, or deleted via the API
|
||||||
|
- scripts with a deploy config containing `qnum` are auto-started as systemd services
|
||||||
|
|
||||||
|
Example deploy file:
|
||||||
|
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"qnum": 1,
|
||||||
|
"enable_at_boot": true,
|
||||||
|
"extra_args": "--log-level INFO"
|
||||||
|
}
|
||||||
|
```
|
||||||
1
backend/example_scripts/hello_nfqueue-requirements.txt
Normal file
1
backend/example_scripts/hello_nfqueue-requirements.txt
Normal file
@@ -0,0 +1 @@
|
|||||||
|
netfilterqueue
|
||||||
4
backend/example_scripts/hello_nfqueue.deploy.json
Normal file
4
backend/example_scripts/hello_nfqueue.deploy.json
Normal file
@@ -0,0 +1,4 @@
|
|||||||
|
{
|
||||||
|
"qnum": 1,
|
||||||
|
"enable_at_boot": true
|
||||||
|
}
|
||||||
68
backend/example_scripts/hello_nfqueue.py
Normal file
68
backend/example_scripts/hello_nfqueue.py
Normal file
@@ -0,0 +1,68 @@
|
|||||||
|
#!/usr/bin/env python3
|
||||||
|
"""Minimal NFQUEUE example script.
|
||||||
|
|
||||||
|
Expected argv:
|
||||||
|
argv[1] = queue number
|
||||||
|
Any extra args are optional.
|
||||||
|
"""
|
||||||
|
|
||||||
|
import logging
|
||||||
|
import signal
|
||||||
|
import sys
|
||||||
|
from typing import Optional
|
||||||
|
|
||||||
|
try:
|
||||||
|
from netfilterqueue import NetfilterQueue
|
||||||
|
except Exception as exc: # pragma: no cover
|
||||||
|
print(f"Failed to import netfilterqueue: {exc}", file=sys.stderr)
|
||||||
|
sys.exit(2)
|
||||||
|
|
||||||
|
logging.basicConfig(level=logging.INFO, format="%(asctime)s %(levelname)s %(message)s")
|
||||||
|
logger = logging.getLogger("hello-nfqueue")
|
||||||
|
|
||||||
|
_running = True
|
||||||
|
|
||||||
|
|
||||||
|
def _stop(_sig: int, _frame: Optional[object]) -> None:
|
||||||
|
global _running
|
||||||
|
_running = False
|
||||||
|
|
||||||
|
|
||||||
|
def _handle_packet(packet) -> None:
|
||||||
|
# This demo accepts all packets and logs basic metadata.
|
||||||
|
logger.info("packet id=%s len=%s", packet.get_id(), len(packet.get_payload()))
|
||||||
|
packet.accept()
|
||||||
|
|
||||||
|
|
||||||
|
def main() -> int:
|
||||||
|
if len(sys.argv) < 2:
|
||||||
|
print("Usage: hello_nfqueue.py <qnum> [extra args...]", file=sys.stderr)
|
||||||
|
return 1
|
||||||
|
|
||||||
|
try:
|
||||||
|
qnum = int(sys.argv[1])
|
||||||
|
except ValueError:
|
||||||
|
print("qnum must be an integer", file=sys.stderr)
|
||||||
|
return 1
|
||||||
|
|
||||||
|
signal.signal(signal.SIGINT, _stop)
|
||||||
|
signal.signal(signal.SIGTERM, _stop)
|
||||||
|
|
||||||
|
nfq = NetfilterQueue()
|
||||||
|
logger.info("Binding to NFQUEUE %d", qnum)
|
||||||
|
nfq.bind(qnum, _handle_packet)
|
||||||
|
|
||||||
|
try:
|
||||||
|
while _running:
|
||||||
|
nfq.run(block=True)
|
||||||
|
except KeyboardInterrupt:
|
||||||
|
pass
|
||||||
|
finally:
|
||||||
|
logger.info("Unbinding NFQUEUE %d", qnum)
|
||||||
|
nfq.unbind()
|
||||||
|
|
||||||
|
return 0
|
||||||
|
|
||||||
|
|
||||||
|
if __name__ == "__main__":
|
||||||
|
raise SystemExit(main())
|
||||||
@@ -15,6 +15,7 @@ Endpoints:
|
|||||||
import os
|
import os
|
||||||
import sys
|
import sys
|
||||||
import re
|
import re
|
||||||
|
import json
|
||||||
import shutil
|
import shutil
|
||||||
import subprocess
|
import subprocess
|
||||||
import time
|
import time
|
||||||
@@ -28,12 +29,15 @@ from pydantic import BaseModel, Field
|
|||||||
|
|
||||||
# ---------- Configuration ----------
|
# ---------- Configuration ----------
|
||||||
SCRIPT_DIR = "/srv/fw-scripts"
|
SCRIPT_DIR = "/srv/fw-scripts"
|
||||||
|
REPO_BACKEND_DIR = os.path.abspath(os.path.join(os.path.dirname(__file__), "..", ".."))
|
||||||
|
EXAMPLE_SCRIPT_DIR = os.path.join(REPO_BACKEND_DIR, "example_scripts")
|
||||||
VENV_BASE = "/srv/fw-scripts/venvs"
|
VENV_BASE = "/srv/fw-scripts/venvs"
|
||||||
UNIT_DIR = "/etc/systemd/system" # retained for writing new units, but discovery uses systemctl
|
UNIT_DIR = "/etc/systemd/system" # retained for writing new units, but discovery uses systemctl
|
||||||
UNIT_PREFIX = "fw-script"
|
UNIT_PREFIX = "fw-script"
|
||||||
|
|
||||||
# ensure dirs exist
|
# ensure dirs exist
|
||||||
os.makedirs(SCRIPT_DIR, exist_ok=True)
|
os.makedirs(SCRIPT_DIR, exist_ok=True)
|
||||||
|
os.makedirs(EXAMPLE_SCRIPT_DIR, exist_ok=True)
|
||||||
os.makedirs(VENV_BASE, exist_ok=True)
|
os.makedirs(VENV_BASE, exist_ok=True)
|
||||||
|
|
||||||
# ---------- Logging ----------
|
# ---------- Logging ----------
|
||||||
@@ -65,6 +69,18 @@ def requirements_path_for(name: str) -> str:
|
|||||||
return os.path.join(SCRIPT_DIR, f"{name}-requirements.txt")
|
return os.path.join(SCRIPT_DIR, f"{name}-requirements.txt")
|
||||||
|
|
||||||
|
|
||||||
|
def example_script_path_for(name: str) -> str:
|
||||||
|
return os.path.join(EXAMPLE_SCRIPT_DIR, f"{name}.py")
|
||||||
|
|
||||||
|
|
||||||
|
def example_requirements_path_for(name: str) -> str:
|
||||||
|
return os.path.join(EXAMPLE_SCRIPT_DIR, f"{name}-requirements.txt")
|
||||||
|
|
||||||
|
|
||||||
|
def example_deploy_config_path_for(name: str) -> str:
|
||||||
|
return os.path.join(EXAMPLE_SCRIPT_DIR, f"{name}.deploy.json")
|
||||||
|
|
||||||
|
|
||||||
def venv_path_for(name: str) -> str:
|
def venv_path_for(name: str) -> str:
|
||||||
return os.path.join(VENV_BASE, name)
|
return os.path.join(VENV_BASE, name)
|
||||||
|
|
||||||
@@ -86,6 +102,122 @@ def unit_path_for_name(service_name: str) -> str:
|
|||||||
return os.path.join(UNIT_DIR, service_name + ".service")
|
return os.path.join(UNIT_DIR, service_name + ".service")
|
||||||
|
|
||||||
|
|
||||||
|
def is_example_script(name: str) -> bool:
|
||||||
|
return os.path.exists(example_script_path_for(name))
|
||||||
|
|
||||||
|
|
||||||
|
def assert_not_example_script(name: str, action: str) -> None:
|
||||||
|
if is_example_script(name):
|
||||||
|
raise HTTPException(status_code=403, detail=f"example script '{name}' is protected and cannot be {action} via API")
|
||||||
|
|
||||||
|
|
||||||
|
def _read_example_deploy_config(name: str) -> Dict[str, Any]:
|
||||||
|
cfg_path = example_deploy_config_path_for(name)
|
||||||
|
if not os.path.exists(cfg_path):
|
||||||
|
return {}
|
||||||
|
try:
|
||||||
|
with open(cfg_path, "r", encoding="utf-8") as fh:
|
||||||
|
data = json.load(fh)
|
||||||
|
if not isinstance(data, dict):
|
||||||
|
logger.warning("Deploy config for %s is not an object: %s", name, cfg_path)
|
||||||
|
return {}
|
||||||
|
return data
|
||||||
|
except Exception:
|
||||||
|
logger.exception("Failed reading deploy config for %s at %s", name, cfg_path)
|
||||||
|
return {}
|
||||||
|
|
||||||
|
|
||||||
|
def deploy_example_scripts_on_startup() -> None:
|
||||||
|
"""
|
||||||
|
Sync example scripts from EXAMPLE_SCRIPT_DIR into SCRIPT_DIR and auto-deploy units
|
||||||
|
if an optional '<name>.deploy.json' contains a queue number.
|
||||||
|
|
||||||
|
Optional config schema per script:
|
||||||
|
{
|
||||||
|
"qnum": 42,
|
||||||
|
"extra_args": "--flag value",
|
||||||
|
"enable_at_boot": true,
|
||||||
|
"service_name": "fw-script-<name>-q<qnum>"
|
||||||
|
}
|
||||||
|
service_name is optional and must start with 'fw-script-'.
|
||||||
|
"""
|
||||||
|
if not os.path.isdir(EXAMPLE_SCRIPT_DIR):
|
||||||
|
return
|
||||||
|
|
||||||
|
for fn in os.listdir(EXAMPLE_SCRIPT_DIR):
|
||||||
|
if not fn.endswith(".py"):
|
||||||
|
continue
|
||||||
|
|
||||||
|
name = fn.rsplit(".", 1)[0]
|
||||||
|
src_script = os.path.join(EXAMPLE_SCRIPT_DIR, fn)
|
||||||
|
dst_script = script_path_for(name)
|
||||||
|
|
||||||
|
try:
|
||||||
|
shutil.copy2(src_script, dst_script)
|
||||||
|
os.chmod(dst_script, 0o700)
|
||||||
|
logger.info("Synced example script %s -> %s", src_script, dst_script)
|
||||||
|
except Exception:
|
||||||
|
logger.exception("Failed syncing example script %s", src_script)
|
||||||
|
continue
|
||||||
|
|
||||||
|
src_req = example_requirements_path_for(name)
|
||||||
|
dst_req = requirements_path_for(name)
|
||||||
|
if os.path.exists(src_req):
|
||||||
|
try:
|
||||||
|
shutil.copy2(src_req, dst_req)
|
||||||
|
logger.info("Synced example requirements %s -> %s", src_req, dst_req)
|
||||||
|
pip_install_requirements(name, dst_req)
|
||||||
|
except Exception:
|
||||||
|
logger.exception("Failed syncing/installing requirements for example %s", name)
|
||||||
|
|
||||||
|
cfg = _read_example_deploy_config(name)
|
||||||
|
qnum_raw = cfg.get("qnum")
|
||||||
|
if qnum_raw is None:
|
||||||
|
continue
|
||||||
|
|
||||||
|
try:
|
||||||
|
qnum = int(qnum_raw)
|
||||||
|
except (TypeError, ValueError):
|
||||||
|
logger.warning("Invalid qnum in deploy config for %s: %r", name, qnum_raw)
|
||||||
|
continue
|
||||||
|
|
||||||
|
if qnum < 0 or qnum > 65535:
|
||||||
|
logger.warning("Out-of-range qnum in deploy config for %s: %d", name, qnum)
|
||||||
|
continue
|
||||||
|
|
||||||
|
default_service_name = make_service_name(name, qnum)
|
||||||
|
configured_service_name = cfg.get("service_name")
|
||||||
|
if configured_service_name:
|
||||||
|
if str(configured_service_name).startswith(UNIT_PREFIX + "-"):
|
||||||
|
service_name = str(configured_service_name)
|
||||||
|
else:
|
||||||
|
logger.warning(
|
||||||
|
"Ignoring invalid service_name for example %s: %r (must start with '%s-'). Falling back to %s",
|
||||||
|
name,
|
||||||
|
configured_service_name,
|
||||||
|
UNIT_PREFIX,
|
||||||
|
default_service_name,
|
||||||
|
)
|
||||||
|
service_name = default_service_name
|
||||||
|
else:
|
||||||
|
service_name = default_service_name
|
||||||
|
extra_args = cfg.get("extra_args") or ""
|
||||||
|
enable_at_boot = bool(cfg.get("enable_at_boot", True))
|
||||||
|
|
||||||
|
python_path = venv_python_for(name)
|
||||||
|
exec_start = f"{python_path} {dst_script} {qnum}"
|
||||||
|
if extra_args:
|
||||||
|
exec_start += " " + str(extra_args)
|
||||||
|
|
||||||
|
try:
|
||||||
|
write_unit(service_name, exec_start, description=f"FW example script {name} queue {qnum}", enable_at_boot=enable_at_boot)
|
||||||
|
time.sleep(0.05)
|
||||||
|
start_unit(service_name)
|
||||||
|
logger.info("Auto-deployed example script %s as %s", name, service_name)
|
||||||
|
except Exception:
|
||||||
|
logger.exception("Failed auto-deploying example script %s", name)
|
||||||
|
|
||||||
|
|
||||||
# ---------- systemd interaction (systemctl-based, no fallback) ----------
|
# ---------- systemd interaction (systemctl-based, no fallback) ----------
|
||||||
def _systemctl_unit_name(unit: str) -> str:
|
def _systemctl_unit_name(unit: str) -> str:
|
||||||
"""Return unit with .service suffix if missing."""
|
"""Return unit with .service suffix if missing."""
|
||||||
@@ -377,9 +509,13 @@ class ScriptWithStatus(ScriptInfo):
|
|||||||
Represents a script plus discovered unit mappings (if any).
|
Represents a script plus discovered unit mappings (if any).
|
||||||
- mappings: list of UnitMapping for that script
|
- mappings: list of UnitMapping for that script
|
||||||
- requirements_exists: whether a requirements file exists on disk
|
- requirements_exists: whether a requirements file exists on disk
|
||||||
|
- is_protected_example: script originates from repository example folder and is immutable via API
|
||||||
|
- requirements_is_protected_example: requirements file originates from repository example folder and is immutable via API
|
||||||
"""
|
"""
|
||||||
mappings: List[UnitMapping] = Field(default_factory=list)
|
mappings: List[UnitMapping] = Field(default_factory=list)
|
||||||
requirements_exists: bool = False
|
requirements_exists: bool = False
|
||||||
|
is_protected_example: bool = False
|
||||||
|
requirements_is_protected_example: bool = False
|
||||||
|
|
||||||
|
|
||||||
class ScriptUploadResponse(ScriptInfo):
|
class ScriptUploadResponse(ScriptInfo):
|
||||||
@@ -447,6 +583,8 @@ def list_scripts_with_status() -> List[ScriptWithStatus]:
|
|||||||
- name, path
|
- name, path
|
||||||
- mappings: list of UnitMapping (service, parsed ExecStart, active flag)
|
- mappings: list of UnitMapping (service, parsed ExecStart, active flag)
|
||||||
- requirements_exists: boolean
|
- requirements_exists: boolean
|
||||||
|
- is_protected_example: boolean
|
||||||
|
- requirements_is_protected_example: boolean
|
||||||
"""
|
"""
|
||||||
out: List[ScriptWithStatus] = []
|
out: List[ScriptWithStatus] = []
|
||||||
|
|
||||||
@@ -469,7 +607,16 @@ def list_scripts_with_status() -> List[ScriptWithStatus]:
|
|||||||
mappings.append(UnitMapping(service=svc, parsed=parsed, active=active))
|
mappings.append(UnitMapping(service=svc, parsed=parsed, active=active))
|
||||||
|
|
||||||
req_exists = os.path.exists(requirements_path_for(name))
|
req_exists = os.path.exists(requirements_path_for(name))
|
||||||
out.append(ScriptWithStatus(name=name, path=spath, mappings=mappings, requirements_exists=req_exists))
|
out.append(
|
||||||
|
ScriptWithStatus(
|
||||||
|
name=name,
|
||||||
|
path=spath,
|
||||||
|
mappings=mappings,
|
||||||
|
requirements_exists=req_exists,
|
||||||
|
is_protected_example=is_example_script(name),
|
||||||
|
requirements_is_protected_example=os.path.exists(example_requirements_path_for(name)),
|
||||||
|
)
|
||||||
|
)
|
||||||
|
|
||||||
logger.debug("Listed %d scripts with status", len(out))
|
logger.debug("Listed %d scripts with status", len(out))
|
||||||
return out
|
return out
|
||||||
@@ -494,6 +641,8 @@ async def upload_script(
|
|||||||
logger.warning("Invalid name provided: %s", name)
|
logger.warning("Invalid name provided: %s", name)
|
||||||
raise HTTPException(status_code=400, detail=str(e))
|
raise HTTPException(status_code=400, detail=str(e))
|
||||||
|
|
||||||
|
assert_not_example_script(name, "overwritten")
|
||||||
|
|
||||||
spath = script_path_for(name)
|
spath = script_path_for(name)
|
||||||
if os.path.exists(spath):
|
if os.path.exists(spath):
|
||||||
logger.warning("Upload rejected: script with name already exists: %s", name)
|
logger.warning("Upload rejected: script with name already exists: %s", name)
|
||||||
@@ -566,7 +715,7 @@ async def upload_script(
|
|||||||
pass
|
pass
|
||||||
raise HTTPException(status_code=500, detail="internal error during upload")
|
raise HTTPException(status_code=500, detail="internal error during upload")
|
||||||
|
|
||||||
resp = {"name": name, "path": spath}
|
resp: Dict[str, Any] = {"name": name, "path": spath}
|
||||||
if pip_output is not None:
|
if pip_output is not None:
|
||||||
resp["pip"] = pip_output
|
resp["pip"] = pip_output
|
||||||
return ScriptUploadResponse(**resp)
|
return ScriptUploadResponse(**resp)
|
||||||
@@ -579,6 +728,10 @@ async def upload_script(
|
|||||||
200: {
|
200: {
|
||||||
"content": {"application/octet-stream": {"schema": {"type": "string", "format": "binary"}}},
|
"content": {"application/octet-stream": {"schema": {"type": "string", "format": "binary"}}},
|
||||||
"description": "Python script file (binary).",
|
"description": "Python script file (binary).",
|
||||||
|
"headers": {
|
||||||
|
"X-Script-Is-Protected-Example": {"schema": {"type": "string"}, "description": "'true' when script is a protected example"},
|
||||||
|
"X-Requirements-Is-Protected-Example": {"schema": {"type": "string"}, "description": "'true' when requirements file is a protected example"},
|
||||||
|
},
|
||||||
},
|
},
|
||||||
404: {"description": "Not found"},
|
404: {"description": "Not found"},
|
||||||
400: {"description": "Invalid name"},
|
400: {"description": "Invalid name"},
|
||||||
@@ -599,8 +752,19 @@ def download_script(name: str) -> FileResponse:
|
|||||||
raise HTTPException(status_code=404, detail="not found")
|
raise HTTPException(status_code=404, detail="not found")
|
||||||
logger.info("Download script %s", name)
|
logger.info("Download script %s", name)
|
||||||
|
|
||||||
|
is_protected = is_example_script(name)
|
||||||
|
req_is_protected = os.path.exists(example_requirements_path_for(name))
|
||||||
|
|
||||||
# Return generic octet-stream so clients treat as binary blob.
|
# Return generic octet-stream so clients treat as binary blob.
|
||||||
return FileResponse(path, media_type="application/octet-stream", filename=f"{name}.py")
|
return FileResponse(
|
||||||
|
path,
|
||||||
|
media_type="application/octet-stream",
|
||||||
|
filename=f"{name}.py",
|
||||||
|
headers={
|
||||||
|
"X-Script-Is-Protected-Example": "true" if is_protected else "false",
|
||||||
|
"X-Requirements-Is-Protected-Example": "true" if req_is_protected else "false",
|
||||||
|
},
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
@router.get(
|
@router.get(
|
||||||
@@ -610,6 +774,10 @@ def download_script(name: str) -> FileResponse:
|
|||||||
200: {
|
200: {
|
||||||
"content": {"application/octet-stream": {"schema": {"type": "string", "format": "binary"}}},
|
"content": {"application/octet-stream": {"schema": {"type": "string", "format": "binary"}}},
|
||||||
"description": "requirements.txt file (binary).",
|
"description": "requirements.txt file (binary).",
|
||||||
|
"headers": {
|
||||||
|
"X-Script-Is-Protected-Example": {"schema": {"type": "string"}, "description": "'true' when script is a protected example"},
|
||||||
|
"X-Requirements-Is-Protected-Example": {"schema": {"type": "string"}, "description": "'true' when requirements file is a protected example"},
|
||||||
|
},
|
||||||
},
|
},
|
||||||
404: {"description": "Not found"},
|
404: {"description": "Not found"},
|
||||||
400: {"description": "Invalid name"},
|
400: {"description": "Invalid name"},
|
||||||
@@ -627,7 +795,17 @@ def download_requirements(name: str) -> FileResponse:
|
|||||||
if not os.path.exists(path):
|
if not os.path.exists(path):
|
||||||
raise HTTPException(status_code=404, detail="requirements not found")
|
raise HTTPException(status_code=404, detail="requirements not found")
|
||||||
logger.info("Download requirements for %s", name)
|
logger.info("Download requirements for %s", name)
|
||||||
return FileResponse(path, media_type="application/octet-stream", filename=f"{name}-requirements.txt")
|
is_protected = is_example_script(name)
|
||||||
|
req_is_protected = os.path.exists(example_requirements_path_for(name))
|
||||||
|
return FileResponse(
|
||||||
|
path,
|
||||||
|
media_type="application/octet-stream",
|
||||||
|
filename=f"{name}-requirements.txt",
|
||||||
|
headers={
|
||||||
|
"X-Script-Is-Protected-Example": "true" if is_protected else "false",
|
||||||
|
"X-Requirements-Is-Protected-Example": "true" if req_is_protected else "false",
|
||||||
|
},
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
@router.put(
|
@router.put(
|
||||||
@@ -650,6 +828,8 @@ async def upload_requirements_install(name: str, requirements: UploadFile = File
|
|||||||
except ValueError as e:
|
except ValueError as e:
|
||||||
raise HTTPException(status_code=400, detail=str(e))
|
raise HTTPException(status_code=400, detail=str(e))
|
||||||
|
|
||||||
|
assert_not_example_script(name, "modified")
|
||||||
|
|
||||||
# ensure script exists (we don't allow attaching requirements to non-existing script)
|
# ensure script exists (we don't allow attaching requirements to non-existing script)
|
||||||
spath = script_path_for(name)
|
spath = script_path_for(name)
|
||||||
if not os.path.exists(spath):
|
if not os.path.exists(spath):
|
||||||
@@ -698,6 +878,8 @@ def delete_requirements_and_cleanup(name: str) -> Dict[str, Any]:
|
|||||||
except ValueError as e:
|
except ValueError as e:
|
||||||
raise HTTPException(status_code=400, detail=str(e))
|
raise HTTPException(status_code=400, detail=str(e))
|
||||||
|
|
||||||
|
assert_not_example_script(name, "modified")
|
||||||
|
|
||||||
spath = script_path_for(name)
|
spath = script_path_for(name)
|
||||||
if not os.path.exists(spath):
|
if not os.path.exists(spath):
|
||||||
raise HTTPException(status_code=404, detail="script not found")
|
raise HTTPException(status_code=404, detail="script not found")
|
||||||
@@ -744,6 +926,8 @@ def enable_script(name: str, req: EnableRequest) -> OperationResult:
|
|||||||
except ValueError as e:
|
except ValueError as e:
|
||||||
raise HTTPException(status_code=400, detail=str(e))
|
raise HTTPException(status_code=400, detail=str(e))
|
||||||
|
|
||||||
|
assert_not_example_script(name, "manually enabled")
|
||||||
|
|
||||||
script_path = script_path_for(name)
|
script_path = script_path_for(name)
|
||||||
if not os.path.exists(script_path):
|
if not os.path.exists(script_path):
|
||||||
raise HTTPException(status_code=404, detail="script not found")
|
raise HTTPException(status_code=404, detail="script not found")
|
||||||
@@ -784,6 +968,8 @@ def disable_script(name: str, qnum: int) -> OperationResult:
|
|||||||
validate_name(name)
|
validate_name(name)
|
||||||
except ValueError as e:
|
except ValueError as e:
|
||||||
raise HTTPException(status_code=400, detail=str(e))
|
raise HTTPException(status_code=400, detail=str(e))
|
||||||
|
|
||||||
|
assert_not_example_script(name, "disabled")
|
||||||
service_name = make_service_name(name, qnum)
|
service_name = make_service_name(name, qnum)
|
||||||
# attempt stop + remove via systemctl-based remove_unit
|
# attempt stop + remove via systemctl-based remove_unit
|
||||||
try:
|
try:
|
||||||
@@ -816,6 +1002,8 @@ def delete_script(name: str, qnum: Optional[int] = Query(None, description="If g
|
|||||||
except ValueError as e:
|
except ValueError as e:
|
||||||
raise HTTPException(status_code=400, detail=str(e))
|
raise HTTPException(status_code=400, detail=str(e))
|
||||||
|
|
||||||
|
assert_not_example_script(name, "deleted")
|
||||||
|
|
||||||
removed_units: List[str] = []
|
removed_units: List[str] = []
|
||||||
failed_units: List[str] = []
|
failed_units: List[str] = []
|
||||||
errors: List[str] = []
|
errors: List[str] = []
|
||||||
@@ -895,6 +1083,11 @@ def delete_script(name: str, qnum: Optional[int] = Query(None, description="If g
|
|||||||
|
|
||||||
# ---------- Lifecycle helper ----------
|
# ---------- Lifecycle helper ----------
|
||||||
def register_lifecycle(app) -> None:
|
def register_lifecycle(app) -> None:
|
||||||
|
@app.on_event("startup")
|
||||||
|
def _startup_event() -> None:
|
||||||
|
logger.info("Startup: syncing and deploying protected example scripts from %s", EXAMPLE_SCRIPT_DIR)
|
||||||
|
deploy_example_scripts_on_startup()
|
||||||
|
|
||||||
@app.on_event("shutdown")
|
@app.on_event("shutdown")
|
||||||
def _shutdown_event() -> None:
|
def _shutdown_event() -> None:
|
||||||
logger.info("Shutdown: stopping/removing manager-created units with prefix %s", UNIT_PREFIX)
|
logger.info("Shutdown: stopping/removing manager-created units with prefix %s", UNIT_PREFIX)
|
||||||
|
|||||||
Reference in New Issue
Block a user