agentscope-runtime 0.1.0__py3-none-any.whl → 0.1.2__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.
- agentscope_runtime/engine/agents/agentscope_agent/agent.py +1 -0
- agentscope_runtime/engine/agents/agno_agent.py +1 -0
- agentscope_runtime/engine/agents/autogen_agent.py +245 -0
- agentscope_runtime/engine/schemas/agent_schemas.py +1 -1
- agentscope_runtime/engine/services/context_manager.py +28 -1
- agentscope_runtime/engine/services/memory_service.py +2 -2
- agentscope_runtime/engine/services/rag_service.py +101 -0
- agentscope_runtime/engine/services/redis_memory_service.py +187 -0
- agentscope_runtime/engine/services/redis_session_history_service.py +155 -0
- agentscope_runtime/sandbox/box/training_box/env_service.py +1 -1
- agentscope_runtime/sandbox/box/training_box/environments/bfcl/bfcl_dataprocess.py +216 -0
- agentscope_runtime/sandbox/box/training_box/environments/bfcl/bfcl_env.py +380 -0
- agentscope_runtime/sandbox/box/training_box/environments/bfcl/env_handler.py +934 -0
- agentscope_runtime/sandbox/box/training_box/training_box.py +139 -9
- agentscope_runtime/sandbox/build.py +1 -1
- agentscope_runtime/sandbox/custom/custom_sandbox.py +0 -1
- agentscope_runtime/sandbox/custom/example.py +0 -1
- agentscope_runtime/sandbox/enums.py +2 -0
- agentscope_runtime/sandbox/manager/container_clients/__init__.py +2 -0
- agentscope_runtime/sandbox/manager/container_clients/docker_client.py +263 -11
- agentscope_runtime/sandbox/manager/container_clients/kubernetes_client.py +605 -0
- agentscope_runtime/sandbox/manager/sandbox_manager.py +112 -113
- agentscope_runtime/sandbox/manager/server/app.py +96 -28
- agentscope_runtime/sandbox/manager/server/config.py +28 -16
- agentscope_runtime/sandbox/model/__init__.py +1 -5
- agentscope_runtime/sandbox/model/container.py +3 -1
- agentscope_runtime/sandbox/model/manager_config.py +21 -15
- agentscope_runtime/sandbox/tools/tool.py +111 -0
- agentscope_runtime/version.py +1 -1
- {agentscope_runtime-0.1.0.dist-info → agentscope_runtime-0.1.2.dist-info}/METADATA +79 -13
- {agentscope_runtime-0.1.0.dist-info → agentscope_runtime-0.1.2.dist-info}/RECORD +35 -28
- agentscope_runtime/sandbox/manager/utils.py +0 -78
- {agentscope_runtime-0.1.0.dist-info → agentscope_runtime-0.1.2.dist-info}/WHEEL +0 -0
- {agentscope_runtime-0.1.0.dist-info → agentscope_runtime-0.1.2.dist-info}/entry_points.txt +0 -0
- {agentscope_runtime-0.1.0.dist-info → agentscope_runtime-0.1.2.dist-info}/licenses/LICENSE +0 -0
- {agentscope_runtime-0.1.0.dist-info → agentscope_runtime-0.1.2.dist-info}/top_level.txt +0 -0
|
@@ -7,6 +7,7 @@ with specific configuration and tool calling methods.
|
|
|
7
7
|
"""
|
|
8
8
|
import platform
|
|
9
9
|
from typing import Dict, Optional
|
|
10
|
+
import os
|
|
10
11
|
|
|
11
12
|
from ...registry import SandboxRegistry
|
|
12
13
|
from ...enums import SandboxType
|
|
@@ -21,14 +22,6 @@ def get_image_tag() -> str:
|
|
|
21
22
|
return IMAGE_TAG
|
|
22
23
|
|
|
23
24
|
|
|
24
|
-
@SandboxRegistry.register(
|
|
25
|
-
f"agentscope/runtime-sandbox-appworld:{get_image_tag()}",
|
|
26
|
-
sandbox_type=SandboxType.APPWORLD,
|
|
27
|
-
runtime_config={"shm_size": "5.06gb"},
|
|
28
|
-
security_level="medium",
|
|
29
|
-
timeout=30,
|
|
30
|
-
description="appworld Sandbox",
|
|
31
|
-
)
|
|
32
25
|
class TrainingSandbox(Sandbox):
|
|
33
26
|
"""
|
|
34
27
|
Training Sandbox class for managing and executing training-related tasks.
|
|
@@ -43,6 +36,7 @@ class TrainingSandbox(Sandbox):
|
|
|
43
36
|
timeout: int = 3000,
|
|
44
37
|
base_url: Optional[str] = None,
|
|
45
38
|
bearer_token: Optional[str] = None,
|
|
39
|
+
box_type: SandboxType = SandboxType.APPWORLD,
|
|
46
40
|
):
|
|
47
41
|
"""
|
|
48
42
|
Initialize the Training Sandbox.
|
|
@@ -58,7 +52,7 @@ class TrainingSandbox(Sandbox):
|
|
|
58
52
|
timeout,
|
|
59
53
|
base_url,
|
|
60
54
|
bearer_token,
|
|
61
|
-
|
|
55
|
+
box_type,
|
|
62
56
|
)
|
|
63
57
|
|
|
64
58
|
def create_instance(
|
|
@@ -217,3 +211,139 @@ class TrainingSandbox(Sandbox):
|
|
|
217
211
|
"instance_id": instance_id,
|
|
218
212
|
},
|
|
219
213
|
)
|
|
214
|
+
|
|
215
|
+
|
|
216
|
+
@SandboxRegistry.register(
|
|
217
|
+
f"agentscope/runtime-sandbox-appworld:{get_image_tag()}",
|
|
218
|
+
sandbox_type=SandboxType.APPWORLD,
|
|
219
|
+
runtime_config={"shm_size": "5.06gb"},
|
|
220
|
+
security_level="medium",
|
|
221
|
+
timeout=30,
|
|
222
|
+
description="appworld Sandbox",
|
|
223
|
+
)
|
|
224
|
+
class APPWorldSandbox(TrainingSandbox):
|
|
225
|
+
"""
|
|
226
|
+
Training Sandbox class for managing and executing training-related tasks.
|
|
227
|
+
|
|
228
|
+
This class provides methods to create, manage, and interact with
|
|
229
|
+
training environment instances using specialized tool calls.
|
|
230
|
+
"""
|
|
231
|
+
|
|
232
|
+
def __init__(
|
|
233
|
+
self,
|
|
234
|
+
sandbox_id: Optional[str] = None,
|
|
235
|
+
timeout: int = 3000,
|
|
236
|
+
base_url: Optional[str] = None,
|
|
237
|
+
bearer_token: Optional[str] = None,
|
|
238
|
+
):
|
|
239
|
+
"""
|
|
240
|
+
Initialize the Training Sandbox.
|
|
241
|
+
|
|
242
|
+
Args:
|
|
243
|
+
sandbox_id (Optional[str]): Unique identifier for the sandbox.
|
|
244
|
+
timeout (int): Maximum time allowed for sandbox operations.
|
|
245
|
+
base_url (Optional[str]): Base URL for sandbox API.
|
|
246
|
+
bearer_token (Optional[str]): Authentication token for API access.
|
|
247
|
+
"""
|
|
248
|
+
super().__init__(
|
|
249
|
+
sandbox_id,
|
|
250
|
+
timeout,
|
|
251
|
+
base_url,
|
|
252
|
+
bearer_token,
|
|
253
|
+
SandboxType.APPWORLD,
|
|
254
|
+
)
|
|
255
|
+
|
|
256
|
+
|
|
257
|
+
DATASET_SUB_TYPE = os.environ.get("DATASET_SUB_TYPE", "multi_turn")
|
|
258
|
+
|
|
259
|
+
|
|
260
|
+
@SandboxRegistry.register(
|
|
261
|
+
f"agentscope/runtime-sandbox-bfcl:{get_image_tag()}",
|
|
262
|
+
sandbox_type=SandboxType.BFCL,
|
|
263
|
+
runtime_config={"shm_size": "8.06gb"},
|
|
264
|
+
security_level="medium",
|
|
265
|
+
environment={
|
|
266
|
+
"OPENAI_API_KEY": os.environ.get("OPENAI_API_KEY", ""),
|
|
267
|
+
"BFCL_DATA_PATH": f"/agentscope_runtime/training_box/bfcl/multi_turn/"
|
|
268
|
+
f"{DATASET_SUB_TYPE}_processed.jsonl",
|
|
269
|
+
"BFCL_SPLID_ID_PATH": f"/agentscope_runtime/training_box/"
|
|
270
|
+
f"bfcl/multi_turn/"
|
|
271
|
+
f"{DATASET_SUB_TYPE}_split_ids.json",
|
|
272
|
+
},
|
|
273
|
+
# ["all","all_scoring","multi_turn","single_turn",
|
|
274
|
+
# "live","non_live","non_python","python"]
|
|
275
|
+
timeout=30,
|
|
276
|
+
description="bfcl Sandbox",
|
|
277
|
+
)
|
|
278
|
+
class BFCLSandbox(TrainingSandbox):
|
|
279
|
+
"""
|
|
280
|
+
Training Sandbox class for managing and executing training-related tasks.
|
|
281
|
+
|
|
282
|
+
This class provides methods to create, manage, and interact with
|
|
283
|
+
training environment instances using specialized tool calls.
|
|
284
|
+
"""
|
|
285
|
+
|
|
286
|
+
def __init__(
|
|
287
|
+
self,
|
|
288
|
+
sandbox_id: Optional[str] = None,
|
|
289
|
+
timeout: int = 3000,
|
|
290
|
+
base_url: Optional[str] = None,
|
|
291
|
+
bearer_token: Optional[str] = None,
|
|
292
|
+
):
|
|
293
|
+
"""
|
|
294
|
+
Initialize the Training Sandbox.
|
|
295
|
+
|
|
296
|
+
Args:
|
|
297
|
+
sandbox_id (Optional[str]): Unique identifier for the sandbox.
|
|
298
|
+
timeout (int): Maximum time allowed for sandbox operations.
|
|
299
|
+
base_url (Optional[str]): Base URL for sandbox API.
|
|
300
|
+
bearer_token (Optional[str]): Authentication token for API access.
|
|
301
|
+
"""
|
|
302
|
+
super().__init__(
|
|
303
|
+
sandbox_id,
|
|
304
|
+
timeout,
|
|
305
|
+
base_url,
|
|
306
|
+
bearer_token,
|
|
307
|
+
SandboxType.BFCL,
|
|
308
|
+
)
|
|
309
|
+
|
|
310
|
+
|
|
311
|
+
@SandboxRegistry.register(
|
|
312
|
+
f"agentscope/runtime-sandbox-webshop:{get_image_tag()}",
|
|
313
|
+
sandbox_type=SandboxType.WEBSHOP,
|
|
314
|
+
runtime_config={"shm_size": "5.06gb"},
|
|
315
|
+
security_level="medium",
|
|
316
|
+
timeout=30,
|
|
317
|
+
description="webshop Sandbox",
|
|
318
|
+
)
|
|
319
|
+
class WebShopSandbox(TrainingSandbox):
|
|
320
|
+
"""
|
|
321
|
+
Training Sandbox class for managing and executing training-related tasks.
|
|
322
|
+
|
|
323
|
+
This class provides methods to create, manage, and interact with
|
|
324
|
+
training environment instances using specialized tool calls.
|
|
325
|
+
"""
|
|
326
|
+
|
|
327
|
+
def __init__(
|
|
328
|
+
self,
|
|
329
|
+
sandbox_id: Optional[str] = None,
|
|
330
|
+
timeout: int = 3000,
|
|
331
|
+
base_url: Optional[str] = None,
|
|
332
|
+
bearer_token: Optional[str] = None,
|
|
333
|
+
):
|
|
334
|
+
"""
|
|
335
|
+
Initialize the Training Sandbox.
|
|
336
|
+
|
|
337
|
+
Args:
|
|
338
|
+
sandbox_id (Optional[str]): Unique identifier for the sandbox.
|
|
339
|
+
timeout (int): Maximum time allowed for sandbox operations.
|
|
340
|
+
base_url (Optional[str]): Base URL for sandbox API.
|
|
341
|
+
bearer_token (Optional[str]): Authentication token for API access.
|
|
342
|
+
"""
|
|
343
|
+
super().__init__(
|
|
344
|
+
sandbox_id,
|
|
345
|
+
timeout,
|
|
346
|
+
base_url,
|
|
347
|
+
bearer_token,
|
|
348
|
+
SandboxType.BFCL,
|
|
349
|
+
)
|
|
@@ -14,7 +14,6 @@ SANDBOXTYPE = "custom_sandbox"
|
|
|
14
14
|
@SandboxRegistry.register(
|
|
15
15
|
f"agentscope/runtime-sandbox-{SANDBOXTYPE}:{IMAGE_TAG}",
|
|
16
16
|
sandbox_type=SANDBOXTYPE,
|
|
17
|
-
resource_limits={"memory": "16Gi", "cpu": "4"},
|
|
18
17
|
security_level="medium",
|
|
19
18
|
timeout=60,
|
|
20
19
|
description="my sandbox",
|
|
@@ -12,7 +12,6 @@ SANDBOX_TYPE = "example"
|
|
|
12
12
|
@SandboxRegistry.register(
|
|
13
13
|
f"agentscope/runtime-sandbox-{SANDBOX_TYPE}:{IMAGE_TAG}",
|
|
14
14
|
sandbox_type=SANDBOX_TYPE,
|
|
15
|
-
resource_limits={"memory": "16Gi", "cpu": "4"},
|
|
16
15
|
security_level="medium",
|
|
17
16
|
timeout=60,
|
|
18
17
|
description="Example sandbox",
|
|
@@ -1,16 +1,201 @@
|
|
|
1
1
|
# -*- coding: utf-8 -*-
|
|
2
2
|
import traceback
|
|
3
3
|
import logging
|
|
4
|
+
import platform
|
|
5
|
+
import socket
|
|
6
|
+
import subprocess
|
|
4
7
|
|
|
5
8
|
import docker
|
|
9
|
+
|
|
6
10
|
from .base_client import BaseClient
|
|
11
|
+
from ..collections import RedisSetCollection, InMemorySetCollection
|
|
7
12
|
|
|
8
13
|
|
|
9
14
|
logger = logging.getLogger(__name__)
|
|
10
15
|
|
|
11
16
|
|
|
17
|
+
def is_port_available(port):
|
|
18
|
+
"""
|
|
19
|
+
Check if a given port is available (not in use) on the local system.
|
|
20
|
+
|
|
21
|
+
Args:
|
|
22
|
+
port (int): The port number to check.
|
|
23
|
+
|
|
24
|
+
Returns:
|
|
25
|
+
bool: True if the port is available, False if it is in use.
|
|
26
|
+
"""
|
|
27
|
+
with socket.socket(socket.AF_INET, socket.SOCK_STREAM) as s:
|
|
28
|
+
try:
|
|
29
|
+
s.bind(("", port))
|
|
30
|
+
# Port is available
|
|
31
|
+
return True
|
|
32
|
+
except OSError:
|
|
33
|
+
# Port is in use
|
|
34
|
+
return False
|
|
35
|
+
|
|
36
|
+
|
|
37
|
+
def sweep_port(port):
|
|
38
|
+
"""Sweep all processes found listening on a given port.
|
|
39
|
+
|
|
40
|
+
Args:
|
|
41
|
+
port (int): The port number.
|
|
42
|
+
|
|
43
|
+
Returns:
|
|
44
|
+
bool: True if successful, False if failed.
|
|
45
|
+
"""
|
|
46
|
+
try:
|
|
47
|
+
system = platform.system().lower()
|
|
48
|
+
if system == "windows":
|
|
49
|
+
return _sweep_port_windows(port)
|
|
50
|
+
else:
|
|
51
|
+
return _sweep_port_unix(port)
|
|
52
|
+
except Exception as e:
|
|
53
|
+
logger.error(
|
|
54
|
+
f"An error occurred while killing processes on port {port}: {e}",
|
|
55
|
+
)
|
|
56
|
+
return False
|
|
57
|
+
|
|
58
|
+
|
|
59
|
+
def _sweep_port_unix(port):
|
|
60
|
+
"""
|
|
61
|
+
Sweep all processes found listening on a given port.
|
|
62
|
+
|
|
63
|
+
Args:
|
|
64
|
+
port (int): The port number.
|
|
65
|
+
|
|
66
|
+
Returns:
|
|
67
|
+
int: Number of processes swept (terminated).
|
|
68
|
+
"""
|
|
69
|
+
try:
|
|
70
|
+
# Use lsof to find the processes using the port
|
|
71
|
+
# TODO: support windows
|
|
72
|
+
result = subprocess.run(
|
|
73
|
+
["lsof", "-i", f":{port}"],
|
|
74
|
+
capture_output=True,
|
|
75
|
+
text=True,
|
|
76
|
+
check=True,
|
|
77
|
+
)
|
|
78
|
+
|
|
79
|
+
# Parse the output
|
|
80
|
+
lines = result.stdout.strip().split("\n")
|
|
81
|
+
if len(lines) <= 1:
|
|
82
|
+
# No process is using the port
|
|
83
|
+
return True
|
|
84
|
+
|
|
85
|
+
# Iterate over each line (excluding the header) and kill each process
|
|
86
|
+
killed_count = 0
|
|
87
|
+
for line in lines[1:]:
|
|
88
|
+
parts = line.split()
|
|
89
|
+
if len(parts) > 1:
|
|
90
|
+
pid = parts[1]
|
|
91
|
+
|
|
92
|
+
# Kill the process using the PID
|
|
93
|
+
subprocess.run(["kill", "-9", pid], check=False)
|
|
94
|
+
killed_count += 1
|
|
95
|
+
|
|
96
|
+
if not is_port_available(port):
|
|
97
|
+
logger.warning(
|
|
98
|
+
f"Port {port} is still in use after killing processes.",
|
|
99
|
+
)
|
|
100
|
+
|
|
101
|
+
return True
|
|
102
|
+
|
|
103
|
+
except Exception as e:
|
|
104
|
+
logger.error(
|
|
105
|
+
f"An error occurred while killing processes on port {port}: {e}",
|
|
106
|
+
)
|
|
107
|
+
return False
|
|
108
|
+
|
|
109
|
+
|
|
110
|
+
def _sweep_port_windows(port):
|
|
111
|
+
"""
|
|
112
|
+
Windows implementation using netstat and taskkill
|
|
113
|
+
"""
|
|
114
|
+
try:
|
|
115
|
+
# Use netstat to find the processes using the port
|
|
116
|
+
result = subprocess.run(
|
|
117
|
+
["netstat", "-ano"],
|
|
118
|
+
capture_output=True,
|
|
119
|
+
text=True,
|
|
120
|
+
check=True,
|
|
121
|
+
)
|
|
122
|
+
|
|
123
|
+
# Parse the output to find processes using the specific port
|
|
124
|
+
lines = result.stdout.strip().split("\n")
|
|
125
|
+
pids_to_kill = set()
|
|
126
|
+
|
|
127
|
+
for line in lines:
|
|
128
|
+
if f":{port}" in line and "LISTENING" in line:
|
|
129
|
+
parts = line.split()
|
|
130
|
+
if len(parts) >= 5:
|
|
131
|
+
pid = parts[-1] # PID is usually the last column
|
|
132
|
+
if pid.isdigit(): # Ensure it's a valid PID
|
|
133
|
+
pids_to_kill.add(pid)
|
|
134
|
+
|
|
135
|
+
if not pids_to_kill:
|
|
136
|
+
return True
|
|
137
|
+
|
|
138
|
+
# Kill the processes
|
|
139
|
+
killed_count = 0
|
|
140
|
+
for pid in pids_to_kill:
|
|
141
|
+
try:
|
|
142
|
+
result = subprocess.run(
|
|
143
|
+
["taskkill", "/PID", pid, "/F"],
|
|
144
|
+
capture_output=True,
|
|
145
|
+
text=True,
|
|
146
|
+
check=False,
|
|
147
|
+
)
|
|
148
|
+
if result.returncode == 0:
|
|
149
|
+
killed_count += 1
|
|
150
|
+
except Exception as e:
|
|
151
|
+
logger.debug(f"Failed to kill process {pid}: {e}")
|
|
152
|
+
continue
|
|
153
|
+
|
|
154
|
+
if not is_port_available(port):
|
|
155
|
+
logger.warning(
|
|
156
|
+
f"Port {port} is still in use after killing processes.",
|
|
157
|
+
)
|
|
158
|
+
|
|
159
|
+
return True
|
|
160
|
+
|
|
161
|
+
except subprocess.CalledProcessError as e:
|
|
162
|
+
logger.error(f"netstat command failed: {e}")
|
|
163
|
+
return False
|
|
164
|
+
except Exception as e:
|
|
165
|
+
logger.error(f"Error in Windows port sweep: {e}")
|
|
166
|
+
return False
|
|
167
|
+
|
|
168
|
+
|
|
12
169
|
class DockerClient(BaseClient):
|
|
13
|
-
def __init__(self):
|
|
170
|
+
def __init__(self, config=None):
|
|
171
|
+
self.config = config
|
|
172
|
+
self.port_range = range(*self.config.port_range)
|
|
173
|
+
|
|
174
|
+
if self.config.redis_enabled:
|
|
175
|
+
import redis
|
|
176
|
+
|
|
177
|
+
redis_client = redis.Redis(
|
|
178
|
+
host=self.config.redis_server,
|
|
179
|
+
port=self.config.redis_port,
|
|
180
|
+
db=self.config.redis_db,
|
|
181
|
+
username=self.config.redis_user,
|
|
182
|
+
password=self.config.redis_password,
|
|
183
|
+
decode_responses=True,
|
|
184
|
+
)
|
|
185
|
+
try:
|
|
186
|
+
redis_client.ping()
|
|
187
|
+
except ConnectionError as e:
|
|
188
|
+
raise RuntimeError(
|
|
189
|
+
"Unable to connect to the Redis server.",
|
|
190
|
+
) from e
|
|
191
|
+
|
|
192
|
+
self.port_set = RedisSetCollection(
|
|
193
|
+
redis_client,
|
|
194
|
+
set_name=self.config.redis_port_key,
|
|
195
|
+
)
|
|
196
|
+
else:
|
|
197
|
+
self.port_set = InMemorySetCollection()
|
|
198
|
+
|
|
14
199
|
try:
|
|
15
200
|
self.client = docker.from_env()
|
|
16
201
|
except Exception as e:
|
|
@@ -32,9 +217,12 @@ class DockerClient(BaseClient):
|
|
|
32
217
|
acr_registry = "agentscope-registry.ap-southeast-1.cr.aliyuncs.com"
|
|
33
218
|
acr_image = f"{acr_registry}/{image}"
|
|
34
219
|
|
|
35
|
-
logger.
|
|
220
|
+
logger.info(
|
|
221
|
+
f"Attempting to pull from ACR: {acr_image}, it might take "
|
|
222
|
+
f"several minutes.",
|
|
223
|
+
)
|
|
36
224
|
self.client.images.pull(acr_image)
|
|
37
|
-
logger.
|
|
225
|
+
logger.info(f"Successfully pulled image from ACR: {acr_image}")
|
|
38
226
|
|
|
39
227
|
# Retag the image
|
|
40
228
|
acr_img_obj = self.client.images.get(acr_image)
|
|
@@ -49,7 +237,9 @@ class DockerClient(BaseClient):
|
|
|
49
237
|
logger.debug(f"Failed to remove original tag: {e}")
|
|
50
238
|
return True
|
|
51
239
|
except Exception as e:
|
|
52
|
-
logger.error(
|
|
240
|
+
logger.error(
|
|
241
|
+
f"Failed to pull from ACR: {e}, {traceback.format_exc()}",
|
|
242
|
+
)
|
|
53
243
|
return False
|
|
54
244
|
|
|
55
245
|
def create(
|
|
@@ -65,17 +255,28 @@ class DockerClient(BaseClient):
|
|
|
65
255
|
if runtime_config is None:
|
|
66
256
|
runtime_config = {}
|
|
67
257
|
|
|
258
|
+
port_mapping = {}
|
|
259
|
+
|
|
260
|
+
if ports:
|
|
261
|
+
free_port = self._find_free_ports(len(ports))
|
|
262
|
+
for container_port, host_port in zip(ports, free_port):
|
|
263
|
+
port_mapping[container_port] = host_port
|
|
264
|
+
|
|
68
265
|
try:
|
|
69
266
|
try:
|
|
70
267
|
# Check if the image exists locally
|
|
71
268
|
self.client.images.get(image)
|
|
72
269
|
logger.debug(f"Image '{image}' found locally.")
|
|
73
270
|
except docker.errors.ImageNotFound:
|
|
74
|
-
logger.
|
|
271
|
+
logger.info(
|
|
75
272
|
f"Image '{image}' not found locally. "
|
|
76
273
|
f"Attempting to pull it...",
|
|
77
274
|
)
|
|
78
275
|
try:
|
|
276
|
+
logger.info(
|
|
277
|
+
f"Attempting to pull: {image}, "
|
|
278
|
+
f"it might take several minutes.",
|
|
279
|
+
)
|
|
79
280
|
self.client.images.pull(image)
|
|
80
281
|
logger.debug(
|
|
81
282
|
f"Image '{image}' successfully pulled from default "
|
|
@@ -86,7 +287,8 @@ class DockerClient(BaseClient):
|
|
|
86
287
|
logger.warning(
|
|
87
288
|
f"Failed to pull from default registry: {e}",
|
|
88
289
|
)
|
|
89
|
-
logger.
|
|
290
|
+
logger.warning("Trying to pull from ACR fallback...")
|
|
291
|
+
|
|
90
292
|
pull_success = self._try_pull_from_acr(image)
|
|
91
293
|
|
|
92
294
|
if not pull_success:
|
|
@@ -94,27 +296,28 @@ class DockerClient(BaseClient):
|
|
|
94
296
|
f"Failed to pull image '{image}' from both "
|
|
95
297
|
f"default and ACR",
|
|
96
298
|
)
|
|
97
|
-
return
|
|
299
|
+
return None, None, None
|
|
98
300
|
|
|
99
301
|
except docker.errors.APIError as e:
|
|
100
302
|
logger.error(f"Error occurred while checking the image: {e}")
|
|
101
|
-
return
|
|
303
|
+
return None, None, None
|
|
102
304
|
|
|
103
305
|
# Create and run the container
|
|
104
306
|
container = self.client.containers.run(
|
|
105
307
|
image,
|
|
106
308
|
detach=True,
|
|
107
|
-
ports=
|
|
309
|
+
ports=port_mapping,
|
|
108
310
|
name=name,
|
|
109
311
|
volumes=volumes,
|
|
110
312
|
environment=environment,
|
|
111
313
|
**runtime_config,
|
|
112
314
|
)
|
|
113
315
|
container.reload()
|
|
114
|
-
|
|
316
|
+
_id = container.id
|
|
317
|
+
return _id, list(port_mapping.values()), "localhost"
|
|
115
318
|
except Exception as e:
|
|
116
319
|
logger.error(f"An error occurred: {e}, {traceback.format_exc()}")
|
|
117
|
-
return
|
|
320
|
+
return None, None, None
|
|
118
321
|
|
|
119
322
|
def start(self, container_id):
|
|
120
323
|
"""Start a Docker container."""
|
|
@@ -122,6 +325,17 @@ class DockerClient(BaseClient):
|
|
|
122
325
|
container = self.client.containers.get(
|
|
123
326
|
container_id,
|
|
124
327
|
)
|
|
328
|
+
|
|
329
|
+
# Check whether the ports are occupied by other processes
|
|
330
|
+
port_mapping = container.attrs["NetworkSettings"]["Ports"]
|
|
331
|
+
for _, mappings in port_mapping.items():
|
|
332
|
+
if mappings is not None:
|
|
333
|
+
for mapping in mappings:
|
|
334
|
+
host_port = int(mapping["HostPort"])
|
|
335
|
+
if is_port_available(host_port):
|
|
336
|
+
continue
|
|
337
|
+
sweep_port(host_port["HostPort"])
|
|
338
|
+
|
|
125
339
|
container.start()
|
|
126
340
|
return True
|
|
127
341
|
except Exception as e:
|
|
@@ -146,7 +360,17 @@ class DockerClient(BaseClient):
|
|
|
146
360
|
container = self.client.containers.get(
|
|
147
361
|
container_id,
|
|
148
362
|
)
|
|
363
|
+
# Remove ports
|
|
364
|
+
port_mapping = container.attrs["NetworkSettings"]["Ports"]
|
|
149
365
|
container.remove(force=force)
|
|
366
|
+
|
|
367
|
+
# Iterate over each port and its mappings
|
|
368
|
+
for _, mappings in port_mapping.items():
|
|
369
|
+
if mappings is not None:
|
|
370
|
+
for mapping in mappings:
|
|
371
|
+
host_port = int(mapping["HostPort"])
|
|
372
|
+
self.port_set.remove(host_port)
|
|
373
|
+
|
|
150
374
|
return True
|
|
151
375
|
except Exception as e:
|
|
152
376
|
logger.error(f"An error occurred: {e}, {traceback.format_exc()}")
|
|
@@ -168,3 +392,31 @@ class DockerClient(BaseClient):
|
|
|
168
392
|
if container_attrs:
|
|
169
393
|
return container_attrs["State"]["Status"]
|
|
170
394
|
return None
|
|
395
|
+
|
|
396
|
+
def _find_free_ports(self, n):
|
|
397
|
+
free_ports = []
|
|
398
|
+
|
|
399
|
+
for port in self.port_range:
|
|
400
|
+
if len(free_ports) >= n:
|
|
401
|
+
break # We have found enough ports
|
|
402
|
+
|
|
403
|
+
if not self.port_set.add(port):
|
|
404
|
+
continue
|
|
405
|
+
|
|
406
|
+
with socket.socket(socket.AF_INET, socket.SOCK_STREAM) as s:
|
|
407
|
+
try:
|
|
408
|
+
s.bind(("", port))
|
|
409
|
+
free_ports.append(port) # Port is available
|
|
410
|
+
|
|
411
|
+
except OSError:
|
|
412
|
+
# Bind failed, port is in use
|
|
413
|
+
self.port_set.remove(port)
|
|
414
|
+
# Try the next one
|
|
415
|
+
continue
|
|
416
|
+
|
|
417
|
+
if len(free_ports) < n:
|
|
418
|
+
raise RuntimeError(
|
|
419
|
+
"Not enough free ports available in the specified range.",
|
|
420
|
+
)
|
|
421
|
+
|
|
422
|
+
return free_ports
|