wnm 0.0.10__tar.gz → 0.0.11__tar.gz
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-0.0.10 → wnm-0.0.11}/PKG-INFO +1 -1
- {wnm-0.0.10 → wnm-0.0.11}/src/wnm/__init__.py +1 -1
- {wnm-0.0.10 → wnm-0.0.11}/src/wnm/process_managers/systemd_manager.py +158 -72
- {wnm-0.0.10 → wnm-0.0.11}/src/wnm/reports.py +2 -2
- {wnm-0.0.10 → wnm-0.0.11}/src/wnm.egg-info/PKG-INFO +1 -1
- {wnm-0.0.10 → wnm-0.0.11}/README.md +0 -0
- {wnm-0.0.10 → wnm-0.0.11}/pyproject.toml +0 -0
- {wnm-0.0.10 → wnm-0.0.11}/requirements-dev.txt +0 -0
- {wnm-0.0.10 → wnm-0.0.11}/requirements.txt +0 -0
- {wnm-0.0.10 → wnm-0.0.11}/setup.cfg +0 -0
- {wnm-0.0.10 → wnm-0.0.11}/src/wnm/__main__.py +0 -0
- {wnm-0.0.10 → wnm-0.0.11}/src/wnm/actions.py +0 -0
- {wnm-0.0.10 → wnm-0.0.11}/src/wnm/common.py +0 -0
- {wnm-0.0.10 → wnm-0.0.11}/src/wnm/config.py +0 -0
- {wnm-0.0.10 → wnm-0.0.11}/src/wnm/decision_engine.py +0 -0
- {wnm-0.0.10 → wnm-0.0.11}/src/wnm/executor.py +0 -0
- {wnm-0.0.10 → wnm-0.0.11}/src/wnm/firewall/__init__.py +0 -0
- {wnm-0.0.10 → wnm-0.0.11}/src/wnm/firewall/base.py +0 -0
- {wnm-0.0.10 → wnm-0.0.11}/src/wnm/firewall/factory.py +0 -0
- {wnm-0.0.10 → wnm-0.0.11}/src/wnm/firewall/null_firewall.py +0 -0
- {wnm-0.0.10 → wnm-0.0.11}/src/wnm/firewall/ufw_manager.py +0 -0
- {wnm-0.0.10 → wnm-0.0.11}/src/wnm/migration.py +0 -0
- {wnm-0.0.10 → wnm-0.0.11}/src/wnm/models.py +0 -0
- {wnm-0.0.10 → wnm-0.0.11}/src/wnm/process_managers/__init__.py +0 -0
- {wnm-0.0.10 → wnm-0.0.11}/src/wnm/process_managers/base.py +0 -0
- {wnm-0.0.10 → wnm-0.0.11}/src/wnm/process_managers/docker_manager.py +0 -0
- {wnm-0.0.10 → wnm-0.0.11}/src/wnm/process_managers/factory.py +0 -0
- {wnm-0.0.10 → wnm-0.0.11}/src/wnm/process_managers/launchd_manager.py +0 -0
- {wnm-0.0.10 → wnm-0.0.11}/src/wnm/process_managers/setsid_manager.py +0 -0
- {wnm-0.0.10 → wnm-0.0.11}/src/wnm/utils.py +0 -0
- {wnm-0.0.10 → wnm-0.0.11}/src/wnm.egg-info/SOURCES.txt +0 -0
- {wnm-0.0.10 → wnm-0.0.11}/src/wnm.egg-info/dependency_links.txt +0 -0
- {wnm-0.0.10 → wnm-0.0.11}/src/wnm.egg-info/entry_points.txt +0 -0
- {wnm-0.0.10 → wnm-0.0.11}/src/wnm.egg-info/requires.txt +0 -0
- {wnm-0.0.10 → wnm-0.0.11}/src/wnm.egg-info/top_level.txt +0 -0
- {wnm-0.0.10 → wnm-0.0.11}/tests/test_decision_engine.py +0 -0
- {wnm-0.0.10 → wnm-0.0.11}/tests/test_firewall.py +0 -0
- {wnm-0.0.10 → wnm-0.0.11}/tests/test_forced_actions.py +0 -0
- {wnm-0.0.10 → wnm-0.0.11}/tests/test_macos_native.py +0 -0
- {wnm-0.0.10 → wnm-0.0.11}/tests/test_models.py +0 -0
- {wnm-0.0.10 → wnm-0.0.11}/tests/test_process_managers.py +0 -0
- {wnm-0.0.10 → wnm-0.0.11}/tests/test_reports.py +0 -0
- {wnm-0.0.10 → wnm-0.0.11}/tests/test_system_metrics.py +0 -0
|
@@ -8,11 +8,12 @@ Requires sudo privileges for systemctl and firewall operations.
|
|
|
8
8
|
import logging
|
|
9
9
|
import os
|
|
10
10
|
import re
|
|
11
|
+
import shutil
|
|
11
12
|
import subprocess
|
|
12
13
|
import time
|
|
13
14
|
|
|
14
15
|
from wnm.common import DEAD, RESTARTING, RUNNING, STOPPED, UPGRADING
|
|
15
|
-
from wnm.config import BOOTSTRAP_CACHE_DIR, LOG_DIR
|
|
16
|
+
from wnm.config import BOOTSTRAP_CACHE_DIR, IS_ROOT, LOG_DIR
|
|
16
17
|
from wnm.models import Node
|
|
17
18
|
from wnm.process_managers.base import NodeProcess, ProcessManager
|
|
18
19
|
from wnm.utils import (
|
|
@@ -24,7 +25,7 @@ from wnm.utils import (
|
|
|
24
25
|
|
|
25
26
|
|
|
26
27
|
class SystemdManager(ProcessManager):
|
|
27
|
-
"""Manage nodes as systemd services"""
|
|
28
|
+
"""Manage nodes as systemd services (system or user mode)"""
|
|
28
29
|
|
|
29
30
|
def __init__(self, session_factory=None, firewall_type: str = None):
|
|
30
31
|
"""
|
|
@@ -32,11 +33,29 @@ class SystemdManager(ProcessManager):
|
|
|
32
33
|
|
|
33
34
|
Args:
|
|
34
35
|
session_factory: SQLAlchemy session factory (optional, for status updates)
|
|
35
|
-
firewall_type: Type of firewall to use (defaults to auto-detect)
|
|
36
|
+
firewall_type: Type of firewall to use (defaults to auto-detect, null for non-root)
|
|
36
37
|
"""
|
|
38
|
+
# Determine if we're using system or user services
|
|
39
|
+
# Root users use system services in /etc/systemd/system/
|
|
40
|
+
# Non-root users use user services in ~/.config/systemd/user/
|
|
41
|
+
self.use_system_services = IS_ROOT
|
|
42
|
+
|
|
43
|
+
# Non-root users should use null firewall by default (to avoid sudo)
|
|
44
|
+
if not IS_ROOT and firewall_type is None:
|
|
45
|
+
firewall_type = "null"
|
|
46
|
+
|
|
37
47
|
super().__init__(firewall_type)
|
|
38
48
|
self.S = session_factory
|
|
39
49
|
|
|
50
|
+
if self.use_system_services:
|
|
51
|
+
self.service_dir = "/etc/systemd/system"
|
|
52
|
+
self.systemctl_cmd = ["sudo", "systemctl"]
|
|
53
|
+
else:
|
|
54
|
+
self.service_dir = os.path.expanduser("~/.config/systemd/user")
|
|
55
|
+
self.systemctl_cmd = ["systemctl", "--user"]
|
|
56
|
+
# Create user service directory if it doesn't exist
|
|
57
|
+
os.makedirs(self.service_dir, exist_ok=True)
|
|
58
|
+
|
|
40
59
|
def create_node(self, node: Node, binary_path: str) -> bool:
|
|
41
60
|
"""
|
|
42
61
|
Create and start a new node as a systemd service.
|
|
@@ -55,70 +74,113 @@ class SystemdManager(ProcessManager):
|
|
|
55
74
|
log_dir = f"{LOG_DIR}/antnode{node.node_name}"
|
|
56
75
|
|
|
57
76
|
# Create directories
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
77
|
+
if self.use_system_services:
|
|
78
|
+
# Root: use sudo for system paths
|
|
79
|
+
try:
|
|
80
|
+
subprocess.run(
|
|
81
|
+
["sudo", "mkdir", "-p", node.root_dir, log_dir],
|
|
82
|
+
stdout=subprocess.PIPE,
|
|
83
|
+
check=True,
|
|
84
|
+
)
|
|
85
|
+
except subprocess.CalledProcessError as err:
|
|
86
|
+
logging.error(f"Failed to create directories: {err}")
|
|
87
|
+
return False
|
|
88
|
+
else:
|
|
89
|
+
# Non-root: create in user paths without sudo
|
|
90
|
+
try:
|
|
91
|
+
os.makedirs(node.root_dir, exist_ok=True)
|
|
92
|
+
os.makedirs(log_dir, exist_ok=True)
|
|
93
|
+
except OSError as err:
|
|
94
|
+
logging.error(f"Failed to create directories: {err}")
|
|
95
|
+
return False
|
|
67
96
|
|
|
68
97
|
# Copy binary to node directory
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
98
|
+
if self.use_system_services:
|
|
99
|
+
# Root: use sudo to copy
|
|
100
|
+
try:
|
|
101
|
+
subprocess.run(
|
|
102
|
+
["sudo", "cp", binary_path, node.root_dir],
|
|
103
|
+
stdout=subprocess.PIPE,
|
|
104
|
+
check=True,
|
|
105
|
+
)
|
|
106
|
+
except subprocess.CalledProcessError as err:
|
|
107
|
+
logging.error(f"Failed to copy binary: {err}")
|
|
108
|
+
return False
|
|
109
|
+
else:
|
|
110
|
+
# Non-root: copy as current user
|
|
111
|
+
try:
|
|
112
|
+
shutil.copy2(binary_path, node.root_dir)
|
|
113
|
+
binary_dest = os.path.join(node.root_dir, "antnode")
|
|
114
|
+
os.chmod(binary_dest, 0o755)
|
|
115
|
+
except (OSError, shutil.Error) as err:
|
|
116
|
+
logging.error(f"Failed to copy binary: {err}")
|
|
117
|
+
return False
|
|
78
118
|
|
|
79
|
-
# Change ownership
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
119
|
+
# Change ownership (only when running as root)
|
|
120
|
+
# When running as non-root user, files remain owned by current user
|
|
121
|
+
if self.use_system_services:
|
|
122
|
+
user = getattr(node, "user", "ant")
|
|
123
|
+
try:
|
|
124
|
+
subprocess.run(
|
|
125
|
+
["sudo", "chown", "-R", f"{user}:{user}", node.root_dir, log_dir],
|
|
126
|
+
stdout=subprocess.PIPE,
|
|
127
|
+
check=True,
|
|
128
|
+
)
|
|
129
|
+
except subprocess.CalledProcessError as err:
|
|
130
|
+
logging.error(f"Failed to change ownership: {err}")
|
|
131
|
+
return False
|
|
90
132
|
|
|
91
133
|
# Build systemd service unit
|
|
92
134
|
env_string = f'Environment="{node.environment}"' if node.environment else ""
|
|
93
135
|
binary_in_node_dir = f"{node.root_dir}/antnode"
|
|
94
136
|
|
|
137
|
+
# Determine which user to run as
|
|
138
|
+
# System services (root): use 'ant' user for security
|
|
139
|
+
# User services (non-root): don't specify User= (runs as current user)
|
|
140
|
+
if self.use_system_services:
|
|
141
|
+
user = getattr(node, "user", "ant")
|
|
142
|
+
user_line = f"User={user}"
|
|
143
|
+
else:
|
|
144
|
+
user_line = "" # User services run as the invoking user
|
|
145
|
+
|
|
95
146
|
service_content = f"""[Unit]
|
|
96
147
|
Description=antnode{node.node_name}
|
|
97
148
|
[Service]
|
|
98
149
|
{env_string}
|
|
99
|
-
|
|
150
|
+
{user_line}
|
|
100
151
|
ExecStart={binary_in_node_dir} --bootstrap-cache-dir {BOOTSTRAP_CACHE_DIR} --root-dir {node.root_dir} --port {node.port} --enable-metrics-server --metrics-server-port {node.metrics_port} --log-output-dest {log_dir} --max-log-files 1 --max-archived-log-files 1 --rewards-address {node.wallet} {node.network}
|
|
101
152
|
Restart=always
|
|
102
153
|
#RestartSec=300
|
|
103
154
|
"""
|
|
104
155
|
|
|
105
156
|
# Write service file
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
157
|
+
service_path = f"{self.service_dir}/{service_name}"
|
|
158
|
+
if self.use_system_services:
|
|
159
|
+
# System services: use sudo to write to /etc/systemd/system
|
|
160
|
+
try:
|
|
161
|
+
subprocess.run(
|
|
162
|
+
["sudo", "tee", service_path],
|
|
163
|
+
input=service_content,
|
|
164
|
+
text=True,
|
|
165
|
+
stdout=subprocess.PIPE,
|
|
166
|
+
check=True,
|
|
167
|
+
)
|
|
168
|
+
except subprocess.CalledProcessError as err:
|
|
169
|
+
logging.error(f"Failed to write service file: {err}")
|
|
170
|
+
return False
|
|
171
|
+
else:
|
|
172
|
+
# User services: write directly to ~/.config/systemd/user
|
|
173
|
+
try:
|
|
174
|
+
with open(service_path, "w") as f:
|
|
175
|
+
f.write(service_content)
|
|
176
|
+
except OSError as err:
|
|
177
|
+
logging.error(f"Failed to write service file: {err}")
|
|
178
|
+
return False
|
|
117
179
|
|
|
118
180
|
# Reload systemd
|
|
119
181
|
try:
|
|
120
182
|
subprocess.run(
|
|
121
|
-
|
|
183
|
+
self.systemctl_cmd + ["daemon-reload"],
|
|
122
184
|
stdout=subprocess.PIPE,
|
|
123
185
|
check=True,
|
|
124
186
|
)
|
|
@@ -144,7 +206,7 @@ Restart=always
|
|
|
144
206
|
# Start service
|
|
145
207
|
try:
|
|
146
208
|
result = subprocess.run(
|
|
147
|
-
|
|
209
|
+
self.systemctl_cmd + ["start", node.service],
|
|
148
210
|
stdout=subprocess.PIPE,
|
|
149
211
|
stderr=subprocess.STDOUT,
|
|
150
212
|
text=True,
|
|
@@ -176,7 +238,7 @@ Restart=always
|
|
|
176
238
|
# Stop service
|
|
177
239
|
try:
|
|
178
240
|
subprocess.run(
|
|
179
|
-
|
|
241
|
+
self.systemctl_cmd + ["stop", node.service],
|
|
180
242
|
stdout=subprocess.PIPE,
|
|
181
243
|
check=True,
|
|
182
244
|
)
|
|
@@ -203,7 +265,7 @@ Restart=always
|
|
|
203
265
|
|
|
204
266
|
try:
|
|
205
267
|
subprocess.run(
|
|
206
|
-
|
|
268
|
+
self.systemctl_cmd + ["restart", node.service],
|
|
207
269
|
stdout=subprocess.PIPE,
|
|
208
270
|
check=True,
|
|
209
271
|
)
|
|
@@ -225,7 +287,8 @@ Restart=always
|
|
|
225
287
|
"""
|
|
226
288
|
try:
|
|
227
289
|
result = subprocess.run(
|
|
228
|
-
|
|
290
|
+
self.systemctl_cmd
|
|
291
|
+
+ ["show", node.service, "--property=MainPID,ActiveState"],
|
|
229
292
|
stdout=subprocess.PIPE,
|
|
230
293
|
text=True,
|
|
231
294
|
check=True,
|
|
@@ -274,29 +337,51 @@ Restart=always
|
|
|
274
337
|
self.stop_node(node)
|
|
275
338
|
|
|
276
339
|
nodename = f"antnode{node.node_name}"
|
|
340
|
+
log_path = f"{LOG_DIR}/{nodename}"
|
|
277
341
|
|
|
278
342
|
# Remove data and logs
|
|
279
|
-
|
|
280
|
-
|
|
281
|
-
|
|
282
|
-
|
|
283
|
-
|
|
284
|
-
|
|
285
|
-
|
|
343
|
+
if self.use_system_services:
|
|
344
|
+
# System services: use sudo to remove
|
|
345
|
+
try:
|
|
346
|
+
subprocess.run(
|
|
347
|
+
["sudo", "rm", "-rf", node.root_dir, log_path],
|
|
348
|
+
check=True,
|
|
349
|
+
)
|
|
350
|
+
except subprocess.CalledProcessError as err:
|
|
351
|
+
logging.error(f"Failed to remove node data: {err}")
|
|
352
|
+
else:
|
|
353
|
+
# User services: remove as current user
|
|
354
|
+
try:
|
|
355
|
+
if os.path.exists(node.root_dir):
|
|
356
|
+
shutil.rmtree(node.root_dir)
|
|
357
|
+
if os.path.exists(log_path):
|
|
358
|
+
shutil.rmtree(log_path)
|
|
359
|
+
except (OSError, shutil.Error) as err:
|
|
360
|
+
logging.error(f"Failed to remove node data: {err}")
|
|
286
361
|
|
|
287
362
|
# Remove service file
|
|
288
|
-
|
|
289
|
-
|
|
290
|
-
|
|
291
|
-
|
|
292
|
-
|
|
293
|
-
|
|
294
|
-
|
|
363
|
+
service_path = f"{self.service_dir}/{node.service}"
|
|
364
|
+
if self.use_system_services:
|
|
365
|
+
# System services: use sudo to remove
|
|
366
|
+
try:
|
|
367
|
+
subprocess.run(
|
|
368
|
+
["sudo", "rm", "-f", service_path],
|
|
369
|
+
check=True,
|
|
370
|
+
)
|
|
371
|
+
except subprocess.CalledProcessError as err:
|
|
372
|
+
logging.error(f"Failed to remove service file: {err}")
|
|
373
|
+
else:
|
|
374
|
+
# User services: remove as current user
|
|
375
|
+
try:
|
|
376
|
+
if os.path.exists(service_path):
|
|
377
|
+
os.remove(service_path)
|
|
378
|
+
except OSError as err:
|
|
379
|
+
logging.error(f"Failed to remove service file: {err}")
|
|
295
380
|
|
|
296
381
|
# Reload systemd
|
|
297
382
|
try:
|
|
298
383
|
subprocess.run(
|
|
299
|
-
|
|
384
|
+
self.systemctl_cmd + ["daemon-reload"],
|
|
300
385
|
stdout=subprocess.PIPE,
|
|
301
386
|
check=True,
|
|
302
387
|
)
|
|
@@ -309,7 +394,7 @@ Restart=always
|
|
|
309
394
|
"""
|
|
310
395
|
Survey all systemd-managed antnode services.
|
|
311
396
|
|
|
312
|
-
Scans
|
|
397
|
+
Scans systemd service directory (system or user) for antnode*.service files and
|
|
313
398
|
collects their configuration and current status.
|
|
314
399
|
|
|
315
400
|
Args:
|
|
@@ -318,17 +403,16 @@ Restart=always
|
|
|
318
403
|
Returns:
|
|
319
404
|
List of node dictionaries ready for database insertion
|
|
320
405
|
"""
|
|
321
|
-
systemd_dir = "/etc/systemd/system"
|
|
322
406
|
service_names = []
|
|
323
407
|
|
|
324
|
-
# Scan for antnode service files
|
|
325
|
-
if os.path.exists(
|
|
408
|
+
# Scan for antnode service files in the appropriate directory
|
|
409
|
+
if os.path.exists(self.service_dir):
|
|
326
410
|
try:
|
|
327
|
-
for file in os.listdir(
|
|
411
|
+
for file in os.listdir(self.service_dir):
|
|
328
412
|
if re.match(r"antnode[\d]+\.service", file):
|
|
329
413
|
service_names.append(file)
|
|
330
414
|
except PermissionError as e:
|
|
331
|
-
logging.error(f"Permission denied reading {
|
|
415
|
+
logging.error(f"Permission denied reading {self.service_dir}: {e}")
|
|
332
416
|
return []
|
|
333
417
|
except Exception as e:
|
|
334
418
|
logging.error(f"Error listing systemd services: {e}")
|
|
@@ -405,7 +489,7 @@ Restart=always
|
|
|
405
489
|
Dictionary with node configuration, or empty dict on error
|
|
406
490
|
"""
|
|
407
491
|
details = {}
|
|
408
|
-
service_path = f"/
|
|
492
|
+
service_path = f"{self.service_dir}/{service_name}"
|
|
409
493
|
|
|
410
494
|
try:
|
|
411
495
|
with open(service_path, "r") as file:
|
|
@@ -413,7 +497,9 @@ Restart=always
|
|
|
413
497
|
|
|
414
498
|
details["id"] = int(re.findall(r"antnode(\d+)", service_name)[0])
|
|
415
499
|
details["binary"] = re.findall(r"ExecStart=([^ ]+)", data)[0]
|
|
416
|
-
|
|
500
|
+
# User field may be empty for user services
|
|
501
|
+
user_matches = re.findall(r"User=(\w+)", data)
|
|
502
|
+
details["user"] = user_matches[0] if user_matches else os.getenv("USER", "nobody")
|
|
417
503
|
details["root_dir"] = re.findall(r"--root-dir ([\w\/]+)", data)[0]
|
|
418
504
|
details["port"] = int(re.findall(r"--port (\d+)", data)[0])
|
|
419
505
|
details["metrics_port"] = int(
|
|
@@ -117,13 +117,13 @@ class NodeReporter:
|
|
|
117
117
|
lines = []
|
|
118
118
|
|
|
119
119
|
# Header
|
|
120
|
-
header = f"{'Service Name':<20}{'Peer ID':<
|
|
120
|
+
header = f"{'Service Name':<20}{'Peer ID':<55}{'Status':<15}{'Connected Peers':>15}"
|
|
121
121
|
lines.append(header)
|
|
122
122
|
|
|
123
123
|
# Node rows
|
|
124
124
|
for node in nodes:
|
|
125
125
|
service_col = f"{node.service:<20}"
|
|
126
|
-
peer_id_col = f"{(node.peer_id or '-'):<
|
|
126
|
+
peer_id_col = f"{(node.peer_id or '-'):<55}"
|
|
127
127
|
status_col = f"{node.status:<15}"
|
|
128
128
|
# Connected peers from connected_peers field
|
|
129
129
|
peers = node.connected_peers if node.connected_peers is not None else 0
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|