manta-node 0.5b0__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.
- manta_node/__init__.py +5 -0
- manta_node/__main__.py +57 -0
- manta_node/cli/__init__.py +6 -0
- manta_node/cli/commands/__init__.py +21 -0
- manta_node/cli/commands/cluster.py +419 -0
- manta_node/cli/commands/config.py +490 -0
- manta_node/cli/commands/logs.py +168 -0
- manta_node/cli/commands/start.py +459 -0
- manta_node/cli/commands/status.py +204 -0
- manta_node/cli/commands/stop.py +253 -0
- manta_node/cli/config_manager.py +139 -0
- manta_node/cli/main.py +133 -0
- manta_node/cli/version.py +106 -0
- manta_node/domain/__init__.py +8 -0
- manta_node/domain/task_lifecycle.py +90 -0
- manta_node/infrastructure/__init__.py +13 -0
- manta_node/infrastructure/config/__init__.py +31 -0
- manta_node/infrastructure/config/node_config_manager.py +918 -0
- manta_node/infrastructure/container/__init__.py +11 -0
- manta_node/infrastructure/container/docker_adapter.py +253 -0
- manta_node/infrastructure/container/image_executor.py +222 -0
- manta_node/infrastructure/container/manager.py +549 -0
- manta_node/infrastructure/filesystem/__init__.py +7 -0
- manta_node/infrastructure/filesystem/dataset_manager.py +469 -0
- manta_node/infrastructure/grpc/__init__.py +11 -0
- manta_node/infrastructure/grpc/client.py +373 -0
- manta_node/infrastructure/grpc/local_servicer.py +151 -0
- manta_node/infrastructure/grpc/world_servicer.py +284 -0
- manta_node/infrastructure/metrics/__init__.py +10 -0
- manta_node/infrastructure/metrics/collector.py +94 -0
- manta_node/infrastructure/metrics/metrics_collector.py +351 -0
- manta_node/infrastructure/mqtt/__init__.py +7 -0
- manta_node/infrastructure/mqtt/command_handler.py +450 -0
- manta_node/node_orchestrator.py +462 -0
- manta_node/task_manager.py +677 -0
- manta_node/tasks.py +273 -0
- manta_node/utils.py +52 -0
- manta_node-0.5b0.dist-info/METADATA +794 -0
- manta_node-0.5b0.dist-info/RECORD +43 -0
- manta_node-0.5b0.dist-info/WHEEL +5 -0
- manta_node-0.5b0.dist-info/entry_points.txt +2 -0
- manta_node-0.5b0.dist-info/licenses/LICENSE +683 -0
- manta_node-0.5b0.dist-info/top_level.txt +1 -0
|
@@ -0,0 +1,549 @@
|
|
|
1
|
+
import os
|
|
2
|
+
import subprocess
|
|
3
|
+
import sys
|
|
4
|
+
from datetime import datetime, timezone
|
|
5
|
+
from math import nan
|
|
6
|
+
from pathlib import Path
|
|
7
|
+
from typing import Any, Dict, Optional, Tuple, Union, cast
|
|
8
|
+
|
|
9
|
+
import aiodocker
|
|
10
|
+
import docker
|
|
11
|
+
from aiodocker.docker import DockerContainer
|
|
12
|
+
|
|
13
|
+
from manta_common.build.common.tasks import TaskStatus
|
|
14
|
+
from manta_common.conversions import ID
|
|
15
|
+
from manta_common.errors import MantaDockerError
|
|
16
|
+
from manta_common.traces import Tracer
|
|
17
|
+
from ...domain.task_lifecycle import TaskLifecycleService
|
|
18
|
+
from .docker_adapter import DockerRaiser
|
|
19
|
+
|
|
20
|
+
|
|
21
|
+
class ContainerManager(DockerRaiser):
|
|
22
|
+
"""
|
|
23
|
+
ContainerManager class to run tasks in docker containers
|
|
24
|
+
|
|
25
|
+
Parameters
|
|
26
|
+
----------
|
|
27
|
+
node_id: ID
|
|
28
|
+
Node ID
|
|
29
|
+
docker_client: DockerClient
|
|
30
|
+
Docker client
|
|
31
|
+
async_client: AiodockerClient
|
|
32
|
+
Aiodocker client
|
|
33
|
+
containers: Dict[ID, DockerContainer]
|
|
34
|
+
Containers
|
|
35
|
+
gpu_config: dict
|
|
36
|
+
GPU configuration
|
|
37
|
+
"""
|
|
38
|
+
|
|
39
|
+
tracer: Tracer
|
|
40
|
+
|
|
41
|
+
def __init__(self):
|
|
42
|
+
self.docker_client = docker.from_env()
|
|
43
|
+
|
|
44
|
+
# Windows-specific: Use TCP connection instead of Named Pipes to avoid event loop conflict
|
|
45
|
+
# SECURITY NOTE: Port 2375 is typically unencrypted. For production deployments on Windows:
|
|
46
|
+
# 1. Enable TLS on Docker daemon (port 2376)
|
|
47
|
+
# 2. Configure client certificates
|
|
48
|
+
# 3. Update connection URL to tcp://localhost:2376 with tls=True
|
|
49
|
+
# See: https://docs.docker.com/engine/security/protect-access/
|
|
50
|
+
if sys.platform.lower() == "win32" or os.name.lower() == "nt":
|
|
51
|
+
try:
|
|
52
|
+
# Docker Desktop on Windows exposes the API on tcp://localhost:2375
|
|
53
|
+
# WARNING: This connection is unencrypted. Use only in development.
|
|
54
|
+
self.async_client = aiodocker.Docker(url="tcp://localhost:2375")
|
|
55
|
+
self.tracer.warning(
|
|
56
|
+
"Using unencrypted Docker TCP connection on Windows (port 2375). "
|
|
57
|
+
"For production, configure TLS on port 2376."
|
|
58
|
+
)
|
|
59
|
+
except Exception as e:
|
|
60
|
+
self.tracer.error(
|
|
61
|
+
f"Failed to connect to Docker on Windows via TCP: {e}. "
|
|
62
|
+
"Ensure Docker Desktop is running and exposing TCP port 2375."
|
|
63
|
+
)
|
|
64
|
+
raise
|
|
65
|
+
else:
|
|
66
|
+
# Unix systems use the default socket
|
|
67
|
+
self.async_client = aiodocker.Docker()
|
|
68
|
+
|
|
69
|
+
self.containers: Dict[Tuple[ID, ID], DockerContainer] = {}
|
|
70
|
+
self.gpu_config = self.get_gpu_config()
|
|
71
|
+
if self.gpu_config is not None:
|
|
72
|
+
self.tracer.debug(
|
|
73
|
+
f"GPU support detected: \n{self.gpu_config}"
|
|
74
|
+
) # pragma: no cover
|
|
75
|
+
else:
|
|
76
|
+
self.tracer.warning("No GPU support detected.")
|
|
77
|
+
|
|
78
|
+
@property
|
|
79
|
+
def gpu_supported(self):
|
|
80
|
+
return self.gpu_config is not None
|
|
81
|
+
|
|
82
|
+
def get_gpu_config(self) -> Optional[Dict[str, Any]]:
|
|
83
|
+
"""
|
|
84
|
+
Check if GPU is supported and return the appropriate config field for aiodocker.
|
|
85
|
+
|
|
86
|
+
Returns
|
|
87
|
+
-------
|
|
88
|
+
Optional[Dict[str, Any]]
|
|
89
|
+
GPU configuration
|
|
90
|
+
"""
|
|
91
|
+
# Get Docker version
|
|
92
|
+
version_info = self.docker_client.version()
|
|
93
|
+
docker_version = tuple(
|
|
94
|
+
map(int, version_info["Version"].split(".")[:2])
|
|
95
|
+
) # Extract major.minor
|
|
96
|
+
|
|
97
|
+
# Check runtimes if Docker < 19.03
|
|
98
|
+
if docker_version < (19, 3):
|
|
99
|
+
if self._is_nvidia_runtime_installed():
|
|
100
|
+
return {"Runtime": "nvidia"} # Old way
|
|
101
|
+
return None # No GPU support
|
|
102
|
+
|
|
103
|
+
# Check for NVIDIA Container Toolkit (Docker >= 19.03)
|
|
104
|
+
if self._is_nvidia_toolkit_installed():
|
|
105
|
+
return {
|
|
106
|
+
"HostConfig": {
|
|
107
|
+
"DeviceRequests": [
|
|
108
|
+
{"Driver": "nvidia", "Count": -1, "Capabilities": [["gpu"]]}
|
|
109
|
+
]
|
|
110
|
+
}
|
|
111
|
+
}
|
|
112
|
+
elif self._is_nvidia_runtime_installed():
|
|
113
|
+
return {"Runtime": "nvidia"}
|
|
114
|
+
|
|
115
|
+
return None # No GPU support detected
|
|
116
|
+
|
|
117
|
+
def _is_nvidia_runtime_installed(self):
|
|
118
|
+
"""Method 1: Check if nvidia runtime is installed."""
|
|
119
|
+
info = self.docker_client.info()
|
|
120
|
+
return "nvidia" in info.get("Runtimes", {})
|
|
121
|
+
|
|
122
|
+
def _is_nvidia_toolkit_installed(self):
|
|
123
|
+
"""Method 2: Check if nvidia-container-toolkit is installed."""
|
|
124
|
+
try:
|
|
125
|
+
subprocess.run(
|
|
126
|
+
["nvidia-container-cli", "-V"],
|
|
127
|
+
stdout=subprocess.PIPE,
|
|
128
|
+
stderr=subprocess.PIPE,
|
|
129
|
+
check=True,
|
|
130
|
+
)
|
|
131
|
+
return True
|
|
132
|
+
except (FileNotFoundError, subprocess.CalledProcessError) as e:
|
|
133
|
+
self.tracer.debug(f"nvidia-container-cli not found: {e}")
|
|
134
|
+
|
|
135
|
+
return False
|
|
136
|
+
|
|
137
|
+
async def ensure_tailscale_service(
|
|
138
|
+
self,
|
|
139
|
+
swarm_id: ID,
|
|
140
|
+
authkey: str,
|
|
141
|
+
login_server: str,
|
|
142
|
+
hostname: str,
|
|
143
|
+
temp_folder: Path,
|
|
144
|
+
):
|
|
145
|
+
"""
|
|
146
|
+
Ensure the tailscale service is running
|
|
147
|
+
|
|
148
|
+
Parameters
|
|
149
|
+
----------
|
|
150
|
+
swarm_id : ID
|
|
151
|
+
Swarm ID
|
|
152
|
+
authkey : str
|
|
153
|
+
Tailscale auth key
|
|
154
|
+
login_server : str
|
|
155
|
+
Tailscale login server
|
|
156
|
+
hostname : str
|
|
157
|
+
Tailscale hostname
|
|
158
|
+
temp_folder : Path
|
|
159
|
+
Temporary folder
|
|
160
|
+
|
|
161
|
+
Returns
|
|
162
|
+
-------
|
|
163
|
+
datetime
|
|
164
|
+
Start time of the container
|
|
165
|
+
"""
|
|
166
|
+
# network = f"tailscale-{swarm_id}"
|
|
167
|
+
# Crée le réseau Docker s’il n’existe pas
|
|
168
|
+
# if not self.docker_client.networks.list(names=[network]):
|
|
169
|
+
# self.docker_client.networks.create(network, driver="bridge", attachable=True)
|
|
170
|
+
# Lancement du conteneur Tailscale
|
|
171
|
+
config = {
|
|
172
|
+
"Image": "tailscale/tailscale:stable",
|
|
173
|
+
"HostConfig": {
|
|
174
|
+
"Hostname": hostname,
|
|
175
|
+
"Binds": [
|
|
176
|
+
f"tailscale-{swarm_id}-data:/var/lib/tailscale",
|
|
177
|
+
"/dev/net/tun:/dev/net/tun",
|
|
178
|
+
],
|
|
179
|
+
"CapAdd": ["NET_ADMIN", "SYS_MODULE"],
|
|
180
|
+
"RestartPolicy": {"Name": "unless-stopped"},
|
|
181
|
+
"NetworkMode": "host",
|
|
182
|
+
},
|
|
183
|
+
"Env": [
|
|
184
|
+
f"TS_AUTHKEY=tskey-client-{authkey}?ephemeral=false",
|
|
185
|
+
"TS_SERVE_CONFIG=/config/config.json",
|
|
186
|
+
"TS_STATE_DIR=/var/lib/tailscale",
|
|
187
|
+
"TS_USERSPACE=false",
|
|
188
|
+
f"TS_EXTRA_ARGS=--login-server={login_server} --advertise-tags=tag:container --reset",
|
|
189
|
+
],
|
|
190
|
+
"Volumes": {f"tailscale-{swarm_id}-data": {}},
|
|
191
|
+
"Detach": True,
|
|
192
|
+
"Labels": {
|
|
193
|
+
"com.docker.compose.project": "manta_tasks",
|
|
194
|
+
"com.docker.compose.service": "stirling-ts", # optionnel
|
|
195
|
+
},
|
|
196
|
+
}
|
|
197
|
+
self.tracer.debug("Running tailscale with following configuration:")
|
|
198
|
+
for key, value in config.items():
|
|
199
|
+
self.tracer.debug(f" {key}: {value}")
|
|
200
|
+
self.containers[
|
|
201
|
+
(swarm_id, ID(f"tailscale-{swarm_id}"))
|
|
202
|
+
] = await self.run_or_raise(config)
|
|
203
|
+
return f"service:tailscale-{swarm_id}"
|
|
204
|
+
|
|
205
|
+
async def run_container(
|
|
206
|
+
self,
|
|
207
|
+
swarm_id: ID,
|
|
208
|
+
task_id: ID,
|
|
209
|
+
image: str,
|
|
210
|
+
command: str,
|
|
211
|
+
volumes: dict,
|
|
212
|
+
environment: Optional[dict] = None,
|
|
213
|
+
memory: Optional[Union[int, str]] = None,
|
|
214
|
+
cpus: Optional[str] = None,
|
|
215
|
+
gpu: Optional[bool] = False,
|
|
216
|
+
network: Optional[str] = None,
|
|
217
|
+
alias: Optional[str] = None,
|
|
218
|
+
force_pull: bool = False,
|
|
219
|
+
) -> datetime:
|
|
220
|
+
"""
|
|
221
|
+
Run a container with the given image, command, volumes and environment
|
|
222
|
+
|
|
223
|
+
Parameters
|
|
224
|
+
----------
|
|
225
|
+
swarm_id : ID
|
|
226
|
+
ID of the swarm
|
|
227
|
+
task_id : ID
|
|
228
|
+
ID of the task
|
|
229
|
+
image : str
|
|
230
|
+
Image to run the container
|
|
231
|
+
command : str
|
|
232
|
+
Command to run in the container
|
|
233
|
+
volumes : list
|
|
234
|
+
Volumes to mount in the container
|
|
235
|
+
environment : Optional[dict]
|
|
236
|
+
Environment variables
|
|
237
|
+
memory : Optional[Union[int, str]]
|
|
238
|
+
Memory limit.
|
|
239
|
+
Accepts float values (which represent the memory limit of
|
|
240
|
+
the created container in bytes) or a string with a units
|
|
241
|
+
identification char (100000b, 1000k, 128m, 1g).
|
|
242
|
+
If a string is specified without a units character, bytes
|
|
243
|
+
are assumed as an intended unit.
|
|
244
|
+
cpus : Optional[str]
|
|
245
|
+
CPU limit. CPUs in which to allow execution (0-3, 0, 1)
|
|
246
|
+
or a percentage of the available CPUs
|
|
247
|
+
gpu : Optional[bool]
|
|
248
|
+
GPU support
|
|
249
|
+
network : Optional[str]
|
|
250
|
+
Network to attach to the container
|
|
251
|
+
alias : Optional[str]
|
|
252
|
+
Alias of the container
|
|
253
|
+
force_pull : bool
|
|
254
|
+
Force pull the image before running the container
|
|
255
|
+
|
|
256
|
+
Returns
|
|
257
|
+
-------
|
|
258
|
+
datetime
|
|
259
|
+
Start time of the container
|
|
260
|
+
"""
|
|
261
|
+
if gpu and not self.gpu_supported:
|
|
262
|
+
raise MantaDockerError(
|
|
263
|
+
"GPU support is not enabled for this container manager.",
|
|
264
|
+
metadata=self.tracer.metadata,
|
|
265
|
+
)
|
|
266
|
+
|
|
267
|
+
host_config = (
|
|
268
|
+
self.gpu_config.get("HostConfig", {}) if (gpu and self.gpu_config) else {}
|
|
269
|
+
)
|
|
270
|
+
host_config["Binds"] = [
|
|
271
|
+
f"{str(host_path) if isinstance(host_path, Path) else host_path}:{bind['bind']}:{bind['mode']}"
|
|
272
|
+
for host_path, bind in volumes.items()
|
|
273
|
+
]
|
|
274
|
+
host_config["NetworkMode"] = network if network else "host"
|
|
275
|
+
host_config["Hostname"] = alias if alias else None
|
|
276
|
+
host_config["ExtraHosts"] = ["host.docker.internal:host-gateway"]
|
|
277
|
+
env = (
|
|
278
|
+
[f"{key}={value}" for key, value in environment.items()]
|
|
279
|
+
if environment
|
|
280
|
+
else None
|
|
281
|
+
)
|
|
282
|
+
log_config = {
|
|
283
|
+
"type": "json-file",
|
|
284
|
+
"config": {"max-size": "10m", "max-file": "3"},
|
|
285
|
+
}
|
|
286
|
+
|
|
287
|
+
config = {
|
|
288
|
+
"Cmd": command.split(),
|
|
289
|
+
"Image": image,
|
|
290
|
+
"HostConfig": host_config,
|
|
291
|
+
"Env": env,
|
|
292
|
+
"Detach": True,
|
|
293
|
+
"Memory": memory,
|
|
294
|
+
"CpuShares": cpus,
|
|
295
|
+
"LogConfig": log_config,
|
|
296
|
+
"Runtime": (
|
|
297
|
+
self.gpu_config.get("Runtime") if (gpu and self.gpu_config) else None
|
|
298
|
+
),
|
|
299
|
+
"Labels": {
|
|
300
|
+
"com.docker.compose.project": "manta_tasks",
|
|
301
|
+
"manta.swarm-id": ID(swarm_id).xid,
|
|
302
|
+
"manta.task-id": ID(task_id).xid,
|
|
303
|
+
},
|
|
304
|
+
}
|
|
305
|
+
|
|
306
|
+
start_time = datetime.now(timezone.utc)
|
|
307
|
+
self.tracer.debug("Running container with following configuration:")
|
|
308
|
+
for key, value in config.items():
|
|
309
|
+
self.tracer.debug(f" {key}: {value}")
|
|
310
|
+
|
|
311
|
+
if force_pull:
|
|
312
|
+
self.tracer.info(f"Pulling image {image}...")
|
|
313
|
+
await self.pull_image_or_raise(image)
|
|
314
|
+
|
|
315
|
+
self.containers[(swarm_id, task_id)] = await self.run_or_raise(config)
|
|
316
|
+
|
|
317
|
+
self.tracer.info(f"Task {task_id} container started.")
|
|
318
|
+
return start_time
|
|
319
|
+
|
|
320
|
+
async def restart_container(self, swarm_id: ID, task_id: ID) -> datetime:
|
|
321
|
+
"""
|
|
322
|
+
Restart a task's container
|
|
323
|
+
|
|
324
|
+
Parameters
|
|
325
|
+
----------
|
|
326
|
+
swarm_id : ID
|
|
327
|
+
Swarm ID
|
|
328
|
+
task_id : ID
|
|
329
|
+
Task ID
|
|
330
|
+
|
|
331
|
+
Returns
|
|
332
|
+
-------
|
|
333
|
+
datetime
|
|
334
|
+
Start time of the container
|
|
335
|
+
|
|
336
|
+
Raises
|
|
337
|
+
------
|
|
338
|
+
MantaDockerError
|
|
339
|
+
Task not found
|
|
340
|
+
"""
|
|
341
|
+
if container := self.containers.get((swarm_id, task_id)):
|
|
342
|
+
start_time = datetime.now(timezone.utc)
|
|
343
|
+
await self.restart_or_raise(container)
|
|
344
|
+
self.tracer.debug(f"Task {task_id} container restarted.")
|
|
345
|
+
return start_time
|
|
346
|
+
message = f"Task {task_id} not found."
|
|
347
|
+
self.tracer.error(message)
|
|
348
|
+
raise MantaDockerError(message, metadata=self.tracer.metadata)
|
|
349
|
+
|
|
350
|
+
async def stop_container(self, swarm_id: ID, task_id: ID):
|
|
351
|
+
"""
|
|
352
|
+
Stop a running container
|
|
353
|
+
|
|
354
|
+
Parameters
|
|
355
|
+
----------
|
|
356
|
+
swarm_id : ID
|
|
357
|
+
Swarm ID
|
|
358
|
+
task_id : ID
|
|
359
|
+
Task ID
|
|
360
|
+
|
|
361
|
+
Raises
|
|
362
|
+
------
|
|
363
|
+
MantaDockerError
|
|
364
|
+
Task not found
|
|
365
|
+
"""
|
|
366
|
+
if container := self.containers.get((swarm_id, task_id)):
|
|
367
|
+
await self.stop_or_raise(container)
|
|
368
|
+
self.tracer.debug(f"Task {task_id} container stopped.")
|
|
369
|
+
else:
|
|
370
|
+
message = f"Task {task_id} not found."
|
|
371
|
+
self.tracer.error(message)
|
|
372
|
+
raise MantaDockerError(message, metadata=self.tracer.metadata)
|
|
373
|
+
|
|
374
|
+
async def remove_container(self, swarm_id: ID, task_id: ID):
|
|
375
|
+
"""
|
|
376
|
+
Remove a container
|
|
377
|
+
|
|
378
|
+
Parameters
|
|
379
|
+
----------
|
|
380
|
+
swarm_id : ID
|
|
381
|
+
Swarm ID
|
|
382
|
+
task_id : ID
|
|
383
|
+
Task ID
|
|
384
|
+
|
|
385
|
+
Raises
|
|
386
|
+
------
|
|
387
|
+
MantaDockerError
|
|
388
|
+
Task not found
|
|
389
|
+
"""
|
|
390
|
+
if container := self.containers.pop((swarm_id, task_id), None):
|
|
391
|
+
await self.remove_or_raise(container)
|
|
392
|
+
self.tracer.debug(f"Task {task_id} container removed.")
|
|
393
|
+
else:
|
|
394
|
+
message = f"Task {task_id} not found."
|
|
395
|
+
self.tracer.error(message)
|
|
396
|
+
raise MantaDockerError(message, metadata=self.tracer.metadata)
|
|
397
|
+
|
|
398
|
+
async def get_task_status(self, swarm_id: ID, task_id: ID) -> TaskStatus:
|
|
399
|
+
"""
|
|
400
|
+
Get the status of a container
|
|
401
|
+
|
|
402
|
+
Parameters
|
|
403
|
+
----------
|
|
404
|
+
swarm_id : ID
|
|
405
|
+
Swarm ID
|
|
406
|
+
task_id : ID
|
|
407
|
+
Task ID
|
|
408
|
+
|
|
409
|
+
Returns
|
|
410
|
+
-------
|
|
411
|
+
TaskStatus
|
|
412
|
+
Status of the container
|
|
413
|
+
|
|
414
|
+
Raises
|
|
415
|
+
------
|
|
416
|
+
MantaDockerError
|
|
417
|
+
Container not found
|
|
418
|
+
Cannot reload container
|
|
419
|
+
"""
|
|
420
|
+
container = self.containers.get((swarm_id, task_id))
|
|
421
|
+
if container is None:
|
|
422
|
+
self.tracer.error("Cannot found container")
|
|
423
|
+
self.tracer.info(
|
|
424
|
+
f"ContainerManager.get_task_status: container NOT FOUND "
|
|
425
|
+
f"task={task_id} swarm={swarm_id} "
|
|
426
|
+
f"containers_keys={list(self.containers.keys())}"
|
|
427
|
+
)
|
|
428
|
+
raise MantaDockerError(
|
|
429
|
+
"Cannot found container", metadata=self.tracer.metadata
|
|
430
|
+
)
|
|
431
|
+
|
|
432
|
+
self.tracer.info(
|
|
433
|
+
f"ContainerManager.get_task_status: container FOUND "
|
|
434
|
+
f"task={task_id} swarm={swarm_id} container_id={container.id}"
|
|
435
|
+
)
|
|
436
|
+
# self.reload_or_raise(container)
|
|
437
|
+
status = await self.get_status_or_raise(container)
|
|
438
|
+
self.tracer.info(
|
|
439
|
+
f"ContainerManager.get_task_status: Docker status={status} task={task_id}"
|
|
440
|
+
)
|
|
441
|
+
|
|
442
|
+
# Use domain service to map container status to task status
|
|
443
|
+
return TaskLifecycleService.calculate_status_from_container(status)
|
|
444
|
+
|
|
445
|
+
async def get_container_logs(
|
|
446
|
+
self, swarm_id: ID, task_id: ID, since: Optional[datetime] = None
|
|
447
|
+
) -> bytes:
|
|
448
|
+
"""
|
|
449
|
+
Get the logs of a container (async)
|
|
450
|
+
|
|
451
|
+
Parameters
|
|
452
|
+
----------
|
|
453
|
+
swarm_id : ID
|
|
454
|
+
Swarm ID
|
|
455
|
+
task_id : ID
|
|
456
|
+
Task ID
|
|
457
|
+
since : Optional[datetime]
|
|
458
|
+
Show logs since a given datetime
|
|
459
|
+
|
|
460
|
+
Returns
|
|
461
|
+
-------
|
|
462
|
+
bytes
|
|
463
|
+
Logs from the container
|
|
464
|
+
|
|
465
|
+
Raises
|
|
466
|
+
------
|
|
467
|
+
MantaDockerError
|
|
468
|
+
Cannot found container
|
|
469
|
+
Cannot get container's logs
|
|
470
|
+
"""
|
|
471
|
+
container = self.containers.get((swarm_id, task_id))
|
|
472
|
+
if container is None:
|
|
473
|
+
message = f"Task {task_id} not found for swarm {swarm_id}."
|
|
474
|
+
self.tracer.error(message)
|
|
475
|
+
raise MantaDockerError(message, metadata=self.tracer.metadata)
|
|
476
|
+
return await self.get_container_logs_or_raise(container, since)
|
|
477
|
+
|
|
478
|
+
async def get_task_stats(self, swarm_id: ID, task_id: ID) -> dict:
|
|
479
|
+
"""
|
|
480
|
+
Get the stats of a task
|
|
481
|
+
|
|
482
|
+
Parameters
|
|
483
|
+
----------
|
|
484
|
+
swarm_id : ID
|
|
485
|
+
Swarm ID
|
|
486
|
+
task_id : ID
|
|
487
|
+
Task ID
|
|
488
|
+
|
|
489
|
+
Returns
|
|
490
|
+
-------
|
|
491
|
+
dict
|
|
492
|
+
Stats from the container
|
|
493
|
+
|
|
494
|
+
Raises
|
|
495
|
+
------
|
|
496
|
+
MantaDockerError
|
|
497
|
+
Task not found
|
|
498
|
+
"""
|
|
499
|
+
if container := self.containers.get((swarm_id, task_id)):
|
|
500
|
+
stats = cast(Dict[str, Any], await container.stats(stream=False))
|
|
501
|
+
|
|
502
|
+
cpu_usage = stats["cpu_stats"]["cpu_usage"]["total_usage"]
|
|
503
|
+
system_cpu_usage = stats["cpu_stats"]["system_cpu_usage"]
|
|
504
|
+
online_cpus = stats["cpu_stats"]["online_cpus"]
|
|
505
|
+
cpu_percent = (cpu_usage / system_cpu_usage) * online_cpus * 100
|
|
506
|
+
|
|
507
|
+
cpu = dict(
|
|
508
|
+
total=float(100.0),
|
|
509
|
+
available=100.0 - cpu_percent,
|
|
510
|
+
used=cpu_percent,
|
|
511
|
+
)
|
|
512
|
+
|
|
513
|
+
memory_usage = stats["memory_stats"]["usage"]
|
|
514
|
+
memory_limit = stats["memory_stats"]["limit"]
|
|
515
|
+
|
|
516
|
+
memory = dict(
|
|
517
|
+
total=float(memory_limit),
|
|
518
|
+
available=float(memory_usage / memory_limit * 100),
|
|
519
|
+
used=float(memory_usage),
|
|
520
|
+
)
|
|
521
|
+
else:
|
|
522
|
+
cpu = dict(
|
|
523
|
+
total=100.0,
|
|
524
|
+
available=nan,
|
|
525
|
+
used=nan,
|
|
526
|
+
)
|
|
527
|
+
memory = dict(
|
|
528
|
+
total=nan,
|
|
529
|
+
available=nan,
|
|
530
|
+
used=nan,
|
|
531
|
+
)
|
|
532
|
+
|
|
533
|
+
return {"cpu": cpu, "memory": memory}
|
|
534
|
+
|
|
535
|
+
# TODO : use (swarm_id, task_id) instead of only task_id
|
|
536
|
+
async def collect_system_summary(self) -> dict:
|
|
537
|
+
"""
|
|
538
|
+
Collect the current system metrics
|
|
539
|
+
|
|
540
|
+
Returns
|
|
541
|
+
-------
|
|
542
|
+
dict
|
|
543
|
+
System informations
|
|
544
|
+
"""
|
|
545
|
+
# Iterate over all containers (tasks) and get their stats
|
|
546
|
+
return {
|
|
547
|
+
task_id.xid: await self.get_task_stats(swarm_id, task_id)
|
|
548
|
+
for (swarm_id, task_id) in self.containers
|
|
549
|
+
}
|