wnm 0.0.12__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.

Potentially problematic release.


This version of wnm might be problematic. Click here for more details.

@@ -0,0 +1,371 @@
1
+ """
2
+ DockerManager: Manage nodes in Docker containers.
3
+
4
+ This manager runs nodes inside Docker containers, allowing for better isolation
5
+ and resource management. Supports both single-node and multi-node containers.
6
+ """
7
+
8
+ import logging
9
+ import os
10
+ import re
11
+ import subprocess
12
+ import time
13
+ from pathlib import Path
14
+
15
+ from wnm.common import DEAD, RESTARTING, RUNNING, STOPPED, UPGRADING
16
+ from wnm.config import BOOTSTRAP_CACHE_DIR
17
+ from wnm.models import Node
18
+ from wnm.process_managers.base import NodeProcess, ProcessManager
19
+
20
+
21
+ class DockerManager(ProcessManager):
22
+ """Manage nodes in Docker containers"""
23
+
24
+ def __init__(
25
+ self,
26
+ session_factory=None,
27
+ image="autonomi/node:latest",
28
+ firewall_type: str = "null",
29
+ ):
30
+ """
31
+ Initialize DockerManager.
32
+
33
+ Args:
34
+ session_factory: SQLAlchemy session factory (optional, for status updates)
35
+ image: Docker image to use for nodes
36
+ firewall_type: Type of firewall (defaults to "null" for Docker)
37
+ """
38
+ super().__init__(firewall_type)
39
+ self.S = session_factory
40
+ self.image = image
41
+
42
+ def _get_container_name(self, node: Node) -> str:
43
+ """Get Docker container name for a node"""
44
+ return f"antnode{node.node_name}"
45
+
46
+ def _ensure_image(self) -> bool:
47
+ """Ensure Docker image is available"""
48
+ try:
49
+ # Check if image exists locally
50
+ result = subprocess.run(
51
+ ["docker", "image", "inspect", self.image],
52
+ stdout=subprocess.PIPE,
53
+ stderr=subprocess.PIPE,
54
+ )
55
+ if result.returncode == 0:
56
+ return True
57
+
58
+ # Pull image if not found
59
+ logging.info(f"Pulling Docker image: {self.image}")
60
+ subprocess.run(
61
+ ["docker", "pull", self.image],
62
+ check=True,
63
+ )
64
+ return True
65
+
66
+ except subprocess.CalledProcessError as err:
67
+ logging.error(f"Failed to ensure Docker image: {err}")
68
+ return False
69
+
70
+ def create_node(self, node: Node, binary_path: str) -> bool:
71
+ """
72
+ Create and start a new node in a Docker container.
73
+
74
+ Args:
75
+ node: Node database record with configuration
76
+ binary_path: Path to the antnode binary (will be mounted into container)
77
+
78
+ Returns:
79
+ True if node was created successfully
80
+ """
81
+ logging.info(f"Creating docker node {node.id}")
82
+
83
+ # Ensure image is available
84
+ if not self._ensure_image():
85
+ return False
86
+
87
+ # Prepare directories
88
+ node_dir = Path(node.root_dir)
89
+ try:
90
+ node_dir.mkdir(parents=True, exist_ok=True)
91
+ except OSError as err:
92
+ logging.error(f"Failed to create node directory: {err}")
93
+ return False
94
+
95
+ container_name = self._get_container_name(node)
96
+
97
+ # Build docker run command
98
+ cmd = [
99
+ "docker",
100
+ "run",
101
+ "-d", # Detached mode
102
+ "--name",
103
+ container_name,
104
+ "--restart",
105
+ "unless-stopped",
106
+ # Port mappings
107
+ "-p",
108
+ f"{node.port}:{node.port}/udp",
109
+ "-p",
110
+ f"{node.metrics_port}:{node.metrics_port}/tcp",
111
+ # Volume mounts
112
+ "-v",
113
+ f"{node.root_dir}:/data",
114
+ "-v",
115
+ f"{binary_path}:/usr/local/bin/antnode:ro",
116
+ "-v",
117
+ f"{BOOTSTRAP_CACHE_DIR}:/bootstrap-cache:ro",
118
+ ]
119
+
120
+ # Add environment variables
121
+ if node.environment:
122
+ for env_var in node.environment.split():
123
+ cmd.extend(["-e", env_var])
124
+
125
+ # Add the image
126
+ cmd.append(self.image)
127
+
128
+ # Add the command to run
129
+ cmd.extend(
130
+ [
131
+ "antnode",
132
+ "--root-dir",
133
+ "/data",
134
+ "--port",
135
+ str(node.port),
136
+ "--enable-metrics-server",
137
+ "--metrics-server-port",
138
+ str(node.metrics_port),
139
+ "--bootstrap-cache-dir",
140
+ "/bootstrap-cache",
141
+ "--rewards-address",
142
+ node.wallet,
143
+ node.network,
144
+ ]
145
+ )
146
+
147
+ # Run the container
148
+ try:
149
+ result = subprocess.run(
150
+ cmd,
151
+ stdout=subprocess.PIPE,
152
+ stderr=subprocess.PIPE,
153
+ text=True,
154
+ check=True,
155
+ )
156
+ container_id = result.stdout.strip()
157
+ logging.info(f"Created container {container_name} with ID {container_id}")
158
+
159
+ except subprocess.CalledProcessError as err:
160
+ logging.error(f"Failed to create container: {err}")
161
+ logging.error(f"stderr: {err.stderr}")
162
+ return False
163
+
164
+ # Update database with container info if we have Container model support
165
+ # For now, we'll store container_id in node metadata if needed
166
+
167
+ # Wait a moment for container to start
168
+ time.sleep(1)
169
+
170
+ return True
171
+
172
+ def start_node(self, node: Node) -> bool:
173
+ """
174
+ Start a stopped Docker container.
175
+
176
+ Args:
177
+ node: Node database record
178
+
179
+ Returns:
180
+ True if node started successfully
181
+ """
182
+ logging.info(f"Starting docker node {node.id}")
183
+
184
+ container_name = self._get_container_name(node)
185
+
186
+ try:
187
+ subprocess.run(
188
+ ["docker", "start", container_name],
189
+ stdout=subprocess.PIPE,
190
+ stderr=subprocess.PIPE,
191
+ check=True,
192
+ )
193
+ except subprocess.CalledProcessError as err:
194
+ logging.error(f"Failed to start container: {err}")
195
+ return False
196
+
197
+ return True
198
+
199
+ def stop_node(self, node: Node) -> bool:
200
+ """
201
+ Stop a Docker container.
202
+
203
+ Args:
204
+ node: Node database record
205
+
206
+ Returns:
207
+ True if node stopped successfully
208
+ """
209
+ logging.info(f"Stopping docker node {node.id}")
210
+
211
+ container_name = self._get_container_name(node)
212
+
213
+ try:
214
+ # Stop with 30 second timeout for graceful shutdown
215
+ subprocess.run(
216
+ ["docker", "stop", "-t", "30", container_name],
217
+ stdout=subprocess.PIPE,
218
+ stderr=subprocess.PIPE,
219
+ check=True,
220
+ )
221
+ except subprocess.CalledProcessError as err:
222
+ logging.error(f"Failed to stop container: {err}")
223
+ return False
224
+
225
+ return True
226
+
227
+ def restart_node(self, node: Node) -> bool:
228
+ """
229
+ Restart a Docker container.
230
+
231
+ Args:
232
+ node: Node database record
233
+
234
+ Returns:
235
+ True if node restarted successfully
236
+ """
237
+ logging.info(f"Restarting docker node {node.id}")
238
+
239
+ container_name = self._get_container_name(node)
240
+
241
+ try:
242
+ subprocess.run(
243
+ ["docker", "restart", "-t", "30", container_name],
244
+ stdout=subprocess.PIPE,
245
+ stderr=subprocess.PIPE,
246
+ check=True,
247
+ )
248
+ except subprocess.CalledProcessError as err:
249
+ logging.error(f"Failed to restart container: {err}")
250
+ return False
251
+
252
+ return True
253
+
254
+ def get_status(self, node: Node) -> NodeProcess:
255
+ """
256
+ Get current status of a Docker node.
257
+
258
+ Args:
259
+ node: Node database record
260
+
261
+ Returns:
262
+ NodeProcess with current status
263
+ """
264
+ container_name = self._get_container_name(node)
265
+
266
+ try:
267
+ # Get container info
268
+ result = subprocess.run(
269
+ [
270
+ "docker",
271
+ "inspect",
272
+ "--format",
273
+ "{{.State.Status}}|{{.State.Pid}}|{{.Id}}",
274
+ container_name,
275
+ ],
276
+ stdout=subprocess.PIPE,
277
+ stderr=subprocess.PIPE,
278
+ text=True,
279
+ check=True,
280
+ )
281
+
282
+ status_str, pid_str, container_id = result.stdout.strip().split("|")
283
+
284
+ # Map Docker status to our status
285
+ if status_str == "running":
286
+ status = RUNNING
287
+ elif status_str in ("created", "restarting"):
288
+ status = RESTARTING
289
+ elif status_str in ("exited", "dead"):
290
+ status = STOPPED
291
+ else:
292
+ status = "UNKNOWN"
293
+
294
+ pid = int(pid_str) if pid_str and pid_str != "0" else None
295
+
296
+ # Check if root directory exists
297
+ if not os.path.isdir(node.root_dir):
298
+ status = DEAD
299
+
300
+ return NodeProcess(
301
+ node_id=node.id,
302
+ pid=pid,
303
+ status=status,
304
+ container_id=container_id,
305
+ )
306
+
307
+ except subprocess.CalledProcessError:
308
+ # Container doesn't exist
309
+ return NodeProcess(node_id=node.id, pid=None, status=STOPPED)
310
+ except (ValueError, IndexError) as err:
311
+ logging.error(f"Failed to parse container status: {err}")
312
+ return NodeProcess(node_id=node.id, pid=None, status="UNKNOWN")
313
+
314
+ def remove_node(self, node: Node) -> bool:
315
+ """
316
+ Stop and remove a Docker container.
317
+
318
+ Args:
319
+ node: Node database record
320
+
321
+ Returns:
322
+ True if node was removed successfully
323
+ """
324
+ logging.info(f"Removing docker node {node.id}")
325
+
326
+ container_name = self._get_container_name(node)
327
+
328
+ # Stop the container first
329
+ self.stop_node(node)
330
+
331
+ # Remove the container
332
+ try:
333
+ subprocess.run(
334
+ ["docker", "rm", "-f", container_name],
335
+ stdout=subprocess.PIPE,
336
+ stderr=subprocess.PIPE,
337
+ check=True,
338
+ )
339
+ except subprocess.CalledProcessError as err:
340
+ logging.error(f"Failed to remove container: {err}")
341
+
342
+ # Remove node data directory
343
+ try:
344
+ import shutil
345
+
346
+ node_dir = Path(node.root_dir)
347
+ if node_dir.exists():
348
+ shutil.rmtree(node_dir)
349
+ except (OSError, Exception) as err:
350
+ logging.error(f"Failed to remove node directory: {err}")
351
+
352
+ return True
353
+
354
+ def survey_nodes(self, machine_config) -> list:
355
+ """
356
+ Survey all docker-managed antnode containers.
357
+
358
+ Docker nodes are typically not used for migration scenarios.
359
+ This returns an empty list as docker containers are created
360
+ fresh by WNM and don't pre-exist.
361
+
362
+ Args:
363
+ machine_config: Machine configuration object
364
+
365
+ Returns:
366
+ Empty list (docker nodes don't pre-exist for migration)
367
+ """
368
+ logging.info(
369
+ "Docker survey not implemented (docker nodes created fresh by WNM)"
370
+ )
371
+ return []
@@ -0,0 +1,83 @@
1
+ """
2
+ Factory for creating process manager instances.
3
+
4
+ Provides a centralized way to instantiate the appropriate process manager
5
+ based on the manager type.
6
+ """
7
+
8
+ import logging
9
+
10
+ from wnm.process_managers.base import ProcessManager
11
+ from wnm.process_managers.docker_manager import DockerManager
12
+ from wnm.process_managers.launchd_manager import LaunchctlManager
13
+ from wnm.process_managers.setsid_manager import SetsidManager
14
+ from wnm.process_managers.systemd_manager import SystemdManager
15
+
16
+
17
+ def get_process_manager(
18
+ manager_type: str, session_factory=None, **kwargs
19
+ ) -> ProcessManager:
20
+ """
21
+ Get a process manager instance for the specified type.
22
+
23
+ Args:
24
+ manager_type: Type of process manager ("systemd", "docker", "setsid", etc.)
25
+ session_factory: SQLAlchemy session factory for database operations
26
+ **kwargs: Additional arguments to pass to the manager constructor
27
+
28
+ Returns:
29
+ ProcessManager instance
30
+
31
+ Raises:
32
+ ValueError: If manager_type is not supported
33
+ """
34
+ managers = {
35
+ "systemd": SystemdManager,
36
+ "docker": DockerManager,
37
+ "setsid": SetsidManager,
38
+ "launchctl": LaunchctlManager,
39
+ # Future managers:
40
+ # "antctl": AntctlManager,
41
+ }
42
+
43
+ manager_class = managers.get(manager_type)
44
+ if not manager_class:
45
+ supported = ", ".join(managers.keys())
46
+ raise ValueError(
47
+ f"Unsupported manager type: {manager_type}. "
48
+ f"Supported types: {supported}"
49
+ )
50
+
51
+ logging.debug(f"Creating {manager_type} process manager")
52
+ return manager_class(session_factory=session_factory, **kwargs)
53
+
54
+
55
+ def get_default_manager_type() -> str:
56
+ """
57
+ Determine the default process manager type for the current system.
58
+
59
+ Returns:
60
+ Default manager type string ("systemd", "setsid", etc.)
61
+ """
62
+ import platform
63
+ import shutil
64
+
65
+ system = platform.system()
66
+
67
+ # Linux: prefer systemd if available
68
+ if system == "Linux":
69
+ if shutil.which("systemctl"):
70
+ return "systemd"
71
+ return "setsid"
72
+
73
+ # macOS: use launchctl for native macOS support
74
+ if system == "Darwin":
75
+ return "launchctl"
76
+
77
+ # Windows: not supported
78
+ if system == "Windows":
79
+ raise RuntimeError("Windows is not currently supported")
80
+
81
+ # Unknown system: fall back to setsid
82
+ logging.warning(f"Unknown system {system}, defaulting to setsid manager")
83
+ return "setsid"