mirror of
https://github.com/trycua/computer.git
synced 2026-05-12 11:29:41 -05:00
Add initial VM Provider
This commit is contained in:
Vendored
+1
-1
@@ -105,7 +105,7 @@
|
||||
"${workspaceFolder:cua-root}/libs/som",
|
||||
"${workspaceFolder:cua-root}/libs/pylume"
|
||||
],
|
||||
"python.languageServer": "Pylance",
|
||||
"python.languageServer": "None",
|
||||
"[python]": {
|
||||
"editor.formatOnSave": true,
|
||||
"editor.defaultFormatter": "ms-python.black-formatter",
|
||||
|
||||
@@ -20,7 +20,7 @@ for path in pythonpath.split(":"):
|
||||
sys.path.append(path)
|
||||
print(f"Added to sys.path: {path}")
|
||||
|
||||
from computer.computer import Computer
|
||||
from computer import Computer, VMProviderType
|
||||
from computer.logger import LogLevel
|
||||
from computer.utils import get_image_size
|
||||
|
||||
@@ -37,6 +37,7 @@ async def main():
|
||||
os_type="macos",
|
||||
verbosity=LogLevel.NORMAL, # Use QUIET to suppress most logs
|
||||
use_host_computer_server=False,
|
||||
provider_type=VMProviderType.LUME, # Explicitly use the Lume provider
|
||||
)
|
||||
try:
|
||||
await computer.run()
|
||||
|
||||
@@ -42,6 +42,10 @@ except Exception as e:
|
||||
# Other issues with telemetry
|
||||
logger.warning(f"Error initializing telemetry: {e}")
|
||||
|
||||
# Core components
|
||||
from .computer import Computer
|
||||
|
||||
__all__ = ["Computer"]
|
||||
# Provider components
|
||||
from .providers.base import VMProviderType
|
||||
|
||||
__all__ = ["Computer", "VMProviderType"]
|
||||
|
||||
@@ -1,6 +1,4 @@
|
||||
from typing import Optional, List, Literal, Dict, Any, Union, TYPE_CHECKING, cast
|
||||
from pylume import PyLume
|
||||
from pylume.models import VMRunOpts, VMUpdateOpts, ImageRef, SharedDirectory, VMStatus
|
||||
import asyncio
|
||||
from .models import Computer as ComputerConfig, Display
|
||||
from .interface.factory import InterfaceFactory
|
||||
@@ -14,13 +12,12 @@ import logging
|
||||
from .telemetry import record_computer_initialization
|
||||
import os
|
||||
|
||||
# Import provider related modules
|
||||
from .providers.base import VMProviderType
|
||||
from .providers.factory import VMProviderFactory
|
||||
|
||||
OSType = Literal["macos", "linux"]
|
||||
|
||||
# Import BaseComputerInterface for type annotations
|
||||
if TYPE_CHECKING:
|
||||
from .interface.base import BaseComputerInterface
|
||||
|
||||
|
||||
class Computer:
|
||||
"""Computer is the main class for interacting with the computer."""
|
||||
|
||||
@@ -36,8 +33,10 @@ class Computer:
|
||||
use_host_computer_server: bool = False,
|
||||
verbosity: Union[int, LogLevel] = logging.INFO,
|
||||
telemetry_enabled: bool = True,
|
||||
provider_type: Union[str, VMProviderType] = VMProviderType.LUME,
|
||||
port: Optional[int] = 3000,
|
||||
host: str = os.environ.get("PYLUME_HOST", "localhost"),
|
||||
storage_path: Optional[str] = None,
|
||||
):
|
||||
"""Initialize a new Computer instance.
|
||||
|
||||
@@ -57,8 +56,11 @@ class Computer:
|
||||
verbosity: Logging level (standard Python logging levels: logging.DEBUG, logging.INFO, etc.)
|
||||
LogLevel enum values are still accepted for backward compatibility
|
||||
telemetry_enabled: Whether to enable telemetry tracking. Defaults to True.
|
||||
port: Optional port to use for the PyLume server
|
||||
host: Host to use for PyLume connections (e.g. "localhost", "host.docker.internal")
|
||||
provider_type: The VM provider type to use (lume, qemu, cloud)
|
||||
port: Optional port to use for the VM provider server
|
||||
host: Host to use for VM provider connections (e.g. "localhost", "host.docker.internal")
|
||||
bin_path: Optional path to the VM provider binary
|
||||
storage_path: Optional path to store VM data
|
||||
"""
|
||||
|
||||
self.logger = Logger("cua.computer", verbosity)
|
||||
@@ -69,6 +71,8 @@ class Computer:
|
||||
self.port = port
|
||||
self.host = host
|
||||
self.os_type = os_type
|
||||
self.provider_type = provider_type
|
||||
self.storage_path = storage_path
|
||||
|
||||
# Store telemetry preference
|
||||
self._telemetry_enabled = telemetry_enabled
|
||||
@@ -116,25 +120,17 @@ class Computer:
|
||||
memory=memory,
|
||||
cpu=cpu,
|
||||
)
|
||||
# Initialize PyLume but don't start the server yet - we'll do that in run()
|
||||
self.config.pylume = PyLume(
|
||||
debug=(self.verbosity == LogLevel.DEBUG),
|
||||
port=3000,
|
||||
use_existing_server=False,
|
||||
server_start_timeout=120, # Increase timeout to 2 minutes
|
||||
)
|
||||
# Initialize VM provider but don't start it yet - we'll do that in run()
|
||||
self.config.vm_provider = None # Will be initialized in run()
|
||||
|
||||
# Store shared directories config
|
||||
self.shared_directories = shared_directories or []
|
||||
|
||||
# Placeholder for VM provider context manager
|
||||
self._provider_context = None
|
||||
|
||||
# Initialize with proper typing - None at first, will be set in run()
|
||||
self._interface = None
|
||||
self.os = os
|
||||
self.shared_paths = []
|
||||
if shared_directories:
|
||||
for path in shared_directories:
|
||||
abs_path = os.path.abspath(os.path.expanduser(path))
|
||||
if not os.path.exists(abs_path):
|
||||
raise ValueError(f"Shared directory does not exist: {path}")
|
||||
self.shared_paths.append(abs_path)
|
||||
self._pylume_context = None
|
||||
self.use_host_computer_server = use_host_computer_server
|
||||
|
||||
# Record initialization in telemetry (if enabled)
|
||||
@@ -164,7 +160,7 @@ class Computer:
|
||||
# We could add cleanup here if needed in the future
|
||||
pass
|
||||
|
||||
async def run(self) -> None:
|
||||
async def run(self) -> Optional[str]:
|
||||
"""Initialize the VM and computer interface."""
|
||||
if TYPE_CHECKING:
|
||||
from .interface.base import BaseComputerInterface
|
||||
@@ -199,74 +195,99 @@ class Computer:
|
||||
else:
|
||||
# Start or connect to VM
|
||||
self.logger.info(f"Starting VM: {self.image}")
|
||||
if not self._pylume_context:
|
||||
if not self._provider_context:
|
||||
try:
|
||||
self.logger.verbose("Initializing PyLume context...")
|
||||
provider_type_name = self.provider_type.name if isinstance(self.provider_type, VMProviderType) else self.provider_type
|
||||
self.logger.verbose(f"Initializing {provider_type_name} provider context...")
|
||||
|
||||
# Configure PyLume based on initialization parameters
|
||||
pylume_kwargs = {
|
||||
"debug": self.verbosity <= LogLevel.DEBUG,
|
||||
"server_start_timeout": 120, # Increase timeout to 2 minutes
|
||||
# Configure provider based on initialization parameters
|
||||
provider_kwargs = {
|
||||
"storage_path": self.storage_path,
|
||||
"verbose": self.verbosity >= LogLevel.DEBUG,
|
||||
}
|
||||
|
||||
# Add port if specified
|
||||
if hasattr(self, "port") and self.port is not None:
|
||||
pylume_kwargs["port"] = self.port
|
||||
self.logger.verbose(f"Using specified port for PyLume: {self.port}")
|
||||
# Set port if specified
|
||||
if self.port is not None:
|
||||
provider_kwargs["port"] = self.port
|
||||
self.logger.verbose(f"Using specified port for provider: {self.port}")
|
||||
|
||||
# Add host if specified
|
||||
if hasattr(self, "host") and self.host != "localhost":
|
||||
pylume_kwargs["host"] = self.host
|
||||
self.logger.verbose(f"Using specified host for PyLume: {self.host}")
|
||||
# Set host if specified
|
||||
if self.host:
|
||||
provider_kwargs["host"] = self.host
|
||||
self.logger.verbose(f"Using specified host for provider: {self.host}")
|
||||
|
||||
# Create PyLume instance with configured parameters
|
||||
self.config.pylume = PyLume(**pylume_kwargs)
|
||||
|
||||
self._pylume_context = await self.config.pylume.__aenter__() # type: ignore[attr-defined]
|
||||
self.logger.verbose("PyLume context initialized successfully")
|
||||
# Create VM provider instance with configured parameters
|
||||
try:
|
||||
self.config.vm_provider = VMProviderFactory.create_provider(
|
||||
self.provider_type, **provider_kwargs
|
||||
)
|
||||
self._provider_context = await self.config.vm_provider.__aenter__()
|
||||
self.logger.verbose("VM provider context initialized successfully")
|
||||
except ImportError as ie:
|
||||
self.logger.error(f"Failed to import provider dependencies: {ie}")
|
||||
if str(ie).find("lume") >= 0:
|
||||
self.logger.error("Please install with: pip install cua-computer[lume]")
|
||||
elif str(ie).find("qemu") >= 0:
|
||||
self.logger.error("Please install with: pip install cua-computer[qemu]")
|
||||
elif str(ie).find("cloud") >= 0:
|
||||
self.logger.error("Please install with: pip install cua-computer[cloud]")
|
||||
raise
|
||||
except Exception as e:
|
||||
self.logger.error(f"Failed to initialize PyLume context: {e}")
|
||||
raise RuntimeError(f"Failed to initialize PyLume: {e}")
|
||||
self.logger.error(f"Failed to initialize provider context: {e}")
|
||||
raise RuntimeError(f"Failed to initialize VM provider: {e}")
|
||||
|
||||
# Try to get the VM, if it doesn't exist, return an error
|
||||
# Check if VM exists or create it
|
||||
try:
|
||||
vm = await self.config.pylume.get_vm(self.config.name) # type: ignore[attr-defined]
|
||||
if self.config.vm_provider is None:
|
||||
raise RuntimeError(f"VM provider not initialized for {self.config.name}")
|
||||
|
||||
vm = await self.config.vm_provider.get_vm(self.config.name)
|
||||
self.logger.verbose(f"Found existing VM: {self.config.name}")
|
||||
except Exception as e:
|
||||
self.logger.error(f"VM not found: {self.config.name}")
|
||||
self.logger.error(
|
||||
f"Please pull the VM first with lume pull macos-sequoia-cua-sparse:latest: {e}"
|
||||
)
|
||||
self.logger.error(f"Error: {e}")
|
||||
raise RuntimeError(
|
||||
f"VM not found: {self.config.name}. Please pull the VM first."
|
||||
f"VM {self.config.name} could not be found or created."
|
||||
)
|
||||
|
||||
# Convert paths to SharedDirectory objects
|
||||
shared_directories = []
|
||||
for path in self.shared_paths:
|
||||
# Convert paths to dictionary format for shared directories
|
||||
shared_dirs = []
|
||||
for path in self.shared_directories:
|
||||
self.logger.verbose(f"Adding shared directory: {path}")
|
||||
shared_directories.append(
|
||||
SharedDirectory(host_path=path) # type: ignore[arg-type]
|
||||
)
|
||||
path = os.path.abspath(os.path.expanduser(path))
|
||||
if not os.path.exists(path):
|
||||
self.logger.warning(f"Shared directory does not exist: {path}")
|
||||
continue
|
||||
shared_dirs.append({"host_path": path, "vm_path": path})
|
||||
|
||||
# Run with shared directories
|
||||
self.logger.info(f"Starting VM {self.config.name}...")
|
||||
run_opts = VMRunOpts(
|
||||
no_display=False, # type: ignore[arg-type]
|
||||
shared_directories=shared_directories, # type: ignore[arg-type]
|
||||
)
|
||||
# Create VM run options with specs from config
|
||||
# Account for optional shared directories
|
||||
run_opts = {
|
||||
"cpu": int(self.config.cpu),
|
||||
"memory": self.config.memory,
|
||||
"display": {
|
||||
"width": self.config.display.width,
|
||||
"height": self.config.display.height
|
||||
}
|
||||
}
|
||||
|
||||
if shared_dirs:
|
||||
run_opts["shared_directories"] = shared_dirs
|
||||
|
||||
# Log the run options for debugging
|
||||
self.logger.info(f"VM run options: {vars(run_opts)}")
|
||||
self.logger.info(f"VM run options: {run_opts}")
|
||||
|
||||
# Log the equivalent curl command for debugging
|
||||
payload = json.dumps({"noDisplay": False, "sharedDirectories": []})
|
||||
curl_cmd = f"curl -X POST 'http://localhost:3000/lume/vms/{self.config.name}/run' -H 'Content-Type: application/json' -d '{payload}'"
|
||||
self.logger.info(f"Equivalent curl command:")
|
||||
self.logger.info(f"{curl_cmd}")
|
||||
# self.logger.info(f"Equivalent curl command:")
|
||||
# self.logger.info(f"{curl_cmd}")
|
||||
|
||||
try:
|
||||
response = await self.config.pylume.run_vm(self.config.name, run_opts) # type: ignore[attr-defined]
|
||||
if self.config.vm_provider is None:
|
||||
raise RuntimeError(f"VM provider not initialized for {self.config.name}")
|
||||
|
||||
response = await self.config.vm_provider.run_vm(self.config.name, run_opts)
|
||||
self.logger.info(f"VM run response: {response if response else 'None'}")
|
||||
except Exception as run_error:
|
||||
self.logger.error(f"Failed to run VM: {run_error}")
|
||||
@@ -275,11 +296,14 @@ class Computer:
|
||||
# Wait for VM to be ready with required properties
|
||||
self.logger.info("Waiting for VM to be ready...")
|
||||
try:
|
||||
vm = await self.wait_vm_ready()
|
||||
if not vm or not vm.ip_address: # type: ignore[attr-defined]
|
||||
ip = await self.get_ip()
|
||||
if ip:
|
||||
self.logger.info(f"VM is ready with IP: {ip}")
|
||||
# Store the IP address for later use instead of returning early
|
||||
ip_address = ip
|
||||
else:
|
||||
# If no IP was found, try to raise a helpful error
|
||||
raise RuntimeError(f"VM {self.config.name} failed to get IP address")
|
||||
ip_address = vm.ip_address # type: ignore[attr-defined]
|
||||
self.logger.info(f"VM is ready with IP: {ip_address}")
|
||||
except Exception as wait_error:
|
||||
self.logger.error(f"Error waiting for VM: {wait_error}")
|
||||
raise RuntimeError(f"VM failed to become ready: {wait_error}")
|
||||
@@ -356,15 +380,18 @@ class Computer:
|
||||
else:
|
||||
self._interface.close()
|
||||
|
||||
if not self.use_host_computer_server and self._pylume_context:
|
||||
if not self.use_host_computer_server and self._provider_context:
|
||||
try:
|
||||
self.logger.info(f"Stopping VM {self.config.name}...")
|
||||
await self.config.pylume.stop_vm(self.config.name) # type: ignore[attr-defined]
|
||||
if self.config.vm_provider is not None:
|
||||
await self.config.vm_provider.stop_vm(self.config.name)
|
||||
except Exception as e:
|
||||
self.logger.verbose(f"Error stopping VM: {e}") # VM might already be stopped
|
||||
self.logger.verbose("Closing PyLume context...")
|
||||
await self.config.pylume.__aexit__(None, None, None) # type: ignore[attr-defined]
|
||||
self._pylume_context = None
|
||||
self.logger.error(f"Error stopping VM: {e}")
|
||||
|
||||
self.logger.verbose("Closing VM provider context...")
|
||||
if self.config.vm_provider is not None:
|
||||
await self.config.vm_provider.__aexit__(None, None, None)
|
||||
self._provider_context = None
|
||||
self.logger.info("Computer stopped")
|
||||
except Exception as e:
|
||||
self.logger.debug(
|
||||
@@ -384,7 +411,7 @@ class Computer:
|
||||
ip = await self.config.get_ip()
|
||||
return ip or "unknown" # Return "unknown" if ip is None
|
||||
|
||||
async def wait_vm_ready(self) -> Optional[Union[Dict[str, Any], "VMStatus"]]:
|
||||
async def wait_vm_ready(self) -> Optional[Dict[str, Any]]:
|
||||
"""Wait for VM to be ready with an IP address.
|
||||
|
||||
Returns:
|
||||
@@ -407,7 +434,7 @@ class Computer:
|
||||
|
||||
try:
|
||||
# Keep polling for VM info
|
||||
vm = await self.config.pylume.get_vm(self.config.name) # type: ignore[attr-defined]
|
||||
vm = await self.config.vm_provider.get_vm(self.config.name)
|
||||
|
||||
# Log full VM properties for debugging (every 30 attempts)
|
||||
if attempts % 30 == 0:
|
||||
@@ -447,10 +474,11 @@ class Computer:
|
||||
self.logger.error(f"Persistent error getting VM status: {str(e)}")
|
||||
self.logger.info("Trying to get VM list for debugging...")
|
||||
try:
|
||||
vms = await self.config.pylume.list_vms() # type: ignore[attr-defined]
|
||||
self.logger.info(
|
||||
f"Available VMs: {[vm.name for vm in vms if hasattr(vm, 'name')]}"
|
||||
)
|
||||
if self.config.vm_provider is not None:
|
||||
vms = await self.config.vm_provider.list_vms()
|
||||
self.logger.info(
|
||||
f"Available VMs: {[getattr(vm, 'name', None) for vm in vms if hasattr(vm, 'name')]}"
|
||||
)
|
||||
except Exception as list_error:
|
||||
self.logger.error(f"Failed to list VMs: {str(list_error)}")
|
||||
|
||||
@@ -462,9 +490,14 @@ class Computer:
|
||||
|
||||
# Try to get final VM status for debugging
|
||||
try:
|
||||
vm = await self.config.pylume.get_vm(self.config.name) # type: ignore[attr-defined]
|
||||
status = getattr(vm, "status", "unknown") if vm else "unknown"
|
||||
ip = getattr(vm, "ip_address", None) if vm else None
|
||||
if self.config.vm_provider is not None:
|
||||
vm = await self.config.vm_provider.get_vm(self.config.name)
|
||||
# VMStatus is a Pydantic model with attributes, not a dictionary
|
||||
status = vm.status if vm else "unknown"
|
||||
ip = vm.ip_address if vm else None
|
||||
else:
|
||||
status = "unknown"
|
||||
ip = None
|
||||
self.logger.error(f"Final VM status: {status}, IP: {ip}")
|
||||
except Exception as e:
|
||||
self.logger.error(f"Failed to get final VM status: {str(e)}")
|
||||
@@ -478,10 +511,14 @@ class Computer:
|
||||
self.logger.info(
|
||||
f"Updating VM settings: CPU={cpu or self.config.cpu}, Memory={memory or self.config.memory}"
|
||||
)
|
||||
update_opts = VMUpdateOpts(
|
||||
cpu=cpu or int(self.config.cpu), memory=memory or self.config.memory
|
||||
)
|
||||
await self.config.pylume.update_vm(self.config.image, update_opts) # type: ignore[attr-defined]
|
||||
update_opts = {
|
||||
"cpu": cpu or int(self.config.cpu),
|
||||
"memory": memory or self.config.memory
|
||||
}
|
||||
if self.config.vm_provider is not None:
|
||||
await self.config.vm_provider.update_vm(self.config.name, update_opts)
|
||||
else:
|
||||
raise RuntimeError("VM provider not initialized")
|
||||
|
||||
def get_screenshot_size(self, screenshot: bytes) -> Dict[str, int]:
|
||||
"""Get the dimensions of a screenshot.
|
||||
|
||||
@@ -1,8 +1,10 @@
|
||||
"""Models for computer configuration."""
|
||||
|
||||
from dataclasses import dataclass
|
||||
from typing import Optional
|
||||
from pylume import PyLume
|
||||
from typing import Optional, Any, Dict
|
||||
|
||||
# Import base provider interface
|
||||
from .providers.base import BaseVMProvider
|
||||
|
||||
@dataclass
|
||||
class Display:
|
||||
@@ -26,10 +28,15 @@ class Computer:
|
||||
display: Display
|
||||
memory: str
|
||||
cpu: str
|
||||
pylume: Optional[PyLume] = None
|
||||
vm_provider: Optional[BaseVMProvider] = None
|
||||
|
||||
# @property # Remove the property decorator
|
||||
async def get_ip(self) -> Optional[str]:
|
||||
"""Get the IP address of the VM."""
|
||||
vm = await self.pylume.get_vm(self.name) # type: ignore[attr-defined]
|
||||
return vm.ip_address if vm else None
|
||||
if not self.vm_provider:
|
||||
return None
|
||||
|
||||
vm = await self.vm_provider.get_vm(self.name)
|
||||
# PyLume returns a VMStatus object, not a dictionary
|
||||
# Access ip_address as an attribute, not with .get()
|
||||
return vm.ip_address if vm else None
|
||||
@@ -0,0 +1,4 @@
|
||||
"""Provider implementations for different VM backends."""
|
||||
|
||||
# Import specific providers only when needed to avoid circular imports
|
||||
__all__ = [] # Let each provider module handle its own exports
|
||||
@@ -0,0 +1,51 @@
|
||||
"""Base provider interface for VM backends."""
|
||||
|
||||
import abc
|
||||
from enum import Enum
|
||||
from typing import Dict, List, Optional, Any, AsyncContextManager
|
||||
|
||||
|
||||
class VMProviderType(str, Enum):
|
||||
"""Enum of supported VM provider types."""
|
||||
LUME = "lume"
|
||||
QEMU = "qemu"
|
||||
CLOUD = "cloud"
|
||||
UNKNOWN = "unknown"
|
||||
|
||||
|
||||
class BaseVMProvider(AsyncContextManager):
|
||||
"""Base interface for VM providers.
|
||||
|
||||
All VM provider implementations must implement this interface.
|
||||
"""
|
||||
|
||||
@property
|
||||
@abc.abstractmethod
|
||||
def provider_type(self) -> VMProviderType:
|
||||
"""Get the provider type."""
|
||||
pass
|
||||
|
||||
@abc.abstractmethod
|
||||
async def get_vm(self, name: str) -> Dict[str, Any]:
|
||||
"""Get VM information by name."""
|
||||
pass
|
||||
|
||||
@abc.abstractmethod
|
||||
async def list_vms(self) -> List[Dict[str, Any]]:
|
||||
"""List all available VMs."""
|
||||
pass
|
||||
|
||||
@abc.abstractmethod
|
||||
async def run_vm(self, name: str, run_opts: Dict[str, Any]) -> Dict[str, Any]:
|
||||
"""Run a VM with the given options."""
|
||||
pass
|
||||
|
||||
@abc.abstractmethod
|
||||
async def stop_vm(self, name: str) -> Dict[str, Any]:
|
||||
"""Stop a running VM."""
|
||||
pass
|
||||
|
||||
@abc.abstractmethod
|
||||
async def update_vm(self, name: str, update_opts: Dict[str, Any]) -> Dict[str, Any]:
|
||||
"""Update VM configuration."""
|
||||
pass
|
||||
@@ -0,0 +1,75 @@
|
||||
"""Factory for creating VM providers."""
|
||||
|
||||
import logging
|
||||
from typing import Dict, Optional, Any, Type, Union
|
||||
|
||||
from .base import BaseVMProvider, VMProviderType
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
|
||||
class VMProviderFactory:
|
||||
"""Factory for creating VM providers based on provider type."""
|
||||
|
||||
@staticmethod
|
||||
def create_provider(
|
||||
provider_type: Union[str, VMProviderType],
|
||||
**kwargs
|
||||
) -> BaseVMProvider:
|
||||
"""Create a VM provider of the specified type.
|
||||
|
||||
Args:
|
||||
provider_type: Type of VM provider to create
|
||||
**kwargs: Additional arguments to pass to the provider constructor
|
||||
|
||||
Returns:
|
||||
An instance of the requested VM provider
|
||||
|
||||
Raises:
|
||||
ImportError: If the required dependencies for the provider are not installed
|
||||
ValueError: If the provider type is not supported
|
||||
"""
|
||||
# Convert string to enum if needed
|
||||
if isinstance(provider_type, str):
|
||||
try:
|
||||
provider_type = VMProviderType(provider_type.lower())
|
||||
except ValueError:
|
||||
provider_type = VMProviderType.UNKNOWN
|
||||
|
||||
if provider_type == VMProviderType.LUME:
|
||||
try:
|
||||
from .lume import LumeProvider, HAS_LUME
|
||||
if not HAS_LUME:
|
||||
raise ImportError(
|
||||
"The pylume package is required for LumeProvider. "
|
||||
"Please install it with 'pip install cua-computer[lume]'"
|
||||
)
|
||||
return LumeProvider(**kwargs)
|
||||
except ImportError as e:
|
||||
logger.error(f"Failed to import LumeProvider: {e}")
|
||||
raise ImportError(
|
||||
"The pylume package is required for LumeProvider. "
|
||||
"Please install it with 'pip install cua-computer[lume]'"
|
||||
) from e
|
||||
elif provider_type == VMProviderType.QEMU:
|
||||
try:
|
||||
from .qemu import QEMUProvider
|
||||
return QEMUProvider(**kwargs)
|
||||
except ImportError as e:
|
||||
logger.error(f"Failed to import QEMUProvider: {e}")
|
||||
raise ImportError(
|
||||
"The qemu package is required for QEMUProvider. "
|
||||
"Please install it with 'pip install cua-computer[qemu]'"
|
||||
) from e
|
||||
elif provider_type == VMProviderType.CLOUD:
|
||||
try:
|
||||
from .cloud import CloudProvider
|
||||
return CloudProvider(**kwargs)
|
||||
except ImportError as e:
|
||||
logger.error(f"Failed to import CloudProvider: {e}")
|
||||
raise ImportError(
|
||||
"Cloud provider dependencies are required for CloudProvider. "
|
||||
"Please install them with 'pip install cua-computer[cloud]'"
|
||||
) from e
|
||||
else:
|
||||
raise ValueError(f"Unsupported provider type: {provider_type}")
|
||||
@@ -0,0 +1,9 @@
|
||||
"""Lume VM provider implementation."""
|
||||
|
||||
try:
|
||||
from .provider import LumeProvider
|
||||
HAS_LUME = True
|
||||
__all__ = ["LumeProvider"]
|
||||
except ImportError:
|
||||
HAS_LUME = False
|
||||
__all__ = []
|
||||
@@ -0,0 +1,129 @@
|
||||
"""Lume VM provider implementation."""
|
||||
|
||||
import logging
|
||||
from typing import Dict, List, Optional, Any, Tuple, TypeVar, Type
|
||||
|
||||
# Only import pylume when this module is actually used
|
||||
try:
|
||||
from pylume import PyLume
|
||||
from pylume.models import VMRunOpts, VMUpdateOpts, ImageRef, SharedDirectory, VMStatus
|
||||
HAS_PYLUME = True
|
||||
except ImportError:
|
||||
HAS_PYLUME = False
|
||||
# Create dummy classes for type checking
|
||||
class PyLume:
|
||||
pass
|
||||
class VMRunOpts:
|
||||
pass
|
||||
class VMUpdateOpts:
|
||||
pass
|
||||
class ImageRef:
|
||||
pass
|
||||
class SharedDirectory:
|
||||
pass
|
||||
class VMStatus:
|
||||
pass
|
||||
|
||||
from ..base import BaseVMProvider, VMProviderType
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
|
||||
class LumeProvider(BaseVMProvider):
|
||||
"""Lume VM provider implementation using pylume."""
|
||||
|
||||
def __init__(
|
||||
self,
|
||||
port: Optional[int] = None,
|
||||
host: str = "localhost",
|
||||
bin_path: Optional[str] = None,
|
||||
storage_path: Optional[str] = None,
|
||||
verbose: bool = False,
|
||||
**kwargs
|
||||
):
|
||||
"""Initialize the Lume provider.
|
||||
|
||||
Args:
|
||||
port: Optional port to use for the PyLume server
|
||||
host: Host to use for PyLume connections
|
||||
bin_path: Optional path to the Lume binary
|
||||
storage_path: Optional path to store VM data
|
||||
verbose: Enable verbose logging
|
||||
"""
|
||||
if not HAS_PYLUME:
|
||||
raise ImportError(
|
||||
"The pylume package is required for LumeProvider. "
|
||||
"Please install it with 'pip install cua-computer[lume]'"
|
||||
)
|
||||
|
||||
# PyLume doesn't accept bin_path or storage_path parameters
|
||||
# Convert verbose to debug parameter for PyLume
|
||||
self._pylume = PyLume(
|
||||
port=port,
|
||||
host=host,
|
||||
debug=verbose,
|
||||
**kwargs
|
||||
)
|
||||
# Store these for reference, even though PyLume doesn't use them directly
|
||||
self._bin_path = bin_path
|
||||
self._storage_path = storage_path
|
||||
self._context = None
|
||||
|
||||
@property
|
||||
def provider_type(self) -> VMProviderType:
|
||||
"""Get the provider type."""
|
||||
return VMProviderType.LUME
|
||||
|
||||
async def __aenter__(self):
|
||||
"""Enter async context manager."""
|
||||
self._context = await self._pylume.__aenter__()
|
||||
return self
|
||||
|
||||
async def __aexit__(self, exc_type, exc_val, exc_tb):
|
||||
"""Exit async context manager."""
|
||||
if self._context:
|
||||
await self._pylume.__aexit__(exc_type, exc_val, exc_tb)
|
||||
self._context = None
|
||||
|
||||
async def get_vm(self, name: str) -> VMStatus:
|
||||
"""Get VM information by name."""
|
||||
# PyLume get_vm returns a VMStatus object, not a dictionary
|
||||
return await self._pylume.get_vm(name)
|
||||
|
||||
async def list_vms(self) -> List[Dict[str, Any]]:
|
||||
"""List all available VMs."""
|
||||
return await self._pylume.list_vms()
|
||||
|
||||
async def run_vm(self, name: str, run_opts: Dict[str, Any]) -> Dict[str, Any]:
|
||||
"""Run a VM with the given options."""
|
||||
# Convert dict to VMRunOpts if needed
|
||||
if isinstance(run_opts, dict):
|
||||
run_opts = VMRunOpts(**run_opts)
|
||||
return await self._pylume.run_vm(name, run_opts)
|
||||
|
||||
async def stop_vm(self, name: str) -> Dict[str, Any]:
|
||||
"""Stop a running VM."""
|
||||
return await self._pylume.stop_vm(name)
|
||||
|
||||
async def update_vm(self, name: str, update_opts: Dict[str, Any]) -> Dict[str, Any]:
|
||||
"""Update VM configuration."""
|
||||
# Convert dict to VMUpdateOpts if needed
|
||||
if isinstance(update_opts, dict):
|
||||
update_opts = VMUpdateOpts(**update_opts)
|
||||
return await self._pylume.update_vm(name, update_opts)
|
||||
|
||||
# Pylume-specific helper methods
|
||||
def get_pylume_instance(self) -> PyLume:
|
||||
"""Get the underlying PyLume instance."""
|
||||
return self._pylume
|
||||
|
||||
# Helper methods for converting between PyLume and generic types
|
||||
@staticmethod
|
||||
def create_vm_run_opts(**kwargs) -> VMRunOpts:
|
||||
"""Create VMRunOpts from kwargs."""
|
||||
return VMRunOpts(**kwargs)
|
||||
|
||||
@staticmethod
|
||||
def create_vm_update_opts(**kwargs) -> VMUpdateOpts:
|
||||
"""Create VMUpdateOpts from kwargs."""
|
||||
return VMUpdateOpts(**kwargs)
|
||||
@@ -0,0 +1,9 @@
|
||||
"""QEMU VM provider implementation."""
|
||||
|
||||
try:
|
||||
from .provider import QEMUProvider
|
||||
HAS_QEMU = True
|
||||
__all__ = ["QEMUProvider"]
|
||||
except ImportError:
|
||||
HAS_QEMU = False
|
||||
__all__ = []
|
||||
@@ -0,0 +1,77 @@
|
||||
"""QEMU VM provider implementation."""
|
||||
|
||||
import logging
|
||||
from typing import Dict, List, Optional, Any, AsyncContextManager
|
||||
|
||||
from ..base import BaseVMProvider, VMProviderType
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
|
||||
class QEMUProvider(BaseVMProvider):
|
||||
"""QEMU VM provider implementation.
|
||||
|
||||
This is a placeholder implementation. The actual implementation would
|
||||
use QEMU's API to manage virtual machines.
|
||||
"""
|
||||
|
||||
def __init__(
|
||||
self,
|
||||
bin_path: Optional[str] = None,
|
||||
storage_path: Optional[str] = None,
|
||||
port: Optional[int] = None,
|
||||
host: str = "localhost",
|
||||
verbose: bool = False,
|
||||
**kwargs
|
||||
):
|
||||
"""Initialize the QEMU provider.
|
||||
|
||||
Args:
|
||||
bin_path: Optional path to the QEMU binary
|
||||
storage_path: Optional path to store VM data
|
||||
port: Optional port for management
|
||||
host: Host to use for connections
|
||||
verbose: Enable verbose logging
|
||||
"""
|
||||
self._context = None
|
||||
self._verbose = verbose
|
||||
self._bin_path = bin_path
|
||||
self._storage_path = storage_path
|
||||
self._port = port
|
||||
self._host = host
|
||||
|
||||
@property
|
||||
def provider_type(self) -> VMProviderType:
|
||||
"""Get the provider type."""
|
||||
return VMProviderType.QEMU
|
||||
|
||||
async def __aenter__(self):
|
||||
"""Enter async context manager."""
|
||||
# In a real implementation, this would initialize the QEMU management API
|
||||
self._context = True
|
||||
return self
|
||||
|
||||
async def __aexit__(self, exc_type, exc_val, exc_tb):
|
||||
"""Exit async context manager."""
|
||||
# In a real implementation, this would clean up QEMU resources
|
||||
self._context = None
|
||||
|
||||
async def get_vm(self, name: str) -> Dict[str, Any]:
|
||||
"""Get VM information by name."""
|
||||
raise NotImplementedError("QEMU provider is not implemented yet")
|
||||
|
||||
async def list_vms(self) -> List[Dict[str, Any]]:
|
||||
"""List all available VMs."""
|
||||
raise NotImplementedError("QEMU provider is not implemented yet")
|
||||
|
||||
async def run_vm(self, name: str, run_opts: Dict[str, Any]) -> Dict[str, Any]:
|
||||
"""Run a VM with the given options."""
|
||||
raise NotImplementedError("QEMU provider is not implemented yet")
|
||||
|
||||
async def stop_vm(self, name: str) -> Dict[str, Any]:
|
||||
"""Stop a running VM."""
|
||||
raise NotImplementedError("QEMU provider is not implemented yet")
|
||||
|
||||
async def update_vm(self, name: str, update_opts: Dict[str, Any]) -> Dict[str, Any]:
|
||||
"""Update VM configuration."""
|
||||
raise NotImplementedError("QEMU provider is not implemented yet")
|
||||
@@ -11,7 +11,6 @@ authors = [
|
||||
{ name = "TryCua", email = "gh@trycua.com" }
|
||||
]
|
||||
dependencies = [
|
||||
"pylume>=0.1.8",
|
||||
"pillow>=10.0.0",
|
||||
"websocket-client>=1.8.0",
|
||||
"websockets>=12.0",
|
||||
@@ -22,10 +21,18 @@ dependencies = [
|
||||
requires-python = ">=3.10"
|
||||
|
||||
[project.optional-dependencies]
|
||||
lume = [
|
||||
"pylume>=0.1.8"
|
||||
]
|
||||
ui = [
|
||||
"gradio>=5.23.3,<6.0.0",
|
||||
"python-dotenv>=1.0.1,<2.0.0",
|
||||
]
|
||||
all = [
|
||||
"pylume>=0.1.8",
|
||||
"gradio>=5.23.3,<6.0.0",
|
||||
"python-dotenv>=1.0.1,<2.0.0",
|
||||
]
|
||||
|
||||
[tool.pdm]
|
||||
distribution = true
|
||||
|
||||
Reference in New Issue
Block a user