Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 2 additions & 1 deletion docs/content/docs/how-to-guides/sandbox/images.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -21,7 +21,8 @@ Image.linux(kind='container') # Ubuntu 24.04 container (lighter, Docker/X
Image.linux('ubuntu', '22.04') # Older Ubuntu
Image.macos() # macOS Tahoe (version '26', latest)
Image.macos('15') # macOS Sequoia
Image.windows() # Windows 11
Image.windows() # Windows Server 2022
Image.windows('11') # Windows 11 (local ISO install only)
Image.android() # Android 14
```

Expand Down
2 changes: 1 addition & 1 deletion libs/python/cua-cli/cua_cli/commands/mcp.py
Original file line number Diff line number Diff line change
Expand Up @@ -244,7 +244,7 @@ async def sandbox_create(
if os_type == "macos":
image = Image.macos("26")
elif os_type == "windows":
image = Image.windows("11")
image = Image.windows()
else:
image = Image.linux("ubuntu", "24.04")
sb = await Sandbox.create(image, api_key=await get_access_token(), region=region)
Expand Down
3 changes: 2 additions & 1 deletion libs/python/cua-cli/cua_cli/commands/sandbox.py
Original file line number Diff line number Diff line change
Expand Up @@ -49,6 +49,7 @@ def _parse_image(image_str: str, vm: bool = False):
"macos:sequoia" -> Image.macos("sequoia")
"ubuntu:24.04" -> Image.linux("ubuntu", "24.04")
"linux" -> Image.linux("ubuntu", "24.04")
"windows" -> Image.windows("2022")
"windows:11" -> Image.windows("11")
"android:14" -> Image.android("14")
"ghcr.io/org/img" -> Image.from_registry("ghcr.io/org/img")
Expand Down Expand Up @@ -80,7 +81,7 @@ def _parse_image(image_str: str, vm: bool = False):
return Image.linux(distro, version, kind=kind)

if base in _WINDOWS_ALIASES:
version = tag or "11"
version = tag or "2022"
return Image.windows(version)

if base in _ANDROID_ALIASES:
Expand Down
39 changes: 28 additions & 11 deletions libs/python/cua-sandbox/cua_sandbox/image.py
Original file line number Diff line number Diff line change
Expand Up @@ -29,6 +29,19 @@
logger = logging.getLogger(__name__)

DEFAULT_LINUX_REGISTRY_IMAGE = "public.ecr.aws/k5j5w0x5/cua-ubuntu-24.04:main-38352d34"
# Anonymously pullable, so the built-in Windows image needs no registry credentials.
# The guest is Windows Server 2022 (build 10.0.20348), which is why ``Image.windows()``
# defaults to "2022". ``Image.windows("11")`` still means client Windows 11, which has
# no containerDisk and is installed locally from a downloaded evaluation ISO.
# Index digest: sha256:6d341afc26a37c4072d22ba403a89ecdad9a29aebab79570b5a38da6b8e16370
DEFAULT_WINDOWS_REGISTRY_IMAGE = "public.ecr.aws/k5j5w0x5/cua-windows-2022:main-bac7daa3"

# Built-in image descriptors that resolve to a pinned KubeVirt containerDisk, so
# Fleet cloud and the local QEMU runtime boot byte-identical disks.
BUILTIN_REGISTRY_IMAGES: Dict[Tuple[str, str, str, Optional[str]], str] = {
("linux", "ubuntu", "24.04", "vm"): DEFAULT_LINUX_REGISTRY_IMAGE,
("windows", "windows", "2022", "vm"): DEFAULT_WINDOWS_REGISTRY_IMAGE,
}

_IMAGE_CACHE = Path.home() / ".cua" / "cua-sandbox" / "image-cache"

Expand Down Expand Up @@ -145,8 +158,14 @@ def macos(cls, version: str = "26", kind: str = "vm") -> Image:
return cls(os_type="macos", distro="macos", version=version, kind=kind)

@classmethod
def windows(cls, version: str = "11", kind: str = "vm") -> Image:
"""Windows image. Always a VM (QEMU or Hyper-V)."""
def windows(cls, version: str = "2022", kind: str = "vm") -> Image:
"""Windows image. Always a VM (QEMU or Hyper-V).

Defaults to ``"2022"`` (Windows Server 2022), the only version with a pinned
containerDisk — see :data:`BUILTIN_REGISTRY_IMAGES`. Other versions, including
``"11"``, have no pinned disk: on Fleet cloud they are unsupported, and locally
they are installed from a downloaded evaluation ISO.
"""
return cls(os_type="windows", distro="windows", version=version, kind=kind)

@classmethod
Expand Down Expand Up @@ -482,14 +501,12 @@ def __repr__(self) -> str:


def cloud_registry_image(image: Image) -> Optional[str]:
"""Return the explicit or built-in registry image used by Fleet cloud."""
"""Return the explicit or built-in containerDisk reference for an image.

An explicit ``Image.from_registry(...)`` reference always wins; otherwise the
built-in descriptors resolve through :data:`BUILTIN_REGISTRY_IMAGES`. Images
with no pinned disk (custom distros, container kinds) return ``None``.
"""
if image._registry is not None:
return image._registry
if (
image.os_type == "linux"
and image.distro == "ubuntu"
and image.version == "24.04"
and image.kind == "vm"
):
return DEFAULT_LINUX_REGISTRY_IMAGE
return None
return BUILTIN_REGISTRY_IMAGES.get((image.os_type, image.distro, image.version, image.kind))
2 changes: 2 additions & 0 deletions libs/python/cua-sandbox/cua_sandbox/registry/__init__.py
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
from cua_sandbox.registry.cache import ImageCache
from cua_sandbox.registry.container_disk import pull_container_disk
from cua_sandbox.registry.manifest import (
ImageFormat,
detect_format,
Expand All @@ -21,6 +22,7 @@
"resolve_image_kind",
"pull_image",
"ImageCache",
"pull_container_disk",
"QEMUImageConfig",
"push_qemu_image",
"pull_qemu_image",
Expand Down
140 changes: 98 additions & 42 deletions libs/python/cua-sandbox/cua_sandbox/runtime/qemu.py
Original file line number Diff line number Diff line change
Expand Up @@ -11,8 +11,10 @@
import json as _json
import logging
import platform as _plat
import re
import shutil
import subprocess
from collections.abc import Callable
from pathlib import Path
from typing import TYPE_CHECKING, Optional

Expand Down Expand Up @@ -152,6 +154,42 @@ async def resume(self, image: "Image", name: str, **opts) -> RuntimeInfo:
return info


# UEFI firmware for Windows guests, as (code, vars-template) pairs. The two halves
# are sized to match each other, so they are always taken from the same entry — a
# 4 MB OVMF build paired with a 2 MB varstore leaves the guest unable to boot.
# Paths are relative to the bundled QEMU directory, or absolute for system installs.
_UEFI_FIRMWARE_CANDIDATES: list[tuple[str, str]] = [
("share/edk2-x86_64-code.fd", "share/edk2-i386-vars.fd"),
("/usr/share/OVMF/OVMF_CODE_4M.fd", "/usr/share/OVMF/OVMF_VARS_4M.fd"),
("/usr/share/OVMF/OVMF_CODE.fd", "/usr/share/OVMF/OVMF_VARS.fd"),
("/usr/share/qemu/edk2-x86_64-code.fd", "/usr/share/qemu/edk2-i386-vars.fd"),
]


def _locate_uefi_firmware(qemu_dir: Path) -> tuple[Optional[Path], Optional[Path]]:
"""Return the first (OVMF code, matching vars template) pair present on this host."""
for code_name, vars_name in _UEFI_FIRMWARE_CANDIDATES:
code = Path(code_name) if code_name.startswith("/") else qemu_dir / code_name
if not code.exists():
continue
template = Path(vars_name) if vars_name.startswith("/") else qemu_dir / vars_name
return code, template if template.exists() else None
return None, None


def _locate_uefi_firmware_in(exists: Callable[[str], bool]) -> tuple[Optional[str], Optional[str]]:
"""Same lookup as :func:`_locate_uefi_firmware`, for a guest filesystem like WSL.

Only the absolute candidates apply — a WSL distro has its own /usr/share, and the
Windows-side bundled QEMU directory is not on its path.
"""
for code_name, vars_name in _UEFI_FIRMWARE_CANDIDATES:
if not code_name.startswith("/") or not exists(code_name):
continue
return code_name, vars_name if exists(vars_name) else None
return None, None


class QEMUBaremetalRuntime(Runtime):
"""Bare-metal QEMU — launches qemu-system-* directly on the host.

Expand Down Expand Up @@ -258,24 +296,16 @@ async def start(self, image: Image, name: str, **opts) -> RuntimeInfo:

# Locate OVMF UEFI firmware for Windows VMs
qemu_dir = Path(self._qemu_bin()).parent
ovmf_code = None
if image.os_type == "windows":
for candidate in [
qemu_dir / "share" / "edk2-x86_64-code.fd",
Path("/usr/share/OVMF/OVMF_CODE.fd"),
Path("/usr/share/qemu/edk2-x86_64-code.fd"),
]:
if candidate.exists():
ovmf_code = candidate
break

# EFI vars — look next to disk or copy template
ovmf_code, vars_template = (
_locate_uefi_firmware(qemu_dir) if image.os_type == "windows" else (None, None)
)

# EFI vars — look next to disk or copy the template that matches the firmware
efivars = Path(disk_path).parent / "efivars.fd"
if ovmf_code and not efivars.exists():
import shutil as _shutil

vars_template = qemu_dir / "share" / "edk2-i386-vars.fd"
if vars_template.exists():
if vars_template is not None:
_shutil.copy2(vars_template, efivars)
else:
efivars.write_bytes(b"\x00" * (256 * 1024))
Expand Down Expand Up @@ -769,6 +799,11 @@ async def _is_ready_qmp(self, info: RuntimeInfo, timeout: float = 120) -> bool:
raise TimeoutError(f"Bare-metal QEMU VM {info.name} QMP not ready after {timeout}s")


# A drive-letter path such as C:\\Users\\... or C:/Users/..., which QEMU running
# inside WSL would otherwise parse as a URI with scheme "C".
_WINDOWS_DRIVE_PATH = re.compile(r"^[A-Za-z]:[\\/]")


def _win_to_wsl(p: Path | str) -> str:
"""Convert a Windows path to WSL /mnt/... path."""
s = str(p).replace("\\", "/")
Expand Down Expand Up @@ -820,6 +855,40 @@ def _wsl(cmd: str, timeout: float = 30) -> str:
raise RuntimeError(f"WSL command failed: {r.stderr.strip()}")
return r.stdout.strip()

def _wsl_file_exists(self, path: str) -> bool:
"""True when `path` is a regular file inside the WSL distribution."""
try:
self._wsl(f"test -f {path}")
return True
except (RuntimeError, subprocess.SubprocessError, OSError):
return False

def _rebase_backing_file(self, wsl_disk: str) -> None:
"""Rewrite a session overlay's backing path into WSL's view of the filesystem.

The overlay is created by the Windows-side builder, so its recorded backing
file is a Windows path. QEMU inside WSL reads the drive letter as a URI scheme
and refuses the drive with ``Could not open backing file: Unknown protocol 'C'``.
The rebase is metadata-only (``-u``): it repoints the overlay without touching
a byte of either image.
"""
try:
info = _json.loads(self._wsl(f"qemu-img info --output=json '{wsl_disk}'"))
except (RuntimeError, ValueError) as exc:
logger.debug("Could not inspect %s for a backing file: %s", wsl_disk, exc)
return

backing = info.get("backing-filename")
if not backing or not _WINDOWS_DRIVE_PATH.match(backing):
return

wsl_backing = _win_to_wsl(backing)
logger.info("Rebasing %s onto its WSL backing path %s", wsl_disk, wsl_backing)
self._wsl(
f"qemu-img rebase -u -f qcow2 -F qcow2 -b '{wsl_backing}' '{wsl_disk}'",
timeout=120,
)

@staticmethod
def available() -> bool:
"""Check if WSL2 + QEMU + KVM are available."""
Expand Down Expand Up @@ -864,44 +933,31 @@ async def start(self, image: Image, name: str, **opts) -> RuntimeInfo:

# Convert Windows paths to WSL paths
wsl_disk = _win_to_wsl(disk_path)
self._rebase_backing_file(wsl_disk)
disk_ext = Path(disk_path).suffix.lower()
disk_fmt = {".qcow2": "qcow2", ".vhdx": "vhdx", ".raw": "raw", ".img": "raw"}.get(
disk_ext, "raw"
)

# Locate OVMF inside WSL
ovmf_code = None
if image.os_type == "windows":
for candidate in [
"/usr/share/OVMF/OVMF_CODE_4M.fd",
"/usr/share/OVMF/OVMF_CODE.fd",
"/usr/share/qemu/edk2-x86_64-code.fd",
]:
try:
self._wsl(f"test -f {candidate}")
ovmf_code = candidate
break
except RuntimeError:
pass
# Locate OVMF inside WSL — code and vars must come from the same entry, or a
# 4 MB firmware ends up backed by a 2 MB varstore and the guest cannot boot.
ovmf_code, vars_template = (
_locate_uefi_firmware_in(self._wsl_file_exists)
if image.os_type == "windows"
else (None, None)
)

# EFI vars — create in same dir as disk (WSL path)
efivars_win = Path(disk_path).parent / "efivars.fd"
wsl_efivars = _win_to_wsl(efivars_win)
if ovmf_code and not efivars_win.exists():
# Copy OVMF vars template via WSL
for vars_candidate in [
"/usr/share/OVMF/OVMF_VARS_4M.fd",
"/usr/share/OVMF/OVMF_VARS.fd",
"/usr/share/qemu/edk2-i386-vars.fd",
]:
try:
self._wsl(f"test -f {vars_candidate} && cp {vars_candidate} '{wsl_efivars}'")
break
except RuntimeError:
continue
else:
# Create empty vars file
efivars_win.write_bytes(b"\x00" * (256 * 1024))
if vars_template is None:
raise RuntimeError(
f"WSL has UEFI firmware at {ovmf_code} but no matching variable store. "
"Install the OVMF package inside the WSL distribution "
"(apt install ovmf) so the two halves match."
)
self._wsl(f"cp {vars_template} '{wsl_efivars}'")

# Build QEMU command (runs inside WSL)
parts = [
Expand Down
Loading
Loading