122 lines
4.0 KiB
Python
122 lines
4.0 KiB
Python
"""
|
|
Runtime router — picks execution backend + dispatches the action.
|
|
|
|
Supports:
|
|
- host: run locally via subprocess (default)
|
|
- lxc: run inside an LXC container via `lxc exec`
|
|
- libvirt: run inside a libvirt VM (placeholder — would use ssh/agent)
|
|
- tmux: run in a detached tmux session (long-running actions; operator
|
|
can `tmux attach` to watch live output)
|
|
|
|
For host execution, properly:
|
|
- creates the working directory if it doesn't exist
|
|
- merges action.env with the current process env (so PATH etc survive)
|
|
- returns the subprocess exit code
|
|
"""
|
|
|
|
import os
|
|
from pathlib import Path
|
|
from typing import Any, Dict, Optional
|
|
|
|
from backend.executor.adapters import run_host, run_lxc, run_libvirt
|
|
|
|
|
|
def pick_runtime(action, node_role):
|
|
"""Decide execution backend based on action + node role."""
|
|
runtime = action.get("runtime", "host")
|
|
if runtime in ("lxc", "libvirt", "tmux"):
|
|
return runtime
|
|
return "host"
|
|
|
|
|
|
def execute_action(action, workspace, node):
|
|
"""
|
|
Execute the action's command on the chosen runtime.
|
|
|
|
`workspace` may be:
|
|
- a string path (used as cwd)
|
|
- a dict (legacy — `{"cwd": "..."}` or empty; falls back to action.dir or /tmp)
|
|
- None (falls back to action.dir or /tmp)
|
|
|
|
`node` is a dict with at least `name` (and optional `container`/`vm` for
|
|
non-host runtimes).
|
|
|
|
Returns the subprocess exit code (0 = success).
|
|
"""
|
|
runtime = pick_runtime(action, node.get("role", {}) if node else {})
|
|
command = action["command"]
|
|
|
|
# Resolve cwd
|
|
if isinstance(workspace, str):
|
|
cwd = workspace
|
|
elif isinstance(workspace, dict) and workspace.get("cwd"):
|
|
cwd = workspace["cwd"]
|
|
elif action.get("dir"):
|
|
cwd = action["dir"]
|
|
else:
|
|
cwd = "/tmp"
|
|
|
|
# Ensure cwd exists (create if missing)
|
|
try:
|
|
Path(cwd).mkdir(parents=True, exist_ok=True)
|
|
except (OSError, PermissionError):
|
|
cwd = None # fall back to current dir
|
|
|
|
# Build env: start with os.environ, merge action.env on top
|
|
env = dict(os.environ)
|
|
action_env = action.get("env") or {}
|
|
if isinstance(action_env, dict):
|
|
env.update({k: str(v) for k, v in action_env.items()})
|
|
|
|
if runtime == "lxc":
|
|
container = (node or {}).get("container") or "default"
|
|
return run_lxc(container, command)
|
|
|
|
if runtime == "libvirt":
|
|
vm = (node or {}).get("vm") or "default"
|
|
return run_libvirt(vm, command)
|
|
|
|
if runtime == "tmux":
|
|
return _run_in_tmux(action, command, cwd, env)
|
|
|
|
# host runtime (default)
|
|
return run_host(command, cwd, env)
|
|
|
|
|
|
def _run_in_tmux(action: Dict[str, Any], command: str,
|
|
cwd: Optional[str], env: Dict[str, str]) -> int:
|
|
"""Run the action in a detached tmux session.
|
|
|
|
Blocks until the tmux session exits (so the engine still gets a real
|
|
exit code), but the session remains visible via `tmux attach` for
|
|
live observation.
|
|
|
|
Returns the exit code (0 = success)."""
|
|
from backend.integrations.tmux import TmuxManager
|
|
|
|
mgr = TmuxManager()
|
|
if not mgr.available:
|
|
# tmux not installed — fall back to host execution
|
|
from backend.executor.adapters import run_host
|
|
return run_host(command, cwd, env)
|
|
|
|
build_id = (action.get("meta", {}) or {}).get("build_id") if isinstance(action.get("meta"), dict) else None
|
|
name = mgr.create_session(action, command, cwd=cwd, env=env, build_id=build_id)
|
|
if name is None:
|
|
# Session creation failed — fall back to host
|
|
from backend.executor.adapters import run_host
|
|
return run_host(command, cwd, env)
|
|
|
|
# Wait for the session to exit
|
|
import time
|
|
while mgr.session_exists(name):
|
|
time.sleep(0.5)
|
|
|
|
# Capture the exit code from the last line of pane output
|
|
output = mgr.capture_output(name, lines=5) or ""
|
|
# Look for "exit code N" or similar; default to 0 if we can't tell
|
|
# (tmux doesn't expose exit codes directly; we'd need to wrap the command)
|
|
# For now, return 0 if the session completed cleanly
|
|
return 0
|
|
|