129 lines
5.0 KiB
Python
Executable File
129 lines
5.0 KiB
Python
Executable File
"""Executor router — dispatch table over runtime adapters.
|
|
|
|
Security note (F-02 / F-11 remediation):
|
|
- The previous 6-branch if/elif chain is replaced with a dispatch
|
|
table (``_RUNTIME_ADAPTERS``). Adding a new runtime is now a
|
|
one-line dict entry instead of a new branch.
|
|
- ``run_firecracker`` no longer builds an SSH command string via
|
|
f-string interpolation; it delegates to the (now arg-list-based)
|
|
adapter in :mod:`backend.executor.adapters`.
|
|
"""
|
|
|
|
from __future__ import annotations
|
|
|
|
import subprocess
|
|
from typing import Any, Callable, Dict, Mapping, Optional
|
|
|
|
|
|
# ---------------------------------------------------------------------------
|
|
# Public dispatch table
|
|
# ---------------------------------------------------------------------------
|
|
def execute(action: Mapping[str, Any], node: Mapping[str, Any],
|
|
spec: Optional[Mapping[str, Any]] = None) -> int:
|
|
"""Dispatch ``action`` to the runtime named by ``spec.execution``.
|
|
|
|
Returns the subprocess exit code (0 = success). Raises
|
|
:class:`ValueError` if the runtime is unknown — this satisfies
|
|
MISRA Rule 16.4 (switch shall have default / if-elif must raise
|
|
on unknown).
|
|
"""
|
|
exec_type = (spec or {}).get("execution") or (action or {}).get("runtime") or "host"
|
|
fn = _RUNTIME_ADAPTERS.get(exec_type)
|
|
if fn is None:
|
|
raise ValueError(f"Unknown execution type: {exec_type}")
|
|
return fn(action, node, spec)
|
|
|
|
|
|
# ---------------------------------------------------------------------------
|
|
# Per-runtime adapters (legacy signature kept for backward compat)
|
|
# ---------------------------------------------------------------------------
|
|
def run_distcc(action: Mapping[str, Any], node: Mapping[str, Any],
|
|
spec: Optional[Mapping[str, Any]]) -> int:
|
|
cmd = action.get("cmd") or action.get("command") or ""
|
|
if not cmd:
|
|
return 2
|
|
# distcc takes the compiler invocation as argv; tokenize safely.
|
|
import shlex
|
|
return subprocess.call(["distcc", *shlex.split(cmd)])
|
|
|
|
|
|
def run_lxc(action: Mapping[str, Any], node: Mapping[str, Any],
|
|
spec: Optional[Mapping[str, Any]]) -> int:
|
|
from backend.executor.adapters import run_lxc as _lxc
|
|
container = node.get("container") or "default"
|
|
cmd = action.get("cmd") or action.get("command") or ""
|
|
if not cmd:
|
|
return 2
|
|
return _lxc(container, cmd)
|
|
|
|
|
|
def run_libvirt(action: Mapping[str, Any], node: Mapping[str, Any],
|
|
spec: Optional[Mapping[str, Any]]) -> int:
|
|
from backend.executor.adapters import run_libvirt as _libvirt
|
|
vm = node.get("vm") or "default"
|
|
cmd = action.get("cmd") or action.get("command") or ""
|
|
if not cmd:
|
|
return 2
|
|
return _libvirt(vm, cmd)
|
|
|
|
|
|
def run_podman(action: Mapping[str, Any], node: Mapping[str, Any],
|
|
spec: Optional[Mapping[str, Any]]) -> int:
|
|
from backend.executor.adapters import run_podman as _podman
|
|
container = node.get("container", "default")
|
|
cmd = action.get("cmd") or action.get("command") or ""
|
|
if not cmd:
|
|
return 2
|
|
cwd = action.get("dir") or action.get("cwd")
|
|
return _podman(container, cmd, cwd=cwd)
|
|
|
|
|
|
def run_firecracker(action: Mapping[str, Any], node: Mapping[str, Any],
|
|
spec: Optional[Mapping[str, Any]]) -> int:
|
|
"""SSH into a Firecracker microVM and run ``action['cmd']``.
|
|
|
|
F-02 fix: no shell=True, no f-string interpolation of untrusted
|
|
fields. Delegates to :func:`backend.executor.adapters.run_firecracker`,
|
|
which builds the SSH argv explicitly.
|
|
"""
|
|
import os
|
|
from backend.executor.adapters import run_firecracker as _fc
|
|
host = node.get("host", "127.0.0.1")
|
|
fc_config = node.get("firecracker", {}) or {}
|
|
ssh_key = fc_config.get(
|
|
"ssh_key",
|
|
os.environ.get("FESTER_FC_SSH_KEY", "/root/.ssh/fc_builder"),
|
|
)
|
|
ssh_port = fc_config.get("ssh_port", 2222)
|
|
cmd = action.get("cmd") or action.get("command") or ""
|
|
if not cmd:
|
|
return 2
|
|
cwd = action.get("dir") or action.get("cwd")
|
|
return _fc(host, ssh_key, ssh_port, cmd, cwd=cwd)
|
|
|
|
|
|
def run_tmux(action: Mapping[str, Any], node: Mapping[str, Any],
|
|
spec: Optional[Mapping[str, Any]]) -> int:
|
|
cmd = action.get("cmd") or action.get("command") or ""
|
|
if not cmd:
|
|
return 2
|
|
import shlex
|
|
return subprocess.call(["tmux", "new", "-d", *shlex.split(cmd)])
|
|
|
|
|
|
# ---------------------------------------------------------------------------
|
|
# Dispatch table — single source of truth for runtime lookup
|
|
# ---------------------------------------------------------------------------
|
|
_RUNTIME_ADAPTERS: Dict[str, Callable[..., int]] = {
|
|
"distcc": run_distcc,
|
|
"lxc": run_lxc,
|
|
"libvirt": run_libvirt,
|
|
"podman": run_podman,
|
|
"firecracker": run_firecracker,
|
|
"tmux": run_tmux,
|
|
# 'host' is handled by runtime_router.execute_action directly via
|
|
# adapters.run_host, but we register it here so the dispatch table
|
|
# is the single source of truth for "is this runtime known?".
|
|
"host": lambda action, node, spec: 0,
|
|
}
|