wnm 0.0.9__py3-none-any.whl → 0.0.10__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 +1 -1
- wnm/__main__.py +184 -1133
- wnm/actions.py +45 -0
- wnm/common.py +21 -0
- wnm/config.py +653 -1
- wnm/decision_engine.py +388 -0
- wnm/executor.py +1292 -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 +305 -126
- 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 +443 -0
- wnm/reports.py +286 -0
- wnm/utils.py +403 -0
- wnm-0.0.10.dist-info/METADATA +316 -0
- wnm-0.0.10.dist-info/RECORD +28 -0
- {wnm-0.0.9.dist-info → wnm-0.0.10.dist-info}/WHEEL +1 -1
- wnm-0.0.9.dist-info/METADATA +0 -95
- wnm-0.0.9.dist-info/RECORD +0 -9
- {wnm-0.0.9.dist-info → wnm-0.0.10.dist-info}/entry_points.txt +0 -0
- {wnm-0.0.9.dist-info → wnm-0.0.10.dist-info}/top_level.txt +0 -0
|
@@ -0,0 +1,203 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Abstract base class for process managers.
|
|
3
|
+
|
|
4
|
+
Process managers handle node lifecycle operations across different
|
|
5
|
+
execution environments (systemd, docker, setsid, etc.)
|
|
6
|
+
"""
|
|
7
|
+
|
|
8
|
+
from abc import ABC, abstractmethod
|
|
9
|
+
from dataclasses import dataclass
|
|
10
|
+
from typing import Optional
|
|
11
|
+
|
|
12
|
+
from wnm.firewall.factory import get_firewall_manager
|
|
13
|
+
from wnm.models import Node
|
|
14
|
+
|
|
15
|
+
|
|
16
|
+
@dataclass
|
|
17
|
+
class NodeProcess:
|
|
18
|
+
"""Represents the runtime state of a node process"""
|
|
19
|
+
|
|
20
|
+
node_id: int
|
|
21
|
+
pid: Optional[int] = None
|
|
22
|
+
status: str = "UNKNOWN" # RUNNING, STOPPED, UPGRADING, etc.
|
|
23
|
+
container_id: Optional[str] = None # For docker-managed nodes
|
|
24
|
+
|
|
25
|
+
|
|
26
|
+
class ProcessManager(ABC):
|
|
27
|
+
"""
|
|
28
|
+
Abstract interface for node lifecycle management.
|
|
29
|
+
|
|
30
|
+
Each implementation handles a specific process management backend:
|
|
31
|
+
- SystemdManager: systemd services (Linux)
|
|
32
|
+
- DockerManager: Docker containers
|
|
33
|
+
- SetsidManager: Background processes via setsid
|
|
34
|
+
- AntctlManager: Wrapper around antctl CLI
|
|
35
|
+
- LaunchctlManager: macOS launchd services
|
|
36
|
+
"""
|
|
37
|
+
|
|
38
|
+
def __init__(self, firewall_type: str = None):
|
|
39
|
+
"""
|
|
40
|
+
Initialize process manager with optional firewall manager.
|
|
41
|
+
|
|
42
|
+
Args:
|
|
43
|
+
firewall_type: Type of firewall to use ("ufw", "null", etc.)
|
|
44
|
+
If None, auto-detects best available option
|
|
45
|
+
"""
|
|
46
|
+
self.firewall = get_firewall_manager(firewall_type)
|
|
47
|
+
|
|
48
|
+
@abstractmethod
|
|
49
|
+
def create_node(self, node: Node, binary_path: str) -> bool:
|
|
50
|
+
"""
|
|
51
|
+
Create and start a new node.
|
|
52
|
+
|
|
53
|
+
Args:
|
|
54
|
+
node: Node database record with configuration
|
|
55
|
+
binary_path: Path to the node binary to execute
|
|
56
|
+
|
|
57
|
+
Returns:
|
|
58
|
+
True if node was created successfully
|
|
59
|
+
"""
|
|
60
|
+
pass
|
|
61
|
+
|
|
62
|
+
@abstractmethod
|
|
63
|
+
def start_node(self, node: Node) -> bool:
|
|
64
|
+
"""
|
|
65
|
+
Start a stopped node.
|
|
66
|
+
|
|
67
|
+
Args:
|
|
68
|
+
node: Node database record
|
|
69
|
+
|
|
70
|
+
Returns:
|
|
71
|
+
True if node started successfully
|
|
72
|
+
"""
|
|
73
|
+
pass
|
|
74
|
+
|
|
75
|
+
@abstractmethod
|
|
76
|
+
def stop_node(self, node: Node) -> bool:
|
|
77
|
+
"""
|
|
78
|
+
Stop a running node.
|
|
79
|
+
|
|
80
|
+
Args:
|
|
81
|
+
node: Node database record
|
|
82
|
+
|
|
83
|
+
Returns:
|
|
84
|
+
True if node stopped successfully
|
|
85
|
+
"""
|
|
86
|
+
pass
|
|
87
|
+
|
|
88
|
+
@abstractmethod
|
|
89
|
+
def restart_node(self, node: Node) -> bool:
|
|
90
|
+
"""
|
|
91
|
+
Restart a node.
|
|
92
|
+
|
|
93
|
+
Args:
|
|
94
|
+
node: Node database record
|
|
95
|
+
|
|
96
|
+
Returns:
|
|
97
|
+
True if node restarted successfully
|
|
98
|
+
"""
|
|
99
|
+
pass
|
|
100
|
+
|
|
101
|
+
@abstractmethod
|
|
102
|
+
def get_status(self, node: Node) -> NodeProcess:
|
|
103
|
+
"""
|
|
104
|
+
Get current runtime status of a node.
|
|
105
|
+
|
|
106
|
+
Args:
|
|
107
|
+
node: Node database record
|
|
108
|
+
|
|
109
|
+
Returns:
|
|
110
|
+
NodeProcess with current status and PID
|
|
111
|
+
"""
|
|
112
|
+
pass
|
|
113
|
+
|
|
114
|
+
@abstractmethod
|
|
115
|
+
def remove_node(self, node: Node) -> bool:
|
|
116
|
+
"""
|
|
117
|
+
Stop and remove all traces of a node.
|
|
118
|
+
|
|
119
|
+
This should:
|
|
120
|
+
1. Stop the node process
|
|
121
|
+
2. Remove service/container definitions
|
|
122
|
+
3. Optionally clean up data directories (controlled by node.keep_data)
|
|
123
|
+
|
|
124
|
+
Args:
|
|
125
|
+
node: Node database record
|
|
126
|
+
|
|
127
|
+
Returns:
|
|
128
|
+
True if node was removed successfully
|
|
129
|
+
"""
|
|
130
|
+
pass
|
|
131
|
+
|
|
132
|
+
@abstractmethod
|
|
133
|
+
def survey_nodes(self, machine_config) -> list:
|
|
134
|
+
"""
|
|
135
|
+
Survey all nodes managed by this process manager.
|
|
136
|
+
|
|
137
|
+
This is used during database initialization/rebuild to discover
|
|
138
|
+
existing nodes by scanning the manager's configuration directory.
|
|
139
|
+
Each manager handles its own path logic internally.
|
|
140
|
+
|
|
141
|
+
The database is the source of truth for regular operations.
|
|
142
|
+
This method is ONLY used for initialization and migration.
|
|
143
|
+
|
|
144
|
+
Args:
|
|
145
|
+
machine_config: Machine configuration object
|
|
146
|
+
|
|
147
|
+
Returns:
|
|
148
|
+
List of node dictionaries ready for database insertion
|
|
149
|
+
"""
|
|
150
|
+
pass
|
|
151
|
+
|
|
152
|
+
def enable_firewall_port(
|
|
153
|
+
self, port: int, protocol: str = "udp", comment: str = None
|
|
154
|
+
) -> bool:
|
|
155
|
+
"""
|
|
156
|
+
Open firewall port for node communication.
|
|
157
|
+
|
|
158
|
+
Uses the configured firewall manager to open the port.
|
|
159
|
+
Subclasses can override for custom firewall behavior.
|
|
160
|
+
|
|
161
|
+
Args:
|
|
162
|
+
port: Port number to open
|
|
163
|
+
protocol: Protocol type (udp/tcp)
|
|
164
|
+
comment: Optional comment for the firewall rule
|
|
165
|
+
|
|
166
|
+
Returns:
|
|
167
|
+
True if port was opened successfully
|
|
168
|
+
"""
|
|
169
|
+
return self.firewall.enable_port(port, protocol, comment)
|
|
170
|
+
|
|
171
|
+
def disable_firewall_port(self, port: int, protocol: str = "udp") -> bool:
|
|
172
|
+
"""
|
|
173
|
+
Close firewall port when node is removed.
|
|
174
|
+
|
|
175
|
+
Uses the configured firewall manager to close the port.
|
|
176
|
+
Subclasses can override for custom firewall behavior.
|
|
177
|
+
|
|
178
|
+
Args:
|
|
179
|
+
port: Port number to close
|
|
180
|
+
protocol: Protocol type (udp/tcp)
|
|
181
|
+
|
|
182
|
+
Returns:
|
|
183
|
+
True if port was closed successfully
|
|
184
|
+
"""
|
|
185
|
+
return self.firewall.disable_port(port, protocol)
|
|
186
|
+
|
|
187
|
+
def teardown_cluster(self) -> bool:
|
|
188
|
+
"""
|
|
189
|
+
Teardown the entire cluster using manager-specific commands.
|
|
190
|
+
|
|
191
|
+
This is an optional method that managers can override to provide
|
|
192
|
+
efficient bulk teardown operations. If not overridden, returns False
|
|
193
|
+
to indicate that individual node removal should be used instead.
|
|
194
|
+
|
|
195
|
+
Examples:
|
|
196
|
+
- AntctlManager: Uses 'antctl reset' command
|
|
197
|
+
- Other managers: Return False to use default individual removal
|
|
198
|
+
|
|
199
|
+
Returns:
|
|
200
|
+
True if cluster was torn down successfully using manager-specific method,
|
|
201
|
+
False to indicate fallback to individual node removal
|
|
202
|
+
"""
|
|
203
|
+
return False
|
|
@@ -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"
|