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,253 @@
|
|
|
1
|
+
"""Node stop command implementation."""
|
|
2
|
+
|
|
3
|
+
import argparse
|
|
4
|
+
import json
|
|
5
|
+
import os
|
|
6
|
+
import signal
|
|
7
|
+
import time
|
|
8
|
+
from pathlib import Path
|
|
9
|
+
from typing import List
|
|
10
|
+
|
|
11
|
+
from rich.console import Console
|
|
12
|
+
|
|
13
|
+
console = Console()
|
|
14
|
+
|
|
15
|
+
|
|
16
|
+
def print_error(message: str):
|
|
17
|
+
"""Print error message in red."""
|
|
18
|
+
console.print(f"[red]Error: {message}[/red]")
|
|
19
|
+
|
|
20
|
+
|
|
21
|
+
def print_success(message: str):
|
|
22
|
+
"""Print success message in green."""
|
|
23
|
+
console.print(f"[green]{message}[/green]")
|
|
24
|
+
|
|
25
|
+
|
|
26
|
+
def print_warning(message: str):
|
|
27
|
+
"""Print warning message in yellow."""
|
|
28
|
+
console.print(f"[yellow]{message}[/yellow]")
|
|
29
|
+
|
|
30
|
+
|
|
31
|
+
def find_running_instances():
|
|
32
|
+
"""Find running node instances by looking for instance JSON files."""
|
|
33
|
+
|
|
34
|
+
instances_dir = Path.home() / ".manta" / "nodes" / "instances"
|
|
35
|
+
instances = []
|
|
36
|
+
|
|
37
|
+
if not instances_dir.exists():
|
|
38
|
+
return instances
|
|
39
|
+
|
|
40
|
+
for instance_file in instances_dir.glob("*.json"):
|
|
41
|
+
try:
|
|
42
|
+
with open(instance_file, "r") as f:
|
|
43
|
+
instance_data = json.load(f)
|
|
44
|
+
|
|
45
|
+
pid = instance_data.get("pid")
|
|
46
|
+
instance_id = instance_data.get("instance_id", instance_file.stem)
|
|
47
|
+
alias = instance_data.get("alias", instance_id)
|
|
48
|
+
|
|
49
|
+
# Check if process is still running
|
|
50
|
+
try:
|
|
51
|
+
# Send signal 0 to check if process exists
|
|
52
|
+
|
|
53
|
+
os.kill(pid, 0)
|
|
54
|
+
instances.append(
|
|
55
|
+
{
|
|
56
|
+
"instance_id": instance_id,
|
|
57
|
+
"alias": alias,
|
|
58
|
+
"pid": pid,
|
|
59
|
+
"instance_file": instance_file,
|
|
60
|
+
}
|
|
61
|
+
)
|
|
62
|
+
except OSError:
|
|
63
|
+
# Process no longer exists, remove stale instance file
|
|
64
|
+
instance_file.unlink(missing_ok=True)
|
|
65
|
+
|
|
66
|
+
except (ValueError, FileNotFoundError):
|
|
67
|
+
# Invalid instance file, remove it
|
|
68
|
+
instance_file.unlink(missing_ok=True)
|
|
69
|
+
|
|
70
|
+
return instances
|
|
71
|
+
|
|
72
|
+
|
|
73
|
+
def stop_instance_by_pid(
|
|
74
|
+
instance: dict, force: bool = False, timeout: int = 10
|
|
75
|
+
) -> bool:
|
|
76
|
+
"""Stop a node instance using its PID.
|
|
77
|
+
|
|
78
|
+
Args:
|
|
79
|
+
instance: Instance information dictionary
|
|
80
|
+
force: If True, use SIGKILL instead of SIGTERM
|
|
81
|
+
timeout: Seconds to wait for graceful shutdown before force killing
|
|
82
|
+
|
|
83
|
+
Returns:
|
|
84
|
+
True if successfully stopped, False otherwise
|
|
85
|
+
"""
|
|
86
|
+
pid = instance["pid"]
|
|
87
|
+
instance_id = instance["instance_id"]
|
|
88
|
+
alias = instance.get("alias", instance_id)
|
|
89
|
+
|
|
90
|
+
try:
|
|
91
|
+
if force:
|
|
92
|
+
# Force kill with SIGKILL
|
|
93
|
+
os.kill(pid, signal.SIGKILL)
|
|
94
|
+
console.print(f"Force killed node '{alias}' (PID: {pid})")
|
|
95
|
+
else:
|
|
96
|
+
# Graceful shutdown with SIGTERM
|
|
97
|
+
os.kill(pid, signal.SIGTERM)
|
|
98
|
+
console.print(f"Sent termination signal to node '{alias}' (PID: {pid})")
|
|
99
|
+
|
|
100
|
+
# Wait for process to terminate gracefully
|
|
101
|
+
console.print(
|
|
102
|
+
f"[dim]Waiting up to {timeout} seconds for graceful shutdown...[/dim]"
|
|
103
|
+
)
|
|
104
|
+
start_time = time.time()
|
|
105
|
+
while time.time() - start_time < timeout:
|
|
106
|
+
try:
|
|
107
|
+
# Check if process is still running
|
|
108
|
+
os.kill(pid, 0)
|
|
109
|
+
time.sleep(0.5)
|
|
110
|
+
except OSError:
|
|
111
|
+
# Process has terminated
|
|
112
|
+
console.print(f"[green]Node '{alias}' stopped gracefully[/green]")
|
|
113
|
+
break
|
|
114
|
+
else:
|
|
115
|
+
# Timeout reached, process still running
|
|
116
|
+
print_warning(
|
|
117
|
+
f"Node '{alias}' did not stop gracefully, sending SIGKILL..."
|
|
118
|
+
)
|
|
119
|
+
os.kill(pid, signal.SIGKILL)
|
|
120
|
+
console.print(f"Force killed node '{alias}' after timeout")
|
|
121
|
+
|
|
122
|
+
# Remove instance file
|
|
123
|
+
instance["instance_file"].unlink(missing_ok=True)
|
|
124
|
+
return True
|
|
125
|
+
|
|
126
|
+
except OSError as e:
|
|
127
|
+
if e.errno == 3: # No such process
|
|
128
|
+
# Process already dead, just remove instance file
|
|
129
|
+
instance["instance_file"].unlink(missing_ok=True)
|
|
130
|
+
print_warning(f"Node '{alias}' was already stopped")
|
|
131
|
+
return True
|
|
132
|
+
else:
|
|
133
|
+
print_error(f"Failed to stop node '{alias}': {e}")
|
|
134
|
+
return False
|
|
135
|
+
|
|
136
|
+
|
|
137
|
+
def stop_all_instances(force: bool = False, timeout: int = 10) -> int:
|
|
138
|
+
"""Stop all running node instances."""
|
|
139
|
+
instances = find_running_instances()
|
|
140
|
+
|
|
141
|
+
if not instances:
|
|
142
|
+
console.print("No running node instances found.")
|
|
143
|
+
return 0
|
|
144
|
+
|
|
145
|
+
console.print(f"Found {len(instances)} running instance(s):")
|
|
146
|
+
for instance in instances:
|
|
147
|
+
console.print(f" - {instance['alias']} (PID: {instance['pid']})")
|
|
148
|
+
|
|
149
|
+
console.print("")
|
|
150
|
+
if force:
|
|
151
|
+
console.print("Force stopping all instances...")
|
|
152
|
+
else:
|
|
153
|
+
console.print("Stopping all instances...")
|
|
154
|
+
|
|
155
|
+
success_count = 0
|
|
156
|
+
for instance in instances:
|
|
157
|
+
if stop_instance_by_pid(instance, force, timeout):
|
|
158
|
+
success_count += 1
|
|
159
|
+
|
|
160
|
+
if success_count == len(instances):
|
|
161
|
+
print_success(f"All {success_count} instances stopped successfully")
|
|
162
|
+
return 0
|
|
163
|
+
else:
|
|
164
|
+
print_error(
|
|
165
|
+
f"Only {success_count}/{len(instances)} instances stopped successfully"
|
|
166
|
+
)
|
|
167
|
+
return 1
|
|
168
|
+
|
|
169
|
+
|
|
170
|
+
def stop_specific_instance(
|
|
171
|
+
instance_name: str, force: bool = False, timeout: int = 10
|
|
172
|
+
) -> int:
|
|
173
|
+
"""Stop a specific node instance by name or ID."""
|
|
174
|
+
instances = find_running_instances()
|
|
175
|
+
|
|
176
|
+
if not instances:
|
|
177
|
+
print_error("No running node instances found")
|
|
178
|
+
return 1
|
|
179
|
+
|
|
180
|
+
# Find matching instance (by alias or instance_id)
|
|
181
|
+
target_instance = None
|
|
182
|
+
for instance in instances:
|
|
183
|
+
if (
|
|
184
|
+
instance["alias"] == instance_name
|
|
185
|
+
or instance["instance_id"] == instance_name
|
|
186
|
+
or instance["instance_id"].startswith(instance_name)
|
|
187
|
+
):
|
|
188
|
+
target_instance = instance
|
|
189
|
+
break
|
|
190
|
+
|
|
191
|
+
if not target_instance:
|
|
192
|
+
print_error(f"Node instance '{instance_name}' not found")
|
|
193
|
+
console.print("Running instances:")
|
|
194
|
+
for instance in instances:
|
|
195
|
+
console.print(f" - {instance['alias']} (PID: {instance['pid']})")
|
|
196
|
+
return 1
|
|
197
|
+
|
|
198
|
+
if stop_instance_by_pid(target_instance, force, timeout):
|
|
199
|
+
print_success(f"Node '{target_instance['alias']}' stopped successfully")
|
|
200
|
+
return 0
|
|
201
|
+
else:
|
|
202
|
+
return 1
|
|
203
|
+
|
|
204
|
+
|
|
205
|
+
def stop_command(args: List[str]) -> int:
|
|
206
|
+
"""Handle node stop command.
|
|
207
|
+
|
|
208
|
+
Args:
|
|
209
|
+
args: Command line arguments
|
|
210
|
+
|
|
211
|
+
Returns:
|
|
212
|
+
Exit code (0 for success, non-zero for error)
|
|
213
|
+
"""
|
|
214
|
+
parser = argparse.ArgumentParser(
|
|
215
|
+
prog="manta_node stop", description="Stop running node instances"
|
|
216
|
+
)
|
|
217
|
+
parser.add_argument(
|
|
218
|
+
"instance",
|
|
219
|
+
nargs="?",
|
|
220
|
+
help="Instance ID or name to stop (if not provided, stops all instances)",
|
|
221
|
+
)
|
|
222
|
+
parser.add_argument("--all", action="store_true", help="Stop all running instances")
|
|
223
|
+
parser.add_argument(
|
|
224
|
+
"--force",
|
|
225
|
+
"-f",
|
|
226
|
+
action="store_true",
|
|
227
|
+
help="Force stop using SIGKILL instead of SIGTERM",
|
|
228
|
+
)
|
|
229
|
+
parser.add_argument(
|
|
230
|
+
"--timeout",
|
|
231
|
+
"-t",
|
|
232
|
+
type=int,
|
|
233
|
+
default=10,
|
|
234
|
+
help="Timeout in seconds to wait for graceful shutdown (default: 10)",
|
|
235
|
+
)
|
|
236
|
+
|
|
237
|
+
try:
|
|
238
|
+
parsed_args = parser.parse_args(args)
|
|
239
|
+
except SystemExit:
|
|
240
|
+
return 1
|
|
241
|
+
|
|
242
|
+
if parsed_args.all or not parsed_args.instance:
|
|
243
|
+
return stop_all_instances(parsed_args.force, parsed_args.timeout)
|
|
244
|
+
else:
|
|
245
|
+
return stop_specific_instance(
|
|
246
|
+
parsed_args.instance, parsed_args.force, parsed_args.timeout
|
|
247
|
+
)
|
|
248
|
+
|
|
249
|
+
|
|
250
|
+
if __name__ == "__main__":
|
|
251
|
+
import sys
|
|
252
|
+
|
|
253
|
+
sys.exit(stop_command(sys.argv[1:]))
|
|
@@ -0,0 +1,139 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Compatibility layer for old configuration classes.
|
|
3
|
+
|
|
4
|
+
This module provides compatibility classes that match the old configuration API
|
|
5
|
+
but delegate to the new NodeConfiguration system.
|
|
6
|
+
"""
|
|
7
|
+
|
|
8
|
+
from dataclasses import dataclass
|
|
9
|
+
from typing import Any, Dict, Optional
|
|
10
|
+
|
|
11
|
+
from manta_node.infrastructure.config.node_config_manager import IdentityConfig
|
|
12
|
+
|
|
13
|
+
from ..infrastructure.config import NodeConfiguration
|
|
14
|
+
|
|
15
|
+
|
|
16
|
+
@dataclass
|
|
17
|
+
class ConnectionConfig:
|
|
18
|
+
"""Compatibility class for old ConnectionConfig."""
|
|
19
|
+
|
|
20
|
+
host: str = "localhost"
|
|
21
|
+
port: int = 50051
|
|
22
|
+
use_tls: bool = False
|
|
23
|
+
cert_folder: Optional[str] = None
|
|
24
|
+
|
|
25
|
+
|
|
26
|
+
@dataclass
|
|
27
|
+
class HardwareConfig:
|
|
28
|
+
"""Compatibility class for old HardwareConfig."""
|
|
29
|
+
|
|
30
|
+
gpu_enabled: Optional[bool] = None
|
|
31
|
+
max_concurrent_tasks: int = 2
|
|
32
|
+
cpu_limit: str = "2.0"
|
|
33
|
+
memory_limit: str = "4G"
|
|
34
|
+
|
|
35
|
+
def get(self, key: str, default=None):
|
|
36
|
+
"""Make this object behave like a dictionary for compatibility."""
|
|
37
|
+
return getattr(self, key, default)
|
|
38
|
+
|
|
39
|
+
def copy(self):
|
|
40
|
+
"""Return a copy of this hardware config."""
|
|
41
|
+
return HardwareConfig(
|
|
42
|
+
gpu_enabled=self.gpu_enabled,
|
|
43
|
+
max_concurrent_tasks=self.max_concurrent_tasks,
|
|
44
|
+
cpu_limit=self.cpu_limit,
|
|
45
|
+
memory_limit=self.memory_limit,
|
|
46
|
+
)
|
|
47
|
+
|
|
48
|
+
|
|
49
|
+
@dataclass
|
|
50
|
+
class NodeConfig:
|
|
51
|
+
"""Compatibility class for old NodeConfig API."""
|
|
52
|
+
|
|
53
|
+
name: str
|
|
54
|
+
alias: str
|
|
55
|
+
connection: ConnectionConfig
|
|
56
|
+
hardware: Optional[HardwareConfig] = None
|
|
57
|
+
datasets: Optional[Dict[str, str]] = None
|
|
58
|
+
auto_register: bool = True
|
|
59
|
+
random_id: bool = False
|
|
60
|
+
created_at: Optional[Any] = None
|
|
61
|
+
updated_at: Optional[Any] = None
|
|
62
|
+
|
|
63
|
+
def __post_init__(self):
|
|
64
|
+
"""Ensure datasets is a proper dict if provided."""
|
|
65
|
+
if self.datasets is None:
|
|
66
|
+
self.datasets = {}
|
|
67
|
+
# Set default timestamps if not provided
|
|
68
|
+
from datetime import datetime
|
|
69
|
+
|
|
70
|
+
if self.created_at is None:
|
|
71
|
+
self.created_at = datetime.now()
|
|
72
|
+
if self.updated_at is None:
|
|
73
|
+
self.updated_at = datetime.now()
|
|
74
|
+
|
|
75
|
+
def to_node_configuration(self) -> NodeConfiguration:
|
|
76
|
+
"""Convert to new NodeConfiguration format."""
|
|
77
|
+
config = NodeConfiguration(
|
|
78
|
+
identity=IdentityConfig(
|
|
79
|
+
alias=self.alias, random_id=self.random_id, secured_token=""
|
|
80
|
+
)
|
|
81
|
+
)
|
|
82
|
+
|
|
83
|
+
# Metadata
|
|
84
|
+
config.metadata.name = self.name
|
|
85
|
+
|
|
86
|
+
# Identity
|
|
87
|
+
config.identity.alias = self.alias
|
|
88
|
+
config.identity.random_id = self.random_id
|
|
89
|
+
|
|
90
|
+
# Network
|
|
91
|
+
config.network.manager_host = self.connection.host
|
|
92
|
+
config.network.manager_port = self.connection.port
|
|
93
|
+
|
|
94
|
+
# Security
|
|
95
|
+
config.security.use_tls = self.connection.use_tls
|
|
96
|
+
if self.connection.cert_folder:
|
|
97
|
+
config.security.cert_folder = self.connection.cert_folder
|
|
98
|
+
|
|
99
|
+
# Containers/Hardware
|
|
100
|
+
if self.hardware:
|
|
101
|
+
config.containers.gpu_enabled = self.hardware.gpu_enabled
|
|
102
|
+
config.containers.default_cpu_limit = self.hardware.cpu_limit
|
|
103
|
+
config.containers.default_memory_limit = self.hardware.memory_limit
|
|
104
|
+
config.tasks.max_concurrent = self.hardware.max_concurrent_tasks
|
|
105
|
+
|
|
106
|
+
# Datasets
|
|
107
|
+
if self.datasets:
|
|
108
|
+
config.datasets.mappings = self.datasets
|
|
109
|
+
|
|
110
|
+
return config
|
|
111
|
+
|
|
112
|
+
@classmethod
|
|
113
|
+
def from_node_configuration(cls, config: NodeConfiguration) -> "NodeConfig":
|
|
114
|
+
"""Create from new NodeConfiguration format."""
|
|
115
|
+
connection = ConnectionConfig(
|
|
116
|
+
host=config.network.manager_host,
|
|
117
|
+
port=config.network.manager_port,
|
|
118
|
+
use_tls=config.security.use_tls,
|
|
119
|
+
cert_folder=config.security.cert_folder,
|
|
120
|
+
)
|
|
121
|
+
|
|
122
|
+
hardware = HardwareConfig(
|
|
123
|
+
gpu_enabled=config.containers.gpu_enabled,
|
|
124
|
+
max_concurrent_tasks=config.tasks.max_concurrent,
|
|
125
|
+
cpu_limit=config.containers.default_cpu_limit,
|
|
126
|
+
memory_limit=config.containers.default_memory_limit,
|
|
127
|
+
)
|
|
128
|
+
|
|
129
|
+
return cls(
|
|
130
|
+
name=config.metadata.name,
|
|
131
|
+
alias=config.identity.alias or config.metadata.name,
|
|
132
|
+
connection=connection,
|
|
133
|
+
hardware=hardware,
|
|
134
|
+
datasets=(
|
|
135
|
+
config.datasets.mappings.copy() if config.datasets.mappings else None
|
|
136
|
+
),
|
|
137
|
+
auto_register=True, # Default assumption
|
|
138
|
+
random_id=config.identity.random_id,
|
|
139
|
+
)
|
manta_node/cli/main.py
ADDED
|
@@ -0,0 +1,133 @@
|
|
|
1
|
+
"""Manta Node CLI main entry point."""
|
|
2
|
+
|
|
3
|
+
import sys
|
|
4
|
+
from typing import List, Optional
|
|
5
|
+
|
|
6
|
+
from rich.console import Console
|
|
7
|
+
|
|
8
|
+
from .commands.cluster import cluster_command
|
|
9
|
+
from .commands.config import config_command
|
|
10
|
+
from .commands.logs import logs_command
|
|
11
|
+
from .commands.start import start_command
|
|
12
|
+
from .commands.status import status_command
|
|
13
|
+
from .commands.stop import stop_command
|
|
14
|
+
from .version import get_version_string, get_full_version_info
|
|
15
|
+
|
|
16
|
+
console = Console()
|
|
17
|
+
|
|
18
|
+
|
|
19
|
+
def show_version(verbose: bool = False):
|
|
20
|
+
"""Show version information."""
|
|
21
|
+
if verbose:
|
|
22
|
+
info = get_full_version_info()
|
|
23
|
+
console.print(
|
|
24
|
+
f"[bold cyan]Manta Node[/bold cyan] version [green]{info['version']}[/green]"
|
|
25
|
+
)
|
|
26
|
+
console.print(f" Package version: {info['package_version']}")
|
|
27
|
+
console.print(f" Build date: {info['date']}")
|
|
28
|
+
console.print(f" Commit ID: {info['commit']}")
|
|
29
|
+
else:
|
|
30
|
+
console.print(
|
|
31
|
+
f"[bold cyan]manta_node[/bold cyan] [green]{get_version_string()}[/green]"
|
|
32
|
+
)
|
|
33
|
+
|
|
34
|
+
|
|
35
|
+
def show_help():
|
|
36
|
+
"""Show CLI help information."""
|
|
37
|
+
help_text = """
|
|
38
|
+
[bold cyan]Manta Node CLI[/bold cyan]
|
|
39
|
+
|
|
40
|
+
[bold]Usage:[/bold]
|
|
41
|
+
manta_node <command> [options]
|
|
42
|
+
|
|
43
|
+
[bold]Commands:[/bold]
|
|
44
|
+
[cyan]start[/cyan] Start a node instance
|
|
45
|
+
[cyan]stop[/cyan] Stop running node instances
|
|
46
|
+
[cyan]status[/cyan] Show status of running nodes
|
|
47
|
+
[cyan]logs[/cyan] View node logs
|
|
48
|
+
[cyan]config[/cyan] Manage node configurations
|
|
49
|
+
[cyan]cluster[/cyan] Manage node clusters
|
|
50
|
+
[cyan]version[/cyan] Show version information
|
|
51
|
+
[cyan]help[/cyan] Show this help message
|
|
52
|
+
|
|
53
|
+
[bold]Global Options:[/bold]
|
|
54
|
+
[cyan]-v, --version[/cyan] Show version information
|
|
55
|
+
[cyan]-h, --help[/cyan] Show this help message
|
|
56
|
+
|
|
57
|
+
[bold]Examples:[/bold]
|
|
58
|
+
manta_node start --config default
|
|
59
|
+
manta_node status --all
|
|
60
|
+
manta_node logs --follow node-123
|
|
61
|
+
manta_node config init <path/to/config.yaml>
|
|
62
|
+
manta_node --version
|
|
63
|
+
|
|
64
|
+
For more help on a specific command:
|
|
65
|
+
manta_node <command> --help
|
|
66
|
+
"""
|
|
67
|
+
console.print(help_text)
|
|
68
|
+
|
|
69
|
+
|
|
70
|
+
def main(args: Optional[List[str]] = None) -> int:
|
|
71
|
+
"""Main CLI entry point for manta-node.
|
|
72
|
+
|
|
73
|
+
Args:
|
|
74
|
+
args: Optional command line arguments (uses sys.argv if None)
|
|
75
|
+
|
|
76
|
+
Returns:
|
|
77
|
+
Exit code (0 for success, non-zero for error)
|
|
78
|
+
"""
|
|
79
|
+
if args is None:
|
|
80
|
+
args = sys.argv[1:] # Skip the script name
|
|
81
|
+
|
|
82
|
+
# Remove 'node' if it's the first argument (when called as 'manta_node <command>')
|
|
83
|
+
if args and args[0] == "node":
|
|
84
|
+
args = args[1:]
|
|
85
|
+
|
|
86
|
+
# Show version if requested
|
|
87
|
+
if args and args[0] in ["-v", "--version", "version"]:
|
|
88
|
+
# Check for verbose flag
|
|
89
|
+
verbose = len(args) > 1 and args[1] in ["-v", "--verbose"]
|
|
90
|
+
show_version(verbose=verbose)
|
|
91
|
+
return 0
|
|
92
|
+
|
|
93
|
+
# Show help if no arguments or help requested
|
|
94
|
+
if not args or args[0] in ["-h", "--help", "help"]:
|
|
95
|
+
show_help()
|
|
96
|
+
return 0
|
|
97
|
+
|
|
98
|
+
command = args[0]
|
|
99
|
+
command_args = args[1:]
|
|
100
|
+
|
|
101
|
+
try:
|
|
102
|
+
# Import and execute the appropriate command
|
|
103
|
+
if command == "start":
|
|
104
|
+
return start_command(command_args)
|
|
105
|
+
elif command == "stop":
|
|
106
|
+
return stop_command(command_args)
|
|
107
|
+
elif command == "status":
|
|
108
|
+
return status_command(command_args)
|
|
109
|
+
elif command == "logs":
|
|
110
|
+
return logs_command(command_args)
|
|
111
|
+
elif command == "config":
|
|
112
|
+
return config_command(command_args)
|
|
113
|
+
elif command == "cluster":
|
|
114
|
+
return cluster_command(command_args)
|
|
115
|
+
else:
|
|
116
|
+
console.print(f"[red]Error: Unknown command '{command}'[/red]")
|
|
117
|
+
console.print("\nUse 'manta_node help' to see available commands.")
|
|
118
|
+
return 1
|
|
119
|
+
|
|
120
|
+
except ImportError as e:
|
|
121
|
+
console.print(f"[red]Error: Command '{command}' not available[/red]")
|
|
122
|
+
console.print(f"[dim]{e}[/dim]")
|
|
123
|
+
return 1
|
|
124
|
+
except KeyboardInterrupt:
|
|
125
|
+
console.print("\n[yellow]Operation cancelled by user[/yellow]")
|
|
126
|
+
return 1
|
|
127
|
+
except Exception as e:
|
|
128
|
+
console.print(f"[red]Unexpected error: {e}[/red]")
|
|
129
|
+
return 1
|
|
130
|
+
|
|
131
|
+
|
|
132
|
+
if __name__ == "__main__":
|
|
133
|
+
sys.exit(main())
|
|
@@ -0,0 +1,106 @@
|
|
|
1
|
+
"""Version information for Manta Node CLI.
|
|
2
|
+
|
|
3
|
+
Version information is loaded from:
|
|
4
|
+
1. _version_generated.py (created at build time by scripts/generate_version.py)
|
|
5
|
+
2. Fall back to dynamic git detection if not available (development mode)
|
|
6
|
+
"""
|
|
7
|
+
|
|
8
|
+
import subprocess
|
|
9
|
+
from datetime import datetime
|
|
10
|
+
from pathlib import Path
|
|
11
|
+
from typing import Optional
|
|
12
|
+
|
|
13
|
+
|
|
14
|
+
def _load_build_version() -> Optional[dict]:
|
|
15
|
+
"""Try to load version from build-time generated file.
|
|
16
|
+
|
|
17
|
+
Returns:
|
|
18
|
+
Dictionary with build version info, or None if file doesn't exist.
|
|
19
|
+
"""
|
|
20
|
+
try:
|
|
21
|
+
# Import the generated version file
|
|
22
|
+
from . import _version_generated
|
|
23
|
+
|
|
24
|
+
return {
|
|
25
|
+
"version": _version_generated.__version__,
|
|
26
|
+
"date": _version_generated.__build_date__,
|
|
27
|
+
"commit": _version_generated.__commit_id__,
|
|
28
|
+
"package_version": _version_generated.__package_version__,
|
|
29
|
+
}
|
|
30
|
+
except ImportError:
|
|
31
|
+
# Generated file doesn't exist (development mode)
|
|
32
|
+
return None
|
|
33
|
+
|
|
34
|
+
|
|
35
|
+
def _get_git_commit_id() -> Optional[str]:
|
|
36
|
+
"""Get the current git commit ID (short form) dynamically.
|
|
37
|
+
|
|
38
|
+
This is used as fallback when build-time version is not available.
|
|
39
|
+
|
|
40
|
+
Returns:
|
|
41
|
+
Short commit ID (first 8 characters) or None if not in a git repo.
|
|
42
|
+
"""
|
|
43
|
+
try:
|
|
44
|
+
# Get the directory where this file is located
|
|
45
|
+
repo_dir = Path(__file__).parent.parent.parent
|
|
46
|
+
|
|
47
|
+
result = subprocess.run(
|
|
48
|
+
["git", "rev-parse", "--short=8", "HEAD"],
|
|
49
|
+
cwd=repo_dir,
|
|
50
|
+
capture_output=True,
|
|
51
|
+
text=True,
|
|
52
|
+
timeout=2,
|
|
53
|
+
)
|
|
54
|
+
|
|
55
|
+
if result.returncode == 0:
|
|
56
|
+
return result.stdout.strip()
|
|
57
|
+
return None
|
|
58
|
+
except (subprocess.SubprocessError, FileNotFoundError, subprocess.TimeoutExpired):
|
|
59
|
+
return None
|
|
60
|
+
|
|
61
|
+
|
|
62
|
+
def get_version_string() -> str:
|
|
63
|
+
"""Get version string in format YYYY.MM.DD.COMMIT_ID.
|
|
64
|
+
|
|
65
|
+
Returns:
|
|
66
|
+
Version string like "2025.11.20.c4a8890" or "2025.11.20.unknown" if no version available.
|
|
67
|
+
"""
|
|
68
|
+
# Try build-time version first
|
|
69
|
+
build_version = _load_build_version()
|
|
70
|
+
if build_version:
|
|
71
|
+
return build_version["version"]
|
|
72
|
+
|
|
73
|
+
# Fall back to dynamic git detection (development mode)
|
|
74
|
+
date_str = datetime.now().strftime("%Y.%m.%d")
|
|
75
|
+
commit_id = _get_git_commit_id()
|
|
76
|
+
|
|
77
|
+
if commit_id:
|
|
78
|
+
return f"{date_str}.{commit_id}"
|
|
79
|
+
else:
|
|
80
|
+
return f"{date_str}.unknown"
|
|
81
|
+
|
|
82
|
+
|
|
83
|
+
def get_full_version_info() -> dict:
|
|
84
|
+
"""Get detailed version information.
|
|
85
|
+
|
|
86
|
+
Returns:
|
|
87
|
+
Dictionary with version details.
|
|
88
|
+
"""
|
|
89
|
+
# Try build-time version first
|
|
90
|
+
build_version = _load_build_version()
|
|
91
|
+
if build_version:
|
|
92
|
+
return build_version
|
|
93
|
+
|
|
94
|
+
# Fall back to dynamic detection (development mode)
|
|
95
|
+
commit_id = _get_git_commit_id()
|
|
96
|
+
|
|
97
|
+
return {
|
|
98
|
+
"version": get_version_string(),
|
|
99
|
+
"date": datetime.now().strftime("%Y.%m.%d"),
|
|
100
|
+
"commit": commit_id or "unknown",
|
|
101
|
+
"package_version": "0.5b0", # From pyproject.toml
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
|
|
105
|
+
if __name__ == "__main__":
|
|
106
|
+
print(get_version_string())
|