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.
Files changed (43) 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 +90 -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 +11 -0
  20. manta_node/infrastructure/container/docker_adapter.py +253 -0
  21. manta_node/infrastructure/container/image_executor.py +222 -0
  22. manta_node/infrastructure/container/manager.py +549 -0
  23. manta_node/infrastructure/filesystem/__init__.py +7 -0
  24. manta_node/infrastructure/filesystem/dataset_manager.py +469 -0
  25. manta_node/infrastructure/grpc/__init__.py +11 -0
  26. manta_node/infrastructure/grpc/client.py +373 -0
  27. manta_node/infrastructure/grpc/local_servicer.py +151 -0
  28. manta_node/infrastructure/grpc/world_servicer.py +284 -0
  29. manta_node/infrastructure/metrics/__init__.py +10 -0
  30. manta_node/infrastructure/metrics/collector.py +94 -0
  31. manta_node/infrastructure/metrics/metrics_collector.py +351 -0
  32. manta_node/infrastructure/mqtt/__init__.py +7 -0
  33. manta_node/infrastructure/mqtt/command_handler.py +450 -0
  34. manta_node/node_orchestrator.py +462 -0
  35. manta_node/task_manager.py +677 -0
  36. manta_node/tasks.py +273 -0
  37. manta_node/utils.py +52 -0
  38. manta_node-0.5b0.dist-info/METADATA +794 -0
  39. manta_node-0.5b0.dist-info/RECORD +43 -0
  40. manta_node-0.5b0.dist-info/WHEEL +5 -0
  41. manta_node-0.5b0.dist-info/entry_points.txt +2 -0
  42. manta_node-0.5b0.dist-info/licenses/LICENSE +683 -0
  43. manta_node-0.5b0.dist-info/top_level.txt +1 -0
@@ -0,0 +1,11 @@
1
+ """Container infrastructure - Docker runtime management."""
2
+
3
+ from .docker_adapter import DockerRaiser
4
+ from .image_executor import ImageExecutor
5
+ from .manager import ContainerManager
6
+
7
+ __all__ = [
8
+ "ContainerManager",
9
+ "DockerRaiser",
10
+ "ImageExecutor",
11
+ ]
@@ -0,0 +1,253 @@
1
+ from datetime import datetime
2
+ from typing import Optional
3
+
4
+ from aiodocker import Docker
5
+ from aiodocker.docker import DockerContainer
6
+ from docker import DockerClient
7
+
8
+ from manta_common.errors import MantaDockerError
9
+ from manta_common.traces import Tracer
10
+
11
+
12
+ class DockerRaiser:
13
+ async_client: Docker
14
+ tracer: Tracer
15
+ docker_client: DockerClient
16
+
17
+ async def run_or_raise(self, config: dict) -> DockerContainer:
18
+ """
19
+ Start, pull or run a container
20
+
21
+ Parameters
22
+ ----------
23
+ config : dict
24
+ Configuration
25
+
26
+ Returns
27
+ -------
28
+ DockerContainer
29
+ Container
30
+
31
+ Raises
32
+ ------
33
+ MantaDockerError
34
+ Cannot run container
35
+ """
36
+ container = None
37
+ try:
38
+ container = await self.async_client.containers.run(config)
39
+ except Exception as exc:
40
+ self.tracer.error("Task cannot be started")
41
+ raise MantaDockerError(
42
+ "Task cannot be started", metadata=self.tracer.metadata
43
+ ) from exc
44
+ return container
45
+
46
+ async def pull_image_or_raise(self, image: str):
47
+ """
48
+ Pull an image
49
+
50
+ Parameters
51
+ ----------
52
+ image : str
53
+ Image to pull
54
+
55
+ Raises
56
+ ------
57
+ MantaDockerError
58
+ Cannot pull image
59
+ """
60
+ try:
61
+ await self.async_client.images.pull(image)
62
+ except Exception as exc:
63
+ self.tracer.error(f"Image {image} cannot be pulled")
64
+ raise MantaDockerError(
65
+ f"Image {image} cannot be pulled", metadata=self.tracer.metadata
66
+ ) from exc
67
+
68
+ async def list_containers_or_raise(self, all_containers: bool = True) -> list:
69
+ """
70
+ List all containers in the docker engine
71
+
72
+ Parameters
73
+ ----------
74
+ all_containers : bool, optional
75
+ Show all containers; only running containers are shown by default
76
+
77
+ Returns
78
+ -------
79
+ list
80
+ List of containers
81
+
82
+ Raises
83
+ ------
84
+ MantaDockerError
85
+ Cannot list containers
86
+ """
87
+ containers = []
88
+ try:
89
+ containers = await self.async_client.containers.list(all=all_containers)
90
+ except Exception as exc:
91
+ self.tracer.error("Unable to list containers")
92
+ raise MantaDockerError(
93
+ "Unable to list containers", metadata=self.tracer.metadata
94
+ ) from exc
95
+ return containers
96
+
97
+ def list_images_or_raise(self) -> list:
98
+ """
99
+ List all images in the docker engine
100
+
101
+ Returns
102
+ -------
103
+ list
104
+ List of images
105
+
106
+ Raises
107
+ ------
108
+ MantaDockerError
109
+ Cannot list images
110
+ """
111
+ images = []
112
+ try:
113
+ images = [
114
+ tag for image in self.docker_client.images.list() for tag in image.tags
115
+ ]
116
+ except Exception as exc:
117
+ self.tracer.error("Unable to list images")
118
+ raise MantaDockerError(
119
+ "Unable to list images", metadata=self.tracer.metadata
120
+ ) from exc
121
+ return images
122
+
123
+ async def restart_or_raise(self, container: DockerContainer):
124
+ """
125
+ Restart container
126
+
127
+ Parameters
128
+ ----------
129
+ container : DockerContainer
130
+ Container
131
+
132
+ Raises
133
+ ------
134
+ MantaDockerError
135
+ Cannot restart container
136
+ """
137
+ try:
138
+ await container.restart()
139
+ except Exception as exc:
140
+ self.tracer.error("Container cannot be restarted")
141
+ raise MantaDockerError(
142
+ "Container cannot be restart", metadata=self.tracer.metadata
143
+ ) from exc
144
+
145
+ async def stop_or_raise(self, container: DockerContainer):
146
+ """
147
+ Stop container
148
+
149
+ Parameters
150
+ ----------
151
+ container : DockerContainer
152
+ Container
153
+
154
+ Raises
155
+ ------
156
+ MantaDockerError
157
+ Cannot stop container
158
+ """
159
+ try:
160
+ await container.stop()
161
+ except Exception as exc:
162
+ self.tracer.error("Container cannot be stopped")
163
+ raise MantaDockerError(
164
+ "Container cannot be stopped", metadata=self.tracer.metadata
165
+ ) from exc
166
+
167
+ async def remove_or_raise(self, container: DockerContainer):
168
+ """
169
+ Remove container
170
+
171
+ Parameters
172
+ ----------
173
+ container : DockerContainer
174
+ Container
175
+
176
+ Raises
177
+ ------
178
+ MantaDockerError
179
+ Cannot remove container
180
+ """
181
+ try:
182
+ await container.delete(force=True)
183
+ except Exception as exc:
184
+ self.tracer.error("Container cannot be removed")
185
+ raise MantaDockerError(
186
+ "Container cannot be removed", metadata=self.tracer.metadata
187
+ ) from exc
188
+
189
+ async def get_status_or_raise(self, container: DockerContainer):
190
+ """
191
+ Reload container
192
+
193
+ Parameters
194
+ ----------
195
+ container : DockerContainer
196
+ Container
197
+
198
+ Raises
199
+ ------
200
+ MantaDockerError
201
+ Cannot reload container
202
+ """
203
+ status = "unknown"
204
+ try:
205
+ status = await container.show()
206
+ status = status["State"]["Status"]
207
+ except Exception as exc:
208
+ self.tracer.error("Cannot get status of container")
209
+ raise MantaDockerError(
210
+ "Cannot get status of container", metadata=self.tracer.metadata
211
+ ) from exc
212
+ return status
213
+
214
+ async def get_container_logs_or_raise(
215
+ self, container: DockerContainer, since: Optional[datetime] = None
216
+ ) -> bytes:
217
+ """
218
+ Get container logs (async)
219
+
220
+ Parameters
221
+ ----------
222
+ container : DockerContainer
223
+ Container
224
+ since: Optional[datetime]
225
+ Start time
226
+
227
+ Returns
228
+ -------
229
+ bytes
230
+ Logs
231
+
232
+ Raises
233
+ ------
234
+ MantaDockerError
235
+ Cannot get logs
236
+ """
237
+ logs = b""
238
+ try:
239
+ # Use async Docker client to avoid blocking the event loop
240
+ async_container = await self.async_client.containers.get(container.id)
241
+ # aiodocker >= 0.25: log() returns a coroutine yielding a list of strings
242
+ # since must be a unix timestamp (int), not a datetime object
243
+ since_ts = int(since.timestamp()) if since else None
244
+ log_lines = await async_container.log(
245
+ stdout=True, stderr=True, since=since_ts
246
+ )
247
+ logs = "".join(log_lines).encode()
248
+ except Exception as exc:
249
+ self.tracer.error(f"Cannot get logs of container: {exc}")
250
+ raise MantaDockerError(
251
+ "Cannot get logs of container", metadata=self.tracer.metadata
252
+ ) from exc
253
+ return logs
@@ -0,0 +1,222 @@
1
+ import asyncio
2
+ from datetime import datetime, timezone
3
+ from typing import Dict, Optional, Tuple
4
+
5
+ import psutil
6
+
7
+ from aiodocker.docker import DockerContainer
8
+
9
+ from manta_common.build.common.tasks import TaskStatus
10
+ from manta_common.conversions import ID
11
+ from manta_common.errors import MantaDockerError
12
+ from manta_common.traces import Tracer
13
+
14
+ from ...domain.task_lifecycle import TaskLifecycleService
15
+ from .docker_adapter import DockerRaiser
16
+
17
+ ORPHAN_PORT = 8080
18
+
19
+
20
+ class ImageExecutor(DockerRaiser):
21
+ """
22
+ Runs plain Docker images with native ENTRYPOINT/CMD.
23
+ No zipapp, no manta SDK, no gRPC inside the container.
24
+ """
25
+
26
+ tracer: Tracer
27
+ containers: Dict[Tuple[ID, ID], DockerContainer]
28
+
29
+ @staticmethod
30
+ def _is_manta_orphan(proc: psutil.Process) -> bool:
31
+ """Return True if *proc* looks like a manta worker that escaped
32
+ its ``--network=host`` container.
33
+
34
+ Checks that the process (a) runs a manta worker cmdline and
35
+ (b) has ``TASK_ID`` / ``SWARM_ID`` in its environment — these
36
+ env vars are injected by ``run_image`` and are unique to manta
37
+ containers. This avoids false positives from non-manta
38
+ uvicorn processes or other teams' containers.
39
+ """
40
+ try:
41
+ cmd = " ".join(proc.cmdline()).lower()
42
+ if "worker_main" not in cmd and "uvicorn" not in cmd:
43
+ return False
44
+ except (psutil.NoSuchProcess, psutil.AccessDenied, psutil.ZombieProcess):
45
+ return False
46
+
47
+ try:
48
+ env = proc.environ()
49
+ if "TASK_ID" not in env or "SWARM_ID" not in env:
50
+ return False
51
+ except (psutil.NoSuchProcess, psutil.AccessDenied):
52
+ return False
53
+
54
+ return True
55
+
56
+ async def _ensure_port_free(self, port: int, tracked: set) -> None:
57
+ """Kill manta-orphan processes holding ``port``.
58
+
59
+ When a ``--network=host`` container is killed its child process
60
+ can escape the PID namespace and remain on the host, still holding
61
+ the port. Only processes that match the manta worker profile
62
+ (``worker_main``/``uvicorn`` cmdline AND ``TASK_ID``/``SWARM_ID``
63
+ in environment) AND are NOT in *tracked* are targeted —
64
+ co-located host services, other teams' containers, and live
65
+ tracked workers are never touched.
66
+ """
67
+ try:
68
+ conns = list(psutil.net_connections(kind="inet"))
69
+ except psutil.AccessDenied:
70
+ self.tracer.warning(
71
+ "Cannot enumerate network connections (non-root?); "
72
+ "skipping port cleanup"
73
+ )
74
+ return
75
+ for conn in conns:
76
+ if conn.status != "LISTEN" or conn.laddr.port != port:
77
+ continue
78
+ if conn.pid is None:
79
+ continue
80
+ try:
81
+ proc = psutil.Process(conn.pid)
82
+ if not self._is_manta_orphan(proc):
83
+ continue
84
+ # Double-check: don't kill a live tracked worker
85
+ try:
86
+ env = proc.environ()
87
+ task_xid = env.get("TASK_ID")
88
+ swarm_xid = env.get("SWARM_ID")
89
+ if task_xid and swarm_xid:
90
+ tid_pair = (ID(swarm_xid).tid, ID(task_xid).tid)
91
+ if tid_pair in tracked:
92
+ continue
93
+ except (psutil.NoSuchProcess, psutil.AccessDenied):
94
+ pass
95
+ self.tracer.warning(
96
+ f"Manta orphan process {conn.pid} ({proc.name()}) "
97
+ f"holding port {port}, sending SIGTERM"
98
+ )
99
+ proc.terminate()
100
+ gone, _ = await asyncio.to_thread(psutil.wait_procs, [proc], timeout=5)
101
+ if not gone:
102
+ self.tracer.warning(
103
+ f"Process {conn.pid} did not exit after SIGTERM, "
104
+ f"sending SIGKILL"
105
+ )
106
+ proc.kill()
107
+ await asyncio.to_thread(psutil.wait_procs, [proc], timeout=3)
108
+ except (
109
+ psutil.NoSuchProcess,
110
+ psutil.AccessDenied,
111
+ psutil.ZombieProcess,
112
+ ):
113
+ pass
114
+
115
+ async def run_image(
116
+ self,
117
+ swarm_id: ID,
118
+ task_id: ID,
119
+ image: str,
120
+ environment: Optional[dict] = None,
121
+ ) -> datetime:
122
+ """
123
+ Pull (if not present locally) and start a Docker image with its native ENTRYPOINT.
124
+
125
+ Returns
126
+ -------
127
+ datetime
128
+ Container start time
129
+ """
130
+ tracked = set(getattr(self, "tasks", None) and self.tasks._env_ids or [])
131
+ tracked |= getattr(self, "_launching", None) or set()
132
+ await self._ensure_port_free(ORPHAN_PORT, tracked)
133
+ try:
134
+ await self.async_client.images.inspect(image)
135
+ except Exception:
136
+ self.tracer.info(f"Image {image} not found locally, pulling...")
137
+ await self.pull_image_or_raise(image)
138
+
139
+ env = [f"{k}={v}" for k, v in environment.items()] if environment else None
140
+ config = {
141
+ "Image": image,
142
+ "HostConfig": {
143
+ "NetworkMode": "host",
144
+ "ExtraHosts": ["host.docker.internal:host-gateway"],
145
+ },
146
+ "Env": env,
147
+ "Detach": True,
148
+ "LogConfig": {
149
+ "type": "json-file",
150
+ "config": {"max-size": "10m", "max-file": "3"},
151
+ },
152
+ "Labels": {
153
+ "com.docker.compose.project": "manta_tasks",
154
+ "manta.swarm-id": ID(swarm_id).xid,
155
+ "manta.task-id": ID(task_id).xid,
156
+ },
157
+ }
158
+
159
+ start_time = datetime.now(timezone.utc)
160
+ self.tracer.debug(f"Starting image {image} for task {task_id}")
161
+ self.containers[(swarm_id, task_id)] = await self.run_or_raise(config)
162
+ self.tracer.info(f"Task {task_id} image container started.")
163
+ return start_time
164
+
165
+ async def get_image_task_status(self, swarm_id: ID, task_id: ID) -> TaskStatus:
166
+ """
167
+ Exit-code-aware status for image-mode tasks (no SDK inside the container).
168
+ """
169
+ container = self.containers.get((swarm_id, task_id))
170
+ if container is None:
171
+ self.tracer.info(
172
+ f"ImageExecutor.get_image_task_status: container NOT FOUND "
173
+ f"task={task_id} swarm={swarm_id} "
174
+ f"containers_keys={list(self.containers.keys())}"
175
+ )
176
+ raise MantaDockerError("Container not found", metadata=self.tracer.metadata)
177
+
178
+ self.tracer.info(
179
+ f"ImageExecutor.get_image_task_status: container FOUND "
180
+ f"task={task_id} swarm={swarm_id} container_id={container.id}"
181
+ )
182
+ inspect = await container.show()
183
+ container_status = inspect["State"]["Status"]
184
+ exit_code = inspect["State"].get("ExitCode", 0)
185
+ self.tracer.info(
186
+ f"ImageExecutor.get_image_task_status: "
187
+ f"container_status={container_status} exit_code={exit_code} "
188
+ f"task={task_id}"
189
+ )
190
+
191
+ status = TaskLifecycleService.status_from_exit(container_status, exit_code)
192
+ self.tracer.info(
193
+ f"ImageExecutor.get_image_task_status: "
194
+ f"container_status={container_status} exit_code={exit_code} "
195
+ f"→ TaskStatus={status} task={task_id}"
196
+ )
197
+ return status
198
+
199
+ async def stop_image(
200
+ self,
201
+ swarm_id: ID,
202
+ task_id: ID,
203
+ timeout: Optional[int] = 10,
204
+ ) -> None:
205
+ """
206
+ Stop a running image-mode container.
207
+
208
+ Sends SIGTERM and waits up to ``timeout`` seconds for the container to
209
+ exit, then falls back to SIGKILL (handled by the Docker daemon).
210
+ Idempotent: aiodocker treats an already-stopped container as success.
211
+
212
+ The container reference is retained in ``self.containers`` so that
213
+ ``get_image_task_status`` can still return the final exit status.
214
+ Removal is the caller's responsibility (see ``remove_container``).
215
+ """
216
+ container = self.containers.get((swarm_id, task_id))
217
+ if container is None:
218
+ raise MantaDockerError("Container not found", metadata=self.tracer.metadata)
219
+
220
+ self.tracer.debug(f"Stopping container for task {task_id}")
221
+ await container.stop(timeout=timeout)
222
+ self.tracer.info(f"Task {task_id} container stopped.")