manta-node 0.5b0__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.
- manta_node/__init__.py +5 -0
- manta_node/__main__.py +57 -0
- manta_node/cli/__init__.py +6 -0
- manta_node/cli/commands/__init__.py +21 -0
- manta_node/cli/commands/cluster.py +419 -0
- manta_node/cli/commands/config.py +490 -0
- manta_node/cli/commands/logs.py +168 -0
- manta_node/cli/commands/start.py +459 -0
- manta_node/cli/commands/status.py +204 -0
- manta_node/cli/commands/stop.py +253 -0
- manta_node/cli/config_manager.py +139 -0
- manta_node/cli/main.py +133 -0
- manta_node/cli/version.py +106 -0
- manta_node/domain/__init__.py +8 -0
- manta_node/domain/task_lifecycle.py +90 -0
- manta_node/infrastructure/__init__.py +13 -0
- manta_node/infrastructure/config/__init__.py +31 -0
- manta_node/infrastructure/config/node_config_manager.py +918 -0
- manta_node/infrastructure/container/__init__.py +11 -0
- manta_node/infrastructure/container/docker_adapter.py +253 -0
- manta_node/infrastructure/container/image_executor.py +222 -0
- manta_node/infrastructure/container/manager.py +549 -0
- manta_node/infrastructure/filesystem/__init__.py +7 -0
- manta_node/infrastructure/filesystem/dataset_manager.py +469 -0
- manta_node/infrastructure/grpc/__init__.py +11 -0
- manta_node/infrastructure/grpc/client.py +373 -0
- manta_node/infrastructure/grpc/local_servicer.py +151 -0
- manta_node/infrastructure/grpc/world_servicer.py +284 -0
- manta_node/infrastructure/metrics/__init__.py +10 -0
- manta_node/infrastructure/metrics/collector.py +94 -0
- manta_node/infrastructure/metrics/metrics_collector.py +351 -0
- manta_node/infrastructure/mqtt/__init__.py +7 -0
- manta_node/infrastructure/mqtt/command_handler.py +450 -0
- manta_node/node_orchestrator.py +462 -0
- manta_node/task_manager.py +677 -0
- manta_node/tasks.py +273 -0
- manta_node/utils.py +52 -0
- manta_node-0.5b0.dist-info/METADATA +794 -0
- manta_node-0.5b0.dist-info/RECORD +43 -0
- manta_node-0.5b0.dist-info/WHEEL +5 -0
- manta_node-0.5b0.dist-info/entry_points.txt +2 -0
- manta_node-0.5b0.dist-info/licenses/LICENSE +683 -0
- manta_node-0.5b0.dist-info/top_level.txt +1 -0
|
@@ -0,0 +1,459 @@
|
|
|
1
|
+
"""Node start command implementation."""
|
|
2
|
+
|
|
3
|
+
import argparse
|
|
4
|
+
import asyncio
|
|
5
|
+
import json
|
|
6
|
+
import os
|
|
7
|
+
import signal
|
|
8
|
+
import subprocess
|
|
9
|
+
import sys
|
|
10
|
+
import time
|
|
11
|
+
import uuid
|
|
12
|
+
from datetime import datetime
|
|
13
|
+
from pathlib import Path
|
|
14
|
+
from typing import List
|
|
15
|
+
|
|
16
|
+
from rich.console import Console
|
|
17
|
+
|
|
18
|
+
from manta_common.logging_config import configure_logging
|
|
19
|
+
|
|
20
|
+
from ...node_orchestrator import Node
|
|
21
|
+
|
|
22
|
+
from ...infrastructure.config import NodeConfigManager
|
|
23
|
+
|
|
24
|
+
console = Console()
|
|
25
|
+
|
|
26
|
+
|
|
27
|
+
def print_error(message: str):
|
|
28
|
+
"""Print error message in red."""
|
|
29
|
+
console.print(f"[red]Error: {message}[/red]")
|
|
30
|
+
|
|
31
|
+
|
|
32
|
+
def print_success(message: str):
|
|
33
|
+
"""Print success message in green."""
|
|
34
|
+
console.print(f"[green]{message}[/green]")
|
|
35
|
+
|
|
36
|
+
|
|
37
|
+
def print_warning(message: str):
|
|
38
|
+
"""Print warning message in yellow."""
|
|
39
|
+
console.print(f"[yellow]{message}[/yellow]")
|
|
40
|
+
|
|
41
|
+
|
|
42
|
+
def start_node_foreground(
|
|
43
|
+
config_manager: NodeConfigManager,
|
|
44
|
+
instance_id: str,
|
|
45
|
+
instance_alias: str,
|
|
46
|
+
skip_instance_file: bool = False,
|
|
47
|
+
) -> int:
|
|
48
|
+
"""Start a node in foreground mode."""
|
|
49
|
+
console.print(f"[cyan]Starting node '{instance_alias}' in foreground...[/cyan]")
|
|
50
|
+
console.print("[dim]Press Ctrl+C to stop[/dim]")
|
|
51
|
+
|
|
52
|
+
# Create instance file to track running node (unless we're a subprocess of background mode)
|
|
53
|
+
instance_file = None
|
|
54
|
+
if not skip_instance_file:
|
|
55
|
+
instance_dir = Path.home() / ".manta" / "nodes" / "instances"
|
|
56
|
+
instance_dir.mkdir(parents=True, exist_ok=True)
|
|
57
|
+
instance_file = instance_dir / f"{instance_id}.json"
|
|
58
|
+
|
|
59
|
+
console.print(f"Instance ID: {instance_id}")
|
|
60
|
+
console.print(f"Log file: {config_manager.config.logging.filename}")
|
|
61
|
+
|
|
62
|
+
instance_data = {
|
|
63
|
+
"instance_id": instance_id,
|
|
64
|
+
"alias": instance_alias,
|
|
65
|
+
"config_name": config_manager.config.metadata.name,
|
|
66
|
+
"pid": os.getpid(),
|
|
67
|
+
"start_time": datetime.now().isoformat(),
|
|
68
|
+
"status": "running",
|
|
69
|
+
"manager_host": config_manager.config.network.manager_host,
|
|
70
|
+
"manager_port": config_manager.config.network.manager_port,
|
|
71
|
+
"log_file": config_manager.config.logging.filename,
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
with open(instance_file, "w") as f:
|
|
75
|
+
json.dump(instance_data, f, indent=2)
|
|
76
|
+
|
|
77
|
+
loop = None # Initialize loop variable for cleanup
|
|
78
|
+
node = None # Initialize node variable for cleanup
|
|
79
|
+
shutdown_requested = False # Track if shutdown was requested
|
|
80
|
+
|
|
81
|
+
def signal_handler(signum, frame):
|
|
82
|
+
"""Handle termination signals (SIGTERM, SIGINT)."""
|
|
83
|
+
nonlocal shutdown_requested, loop
|
|
84
|
+
if not shutdown_requested:
|
|
85
|
+
shutdown_requested = True
|
|
86
|
+
sig_name = signal.Signals(signum).name
|
|
87
|
+
console.print(f"\n[yellow]Received {sig_name}, stopping node...[/yellow]")
|
|
88
|
+
# Schedule shutdown on the event loop (thread-safe)
|
|
89
|
+
if loop and loop.is_running():
|
|
90
|
+
loop.call_soon_threadsafe(loop.stop)
|
|
91
|
+
|
|
92
|
+
# Set up signal handlers for graceful shutdown
|
|
93
|
+
signal.signal(signal.SIGTERM, signal_handler)
|
|
94
|
+
signal.signal(signal.SIGINT, signal_handler)
|
|
95
|
+
|
|
96
|
+
try:
|
|
97
|
+
# Configure logging based on node configuration
|
|
98
|
+
log_config = config_manager.config.logging
|
|
99
|
+
|
|
100
|
+
# Ensure log directory exists
|
|
101
|
+
log_path = Path(log_config.filename)
|
|
102
|
+
log_path.parent.mkdir(parents=True, exist_ok=True)
|
|
103
|
+
|
|
104
|
+
# Create config dictionary based on MANTA_LOGGING_CONFIG template
|
|
105
|
+
config_dict = {
|
|
106
|
+
"version": 1,
|
|
107
|
+
"disable_existing_loggers": False,
|
|
108
|
+
"formatters": {
|
|
109
|
+
"standard": {
|
|
110
|
+
"format": "%(asctime)s - %(levelname)s - %(name)s - %(message)s"
|
|
111
|
+
}
|
|
112
|
+
},
|
|
113
|
+
"filters": {
|
|
114
|
+
"manta_filter": {
|
|
115
|
+
"()": "manta_common.logging_config.filter_manta",
|
|
116
|
+
"level": "INFO",
|
|
117
|
+
},
|
|
118
|
+
"warnings_and_below": {
|
|
119
|
+
"()": "manta_common.logging_config.filter_maker",
|
|
120
|
+
"level": "WARNING",
|
|
121
|
+
},
|
|
122
|
+
},
|
|
123
|
+
"handlers": {
|
|
124
|
+
"file": {
|
|
125
|
+
"class": "logging.FileHandler",
|
|
126
|
+
"formatter": "standard",
|
|
127
|
+
"filename": log_config.filename,
|
|
128
|
+
"filters": ["manta_filter"],
|
|
129
|
+
"mode": "w",
|
|
130
|
+
},
|
|
131
|
+
"rich": {
|
|
132
|
+
"class": "rich.logging.RichHandler",
|
|
133
|
+
"formatter": "standard",
|
|
134
|
+
"markup": True,
|
|
135
|
+
"filters": ["manta_filter"],
|
|
136
|
+
"console": Console(
|
|
137
|
+
_environ={"COLUMNS": "284"},
|
|
138
|
+
),
|
|
139
|
+
},
|
|
140
|
+
},
|
|
141
|
+
"root": {"level": log_config.level, "handlers": []},
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
# Set up handlers based on configuration
|
|
145
|
+
if log_config.log_to_file:
|
|
146
|
+
config_dict["root"]["handlers"].append("file")
|
|
147
|
+
if log_config.log_to_console:
|
|
148
|
+
config_dict["root"]["handlers"].append("rich")
|
|
149
|
+
|
|
150
|
+
# Call configure_logging with the config dictionary
|
|
151
|
+
configure_logging(config=config_dict, debug=log_config.debug_mode)
|
|
152
|
+
|
|
153
|
+
# Display connection information before starting
|
|
154
|
+
security_mode = (
|
|
155
|
+
"TLS"
|
|
156
|
+
if (
|
|
157
|
+
config_manager.config.security.use_tls
|
|
158
|
+
or config_manager.config.network.manager_port == 443
|
|
159
|
+
)
|
|
160
|
+
else "insecure"
|
|
161
|
+
)
|
|
162
|
+
endpoint = (
|
|
163
|
+
f"{security_mode}://"
|
|
164
|
+
f"{config_manager.config.network.manager_host}:"
|
|
165
|
+
f"{config_manager.config.network.manager_port}"
|
|
166
|
+
)
|
|
167
|
+
|
|
168
|
+
console.print(f"[cyan]Manager endpoint:[/cyan] {endpoint}")
|
|
169
|
+
|
|
170
|
+
# Show JWT token status (without exposing the token)
|
|
171
|
+
token = config_manager.config.identity.secured_token
|
|
172
|
+
if token:
|
|
173
|
+
token_parts = token.count(".") + 1
|
|
174
|
+
token_status = (
|
|
175
|
+
"valid format"
|
|
176
|
+
if token_parts == 3
|
|
177
|
+
else f"invalid format ({token_parts} parts, expected 3)"
|
|
178
|
+
)
|
|
179
|
+
console.print(
|
|
180
|
+
f"[cyan]JWT token:[/cyan] present ({len(token)} chars, {token_status})"
|
|
181
|
+
)
|
|
182
|
+
else:
|
|
183
|
+
console.print("[red]JWT token:[/red] missing")
|
|
184
|
+
|
|
185
|
+
console.print("") # Empty line for readability
|
|
186
|
+
|
|
187
|
+
# Create Node instance with configuration manager
|
|
188
|
+
node = Node(config_manager)
|
|
189
|
+
|
|
190
|
+
# Start node in current thread (blocking)
|
|
191
|
+
loop = asyncio.new_event_loop()
|
|
192
|
+
asyncio.set_event_loop(loop)
|
|
193
|
+
try:
|
|
194
|
+
loop.run_until_complete(node.start())
|
|
195
|
+
except KeyboardInterrupt:
|
|
196
|
+
# Direct Ctrl+C in terminal (not from signal handler)
|
|
197
|
+
shutdown_requested = True
|
|
198
|
+
|
|
199
|
+
# Check if shutdown was requested (either via signal or Ctrl+C)
|
|
200
|
+
if shutdown_requested:
|
|
201
|
+
try:
|
|
202
|
+
if node:
|
|
203
|
+
# Ensure cleanup is called when stopping the node
|
|
204
|
+
loop.run_until_complete(node.stop())
|
|
205
|
+
except Exception as e:
|
|
206
|
+
console.print(f"[red]Error during node cleanup: {e}[/red]")
|
|
207
|
+
|
|
208
|
+
# Clean up instance file
|
|
209
|
+
if instance_file and instance_file.exists():
|
|
210
|
+
instance_file.unlink()
|
|
211
|
+
|
|
212
|
+
console.print("[green]Node stopped gracefully[/green]")
|
|
213
|
+
return 0
|
|
214
|
+
|
|
215
|
+
print_success("Node stopped successfully")
|
|
216
|
+
return 0
|
|
217
|
+
|
|
218
|
+
except Exception as e:
|
|
219
|
+
print_error(f"Node failed with error: {e}")
|
|
220
|
+
# Clean up instance file on error
|
|
221
|
+
if instance_file and instance_file.exists():
|
|
222
|
+
instance_file.unlink()
|
|
223
|
+
return 1
|
|
224
|
+
|
|
225
|
+
|
|
226
|
+
def start_node_background(
|
|
227
|
+
config_manager: NodeConfigManager,
|
|
228
|
+
instance_id: str,
|
|
229
|
+
instance_alias: str,
|
|
230
|
+
log_file: Path,
|
|
231
|
+
) -> int:
|
|
232
|
+
"""Start a node in background mode."""
|
|
233
|
+
console.print(f"[cyan]Starting node '{instance_alias}' in background...[/cyan]")
|
|
234
|
+
|
|
235
|
+
try:
|
|
236
|
+
# Prepare command to run node in background
|
|
237
|
+
# We call the CLI with the start command in foreground mode
|
|
238
|
+
cmd = [
|
|
239
|
+
sys.executable,
|
|
240
|
+
"-m",
|
|
241
|
+
"manta_node",
|
|
242
|
+
"start",
|
|
243
|
+
str(config_manager.config_path.stem), # Use config name without extension
|
|
244
|
+
"--foreground", # This will run in foreground for the subprocess
|
|
245
|
+
"--subprocess", # Tell the subprocess not to create instance file
|
|
246
|
+
]
|
|
247
|
+
|
|
248
|
+
# If alias was overridden, add it to the command
|
|
249
|
+
if instance_alias != config_manager.config.identity.alias:
|
|
250
|
+
cmd.extend(["--alias", instance_alias])
|
|
251
|
+
|
|
252
|
+
# Ensure log directory exists
|
|
253
|
+
log_file.parent.mkdir(parents=True, exist_ok=True)
|
|
254
|
+
|
|
255
|
+
# Start subprocess
|
|
256
|
+
with open(log_file, "w") as log:
|
|
257
|
+
proc = subprocess.Popen(
|
|
258
|
+
cmd,
|
|
259
|
+
stdout=log,
|
|
260
|
+
stderr=log,
|
|
261
|
+
start_new_session=True, # Detach from parent
|
|
262
|
+
)
|
|
263
|
+
|
|
264
|
+
# Wait a moment and check if process is running
|
|
265
|
+
time.sleep(2)
|
|
266
|
+
|
|
267
|
+
if proc.poll() is None: # Process is still running
|
|
268
|
+
print_success(f"Node '{instance_alias}' started successfully")
|
|
269
|
+
console.print(f"Instance ID: {instance_id}")
|
|
270
|
+
console.print(f"Process ID: {proc.pid}")
|
|
271
|
+
console.print(f"Log file: {log_file}")
|
|
272
|
+
|
|
273
|
+
# Create instance file for tracking (same format as foreground)
|
|
274
|
+
instance_dir = Path.home() / ".manta" / "nodes" / "instances"
|
|
275
|
+
instance_dir.mkdir(parents=True, exist_ok=True)
|
|
276
|
+
instance_file = instance_dir / f"{instance_id}.json"
|
|
277
|
+
|
|
278
|
+
instance_data = {
|
|
279
|
+
"instance_id": instance_id,
|
|
280
|
+
"alias": instance_alias,
|
|
281
|
+
"config_name": config_manager.config.metadata.name,
|
|
282
|
+
"pid": proc.pid,
|
|
283
|
+
"start_time": datetime.now().isoformat(),
|
|
284
|
+
"status": "running",
|
|
285
|
+
"manager_host": config_manager.config.network.manager_host,
|
|
286
|
+
"manager_port": config_manager.config.network.manager_port,
|
|
287
|
+
"log_file": str(log_file),
|
|
288
|
+
}
|
|
289
|
+
|
|
290
|
+
with open(instance_file, "w") as f:
|
|
291
|
+
json.dump(instance_data, f, indent=2)
|
|
292
|
+
|
|
293
|
+
return 0
|
|
294
|
+
else:
|
|
295
|
+
print_error("Node failed to start. Check logs for details.")
|
|
296
|
+
if log_file.exists():
|
|
297
|
+
console.print(f"Log file: {log_file}")
|
|
298
|
+
return 1
|
|
299
|
+
|
|
300
|
+
except Exception as e:
|
|
301
|
+
print_error(f"Failed to start background node: {e}")
|
|
302
|
+
return 1
|
|
303
|
+
|
|
304
|
+
|
|
305
|
+
def start_command(args: List[str]) -> int:
|
|
306
|
+
"""Handle node start command.
|
|
307
|
+
|
|
308
|
+
Args:
|
|
309
|
+
args: Command line arguments
|
|
310
|
+
|
|
311
|
+
Returns:
|
|
312
|
+
Exit code (0 for success, non-zero for error)
|
|
313
|
+
"""
|
|
314
|
+
parser = argparse.ArgumentParser(
|
|
315
|
+
prog="manta_node start", description="Start a node instance"
|
|
316
|
+
)
|
|
317
|
+
parser.add_argument(
|
|
318
|
+
"config",
|
|
319
|
+
nargs="?",
|
|
320
|
+
default="default",
|
|
321
|
+
help="Configuration name to use (default: default)",
|
|
322
|
+
)
|
|
323
|
+
parser.add_argument("--alias", help="Override node alias")
|
|
324
|
+
parser.add_argument(
|
|
325
|
+
"--background", "-d", action="store_true", help="Run node in background"
|
|
326
|
+
)
|
|
327
|
+
parser.add_argument(
|
|
328
|
+
"--foreground", action="store_true", help="Run node in foreground (default)"
|
|
329
|
+
)
|
|
330
|
+
parser.add_argument(
|
|
331
|
+
"--subprocess",
|
|
332
|
+
action="store_true",
|
|
333
|
+
help=argparse.SUPPRESS, # Hidden flag for internal use
|
|
334
|
+
)
|
|
335
|
+
|
|
336
|
+
try:
|
|
337
|
+
parsed_args = parser.parse_args(args)
|
|
338
|
+
except SystemExit:
|
|
339
|
+
return 1
|
|
340
|
+
|
|
341
|
+
config_name = parsed_args.config
|
|
342
|
+
alias = parsed_args.alias
|
|
343
|
+
background = parsed_args.background and not parsed_args.foreground
|
|
344
|
+
|
|
345
|
+
# Build configuration path
|
|
346
|
+
config_path = Path.home() / ".manta" / "nodes" / f"{config_name}.toml"
|
|
347
|
+
|
|
348
|
+
if not config_path.exists():
|
|
349
|
+
print_error(f"Configuration '{config_name}' not found at {config_path}")
|
|
350
|
+
console.print("Available configurations:")
|
|
351
|
+
config_mgr = NodeConfigManager()
|
|
352
|
+
for cfg in config_mgr.list_available_configs():
|
|
353
|
+
console.print(f" - {cfg}")
|
|
354
|
+
console.print("")
|
|
355
|
+
console.print("Create a new configuration with:")
|
|
356
|
+
console.print(" manta_node config init <path/to/config.yaml>")
|
|
357
|
+
return 1
|
|
358
|
+
|
|
359
|
+
try:
|
|
360
|
+
# Create NodeConfigManager with the config path
|
|
361
|
+
config_manager = NodeConfigManager(config_path)
|
|
362
|
+
|
|
363
|
+
# Override alias if provided
|
|
364
|
+
if alias:
|
|
365
|
+
config_manager.config.identity.alias = alias
|
|
366
|
+
config_manager.save()
|
|
367
|
+
elif (
|
|
368
|
+
config_manager.config.identity.alias is None
|
|
369
|
+
and config_manager.config.metadata.name is not None
|
|
370
|
+
):
|
|
371
|
+
config_manager.config.identity.alias = config_manager.config.metadata.name
|
|
372
|
+
config_manager.save()
|
|
373
|
+
elif (
|
|
374
|
+
config_manager.config.identity.alias is None
|
|
375
|
+
and config_manager.config.metadata.name is None
|
|
376
|
+
):
|
|
377
|
+
config_manager.config.identity.alias = config_name
|
|
378
|
+
config_manager.save()
|
|
379
|
+
|
|
380
|
+
# Validate configuration
|
|
381
|
+
issues = config_manager.validate()
|
|
382
|
+
if issues:
|
|
383
|
+
print_error("Configuration validation failed:")
|
|
384
|
+
for issue in issues:
|
|
385
|
+
console.print(f" - {issue}")
|
|
386
|
+
return 1
|
|
387
|
+
|
|
388
|
+
# Generate instance ID
|
|
389
|
+
instance_id = f"{config_name}-{uuid.uuid4().hex[:8]}"
|
|
390
|
+
instance_alias = alias or config_manager.config.identity.alias or config_name
|
|
391
|
+
|
|
392
|
+
# Check for token in configuration
|
|
393
|
+
if (
|
|
394
|
+
not config_manager.config.identity.secured_token
|
|
395
|
+
or config_manager.config.identity.secured_token.startswith("<")
|
|
396
|
+
):
|
|
397
|
+
print_error("No valid secured token found in configuration")
|
|
398
|
+
console.print("A secured token (JWT) is required for node authentication.")
|
|
399
|
+
console.print("")
|
|
400
|
+
console.print("Solutions:")
|
|
401
|
+
console.print(
|
|
402
|
+
" • Set environment variable: export MANTA_NODE_TOKEN=your_jwt_token"
|
|
403
|
+
)
|
|
404
|
+
console.print(f" • Edit config file: {config_path}")
|
|
405
|
+
console.print(' • Update: secured_token = "your_jwt_token_here"')
|
|
406
|
+
console.print(
|
|
407
|
+
" • Or recreate config: manta_node config init <path/to/config.yaml>"
|
|
408
|
+
)
|
|
409
|
+
return 1
|
|
410
|
+
|
|
411
|
+
# Prepare log file - use configured filename
|
|
412
|
+
# Defensively handle tilde expansion
|
|
413
|
+
log_filename = config_manager.config.logging.filename
|
|
414
|
+
|
|
415
|
+
# Expand tilde if present at the start of the path
|
|
416
|
+
if log_filename.startswith("~"):
|
|
417
|
+
log_path = Path(log_filename).expanduser()
|
|
418
|
+
else:
|
|
419
|
+
log_path = Path(log_filename)
|
|
420
|
+
|
|
421
|
+
# Check if path is now absolute or still relative
|
|
422
|
+
if log_path.is_absolute():
|
|
423
|
+
log_file = log_path
|
|
424
|
+
else:
|
|
425
|
+
# If relative path, place it in the standard log directory
|
|
426
|
+
log_dir = Path.home() / ".manta" / "logs" / "nodes"
|
|
427
|
+
log_dir.mkdir(parents=True, exist_ok=True)
|
|
428
|
+
log_file = log_dir / log_path
|
|
429
|
+
|
|
430
|
+
if background:
|
|
431
|
+
return start_node_background(
|
|
432
|
+
config_manager, instance_id, instance_alias, log_file
|
|
433
|
+
)
|
|
434
|
+
else:
|
|
435
|
+
# Check if we're being called as a subprocess from background mode
|
|
436
|
+
skip_instance_file = getattr(parsed_args, "subprocess", False)
|
|
437
|
+
return start_node_foreground(
|
|
438
|
+
config_manager, instance_id, instance_alias, skip_instance_file
|
|
439
|
+
)
|
|
440
|
+
|
|
441
|
+
except Exception as e:
|
|
442
|
+
print_error(f"Failed to start node: {e}")
|
|
443
|
+
console.print("")
|
|
444
|
+
console.print("Troubleshooting steps:")
|
|
445
|
+
console.print(
|
|
446
|
+
" 1. Check if configuration is valid: manta_node config validate"
|
|
447
|
+
)
|
|
448
|
+
console.print(
|
|
449
|
+
" 2. Verify manager is running: docker ps (look for manta services)"
|
|
450
|
+
)
|
|
451
|
+
console.print(" 3. Check if port is available: netstat -ln | grep :50051")
|
|
452
|
+
console.print(" 4. Validate secured_token is set in config file")
|
|
453
|
+
return 1
|
|
454
|
+
|
|
455
|
+
|
|
456
|
+
if __name__ == "__main__":
|
|
457
|
+
import sys
|
|
458
|
+
|
|
459
|
+
sys.exit(start_command(sys.argv[1:]))
|
|
@@ -0,0 +1,204 @@
|
|
|
1
|
+
"""Node status command implementation."""
|
|
2
|
+
|
|
3
|
+
import argparse
|
|
4
|
+
import json
|
|
5
|
+
import os
|
|
6
|
+
from datetime import datetime
|
|
7
|
+
from pathlib import Path
|
|
8
|
+
from typing import List
|
|
9
|
+
|
|
10
|
+
import psutil
|
|
11
|
+
from rich.console import Console
|
|
12
|
+
from rich.table import Table
|
|
13
|
+
|
|
14
|
+
console = Console()
|
|
15
|
+
|
|
16
|
+
|
|
17
|
+
def find_running_instances():
|
|
18
|
+
"""Find running node instances by looking for instance JSON files."""
|
|
19
|
+
|
|
20
|
+
instances_dir = Path.home() / ".manta" / "nodes" / "instances"
|
|
21
|
+
instances = []
|
|
22
|
+
|
|
23
|
+
if not instances_dir.exists():
|
|
24
|
+
return instances
|
|
25
|
+
|
|
26
|
+
for instance_file in instances_dir.glob("*.json"):
|
|
27
|
+
try:
|
|
28
|
+
with open(instance_file, "r") as f:
|
|
29
|
+
instance_data = json.load(f)
|
|
30
|
+
|
|
31
|
+
pid = instance_data.get("pid")
|
|
32
|
+
instance_id = instance_data.get("instance_id", instance_file.stem)
|
|
33
|
+
|
|
34
|
+
# Check if process is still running
|
|
35
|
+
try:
|
|
36
|
+
# Send signal 0 to check if process exists
|
|
37
|
+
|
|
38
|
+
os.kill(pid, 0)
|
|
39
|
+
|
|
40
|
+
# Get process info if psutil is available
|
|
41
|
+
try:
|
|
42
|
+
process = psutil.Process(pid)
|
|
43
|
+
cpu_percent = process.cpu_percent()
|
|
44
|
+
memory_mb = process.memory_info().rss // (1024 * 1024)
|
|
45
|
+
create_time = datetime.fromtimestamp(process.create_time())
|
|
46
|
+
status = "running"
|
|
47
|
+
except ImportError:
|
|
48
|
+
cpu_percent = None
|
|
49
|
+
memory_mb = None
|
|
50
|
+
create_time = None
|
|
51
|
+
status = "running"
|
|
52
|
+
except psutil.NoSuchProcess:
|
|
53
|
+
# Process disappeared
|
|
54
|
+
instance_file.unlink(missing_ok=True)
|
|
55
|
+
continue
|
|
56
|
+
|
|
57
|
+
# Add additional info from JSON
|
|
58
|
+
alias = instance_data.get("alias", instance_id)
|
|
59
|
+
config_name = instance_data.get("config_name", "unknown")
|
|
60
|
+
start_time_str = instance_data.get("start_time")
|
|
61
|
+
if start_time_str and not create_time:
|
|
62
|
+
try:
|
|
63
|
+
create_time = datetime.fromisoformat(start_time_str)
|
|
64
|
+
except Exception:
|
|
65
|
+
pass
|
|
66
|
+
|
|
67
|
+
instances.append(
|
|
68
|
+
{
|
|
69
|
+
"instance_id": instance_id,
|
|
70
|
+
"alias": alias,
|
|
71
|
+
"config_name": config_name,
|
|
72
|
+
"pid": pid,
|
|
73
|
+
"status": status,
|
|
74
|
+
"cpu_percent": cpu_percent,
|
|
75
|
+
"memory_mb": memory_mb,
|
|
76
|
+
"start_time": create_time,
|
|
77
|
+
"instance_file": instance_file,
|
|
78
|
+
}
|
|
79
|
+
)
|
|
80
|
+
|
|
81
|
+
except OSError:
|
|
82
|
+
# Process no longer exists, remove stale instance file
|
|
83
|
+
instance_file.unlink(missing_ok=True)
|
|
84
|
+
|
|
85
|
+
except (ValueError, FileNotFoundError):
|
|
86
|
+
# Invalid instance file, remove it
|
|
87
|
+
instance_file.unlink(missing_ok=True)
|
|
88
|
+
|
|
89
|
+
return instances
|
|
90
|
+
|
|
91
|
+
|
|
92
|
+
def show_status_table(instances: List[dict]) -> None:
|
|
93
|
+
"""Display instances in a formatted table."""
|
|
94
|
+
if not instances:
|
|
95
|
+
console.print("[yellow]No running node instances found.[/yellow]")
|
|
96
|
+
console.print("")
|
|
97
|
+
console.print("Start a node with:")
|
|
98
|
+
console.print(" manta_node start [config_name]")
|
|
99
|
+
return
|
|
100
|
+
|
|
101
|
+
table = Table(title="Running Node Instances")
|
|
102
|
+
table.add_column("Alias", style="cyan")
|
|
103
|
+
table.add_column("Config", style="magenta")
|
|
104
|
+
table.add_column("PID", style="green")
|
|
105
|
+
table.add_column("Status", style="yellow")
|
|
106
|
+
table.add_column("CPU %", style="blue")
|
|
107
|
+
table.add_column("Memory (MB)", style="blue")
|
|
108
|
+
table.add_column("Started", style="dim")
|
|
109
|
+
|
|
110
|
+
for instance in instances:
|
|
111
|
+
cpu_str = (
|
|
112
|
+
f"{instance['cpu_percent']:.1f}%"
|
|
113
|
+
if instance["cpu_percent"] is not None
|
|
114
|
+
else "N/A"
|
|
115
|
+
)
|
|
116
|
+
memory_str = (
|
|
117
|
+
str(instance["memory_mb"]) if instance["memory_mb"] is not None else "N/A"
|
|
118
|
+
)
|
|
119
|
+
start_str = (
|
|
120
|
+
instance["start_time"].strftime("%Y-%m-%d %H:%M:%S")
|
|
121
|
+
if instance["start_time"]
|
|
122
|
+
else "N/A"
|
|
123
|
+
)
|
|
124
|
+
|
|
125
|
+
table.add_row(
|
|
126
|
+
instance["alias"],
|
|
127
|
+
instance["config_name"],
|
|
128
|
+
str(instance["pid"]),
|
|
129
|
+
instance["status"],
|
|
130
|
+
cpu_str,
|
|
131
|
+
memory_str,
|
|
132
|
+
start_str,
|
|
133
|
+
)
|
|
134
|
+
|
|
135
|
+
console.print(table)
|
|
136
|
+
|
|
137
|
+
|
|
138
|
+
def show_status_plain(instances: List[dict]) -> None:
|
|
139
|
+
"""Display instances in plain text format."""
|
|
140
|
+
if not instances:
|
|
141
|
+
console.print("No running node instances found.")
|
|
142
|
+
console.print("")
|
|
143
|
+
console.print("Start a node with:")
|
|
144
|
+
console.print(" manta_node start [config_name]")
|
|
145
|
+
return
|
|
146
|
+
|
|
147
|
+
console.print(f"Found {len(instances)} running instance(s):")
|
|
148
|
+
console.print("")
|
|
149
|
+
|
|
150
|
+
for instance in instances:
|
|
151
|
+
console.print(f"Instance: [cyan]{instance['instance_id']}[/cyan]")
|
|
152
|
+
console.print(f" PID: {instance['pid']}")
|
|
153
|
+
console.print(f" Status: {instance['status']}")
|
|
154
|
+
|
|
155
|
+
if instance["cpu_percent"] is not None:
|
|
156
|
+
console.print(f" CPU: {instance['cpu_percent']:.1f}%")
|
|
157
|
+
if instance["memory_mb"] is not None:
|
|
158
|
+
console.print(f" Memory: {instance['memory_mb']} MB")
|
|
159
|
+
if instance["start_time"]:
|
|
160
|
+
console.print(
|
|
161
|
+
f" Started: {instance['start_time'].strftime('%Y-%m-%d %H:%M:%S')}"
|
|
162
|
+
)
|
|
163
|
+
|
|
164
|
+
console.print("")
|
|
165
|
+
|
|
166
|
+
|
|
167
|
+
def status_command(args: List[str]) -> int:
|
|
168
|
+
"""Handle node status command.
|
|
169
|
+
|
|
170
|
+
Args:
|
|
171
|
+
args: Command line arguments
|
|
172
|
+
|
|
173
|
+
Returns:
|
|
174
|
+
Exit code (0 for success, non-zero for error)
|
|
175
|
+
"""
|
|
176
|
+
parser = argparse.ArgumentParser(
|
|
177
|
+
prog="manta_node status", description="Show status of running node instances"
|
|
178
|
+
)
|
|
179
|
+
parser.add_argument(
|
|
180
|
+
"--plain", action="store_true", help="Use plain text output instead of table"
|
|
181
|
+
)
|
|
182
|
+
parser.add_argument(
|
|
183
|
+
"--all", action="store_true", help="Show all instances (same as default)"
|
|
184
|
+
)
|
|
185
|
+
|
|
186
|
+
try:
|
|
187
|
+
parsed_args = parser.parse_args(args)
|
|
188
|
+
except SystemExit:
|
|
189
|
+
return 1
|
|
190
|
+
|
|
191
|
+
instances = find_running_instances()
|
|
192
|
+
|
|
193
|
+
if parsed_args.plain:
|
|
194
|
+
show_status_plain(instances)
|
|
195
|
+
else:
|
|
196
|
+
show_status_table(instances)
|
|
197
|
+
|
|
198
|
+
return 0
|
|
199
|
+
|
|
200
|
+
|
|
201
|
+
if __name__ == "__main__":
|
|
202
|
+
import sys
|
|
203
|
+
|
|
204
|
+
sys.exit(status_command(sys.argv[1:]))
|