lybic-guiagents 0.1.0__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Potentially problematic release.
This version of lybic-guiagents might be problematic. Click here for more details.
- desktop_env/__init__.py +1 -0
- desktop_env/actions.py +203 -0
- desktop_env/controllers/__init__.py +0 -0
- desktop_env/controllers/python.py +471 -0
- desktop_env/controllers/setup.py +882 -0
- desktop_env/desktop_env.py +509 -0
- desktop_env/evaluators/__init__.py +5 -0
- desktop_env/evaluators/getters/__init__.py +41 -0
- desktop_env/evaluators/getters/calc.py +15 -0
- desktop_env/evaluators/getters/chrome.py +1774 -0
- desktop_env/evaluators/getters/file.py +154 -0
- desktop_env/evaluators/getters/general.py +42 -0
- desktop_env/evaluators/getters/gimp.py +38 -0
- desktop_env/evaluators/getters/impress.py +126 -0
- desktop_env/evaluators/getters/info.py +24 -0
- desktop_env/evaluators/getters/misc.py +406 -0
- desktop_env/evaluators/getters/replay.py +20 -0
- desktop_env/evaluators/getters/vlc.py +86 -0
- desktop_env/evaluators/getters/vscode.py +35 -0
- desktop_env/evaluators/metrics/__init__.py +160 -0
- desktop_env/evaluators/metrics/basic_os.py +68 -0
- desktop_env/evaluators/metrics/chrome.py +493 -0
- desktop_env/evaluators/metrics/docs.py +1011 -0
- desktop_env/evaluators/metrics/general.py +665 -0
- desktop_env/evaluators/metrics/gimp.py +637 -0
- desktop_env/evaluators/metrics/libreoffice.py +28 -0
- desktop_env/evaluators/metrics/others.py +92 -0
- desktop_env/evaluators/metrics/pdf.py +31 -0
- desktop_env/evaluators/metrics/slides.py +957 -0
- desktop_env/evaluators/metrics/table.py +585 -0
- desktop_env/evaluators/metrics/thunderbird.py +176 -0
- desktop_env/evaluators/metrics/utils.py +719 -0
- desktop_env/evaluators/metrics/vlc.py +524 -0
- desktop_env/evaluators/metrics/vscode.py +283 -0
- desktop_env/providers/__init__.py +35 -0
- desktop_env/providers/aws/__init__.py +0 -0
- desktop_env/providers/aws/manager.py +278 -0
- desktop_env/providers/aws/provider.py +186 -0
- desktop_env/providers/aws/provider_with_proxy.py +315 -0
- desktop_env/providers/aws/proxy_pool.py +193 -0
- desktop_env/providers/azure/__init__.py +0 -0
- desktop_env/providers/azure/manager.py +87 -0
- desktop_env/providers/azure/provider.py +207 -0
- desktop_env/providers/base.py +97 -0
- desktop_env/providers/gcp/__init__.py +0 -0
- desktop_env/providers/gcp/manager.py +0 -0
- desktop_env/providers/gcp/provider.py +0 -0
- desktop_env/providers/virtualbox/__init__.py +0 -0
- desktop_env/providers/virtualbox/manager.py +463 -0
- desktop_env/providers/virtualbox/provider.py +124 -0
- desktop_env/providers/vmware/__init__.py +0 -0
- desktop_env/providers/vmware/manager.py +455 -0
- desktop_env/providers/vmware/provider.py +105 -0
- gui_agents/__init__.py +0 -0
- gui_agents/agents/Action.py +209 -0
- gui_agents/agents/__init__.py +0 -0
- gui_agents/agents/agent_s.py +832 -0
- gui_agents/agents/global_state.py +610 -0
- gui_agents/agents/grounding.py +651 -0
- gui_agents/agents/hardware_interface.py +129 -0
- gui_agents/agents/manager.py +568 -0
- gui_agents/agents/translator.py +132 -0
- gui_agents/agents/worker.py +355 -0
- gui_agents/cli_app.py +560 -0
- gui_agents/core/__init__.py +0 -0
- gui_agents/core/engine.py +1496 -0
- gui_agents/core/knowledge.py +449 -0
- gui_agents/core/mllm.py +555 -0
- gui_agents/tools/__init__.py +0 -0
- gui_agents/tools/tools.py +727 -0
- gui_agents/unit_test/__init__.py +0 -0
- gui_agents/unit_test/run_tests.py +65 -0
- gui_agents/unit_test/test_manager.py +330 -0
- gui_agents/unit_test/test_worker.py +269 -0
- gui_agents/utils/__init__.py +0 -0
- gui_agents/utils/analyze_display.py +301 -0
- gui_agents/utils/common_utils.py +263 -0
- gui_agents/utils/display_viewer.py +281 -0
- gui_agents/utils/embedding_manager.py +53 -0
- gui_agents/utils/image_axis_utils.py +27 -0
- lybic_guiagents-0.1.0.dist-info/METADATA +416 -0
- lybic_guiagents-0.1.0.dist-info/RECORD +85 -0
- lybic_guiagents-0.1.0.dist-info/WHEEL +5 -0
- lybic_guiagents-0.1.0.dist-info/licenses/LICENSE +201 -0
- lybic_guiagents-0.1.0.dist-info/top_level.txt +2 -0
|
@@ -0,0 +1,193 @@
|
|
|
1
|
+
import random
|
|
2
|
+
import requests
|
|
3
|
+
import logging
|
|
4
|
+
import time
|
|
5
|
+
from typing import List, Dict, Optional
|
|
6
|
+
from dataclasses import dataclass
|
|
7
|
+
from threading import Lock
|
|
8
|
+
import json
|
|
9
|
+
|
|
10
|
+
logger = logging.getLogger("desktopenv.providers.aws.ProxyPool")
|
|
11
|
+
logger.setLevel(logging.INFO)
|
|
12
|
+
|
|
13
|
+
@dataclass
|
|
14
|
+
class ProxyInfo:
|
|
15
|
+
host: str
|
|
16
|
+
port: int
|
|
17
|
+
username: Optional[str] = None
|
|
18
|
+
password: Optional[str] = None
|
|
19
|
+
protocol: str = "http" # http, https, socks5
|
|
20
|
+
failed_count: int = 0
|
|
21
|
+
last_used: float = 0
|
|
22
|
+
is_active: bool = True
|
|
23
|
+
|
|
24
|
+
class ProxyPool:
|
|
25
|
+
def __init__(self, config_file: str = None):
|
|
26
|
+
self.proxies: List[ProxyInfo] = []
|
|
27
|
+
self.current_index = 0
|
|
28
|
+
self.lock = Lock()
|
|
29
|
+
self.max_failures = 3 # 最大失败次数
|
|
30
|
+
self.cooldown_time = 300 # 5分钟冷却时间
|
|
31
|
+
|
|
32
|
+
if config_file:
|
|
33
|
+
self.load_proxies_from_file(config_file)
|
|
34
|
+
|
|
35
|
+
def load_proxies_from_file(self, config_file: str):
|
|
36
|
+
"""从配置文件加载代理列表"""
|
|
37
|
+
try:
|
|
38
|
+
with open(config_file, 'r') as f:
|
|
39
|
+
proxy_configs = json.load(f)
|
|
40
|
+
|
|
41
|
+
for config in proxy_configs:
|
|
42
|
+
proxy = ProxyInfo(
|
|
43
|
+
host=config['host'],
|
|
44
|
+
port=config['port'],
|
|
45
|
+
username=config.get('username'),
|
|
46
|
+
password=config.get('password'),
|
|
47
|
+
protocol=config.get('protocol', 'http')
|
|
48
|
+
)
|
|
49
|
+
self.proxies.append(proxy)
|
|
50
|
+
|
|
51
|
+
logger.info(f"Loaded {len(self.proxies)} proxies from {config_file}")
|
|
52
|
+
except Exception as e:
|
|
53
|
+
logger.error(f"Failed to load proxies from {config_file}: {e}")
|
|
54
|
+
|
|
55
|
+
def add_proxy(self, host: str, port: int, username: str = None,
|
|
56
|
+
password: str = None, protocol: str = "http"):
|
|
57
|
+
"""添加代理到池中"""
|
|
58
|
+
proxy = ProxyInfo(host=host, port=port, username=username,
|
|
59
|
+
password=password, protocol=protocol)
|
|
60
|
+
with self.lock:
|
|
61
|
+
self.proxies.append(proxy)
|
|
62
|
+
logger.info(f"Added proxy {host}:{port}")
|
|
63
|
+
|
|
64
|
+
def get_next_proxy(self) -> Optional[ProxyInfo]:
|
|
65
|
+
"""获取下一个可用的代理"""
|
|
66
|
+
with self.lock:
|
|
67
|
+
if not self.proxies:
|
|
68
|
+
return None
|
|
69
|
+
|
|
70
|
+
# 过滤掉失败次数过多的代理
|
|
71
|
+
active_proxies = [p for p in self.proxies if self._is_proxy_available(p)]
|
|
72
|
+
|
|
73
|
+
if not active_proxies:
|
|
74
|
+
logger.warning("No active proxies available")
|
|
75
|
+
return None
|
|
76
|
+
|
|
77
|
+
# 轮询选择代理
|
|
78
|
+
proxy = active_proxies[self.current_index % len(active_proxies)]
|
|
79
|
+
self.current_index += 1
|
|
80
|
+
proxy.last_used = time.time()
|
|
81
|
+
|
|
82
|
+
return proxy
|
|
83
|
+
|
|
84
|
+
def _is_proxy_available(self, proxy: ProxyInfo) -> bool:
|
|
85
|
+
"""检查代理是否可用"""
|
|
86
|
+
if not proxy.is_active:
|
|
87
|
+
return False
|
|
88
|
+
|
|
89
|
+
if proxy.failed_count >= self.max_failures:
|
|
90
|
+
# 检查是否过了冷却时间
|
|
91
|
+
if time.time() - proxy.last_used < self.cooldown_time:
|
|
92
|
+
return False
|
|
93
|
+
else:
|
|
94
|
+
# 重置失败计数
|
|
95
|
+
proxy.failed_count = 0
|
|
96
|
+
|
|
97
|
+
return True
|
|
98
|
+
|
|
99
|
+
def mark_proxy_failed(self, proxy: ProxyInfo):
|
|
100
|
+
"""标记代理失败"""
|
|
101
|
+
with self.lock:
|
|
102
|
+
proxy.failed_count += 1
|
|
103
|
+
if proxy.failed_count >= self.max_failures:
|
|
104
|
+
logger.warning(f"Proxy {proxy.host}:{proxy.port} marked as failed "
|
|
105
|
+
f"(failures: {proxy.failed_count})")
|
|
106
|
+
|
|
107
|
+
def mark_proxy_success(self, proxy: ProxyInfo):
|
|
108
|
+
"""标记代理成功"""
|
|
109
|
+
with self.lock:
|
|
110
|
+
proxy.failed_count = 0
|
|
111
|
+
|
|
112
|
+
def test_proxy(self, proxy: ProxyInfo, test_url: str = "http://httpbin.org/ip",
|
|
113
|
+
timeout: int = 10) -> bool:
|
|
114
|
+
"""测试代理是否正常工作"""
|
|
115
|
+
try:
|
|
116
|
+
proxy_url = self._format_proxy_url(proxy)
|
|
117
|
+
proxies = {
|
|
118
|
+
'http': proxy_url,
|
|
119
|
+
'https': proxy_url
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
response = requests.get(test_url, proxies=proxies, timeout=timeout)
|
|
123
|
+
if response.status_code == 200:
|
|
124
|
+
self.mark_proxy_success(proxy)
|
|
125
|
+
return True
|
|
126
|
+
else:
|
|
127
|
+
self.mark_proxy_failed(proxy)
|
|
128
|
+
return False
|
|
129
|
+
|
|
130
|
+
except Exception as e:
|
|
131
|
+
logger.debug(f"Proxy test failed for {proxy.host}:{proxy.port}: {e}")
|
|
132
|
+
self.mark_proxy_failed(proxy)
|
|
133
|
+
return False
|
|
134
|
+
|
|
135
|
+
def _format_proxy_url(self, proxy: ProxyInfo) -> str:
|
|
136
|
+
"""格式化代理URL"""
|
|
137
|
+
if proxy.username and proxy.password:
|
|
138
|
+
return f"{proxy.protocol}://{proxy.username}:{proxy.password}@{proxy.host}:{proxy.port}"
|
|
139
|
+
else:
|
|
140
|
+
return f"{proxy.protocol}://{proxy.host}:{proxy.port}"
|
|
141
|
+
|
|
142
|
+
def get_proxy_dict(self, proxy: ProxyInfo) -> Dict[str, str]:
|
|
143
|
+
"""获取requests库使用的代理字典"""
|
|
144
|
+
proxy_url = self._format_proxy_url(proxy)
|
|
145
|
+
return {
|
|
146
|
+
'http': proxy_url,
|
|
147
|
+
'https': proxy_url
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
def test_all_proxies(self, test_url: str = "http://httpbin.org/ip"):
|
|
151
|
+
"""测试所有代理"""
|
|
152
|
+
logger.info("Testing all proxies...")
|
|
153
|
+
working_count = 0
|
|
154
|
+
|
|
155
|
+
for proxy in self.proxies:
|
|
156
|
+
if self.test_proxy(proxy, test_url):
|
|
157
|
+
working_count += 1
|
|
158
|
+
logger.info(f"✓ Proxy {proxy.host}:{proxy.port} is working")
|
|
159
|
+
else:
|
|
160
|
+
logger.warning(f"✗ Proxy {proxy.host}:{proxy.port} failed")
|
|
161
|
+
|
|
162
|
+
logger.info(f"Proxy test completed: {working_count}/{len(self.proxies)} working")
|
|
163
|
+
return working_count
|
|
164
|
+
|
|
165
|
+
def get_stats(self) -> Dict:
|
|
166
|
+
"""获取代理池统计信息"""
|
|
167
|
+
with self.lock:
|
|
168
|
+
total = len(self.proxies)
|
|
169
|
+
active = len([p for p in self.proxies if self._is_proxy_available(p)])
|
|
170
|
+
failed = len([p for p in self.proxies if p.failed_count >= self.max_failures])
|
|
171
|
+
|
|
172
|
+
return {
|
|
173
|
+
'total': total,
|
|
174
|
+
'active': active,
|
|
175
|
+
'failed': failed,
|
|
176
|
+
'success_rate': active / total if total > 0 else 0
|
|
177
|
+
}
|
|
178
|
+
|
|
179
|
+
# 全局代理池实例
|
|
180
|
+
_proxy_pool = None
|
|
181
|
+
|
|
182
|
+
def get_global_proxy_pool() -> ProxyPool:
|
|
183
|
+
"""获取全局代理池实例"""
|
|
184
|
+
global _proxy_pool
|
|
185
|
+
if _proxy_pool is None:
|
|
186
|
+
_proxy_pool = ProxyPool()
|
|
187
|
+
return _proxy_pool
|
|
188
|
+
|
|
189
|
+
def init_proxy_pool(config_file: str = None):
|
|
190
|
+
"""初始化全局代理池"""
|
|
191
|
+
global _proxy_pool
|
|
192
|
+
_proxy_pool = ProxyPool(config_file)
|
|
193
|
+
return _proxy_pool
|
|
File without changes
|
|
@@ -0,0 +1,87 @@
|
|
|
1
|
+
import os
|
|
2
|
+
import threading
|
|
3
|
+
import boto3
|
|
4
|
+
import psutil
|
|
5
|
+
|
|
6
|
+
import logging
|
|
7
|
+
|
|
8
|
+
from desktop_env.providers.base import VMManager
|
|
9
|
+
|
|
10
|
+
logger = logging.getLogger("desktopenv.providers.azure.AzureVMManager")
|
|
11
|
+
logger.setLevel(logging.INFO)
|
|
12
|
+
|
|
13
|
+
REGISTRY_PATH = '.azure_vms'
|
|
14
|
+
|
|
15
|
+
|
|
16
|
+
def _allocate_vm(region):
|
|
17
|
+
raise NotImplementedError
|
|
18
|
+
|
|
19
|
+
|
|
20
|
+
class AzureVMManager(VMManager):
|
|
21
|
+
def __init__(self, registry_path=REGISTRY_PATH):
|
|
22
|
+
self.registry_path = registry_path
|
|
23
|
+
self.lock = threading.Lock()
|
|
24
|
+
self.initialize_registry()
|
|
25
|
+
|
|
26
|
+
def initialize_registry(self):
|
|
27
|
+
with self.lock: # Locking during initialization
|
|
28
|
+
if not os.path.exists(self.registry_path):
|
|
29
|
+
with open(self.registry_path, 'w') as file:
|
|
30
|
+
file.write('')
|
|
31
|
+
|
|
32
|
+
def add_vm(self, vm_path, region):
|
|
33
|
+
with self.lock:
|
|
34
|
+
with open(self.registry_path, 'r') as file:
|
|
35
|
+
lines = file.readlines()
|
|
36
|
+
vm_path_at_vm_region = "{}@{}".format(vm_path, region)
|
|
37
|
+
new_lines = lines + [f'{vm_path_at_vm_region}|free\n']
|
|
38
|
+
with open(self.registry_path, 'w') as file:
|
|
39
|
+
file.writelines(new_lines)
|
|
40
|
+
|
|
41
|
+
def occupy_vm(self, vm_path, pid, region):
|
|
42
|
+
with self.lock:
|
|
43
|
+
new_lines = []
|
|
44
|
+
with open(self.registry_path, 'r') as file:
|
|
45
|
+
lines = file.readlines()
|
|
46
|
+
for line in lines:
|
|
47
|
+
registered_vm_path, _ = line.strip().split('|')
|
|
48
|
+
if registered_vm_path == "{}@{}".format(vm_path, region):
|
|
49
|
+
new_lines.append(f'{registered_vm_path}|{pid}\n')
|
|
50
|
+
else:
|
|
51
|
+
new_lines.append(line)
|
|
52
|
+
with open(self.registry_path, 'w') as file:
|
|
53
|
+
file.writelines(new_lines)
|
|
54
|
+
|
|
55
|
+
def check_and_clean(self):
|
|
56
|
+
raise NotImplementedError
|
|
57
|
+
|
|
58
|
+
def list_free_vms(self, region):
|
|
59
|
+
with self.lock: # Lock when reading the registry
|
|
60
|
+
free_vms = []
|
|
61
|
+
with open(self.registry_path, 'r') as file:
|
|
62
|
+
lines = file.readlines()
|
|
63
|
+
for line in lines:
|
|
64
|
+
vm_path_at_vm_region, pid_str = line.strip().split('|')
|
|
65
|
+
vm_path, vm_region = vm_path_at_vm_region.split("@")
|
|
66
|
+
if pid_str == "free" and vm_region == region:
|
|
67
|
+
free_vms.append((vm_path, pid_str))
|
|
68
|
+
return free_vms
|
|
69
|
+
|
|
70
|
+
def get_vm_path(self, region, screen_size=(1920, 1080), **kwargs):
|
|
71
|
+
# Note: screen_size parameter is ignored for Azure provider
|
|
72
|
+
# but kept for interface consistency with other providers
|
|
73
|
+
self.check_and_clean()
|
|
74
|
+
free_vms_paths = self.list_free_vms(region)
|
|
75
|
+
if len(free_vms_paths) == 0:
|
|
76
|
+
# No free virtual machine available, generate a new one
|
|
77
|
+
logger.info("No free virtual machine available. Generating a new one, which would take a while...☕")
|
|
78
|
+
new_vm_path = _allocate_vm(region)
|
|
79
|
+
self.add_vm(new_vm_path, region)
|
|
80
|
+
self.occupy_vm(new_vm_path, os.getpid(), region)
|
|
81
|
+
return new_vm_path
|
|
82
|
+
else:
|
|
83
|
+
# Choose the first free virtual machine
|
|
84
|
+
chosen_vm_path = free_vms_paths[0][0]
|
|
85
|
+
self.occupy_vm(chosen_vm_path, os.getpid(), region)
|
|
86
|
+
return chosen_vm_path
|
|
87
|
+
|
|
@@ -0,0 +1,207 @@
|
|
|
1
|
+
import os
|
|
2
|
+
import time
|
|
3
|
+
from azure.identity import DefaultAzureCredential
|
|
4
|
+
from azure.mgmt.compute import ComputeManagementClient
|
|
5
|
+
from azure.mgmt.network import NetworkManagementClient
|
|
6
|
+
from azure.core.exceptions import ResourceNotFoundError
|
|
7
|
+
|
|
8
|
+
import logging
|
|
9
|
+
|
|
10
|
+
from desktop_env.providers.base import Provider
|
|
11
|
+
|
|
12
|
+
logger = logging.getLogger("desktopenv.providers.azure.AzureProvider")
|
|
13
|
+
logger.setLevel(logging.INFO)
|
|
14
|
+
|
|
15
|
+
WAIT_DELAY = 15
|
|
16
|
+
MAX_ATTEMPTS = 10
|
|
17
|
+
|
|
18
|
+
# To use the Azure provider, download azure-cli by https://learn.microsoft.com/en-us/cli/azure/install-azure-cli,
|
|
19
|
+
# use "az login" to log into you Azure account,
|
|
20
|
+
# and set environment variable "AZURE_SUBSCRIPTION_ID" to your subscription ID.
|
|
21
|
+
# Provide your resource group name and VM name in the format "RESOURCE_GROUP_NAME/VM_NAME" and pass as an argument for "-p".
|
|
22
|
+
|
|
23
|
+
class AzureProvider(Provider):
|
|
24
|
+
def __init__(self, region: str = None):
|
|
25
|
+
super().__init__(region)
|
|
26
|
+
credential = DefaultAzureCredential()
|
|
27
|
+
try:
|
|
28
|
+
self.subscription_id = os.environ["AZURE_SUBSCRIPTION_ID"]
|
|
29
|
+
except:
|
|
30
|
+
logger.error("Azure subscription ID not found. Please set environment variable \"AZURE_SUBSCRIPTION_ID\".")
|
|
31
|
+
raise
|
|
32
|
+
self.compute_client = ComputeManagementClient(credential, self.subscription_id)
|
|
33
|
+
self.network_client = NetworkManagementClient(credential, self.subscription_id)
|
|
34
|
+
|
|
35
|
+
def start_emulator(self, path_to_vm: str, headless: bool, os_type: str = None, *args, **kwargs):
|
|
36
|
+
# Note: os_type parameter is ignored for Azure provider
|
|
37
|
+
# but kept for interface consistency with other providers
|
|
38
|
+
logger.info("Starting Azure VM...")
|
|
39
|
+
resource_group_name, vm_name = path_to_vm.split('/')
|
|
40
|
+
|
|
41
|
+
vm = self.compute_client.virtual_machines.get(resource_group_name, vm_name, expand='instanceView')
|
|
42
|
+
power_state = vm.instance_view.statuses[-1].code
|
|
43
|
+
if power_state == "PowerState/running":
|
|
44
|
+
logger.info("VM is already running.")
|
|
45
|
+
return
|
|
46
|
+
|
|
47
|
+
try:
|
|
48
|
+
# Start the instance
|
|
49
|
+
for _ in range(MAX_ATTEMPTS):
|
|
50
|
+
async_vm_start = self.compute_client.virtual_machines.begin_start(resource_group_name, vm_name)
|
|
51
|
+
logger.info(f"VM {path_to_vm} is starting...")
|
|
52
|
+
# Wait for the instance to start
|
|
53
|
+
async_vm_start.wait(timeout=WAIT_DELAY)
|
|
54
|
+
vm = self.compute_client.virtual_machines.get(resource_group_name, vm_name, expand='instanceView')
|
|
55
|
+
power_state = vm.instance_view.statuses[-1].code
|
|
56
|
+
if power_state == "PowerState/running":
|
|
57
|
+
logger.info(f"VM {path_to_vm} is already running.")
|
|
58
|
+
break
|
|
59
|
+
except Exception as e:
|
|
60
|
+
logger.error(f"Failed to start the Azure VM {path_to_vm}: {str(e)}")
|
|
61
|
+
raise
|
|
62
|
+
|
|
63
|
+
def get_ip_address(self, path_to_vm: str) -> str:
|
|
64
|
+
logger.info("Getting Azure VM IP address...")
|
|
65
|
+
resource_group_name, vm_name = path_to_vm.split('/')
|
|
66
|
+
|
|
67
|
+
vm = self.compute_client.virtual_machines.get(resource_group_name, vm_name)
|
|
68
|
+
|
|
69
|
+
for interface in vm.network_profile.network_interfaces:
|
|
70
|
+
name=" ".join(interface.id.split('/')[-1:])
|
|
71
|
+
sub="".join(interface.id.split('/')[4])
|
|
72
|
+
|
|
73
|
+
try:
|
|
74
|
+
thing=self.network_client.network_interfaces.get(sub, name).ip_configurations
|
|
75
|
+
|
|
76
|
+
network_card_id = thing[0].public_ip_address.id.split('/')[-1]
|
|
77
|
+
public_ip_address = self.network_client.public_ip_addresses.get(resource_group_name, network_card_id)
|
|
78
|
+
logger.info(f"VM IP address is {public_ip_address.ip_address}")
|
|
79
|
+
return public_ip_address.ip_address
|
|
80
|
+
|
|
81
|
+
except Exception as e:
|
|
82
|
+
logger.error(f"Cannot get public IP for VM {path_to_vm}")
|
|
83
|
+
raise
|
|
84
|
+
|
|
85
|
+
def save_state(self, path_to_vm: str, snapshot_name: str):
|
|
86
|
+
print("Saving Azure VM state...")
|
|
87
|
+
resource_group_name, vm_name = path_to_vm.split('/')
|
|
88
|
+
|
|
89
|
+
vm = self.compute_client.virtual_machines.get(resource_group_name, vm_name)
|
|
90
|
+
|
|
91
|
+
try:
|
|
92
|
+
# Backup each disk attached to the VM
|
|
93
|
+
for disk in vm.storage_profile.data_disks + [vm.storage_profile.os_disk]:
|
|
94
|
+
# Create a snapshot of the disk
|
|
95
|
+
snapshot = {
|
|
96
|
+
'location': vm.location,
|
|
97
|
+
'creation_data': {
|
|
98
|
+
'create_option': 'Copy',
|
|
99
|
+
'source_uri': disk.managed_disk.id
|
|
100
|
+
}
|
|
101
|
+
}
|
|
102
|
+
async_snapshot_creation = self.compute_client.snapshots.begin_create_or_update(resource_group_name, snapshot_name, snapshot)
|
|
103
|
+
async_snapshot_creation.wait(timeout=WAIT_DELAY)
|
|
104
|
+
|
|
105
|
+
logger.info(f"Successfully created snapshot {snapshot_name} for VM {path_to_vm}.")
|
|
106
|
+
except Exception as e:
|
|
107
|
+
logger.error(f"Failed to create snapshot {snapshot_name} of the Azure VM {path_to_vm}: {str(e)}")
|
|
108
|
+
raise
|
|
109
|
+
|
|
110
|
+
def revert_to_snapshot(self, path_to_vm: str, snapshot_name: str):
|
|
111
|
+
logger.info(f"Reverting VM to snapshot: {snapshot_name}...")
|
|
112
|
+
resource_group_name, vm_name = path_to_vm.split('/')
|
|
113
|
+
|
|
114
|
+
vm = self.compute_client.virtual_machines.get(resource_group_name, vm_name)
|
|
115
|
+
|
|
116
|
+
# Stop the VM for disk creation
|
|
117
|
+
logger.info(f"Stopping VM: {vm_name}")
|
|
118
|
+
async_vm_stop = self.compute_client.virtual_machines.begin_deallocate(resource_group_name, vm_name)
|
|
119
|
+
async_vm_stop.wait(timeout=WAIT_DELAY) # Wait for the VM to stop
|
|
120
|
+
|
|
121
|
+
try:
|
|
122
|
+
# Get the snapshot
|
|
123
|
+
snapshot = self.compute_client.snapshots.get(resource_group_name, snapshot_name)
|
|
124
|
+
|
|
125
|
+
# Get the original disk information
|
|
126
|
+
original_disk_id = vm.storage_profile.os_disk.managed_disk.id
|
|
127
|
+
disk_name = original_disk_id.split('/')[-1]
|
|
128
|
+
if disk_name[-1] in ['0', '1']:
|
|
129
|
+
new_disk_name = disk_name[:-1] + str(int(disk_name[-1])^1)
|
|
130
|
+
else:
|
|
131
|
+
new_disk_name = disk_name + "0"
|
|
132
|
+
|
|
133
|
+
# Delete the disk if it exists
|
|
134
|
+
self.compute_client.disks.begin_delete(resource_group_name, new_disk_name).wait(timeout=WAIT_DELAY)
|
|
135
|
+
|
|
136
|
+
# Make sure the disk is deleted before proceeding to the next step
|
|
137
|
+
disk_deleted = False
|
|
138
|
+
polling_interval = 10
|
|
139
|
+
attempts = 0
|
|
140
|
+
while not disk_deleted and attempts < MAX_ATTEMPTS:
|
|
141
|
+
try:
|
|
142
|
+
self.compute_client.disks.get(resource_group_name, new_disk_name)
|
|
143
|
+
# If the above line does not raise an exception, the disk still exists
|
|
144
|
+
time.sleep(polling_interval)
|
|
145
|
+
attempts += 1
|
|
146
|
+
except ResourceNotFoundError:
|
|
147
|
+
disk_deleted = True
|
|
148
|
+
|
|
149
|
+
if not disk_deleted:
|
|
150
|
+
logger.error(f"Disk {new_disk_name} deletion timed out.")
|
|
151
|
+
raise
|
|
152
|
+
|
|
153
|
+
# Create a new managed disk from the snapshot
|
|
154
|
+
snapshot = self.compute_client.snapshots.get(resource_group_name, snapshot_name)
|
|
155
|
+
disk_creation = {
|
|
156
|
+
'location': snapshot.location,
|
|
157
|
+
'creation_data': {
|
|
158
|
+
'create_option': 'Copy',
|
|
159
|
+
'source_resource_id': snapshot.id
|
|
160
|
+
},
|
|
161
|
+
'zones': vm.zones if vm.zones else None # Preserve the original disk's zone
|
|
162
|
+
}
|
|
163
|
+
async_disk_creation = self.compute_client.disks.begin_create_or_update(resource_group_name, new_disk_name, disk_creation)
|
|
164
|
+
restored_disk = async_disk_creation.result() # Wait for the disk creation to complete
|
|
165
|
+
|
|
166
|
+
vm.storage_profile.os_disk = {
|
|
167
|
+
'create_option': vm.storage_profile.os_disk.create_option,
|
|
168
|
+
'managed_disk': {
|
|
169
|
+
'id': restored_disk.id
|
|
170
|
+
}
|
|
171
|
+
}
|
|
172
|
+
|
|
173
|
+
async_vm_creation = self.compute_client.virtual_machines.begin_create_or_update(resource_group_name, vm_name, vm)
|
|
174
|
+
async_vm_creation.wait(timeout=WAIT_DELAY)
|
|
175
|
+
|
|
176
|
+
# Delete the original disk
|
|
177
|
+
self.compute_client.disks.begin_delete(resource_group_name, disk_name).wait()
|
|
178
|
+
|
|
179
|
+
logger.info(f"Successfully reverted to snapshot {snapshot_name}.")
|
|
180
|
+
except Exception as e:
|
|
181
|
+
logger.error(f"Failed to revert the Azure VM {path_to_vm} to snapshot {snapshot_name}: {str(e)}")
|
|
182
|
+
raise
|
|
183
|
+
|
|
184
|
+
def stop_emulator(self, path_to_vm, region=None):
|
|
185
|
+
logger.info(f"Stopping Azure VM {path_to_vm}...")
|
|
186
|
+
resource_group_name, vm_name = path_to_vm.split('/')
|
|
187
|
+
|
|
188
|
+
vm = self.compute_client.virtual_machines.get(resource_group_name, vm_name, expand='instanceView')
|
|
189
|
+
power_state = vm.instance_view.statuses[-1].code
|
|
190
|
+
if power_state == "PowerState/deallocated":
|
|
191
|
+
print("VM is already stopped.")
|
|
192
|
+
return
|
|
193
|
+
|
|
194
|
+
try:
|
|
195
|
+
for _ in range(MAX_ATTEMPTS):
|
|
196
|
+
async_vm_deallocate = self.compute_client.virtual_machines.begin_deallocate(resource_group_name, vm_name)
|
|
197
|
+
logger.info(f"Stopping VM {path_to_vm}...")
|
|
198
|
+
# Wait for the instance to start
|
|
199
|
+
async_vm_deallocate.wait(timeout=WAIT_DELAY)
|
|
200
|
+
vm = self.compute_client.virtual_machines.get(resource_group_name, vm_name, expand='instanceView')
|
|
201
|
+
power_state = vm.instance_view.statuses[-1].code
|
|
202
|
+
if power_state == "PowerState/deallocated":
|
|
203
|
+
logger.info(f"VM {path_to_vm} is already stopped.")
|
|
204
|
+
break
|
|
205
|
+
except Exception as e:
|
|
206
|
+
logger.error(f"Failed to stop the Azure VM {path_to_vm}: {str(e)}")
|
|
207
|
+
raise
|
|
@@ -0,0 +1,97 @@
|
|
|
1
|
+
from abc import ABC, abstractmethod
|
|
2
|
+
|
|
3
|
+
|
|
4
|
+
class Provider(ABC):
|
|
5
|
+
def __init__(self, region: str = None):
|
|
6
|
+
"""
|
|
7
|
+
Region of the cloud service.
|
|
8
|
+
"""
|
|
9
|
+
self.region = region
|
|
10
|
+
|
|
11
|
+
@abstractmethod
|
|
12
|
+
def start_emulator(self, path_to_vm: str, headless: bool):
|
|
13
|
+
"""
|
|
14
|
+
Method to start the emulator.
|
|
15
|
+
"""
|
|
16
|
+
pass
|
|
17
|
+
|
|
18
|
+
@abstractmethod
|
|
19
|
+
def get_ip_address(self, path_to_vm: str) -> str:
|
|
20
|
+
"""
|
|
21
|
+
Method to get the private IP address of the VM. Private IP means inside the VPC.
|
|
22
|
+
"""
|
|
23
|
+
pass
|
|
24
|
+
|
|
25
|
+
@abstractmethod
|
|
26
|
+
def save_state(self, path_to_vm: str, snapshot_name: str):
|
|
27
|
+
"""
|
|
28
|
+
Method to save the state of the VM.
|
|
29
|
+
"""
|
|
30
|
+
pass
|
|
31
|
+
|
|
32
|
+
@abstractmethod
|
|
33
|
+
def revert_to_snapshot(self, path_to_vm: str, snapshot_name: str) -> str:
|
|
34
|
+
"""
|
|
35
|
+
Method to revert the VM to a given snapshot.
|
|
36
|
+
"""
|
|
37
|
+
pass
|
|
38
|
+
|
|
39
|
+
@abstractmethod
|
|
40
|
+
def stop_emulator(self, path_to_vm: str):
|
|
41
|
+
"""
|
|
42
|
+
Method to stop the emulator.
|
|
43
|
+
"""
|
|
44
|
+
pass
|
|
45
|
+
|
|
46
|
+
|
|
47
|
+
class VMManager(ABC):
|
|
48
|
+
checked_and_cleaned = False
|
|
49
|
+
|
|
50
|
+
@abstractmethod
|
|
51
|
+
def initialize_registry(self, **kwargs):
|
|
52
|
+
"""
|
|
53
|
+
Initialize registry.
|
|
54
|
+
"""
|
|
55
|
+
pass
|
|
56
|
+
|
|
57
|
+
@abstractmethod
|
|
58
|
+
def add_vm(self, vm_path, **kwargs):
|
|
59
|
+
"""
|
|
60
|
+
Add the path of new VM to the registration.
|
|
61
|
+
"""
|
|
62
|
+
pass
|
|
63
|
+
|
|
64
|
+
@abstractmethod
|
|
65
|
+
def delete_vm(self, vm_path, **kwargs):
|
|
66
|
+
"""
|
|
67
|
+
Delete the registration of VM by path.
|
|
68
|
+
"""
|
|
69
|
+
pass
|
|
70
|
+
|
|
71
|
+
@abstractmethod
|
|
72
|
+
def occupy_vm(self, vm_path, pid, **kwargs):
|
|
73
|
+
"""
|
|
74
|
+
Mark the path of VM occupied by the pid.
|
|
75
|
+
"""
|
|
76
|
+
pass
|
|
77
|
+
|
|
78
|
+
@abstractmethod
|
|
79
|
+
def list_free_vms(self, **kwargs):
|
|
80
|
+
"""
|
|
81
|
+
List the paths of VM that are free to use allocated.
|
|
82
|
+
"""
|
|
83
|
+
pass
|
|
84
|
+
|
|
85
|
+
@abstractmethod
|
|
86
|
+
def check_and_clean(self, **kwargs):
|
|
87
|
+
"""
|
|
88
|
+
Check the registration list, and remove the paths of VM that are not in use.
|
|
89
|
+
"""
|
|
90
|
+
pass
|
|
91
|
+
|
|
92
|
+
@abstractmethod
|
|
93
|
+
def get_vm_path(self, **kwargs):
|
|
94
|
+
"""
|
|
95
|
+
Get a virtual machine that is not occupied, generate a new one if no free VM.
|
|
96
|
+
"""
|
|
97
|
+
pass
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|