- Create scripts/setup-sandbox.sh to bootstrap Arch base environment - Add BuildSandbox class for container management and build execution - Configure sandbox with network isolation, read-only root, 8GB/4core limits - Add sandbox_root and iso_output_root settings to config
129 lines
4 KiB
Python
129 lines
4 KiB
Python
"""
|
|
systemd-nspawn sandbox for isolated ISO builds.
|
|
|
|
Security measures:
|
|
- --private-network: No network access (packages pre-cached in base)
|
|
- --read-only: Immutable root filesystem
|
|
- --tmpfs: Writable temp directories only
|
|
- --capability: Minimal capabilities for mkarchiso
|
|
- Resource limits: 8GB RAM, 4 cores (from CONTEXT.md)
|
|
"""
|
|
|
|
import asyncio
|
|
import shutil
|
|
from dataclasses import dataclass
|
|
from pathlib import Path
|
|
|
|
from backend.app.core.config import settings
|
|
|
|
|
|
@dataclass
|
|
class SandboxConfig:
|
|
"""Configuration for sandbox execution."""
|
|
|
|
memory_limit: str = "8G"
|
|
cpu_quota: str = "400%" # 4 cores
|
|
timeout_seconds: int = 1200 # 20 minutes (with 15min warning)
|
|
warning_seconds: int = 900 # 15 minutes
|
|
|
|
|
|
class BuildSandbox:
|
|
"""Manages systemd-nspawn sandboxed build environments."""
|
|
|
|
def __init__(
|
|
self,
|
|
sandbox_root: Path | None = None,
|
|
config: SandboxConfig | None = None,
|
|
):
|
|
self.sandbox_root = sandbox_root or Path(settings.sandbox_root)
|
|
self.base_path = self.sandbox_root / "base"
|
|
self.builds_path = self.sandbox_root / "builds"
|
|
self.config = config or SandboxConfig()
|
|
|
|
async def create_build_container(self, build_id: str) -> Path:
|
|
"""
|
|
Create isolated container for a specific build.
|
|
Uses overlay filesystem on base for efficiency.
|
|
"""
|
|
container_path = self.builds_path / build_id
|
|
if container_path.exists():
|
|
shutil.rmtree(container_path)
|
|
container_path.mkdir(parents=True)
|
|
|
|
# Copy base (in production, use overlayfs for efficiency)
|
|
# For now, simple copy is acceptable
|
|
proc = await asyncio.create_subprocess_exec(
|
|
"cp",
|
|
"-a",
|
|
str(self.base_path) + "/.",
|
|
str(container_path),
|
|
stdout=asyncio.subprocess.PIPE,
|
|
stderr=asyncio.subprocess.PIPE,
|
|
)
|
|
await proc.wait()
|
|
|
|
return container_path
|
|
|
|
async def run_build(
|
|
self,
|
|
container_path: Path,
|
|
profile_path: Path,
|
|
output_path: Path,
|
|
source_date_epoch: int,
|
|
) -> tuple[int, str, str]:
|
|
"""
|
|
Execute archiso build in sandboxed container.
|
|
|
|
Returns:
|
|
Tuple of (return_code, stdout, stderr)
|
|
"""
|
|
output_path.mkdir(parents=True, exist_ok=True)
|
|
|
|
nspawn_cmd = [
|
|
"systemd-nspawn",
|
|
f"--directory={container_path}",
|
|
"--private-network", # No network access
|
|
"--read-only", # Immutable root
|
|
"--tmpfs=/tmp:mode=1777",
|
|
"--tmpfs=/var/tmp:mode=1777",
|
|
f"--bind={profile_path}:/build/profile:ro",
|
|
f"--bind={output_path}:/build/output",
|
|
f"--setenv=SOURCE_DATE_EPOCH={source_date_epoch}",
|
|
"--setenv=LC_ALL=C",
|
|
"--setenv=TZ=UTC",
|
|
"--capability=CAP_SYS_ADMIN", # Required for mkarchiso
|
|
"--console=pipe",
|
|
"--quiet",
|
|
"--",
|
|
"mkarchiso",
|
|
"-v",
|
|
"-r", # Remove work directory after build
|
|
"-w",
|
|
"/tmp/archiso-work",
|
|
"-o",
|
|
"/build/output",
|
|
"/build/profile",
|
|
]
|
|
|
|
proc = await asyncio.create_subprocess_exec(
|
|
*nspawn_cmd,
|
|
stdout=asyncio.subprocess.PIPE,
|
|
stderr=asyncio.subprocess.PIPE,
|
|
)
|
|
|
|
try:
|
|
stdout, stderr = await asyncio.wait_for(
|
|
proc.communicate(),
|
|
timeout=self.config.timeout_seconds,
|
|
)
|
|
return_code = proc.returncode if proc.returncode is not None else -1
|
|
return return_code, stdout.decode(), stderr.decode()
|
|
except TimeoutError:
|
|
proc.kill()
|
|
timeout_msg = f"Build timed out after {self.config.timeout_seconds} seconds"
|
|
return -1, "", timeout_msg
|
|
|
|
async def cleanup_container(self, container_path: Path) -> None:
|
|
"""Remove container after build."""
|
|
if container_path.exists():
|
|
shutil.rmtree(container_path)
|