wnm 0.0.10__py3-none-any.whl → 0.0.11__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 CHANGED
@@ -1,3 +1,3 @@
1
1
  """A service to manage a cluster of decentralized Autonomi nodes"""
2
2
 
3
- __version__ = "0.0.10"
3
+ __version__ = "0.0.11"
@@ -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
- try:
59
- subprocess.run(
60
- ["sudo", "mkdir", "-p", node.root_dir, log_dir],
61
- stdout=subprocess.PIPE,
62
- check=True,
63
- )
64
- except subprocess.CalledProcessError as err:
65
- logging.error(f"Failed to create directories: {err}")
66
- return False
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
- try:
70
- subprocess.run(
71
- ["sudo", "cp", binary_path, node.root_dir],
72
- stdout=subprocess.PIPE,
73
- check=True,
74
- )
75
- except subprocess.CalledProcessError as err:
76
- logging.error(f"Failed to copy binary: {err}")
77
- return False
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
- user = getattr(node, "user", "ant")
81
- try:
82
- subprocess.run(
83
- ["sudo", "chown", "-R", f"{user}:{user}", node.root_dir, log_dir],
84
- stdout=subprocess.PIPE,
85
- check=True,
86
- )
87
- except subprocess.CalledProcessError as err:
88
- logging.error(f"Failed to change ownership: {err}")
89
- return False
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
- User={user}
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
- try:
107
- subprocess.run(
108
- ["sudo", "tee", f"/etc/systemd/system/{service_name}"],
109
- input=service_content,
110
- text=True,
111
- stdout=subprocess.PIPE,
112
- check=True,
113
- )
114
- except subprocess.CalledProcessError as err:
115
- logging.error(f"Failed to write service file: {err}")
116
- return False
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
- ["sudo", "systemctl", "daemon-reload"],
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
- ["sudo", "systemctl", "start", node.service],
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
- ["sudo", "systemctl", "stop", node.service],
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
- ["sudo", "systemctl", "restart", node.service],
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
- ["systemctl", "show", node.service, "--property=MainPID,ActiveState"],
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
- try:
280
- subprocess.run(
281
- ["sudo", "rm", "-rf", node.root_dir, f"{LOG_DIR}/{nodename}"],
282
- check=True,
283
- )
284
- except subprocess.CalledProcessError as err:
285
- logging.error(f"Failed to remove node data: {err}")
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
- try:
289
- subprocess.run(
290
- ["sudo", "rm", "-f", f"/etc/systemd/system/{node.service}"],
291
- check=True,
292
- )
293
- except subprocess.CalledProcessError as err:
294
- logging.error(f"Failed to remove service file: {err}")
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
- ["sudo", "systemctl", "daemon-reload"],
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 /etc/systemd/system for antnode*.service files and
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(systemd_dir):
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(systemd_dir):
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 {systemd_dir}: {e}")
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"/etc/systemd/system/{service_name}"
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
- details["user"] = re.findall(r"User=(\w+)", data)[0]
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(
wnm/reports.py CHANGED
@@ -117,13 +117,13 @@ class NodeReporter:
117
117
  lines = []
118
118
 
119
119
  # Header
120
- header = f"{'Service Name':<20}{'Peer ID':<50}{'Status':<15}{'Connected Peers':>15}"
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 '-'):<50}"
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
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: wnm
3
- Version: 0.0.10
3
+ Version: 0.0.11
4
4
  Summary: Manager for Autonomi nodes
5
5
  Author-email: Troy Johnson <troy@weave.sh>
6
6
  License: GPL-3.0
@@ -1,4 +1,4 @@
1
- wnm/__init__.py,sha256=7CHBeyet1CmnGNx7WojFYCb23tOP3zcbUbugUF8Bc5I,92
1
+ wnm/__init__.py,sha256=xIH1hzb_uGn2clXWTf2hRrSQ7PABiQyyx8uz8GMBHOc,92
2
2
  wnm/__main__.py,sha256=fMlM5gQIFGeFp19oowxmSKwtAE7-xS5-_ThTPaO7J-0,7989
3
3
  wnm/actions.py,sha256=h3ieTbQQIggKHqCLVWe5eph5YGif39tZRW3ch82tqys,1314
4
4
  wnm/common.py,sha256=AAuKvU3HtpsyKIHL105b9ZI_tIO_m8dp8bvKn3PDyF8,992
@@ -7,7 +7,7 @@ wnm/decision_engine.py,sha256=l_vqrw7imj9EsueLKWkz94P0TtA7UCyNPxBCfgkmNUg,13817
7
7
  wnm/executor.py,sha256=XeF6C5fwxkQEPR7D7xdJANRmE-b_TGhZBgdsUR_fZvg,52272
8
8
  wnm/migration.py,sha256=QcfvN9S0FIrEDq68IrgM1JBO288ub1tiSXdsrg53eSQ,1389
9
9
  wnm/models.py,sha256=qCCNu-Beg5TSQtIi7_1qnfuv5YmzzMq3We9PGektR1E,16684
10
- wnm/reports.py,sha256=s8rXAy-lNi75C-NMIE7wI_Ux01SuqFyXyVSvYcl6AIc,9133
10
+ wnm/reports.py,sha256=m_EW_-_CjlKPE6uXMWC_02RDPMlL0LirdctmgnGfpEM,9133
11
11
  wnm/utils.py,sha256=Pzry_8y1nrHkdLshsO56wY_rn4ToPUS8Xaf3n9_HRnY,15642
12
12
  wnm/firewall/__init__.py,sha256=2Ur3cSiP2hKVJp9guuuyDDgE2vklyU46KnstDfRq12Q,471
13
13
  wnm/firewall/base.py,sha256=cQQ8v7pYgVIjDcXj2jsU4_iZDXRyw7xtlfoJhcy8Gps,1882
@@ -20,9 +20,9 @@ wnm/process_managers/docker_manager.py,sha256=U-_VMkWK7WzxpWGtMStIRsXBKez8QOcCLZ
20
20
  wnm/process_managers/factory.py,sha256=TVkcN-o3kRtf1VylsTXMvpcTM2rwfcZT6QY3JsG5HkQ,2437
21
21
  wnm/process_managers/launchd_manager.py,sha256=c0QV_0eowCaNga81mNINvJM3We8BJm2_UvPX0v6-4v4,19860
22
22
  wnm/process_managers/setsid_manager.py,sha256=DW3BylkmmdUa4_mk68wpdwy5kqG-IJ_s13vG5eKE_Rs,10650
23
- wnm/process_managers/systemd_manager.py,sha256=4wcWTj_foHdB0Mp88_u1LH7SSenXeqt66CwegHu2VPk,14641
24
- wnm-0.0.10.dist-info/METADATA,sha256=lka-IqCY4A0YosJCufnPMR13EDyl65PnMFlkCp0GGfM,8835
25
- wnm-0.0.10.dist-info/WHEEL,sha256=_zCd3N1l69ArxyTb8rzEoP9TpbYXkqRFSNOD5OuxnTs,91
26
- wnm-0.0.10.dist-info/entry_points.txt,sha256=jfoemjoLVPeeiBMHKqAExrHQ4Rhf9IXxL4JCnS7ZYFo,42
27
- wnm-0.0.10.dist-info/top_level.txt,sha256=E6dTE5k6efMEB9LaJAZSBu8zzs__l4R55t0-F-LwufI,4
28
- wnm-0.0.10.dist-info/RECORD,,
23
+ wnm/process_managers/systemd_manager.py,sha256=dzEKngo2Bz5562h8PZRA1-m51JMcqU5Qc-f9CullNzk,18718
24
+ wnm-0.0.11.dist-info/METADATA,sha256=ocWt1WLti8zOZsP33_mkpSz219uMFDhYdlUXJ_1qbx0,8835
25
+ wnm-0.0.11.dist-info/WHEEL,sha256=_zCd3N1l69ArxyTb8rzEoP9TpbYXkqRFSNOD5OuxnTs,91
26
+ wnm-0.0.11.dist-info/entry_points.txt,sha256=jfoemjoLVPeeiBMHKqAExrHQ4Rhf9IXxL4JCnS7ZYFo,42
27
+ wnm-0.0.11.dist-info/top_level.txt,sha256=E6dTE5k6efMEB9LaJAZSBu8zzs__l4R55t0-F-LwufI,4
28
+ wnm-0.0.11.dist-info/RECORD,,
File without changes