e0e362d700
SDK Tests / changes (push) Successful in 2m29s
Real E2E Tests / changes (push) Successful in 2m29s
Deploy Docs Pages / build (push) Has been cancelled
Deploy Docs Pages / deploy (push) Has been cancelled
Real E2E Tests / JavaScript E2E (docker bridge) (push) Has been cancelled
Real E2E Tests / Python E2E (docker bridge) (push) Has been cancelled
Real E2E Tests / Java E2E (docker bridge) (push) Has been cancelled
Real E2E Tests / C# E2E (docker bridge) (push) Has been cancelled
Real E2E Tests / Go E2E (docker bridge) (push) Has been cancelled
Real E2E Tests / Real E2E CI (push) Has been cancelled
SDK Tests / SDK CI (push) Has been cancelled
SDK Tests / CLI Tests (push) Has been cancelled
SDK Tests / Python SDK Quality (code-interpreter) (push) Has been cancelled
SDK Tests / Python SDK Quality (sandbox) (push) Has been cancelled
SDK Tests / Python SDK Tests (code-interpreter) (push) Has been cancelled
SDK Tests / JavaScript SDK Quality And Tests (code-interpreter) (push) Has been cancelled
SDK Tests / JavaScript SDK Quality And Tests (sandbox) (push) Has been cancelled
SDK Tests / Python SDK Tests (sandbox) (push) Has been cancelled
SDK Tests / CLI Quality (push) Has been cancelled
SDK Tests / Kotlin SDK Quality And Tests (sandbox) (push) Has been cancelled
SDK Tests / Kotlin SDK Quality And Tests (code-interpreter) (push) Has been cancelled
SDK Tests / C# SDK Quality And Tests (code-interpreter) (push) Has been cancelled
SDK Tests / C# SDK Quality And Tests (sandbox) (push) Has been cancelled
SDK Tests / Go SDK Quality And Tests (push) Has been cancelled
318 lines
12 KiB
Python
318 lines
12 KiB
Python
# Copyright 2026 Alibaba Group Holding Ltd.
|
|
#
|
|
# Licensed under the Apache License, Version 2.0 (the "License");
|
|
# you may not use this file except in compliance with the License.
|
|
# You may obtain a copy of the License at
|
|
#
|
|
# http://www.apache.org/licenses/LICENSE-2.0
|
|
#
|
|
# Unless required by applicable law or agreed to in writing, software
|
|
# distributed under the License is distributed on an "AS IS" BASIS,
|
|
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
|
# See the License for the specific language governing permissions and
|
|
# limitations under the License.
|
|
|
|
"""
|
|
Windows-profile helpers specific to Docker sandbox provisioning.
|
|
|
|
Mixed into DockerSandboxService via DockerContainerOpsMixin.
|
|
"""
|
|
|
|
from __future__ import annotations
|
|
|
|
import io
|
|
import logging
|
|
import os
|
|
import tarfile
|
|
import time
|
|
from threading import Lock
|
|
from typing import Callable, Optional, TYPE_CHECKING
|
|
from uuid import uuid4
|
|
|
|
from docker.errors import DockerException
|
|
from fastapi import HTTPException, status
|
|
|
|
from opensandbox_server.services.constants import SandboxErrorCodes
|
|
from opensandbox_server.services.windows_common import is_windows_platform
|
|
|
|
if TYPE_CHECKING:
|
|
from opensandbox_server.api.schema import PlatformSpec
|
|
|
|
WINDOWS_REQUIRED_DEVICES = ("/dev/kvm", "/dev/net/tun")
|
|
WINDOWS_REQUIRED_CAP_ADD = ("NET_ADMIN", "NET_RAW")
|
|
WINDOWS_OEM_VOLUME_PREFIX = "opensandbox-win-oem"
|
|
|
|
|
|
def resolve_docker_platform(platform: Optional["PlatformSpec"]) -> Optional[str]:
|
|
"""Resolve Docker API ``platform`` argument for container create.
|
|
|
|
For windows profile (dockur/windows), the image itself is linux-based and
|
|
should not be forced to windows/* via Docker API platform pinning.
|
|
"""
|
|
if platform is None or is_windows_platform(platform):
|
|
return None
|
|
return f"{platform.os}/{platform.arch}"
|
|
|
|
|
|
def normalize_bootstrap_command(
|
|
bootstrap_command: list[str],
|
|
requested_windows_platform: bool,
|
|
) -> list[str]:
|
|
# For linux profile, normalize single-string command with spaces
|
|
# so bootstrap can exec reliably.
|
|
if requested_windows_platform:
|
|
return bootstrap_command
|
|
if len(bootstrap_command) != 1 or " " not in bootstrap_command[0]:
|
|
return bootstrap_command
|
|
|
|
import shlex
|
|
|
|
return shlex.split(bootstrap_command[0])
|
|
|
|
|
|
def validate_windows_runtime_prerequisites() -> None:
|
|
"""Validate host device paths required by dockur/windows runtime profile."""
|
|
missing = [device for device in WINDOWS_REQUIRED_DEVICES if not os.path.exists(device)]
|
|
if not missing:
|
|
return
|
|
raise HTTPException(
|
|
status_code=status.HTTP_400_BAD_REQUEST,
|
|
detail={
|
|
"code": SandboxErrorCodes.INVALID_PARAMETER,
|
|
"message": (
|
|
"Windows profile requires host devices to be present: "
|
|
f"{', '.join(missing)}."
|
|
),
|
|
},
|
|
)
|
|
|
|
|
|
def apply_windows_runtime_host_config_defaults(
|
|
host_config_kwargs: dict,
|
|
sandbox_id: str,
|
|
) -> dict:
|
|
"""Apply runtime defaults required by Windows profile."""
|
|
updated = dict(host_config_kwargs)
|
|
|
|
default_binds = [f"{WINDOWS_OEM_VOLUME_PREFIX}-{sandbox_id}:/oem:rw"]
|
|
existing_binds = list(updated.get("binds") or [])
|
|
updated["binds"] = existing_binds + default_binds
|
|
|
|
existing_devices = list(updated.get("devices") or [])
|
|
existing_devices.extend(WINDOWS_REQUIRED_DEVICES)
|
|
updated["devices"] = existing_devices
|
|
|
|
required_caps = set(WINDOWS_REQUIRED_CAP_ADD)
|
|
cap_drop = [cap for cap in (updated.get("cap_drop") or []) if cap not in required_caps]
|
|
if cap_drop:
|
|
updated["cap_drop"] = cap_drop
|
|
else:
|
|
updated.pop("cap_drop", None)
|
|
|
|
cap_add = set(updated.get("cap_add") or [])
|
|
cap_add.update(WINDOWS_REQUIRED_CAP_ADD)
|
|
updated["cap_add"] = sorted(cap_add)
|
|
|
|
return updated
|
|
|
|
|
|
def fetch_execd_install_bat(
|
|
*,
|
|
docker_client,
|
|
execd_image: str,
|
|
cache: dict[str, bytes],
|
|
cache_lock: Lock,
|
|
docker_operation: Callable[[str, Optional[str]], object],
|
|
logger: logging.Logger,
|
|
) -> bytes:
|
|
"""Fetch install.bat from execd image and memoize in caller-provided cache."""
|
|
cached = cache.get("install_bat")
|
|
if cached is not None:
|
|
return cached
|
|
|
|
with cache_lock:
|
|
cached = cache.get("install_bat")
|
|
if cached is not None:
|
|
return cached
|
|
|
|
container = None
|
|
try:
|
|
with docker_operation("execd install.bat cache create container", "execd-cache"):
|
|
container = docker_client.containers.create(
|
|
image=execd_image,
|
|
command=["tail", "-f", "/dev/null"],
|
|
name=f"sandbox-execd-installbat-{uuid4()}",
|
|
)
|
|
with docker_operation("execd install.bat cache start container", "execd-cache"):
|
|
container.start()
|
|
with docker_operation("execd install.bat cache read archive", "execd-cache"):
|
|
stream, _ = container.get_archive("/install.bat")
|
|
tar_blob = b"".join(stream)
|
|
with tarfile.open(fileobj=io.BytesIO(tar_blob), mode="r:*") as tar:
|
|
member = next(
|
|
(m for m in tar.getmembers() if m.isfile() and m.name.endswith("install.bat")),
|
|
None,
|
|
)
|
|
if member is None:
|
|
raise HTTPException(
|
|
status_code=status.HTTP_500_INTERNAL_SERVER_ERROR,
|
|
detail={
|
|
"code": SandboxErrorCodes.EXECD_DISTRIBUTION_FAILED,
|
|
"message": "install.bat was not found in execd image archive.",
|
|
},
|
|
)
|
|
extracted = tar.extractfile(member)
|
|
if extracted is None:
|
|
raise HTTPException(
|
|
status_code=status.HTTP_500_INTERNAL_SERVER_ERROR,
|
|
detail={
|
|
"code": SandboxErrorCodes.EXECD_DISTRIBUTION_FAILED,
|
|
"message": "Failed to extract install.bat from execd image archive.",
|
|
},
|
|
)
|
|
data = extracted.read()
|
|
except HTTPException:
|
|
raise
|
|
except DockerException as exc:
|
|
raise HTTPException(
|
|
status_code=status.HTTP_500_INTERNAL_SERVER_ERROR,
|
|
detail={
|
|
"code": SandboxErrorCodes.EXECD_DISTRIBUTION_FAILED,
|
|
"message": f"Failed to fetch install.bat from execd image: {str(exc)}",
|
|
},
|
|
) from exc
|
|
finally:
|
|
if container is not None:
|
|
try:
|
|
with docker_operation("execd install.bat cache cleanup container", "execd-cache"):
|
|
container.remove(force=True)
|
|
except DockerException as cleanup_exc:
|
|
logger.warning(
|
|
"Failed to cleanup temporary execd install.bat container: %s", cleanup_exc
|
|
)
|
|
|
|
cache["install_bat"] = data
|
|
return data
|
|
|
|
|
|
def fetch_execd_windows_binary(
|
|
*,
|
|
docker_client,
|
|
execd_image: str,
|
|
cache: dict[str, bytes],
|
|
cache_lock: Lock,
|
|
docker_operation: Callable[[str, Optional[str]], object],
|
|
logger: logging.Logger,
|
|
) -> bytes:
|
|
"""Fetch execd.exe from execd image and memoize in caller-provided cache."""
|
|
cached = cache.get("windows_execd_bin")
|
|
if cached is not None:
|
|
return cached
|
|
|
|
with cache_lock:
|
|
cached = cache.get("windows_execd_bin")
|
|
if cached is not None:
|
|
return cached
|
|
|
|
container = None
|
|
try:
|
|
with docker_operation("execd windows bin cache create container", "execd-cache"):
|
|
container = docker_client.containers.create(
|
|
image=execd_image,
|
|
command=["tail", "-f", "/dev/null"],
|
|
name=f"sandbox-execd-winbin-{uuid4()}",
|
|
)
|
|
with docker_operation("execd windows bin cache start container", "execd-cache"):
|
|
container.start()
|
|
with docker_operation("execd windows bin cache read archive", "execd-cache"):
|
|
stream, _ = container.get_archive("/execd.exe")
|
|
tar_blob = b"".join(stream)
|
|
with tarfile.open(fileobj=io.BytesIO(tar_blob), mode="r:*") as tar:
|
|
member = next(
|
|
(m for m in tar.getmembers() if m.isfile() and m.name.endswith("execd.exe")),
|
|
None,
|
|
)
|
|
if member is None:
|
|
raise HTTPException(
|
|
status_code=status.HTTP_500_INTERNAL_SERVER_ERROR,
|
|
detail={
|
|
"code": SandboxErrorCodes.EXECD_DISTRIBUTION_FAILED,
|
|
"message": "execd.exe was not found in execd image archive.",
|
|
},
|
|
)
|
|
extracted = tar.extractfile(member)
|
|
if extracted is None:
|
|
raise HTTPException(
|
|
status_code=status.HTTP_500_INTERNAL_SERVER_ERROR,
|
|
detail={
|
|
"code": SandboxErrorCodes.EXECD_DISTRIBUTION_FAILED,
|
|
"message": "Failed to extract execd.exe from execd image archive.",
|
|
},
|
|
)
|
|
data = extracted.read()
|
|
except HTTPException:
|
|
raise
|
|
except DockerException as exc:
|
|
raise HTTPException(
|
|
status_code=status.HTTP_500_INTERNAL_SERVER_ERROR,
|
|
detail={
|
|
"code": SandboxErrorCodes.EXECD_DISTRIBUTION_FAILED,
|
|
"message": f"Failed to fetch execd.exe from execd image: {str(exc)}",
|
|
},
|
|
) from exc
|
|
finally:
|
|
if container is not None:
|
|
try:
|
|
with docker_operation(
|
|
"execd windows bin cache cleanup container", "execd-cache"
|
|
):
|
|
container.remove(force=True)
|
|
except DockerException as cleanup_exc:
|
|
logger.warning(
|
|
"Failed to cleanup temporary execd windows bin container: %s", cleanup_exc
|
|
)
|
|
|
|
cache["windows_execd_bin"] = data
|
|
return data
|
|
|
|
|
|
def install_windows_oem_scripts(
|
|
*,
|
|
container,
|
|
sandbox_id: str,
|
|
install_bat_bytes: bytes,
|
|
execd_windows_bin_bytes: bytes,
|
|
ensure_directory: Callable[[object, str, Optional[str]], None],
|
|
docker_operation: Callable[[str, Optional[str]], object],
|
|
) -> None:
|
|
"""Install OEM script for dockur/windows:
|
|
- C:\\OEM\\install.bat from execd image
|
|
"""
|
|
ensure_directory(container, "/oem", sandbox_id)
|
|
|
|
tar_stream = io.BytesIO()
|
|
with tarfile.open(fileobj=tar_stream, mode="w") as tar:
|
|
install_script = tarfile.TarInfo(name="oem/install.bat")
|
|
install_script.mode = 0o644
|
|
install_script.size = len(install_bat_bytes)
|
|
install_script.mtime = int(time.time())
|
|
tar.addfile(install_script, io.BytesIO(install_bat_bytes))
|
|
|
|
execd_bin = tarfile.TarInfo(name="oem/execd.exe")
|
|
execd_bin.mode = 0o644
|
|
execd_bin.size = len(execd_windows_bin_bytes)
|
|
execd_bin.mtime = int(time.time())
|
|
tar.addfile(execd_bin, io.BytesIO(execd_windows_bin_bytes))
|
|
tar_stream.seek(0)
|
|
try:
|
|
with docker_operation("install windows OEM scripts", sandbox_id):
|
|
container.put_archive(path="/", data=tar_stream.getvalue())
|
|
except DockerException as exc:
|
|
raise HTTPException(
|
|
status_code=status.HTTP_500_INTERNAL_SERVER_ERROR,
|
|
detail={
|
|
"code": SandboxErrorCodes.BOOTSTRAP_INSTALL_FAILED,
|
|
"message": f"Failed to install windows OEM scripts: {str(exc)}",
|
|
},
|
|
) from exc
|