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,918 @@
|
|
|
1
|
+
"""Node Configuration Manager for manta-node.
|
|
2
|
+
|
|
3
|
+
This module provides a comprehensive configuration management system for manta-node,
|
|
4
|
+
using TOML files for persistent storage and dataclasses for type-safe configuration.
|
|
5
|
+
"""
|
|
6
|
+
|
|
7
|
+
import os
|
|
8
|
+
import threading
|
|
9
|
+
|
|
10
|
+
try:
|
|
11
|
+
import tomllib # Python 3.11+
|
|
12
|
+
except ImportError:
|
|
13
|
+
import tomli as tomllib # Python < 3.11 fallback
|
|
14
|
+
|
|
15
|
+
from dataclasses import asdict, dataclass, field
|
|
16
|
+
from datetime import datetime
|
|
17
|
+
from pathlib import Path
|
|
18
|
+
from typing import Any, Callable, Dict, List, Optional, Union
|
|
19
|
+
|
|
20
|
+
import toml # For writing TOML files
|
|
21
|
+
from manta_common.errors import MantaConfigurationError
|
|
22
|
+
|
|
23
|
+
__all__ = ["NodeConfigManager", "NodeConfiguration"]
|
|
24
|
+
|
|
25
|
+
|
|
26
|
+
# Configuration Section Dataclasses
|
|
27
|
+
|
|
28
|
+
|
|
29
|
+
@dataclass
|
|
30
|
+
class IdentityConfig:
|
|
31
|
+
"""Node identity configuration."""
|
|
32
|
+
|
|
33
|
+
secured_token: str # Required field - must be provided
|
|
34
|
+
alias: Optional[str] = None
|
|
35
|
+
random_id: bool = False
|
|
36
|
+
|
|
37
|
+
|
|
38
|
+
@dataclass
|
|
39
|
+
class NetworkConfig:
|
|
40
|
+
"""Network configuration for node connections."""
|
|
41
|
+
|
|
42
|
+
manager_host: str = "localhost"
|
|
43
|
+
manager_port: int = 50051
|
|
44
|
+
light_service_host: str = "0.0.0.0"
|
|
45
|
+
light_service_port: Optional[int] = None # Auto-assigned if not specified
|
|
46
|
+
|
|
47
|
+
|
|
48
|
+
@dataclass
|
|
49
|
+
class MQTTConfig:
|
|
50
|
+
"""MQTT broker configuration."""
|
|
51
|
+
|
|
52
|
+
ping_interval: int = 1
|
|
53
|
+
reconnect_attempts: int = 5
|
|
54
|
+
reconnect_delay: float = 5.0
|
|
55
|
+
# Runtime populated from manager registration
|
|
56
|
+
broker_host: Optional[str] = None
|
|
57
|
+
broker_port: Optional[int] = None
|
|
58
|
+
topics: List[str] = field(default_factory=list)
|
|
59
|
+
|
|
60
|
+
|
|
61
|
+
@dataclass
|
|
62
|
+
class ContainerConfig:
|
|
63
|
+
"""Container/Docker configuration."""
|
|
64
|
+
|
|
65
|
+
gpu_enabled: Optional[bool] = None # Auto-detect if None
|
|
66
|
+
default_memory_limit: str = "4G"
|
|
67
|
+
default_cpu_limit: str = "2.0"
|
|
68
|
+
docker_network: str = "host"
|
|
69
|
+
runtime: str = "docker" # docker or podman
|
|
70
|
+
pull_timeout: int = 300
|
|
71
|
+
log_config: Dict[str, Any] = field(
|
|
72
|
+
default_factory=lambda: {
|
|
73
|
+
"type": "json-file",
|
|
74
|
+
"config": {"max-size": "10m", "max-file": "3"},
|
|
75
|
+
}
|
|
76
|
+
)
|
|
77
|
+
|
|
78
|
+
|
|
79
|
+
@dataclass
|
|
80
|
+
class DatasetConfig:
|
|
81
|
+
"""Dataset management configuration."""
|
|
82
|
+
|
|
83
|
+
base_path: str = "/data/datasets"
|
|
84
|
+
mount_readonly: bool = True
|
|
85
|
+
mappings: Dict[str, str] = field(default_factory=dict) # name -> path
|
|
86
|
+
cache_metadata: bool = True
|
|
87
|
+
|
|
88
|
+
|
|
89
|
+
@dataclass
|
|
90
|
+
class TaskConfig:
|
|
91
|
+
"""Task execution configuration."""
|
|
92
|
+
|
|
93
|
+
max_concurrent: int = 2
|
|
94
|
+
task_timeout: int = 3600 # seconds
|
|
95
|
+
retry_attempts: int = 3
|
|
96
|
+
temp_folder: Optional[str] = None
|
|
97
|
+
cleanup_on_failure: bool = True
|
|
98
|
+
stream_logs: bool = True
|
|
99
|
+
|
|
100
|
+
|
|
101
|
+
@dataclass
|
|
102
|
+
class ResourceConfig:
|
|
103
|
+
"""Resource management configuration."""
|
|
104
|
+
|
|
105
|
+
reserve_cpu_percent: int = 10
|
|
106
|
+
reserve_memory_mb: int = 512
|
|
107
|
+
max_disk_usage_percent: int = 90
|
|
108
|
+
monitor_interval: int = 30 # seconds
|
|
109
|
+
|
|
110
|
+
|
|
111
|
+
@dataclass
|
|
112
|
+
class LoggingConfig:
|
|
113
|
+
"""Logging configuration."""
|
|
114
|
+
|
|
115
|
+
level: str = "INFO"
|
|
116
|
+
filename: str = "manta_node.log"
|
|
117
|
+
log_to_file: bool = True
|
|
118
|
+
log_to_console: bool = False
|
|
119
|
+
debug_mode: bool = True
|
|
120
|
+
max_file_size: str = "100M"
|
|
121
|
+
backup_count: int = 5
|
|
122
|
+
|
|
123
|
+
|
|
124
|
+
@dataclass
|
|
125
|
+
class SecurityConfig:
|
|
126
|
+
"""Security configuration."""
|
|
127
|
+
|
|
128
|
+
use_tls: bool = False
|
|
129
|
+
cert_folder: str = "~/.manta/certs"
|
|
130
|
+
verify_ssl: bool = True
|
|
131
|
+
token_refresh_interval: int = 3600 # seconds
|
|
132
|
+
|
|
133
|
+
|
|
134
|
+
@dataclass
|
|
135
|
+
class MetadataConfig:
|
|
136
|
+
"""Configuration metadata."""
|
|
137
|
+
|
|
138
|
+
name: str = "default"
|
|
139
|
+
created_at: str = field(default_factory=lambda: datetime.now().isoformat())
|
|
140
|
+
version: str = "1.0.0"
|
|
141
|
+
description: str = ""
|
|
142
|
+
|
|
143
|
+
|
|
144
|
+
@dataclass
|
|
145
|
+
class NodeConfiguration:
|
|
146
|
+
"""Complete node configuration."""
|
|
147
|
+
|
|
148
|
+
identity: IdentityConfig # Required field - must have secured_token
|
|
149
|
+
metadata: MetadataConfig = field(default_factory=MetadataConfig)
|
|
150
|
+
network: NetworkConfig = field(default_factory=NetworkConfig)
|
|
151
|
+
mqtt: MQTTConfig = field(default_factory=MQTTConfig)
|
|
152
|
+
containers: ContainerConfig = field(default_factory=ContainerConfig)
|
|
153
|
+
datasets: DatasetConfig = field(default_factory=DatasetConfig)
|
|
154
|
+
tasks: TaskConfig = field(default_factory=TaskConfig)
|
|
155
|
+
resources: ResourceConfig = field(default_factory=ResourceConfig)
|
|
156
|
+
logging: LoggingConfig = field(default_factory=LoggingConfig)
|
|
157
|
+
security: SecurityConfig = field(default_factory=SecurityConfig)
|
|
158
|
+
|
|
159
|
+
def __post_init__(self):
|
|
160
|
+
"""Post-initialization to set automatic values."""
|
|
161
|
+
# Only set log filename if it's still the default value "manta_node.log"
|
|
162
|
+
# This means it wasn't explicitly set in the configuration
|
|
163
|
+
if self.logging.filename == "manta_node.log":
|
|
164
|
+
# Use alias if available, otherwise use metadata name if not default, else "node"
|
|
165
|
+
if self.identity.alias:
|
|
166
|
+
node_name = self.identity.alias
|
|
167
|
+
elif self.metadata.name and self.metadata.name != "default":
|
|
168
|
+
node_name = self.metadata.name
|
|
169
|
+
else:
|
|
170
|
+
node_name = "node"
|
|
171
|
+
|
|
172
|
+
# Clean the name to be filesystem-safe
|
|
173
|
+
safe_name = "".join(
|
|
174
|
+
c if c.isalnum() or c in "-_" else "_" for c in node_name
|
|
175
|
+
)
|
|
176
|
+
# Set to centralized logs directory with nodes subfolder
|
|
177
|
+
self.logging.filename = str(
|
|
178
|
+
Path.home() / ".manta" / "logs" / "nodes" / f"{safe_name}.log"
|
|
179
|
+
)
|
|
180
|
+
|
|
181
|
+
def to_dict(self) -> Dict[str, Any]:
|
|
182
|
+
"""Convert configuration to dictionary."""
|
|
183
|
+
return asdict(self)
|
|
184
|
+
|
|
185
|
+
@classmethod
|
|
186
|
+
def from_dict(cls, data: Dict[str, Any]) -> "NodeConfiguration":
|
|
187
|
+
"""Create configuration from dictionary."""
|
|
188
|
+
# For identity, we need to ensure secured_token is present
|
|
189
|
+
identity_data = data.get("identity", {})
|
|
190
|
+
if "secured_token" not in identity_data:
|
|
191
|
+
raise ValueError("secured_token is required in identity configuration")
|
|
192
|
+
|
|
193
|
+
# Create identity config (required)
|
|
194
|
+
identity = IdentityConfig(**identity_data)
|
|
195
|
+
|
|
196
|
+
# Create configuration with required identity
|
|
197
|
+
config = cls(identity=identity)
|
|
198
|
+
|
|
199
|
+
# Update each section if present
|
|
200
|
+
if "metadata" in data:
|
|
201
|
+
config.metadata = MetadataConfig(**data["metadata"])
|
|
202
|
+
if "network" in data:
|
|
203
|
+
config.network = NetworkConfig(**data["network"])
|
|
204
|
+
if "mqtt" in data:
|
|
205
|
+
config.mqtt = MQTTConfig(**data["mqtt"])
|
|
206
|
+
if "containers" in data:
|
|
207
|
+
config.containers = ContainerConfig(**data["containers"])
|
|
208
|
+
if "datasets" in data:
|
|
209
|
+
# Handle nested mappings
|
|
210
|
+
datasets_data = data["datasets"].copy()
|
|
211
|
+
if "mappings" not in datasets_data:
|
|
212
|
+
datasets_data["mappings"] = {}
|
|
213
|
+
config.datasets = DatasetConfig(**datasets_data)
|
|
214
|
+
if "tasks" in data:
|
|
215
|
+
config.tasks = TaskConfig(**data["tasks"])
|
|
216
|
+
if "resources" in data:
|
|
217
|
+
config.resources = ResourceConfig(**data["resources"])
|
|
218
|
+
if "logging" in data:
|
|
219
|
+
config.logging = LoggingConfig(**data["logging"])
|
|
220
|
+
if "security" in data:
|
|
221
|
+
config.security = SecurityConfig(**data["security"])
|
|
222
|
+
|
|
223
|
+
return config
|
|
224
|
+
|
|
225
|
+
|
|
226
|
+
class NodeConfigManager:
|
|
227
|
+
"""Configuration manager for manta-node.
|
|
228
|
+
|
|
229
|
+
Manages loading, saving, and runtime updates of node configuration.
|
|
230
|
+
Configuration is stored in TOML format in ~/.manta/nodes/ directory.
|
|
231
|
+
"""
|
|
232
|
+
|
|
233
|
+
# Runtime update categories
|
|
234
|
+
SAFE_RUNTIME_UPDATES = [
|
|
235
|
+
"logging", # Can change log levels
|
|
236
|
+
"datasets.mappings", # Can add/remove datasets
|
|
237
|
+
"tasks.max_concurrent", # Can adjust concurrency
|
|
238
|
+
"resources", # Can adjust resource limits
|
|
239
|
+
]
|
|
240
|
+
|
|
241
|
+
REQUIRES_RESTART = [
|
|
242
|
+
"identity", # Node ID is immutable
|
|
243
|
+
"network", # Port changes need restart
|
|
244
|
+
"mqtt.broker_host", # MQTT connection needs restart
|
|
245
|
+
"mqtt.broker_port",
|
|
246
|
+
"security", # Certificate changes need reconnect
|
|
247
|
+
"containers.runtime", # Runtime change needs restart
|
|
248
|
+
]
|
|
249
|
+
|
|
250
|
+
def __init__(self, config_path: Optional[Union[str, Path]] = None):
|
|
251
|
+
"""Initialize configuration manager.
|
|
252
|
+
|
|
253
|
+
Args:
|
|
254
|
+
config_path: Path to configuration file or None for default
|
|
255
|
+
"""
|
|
256
|
+
self._lock = threading.RLock()
|
|
257
|
+
self._observers: List[Callable] = []
|
|
258
|
+
self._config = NodeConfiguration(IdentityConfig(secured_token="<REQUIRED>"))
|
|
259
|
+
self._instances: List[Any] = [] # Track running instances
|
|
260
|
+
|
|
261
|
+
# Determine config path
|
|
262
|
+
if config_path:
|
|
263
|
+
self._config_path = Path(config_path)
|
|
264
|
+
else:
|
|
265
|
+
self._config_path = self._get_default_config_path()
|
|
266
|
+
|
|
267
|
+
# Ensure config directory exists
|
|
268
|
+
self._ensure_config_dir()
|
|
269
|
+
|
|
270
|
+
# Load configuration if file exists, otherwise create default template
|
|
271
|
+
if self._config_path.exists():
|
|
272
|
+
self.load()
|
|
273
|
+
else:
|
|
274
|
+
# Create a default template configuration
|
|
275
|
+
self._create_default_config()
|
|
276
|
+
|
|
277
|
+
@property
|
|
278
|
+
def config(self) -> NodeConfiguration:
|
|
279
|
+
"""Get current configuration."""
|
|
280
|
+
with self._lock:
|
|
281
|
+
return self._config
|
|
282
|
+
|
|
283
|
+
@property
|
|
284
|
+
def current_config(self) -> NodeConfiguration:
|
|
285
|
+
"""Get current configuration (alias for config property)."""
|
|
286
|
+
return self.config
|
|
287
|
+
|
|
288
|
+
@current_config.setter
|
|
289
|
+
def current_config(self, config_dict: Union[Dict[str, Any], NodeConfiguration]):
|
|
290
|
+
"""Set current configuration from dictionary or NodeConfiguration.
|
|
291
|
+
|
|
292
|
+
Args:
|
|
293
|
+
config_dict: Configuration as dict or NodeConfiguration instance
|
|
294
|
+
"""
|
|
295
|
+
with self._lock:
|
|
296
|
+
if isinstance(config_dict, NodeConfiguration):
|
|
297
|
+
self._config = config_dict
|
|
298
|
+
elif isinstance(config_dict, dict):
|
|
299
|
+
# Handle legacy format with 'manager' key
|
|
300
|
+
if "manager" in config_dict:
|
|
301
|
+
manager_config = config_dict["manager"]
|
|
302
|
+
if "host" in manager_config:
|
|
303
|
+
self._config.network.manager_host = manager_config["host"]
|
|
304
|
+
if "port" in manager_config:
|
|
305
|
+
self._config.network.manager_port = manager_config["port"]
|
|
306
|
+
if "token" in manager_config:
|
|
307
|
+
self._config.identity.secured_token = manager_config["token"]
|
|
308
|
+
else:
|
|
309
|
+
# Handle the standard configuration format
|
|
310
|
+
self._config = NodeConfiguration.from_dict(config_dict)
|
|
311
|
+
|
|
312
|
+
@property
|
|
313
|
+
def config_path(self) -> Path:
|
|
314
|
+
"""Get configuration file path."""
|
|
315
|
+
return self._config_path
|
|
316
|
+
|
|
317
|
+
def _get_default_config_path(self) -> Path:
|
|
318
|
+
"""Get default configuration path."""
|
|
319
|
+
config_dir = Path.home() / ".manta" / "nodes"
|
|
320
|
+
return config_dir / "default.toml"
|
|
321
|
+
|
|
322
|
+
def _ensure_config_dir(self):
|
|
323
|
+
"""Ensure configuration directory exists."""
|
|
324
|
+
config_dir = self._config_path.parent
|
|
325
|
+
config_dir.mkdir(parents=True, exist_ok=True)
|
|
326
|
+
|
|
327
|
+
def _create_default_config(self):
|
|
328
|
+
"""Create a default configuration template file."""
|
|
329
|
+
# Create a template configuration with placeholder token
|
|
330
|
+
template_config = NodeConfiguration(
|
|
331
|
+
identity=IdentityConfig(secured_token="<YOUR_JWT_TOKEN_HERE>")
|
|
332
|
+
)
|
|
333
|
+
|
|
334
|
+
# Set some helpful defaults
|
|
335
|
+
template_config.metadata.name = "default"
|
|
336
|
+
template_config.metadata.description = (
|
|
337
|
+
"Default node configuration template - Please set your secured_token"
|
|
338
|
+
)
|
|
339
|
+
|
|
340
|
+
# Save the template to disk
|
|
341
|
+
self._config = template_config
|
|
342
|
+
self.save()
|
|
343
|
+
|
|
344
|
+
# Print helpful message to inform user
|
|
345
|
+
print(f"📝 Created default configuration template at: {self._config_path}")
|
|
346
|
+
print("")
|
|
347
|
+
print("⚠️ IMPORTANT: Please set your secured_token before starting the node!")
|
|
348
|
+
print("")
|
|
349
|
+
print("Next steps:")
|
|
350
|
+
print(" 1. Get your JWT token from the manta manager")
|
|
351
|
+
print(f" 2. Edit the config file: {self._config_path}")
|
|
352
|
+
print(" 3. Replace '<YOUR_JWT_TOKEN_HERE>' with your actual token")
|
|
353
|
+
print(
|
|
354
|
+
" 4. Or run interactive setup: manta_node config init <path/to/config.yaml>"
|
|
355
|
+
)
|
|
356
|
+
print("")
|
|
357
|
+
|
|
358
|
+
def _expand_env_vars(self, value: Any) -> Any:
|
|
359
|
+
"""Expand environment variables in configuration values.
|
|
360
|
+
|
|
361
|
+
Supports ${VAR_NAME} and ${VAR_NAME:default} syntax.
|
|
362
|
+
"""
|
|
363
|
+
if isinstance(value, str) and value.startswith("${") and value.endswith("}"):
|
|
364
|
+
var_expr = value[2:-1]
|
|
365
|
+
if ":" in var_expr:
|
|
366
|
+
var_name, default = var_expr.split(":", 1)
|
|
367
|
+
else:
|
|
368
|
+
var_name = var_expr
|
|
369
|
+
default = None
|
|
370
|
+
|
|
371
|
+
result = os.environ.get(var_name, default)
|
|
372
|
+
if result is None:
|
|
373
|
+
raise MantaConfigurationError(
|
|
374
|
+
f"Environment variable {var_name} not found and no default provided"
|
|
375
|
+
)
|
|
376
|
+
return result
|
|
377
|
+
elif isinstance(value, dict):
|
|
378
|
+
return {k: self._expand_env_vars(v) for k, v in value.items()}
|
|
379
|
+
elif isinstance(value, list):
|
|
380
|
+
return [self._expand_env_vars(item) for item in value]
|
|
381
|
+
return value
|
|
382
|
+
|
|
383
|
+
def _expand_paths(self, value: Any) -> Any:
|
|
384
|
+
"""Expand tilde (~) and environment variables in path strings.
|
|
385
|
+
|
|
386
|
+
This method recursively processes configuration values and expands:
|
|
387
|
+
- Tilde (~) to home directory
|
|
388
|
+
- Environment variables in ${VAR} format
|
|
389
|
+
|
|
390
|
+
Args:
|
|
391
|
+
value: Configuration value to process (can be string, dict, list, etc.)
|
|
392
|
+
|
|
393
|
+
Returns:
|
|
394
|
+
Processed value with expanded paths
|
|
395
|
+
"""
|
|
396
|
+
if isinstance(value, str):
|
|
397
|
+
# First expand environment variables if present
|
|
398
|
+
if value.startswith("${") and value.endswith("}"):
|
|
399
|
+
value = self._expand_env_vars(value)
|
|
400
|
+
# Then expand tilde to home directory
|
|
401
|
+
if value.startswith("~"):
|
|
402
|
+
return str(Path(value).expanduser())
|
|
403
|
+
return value
|
|
404
|
+
elif isinstance(value, dict):
|
|
405
|
+
return {k: self._expand_paths(v) for k, v in value.items()}
|
|
406
|
+
elif isinstance(value, list):
|
|
407
|
+
return [self._expand_paths(item) for item in value]
|
|
408
|
+
return value
|
|
409
|
+
|
|
410
|
+
def load(self, config_path: Optional[Union[str, Path]] = None) -> NodeConfiguration:
|
|
411
|
+
"""Load configuration from TOML file.
|
|
412
|
+
|
|
413
|
+
Args:
|
|
414
|
+
config_path: Optional path to load from (uses default if None)
|
|
415
|
+
|
|
416
|
+
Returns:
|
|
417
|
+
Loaded configuration
|
|
418
|
+
|
|
419
|
+
Raises:
|
|
420
|
+
MantaConfigurationError: If configuration is invalid
|
|
421
|
+
"""
|
|
422
|
+
path = Path(config_path) if config_path else self._config_path
|
|
423
|
+
|
|
424
|
+
if not path.exists():
|
|
425
|
+
raise MantaConfigurationError(f"Configuration file not found: {path}")
|
|
426
|
+
|
|
427
|
+
try:
|
|
428
|
+
with open(path, "rb") as f:
|
|
429
|
+
data = tomllib.load(f)
|
|
430
|
+
|
|
431
|
+
# Expand environment variables
|
|
432
|
+
data = self._expand_env_vars(data)
|
|
433
|
+
|
|
434
|
+
# Expand paths (tilde to home directory)
|
|
435
|
+
data = self._expand_paths(data)
|
|
436
|
+
|
|
437
|
+
# Create configuration from data
|
|
438
|
+
with self._lock:
|
|
439
|
+
self._config = NodeConfiguration.from_dict(data)
|
|
440
|
+
if config_path:
|
|
441
|
+
self._config_path = path
|
|
442
|
+
|
|
443
|
+
# Notify observers
|
|
444
|
+
self._notify_observers({"action": "load", "path": str(path)})
|
|
445
|
+
|
|
446
|
+
return self._config
|
|
447
|
+
|
|
448
|
+
except Exception as e:
|
|
449
|
+
raise MantaConfigurationError(f"Failed to load configuration: {e}")
|
|
450
|
+
|
|
451
|
+
def save(self, config_path: Optional[Union[str, Path]] = None) -> bool:
|
|
452
|
+
"""Save configuration to TOML file.
|
|
453
|
+
|
|
454
|
+
Args:
|
|
455
|
+
config_path: Optional path to save to (uses default if None)
|
|
456
|
+
|
|
457
|
+
Returns:
|
|
458
|
+
True if successful
|
|
459
|
+
|
|
460
|
+
Raises:
|
|
461
|
+
MantaConfigurationError: If save fails
|
|
462
|
+
"""
|
|
463
|
+
path = Path(config_path) if config_path else self._config_path
|
|
464
|
+
|
|
465
|
+
# Ensure directory exists
|
|
466
|
+
path.parent.mkdir(parents=True, exist_ok=True)
|
|
467
|
+
|
|
468
|
+
try:
|
|
469
|
+
with self._lock:
|
|
470
|
+
data = self._config.to_dict()
|
|
471
|
+
|
|
472
|
+
# Write TOML file
|
|
473
|
+
with open(path, "w") as f:
|
|
474
|
+
toml.dump(data, f)
|
|
475
|
+
|
|
476
|
+
# Update config path if different
|
|
477
|
+
if config_path:
|
|
478
|
+
self._config_path = path
|
|
479
|
+
|
|
480
|
+
# Notify observers
|
|
481
|
+
self._notify_observers({"action": "save", "path": str(path)})
|
|
482
|
+
|
|
483
|
+
return True
|
|
484
|
+
|
|
485
|
+
except Exception as e:
|
|
486
|
+
raise MantaConfigurationError(f"Failed to save configuration: {e}")
|
|
487
|
+
|
|
488
|
+
def update(
|
|
489
|
+
self, updates: Dict[str, Any], allow_restart_required: bool = False
|
|
490
|
+
) -> bool:
|
|
491
|
+
"""Update configuration with validation.
|
|
492
|
+
|
|
493
|
+
Args:
|
|
494
|
+
updates: Dictionary of updates (can be nested)
|
|
495
|
+
allow_restart_required: Allow updates that require restart
|
|
496
|
+
|
|
497
|
+
Returns:
|
|
498
|
+
True if update successful
|
|
499
|
+
|
|
500
|
+
Raises:
|
|
501
|
+
MantaConfigurationError: If update is invalid or requires restart
|
|
502
|
+
"""
|
|
503
|
+
# Check if updates require restart
|
|
504
|
+
if not allow_restart_required:
|
|
505
|
+
for key in updates:
|
|
506
|
+
if any(
|
|
507
|
+
key.startswith(restricted) for restricted in self.REQUIRES_RESTART
|
|
508
|
+
):
|
|
509
|
+
raise MantaConfigurationError(
|
|
510
|
+
f"Update to '{key}' requires node restart. "
|
|
511
|
+
"Set allow_restart_required=True to proceed."
|
|
512
|
+
)
|
|
513
|
+
|
|
514
|
+
try:
|
|
515
|
+
with self._lock:
|
|
516
|
+
# Apply updates to configuration
|
|
517
|
+
for key, value in updates.items():
|
|
518
|
+
self._apply_update(key, value)
|
|
519
|
+
|
|
520
|
+
# Save updated configuration
|
|
521
|
+
self.save()
|
|
522
|
+
|
|
523
|
+
# Notify observers
|
|
524
|
+
self._notify_observers({"action": "update", "updates": updates})
|
|
525
|
+
|
|
526
|
+
return True
|
|
527
|
+
|
|
528
|
+
except Exception as e:
|
|
529
|
+
raise MantaConfigurationError(f"Failed to update configuration: {e}")
|
|
530
|
+
|
|
531
|
+
def _apply_update(self, key: str, value: Any):
|
|
532
|
+
"""Apply a single update to configuration.
|
|
533
|
+
|
|
534
|
+
Args:
|
|
535
|
+
key: Dot-separated path to configuration field
|
|
536
|
+
value: New value to set
|
|
537
|
+
"""
|
|
538
|
+
parts = key.split(".")
|
|
539
|
+
target = self._config
|
|
540
|
+
|
|
541
|
+
# Navigate to the target field
|
|
542
|
+
for part in parts[:-1]:
|
|
543
|
+
target = getattr(target, part)
|
|
544
|
+
|
|
545
|
+
# Set the value
|
|
546
|
+
setattr(target, parts[-1], value)
|
|
547
|
+
|
|
548
|
+
def validate(self) -> List[str]:
|
|
549
|
+
"""Validate configuration.
|
|
550
|
+
|
|
551
|
+
Returns:
|
|
552
|
+
List of validation issues (empty if valid)
|
|
553
|
+
"""
|
|
554
|
+
issues = []
|
|
555
|
+
|
|
556
|
+
# Check required fields
|
|
557
|
+
if (
|
|
558
|
+
not self._config.identity.secured_token
|
|
559
|
+
or self._config.identity.secured_token.startswith("<")
|
|
560
|
+
):
|
|
561
|
+
issues.append(
|
|
562
|
+
"identity.secured_token is required (cannot be empty or placeholder)"
|
|
563
|
+
)
|
|
564
|
+
issues.append(" → Set your JWT token in the configuration file")
|
|
565
|
+
issues.append(" → Or run: manta_node config init <path/to/config.yaml>")
|
|
566
|
+
|
|
567
|
+
# Validate network ports
|
|
568
|
+
if not (443 <= self._config.network.manager_port <= 65535):
|
|
569
|
+
issues.append(
|
|
570
|
+
f"network.manager_port ({self._config.network.manager_port}) must be between 443 and 65535"
|
|
571
|
+
)
|
|
572
|
+
if self._config.network.light_service_port is not None:
|
|
573
|
+
if not (1024 <= self._config.network.light_service_port <= 65535):
|
|
574
|
+
issues.append(
|
|
575
|
+
f"network.light_service_port ({self._config.network.light_service_port}) must be between 1024 and 65535"
|
|
576
|
+
)
|
|
577
|
+
|
|
578
|
+
# Validate dataset paths
|
|
579
|
+
for name, path in self._config.datasets.mappings.items():
|
|
580
|
+
dataset_path = Path(path).expanduser()
|
|
581
|
+
if not dataset_path.exists():
|
|
582
|
+
issues.append(f"Dataset path does not exist: {name} -> {path}")
|
|
583
|
+
|
|
584
|
+
# Validate resource limits
|
|
585
|
+
if self._config.resources.reserve_cpu_percent > 50:
|
|
586
|
+
issues.append("resources.reserve_cpu_percent should not exceed 50%")
|
|
587
|
+
|
|
588
|
+
return issues
|
|
589
|
+
|
|
590
|
+
def register_observer(self, callback: Callable[[Dict[str, Any]], None]):
|
|
591
|
+
"""Register observer for configuration changes.
|
|
592
|
+
|
|
593
|
+
Args:
|
|
594
|
+
callback: Function to call on configuration changes
|
|
595
|
+
"""
|
|
596
|
+
self._observers.append(callback)
|
|
597
|
+
|
|
598
|
+
def _notify_observers(self, change: Dict[str, Any]):
|
|
599
|
+
"""Notify all observers of configuration change.
|
|
600
|
+
|
|
601
|
+
Args:
|
|
602
|
+
change: Dictionary describing the change
|
|
603
|
+
"""
|
|
604
|
+
for observer in self._observers:
|
|
605
|
+
try:
|
|
606
|
+
observer(change)
|
|
607
|
+
except Exception:
|
|
608
|
+
pass # Ignore observer errors
|
|
609
|
+
|
|
610
|
+
def list_available_configs(self) -> List[str]:
|
|
611
|
+
"""List available configuration files.
|
|
612
|
+
|
|
613
|
+
Returns:
|
|
614
|
+
List of configuration names
|
|
615
|
+
"""
|
|
616
|
+
config_dir = Path.home() / ".manta" / "nodes"
|
|
617
|
+
if not config_dir.exists():
|
|
618
|
+
return []
|
|
619
|
+
|
|
620
|
+
configs = []
|
|
621
|
+
for path in config_dir.glob("*.toml"):
|
|
622
|
+
configs.append(path.stem)
|
|
623
|
+
|
|
624
|
+
return sorted(configs)
|
|
625
|
+
|
|
626
|
+
def get_node_config(self, config_name: str) -> Optional[NodeConfiguration]:
|
|
627
|
+
"""Load and return a specific node configuration by name.
|
|
628
|
+
|
|
629
|
+
Args:
|
|
630
|
+
config_name: Name of the configuration to load
|
|
631
|
+
|
|
632
|
+
Returns:
|
|
633
|
+
NodeConfiguration if found and valid, None otherwise
|
|
634
|
+
"""
|
|
635
|
+
# Construct config path
|
|
636
|
+
config_dir = Path.home() / ".manta" / "nodes"
|
|
637
|
+
config_path = config_dir / f"{config_name}.toml"
|
|
638
|
+
|
|
639
|
+
# Check if config exists
|
|
640
|
+
if not config_path.exists():
|
|
641
|
+
return None
|
|
642
|
+
|
|
643
|
+
# Load the configuration
|
|
644
|
+
try:
|
|
645
|
+
temp_manager = NodeConfigManager(config_path)
|
|
646
|
+
return temp_manager.config
|
|
647
|
+
except Exception:
|
|
648
|
+
return None
|
|
649
|
+
|
|
650
|
+
def export_template(self, path: Optional[Union[str, Path]] = None) -> str:
|
|
651
|
+
"""Export a configuration template.
|
|
652
|
+
|
|
653
|
+
Args:
|
|
654
|
+
path: Optional path to save template
|
|
655
|
+
|
|
656
|
+
Returns:
|
|
657
|
+
Template content as string
|
|
658
|
+
"""
|
|
659
|
+
template = NodeConfiguration(IdentityConfig("token"))
|
|
660
|
+
template.metadata.name = "template"
|
|
661
|
+
template.metadata.description = "Template configuration for manta-node"
|
|
662
|
+
|
|
663
|
+
content = toml.dumps(template.to_dict())
|
|
664
|
+
|
|
665
|
+
if path:
|
|
666
|
+
path = Path(path)
|
|
667
|
+
path.parent.mkdir(parents=True, exist_ok=True)
|
|
668
|
+
with open(path, "w") as f:
|
|
669
|
+
f.write(content)
|
|
670
|
+
|
|
671
|
+
return content
|
|
672
|
+
|
|
673
|
+
def set_manager_config(self, host: str, port: int, token: str) -> bool:
|
|
674
|
+
"""Set manager configuration (host, port, token).
|
|
675
|
+
|
|
676
|
+
Args:
|
|
677
|
+
host: Manager hostname
|
|
678
|
+
port: Manager port
|
|
679
|
+
token: JWT token
|
|
680
|
+
|
|
681
|
+
Returns:
|
|
682
|
+
True if configuration was set successfully
|
|
683
|
+
"""
|
|
684
|
+
try:
|
|
685
|
+
with self._lock:
|
|
686
|
+
self._config.network.manager_host = host
|
|
687
|
+
self._config.network.manager_port = port
|
|
688
|
+
self._config.identity.secured_token = token
|
|
689
|
+
|
|
690
|
+
# Notify observers
|
|
691
|
+
self._notify_observers(
|
|
692
|
+
{
|
|
693
|
+
"action": "update",
|
|
694
|
+
"updates": {
|
|
695
|
+
"network.manager_host": host,
|
|
696
|
+
"network.manager_port": port,
|
|
697
|
+
"identity.secured_token": token,
|
|
698
|
+
},
|
|
699
|
+
}
|
|
700
|
+
)
|
|
701
|
+
|
|
702
|
+
return True
|
|
703
|
+
|
|
704
|
+
except Exception as e:
|
|
705
|
+
raise MantaConfigurationError(f"Failed to set manager configuration: {e}")
|
|
706
|
+
|
|
707
|
+
def get_manager_config(self) -> Dict[str, Any]:
|
|
708
|
+
"""Get manager configuration as dictionary.
|
|
709
|
+
|
|
710
|
+
Returns:
|
|
711
|
+
Dictionary with manager configuration
|
|
712
|
+
"""
|
|
713
|
+
with self._lock:
|
|
714
|
+
return {
|
|
715
|
+
"host": self._config.network.manager_host,
|
|
716
|
+
"port": self._config.network.manager_port,
|
|
717
|
+
"token": self._config.identity.secured_token,
|
|
718
|
+
}
|
|
719
|
+
|
|
720
|
+
def reset_to_defaults(self) -> bool:
|
|
721
|
+
"""Reset configuration to default values.
|
|
722
|
+
|
|
723
|
+
Returns:
|
|
724
|
+
True if reset was successful
|
|
725
|
+
"""
|
|
726
|
+
try:
|
|
727
|
+
with self._lock:
|
|
728
|
+
self._config = NodeConfiguration(IdentityConfig("token"))
|
|
729
|
+
|
|
730
|
+
# Notify observers
|
|
731
|
+
self._notify_observers({"action": "reset"})
|
|
732
|
+
|
|
733
|
+
return True
|
|
734
|
+
|
|
735
|
+
except Exception as e:
|
|
736
|
+
raise MantaConfigurationError(f"Failed to reset configuration: {e}")
|
|
737
|
+
|
|
738
|
+
def is_using_config_file(self) -> bool:
|
|
739
|
+
"""Check if a configuration file exists and is being used.
|
|
740
|
+
|
|
741
|
+
Returns:
|
|
742
|
+
True if configuration file exists and is loaded
|
|
743
|
+
"""
|
|
744
|
+
return self._config_path.exists()
|
|
745
|
+
|
|
746
|
+
def save_node_config(self, config) -> bool:
|
|
747
|
+
"""Save a node configuration (compatibility method).
|
|
748
|
+
|
|
749
|
+
Args:
|
|
750
|
+
config: NodeConfig (old format) or NodeConfiguration object
|
|
751
|
+
|
|
752
|
+
Returns:
|
|
753
|
+
bool: True if saved successfully, False otherwise
|
|
754
|
+
"""
|
|
755
|
+
try:
|
|
756
|
+
# Handle old NodeConfig format by converting to NodeConfiguration
|
|
757
|
+
if hasattr(config, "to_node_configuration"):
|
|
758
|
+
node_config = config.to_node_configuration()
|
|
759
|
+
else:
|
|
760
|
+
node_config = config
|
|
761
|
+
|
|
762
|
+
# Set as current config and save
|
|
763
|
+
with self._lock:
|
|
764
|
+
self._config = node_config
|
|
765
|
+
|
|
766
|
+
return self.save()
|
|
767
|
+
except Exception:
|
|
768
|
+
return False
|
|
769
|
+
|
|
770
|
+
def create_node_config_from_template(self, config_name: str, template: str):
|
|
771
|
+
"""Create node configuration from template (compatibility method).
|
|
772
|
+
|
|
773
|
+
Args:
|
|
774
|
+
config_name: Name of the configuration
|
|
775
|
+
template: Template type ('basic', 'gpu', 'cpu')
|
|
776
|
+
|
|
777
|
+
Returns:
|
|
778
|
+
NodeConfig (old format) for compatibility
|
|
779
|
+
"""
|
|
780
|
+
# Define minimal local classes for backward compatibility
|
|
781
|
+
from dataclasses import dataclass
|
|
782
|
+
|
|
783
|
+
@dataclass
|
|
784
|
+
class ConnectionConfig:
|
|
785
|
+
host: str
|
|
786
|
+
port: int
|
|
787
|
+
use_tls: bool
|
|
788
|
+
|
|
789
|
+
@dataclass
|
|
790
|
+
class HardwareConfig:
|
|
791
|
+
gpu_enabled: Optional[bool]
|
|
792
|
+
max_concurrent_tasks: int
|
|
793
|
+
cpu_limit: str
|
|
794
|
+
memory_limit: str
|
|
795
|
+
|
|
796
|
+
@dataclass
|
|
797
|
+
class NodeConfig:
|
|
798
|
+
name: str
|
|
799
|
+
alias: str
|
|
800
|
+
connection: ConnectionConfig
|
|
801
|
+
hardware: HardwareConfig
|
|
802
|
+
datasets: Optional[Any]
|
|
803
|
+
auto_register: bool
|
|
804
|
+
random_id: bool
|
|
805
|
+
|
|
806
|
+
# Template configurations
|
|
807
|
+
templates = {
|
|
808
|
+
"basic": {
|
|
809
|
+
"gpu_enabled": None, # Auto-detect
|
|
810
|
+
"max_concurrent_tasks": 2,
|
|
811
|
+
"cpu_limit": "2.0",
|
|
812
|
+
"memory_limit": "4G",
|
|
813
|
+
},
|
|
814
|
+
"gpu": {
|
|
815
|
+
"gpu_enabled": True,
|
|
816
|
+
"max_concurrent_tasks": 2,
|
|
817
|
+
"cpu_limit": "4.0",
|
|
818
|
+
"memory_limit": "8G",
|
|
819
|
+
},
|
|
820
|
+
"cpu": {
|
|
821
|
+
"gpu_enabled": False,
|
|
822
|
+
"max_concurrent_tasks": 4,
|
|
823
|
+
"cpu_limit": "4.0",
|
|
824
|
+
"memory_limit": "4G",
|
|
825
|
+
},
|
|
826
|
+
}
|
|
827
|
+
|
|
828
|
+
template_config = templates.get(template, templates["basic"])
|
|
829
|
+
|
|
830
|
+
# Create hardware config
|
|
831
|
+
hardware = HardwareConfig(
|
|
832
|
+
gpu_enabled=template_config["gpu_enabled"],
|
|
833
|
+
max_concurrent_tasks=template_config["max_concurrent_tasks"],
|
|
834
|
+
cpu_limit=template_config["cpu_limit"],
|
|
835
|
+
memory_limit=template_config["memory_limit"],
|
|
836
|
+
)
|
|
837
|
+
|
|
838
|
+
# Create connection config with defaults
|
|
839
|
+
connection = ConnectionConfig(host="localhost", port=50051, use_tls=False)
|
|
840
|
+
|
|
841
|
+
# Create NodeConfig with template settings
|
|
842
|
+
return NodeConfig(
|
|
843
|
+
name=config_name,
|
|
844
|
+
alias=config_name, # Default alias same as name
|
|
845
|
+
connection=connection,
|
|
846
|
+
hardware=hardware,
|
|
847
|
+
datasets=None,
|
|
848
|
+
auto_register=True,
|
|
849
|
+
random_id=False,
|
|
850
|
+
)
|
|
851
|
+
|
|
852
|
+
def list_running_instances(self) -> List[Any]:
|
|
853
|
+
"""List all running node instances.
|
|
854
|
+
|
|
855
|
+
Returns:
|
|
856
|
+
List of running node instances
|
|
857
|
+
"""
|
|
858
|
+
return self._instances.copy()
|
|
859
|
+
|
|
860
|
+
def save_instance(self, instance: Any) -> None:
|
|
861
|
+
"""Save a running node instance.
|
|
862
|
+
|
|
863
|
+
Args:
|
|
864
|
+
instance: The node instance to save
|
|
865
|
+
"""
|
|
866
|
+
self._instances.append(instance)
|
|
867
|
+
|
|
868
|
+
def remove_instance(self, instance_id: str) -> bool:
|
|
869
|
+
"""Remove a running node instance.
|
|
870
|
+
|
|
871
|
+
Args:
|
|
872
|
+
instance_id: The ID of the instance to remove
|
|
873
|
+
|
|
874
|
+
Returns:
|
|
875
|
+
True if instance was removed, False otherwise
|
|
876
|
+
"""
|
|
877
|
+
for i, inst in enumerate(self._instances):
|
|
878
|
+
if hasattr(inst, "instance_id") and inst.instance_id == instance_id:
|
|
879
|
+
del self._instances[i]
|
|
880
|
+
return True
|
|
881
|
+
return False
|
|
882
|
+
|
|
883
|
+
def list_node_configs(self) -> List[Any]:
|
|
884
|
+
"""List all available node configurations.
|
|
885
|
+
|
|
886
|
+
Returns:
|
|
887
|
+
List of node configurations with their metadata
|
|
888
|
+
"""
|
|
889
|
+
configs = []
|
|
890
|
+
config_dir = Path.home() / ".manta" / "nodes"
|
|
891
|
+
|
|
892
|
+
if not config_dir.exists():
|
|
893
|
+
return configs
|
|
894
|
+
|
|
895
|
+
# Iterate through all .toml files
|
|
896
|
+
for config_file in config_dir.glob("*.toml"):
|
|
897
|
+
try:
|
|
898
|
+
# Try to load and parse each config
|
|
899
|
+
temp_manager = NodeConfigManager(config_file)
|
|
900
|
+
if temp_manager.config:
|
|
901
|
+
# Create a simple config object with name and metadata
|
|
902
|
+
config_info = type(
|
|
903
|
+
"NodeConfigInfo",
|
|
904
|
+
(),
|
|
905
|
+
{
|
|
906
|
+
"name": config_file.stem,
|
|
907
|
+
"path": str(config_file),
|
|
908
|
+
"metadata": temp_manager.config.metadata,
|
|
909
|
+
"identity": temp_manager.config.identity,
|
|
910
|
+
"network": temp_manager.config.network,
|
|
911
|
+
},
|
|
912
|
+
)()
|
|
913
|
+
configs.append(config_info)
|
|
914
|
+
except Exception:
|
|
915
|
+
# Skip invalid configs
|
|
916
|
+
continue
|
|
917
|
+
|
|
918
|
+
return configs
|