manta-node 0.5b0.dev9__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 +83 -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 +9 -0
- manta_node/infrastructure/container/docker_adapter.py +253 -0
- manta_node/infrastructure/container/manager.py +536 -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 +315 -0
- manta_node/node_orchestrator.py +462 -0
- manta_node/task_manager.py +519 -0
- manta_node/tasks.py +272 -0
- manta_node/utils.py +52 -0
- manta_node-0.5b0.dev9.dist-info/METADATA +799 -0
- manta_node-0.5b0.dev9.dist-info/RECORD +42 -0
- manta_node-0.5b0.dev9.dist-info/WHEEL +5 -0
- manta_node-0.5b0.dev9.dist-info/entry_points.txt +2 -0
- manta_node-0.5b0.dev9.dist-info/licenses/LICENSE +683 -0
- manta_node-0.5b0.dev9.dist-info/top_level.txt +1 -0
|
@@ -0,0 +1,490 @@
|
|
|
1
|
+
"""Node configuration management command implementation."""
|
|
2
|
+
|
|
3
|
+
import argparse
|
|
4
|
+
import os
|
|
5
|
+
import subprocess
|
|
6
|
+
import sys
|
|
7
|
+
from pathlib import Path
|
|
8
|
+
from typing import List
|
|
9
|
+
|
|
10
|
+
from rich.console import Console
|
|
11
|
+
from rich.panel import Panel
|
|
12
|
+
from rich.prompt import Confirm
|
|
13
|
+
from rich.table import Table
|
|
14
|
+
|
|
15
|
+
from ...infrastructure.config import (
|
|
16
|
+
NodeConfigManager,
|
|
17
|
+
NodeConfiguration,
|
|
18
|
+
)
|
|
19
|
+
|
|
20
|
+
console = Console()
|
|
21
|
+
|
|
22
|
+
|
|
23
|
+
def print_error(message: str):
|
|
24
|
+
"""Print error message in red."""
|
|
25
|
+
console.print(f"[red]Error: {message}[/red]")
|
|
26
|
+
|
|
27
|
+
|
|
28
|
+
def print_success(message: str):
|
|
29
|
+
"""Print success message in green."""
|
|
30
|
+
console.print(f"[green]{message}[/green]")
|
|
31
|
+
|
|
32
|
+
|
|
33
|
+
def print_warning(message: str):
|
|
34
|
+
"""Print warning message in yellow."""
|
|
35
|
+
console.print(f"[yellow]{message}[/yellow]")
|
|
36
|
+
|
|
37
|
+
|
|
38
|
+
def init_config_from_file(config_file_path: str) -> int:
|
|
39
|
+
"""Create configuration from a YAML or TOML file.
|
|
40
|
+
|
|
41
|
+
Args:
|
|
42
|
+
config_file_path: Path to the configuration file (YAML or TOML)
|
|
43
|
+
|
|
44
|
+
Returns:
|
|
45
|
+
Exit code (0 for success, non-zero for error)
|
|
46
|
+
"""
|
|
47
|
+
import yaml
|
|
48
|
+
|
|
49
|
+
try:
|
|
50
|
+
import tomllib
|
|
51
|
+
except ImportError:
|
|
52
|
+
import tomli as tomllib
|
|
53
|
+
|
|
54
|
+
console.print(
|
|
55
|
+
Panel(
|
|
56
|
+
"[bold cyan]Node Configuration Setup[/bold cyan]\n\n"
|
|
57
|
+
"Loading configuration from file...\n"
|
|
58
|
+
"Configuration will be saved in ~/.manta/nodes/",
|
|
59
|
+
title="manta_node Configuration",
|
|
60
|
+
)
|
|
61
|
+
)
|
|
62
|
+
|
|
63
|
+
try:
|
|
64
|
+
# Validate input file exists
|
|
65
|
+
source_path = Path(config_file_path).expanduser().absolute()
|
|
66
|
+
if not source_path.exists():
|
|
67
|
+
print_error(f"Configuration file not found: {source_path}")
|
|
68
|
+
return 1
|
|
69
|
+
|
|
70
|
+
# Extract config name from filename (without extension)
|
|
71
|
+
config_name = source_path.stem
|
|
72
|
+
|
|
73
|
+
# Determine file format from extension
|
|
74
|
+
file_ext = source_path.suffix.lower()
|
|
75
|
+
if file_ext not in [".yaml", ".yml", ".toml"]:
|
|
76
|
+
print_error(f"Unsupported file format: {file_ext}")
|
|
77
|
+
console.print("Supported formats: .yaml, .yml, .toml")
|
|
78
|
+
return 1
|
|
79
|
+
|
|
80
|
+
console.print(f"Loading configuration from: [cyan]{source_path}[/cyan]")
|
|
81
|
+
console.print(f"Configuration name: [cyan]{config_name}[/cyan]")
|
|
82
|
+
|
|
83
|
+
# Load configuration data based on file format
|
|
84
|
+
try:
|
|
85
|
+
with open(source_path, "rb") as f:
|
|
86
|
+
if file_ext in [".yaml", ".yml"]:
|
|
87
|
+
config_data = yaml.safe_load(f)
|
|
88
|
+
else: # .toml
|
|
89
|
+
config_data = tomllib.load(f)
|
|
90
|
+
except yaml.YAMLError as e:
|
|
91
|
+
print_error(f"Invalid YAML format: {e}")
|
|
92
|
+
return 1
|
|
93
|
+
except Exception as e:
|
|
94
|
+
print_error(f"Failed to parse configuration file: {e}")
|
|
95
|
+
return 1
|
|
96
|
+
|
|
97
|
+
if not isinstance(config_data, dict):
|
|
98
|
+
print_error(
|
|
99
|
+
"Configuration file must contain a dictionary/object at the root level"
|
|
100
|
+
)
|
|
101
|
+
return 1
|
|
102
|
+
|
|
103
|
+
# Create the target directory if it doesn't exist
|
|
104
|
+
nodes_dir = Path.home() / ".manta" / "nodes"
|
|
105
|
+
nodes_dir.mkdir(parents=True, exist_ok=True)
|
|
106
|
+
console.print(f"[green]✓[/green] Configuration directory ready: {nodes_dir}")
|
|
107
|
+
|
|
108
|
+
# Check if configuration already exists
|
|
109
|
+
target_path = nodes_dir / f"{config_name}.toml"
|
|
110
|
+
if target_path.exists():
|
|
111
|
+
print_warning(
|
|
112
|
+
f"Configuration '{config_name}' already exists at: {target_path}"
|
|
113
|
+
)
|
|
114
|
+
overwrite = Confirm.ask(
|
|
115
|
+
"Overwrite existing configuration?",
|
|
116
|
+
default=False,
|
|
117
|
+
)
|
|
118
|
+
if not overwrite:
|
|
119
|
+
console.print("[yellow]Configuration creation cancelled[/yellow]")
|
|
120
|
+
return 1
|
|
121
|
+
|
|
122
|
+
# Create NodeConfiguration from the loaded data
|
|
123
|
+
try:
|
|
124
|
+
config = NodeConfiguration.from_dict(config_data)
|
|
125
|
+
except Exception as e:
|
|
126
|
+
print_error(f"Invalid configuration structure: {e}")
|
|
127
|
+
console.print(
|
|
128
|
+
"\n[dim]Please ensure your configuration file contains all required fields.[/dim]"
|
|
129
|
+
)
|
|
130
|
+
console.print("[dim]Required: identity.secured_token[/dim]")
|
|
131
|
+
return 1
|
|
132
|
+
|
|
133
|
+
# Update metadata with the config name if not already set
|
|
134
|
+
if not config.metadata.name or config.metadata.name == "default":
|
|
135
|
+
config.metadata.name = config_name
|
|
136
|
+
|
|
137
|
+
# Update logging filename based on config name
|
|
138
|
+
if config.identity.alias:
|
|
139
|
+
node_name = config.identity.alias
|
|
140
|
+
elif config.metadata.name and config.metadata.name != "default":
|
|
141
|
+
node_name = config.metadata.name
|
|
142
|
+
else:
|
|
143
|
+
node_name = "node"
|
|
144
|
+
|
|
145
|
+
# Clean the name to be filesystem-safe
|
|
146
|
+
safe_name = "".join(c if c.isalnum() or c in "-_" else "_" for c in node_name)
|
|
147
|
+
# Set to centralized logs directory with nodes subfolder
|
|
148
|
+
config.logging.filename = str(
|
|
149
|
+
Path.home() / ".manta" / "logs" / "nodes" / f"{safe_name}.log"
|
|
150
|
+
)
|
|
151
|
+
|
|
152
|
+
# Save configuration as TOML
|
|
153
|
+
config_manager = NodeConfigManager(target_path)
|
|
154
|
+
config_manager._config = config
|
|
155
|
+
config_manager.save()
|
|
156
|
+
|
|
157
|
+
console.print(
|
|
158
|
+
f"\n[green]✓[/green] Configuration '{config_name}' created successfully!"
|
|
159
|
+
)
|
|
160
|
+
console.print(f"[dim]Saved to: {target_path}[/dim]")
|
|
161
|
+
|
|
162
|
+
# Validate configuration
|
|
163
|
+
issues = config_manager.validate()
|
|
164
|
+
if issues:
|
|
165
|
+
print_warning("Configuration has validation issues:")
|
|
166
|
+
for issue in issues:
|
|
167
|
+
console.print(f" - {issue}")
|
|
168
|
+
else:
|
|
169
|
+
console.print("[green]✓[/green] Configuration is valid")
|
|
170
|
+
|
|
171
|
+
console.print("\nYou can now start a node with:")
|
|
172
|
+
console.print(f" [cyan]manta_node start {config_name}[/cyan]")
|
|
173
|
+
|
|
174
|
+
return 0
|
|
175
|
+
|
|
176
|
+
except KeyboardInterrupt:
|
|
177
|
+
console.print("\n[yellow]Configuration creation cancelled[/yellow]")
|
|
178
|
+
return 1
|
|
179
|
+
except Exception as e:
|
|
180
|
+
print_error(f"Failed to create configuration: {e}")
|
|
181
|
+
import traceback
|
|
182
|
+
|
|
183
|
+
console.print(f"[dim]{traceback.format_exc()}[/dim]")
|
|
184
|
+
return 1
|
|
185
|
+
|
|
186
|
+
|
|
187
|
+
def list_configs() -> int:
|
|
188
|
+
"""List all available configurations."""
|
|
189
|
+
config_manager = NodeConfigManager()
|
|
190
|
+
configs = config_manager.list_available_configs()
|
|
191
|
+
|
|
192
|
+
if not configs:
|
|
193
|
+
console.print("No configurations found.")
|
|
194
|
+
console.print("")
|
|
195
|
+
console.print("Create a configuration with:")
|
|
196
|
+
console.print(" manta_node config init <path/to/config.yaml>")
|
|
197
|
+
return 0
|
|
198
|
+
|
|
199
|
+
table = Table(title="Available Node Configurations")
|
|
200
|
+
table.add_column("Name", style="cyan")
|
|
201
|
+
table.add_column("Path", style="dim")
|
|
202
|
+
table.add_column("Status", style="magenta")
|
|
203
|
+
|
|
204
|
+
for config_name in configs:
|
|
205
|
+
config_path = Path.home() / ".manta" / "nodes" / f"{config_name}.toml"
|
|
206
|
+
|
|
207
|
+
# Try to load and validate config
|
|
208
|
+
try:
|
|
209
|
+
temp_manager = NodeConfigManager(config_path)
|
|
210
|
+
issues = temp_manager.validate()
|
|
211
|
+
status = (
|
|
212
|
+
"[green]Valid[/green]"
|
|
213
|
+
if not issues
|
|
214
|
+
else f"[yellow]{len(issues)} issues[/yellow]"
|
|
215
|
+
)
|
|
216
|
+
except Exception:
|
|
217
|
+
status = "[red]Invalid[/red]"
|
|
218
|
+
|
|
219
|
+
table.add_row(config_name, str(config_path), status)
|
|
220
|
+
|
|
221
|
+
console.print(table)
|
|
222
|
+
return 0
|
|
223
|
+
|
|
224
|
+
|
|
225
|
+
def show_config(config_name: str) -> int:
|
|
226
|
+
"""Show details of a specific configuration."""
|
|
227
|
+
config_path = Path.home() / ".manta" / "nodes" / f"{config_name}.toml"
|
|
228
|
+
|
|
229
|
+
if not config_path.exists():
|
|
230
|
+
print_error(f"Configuration '{config_name}' not found")
|
|
231
|
+
console.print("Available configurations:")
|
|
232
|
+
config_manager = NodeConfigManager()
|
|
233
|
+
for cfg in config_manager.list_available_configs():
|
|
234
|
+
console.print(f" - {cfg}")
|
|
235
|
+
return 1
|
|
236
|
+
|
|
237
|
+
try:
|
|
238
|
+
config_manager = NodeConfigManager(config_path)
|
|
239
|
+
config = config_manager.config
|
|
240
|
+
|
|
241
|
+
console.print(f"\n[bold]Configuration: {config_name}[/bold]")
|
|
242
|
+
console.print(f"Path: [dim]{config_path}[/dim]")
|
|
243
|
+
|
|
244
|
+
# Show identity
|
|
245
|
+
console.print("\n[bold cyan]Identity:[/bold cyan]")
|
|
246
|
+
console.print(f" Alias: {config.identity.alias or 'Auto-generated'}")
|
|
247
|
+
console.print(f" Random ID: {config.identity.random_id}")
|
|
248
|
+
console.print(
|
|
249
|
+
f" Secured Token: {'Set' if config.identity.secured_token else 'Not set'}"
|
|
250
|
+
)
|
|
251
|
+
|
|
252
|
+
# Show network
|
|
253
|
+
console.print("\n[bold cyan]Network:[/bold cyan]")
|
|
254
|
+
console.print(
|
|
255
|
+
f" Manager: {config.network.manager_host}:{config.network.manager_port}"
|
|
256
|
+
)
|
|
257
|
+
# Don't show light service configuration by default (simplify for users)
|
|
258
|
+
|
|
259
|
+
# Show datasets if configured
|
|
260
|
+
if config.datasets.mappings:
|
|
261
|
+
console.print("\n[bold cyan]Datasets:[/bold cyan]")
|
|
262
|
+
for name, path in config.datasets.mappings.items():
|
|
263
|
+
console.print(f" {name}: {path}")
|
|
264
|
+
|
|
265
|
+
# Show metadata
|
|
266
|
+
console.print("\n[bold cyan]Metadata:[/bold cyan]")
|
|
267
|
+
console.print(f" Name: {config.metadata.name}")
|
|
268
|
+
console.print(f" Description: {config.metadata.description}")
|
|
269
|
+
console.print(f" Created: {config.metadata.created_at}")
|
|
270
|
+
console.print(f" Version: {config.metadata.version}")
|
|
271
|
+
|
|
272
|
+
# Validate and show issues
|
|
273
|
+
issues = config_manager.validate()
|
|
274
|
+
if issues:
|
|
275
|
+
console.print("\n[bold red]Validation Issues:[/bold red]")
|
|
276
|
+
for issue in issues:
|
|
277
|
+
console.print(f" - {issue}")
|
|
278
|
+
else:
|
|
279
|
+
console.print("\n[green]✓ Configuration is valid[/green]")
|
|
280
|
+
|
|
281
|
+
return 0
|
|
282
|
+
|
|
283
|
+
except Exception as e:
|
|
284
|
+
print_error(f"Failed to load configuration: {e}")
|
|
285
|
+
return 1
|
|
286
|
+
|
|
287
|
+
|
|
288
|
+
def validate_config(config_name: str) -> int:
|
|
289
|
+
"""Validate a configuration."""
|
|
290
|
+
config_path = Path.home() / ".manta" / "nodes" / f"{config_name}.toml"
|
|
291
|
+
|
|
292
|
+
if not config_path.exists():
|
|
293
|
+
print_error(f"Configuration '{config_name}' not found")
|
|
294
|
+
return 1
|
|
295
|
+
|
|
296
|
+
try:
|
|
297
|
+
config_manager = NodeConfigManager(config_path)
|
|
298
|
+
issues = config_manager.validate()
|
|
299
|
+
|
|
300
|
+
if not issues:
|
|
301
|
+
print_success(f"Configuration '{config_name}' is valid")
|
|
302
|
+
return 0
|
|
303
|
+
else:
|
|
304
|
+
print_error(
|
|
305
|
+
f"Configuration '{config_name}' has {len(issues)} validation issue(s):"
|
|
306
|
+
)
|
|
307
|
+
for issue in issues:
|
|
308
|
+
console.print(f" - {issue}")
|
|
309
|
+
return 1
|
|
310
|
+
|
|
311
|
+
except Exception as e:
|
|
312
|
+
print_error(f"Failed to validate configuration: {e}")
|
|
313
|
+
return 1
|
|
314
|
+
|
|
315
|
+
|
|
316
|
+
def delete_config(config_name: str) -> int:
|
|
317
|
+
"""Delete a configuration file."""
|
|
318
|
+
config_path = Path.home() / ".manta" / "nodes" / f"{config_name}.toml"
|
|
319
|
+
|
|
320
|
+
if not config_path.exists():
|
|
321
|
+
print_error(f"Configuration '{config_name}' not found")
|
|
322
|
+
console.print("Available configurations:")
|
|
323
|
+
config_manager = NodeConfigManager()
|
|
324
|
+
for cfg in config_manager.list_available_configs():
|
|
325
|
+
console.print(f" - {cfg}")
|
|
326
|
+
return 1
|
|
327
|
+
|
|
328
|
+
# Confirm deletion
|
|
329
|
+
console.print(f"Configuration to delete: [cyan]{config_name}[/cyan]")
|
|
330
|
+
console.print(f"File: [dim]{config_path}[/dim]")
|
|
331
|
+
|
|
332
|
+
if not Confirm.ask(
|
|
333
|
+
f"Are you sure you want to delete configuration '{config_name}'?", default=False
|
|
334
|
+
):
|
|
335
|
+
console.print("[yellow]Deletion cancelled[/yellow]")
|
|
336
|
+
return 0
|
|
337
|
+
|
|
338
|
+
try:
|
|
339
|
+
config_path.unlink()
|
|
340
|
+
print_success(f"Configuration '{config_name}' deleted successfully")
|
|
341
|
+
return 0
|
|
342
|
+
|
|
343
|
+
except Exception as e:
|
|
344
|
+
print_error(f"Failed to delete configuration: {e}")
|
|
345
|
+
return 1
|
|
346
|
+
|
|
347
|
+
|
|
348
|
+
def edit_config(config_name: str) -> int:
|
|
349
|
+
"""Edit a configuration file in the default text editor."""
|
|
350
|
+
try:
|
|
351
|
+
# Build configuration path
|
|
352
|
+
config_path = Path.home() / ".manta" / "nodes" / f"{config_name}.toml"
|
|
353
|
+
|
|
354
|
+
if not config_path.exists():
|
|
355
|
+
print_error(f"Configuration '{config_name}' not found")
|
|
356
|
+
console.print("Available configurations:")
|
|
357
|
+
config_manager = NodeConfigManager()
|
|
358
|
+
for cfg in config_manager.list_available_configs():
|
|
359
|
+
console.print(f" - {cfg}")
|
|
360
|
+
return 1
|
|
361
|
+
|
|
362
|
+
# Determine which editor to use
|
|
363
|
+
editor = os.environ.get("EDITOR") or os.environ.get("VISUAL")
|
|
364
|
+
|
|
365
|
+
if not editor:
|
|
366
|
+
# Try common editors
|
|
367
|
+
for cmd in ["nano", "vim", "vi", "emacs", "code", "notepad"]:
|
|
368
|
+
try:
|
|
369
|
+
subprocess.run(["which", cmd], capture_output=True, check=True)
|
|
370
|
+
editor = cmd
|
|
371
|
+
break
|
|
372
|
+
except (subprocess.CalledProcessError, FileNotFoundError):
|
|
373
|
+
continue
|
|
374
|
+
|
|
375
|
+
if not editor:
|
|
376
|
+
print_error(
|
|
377
|
+
"No text editor found. Please set the EDITOR environment variable."
|
|
378
|
+
)
|
|
379
|
+
console.print(f"Configuration file location: {config_path}")
|
|
380
|
+
return 1
|
|
381
|
+
|
|
382
|
+
# Open the file in the editor
|
|
383
|
+
console.print(f"Opening {config_path} in {editor}...")
|
|
384
|
+
|
|
385
|
+
try:
|
|
386
|
+
result = subprocess.run([editor, str(config_path)])
|
|
387
|
+
if result.returncode == 0:
|
|
388
|
+
print_success(f"Configuration '{config_name}' edited successfully")
|
|
389
|
+
|
|
390
|
+
# Validate the configuration after editing
|
|
391
|
+
config_manager = NodeConfigManager(config_path)
|
|
392
|
+
issues = config_manager.validate()
|
|
393
|
+
if issues:
|
|
394
|
+
print_warning("Configuration has validation issues:")
|
|
395
|
+
for issue in issues:
|
|
396
|
+
console.print(f" - {issue}")
|
|
397
|
+
|
|
398
|
+
return 0
|
|
399
|
+
else:
|
|
400
|
+
print_error(f"Editor exited with error code {result.returncode}")
|
|
401
|
+
return 1
|
|
402
|
+
|
|
403
|
+
except Exception as e:
|
|
404
|
+
print_error(f"Failed to open editor: {e}")
|
|
405
|
+
console.print(f"Configuration file location: {config_path}")
|
|
406
|
+
return 1
|
|
407
|
+
|
|
408
|
+
except Exception as e:
|
|
409
|
+
print_error(f"Failed to edit configuration: {e}")
|
|
410
|
+
return 1
|
|
411
|
+
|
|
412
|
+
|
|
413
|
+
def config_command(args: List[str]) -> int:
|
|
414
|
+
"""Handle node config command.
|
|
415
|
+
|
|
416
|
+
Args:
|
|
417
|
+
args: Command line arguments
|
|
418
|
+
|
|
419
|
+
Returns:
|
|
420
|
+
Exit code (0 for success, non-zero for error)
|
|
421
|
+
"""
|
|
422
|
+
parser = argparse.ArgumentParser(
|
|
423
|
+
prog="manta_node config", description="Manage node configurations"
|
|
424
|
+
)
|
|
425
|
+
subparsers = parser.add_subparsers(dest="action", help="Configuration actions")
|
|
426
|
+
|
|
427
|
+
# Init subcommand
|
|
428
|
+
init_parser = subparsers.add_parser(
|
|
429
|
+
"init", help="Create a new configuration from a YAML or TOML file"
|
|
430
|
+
)
|
|
431
|
+
init_parser.add_argument(
|
|
432
|
+
"config_file", help="Path to configuration file (.yaml, .yml, or .toml)"
|
|
433
|
+
)
|
|
434
|
+
|
|
435
|
+
# List subcommand
|
|
436
|
+
subparsers.add_parser("list", help="List all configurations")
|
|
437
|
+
|
|
438
|
+
# Show subcommand
|
|
439
|
+
show_parser = subparsers.add_parser("show", help="Show configuration details")
|
|
440
|
+
show_parser.add_argument("config", help="Configuration name to show")
|
|
441
|
+
|
|
442
|
+
# Validate subcommand
|
|
443
|
+
validate_parser = subparsers.add_parser("validate", help="Validate a configuration")
|
|
444
|
+
validate_parser.add_argument("config", help="Configuration name to validate")
|
|
445
|
+
|
|
446
|
+
# Delete subcommand
|
|
447
|
+
delete_parser = subparsers.add_parser("delete", help="Delete a configuration")
|
|
448
|
+
delete_parser.add_argument("config", help="Configuration name to delete")
|
|
449
|
+
|
|
450
|
+
# Edit subcommand
|
|
451
|
+
edit_parser = subparsers.add_parser(
|
|
452
|
+
"edit", help="Edit configuration file in text editor"
|
|
453
|
+
)
|
|
454
|
+
edit_parser.add_argument(
|
|
455
|
+
"config",
|
|
456
|
+
nargs="?",
|
|
457
|
+
default="default",
|
|
458
|
+
help="Configuration name to edit (default: default)",
|
|
459
|
+
)
|
|
460
|
+
|
|
461
|
+
try:
|
|
462
|
+
parsed_args = parser.parse_args(args)
|
|
463
|
+
except SystemExit:
|
|
464
|
+
return 1
|
|
465
|
+
|
|
466
|
+
if not parsed_args.action:
|
|
467
|
+
parser.print_help()
|
|
468
|
+
return 1
|
|
469
|
+
|
|
470
|
+
if parsed_args.action == "init":
|
|
471
|
+
return init_config_from_file(parsed_args.config_file)
|
|
472
|
+
elif parsed_args.action == "list":
|
|
473
|
+
return list_configs()
|
|
474
|
+
elif parsed_args.action == "show":
|
|
475
|
+
return show_config(parsed_args.config)
|
|
476
|
+
elif parsed_args.action == "validate":
|
|
477
|
+
return validate_config(parsed_args.config)
|
|
478
|
+
elif parsed_args.action == "delete":
|
|
479
|
+
return delete_config(parsed_args.config)
|
|
480
|
+
elif parsed_args.action == "edit":
|
|
481
|
+
return edit_config(parsed_args.config)
|
|
482
|
+
else:
|
|
483
|
+
print_error(f"Unknown action: {parsed_args.action}")
|
|
484
|
+
return 1
|
|
485
|
+
|
|
486
|
+
|
|
487
|
+
if __name__ == "__main__":
|
|
488
|
+
import sys
|
|
489
|
+
|
|
490
|
+
sys.exit(config_command(sys.argv[1:]))
|
|
@@ -0,0 +1,168 @@
|
|
|
1
|
+
"""Node logs command implementation."""
|
|
2
|
+
|
|
3
|
+
import argparse
|
|
4
|
+
import time
|
|
5
|
+
from pathlib import Path
|
|
6
|
+
from typing import List
|
|
7
|
+
|
|
8
|
+
from rich.console import Console
|
|
9
|
+
|
|
10
|
+
console = Console()
|
|
11
|
+
|
|
12
|
+
|
|
13
|
+
def print_error(message: str):
|
|
14
|
+
"""Print error message in red."""
|
|
15
|
+
console.print(f"[red]Error: {message}[/red]")
|
|
16
|
+
|
|
17
|
+
|
|
18
|
+
def find_log_files():
|
|
19
|
+
"""Find available log files."""
|
|
20
|
+
logs_dir = Path.home() / ".manta" / "logs" / "nodes"
|
|
21
|
+
log_files = {}
|
|
22
|
+
|
|
23
|
+
if not logs_dir.exists():
|
|
24
|
+
return log_files
|
|
25
|
+
|
|
26
|
+
for log_file in logs_dir.glob("*.log"):
|
|
27
|
+
instance_id = log_file.stem
|
|
28
|
+
log_files[instance_id] = log_file
|
|
29
|
+
|
|
30
|
+
return log_files
|
|
31
|
+
|
|
32
|
+
|
|
33
|
+
def tail_log_file(log_file: Path, lines: int = 50, follow: bool = False) -> None:
|
|
34
|
+
"""Tail a log file, similar to 'tail -f'."""
|
|
35
|
+
try:
|
|
36
|
+
if not log_file.exists():
|
|
37
|
+
print_error(f"Log file not found: {log_file}")
|
|
38
|
+
return
|
|
39
|
+
|
|
40
|
+
# Read last N lines if not following
|
|
41
|
+
if not follow:
|
|
42
|
+
with open(log_file, "r") as f:
|
|
43
|
+
# Read all lines and take the last N
|
|
44
|
+
all_lines = f.readlines()
|
|
45
|
+
last_lines = all_lines[-lines:] if len(all_lines) > lines else all_lines
|
|
46
|
+
for line in last_lines:
|
|
47
|
+
console.print(line.rstrip())
|
|
48
|
+
return
|
|
49
|
+
|
|
50
|
+
# Follow mode - continuously read new lines
|
|
51
|
+
console.print(f"Following log file: {log_file}")
|
|
52
|
+
console.print("Press Ctrl+C to stop")
|
|
53
|
+
console.print("-" * 60)
|
|
54
|
+
|
|
55
|
+
with open(log_file, "r") as f:
|
|
56
|
+
# First, show last N lines
|
|
57
|
+
all_lines = f.readlines()
|
|
58
|
+
last_lines = all_lines[-lines:] if len(all_lines) > lines else all_lines
|
|
59
|
+
for line in last_lines:
|
|
60
|
+
console.print(line.rstrip())
|
|
61
|
+
|
|
62
|
+
# Then follow new lines
|
|
63
|
+
while True:
|
|
64
|
+
line = f.readline()
|
|
65
|
+
if line:
|
|
66
|
+
console.print(line.rstrip())
|
|
67
|
+
else:
|
|
68
|
+
time.sleep(0.1)
|
|
69
|
+
|
|
70
|
+
except KeyboardInterrupt:
|
|
71
|
+
console.print("\n[yellow]Stopped following log file[/yellow]")
|
|
72
|
+
except Exception as e:
|
|
73
|
+
print_error(f"Error reading log file: {e}")
|
|
74
|
+
|
|
75
|
+
|
|
76
|
+
def logs_command(args: List[str]) -> int:
|
|
77
|
+
"""Handle node logs command.
|
|
78
|
+
|
|
79
|
+
Args:
|
|
80
|
+
args: Command line arguments
|
|
81
|
+
|
|
82
|
+
Returns:
|
|
83
|
+
Exit code (0 for success, non-zero for error)
|
|
84
|
+
"""
|
|
85
|
+
parser = argparse.ArgumentParser(
|
|
86
|
+
prog="manta_node logs", description="View node logs"
|
|
87
|
+
)
|
|
88
|
+
parser.add_argument(
|
|
89
|
+
"instance", nargs="?", help="Instance ID or name to show logs for"
|
|
90
|
+
)
|
|
91
|
+
parser.add_argument(
|
|
92
|
+
"--follow", "-f", action="store_true", help="Follow log output (like tail -f)"
|
|
93
|
+
)
|
|
94
|
+
parser.add_argument(
|
|
95
|
+
"--lines",
|
|
96
|
+
"-n",
|
|
97
|
+
type=int,
|
|
98
|
+
default=50,
|
|
99
|
+
help="Number of lines to show (default: 50)",
|
|
100
|
+
)
|
|
101
|
+
parser.add_argument("--list", action="store_true", help="List available log files")
|
|
102
|
+
|
|
103
|
+
try:
|
|
104
|
+
parsed_args = parser.parse_args(args)
|
|
105
|
+
except SystemExit:
|
|
106
|
+
return 1
|
|
107
|
+
|
|
108
|
+
log_files = find_log_files()
|
|
109
|
+
|
|
110
|
+
if parsed_args.list:
|
|
111
|
+
if not log_files:
|
|
112
|
+
console.print("No log files found.")
|
|
113
|
+
console.print("")
|
|
114
|
+
console.print("Log files are created when nodes are started.")
|
|
115
|
+
console.print("Start a node with: manta_node start")
|
|
116
|
+
else:
|
|
117
|
+
console.print("Available log files:")
|
|
118
|
+
for instance_id, log_file in log_files.items():
|
|
119
|
+
size_mb = log_file.stat().st_size / (1024 * 1024)
|
|
120
|
+
console.print(f" - {instance_id} ({size_mb:.1f} MB) - {log_file}")
|
|
121
|
+
return 0
|
|
122
|
+
|
|
123
|
+
if not parsed_args.instance:
|
|
124
|
+
if not log_files:
|
|
125
|
+
print_error("No log files found and no instance specified")
|
|
126
|
+
console.print("")
|
|
127
|
+
console.print("Available options:")
|
|
128
|
+
console.print(" - Start a node: manta_node start")
|
|
129
|
+
console.print(" - List log files: manta_node logs --list")
|
|
130
|
+
return 1
|
|
131
|
+
|
|
132
|
+
if len(log_files) == 1:
|
|
133
|
+
# Only one log file, use it
|
|
134
|
+
instance_id, log_file = next(iter(log_files.items()))
|
|
135
|
+
console.print(f"Using log file for instance: {instance_id}")
|
|
136
|
+
else:
|
|
137
|
+
# Multiple log files, ask user to specify
|
|
138
|
+
print_error("Multiple log files found. Please specify an instance:")
|
|
139
|
+
for instance_id in log_files.keys():
|
|
140
|
+
console.print(f" - {instance_id}")
|
|
141
|
+
return 1
|
|
142
|
+
else:
|
|
143
|
+
instance_name = parsed_args.instance
|
|
144
|
+
|
|
145
|
+
# Find matching log file
|
|
146
|
+
log_file = None
|
|
147
|
+
for instance_id, file_path in log_files.items():
|
|
148
|
+
if instance_id == instance_name or instance_id.startswith(instance_name):
|
|
149
|
+
log_file = file_path
|
|
150
|
+
break
|
|
151
|
+
|
|
152
|
+
if not log_file:
|
|
153
|
+
print_error(f"No log file found for instance '{instance_name}'")
|
|
154
|
+
if log_files:
|
|
155
|
+
console.print("Available log files:")
|
|
156
|
+
for instance_id in log_files.keys():
|
|
157
|
+
console.print(f" - {instance_id}")
|
|
158
|
+
return 1
|
|
159
|
+
|
|
160
|
+
# Show logs
|
|
161
|
+
tail_log_file(log_file, parsed_args.lines, parsed_args.follow)
|
|
162
|
+
return 0
|
|
163
|
+
|
|
164
|
+
|
|
165
|
+
if __name__ == "__main__":
|
|
166
|
+
import sys
|
|
167
|
+
|
|
168
|
+
sys.exit(logs_command(sys.argv[1:]))
|