from __future__ import annotations from typing import Any import subprocess import json from pathlib import Path from core.tools.base import BaseTool, ToolContext from core.tools.registry import registry from core.safety import safety from core.events import bus VM_STATE_FILE = Path("./vm_state.json") class QemuTool(BaseTool): name = "qemu" description = "QEMU VM lifecycle management" # ------------------------- # EXECUTE ROUTER # ------------------------- def execute(self, payload: dict[str, Any], ctx: ToolContext): action = str(payload.get("action", "")).strip() bus.log( "QEMU", "qemu_execute", "INFO", {"action": action} ) match action: case "start": return self.start_vm(payload, ctx) case "stop": return self.stop_vm(payload, ctx) case "status": return self.status(payload) case "list": return self.list_vms() case _: raise ValueError(f"Unknown qemu action: {action}") # ------------------------- # STATE HELPERS # ------------------------- def _load_state(self) -> dict[str, Any]: if VM_STATE_FILE.exists(): return json.loads(VM_STATE_FILE.read_text()) return {} def _save_state(self, state: dict[str, Any]): VM_STATE_FILE.write_text(json.dumps(state, indent=2)) # ------------------------- # START VM # ------------------------- def start_vm(self, payload: dict[str, Any], ctx: ToolContext): name = payload.get("name") image = payload.get("image") if not isinstance(name, str): raise ValueError("name must be string") if not isinstance(image, str): raise ValueError("image must be string") image_path = safety.validate_path(image) state = self._load_state() if name in state and state[name].get("running"): return {"status": "already_running", "name": name} cmd = [ "qemu-system-x86_64", "-m", "2048", "-drive", f"file={image_path},format=qcow2", "-nographic" ] process = subprocess.Popen(cmd) state[name] = { "pid": process.pid, "image": str(image_path), "running": True } self._save_state(state) return { "status": "started", "name": name, "pid": process.pid } # ------------------------- # STOP VM # ------------------------- def stop_vm(self, payload: dict[str, Any], ctx: ToolContext): name = payload.get("name") if not isinstance(name, str): raise ValueError("name must be string") state = self._load_state() vm = state.get(name) if not vm or not vm.get("running"): return {"status": "not_running", "name": name} pid = vm["pid"] try: subprocess.run(["kill", str(pid)], check=False) except Exception: pass vm["running"] = False self._save_state(state) return {"status": "stopped", "name": name, "pid": pid} # ------------------------- # STATUS # ------------------------- def status(self, payload: dict[str, Any]): name = payload.get("name") if not isinstance(name, str): raise ValueError("name must be string") state = self._load_state() return state.get(name, {"status": "unknown"}) # ------------------------- # LIST # ------------------------- def list_vms(self): return self._load_state() registry.register(QemuTool())