155 lines
3.7 KiB
Python
155 lines
3.7 KiB
Python
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()) |
