Merge pull request #339 from trycua/feat/BYOContainer

[Computer] Add Docker as a local VM provider
This commit is contained in:
ddupont
2025-08-14 10:35:24 -04:00
committed by GitHub
15 changed files with 1029 additions and 45 deletions

View File

@@ -45,34 +45,90 @@ This is a cloud container running the Computer Server. This is the easiest & saf
## cua local containers
cua provides local containers. This can be done using either the Lume CLI (macOS) or Docker CLI (Linux, Windows).
cua provides local containers using different providers depending on your host operating system:
### Lume (macOS Only):
<Tabs items={['Lume (macOS Only)', 'Windows Sandbox (Windows Only)', 'Docker (macOS, Windows, Linux)']}>
<Tab value="Lume (macOS Only)">
1. Install lume cli
1. Install lume cli
```bash
/bin/bash -c "$(curl -fsSL https://raw.githubusercontent.com/trycua/cua/main/libs/lume/scripts/install.sh)"
```
```bash
/bin/bash -c "$(curl -fsSL https://raw.githubusercontent.com/trycua/cua/main/libs/lume/scripts/install.sh)"
```
2. Start a local cua container
2. Start a local cua container
```bash
lume run macos-sequoia-cua:latest
```
```bash
lume run macos-sequoia-cua:latest
```
3. Connect with Computer
3. Connect with Computer
<Tabs items={['Python']}>
<Tab value="Python">
```python
from computer import Computer
computer = Computer(
os_type="macos",
provider_type="lume",
name="macos-sequoia-cua:latest"
)
await computer.run() # Connect to the container
await computer.run() # Launch & connect to the container
```
</Tab>
<Tab value="Windows Sandbox (Windows Only)">
1. Enable Windows Sandbox (requires Windows 10 Pro/Enterprise or Windows 11)
2. Install pywinsandbox dependency
```bash
pip install -U git+git://github.com/karkason/pywinsandbox.git
```
3. Windows Sandbox will be automatically configured when you run the CLI
```python
from computer import Computer
computer = Computer(
os_type="windows",
provider_type="winsandbox",
ephemeral=True # Windows Sandbox is always ephemeral
)
await computer.run() # Launch & connect to Windows Sandbox
```
</Tab>
<Tab value="Docker (macOS, Windows, Linux)">
1. Install Docker Desktop or Docker Engine
2. Build or pull the CUA Ubuntu container
```bash
# Option 1: Pull from Docker Hub
docker pull trycua/cua-ubuntu:latest
# Option 2: Build locally
cd libs/kasm
docker build -t cua-ubuntu:latest .
```
3. Connect with Computer
```python
from computer import Computer
computer = Computer(
os_type="linux",
provider_type="docker",
image="trycua/cua-ubuntu:latest",
name="my-cua-container"
)
await computer.run() # Launch & connect to Docker container
```
</Tab>
@@ -93,8 +149,8 @@ Connect with:
<Tab value="Python">
```python
computer = Computer(use_host_computer_server=True) await
computer.run() # Connect to the host desktop
computer = Computer(use_host_computer_server=True)
await computer.run() # Connect to the host desktop
```

View File

@@ -21,12 +21,62 @@ cua combines Computer (interface) + Agent (AI) for automating desktop apps. The
<Step>
## Create Your First cua Container
## Set Up Your Computer Environment
1. Go to [trycua.com/signin](https://www.trycua.com/signin)
2. Navigate to **Dashboard > Containers > Create Instance**
3. Create a **Medium, Ubuntu 22** container
4. Note your container name and API key
Choose how you want to run your cua computer. **Cloud containers are recommended** for the easiest setup:
<Tabs items={['☁️ Cloud (Recommended)', 'Lume (macOS Only)', 'Windows Sandbox (Windows Only)', 'Docker (Cross-Platform)']}>
<Tab value="☁️ Cloud (Recommended)">
**Easiest & safest way to get started**
1. Go to [trycua.com/signin](https://www.trycua.com/signin)
2. Navigate to **Dashboard > Containers > Create Instance**
3. Create a **Medium, Ubuntu 22** container
4. Note your container name and API key
Your cloud container will be automatically configured and ready to use.
</Tab>
<Tab value="Lume (macOS Only)">
1. Install lume cli
```bash
/bin/bash -c "$(curl -fsSL https://raw.githubusercontent.com/trycua/cua/main/libs/lume/scripts/install.sh)"
```
2. Start a local cua container
```bash
lume run macos-sequoia-cua:latest
```
</Tab>
<Tab value="Windows Sandbox (Windows Only)">
1. Enable Windows Sandbox (requires Windows 10 Pro/Enterprise or Windows 11)
2. Install pywinsandbox dependency
```bash
pip install -U git+git://github.com/karkason/pywinsandbox.git
```
3. Windows Sandbox will be automatically configured when you run the CLI
</Tab>
<Tab value="Docker (Cross-Platform)">
1. Install Docker Desktop or Docker Engine
2. Pull the CUA Ubuntu container
```bash
docker pull trycua/cua-ubuntu:latest
```
</Tab>
</Tabs>
</Step>

View File

@@ -20,12 +20,62 @@ cua combines Computer (interface) + Agent (AI) for automating desktop apps. Comp
<Step>
## Create Your First cua Container
## Set Up Your Computer Environment
1. Go to [trycua.com/signin](https://www.trycua.com/signin)
2. Navigate to **Dashboard > Containers > Create Instance**
3. Create a **Medium, Ubuntu 22** container
4. Note your container name and API key
Choose how you want to run your cua computer. **Cloud containers are recommended** for the easiest setup:
<Tabs items={['☁️ Cloud (Recommended)', 'Lume (macOS Only)', 'Windows Sandbox (Windows Only)', 'Docker (Cross-Platform)']}>
<Tab value="☁️ Cloud (Recommended)">
**Easiest & safest way to get started**
1. Go to [trycua.com/signin](https://www.trycua.com/signin)
2. Navigate to **Dashboard > Containers > Create Instance**
3. Create a **Medium, Ubuntu 22** container
4. Note your container name and API key
Your cloud container will be automatically configured and ready to use.
</Tab>
<Tab value="Lume (macOS Only)">
1. Install lume cli
```bash
/bin/bash -c "$(curl -fsSL https://raw.githubusercontent.com/trycua/cua/main/libs/lume/scripts/install.sh)"
```
2. Start a local cua container
```bash
lume run macos-sequoia-cua:latest
```
</Tab>
<Tab value="Windows Sandbox (Windows Only)">
1. Enable Windows Sandbox (requires Windows 10 Pro/Enterprise or Windows 11)
2. Install pywinsandbox dependency
```bash
pip install -U git+git://github.com/karkason/pywinsandbox.git
```
3. Windows Sandbox will be automatically configured when you run the CLI
</Tab>
<Tab value="Docker (Cross-Platform)">
1. Install Docker Desktop or Docker Engine
2. Pull the CUA Ubuntu container
```bash
docker pull trycua/cua-ubuntu:latest
```
</Tab>
</Tabs>
</Step>
@@ -35,9 +85,15 @@ cua combines Computer (interface) + Agent (AI) for automating desktop apps. Comp
<Tabs items={['Python', 'TypeScript']}>
<Tab value="Python">
```bash pip install "cua-agent[all]" cua-computer ```
```bash
pip install "cua-agent[all]" cua-computer
```
</Tab>
<Tab value="TypeScript">
```bash
npm install @trycua/computer
```
</Tab>
<Tab value="TypeScript">```bash npm install @trycua/computer ```</Tab>
</Tabs>
</Step>

View File

@@ -21,12 +21,62 @@ cua combines Computer (interface) + Agent (AI) for automating desktop apps. The
<Step>
## Create Your First cua Container
## Set Up Your Computer Environment
1. Go to [trycua.com/signin](https://www.trycua.com/signin)
2. Navigate to **Dashboard > Containers > Create Instance**
3. Create a **Medium, Ubuntu 22** container
4. Note your container name and API key
Choose how you want to run your cua computer. **Cloud containers are recommended** for the easiest setup:
<Tabs items={['☁️ Cloud (Recommended)', 'Lume (macOS Only)', 'Windows Sandbox (Windows Only)', 'Docker (Cross-Platform)']}>
<Tab value="☁️ Cloud (Recommended)">
**Easiest & safest way to get started**
1. Go to [trycua.com/signin](https://www.trycua.com/signin)
2. Navigate to **Dashboard > Containers > Create Instance**
3. Create a **Medium, Ubuntu 22** container
4. Note your container name and API key
Your cloud container will be automatically configured and ready to use.
</Tab>
<Tab value="Lume (macOS Only)">
1. Install lume cli
```bash
/bin/bash -c "$(curl -fsSL https://raw.githubusercontent.com/trycua/cua/main/libs/lume/scripts/install.sh)"
```
2. Start a local cua container
```bash
lume run macos-sequoia-cua:latest
```
</Tab>
<Tab value="Windows Sandbox (Windows Only)">
1. Enable Windows Sandbox (requires Windows 10 Pro/Enterprise or Windows 11)
2. Install pywinsandbox dependency
```bash
pip install -U git+git://github.com/karkason/pywinsandbox.git
```
3. Windows Sandbox will be automatically configured when you run the CLI
</Tab>
<Tab value="Docker (Cross-Platform)">
1. Install Docker Desktop or Docker Engine
2. Pull the CUA Ubuntu container
```bash
docker pull trycua/cua-ubuntu:latest
```
</Tab>
</Tabs>
</Step>

View File

@@ -0,0 +1,43 @@
import asyncio
from computer.providers.factory import VMProviderFactory
from computer import Computer, VMProviderType
import os
async def main():
# # Create docker provider
# provider = VMProviderFactory.create_provider(
# provider_type="docker",
# image="cua-ubuntu:latest", # Your CUA Ubuntu image
# port=8080,
# vnc_port=6901
# )
# # Run a container
# async with provider:
# vm_info = await provider.run_vm(
# image="cua-ubuntu:latest",
# name="my-cua-container",
# run_opts={
# "memory": "4GB",
# "cpu": 2,
# "vnc_port": 6901,
# "api_port": 8080
# }
# )
# print(vm_info)
computer = Computer(
os_type="linux",
provider_type=VMProviderType.DOCKER,
name="my-cua-container",
image="cua-ubuntu:latest",
)
await computer.run()
screenshot = await computer.interface.screenshot()
with open("screenshot_docker.png", "wb") as f:
f.write(screenshot)
if __name__ == "__main__":
asyncio.run(main())

37
libs/kasm/Dockerfile Normal file
View File

@@ -0,0 +1,37 @@
FROM kasmweb/core-ubuntu-jammy:1.17.0
USER root
ENV HOME=/home/kasm-default-profile
ENV STARTUPDIR=/dockerstartup
ENV INST_SCRIPTS=$STARTUPDIR/install
WORKDIR $HOME
######### Customize Container Here ###########
# Installing python, pip, and libraries
RUN apt-get update
RUN apt install -y wget build-essential libncursesw5-dev libssl-dev \
libsqlite3-dev tk-dev libgdbm-dev libc6-dev libbz2-dev libffi-dev zlib1g-dev
RUN add-apt-repository ppa:deadsnakes/ppa
RUN apt install -y python3.11 python3-pip python3-tk python3-dev \
gnome-screenshot wmctrl ffmpeg socat xclip
RUN pip install cua-computer-server
# Disable SSL requirement
RUN sed -i 's/require_ssl: true/require_ssl: false/g' /usr/share/kasmvnc/kasmvnc_defaults.yaml
RUN sed -i 's/-sslOnly//g' /dockerstartup/vnc_startup.sh
# Running the python script on startup
RUN echo "/usr/bin/python3 -m computer_server" > $STARTUPDIR/custom_startup.sh \
&& chmod +x $STARTUPDIR/custom_startup.sh
######### End Customizations ###########
RUN chown 1000:0 $HOME
RUN $STARTUPDIR/set_user_permission.sh $HOME
ENV HOME=/home/kasm-user
WORKDIR $HOME
RUN mkdir -p $HOME && chown -R 1000:0 $HOME
USER 1000

121
libs/kasm/README.md Normal file
View File

@@ -0,0 +1,121 @@
# CUA Ubuntu Container
Containerized virtual desktop for Computer-Using Agents (CUA). Utilizes Kasm's MIT-licensed Ubuntu XFCE container as a base with computer-server pre-installed.
## Features
- Ubuntu 22.04 (Jammy) with XFCE desktop environment
- Pre-installed computer-server for remote computer control
- VNC access for visual desktop interaction
- Python 3.11 with necessary libraries
- Screen capture tools (gnome-screenshot, wmctrl, ffmpeg)
- Clipboard utilities (xclip, socat)
## Usage
### Building the Container
```bash
docker build -t cua-ubuntu:latest .
```
### Pushing to Registry
To push the container to a Docker registry:
```bash
# Tag for your registry (replace with your registry URL)
docker tag cua-ubuntu:latest your-registry.com/cua-ubuntu:latest
# Push to registry
docker push your-registry.com/cua-ubuntu:latest
```
For Docker Hub:
```bash
# Tag for Docker Hub (replace 'trycua' with your Docker Hub username)
docker tag cua-ubuntu:latest trycua/cua-ubuntu:latest
# Login to Docker Hub
docker login
# Push to Docker Hub
docker push trycua/cua-ubuntu:latest
```
### Running the Container Manually
```bash
docker run --rm -it --shm-size=512m -p 6901:6901 -p 8000:8000 -e VNC_PW=password cua-ubuntu:latest
```
- **VNC Access**: Available at `http://localhost:6901` (username: `kasm-user`, password: `password`)
- **Computer Server API**: Available at `http://localhost:8000`
### Using with CUA Docker Provider
This container is designed to work with the CUA Docker provider for automated container management:
```python
from computer.providers.factory import VMProviderFactory
# Create docker provider
provider = VMProviderFactory.create_provider(
provider_type="docker",
image="cua-ubuntu:latest",
port=8000, # computer-server API port
noVNC_port=6901 # VNC port
)
# Run a container
async with provider:
vm_info = await provider.run_vm(
image="cua-ubuntu:latest",
name="my-cua-container",
run_opts={
"memory": "4GB",
"cpu": 2,
"vnc_port": 6901,
"api_port": 8000
}
)
```
## Container Configuration
### Ports
- **6901**: VNC web interface (noVNC)
- **8080**: Computer-server API endpoint
### Environment Variables
- `VNC_PW`: VNC password (default: "password")
- `DISPLAY`: X11 display (set to ":0")
### Volumes
- `/home/kasm-user/storage`: Persistent storage mount point
- `/home/kasm-user/shared`: Shared folder mount point
## Creating Snapshots
You can create a snapshot of the container at any time:
```bash
docker commit <container_id> cua-ubuntu-snapshot:latest
```
Then run the snapshot:
```bash
docker run --rm -it --shm-size=512m -p 6901:6901 -p 8080:8080 -e VNC_PW=password cua-ubuntu-snapshot:latest
```
## Integration with CUA System
This container integrates seamlessly with the CUA computer provider system:
- **Automatic Management**: Use the Docker provider for lifecycle management
- **Resource Control**: Configure memory, CPU, and storage limits
- **Network Access**: Automatic port mapping and IP detection
- **Storage Persistence**: Mount host directories for persistent data
- **Monitoring**: Real-time container status and health checking

View File

@@ -94,7 +94,7 @@ def print_action(action_type: str, details: Dict[str, Any], total_cost: float):
# Format action details
args_str = ""
if action_type == "click" and "x" in details and "y" in details:
args_str = f"_{details['button']}({details['x']}, {details['y']})"
args_str = f"_{details.get('button', 'left')}({details['x']}, {details['y']})"
elif action_type == "type" and "text" in details:
text = details["text"]
if len(text) > 50:

View File

@@ -39,6 +39,7 @@ global_agent = None
global_computer = None
SETTINGS_FILE = Path(".gradio_settings.json")
logging.basicConfig(level=logging.INFO)
import dotenv
if dotenv.load_dotenv():

View File

@@ -187,7 +187,7 @@ if __name__ == "__main__":
"""
<div style="display: flex; justify-content: center; margin-bottom: 0.5em">
<img alt="CUA Logo" style="width: 80px;"
src="https://github.com/trycua/cua/blob/main/img/logo_black.png?raw=true" />
src="https://github.com/trycua/cua/blob/main/img/logo_white.png?raw=true" />
</div>
"""
)
@@ -201,22 +201,33 @@ if __name__ == "__main__":
)
with gr.Accordion("Computer Configuration", open=True):
computer_os = gr.Radio(
choices=["macos", "linux", "windows"],
label="Operating System",
value="macos",
info="Select the operating system for the computer",
)
is_windows = platform.system().lower() == "windows"
is_mac = platform.system().lower() == "darwin"
providers = ["cloud", "localhost"]
providers = ["cloud", "localhost", "docker"]
if is_mac:
providers += ["lume"]
if is_windows:
providers += ["winsandbox"]
# Remove unavailable options
# MacOS is unavailable if Lume is not available
# Windows is unavailable if Winsandbox is not available
# Linux is always available
# This should be removed once we support macOS and Windows on the cloud provider
computer_choices = ["macos", "linux", "windows"]
if not is_mac or "lume" not in providers:
computer_choices.remove("macos")
if not is_windows or "winsandbox" not in providers:
computer_choices.remove("windows")
computer_os = gr.Radio(
choices=computer_choices,
label="Operating System",
value=computer_choices[0],
info="Select the operating system for the computer",
)
computer_provider = gr.Radio(
choices=providers,
label="Provider",

View File

@@ -43,7 +43,7 @@ class Computer:
cpu: str = "4",
os_type: OSType = "macos",
name: str = "",
image: str = "macos-sequoia-cua:latest",
image: Optional[str] = None,
shared_directories: Optional[List[str]] = None,
use_host_computer_server: bool = False,
verbosity: Union[int, LogLevel] = logging.INFO,
@@ -88,6 +88,12 @@ class Computer:
self.logger = Logger("computer", verbosity)
self.logger.info("Initializing Computer...")
if os_type == "macos":
image = "macos-sequoia-cua:latest"
elif os_type == "linux":
image = "trycua/cua-ubuntu:latest"
image = str(image)
# Store original parameters
self.image = image
self.port = port
@@ -301,6 +307,19 @@ class Computer:
storage=storage,
verbose=verbose,
ephemeral=ephemeral,
noVNC_port=noVNC_port,
)
elif self.provider_type == VMProviderType.DOCKER:
self.config.vm_provider = VMProviderFactory.create_provider(
self.provider_type,
port=port,
host=host,
storage=storage,
shared_path=shared_path,
image=image or "trycua/cua-ubuntu:latest",
verbose=verbose,
ephemeral=ephemeral,
noVNC_port=noVNC_port,
)
else:
raise ValueError(f"Unsupported provider type: {self.provider_type}")

View File

@@ -11,6 +11,7 @@ class VMProviderType(StrEnum):
LUMIER = "lumier"
CLOUD = "cloud"
WINSANDBOX = "winsandbox"
DOCKER = "docker"
UNKNOWN = "unknown"

View File

@@ -0,0 +1,13 @@
"""Docker provider for running containers with computer-server."""
from .provider import DockerProvider
# Check if Docker is available
try:
import subprocess
subprocess.run(["docker", "--version"], capture_output=True, check=True)
HAS_DOCKER = True
except (subprocess.SubprocessError, FileNotFoundError):
HAS_DOCKER = False
__all__ = ["DockerProvider", "HAS_DOCKER"]

View File

@@ -0,0 +1,502 @@
"""
Docker VM provider implementation.
This provider uses Docker containers running the CUA Ubuntu image to create
Linux VMs with computer-server. It handles VM lifecycle operations through Docker
commands and container management.
"""
import logging
import json
import asyncio
from typing import Dict, List, Optional, Any
import subprocess
import time
import re
from ..base import BaseVMProvider, VMProviderType
# Setup logging
logger = logging.getLogger(__name__)
# Check if Docker is available
try:
subprocess.run(["docker", "--version"], capture_output=True, check=True)
HAS_DOCKER = True
except (subprocess.SubprocessError, FileNotFoundError):
HAS_DOCKER = False
class DockerProvider(BaseVMProvider):
"""
Docker VM Provider implementation using Docker containers.
This provider uses Docker to run containers with the CUA Ubuntu image
that includes computer-server for remote computer use.
"""
def __init__(
self,
port: Optional[int] = 8000,
host: str = "localhost",
storage: Optional[str] = None,
shared_path: Optional[str] = None,
image: str = "trycua/cua-ubuntu:latest",
verbose: bool = False,
ephemeral: bool = False,
vnc_port: Optional[int] = 6901,
):
"""Initialize the Docker VM Provider.
Args:
port: Currently unused (VM provider port)
host: Hostname for the API server (default: localhost)
storage: Path for persistent VM storage
shared_path: Path for shared folder between host and container
image: Docker image to use (default: "trycua/cua-ubuntu:latest")
verbose: Enable verbose logging
ephemeral: Use ephemeral (temporary) storage
vnc_port: Port for VNC interface (default: 6901)
"""
self.host = host
self.api_port = 8000
self.vnc_port = vnc_port
self.ephemeral = ephemeral
# Handle ephemeral storage (temporary directory)
if ephemeral:
self.storage = "ephemeral"
else:
self.storage = storage
self.shared_path = shared_path
self.image = image
self.verbose = verbose
self._container_id = None
self._running_containers = {} # Track running containers by name
@property
def provider_type(self) -> VMProviderType:
"""Return the provider type."""
return VMProviderType.DOCKER
def _parse_memory(self, memory_str: str) -> str:
"""Parse memory string to Docker format.
Examples:
"8GB" -> "8g"
"1024MB" -> "1024m"
"512" -> "512m"
"""
if isinstance(memory_str, int):
return f"{memory_str}m"
if isinstance(memory_str, str):
# Extract number and unit
match = re.match(r"(\d+)([A-Za-z]*)", memory_str)
if match:
value, unit = match.groups()
unit = unit.upper()
if unit == "GB" or unit == "G":
return f"{value}g"
elif unit == "MB" or unit == "M" or unit == "":
return f"{value}m"
# Default fallback
logger.warning(f"Could not parse memory string '{memory_str}', using 4g default")
return "4g" # Default to 4GB
async def get_vm(self, name: str, storage: Optional[str] = None) -> Dict[str, Any]:
"""Get VM information by name.
Args:
name: Name of the VM to get information for
storage: Optional storage path override. If provided, this will be used
instead of the provider's default storage path.
Returns:
Dictionary with VM information including status, IP address, etc.
"""
try:
# Check if container exists and get its status
cmd = ["docker", "inspect", name]
result = subprocess.run(cmd, capture_output=True, text=True)
if result.returncode != 0:
# Container doesn't exist
return {
"name": name,
"status": "not_found",
"ip_address": None,
"ports": {},
"image": self.image,
"provider": "docker"
}
# Parse container info
container_info = json.loads(result.stdout)[0]
state = container_info["State"]
network_settings = container_info["NetworkSettings"]
# Determine status
if state["Running"]:
status = "running"
elif state["Paused"]:
status = "paused"
else:
status = "stopped"
# Get IP address
ip_address = network_settings.get("IPAddress", "")
if not ip_address and "Networks" in network_settings:
# Try to get IP from bridge network
for network_name, network_info in network_settings["Networks"].items():
if network_info.get("IPAddress"):
ip_address = network_info["IPAddress"]
break
# Get port mappings
ports = {}
if "Ports" in network_settings and network_settings["Ports"]:
# network_settings["Ports"] is a dict like:
# {'6901/tcp': [{'HostIp': '0.0.0.0', 'HostPort': '6901'}, ...], ...}
for container_port, port_mappings in network_settings["Ports"].items():
if port_mappings: # Check if there are any port mappings
# Take the first mapping (usually the IPv4 one)
for mapping in port_mappings:
if mapping.get("HostPort"):
ports[container_port] = mapping["HostPort"]
break # Use the first valid mapping
return {
"name": name,
"status": status,
"ip_address": ip_address or "127.0.0.1", # Use localhost if no IP
"ports": ports,
"image": container_info["Config"]["Image"],
"provider": "docker",
"container_id": container_info["Id"][:12], # Short ID
"created": container_info["Created"],
"started": state.get("StartedAt", ""),
}
except Exception as e:
logger.error(f"Error getting VM info for {name}: {e}")
import traceback
traceback.print_exc()
return {
"name": name,
"status": "error",
"error": str(e),
"provider": "docker"
}
async def list_vms(self) -> List[Dict[str, Any]]:
"""List all Docker containers managed by this provider."""
try:
# List all containers (running and stopped) with the CUA image
cmd = ["docker", "ps", "-a", "--filter", f"ancestor={self.image}", "--format", "json"]
result = subprocess.run(cmd, capture_output=True, text=True, check=True)
containers = []
if result.stdout.strip():
for line in result.stdout.strip().split('\n'):
if line.strip():
container_data = json.loads(line)
vm_info = await self.get_vm(container_data["Names"])
containers.append(vm_info)
return containers
except subprocess.CalledProcessError as e:
logger.error(f"Error listing containers: {e.stderr}")
return []
except Exception as e:
logger.error(f"Error listing VMs: {e}")
import traceback
traceback.print_exc()
return []
async def run_vm(self, image: str, name: str, run_opts: Dict[str, Any], storage: Optional[str] = None) -> Dict[str, Any]:
"""Run a VM with the given options.
Args:
image: Name/tag of the Docker image to use
name: Name of the container to run
run_opts: Options for running the VM, including:
- memory: Memory limit (e.g., "4GB", "2048MB")
- cpu: CPU limit (e.g., 2 for 2 cores)
- vnc_port: Specific port for VNC interface
- api_port: Specific port for computer-server API
Returns:
Dictionary with VM status information
"""
try:
# Check if container already exists
existing_vm = await self.get_vm(name, storage)
if existing_vm["status"] == "running":
logger.info(f"Container {name} is already running")
return existing_vm
elif existing_vm["status"] in ["stopped", "paused"]:
# Start existing container
logger.info(f"Starting existing container {name}")
start_cmd = ["docker", "start", name]
result = subprocess.run(start_cmd, capture_output=True, text=True, check=True)
# Wait for container to be ready
await self._wait_for_container_ready(name)
return await self.get_vm(name, storage)
# Use provided image or default
docker_image = image if image != "default" else self.image
# Build docker run command
cmd = ["docker", "run", "-d", "--name", name]
# Add memory limit if specified
if "memory" in run_opts:
memory_limit = self._parse_memory(run_opts["memory"])
cmd.extend(["--memory", memory_limit])
# Add CPU limit if specified
if "cpu" in run_opts:
cpu_count = str(run_opts["cpu"])
cmd.extend(["--cpus", cpu_count])
# Add port mappings
vnc_port = run_opts.get("vnc_port", self.vnc_port)
api_port = run_opts.get("api_port", self.api_port)
if vnc_port:
cmd.extend(["-p", f"{vnc_port}:6901"]) # VNC port
if api_port:
cmd.extend(["-p", f"{api_port}:8000"]) # computer-server API port
# Add volume mounts if storage is specified
storage_path = storage or self.storage
if storage_path and storage_path != "ephemeral":
# Mount storage directory
cmd.extend(["-v", f"{storage_path}:/home/kasm-user/storage"])
# Add shared path if specified
if self.shared_path:
cmd.extend(["-v", f"{self.shared_path}:/home/kasm-user/shared"])
# Add environment variables
cmd.extend(["-e", "VNC_PW=password"]) # Set VNC password
cmd.extend(["-e", "VNCOPTIONS=-disableBasicAuth"]) # Disable VNC basic auth
# Add the image
cmd.append(docker_image)
logger.info(f"Running Docker container with command: {' '.join(cmd)}")
# Run the container
result = subprocess.run(cmd, capture_output=True, text=True, check=True)
container_id = result.stdout.strip()
logger.info(f"Container {name} started with ID: {container_id[:12]}")
# Store container info
self._container_id = container_id
self._running_containers[name] = container_id
# Wait for container to be ready
await self._wait_for_container_ready(name)
# Return VM info
vm_info = await self.get_vm(name, storage)
vm_info["container_id"] = container_id[:12]
return vm_info
except subprocess.CalledProcessError as e:
error_msg = f"Failed to run container {name}: {e.stderr}"
logger.error(error_msg)
return {
"name": name,
"status": "error",
"error": error_msg,
"provider": "docker"
}
except Exception as e:
error_msg = f"Error running VM {name}: {e}"
logger.error(error_msg)
return {
"name": name,
"status": "error",
"error": error_msg,
"provider": "docker"
}
async def _wait_for_container_ready(self, container_name: str, timeout: int = 60) -> bool:
"""Wait for the Docker container to be fully ready.
Args:
container_name: Name of the Docker container to check
timeout: Maximum time to wait in seconds (default: 60 seconds)
Returns:
True if the container is running and ready
"""
logger.info(f"Waiting for container {container_name} to be ready...")
start_time = time.time()
while time.time() - start_time < timeout:
try:
# Check if container is running
vm_info = await self.get_vm(container_name)
if vm_info["status"] == "running":
logger.info(f"Container {container_name} is running")
# Additional check: try to connect to computer-server API
# This is optional - we'll just wait a bit more for services to start
await asyncio.sleep(5)
return True
except Exception as e:
logger.debug(f"Container {container_name} not ready yet: {e}")
await asyncio.sleep(2)
logger.warning(f"Container {container_name} did not become ready within {timeout} seconds")
return False
async def stop_vm(self, name: str, storage: Optional[str] = None) -> Dict[str, Any]:
"""Stop a running VM by stopping the Docker container."""
try:
logger.info(f"Stopping container {name}")
# Stop the container
cmd = ["docker", "stop", name]
result = subprocess.run(cmd, capture_output=True, text=True, check=True)
# Remove from running containers tracking
if name in self._running_containers:
del self._running_containers[name]
logger.info(f"Container {name} stopped successfully")
return {
"name": name,
"status": "stopped",
"message": "Container stopped successfully",
"provider": "docker"
}
except subprocess.CalledProcessError as e:
error_msg = f"Failed to stop container {name}: {e.stderr}"
logger.error(error_msg)
return {
"name": name,
"status": "error",
"error": error_msg,
"provider": "docker"
}
except Exception as e:
error_msg = f"Error stopping VM {name}: {e}"
logger.error(error_msg)
return {
"name": name,
"status": "error",
"error": error_msg,
"provider": "docker"
}
async def update_vm(self, name: str, update_opts: Dict[str, Any], storage: Optional[str] = None) -> Dict[str, Any]:
"""Update VM configuration.
Note: Docker containers cannot be updated while running.
This method will return an error suggesting to recreate the container.
"""
return {
"name": name,
"status": "error",
"error": "Docker containers cannot be updated while running. Please stop and recreate the container with new options.",
"provider": "docker"
}
async def get_ip(self, name: str, storage: Optional[str] = None, retry_delay: int = 2) -> str:
"""Get the IP address of a VM, waiting indefinitely until it's available.
Args:
name: Name of the VM to get the IP for
storage: Optional storage path override
retry_delay: Delay between retries in seconds (default: 2)
Returns:
IP address of the VM when it becomes available
"""
logger.info(f"Getting IP address for container {name}")
total_attempts = 0
while True:
total_attempts += 1
try:
vm_info = await self.get_vm(name, storage)
if vm_info["status"] == "error":
raise Exception(f"VM is in error state: {vm_info.get('error', 'Unknown error')}")
# TODO: for now, return localhost
# it seems the docker container is not accessible from the host
# on WSL2, unless you port forward? not sure
if True:
logger.warning("Overriding container IP with localhost")
return "localhost"
# Check if we got a valid IP
ip = vm_info.get("ip_address", None)
if ip and ip != "unknown" and not ip.startswith("0.0.0.0"):
logger.info(f"Got valid container IP address: {ip}")
return ip
# For Docker containers, we can also use localhost if ports are mapped
if vm_info["status"] == "running" and vm_info.get("ports"):
logger.info(f"Container is running with port mappings, using localhost")
return "127.0.0.1"
# Check the container status
status = vm_info.get("status", "unknown")
if status == "stopped":
logger.info(f"Container status is {status}, but still waiting for it to start")
elif status != "running":
logger.info(f"Container is not running yet (status: {status}). Waiting...")
else:
logger.info("Container is running but no valid IP address yet. Waiting...")
except Exception as e:
logger.warning(f"Error getting container {name} IP: {e}, continuing to wait...")
# Wait before next retry
await asyncio.sleep(retry_delay)
# Add progress log every 10 attempts
if total_attempts % 10 == 0:
logger.info(f"Still waiting for container {name} IP after {total_attempts} attempts...")
async def __aenter__(self):
"""Async context manager entry."""
logger.debug("Entering DockerProvider context")
return self
async def __aexit__(self, exc_type, exc_val, exc_tb):
"""Async context manager exit.
This method handles cleanup of running containers if needed.
"""
logger.debug(f"Exiting DockerProvider context, handling exceptions: {exc_type}")
try:
# Optionally stop running containers on context exit
# For now, we'll leave containers running as they might be needed
# Users can manually stop them if needed
pass
except Exception as e:
logger.error(f"Error during DockerProvider cleanup: {e}")
if exc_type is None:
raise
return False

View File

@@ -134,5 +134,29 @@ class VMProviderFactory:
"pywinsandbox is required for WinSandboxProvider. "
"Please install it with 'pip install -U git+https://github.com/karkason/pywinsandbox.git'"
) from e
elif provider_type == VMProviderType.DOCKER:
try:
from .docker import DockerProvider, HAS_DOCKER
if not HAS_DOCKER:
raise ImportError(
"Docker is required for DockerProvider. "
"Please install Docker and ensure it is running."
)
return DockerProvider(
port=port,
host=host,
storage=storage,
shared_path=shared_path,
image=image or "trycua/cua-ubuntu:latest",
verbose=verbose,
ephemeral=ephemeral,
vnc_port=noVNC_port
)
except ImportError as e:
logger.error(f"Failed to import DockerProvider: {e}")
raise ImportError(
"Docker is required for DockerProvider. "
"Please install Docker and ensure it is running."
) from e
else:
raise ValueError(f"Unsupported provider type: {provider_type}")