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.
Files changed (42) hide show
  1. manta_node/__init__.py +5 -0
  2. manta_node/__main__.py +57 -0
  3. manta_node/cli/__init__.py +6 -0
  4. manta_node/cli/commands/__init__.py +21 -0
  5. manta_node/cli/commands/cluster.py +419 -0
  6. manta_node/cli/commands/config.py +490 -0
  7. manta_node/cli/commands/logs.py +168 -0
  8. manta_node/cli/commands/start.py +459 -0
  9. manta_node/cli/commands/status.py +204 -0
  10. manta_node/cli/commands/stop.py +253 -0
  11. manta_node/cli/config_manager.py +139 -0
  12. manta_node/cli/main.py +133 -0
  13. manta_node/cli/version.py +106 -0
  14. manta_node/domain/__init__.py +8 -0
  15. manta_node/domain/task_lifecycle.py +83 -0
  16. manta_node/infrastructure/__init__.py +13 -0
  17. manta_node/infrastructure/config/__init__.py +31 -0
  18. manta_node/infrastructure/config/node_config_manager.py +918 -0
  19. manta_node/infrastructure/container/__init__.py +9 -0
  20. manta_node/infrastructure/container/docker_adapter.py +253 -0
  21. manta_node/infrastructure/container/manager.py +536 -0
  22. manta_node/infrastructure/filesystem/__init__.py +7 -0
  23. manta_node/infrastructure/filesystem/dataset_manager.py +469 -0
  24. manta_node/infrastructure/grpc/__init__.py +11 -0
  25. manta_node/infrastructure/grpc/client.py +373 -0
  26. manta_node/infrastructure/grpc/local_servicer.py +151 -0
  27. manta_node/infrastructure/grpc/world_servicer.py +284 -0
  28. manta_node/infrastructure/metrics/__init__.py +10 -0
  29. manta_node/infrastructure/metrics/collector.py +94 -0
  30. manta_node/infrastructure/metrics/metrics_collector.py +351 -0
  31. manta_node/infrastructure/mqtt/__init__.py +7 -0
  32. manta_node/infrastructure/mqtt/command_handler.py +315 -0
  33. manta_node/node_orchestrator.py +462 -0
  34. manta_node/task_manager.py +519 -0
  35. manta_node/tasks.py +272 -0
  36. manta_node/utils.py +52 -0
  37. manta_node-0.5b0.dev9.dist-info/METADATA +799 -0
  38. manta_node-0.5b0.dev9.dist-info/RECORD +42 -0
  39. manta_node-0.5b0.dev9.dist-info/WHEEL +5 -0
  40. manta_node-0.5b0.dev9.dist-info/entry_points.txt +2 -0
  41. manta_node-0.5b0.dev9.dist-info/licenses/LICENSE +683 -0
  42. 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