manta-node 0.5b0.dev9__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.
Files changed (42) hide show
  1. manta_node/__init__.py +5 -0
  2. manta_node/__main__.py +57 -0
  3. manta_node/cli/__init__.py +6 -0
  4. manta_node/cli/commands/__init__.py +21 -0
  5. manta_node/cli/commands/cluster.py +419 -0
  6. manta_node/cli/commands/config.py +490 -0
  7. manta_node/cli/commands/logs.py +168 -0
  8. manta_node/cli/commands/start.py +459 -0
  9. manta_node/cli/commands/status.py +204 -0
  10. manta_node/cli/commands/stop.py +253 -0
  11. manta_node/cli/config_manager.py +139 -0
  12. manta_node/cli/main.py +133 -0
  13. manta_node/cli/version.py +106 -0
  14. manta_node/domain/__init__.py +8 -0
  15. manta_node/domain/task_lifecycle.py +83 -0
  16. manta_node/infrastructure/__init__.py +13 -0
  17. manta_node/infrastructure/config/__init__.py +31 -0
  18. manta_node/infrastructure/config/node_config_manager.py +918 -0
  19. manta_node/infrastructure/container/__init__.py +9 -0
  20. manta_node/infrastructure/container/docker_adapter.py +253 -0
  21. manta_node/infrastructure/container/manager.py +536 -0
  22. manta_node/infrastructure/filesystem/__init__.py +7 -0
  23. manta_node/infrastructure/filesystem/dataset_manager.py +469 -0
  24. manta_node/infrastructure/grpc/__init__.py +11 -0
  25. manta_node/infrastructure/grpc/client.py +373 -0
  26. manta_node/infrastructure/grpc/local_servicer.py +151 -0
  27. manta_node/infrastructure/grpc/world_servicer.py +284 -0
  28. manta_node/infrastructure/metrics/__init__.py +10 -0
  29. manta_node/infrastructure/metrics/collector.py +94 -0
  30. manta_node/infrastructure/metrics/metrics_collector.py +351 -0
  31. manta_node/infrastructure/mqtt/__init__.py +7 -0
  32. manta_node/infrastructure/mqtt/command_handler.py +315 -0
  33. manta_node/node_orchestrator.py +462 -0
  34. manta_node/task_manager.py +519 -0
  35. manta_node/tasks.py +272 -0
  36. manta_node/utils.py +52 -0
  37. manta_node-0.5b0.dev9.dist-info/METADATA +799 -0
  38. manta_node-0.5b0.dev9.dist-info/RECORD +42 -0
  39. manta_node-0.5b0.dev9.dist-info/WHEEL +5 -0
  40. manta_node-0.5b0.dev9.dist-info/entry_points.txt +2 -0
  41. manta_node-0.5b0.dev9.dist-info/licenses/LICENSE +683 -0
  42. manta_node-0.5b0.dev9.dist-info/top_level.txt +1 -0
@@ -0,0 +1,536 @@
1
+ import os
2
+ import subprocess
3
+ import sys
4
+ from datetime import datetime
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
+ },
302
+ }
303
+
304
+ start_time = datetime.now()
305
+ self.tracer.debug("Running container with following configuration:")
306
+ for key, value in config.items():
307
+ self.tracer.debug(f" {key}: {value}")
308
+
309
+ if force_pull:
310
+ self.tracer.info(f"Pulling image {image}...")
311
+ await self.pull_image_or_raise(image)
312
+
313
+ self.containers[(swarm_id, task_id)] = await self.run_or_raise(config)
314
+
315
+ self.tracer.info(f"Task {task_id} container started.")
316
+ return start_time
317
+
318
+ async def restart_container(self, swarm_id: ID, task_id: ID) -> datetime:
319
+ """
320
+ Restart a task's container
321
+
322
+ Parameters
323
+ ----------
324
+ swarm_id : ID
325
+ Swarm ID
326
+ task_id : ID
327
+ Task ID
328
+
329
+ Returns
330
+ -------
331
+ datetime
332
+ Start time of the container
333
+
334
+ Raises
335
+ ------
336
+ MantaDockerError
337
+ Task not found
338
+ """
339
+ if container := self.containers.get((swarm_id, task_id)):
340
+ start_time = datetime.now()
341
+ await self.restart_or_raise(container)
342
+ self.tracer.debug(f"Task {task_id} container restarted.")
343
+ return start_time
344
+ message = f"Task {task_id} not found."
345
+ self.tracer.error(message)
346
+ raise MantaDockerError(message, metadata=self.tracer.metadata)
347
+
348
+ async def stop_container(self, swarm_id: ID, task_id: ID):
349
+ """
350
+ Stop a running container
351
+
352
+ Parameters
353
+ ----------
354
+ swarm_id : ID
355
+ Swarm ID
356
+ task_id : ID
357
+ Task ID
358
+
359
+ Raises
360
+ ------
361
+ MantaDockerError
362
+ Task not found
363
+ """
364
+ if container := self.containers.get((swarm_id, task_id)):
365
+ await self.stop_or_raise(container)
366
+ self.tracer.debug(f"Task {task_id} container stopped.")
367
+ else:
368
+ message = f"Task {task_id} not found."
369
+ self.tracer.error(message)
370
+ raise MantaDockerError(message, metadata=self.tracer.metadata)
371
+
372
+ async def remove_container(self, swarm_id: ID, task_id: ID):
373
+ """
374
+ Remove a container
375
+
376
+ Parameters
377
+ ----------
378
+ swarm_id : ID
379
+ Swarm ID
380
+ task_id : ID
381
+ Task ID
382
+
383
+ Raises
384
+ ------
385
+ MantaDockerError
386
+ Task not found
387
+ """
388
+ if container := self.containers.pop((swarm_id, task_id), None):
389
+ await self.remove_or_raise(container)
390
+ self.tracer.debug(f"Task {task_id} container removed.")
391
+ else:
392
+ message = f"Task {task_id} not found."
393
+ self.tracer.error(message)
394
+ raise MantaDockerError(message, metadata=self.tracer.metadata)
395
+
396
+ async def get_task_status(self, swarm_id: ID, task_id: ID) -> TaskStatus:
397
+ """
398
+ Get the status of a container
399
+
400
+ Parameters
401
+ ----------
402
+ swarm_id : ID
403
+ Swarm ID
404
+ task_id : ID
405
+ Task ID
406
+
407
+ Returns
408
+ -------
409
+ TaskStatus
410
+ Status of the container
411
+
412
+ Raises
413
+ ------
414
+ MantaDockerError
415
+ Container not found
416
+ Cannot reload container
417
+ """
418
+ container = self.containers.get((swarm_id, task_id))
419
+ if container is None:
420
+ self.tracer.error("Cannot found container")
421
+ raise MantaDockerError(
422
+ "Cannot found container", metadata=self.tracer.metadata
423
+ )
424
+
425
+ # self.reload_or_raise(container)
426
+ status = await self.get_status_or_raise(container)
427
+ self.tracer.debug(f"Task {task_id} container's status: {status}")
428
+
429
+ # Use domain service to map container status to task status
430
+ return TaskLifecycleService.calculate_status_from_container(status)
431
+
432
+ async def get_container_logs(
433
+ self, swarm_id: ID, task_id: ID, since: Optional[datetime] = None
434
+ ) -> bytes:
435
+ """
436
+ Get the logs of a container (async)
437
+
438
+ Parameters
439
+ ----------
440
+ swarm_id : ID
441
+ Swarm ID
442
+ task_id : ID
443
+ Task ID
444
+ since : Optional[datetime]
445
+ Show logs since a given datetime
446
+
447
+ Returns
448
+ -------
449
+ bytes
450
+ Logs from the container
451
+
452
+ Raises
453
+ ------
454
+ MantaDockerError
455
+ Cannot found container
456
+ Cannot get container's logs
457
+ """
458
+ container = self.containers.get((swarm_id, task_id))
459
+ if container is None:
460
+ message = f"Task {task_id} not found for swarm {swarm_id}."
461
+ self.tracer.error(message)
462
+ raise MantaDockerError(message, metadata=self.tracer.metadata)
463
+ return await self.get_container_logs_or_raise(container, since)
464
+
465
+ async def get_task_stats(self, swarm_id: ID, task_id: ID) -> dict:
466
+ """
467
+ Get the stats of a task
468
+
469
+ Parameters
470
+ ----------
471
+ swarm_id : ID
472
+ Swarm ID
473
+ task_id : ID
474
+ Task ID
475
+
476
+ Returns
477
+ -------
478
+ dict
479
+ Stats from the container
480
+
481
+ Raises
482
+ ------
483
+ MantaDockerError
484
+ Task not found
485
+ """
486
+ if container := self.containers.get((swarm_id, task_id)):
487
+ stats = cast(Dict[str, Any], await container.stats(stream=False))
488
+
489
+ cpu_usage = stats["cpu_stats"]["cpu_usage"]["total_usage"]
490
+ system_cpu_usage = stats["cpu_stats"]["system_cpu_usage"]
491
+ online_cpus = stats["cpu_stats"]["online_cpus"]
492
+ cpu_percent = (cpu_usage / system_cpu_usage) * online_cpus * 100
493
+
494
+ cpu = dict(
495
+ total=float(100.0),
496
+ available=100.0 - cpu_percent,
497
+ used=cpu_percent,
498
+ )
499
+
500
+ memory_usage = stats["memory_stats"]["usage"]
501
+ memory_limit = stats["memory_stats"]["limit"]
502
+
503
+ memory = dict(
504
+ total=float(memory_limit),
505
+ available=float(memory_usage / memory_limit * 100),
506
+ used=float(memory_usage),
507
+ )
508
+ else:
509
+ cpu = dict(
510
+ total=100.0,
511
+ available=nan,
512
+ used=nan,
513
+ )
514
+ memory = dict(
515
+ total=nan,
516
+ available=nan,
517
+ used=nan,
518
+ )
519
+
520
+ return {"cpu": cpu, "memory": memory}
521
+
522
+ # TODO : use (swarm_id, task_id) instead of only task_id
523
+ async def collect_system_summary(self) -> dict:
524
+ """
525
+ Collect the current system metrics
526
+
527
+ Returns
528
+ -------
529
+ dict
530
+ System informations
531
+ """
532
+ # Iterate over all containers (tasks) and get their stats
533
+ return {
534
+ task_id.xid: await self.get_task_stats(swarm_id, task_id)
535
+ for (swarm_id, task_id) in self.containers
536
+ }
@@ -0,0 +1,7 @@
1
+ """Filesystem infrastructure - Dataset and file management."""
2
+
3
+ from .dataset_manager import DatasetManager
4
+
5
+ __all__ = [
6
+ "DatasetManager",
7
+ ]