feat: Add Aliyun provider support for desktop environment (#304)
* Adding support for aliyun as a provider * feat: enhance Aliyun provider support - Added Aliyun as a new provider in the desktop environment. - Updated the environment configuration guidelines for Aliyun, including prerequisites and environment variables. - Implemented instance allocation and management functions for Aliyun ECS, including signal handling for graceful termination. - Improved logging and error handling during instance creation and status checks. - Adjusted the provider's methods to utilize the new instance management functions.
This commit is contained in:
@@ -158,7 +158,7 @@ class DesktopEnv(gym.Env):
|
||||
# Track whether environment has been used (step/setup) to optimize snapshot revert
|
||||
# docker, aws, gcp, azure are always unused as the emulator starts from a clean state
|
||||
# vmware, virtualbox are always used as the emulator starts from a dirty state
|
||||
if self.provider_name in {"docker", "aws", "gcp", "azure"}:
|
||||
if self.provider_name in {"docker", "aws", "gcp", "azure", "aliyun"}:
|
||||
self.is_environment_used = False
|
||||
elif self.provider_name in {"vmware", "virtualbox"}:
|
||||
self.is_environment_used = True
|
||||
|
||||
@@ -31,5 +31,9 @@ def create_vm_manager_and_provider(provider_name: str, region: str, use_proxy: b
|
||||
from desktop_env.providers.docker.manager import DockerVMManager
|
||||
from desktop_env.providers.docker.provider import DockerProvider
|
||||
return DockerVMManager(), DockerProvider(region)
|
||||
elif provider_name == "aliyun":
|
||||
from desktop_env.providers.aliyun.manager import AliyunVMManager
|
||||
from desktop_env.providers.aliyun.provider import AliyunProvider
|
||||
return AliyunVMManager(), AliyunProvider()
|
||||
else:
|
||||
raise NotImplementedError(f"{provider_name} not implemented!")
|
||||
|
||||
79
desktop_env/providers/aliyun/ALIYUN_GUIDELINE.md
Normal file
79
desktop_env/providers/aliyun/ALIYUN_GUIDELINE.md
Normal file
@@ -0,0 +1,79 @@
|
||||
# Aliyun ECS Provider Configuration Guide
|
||||
|
||||
This guide explains how to configure and use the Aliyun ECS provider for OSWorld desktop environments.
|
||||
|
||||
## Configuration Process
|
||||
|
||||
1. **Aliyun Account**: You need an active Aliyun Cloud account. This script uses pay-as-you-go billing by default, so ensure your account balance is above 100.
|
||||
2. **Access Keys**: Create AccessKey ID and AccessKey Secret in Aliyun RAM Access Control Console and grant ECS control permissions
|
||||
3. **VPC Setup**: Create a VPC, VSwitch, and Security Group in your target region
|
||||
4. **Custom Images**: Create OSWorld custom images
|
||||
5. It is recommended to manually complete the ECS creation process once to record all required environment variable information.
|
||||
|
||||
## Environment Variables
|
||||
|
||||
Set the following environment variables in your `.env` file:
|
||||
|
||||
```bash
|
||||
# Aliyun Access Credentials
|
||||
ALIYUN_ACCESS_KEY_ID=your_access_key_id
|
||||
ALIYUN_ACCESS_KEY_SECRET=your_access_key_secret
|
||||
|
||||
# ECS Configuration Information
|
||||
ALIYUN_REGION=eu-central-1
|
||||
ALIYUN_IMAGE_ID=your_image_id
|
||||
ALIYUN_INSTANCE_TYPE=ecs.e-c1m2.large
|
||||
ALIYUN_VSWITCH_ID=vsw-xxxxxxxxx
|
||||
ALIYUN_SECURITY_GROUP_ID=sg-xxxxxxxxx
|
||||
```
|
||||
|
||||
## Required Aliyun Resources
|
||||
|
||||
### 1. VPC and VSwitch
|
||||
- Create a VPC in your target region
|
||||
- Create a VSwitch within the VPC
|
||||
- Ensure the VSwitch has internet access for VNC connectivity
|
||||
|
||||
### 2. Security Group
|
||||
**⚠️ Important**: Please strictly follow the port settings below to prevent OSWorld tasks from failing due to connection issues:
|
||||
|
||||
#### Inbound Rules (8 rules required)
|
||||
|
||||
| Type | Protocol | Port Range | Source | Description |
|
||||
|------|----------|------------|--------|-------------|
|
||||
| SSH | TCP | 22 | 0.0.0.0/0 | SSH access |
|
||||
| HTTP | TCP | 80 | 172.31.0.0/16 | HTTP traffic |
|
||||
| Custom TCP | TCP | 5000 | 172.31.0.0/16 | OSWorld backend service |
|
||||
| Custom TCP | TCP | 5910 | 0.0.0.0/0 | NoVNC visualization port |
|
||||
| Custom TCP | TCP | 8006 | 172.31.0.0/16 | VNC service port |
|
||||
| Custom TCP | TCP | 8080 | 172.31.0.0/16 | VLC service port |
|
||||
| Custom TCP | TCP | 8081 | 172.31.0.0/16 | Additional service port |
|
||||
| Custom TCP | TCP | 9222 | 172.31.0.0/16 | Chrome control port |
|
||||
|
||||
#### Outbound Rules (1 rule required)
|
||||
|
||||
| Type | Protocol | Port Range | Destination | Description |
|
||||
|------|----------|------------|-------------|-------------|
|
||||
| All traffic | All | All | 0.0.0.0/0 | Allow all outbound traffic |
|
||||
|
||||
### 3. Custom Images
|
||||
You need to create a custom OSWorld image for Aliyun ECS. Please follow the instructions in the "Creating Custom ECS Images for OSWorld" section.
|
||||
|
||||
## Creating Custom ECS Images for OSWorld
|
||||
|
||||
This section provides guidance on how to create the custom ECS images required for OSWorld desktop environments. The process involves setting up a base instance with desktop environment and VNC server, then creating a custom image from it.
|
||||
|
||||
### Step-by-Step Image Creation Process
|
||||
#### Step 1: Upload existing qcow2 image to Aliyun
|
||||
- Download the provided qcow2 image from the link in `desktop_env/providers/docker/manager.py`: https://huggingface.co/datasets/xlangai/ubuntu_osworld/resolve/main/Ubuntu.qcow2.zip
|
||||
- Unzip the downloaded file and upload it to Aliyun Object Storage Service (OSS). Make sure the OSS is in the same region as your target region to launch ECS instance.
|
||||
- In your ECS dashboard, go to "Images" and You will see the "Import Image" button. Click it and follow the instructions to import the qcow2 image from OSS.
|
||||
- After the import is complete, you will see the imported image in the "Images" list.
|
||||
#### Step 2: Create a new image
|
||||
Note that the image you created in Step 1 will have a different resolution than the one you want to use for OSWorld (1920x1080). We need to customize the image to have the correct resolution and setup noVNC.
|
||||
- Go to `Instances` tab and create a new instance with the imported image.
|
||||
- Connect to the running instance via VNC.
|
||||
- After connecting to the instance, please open the terminal and download this configuration script: `https://gist.githubusercontent.com/qykong/bea58ff98f20057d3a69921276dd4553/raw/cd1a91a0840c4192d793f43cfb90553370343b08/config.sh`.
|
||||
- Run the script and reboot your instance.
|
||||
- After rebooting, the instance will have the correct resolution and noVNC setup. You can connect to the instance via "http://<your_instance_public_ip>:5910/vnc.html" (make sure your security group allows port 5910).
|
||||
- Save the running instance as a new image. The new image will be used as the OSWorld image.
|
||||
81
desktop_env/providers/aliyun/ALIYUN_GUIDELINE_CN.md
Normal file
81
desktop_env/providers/aliyun/ALIYUN_GUIDELINE_CN.md
Normal file
@@ -0,0 +1,81 @@
|
||||
# 阿里云ECS提供商配置指南
|
||||
|
||||
本指南介绍如何为OSWorld桌面环境配置和使用阿里云ECS。
|
||||
|
||||
## 配置流程
|
||||
|
||||
1. **阿里云账户**:您需要一个有效的阿里云账户,本脚本默认ECS通过按量付费方式拉起,需保证账户余额在100以上。
|
||||
2. **访问密钥**:在阿里云RAM访问控制控制台中创建AccessKey ID和AccessKey Secret,并授权ECS控制权限
|
||||
3. **VPC设置**:在目标地域创建VPC、交换机和安全组
|
||||
4. **自定义镜像**:创建OSWorld自定义镜像。
|
||||
5. 建议手动完成一次ECS创建流程后,记录所有需要的环境变量信息。
|
||||
|
||||
## 环境变量
|
||||
|
||||
在您的`.env`文件中设置以下环境变量:
|
||||
|
||||
```bash
|
||||
# 阿里云访问凭证
|
||||
ALIYUN_ACCESS_KEY_ID=your_access_key_id
|
||||
ALIYUN_ACCESS_KEY_SECRET=your_access_key_secret
|
||||
|
||||
# ECS配置信息
|
||||
ALIYUN_REGION=eu-central-1
|
||||
ALIYUN_IMAGE_ID=your_image_id
|
||||
ALIYUN_INSTANCE_TYPE=ecs.e-c1m2.large
|
||||
ALIYUN_VSWITCH_ID=vsw-xxxxxxxxx
|
||||
ALIYUN_SECURITY_GROUP_ID=sg-xxxxxxxxx
|
||||
```
|
||||
|
||||
## 所需阿里云资源
|
||||
|
||||
### 1. VPC和交换机
|
||||
- 在目标地域创建VPC
|
||||
- 在VPC内创建交换机
|
||||
- 确保交换机具有互联网访问能力以支持VNC连接
|
||||
|
||||
### 2. 安全组
|
||||
**⚠️ 重要提示**:请严格按照以下端口设置,以防止OSWorld任务因连接问题而失败:
|
||||
|
||||
#### 入方向规则(需要8条规则)
|
||||
|
||||
| 类型 | 协议 | 端口范围 | 源地址 | 描述 |
|
||||
|------|------|----------|--------|------|
|
||||
| SSH | TCP | 22 | 0.0.0.0/0 | SSH访问 |
|
||||
| HTTP | TCP | 80 | 172.31.0.0/16 | HTTP流量 |
|
||||
| 自定义TCP | TCP | 5000 | 172.31.0.0/16 | OSWorld后端服务 |
|
||||
| 自定义TCP | TCP | 5910 | 0.0.0.0/0 | NoVNC可视化端口 |
|
||||
| 自定义TCP | TCP | 8006 | 172.31.0.0/16 | VNC服务端口 |
|
||||
| 自定义TCP | TCP | 8080 | 172.31.0.0/16 | VLC服务端口 |
|
||||
| 自定义TCP | TCP | 8081 | 172.31.0.0/16 | 附加服务端口 |
|
||||
| 自定义TCP | TCP | 9222 | 172.31.0.0/16 | Chrome控制端口 |
|
||||
|
||||
#### 出方向规则(需要1条规则)
|
||||
|
||||
| 类型 | 协议 | 端口范围 | 目标地址 | 描述 |
|
||||
|------|------|----------|----------|------|
|
||||
| 全部流量 | 全部 | 全部 | 0.0.0.0/0 | 允许所有出站流量 |
|
||||
|
||||
### 3. 自定义镜像
|
||||
您需要为阿里云ECS创建自定义OSWorld镜像。请按照"为OSWorld创建自定义ECS镜像"部分的说明进行操作。
|
||||
|
||||
|
||||
## 为OSWorld创建自定义ECS镜像
|
||||
|
||||
本部分提供如何创建OSWorld桌面环境所需的自定义ECS镜像的指导。该过程包括设置带有桌面环境和VNC服务器的基础实例,然后从中创建自定义镜像。
|
||||
|
||||
### 分步镜像创建过程
|
||||
#### 步骤1:上传现有qcow2镜像到阿里云
|
||||
- 从`desktop_env/providers/docker/manager.py`中的链接下载提供的qcow2镜像:https://huggingface.co/datasets/xlangai/ubuntu_osworld/resolve/main/Ubuntu.qcow2.zip
|
||||
- 解压下载的文件并上传到阿里云对象存储服务(OSS)。确保OSS与您要启动ECS实例的目标地域在同一地域。
|
||||
- 在您的ECS控制台中,转到"镜像"页面,您将看到"导入镜像"按钮。点击它并按照说明从OSS导入qcow2镜像。
|
||||
- 导入完成后,您将在"镜像"列表中看到导入的镜像。
|
||||
|
||||
#### 步骤2:创建新镜像
|
||||
请注意,您在步骤1中创建的镜像分辨率与您想要用于OSWorld的分辨率(1920x1080)不同。我们需要自定义镜像以具有正确的分辨率并设置noVNC。
|
||||
- 转到"实例"选项卡,使用导入的镜像创建新实例。
|
||||
- 通过VNC连接到正在运行的实例。
|
||||
- 连接到实例后,请打开终端并下载此配置脚本:`https://gist.githubusercontent.com/qykong/bea58ff98f20057d3a69921276dd4553/raw/cd1a91a0840c4192d793f43cfb90553370343b08/config.sh`。
|
||||
- 运行脚本并重启您的实例。
|
||||
- 重启后,实例将具有正确的分辨率和noVNC设置。您可以通过"http://<your_instance_public_ip>:5910/vnc.html"连接到实例(确保您的安全组允许端口5910)。
|
||||
- 将正在运行的实例保存为新镜像。新镜像将用作OSWorld镜像。
|
||||
0
desktop_env/providers/aliyun/__init__.py
Normal file
0
desktop_env/providers/aliyun/__init__.py
Normal file
289
desktop_env/providers/aliyun/manager.py
Normal file
289
desktop_env/providers/aliyun/manager.py
Normal file
@@ -0,0 +1,289 @@
|
||||
import os
|
||||
import logging
|
||||
import dotenv
|
||||
import time
|
||||
import signal
|
||||
import requests
|
||||
|
||||
from alibabacloud_ecs20140526.client import Client as ECSClient
|
||||
from alibabacloud_tea_openapi import models as open_api_models
|
||||
from alibabacloud_ecs20140526 import models as ecs_models
|
||||
from alibabacloud_tea_util.client import Client as UtilClient
|
||||
from desktop_env.providers.base import VMManager
|
||||
|
||||
|
||||
dotenv.load_dotenv()
|
||||
|
||||
for env_name in [
|
||||
"ALIYUN_REGION",
|
||||
"ALIYUN_VSWITCH_ID",
|
||||
"ALIYUN_SECURITY_GROUP_ID",
|
||||
"ALIYUN_IMAGE_ID",
|
||||
"ALIYUN_ACCESS_KEY_ID",
|
||||
"ALIYUN_ACCESS_KEY_SECRET",
|
||||
"ALIYUN_INSTANCE_TYPE",
|
||||
]:
|
||||
if not os.getenv(env_name):
|
||||
raise EnvironmentError(f"{env_name} must be set in the environment variables.")
|
||||
|
||||
|
||||
logger = logging.getLogger("desktopenv.providers.aliyun.AliyunVMManager")
|
||||
logger.setLevel(logging.INFO)
|
||||
|
||||
ALIYUN_INSTANCE_TYPE = os.getenv("ALIYUN_INSTANCE_TYPE")
|
||||
ALIYUN_ACCESS_KEY_ID = os.getenv("ALIYUN_ACCESS_KEY_ID")
|
||||
ALIYUN_ACCESS_KEY_SECRET = os.getenv("ALIYUN_ACCESS_KEY_SECRET")
|
||||
ALIYUN_REGION = os.getenv("ALIYUN_REGION")
|
||||
ALIYUN_IMAGE_ID = os.getenv("ALIYUN_IMAGE_ID")
|
||||
ALIYUN_SECURITY_GROUP_ID = os.getenv("ALIYUN_SECURITY_GROUP_ID")
|
||||
ALIYUN_VSWITCH_ID = os.getenv("ALIYUN_VSWITCH_ID")
|
||||
|
||||
WAIT_DELAY = 20
|
||||
MAX_ATTEMPTS = 15
|
||||
|
||||
|
||||
def _allocate_vm(screen_size=(1920, 1080)):
|
||||
"""
|
||||
Allocate a new Aliyun ECS instance
|
||||
"""
|
||||
assert screen_size == (1920, 1080), "Only 1920x1080 screen size is supported"
|
||||
|
||||
config = open_api_models.Config(
|
||||
access_key_id=ALIYUN_ACCESS_KEY_ID,
|
||||
access_key_secret=ALIYUN_ACCESS_KEY_SECRET,
|
||||
region_id=ALIYUN_REGION,
|
||||
)
|
||||
client = ECSClient(config)
|
||||
instance_id = None
|
||||
original_sigint_handler = signal.getsignal(signal.SIGINT)
|
||||
original_sigterm_handler = signal.getsignal(signal.SIGTERM)
|
||||
|
||||
def signal_handler(sig, frame):
|
||||
if instance_id:
|
||||
signal_name = "SIGINT" if sig == signal.SIGINT else "SIGTERM"
|
||||
logger.warning(
|
||||
f"Received {signal_name} signal, terminating instance {instance_id}..."
|
||||
)
|
||||
try:
|
||||
delete_request = ecs_models.DeleteInstancesRequest(
|
||||
region_id=ALIYUN_REGION,
|
||||
instance_ids=UtilClient.to_jsonstring([instance_id]),
|
||||
force=True,
|
||||
)
|
||||
client.delete_instances(delete_request)
|
||||
logger.info(
|
||||
f"Successfully terminated instance {instance_id} after {signal_name}."
|
||||
)
|
||||
except Exception as cleanup_error:
|
||||
logger.error(
|
||||
f"Failed to terminate instance {instance_id} after {signal_name}: {str(cleanup_error)}"
|
||||
)
|
||||
|
||||
# Restore original signal handlers
|
||||
signal.signal(signal.SIGINT, original_sigint_handler)
|
||||
signal.signal(signal.SIGTERM, original_sigterm_handler)
|
||||
|
||||
# Raise appropriate exception based on signal type
|
||||
if sig == signal.SIGINT:
|
||||
raise KeyboardInterrupt
|
||||
else:
|
||||
# For SIGTERM, exit gracefully
|
||||
import sys
|
||||
|
||||
sys.exit(0)
|
||||
|
||||
try:
|
||||
# Set up signal handlers for both SIGINT and SIGTERM
|
||||
signal.signal(signal.SIGINT, signal_handler)
|
||||
signal.signal(signal.SIGTERM, signal_handler)
|
||||
|
||||
logger.info(
|
||||
f"Creating new ECS instance in region {ALIYUN_REGION} with image {ALIYUN_IMAGE_ID}"
|
||||
)
|
||||
|
||||
# Create instance request
|
||||
request = ecs_models.RunInstancesRequest(
|
||||
region_id=ALIYUN_REGION,
|
||||
image_id=ALIYUN_IMAGE_ID,
|
||||
instance_type=ALIYUN_INSTANCE_TYPE,
|
||||
security_group_id=ALIYUN_SECURITY_GROUP_ID,
|
||||
v_switch_id=ALIYUN_VSWITCH_ID,
|
||||
instance_name=f"OSWorld-Desktop-{int(time.time())}",
|
||||
description="OSWorld Desktop Environment Instance",
|
||||
internet_max_bandwidth_out=10,
|
||||
internet_charge_type="PayByTraffic",
|
||||
instance_charge_type="PostPaid",
|
||||
system_disk=ecs_models.RunInstancesRequestSystemDisk(
|
||||
size="50",
|
||||
category="cloud_essd",
|
||||
),
|
||||
deletion_protection=False,
|
||||
)
|
||||
|
||||
# Create the instance
|
||||
response = client.run_instances(request)
|
||||
instance_ids = response.body.instance_id_sets.instance_id_set
|
||||
|
||||
if not instance_ids:
|
||||
raise RuntimeError(
|
||||
"Failed to create ECS instance - no instance ID returned"
|
||||
)
|
||||
|
||||
instance_id = instance_ids[0]
|
||||
logger.info(f"ECS instance {instance_id} created successfully")
|
||||
|
||||
# Wait for the instance to be running
|
||||
logger.info(f"Waiting for instance {instance_id} to be running...")
|
||||
_wait_for_instance_running(client, instance_id)
|
||||
|
||||
logger.info(f"Instance {instance_id} is now running and ready")
|
||||
|
||||
except KeyboardInterrupt:
|
||||
logger.warning("VM allocation interrupted by user (SIGINT).")
|
||||
if instance_id:
|
||||
logger.info(f"Terminating instance {instance_id} due to interruption.")
|
||||
try:
|
||||
delete_request = ecs_models.DeleteInstancesRequest(
|
||||
region_id=ALIYUN_REGION,
|
||||
instance_ids=UtilClient.to_jsonstring([instance_id]),
|
||||
force=True,
|
||||
)
|
||||
client.delete_instances(delete_request)
|
||||
except Exception as cleanup_error:
|
||||
logger.error(
|
||||
f"Failed to cleanup instance {instance_id}: {str(cleanup_error)}"
|
||||
)
|
||||
raise
|
||||
except Exception as e:
|
||||
logger.error(f"Failed to allocate ECS instance: {str(e)}")
|
||||
if instance_id:
|
||||
logger.info(f"Terminating instance {instance_id} due to an error.")
|
||||
try:
|
||||
delete_request = ecs_models.DeleteInstancesRequest(
|
||||
region_id=ALIYUN_REGION,
|
||||
instance_ids=UtilClient.to_jsonstring([instance_id]),
|
||||
force=True,
|
||||
)
|
||||
client.delete_instances(delete_request)
|
||||
except Exception as cleanup_error:
|
||||
logger.error(
|
||||
f"Failed to cleanup instance {instance_id}: {str(cleanup_error)}"
|
||||
)
|
||||
raise
|
||||
finally:
|
||||
# Restore original signal handlers
|
||||
signal.signal(signal.SIGINT, original_sigint_handler)
|
||||
signal.signal(signal.SIGTERM, original_sigterm_handler)
|
||||
|
||||
return instance_id
|
||||
|
||||
|
||||
def _wait_for_instance_running(
|
||||
client: ECSClient, instance_id: str, max_attempts: int = MAX_ATTEMPTS
|
||||
):
|
||||
"""Wait for instance to reach Running state"""
|
||||
for _ in range(max_attempts):
|
||||
try:
|
||||
req = ecs_models.DescribeInstancesRequest(
|
||||
region_id=ALIYUN_REGION,
|
||||
instance_ids=UtilClient.to_jsonstring([instance_id]),
|
||||
)
|
||||
response = client.describe_instances(req)
|
||||
|
||||
if response.body.instances.instance:
|
||||
instance = response.body.instances.instance[0]
|
||||
status = instance.status
|
||||
logger.info(f"Instance {instance_id} status: {status}")
|
||||
|
||||
if status == "Running":
|
||||
return
|
||||
elif status in ["Stopped", "Stopping"]:
|
||||
start_req = ecs_models.StartInstanceRequest(instance_id=instance_id)
|
||||
client.start_instance(start_req)
|
||||
logger.info(f"Started instance {instance_id}")
|
||||
|
||||
time.sleep(WAIT_DELAY)
|
||||
|
||||
except Exception as e:
|
||||
logger.warning(f"Error checking instance status: {e}")
|
||||
time.sleep(WAIT_DELAY)
|
||||
|
||||
raise TimeoutError(
|
||||
f"Instance {instance_id} did not reach Running state within {max_attempts * WAIT_DELAY} seconds"
|
||||
)
|
||||
|
||||
|
||||
def _wait_until_server_ready(public_ip: str):
|
||||
"""Wait until the server is ready"""
|
||||
for _ in range(MAX_ATTEMPTS):
|
||||
try:
|
||||
logger.info(f"Checking server status on {public_ip}...")
|
||||
response = requests.get(f"http://{public_ip}:5000/", timeout=2)
|
||||
if response.status_code == 404:
|
||||
logger.info(f"Server {public_ip} is ready")
|
||||
return
|
||||
except Exception:
|
||||
time.sleep(WAIT_DELAY)
|
||||
|
||||
raise TimeoutError(
|
||||
f"Server {public_ip} did not respond within {MAX_ATTEMPTS * WAIT_DELAY} seconds"
|
||||
)
|
||||
|
||||
|
||||
class AliyunVMManager(VMManager):
|
||||
"""
|
||||
Aliyun ECS VM Manager for managing virtual machines on Aliyun Cloud.
|
||||
|
||||
Aliyun ECS does not need to maintain a registry of VMs, as it can dynamically allocate and deallocate VMs.
|
||||
"""
|
||||
|
||||
def __init__(self, **kwargs):
|
||||
self.initialize_registry()
|
||||
|
||||
def initialize_registry(self, **kwargs):
|
||||
pass
|
||||
|
||||
def add_vm(self, vm_path, lock_needed=True, **kwargs):
|
||||
pass
|
||||
|
||||
def _add_vm(self, vm_path):
|
||||
pass
|
||||
|
||||
def delete_vm(self, vm_path, lock_needed=True, **kwargs):
|
||||
pass
|
||||
|
||||
def _delete_vm(self, vm_path):
|
||||
pass
|
||||
|
||||
def occupy_vm(self, vm_path, pid, lock_needed=True, **kwargs):
|
||||
pass
|
||||
|
||||
def _occupy_vm(self, vm_path, pid):
|
||||
pass
|
||||
|
||||
def check_and_clean(self, lock_needed=True, **kwargs):
|
||||
pass
|
||||
|
||||
def _check_and_clean(self):
|
||||
pass
|
||||
|
||||
def list_free_vms(self, lock_needed=True, **kwargs):
|
||||
pass
|
||||
|
||||
def _list_free_vms(self):
|
||||
pass
|
||||
|
||||
def get_vm_path(self, screen_size=(1920, 1080), **kwargs):
|
||||
"""Get a VM path (instance ID) for use"""
|
||||
logger.info(
|
||||
f"Allocating new ECS instance in region {ALIYUN_REGION} with screen size {screen_size}"
|
||||
)
|
||||
|
||||
try:
|
||||
instance_id = _allocate_vm(screen_size)
|
||||
logger.info(f"Successfully allocated instance {instance_id}")
|
||||
return instance_id
|
||||
|
||||
except Exception as e:
|
||||
logger.error(f"Failed to allocate instance: {str(e)}")
|
||||
raise
|
||||
214
desktop_env/providers/aliyun/provider.py
Normal file
214
desktop_env/providers/aliyun/provider.py
Normal file
@@ -0,0 +1,214 @@
|
||||
import os
|
||||
import logging
|
||||
from datetime import datetime
|
||||
|
||||
from alibabacloud_ecs20140526.client import Client as ECSClient
|
||||
from alibabacloud_tea_openapi import models as open_api_models
|
||||
from alibabacloud_ecs20140526 import models as ecs_models
|
||||
from alibabacloud_tea_util.client import Client as UtilClient
|
||||
|
||||
from desktop_env.providers.base import Provider
|
||||
from desktop_env.providers.aliyun.manager import (
|
||||
_allocate_vm,
|
||||
_wait_for_instance_running,
|
||||
_wait_until_server_ready,
|
||||
)
|
||||
|
||||
|
||||
logger = logging.getLogger("desktopenv.providers.aliyun.AliyunProvider")
|
||||
logger.setLevel(logging.INFO)
|
||||
|
||||
|
||||
class AliyunProvider(Provider):
|
||||
def __init__(self, **kwargs):
|
||||
super().__init__(**kwargs)
|
||||
self.region = os.getenv("ALIYUN_REGION", "eu-central-1")
|
||||
self.client = self._create_client()
|
||||
|
||||
def _create_client(self) -> ECSClient:
|
||||
config = open_api_models.Config(
|
||||
access_key_id=os.getenv("ALIYUN_ACCESS_KEY_ID"),
|
||||
access_key_secret=os.getenv("ALIYUN_ACCESS_KEY_SECRET"),
|
||||
region_id=self.region,
|
||||
)
|
||||
return ECSClient(config)
|
||||
|
||||
def start_emulator(self, path_to_vm: str, headless: bool, *args, **kwargs):
|
||||
logger.info("Starting Aliyun ECS instance...")
|
||||
|
||||
try:
|
||||
# Check the current state of the instance
|
||||
response = self._describe_instance(path_to_vm)
|
||||
if not response.body.instances.instance:
|
||||
logger.error(f"Instance {path_to_vm} not found")
|
||||
return
|
||||
|
||||
instance = response.body.instances.instance[0]
|
||||
state = instance.status
|
||||
logger.info(f"Instance {path_to_vm} current state: {state}")
|
||||
|
||||
if state == "Running":
|
||||
# If the instance is already running, skip starting it
|
||||
logger.info(
|
||||
f"Instance {path_to_vm} is already running. Skipping start."
|
||||
)
|
||||
return
|
||||
|
||||
if state == "Stopped":
|
||||
# Start the instance if it's currently stopped
|
||||
req = ecs_models.StartInstanceRequest(instance_id=path_to_vm)
|
||||
self.client.start_instance(req)
|
||||
logger.info(f"Instance {path_to_vm} is starting...")
|
||||
|
||||
# Wait until the instance reaches 'Running' state
|
||||
_wait_for_instance_running(self.client, path_to_vm)
|
||||
logger.info(f"Instance {path_to_vm} is now running.")
|
||||
else:
|
||||
# For all other states (Pending, Starting, etc.), log a warning
|
||||
logger.warning(
|
||||
f"Instance {path_to_vm} is in state '{state}' and cannot be started."
|
||||
)
|
||||
|
||||
except Exception as e:
|
||||
logger.error(
|
||||
f"Failed to start the Aliyun ECS instance {path_to_vm}: {str(e)}"
|
||||
)
|
||||
raise
|
||||
|
||||
def get_ip_address(self, path_to_vm: str) -> str:
|
||||
logger.info("Getting Aliyun ECS instance IP address...")
|
||||
|
||||
try:
|
||||
response = self._describe_instance(path_to_vm)
|
||||
if not response.body.instances.instance:
|
||||
logger.error(f"Instance {path_to_vm} not found")
|
||||
return ""
|
||||
|
||||
instance = response.body.instances.instance[0]
|
||||
|
||||
# Get private and public IP addresses
|
||||
private_ip = ""
|
||||
public_ip = ""
|
||||
|
||||
if hasattr(instance, "vpc_attributes") and instance.vpc_attributes:
|
||||
private_ip = (
|
||||
instance.vpc_attributes.private_ip_address.ip_address[0]
|
||||
if instance.vpc_attributes.private_ip_address.ip_address
|
||||
else ""
|
||||
)
|
||||
|
||||
if hasattr(instance, "public_ip_address") and instance.public_ip_address:
|
||||
public_ip = (
|
||||
instance.public_ip_address.ip_address[0]
|
||||
if instance.public_ip_address.ip_address
|
||||
else ""
|
||||
)
|
||||
|
||||
if hasattr(instance, "eip_address") and instance.eip_address:
|
||||
public_ip = instance.eip_address.ip_address or public_ip
|
||||
|
||||
_wait_until_server_ready(public_ip)
|
||||
|
||||
if public_ip:
|
||||
vnc_url = f"http://{public_ip}:5910/vnc.html"
|
||||
logger.info("=" * 80)
|
||||
logger.info(f"🖥️ VNC Web Access URL: {vnc_url}")
|
||||
logger.info(f"📡 Public IP: {public_ip}")
|
||||
logger.info(f"🏠 Private IP: {private_ip}")
|
||||
logger.info("=" * 80)
|
||||
print(f"\n🌐 VNC Web Access URL: {vnc_url}")
|
||||
print(
|
||||
"📍 Please open the above address in the browser "
|
||||
"for remote desktop access\n"
|
||||
)
|
||||
else:
|
||||
logger.warning("No public IP address available for VNC access")
|
||||
|
||||
return public_ip
|
||||
|
||||
except Exception as e:
|
||||
logger.error(
|
||||
f"Failed to retrieve IP address for the instance {path_to_vm}: {str(e)}"
|
||||
)
|
||||
raise
|
||||
|
||||
def save_state(self, path_to_vm: str, snapshot_name: str):
|
||||
logger.info("Saving Aliyun ECS instance state...")
|
||||
|
||||
try:
|
||||
req = ecs_models.CreateImageRequest(
|
||||
region_id=self.region,
|
||||
instance_id=path_to_vm,
|
||||
image_name=snapshot_name,
|
||||
description=f"Snapshot created at {datetime.now().isoformat()}",
|
||||
)
|
||||
response = self.client.create_image(req)
|
||||
image_id = response.body.image_id
|
||||
logger.info(
|
||||
f"Image {image_id} created successfully from instance {path_to_vm}."
|
||||
)
|
||||
return image_id
|
||||
|
||||
except Exception as e:
|
||||
logger.error(
|
||||
f"Failed to create image from the instance {path_to_vm}: {str(e)}"
|
||||
)
|
||||
raise
|
||||
|
||||
def revert_to_snapshot(self, path_to_vm: str, snapshot_name: str):
|
||||
logger.info(
|
||||
f"Reverting Aliyun ECS instance to snapshot image: {snapshot_name}..."
|
||||
)
|
||||
|
||||
try:
|
||||
# Step 1: Retrieve the original instance details
|
||||
response = self._describe_instance(path_to_vm)
|
||||
if not response.body.instances.instance:
|
||||
logger.error(f"Instance {path_to_vm} not found")
|
||||
return
|
||||
# Step 2: Delete the old instance
|
||||
req = ecs_models.DeleteInstancesRequest(
|
||||
region_id=self.region, instance_id=[path_to_vm], force=True
|
||||
)
|
||||
self.client.delete_instances(req)
|
||||
logger.info(f"Old instance {path_to_vm} has been deleted.")
|
||||
|
||||
# Step 3: Launch a new instance from the snapshot image
|
||||
new_instance_id = _allocate_vm()
|
||||
logger.info(f"Instance {new_instance_id} is ready.")
|
||||
|
||||
# Get VNC access information
|
||||
self.get_ip_address(new_instance_id)
|
||||
|
||||
return new_instance_id
|
||||
|
||||
except Exception as e:
|
||||
logger.error(
|
||||
f"Failed to revert to snapshot {snapshot_name} for the instance {path_to_vm}: {str(e)}"
|
||||
)
|
||||
raise
|
||||
|
||||
def stop_emulator(self, path_to_vm: str, region: str = None):
|
||||
logger.info(f"Stopping Aliyun ECS instance {path_to_vm}...")
|
||||
|
||||
try:
|
||||
req = ecs_models.DeleteInstancesRequest(
|
||||
region_id=self.region, instance_id=[path_to_vm], force=True
|
||||
)
|
||||
self.client.delete_instances(req)
|
||||
logger.info(f"Instance {path_to_vm} has been deleted.")
|
||||
|
||||
except Exception as e:
|
||||
logger.error(
|
||||
f"Failed to stop the Aliyun ECS instance {path_to_vm}: {str(e)}"
|
||||
)
|
||||
raise
|
||||
|
||||
def _describe_instance(
|
||||
self, instance_id: str
|
||||
) -> ecs_models.DescribeInstancesResponse:
|
||||
"""Get instance details"""
|
||||
req = ecs_models.DescribeInstancesRequest(
|
||||
region_id=self.region, instance_ids=UtilClient.to_jsonstring([instance_id])
|
||||
)
|
||||
return self.client.describe_instances(req)
|
||||
Reference in New Issue
Block a user