from __future__ import annotations import re from pathlib import Path from sysadmin_env.models import DiagnosticTrigger from sysadmin_env.models import DifficultyTier from sysadmin_env.models import TaskMetadata from sysadmin_env.models import TaskScenarioDefinition from sysadmin_env.models import TaskScenarioState TASK_ID = "nginx_crash" COMPLETION_HEALTH = 0.99 PID_PATH = Path("var/run/nginx.pid") CONFIG_PATH = Path("etc/nginx/nginx.conf") ERROR_LOG_PATH = Path("var/log/nginx/error.log") RUNNING_FLAG_PATH = Path("run/nginx.running") PORT_FLAG_PATH = Path("run/nginx.port") BROKEN_CONFIG = """worker_processes 1; events { worker_connections 128; } http { server { listen 8080 location / { return 200 ok; } } } """ FIXED_CONFIG = """worker_processes 1; events { worker_connections 128; } http { server { listen 8080; location / { return 200 ok; } } } """ ERROR_LOG = "nginx emerg unexpected end of statement in /etc/nginx/nginx.conf line 7\n" def build_definition(base_filesystem_path: str) -> TaskScenarioDefinition: metadata = TaskMetadata( task_id=TASK_ID, difficulty=DifficultyTier.easy, description="nginx crashed with stale pid and config syntax error", max_steps=40, time_limit=300.0, base_filesystem_path=base_filesystem_path, ) return TaskScenarioDefinition( metadata=metadata, requires_network_isolation=False, diagnostic_triggers=diagnostic_triggers(), ) def diagnostic_triggers() -> list[DiagnosticTrigger]: return [ DiagnosticTrigger( fact_id="nginx_error_log_checked", command_patterns=[r"cat\s+.+error\.log", r"tail\s+.+error\.log", r"grep\s+.+error\.log"], reward=0.05, ), DiagnosticTrigger( fact_id="nginx_config_tested", command_patterns=[r"nginx\s+-t", r"/usr/sbin/nginx\s+-t"], reward=0.08, ), DiagnosticTrigger( fact_id="nginx_pid_checked", command_patterns=[r"cat\s+.+nginx\.pid", r"sed\s+.+nginx\.pid"], reward=0.04, ), DiagnosticTrigger( fact_id="nginx_process_table_checked", command_patterns=[r"ps\b.*nginx", r"pgrep\b.*nginx"], reward=0.04, ), ] def prepare_filesystem(root: str | Path) -> None: root_path = Path(root) for relative in [ Path("etc/nginx"), Path("var/run"), Path("var/log/nginx"), Path("run"), Path("usr/local/bin"), Path("root"), Path("tmp"), Path("home"), ]: (root_path / relative).mkdir(parents=True, exist_ok=True) (root_path / CONFIG_PATH).write_text(BROKEN_CONFIG) (root_path / PID_PATH).write_text("424242\n") (root_path / ERROR_LOG_PATH).write_text(ERROR_LOG) (root_path / RUNNING_FLAG_PATH).write_text("stopped\n") (root_path / PORT_FLAG_PATH).write_text("8080\n") _write_executable(root_path / "usr/local/bin/nginx", _nginx_stub()) _write_executable(root_path / "usr/local/bin/curl", _curl_stub()) _write_executable(root_path / "usr/local/bin/ps", _ps_stub()) _write_executable(root_path / "usr/local/bin/pgrep", _pgrep_stub()) _write_executable(root_path / "usr/local/bin/service", _service_stub()) _write_executable(root_path / "usr/local/bin/systemctl", _systemctl_stub()) def inject_fault(root: str | Path) -> None: prepare_filesystem(root) def observe_command(root: str | Path, command: str, _result) -> None: root_path = Path(root) if re.search(r"\b(service|systemctl)\b.*\bstatus\b", command, flags=re.IGNORECASE): if _service_is_running(root_path): (root_path / RUNNING_FLAG_PATH).write_text("running\n") else: (root_path / RUNNING_FLAG_PATH).write_text("stopped\n") def synchronize(root: str | Path) -> None: root_path = Path(root) running_flag = root_path / RUNNING_FLAG_PATH if not running_flag.exists(): running_flag.write_text("stopped\n") def grade(root: str | Path) -> TaskScenarioState: root_path = Path(root) stale_pid_removed = _stale_pid_cleared(root_path / PID_PATH) config_fixed = _config_is_fixed(root_path / CONFIG_PATH) running = _service_is_running(root_path) health = 0.0 if stale_pid_removed: health += 0.25 if config_fixed: health += 0.35 if running: health += 0.39 if running: health = COMPLETION_HEALTH return TaskScenarioState( health=health, done=running, details={ "stale_pid_removed": stale_pid_removed, "config_fixed": config_fixed, "service_running": running, }, ) def command_reveals_fact(command: str, trigger: DiagnosticTrigger) -> bool: return any(re.search(pattern, command, flags=re.IGNORECASE) for pattern in trigger.command_patterns) def _config_is_fixed(config_path: Path) -> bool: if not config_path.exists(): return False config_text = config_path.read_text() return re.search(r"listen\s+8080\s*;", config_text) is not None def _stale_pid_cleared(pid_path: Path) -> bool: if not pid_path.exists(): return True return pid_path.read_text().strip() == "1234" def _service_is_running(root_path: Path) -> bool: if not _config_is_fixed(root_path / CONFIG_PATH): return False running_flag = root_path / RUNNING_FLAG_PATH return running_flag.exists() and running_flag.read_text().strip() == "running" def _write_executable(path: Path, content: str) -> None: path.write_text(content) path.chmod(0o755) def _nginx_stub() -> str: return """#!/bin/sh config="/etc/nginx/nginx.conf" pidfile="/var/run/nginx.pid" statefile="/run/nginx.running" if [ "$1" = "-t" ]; then if grep -Eq 'listen[[:space:]]+8080;' "$config"; then echo "nginx config ok" exit 0 fi printf '%s\n' "nginx emerg unexpected end of statement in /etc/nginx/nginx.conf line 7" >&2 exit 1 fi if [ "$1" = "-s" ] && [ "$2" = "stop" ]; then rm -f "$pidfile" printf '%s\n' stopped > "$statefile" exit 0 fi if [ -f "$pidfile" ] && [ "$(cat "$pidfile" 2>/dev/null)" != "1234" ]; then printf '%s\n' "nginx stale pid present" >&2 exit 1 fi if grep -Eq 'listen[[:space:]]+8080;' "$config"; then printf '%s\n' running > "$statefile" printf '%s\n' 1234 > "$pidfile" printf '%s\n' "nginx started" exit 0 fi printf '%s\n' "nginx failed to start" >&2 exit 1 """ def _curl_stub() -> str: return """#!/bin/sh state="$(cat /run/nginx.running 2>/dev/null || printf '%s' stopped)" if [ "$state" = "running" ]; then printf '%s\n' "HTTP/1.1 200 ok" printf '%s\n' "server: nginx" exit 0 fi printf '%s\n' "curl failed to connect" >&2 exit 7 """ def _ps_stub() -> str: return """#!/bin/sh printf '%s\n' "USER PID CMD" state="$(cat /run/nginx.running 2>/dev/null || printf '%s' stopped)" if [ "$state" = "running" ]; then printf '%s\n' "root 1234 nginx" fi exit 0 """ def _pgrep_stub() -> str: return """#!/bin/sh state="$(cat /run/nginx.running 2>/dev/null || printf '%s' stopped)" if [ "$state" = "running" ]; then printf '%s\n' 1234 exit 0 fi exit 1 """ def _service_stub() -> str: return """#!/bin/sh name="$1" action="$2" if [ "$name" = "nginx" ] && [ "$action" = "start" ]; then exec nginx fi if [ "$name" = "nginx" ] && [ "$action" = "stop" ]; then exec nginx -s stop fi if [ "$name" = "nginx" ] && [ "$action" = "status" ]; then state="$(cat /run/nginx.running 2>/dev/null || printf '%s' stopped)" if [ "$state" = "running" ]; then printf '%s\n' "nginx active" exit 0 fi printf '%s\n' "nginx inactive" >&2 exit 3 fi printf '%s\n' "unsupported service action" >&2 exit 1 """ def _systemctl_stub() -> str: return """#!/bin/sh action="$1" name="$2" if [ "$name" = "nginx" ] && [ "$action" = "start" ]; then exec nginx fi if [ "$name" = "nginx" ] && [ "$action" = "stop" ]; then exec nginx -s stop fi if [ "$name" = "nginx" ] && [ "$action" = "restart" ]; then nginx -s stop >/dev/null 2>&1 || true exec nginx fi if [ "$name" = "nginx" ] && [ "$action" = "status" ]; then exec service nginx status fi printf '%s\n' "unsupported systemctl action" >&2 exit 1 """