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,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.")
|