fester/backend/executor/router.py

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,
}