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.
- wnm/__init__.py +3 -0
- wnm/__main__.py +236 -0
- wnm/actions.py +45 -0
- wnm/common.py +23 -0
- wnm/config.py +673 -0
- wnm/decision_engine.py +388 -0
- wnm/executor.py +1299 -0
- wnm/firewall/__init__.py +13 -0
- wnm/firewall/base.py +71 -0
- wnm/firewall/factory.py +95 -0
- wnm/firewall/null_firewall.py +71 -0
- wnm/firewall/ufw_manager.py +118 -0
- wnm/migration.py +42 -0
- wnm/models.py +459 -0
- wnm/process_managers/__init__.py +23 -0
- wnm/process_managers/base.py +203 -0
- wnm/process_managers/docker_manager.py +371 -0
- wnm/process_managers/factory.py +83 -0
- wnm/process_managers/launchd_manager.py +592 -0
- wnm/process_managers/setsid_manager.py +340 -0
- wnm/process_managers/systemd_manager.py +529 -0
- wnm/reports.py +286 -0
- wnm/utils.py +407 -0
- wnm/wallets.py +177 -0
- wnm-0.0.12.dist-info/METADATA +367 -0
- wnm-0.0.12.dist-info/RECORD +29 -0
- wnm-0.0.12.dist-info/WHEEL +5 -0
- wnm-0.0.12.dist-info/entry_points.txt +2 -0
- wnm-0.0.12.dist-info/top_level.txt +1 -0
|
@@ -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"
|