mantatech-sdk 0.5b0.dev65__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 (54) hide show
  1. manta/__init__.light.py +22 -0
  2. manta/__init__.py +83 -0
  3. manta/__main__.py +21 -0
  4. manta/apis/__init__.py +7 -0
  5. manta/apis/async_user_api.py +6458 -0
  6. manta/apis/graph.py +498 -0
  7. manta/apis/module.py +316 -0
  8. manta/apis/results.py +251 -0
  9. manta/apis/swarm.py +206 -0
  10. manta/apis/user_api.py +1016 -0
  11. manta/cli/__init__.py +1 -0
  12. manta/cli/commands/__init__.py +1 -0
  13. manta/cli/commands/base_handler.py +229 -0
  14. manta/cli/commands/doc.py +192 -0
  15. manta/cli/commands/install.py +346 -0
  16. manta/cli/commands/sdk.py +9 -0
  17. manta/cli/commands/sdk_cluster.py +211 -0
  18. manta/cli/commands/sdk_config.py +347 -0
  19. manta/cli/commands/sdk_globals.py +280 -0
  20. manta/cli/commands/sdk_logs.py +174 -0
  21. manta/cli/commands/sdk_main.py +167 -0
  22. manta/cli/commands/sdk_module.py +516 -0
  23. manta/cli/commands/sdk_nodes.py +168 -0
  24. manta/cli/commands/sdk_original.py +3873 -0
  25. manta/cli/commands/sdk_results.py +265 -0
  26. manta/cli/commands/sdk_swarm.py +454 -0
  27. manta/cli/commands/sdk_user.py +234 -0
  28. manta/cli/commands/status.py +292 -0
  29. manta/cli/component_detector.py +112 -0
  30. manta/cli/config_manager.py +445 -0
  31. manta/cli/main.py +265 -0
  32. manta/cli/utils/__init__.py +27 -0
  33. manta/cli/utils/converters.py +140 -0
  34. manta/clients/cluster_management_client.py +486 -0
  35. manta/clients/local_client.py +149 -0
  36. manta/clients/module_management_client.py +217 -0
  37. manta/clients/swarm_management_client.py +562 -0
  38. manta/clients/user_management_client.py +395 -0
  39. manta/clients/world_client.py +195 -0
  40. manta/light/__init__.py +31 -0
  41. manta/light/globals.py +245 -0
  42. manta/light/local.py +407 -0
  43. manta/light/logging_config.py +39 -0
  44. manta/light/path.py +116 -0
  45. manta/light/results.py +236 -0
  46. manta/light/task.py +100 -0
  47. manta/light/utils.py +217 -0
  48. manta/light/world.py +177 -0
  49. mantatech_sdk-0.5b0.dev65.dist-info/METADATA +1039 -0
  50. mantatech_sdk-0.5b0.dev65.dist-info/RECORD +54 -0
  51. mantatech_sdk-0.5b0.dev65.dist-info/WHEEL +5 -0
  52. mantatech_sdk-0.5b0.dev65.dist-info/entry_points.txt +2 -0
  53. mantatech_sdk-0.5b0.dev65.dist-info/licenses/LICENSE +683 -0
  54. mantatech_sdk-0.5b0.dev65.dist-info/top_level.txt +1 -0
@@ -0,0 +1,3873 @@
1
+ """SDK commands that integrate with manta-sdk user API functionality."""
2
+
3
+ import asyncio
4
+ import json
5
+ import signal
6
+ import sys
7
+ import time
8
+ from datetime import datetime
9
+ from pathlib import Path
10
+ from typing import Any, Dict, List, Optional
11
+
12
+ from rich.columns import Columns
13
+ from rich.console import Console
14
+ from rich.layout import Layout
15
+ from rich.live import Live
16
+ from rich.panel import Panel
17
+ from rich.progress import Progress, SpinnerColumn, TextColumn
18
+ from rich.prompt import Confirm, IntPrompt, Prompt
19
+ from rich.table import Table
20
+ from rich.tree import Tree
21
+
22
+ from ..config_manager import ConfigManager, CredentialManager, ProfileManager
23
+
24
+
25
+ class SDKCommands:
26
+ """Handle SDK-related CLI commands that use manta-sdk functionality."""
27
+
28
+ def __init__(
29
+ self,
30
+ config_manager: ConfigManager,
31
+ profile_manager: ProfileManager,
32
+ credential_manager: CredentialManager,
33
+ console: Optional[Console] = None,
34
+ ):
35
+ self.config_manager = config_manager
36
+ self.profile_manager = profile_manager
37
+ self.credential_manager = credential_manager
38
+ self.console = console
39
+
40
+ def print(self, *args, **kwargs):
41
+ """Print with console if available."""
42
+ self.console.print(*args, **kwargs)
43
+
44
+ def print_error(self, message: str):
45
+ """Print error message."""
46
+ self.console.print(f"[red]Error:[/red] {message}")
47
+
48
+ def print_success(self, message: str):
49
+ """Print success message."""
50
+ self.console.print(f"[green]Success:[/green] {message}")
51
+
52
+ def _run_with_progress(self, async_func, message: str):
53
+ """Run an async function with progress indication."""
54
+ try:
55
+ with Progress(
56
+ SpinnerColumn(),
57
+ TextColumn("[progress.description]{task.description}"),
58
+ console=self.console,
59
+ ) as progress:
60
+ task = progress.add_task(message, total=None)
61
+ result = asyncio.run(async_func())
62
+ progress.update(task, completed=True)
63
+ return result
64
+ except Exception:
65
+ # Fallback to simple progress indication if Rich progress fails
66
+ self.print(message)
67
+ return asyncio.run(async_func())
68
+
69
+ def handle(self, args) -> int:
70
+ """Handle SDK commands."""
71
+ command = args.command
72
+
73
+ # Set profile override if specified
74
+ if hasattr(args, "profile") and args.profile:
75
+ self._profile_override = args.profile
76
+ else:
77
+ self._profile_override = None
78
+
79
+ if command == "user":
80
+ return self.handle_user_commands(args)
81
+ elif command == "cluster":
82
+ return self.handle_cluster_commands(args)
83
+ elif command == "simulation":
84
+ return self.handle_simulation_commands(args)
85
+ elif command == "module":
86
+ return self.handle_module_commands(args)
87
+ elif command == "swarm":
88
+ return self.handle_swarm_commands(args)
89
+ elif command == "node":
90
+ return self.handle_node_commands(args)
91
+ elif command == "results":
92
+ return self.handle_results_commands(args)
93
+ elif command == "logs":
94
+ return self.handle_logs_commands(args)
95
+ else:
96
+ self.print_error(f"Unknown SDK command: {command}")
97
+ return 1
98
+
99
+ def handle_user_commands(self, args) -> int:
100
+ """Handle user API commands."""
101
+ if not args.user_command:
102
+ self.print_error("User command required")
103
+ return 1
104
+
105
+ if args.user_command == "status":
106
+ return self.check_user_service_status()
107
+ elif args.user_command == "get-user":
108
+ return self.get_user_info()
109
+ elif args.user_command == "list-clusters":
110
+ return self.list_clusters()
111
+ elif args.user_command == "list-swarms":
112
+ return self.list_swarms(getattr(args, "cluster_id", None))
113
+ else:
114
+ self.print_error(f"Unknown user command: {args.user_command}")
115
+ return 1
116
+
117
+ def handle_cluster_commands(self, args) -> int:
118
+ """Handle cluster commands."""
119
+ if not args.cluster_command:
120
+ self.print_error("Cluster command required")
121
+ return 1
122
+
123
+ if args.cluster_command == "list":
124
+ return self.list_clusters()
125
+ elif args.cluster_command == "show":
126
+ return self.show_cluster(args.cluster_id)
127
+ else:
128
+ self.print_error(f"Unknown cluster command: {args.cluster_command}")
129
+ return 1
130
+
131
+ def handle_simulation_commands(self, args) -> int:
132
+ """Handle simulation commands."""
133
+ if not args.simulation_command:
134
+ self.print_error("Simulation command required")
135
+ return 1
136
+
137
+ if args.simulation_command == "list":
138
+ return self.list_simulations()
139
+ elif args.simulation_command == "deploy":
140
+ return self.deploy_simulation(args.swarm_file, args.cluster_id)
141
+ else:
142
+ self.print_error(f"Unknown simulation command: {args.simulation_command}")
143
+ return 1
144
+
145
+ def _get_user_api(self):
146
+ """Get configured AsyncUserAPI instance."""
147
+ try:
148
+ # Dynamic import of manta-sdk
149
+ from manta.apis.user_api import AsyncUserAPI
150
+
151
+ # Get configuration
152
+ config = self._load_config()
153
+ if not config:
154
+ self.print_error(
155
+ "No configuration found. Run 'manta config init' first."
156
+ )
157
+ return None
158
+
159
+ # Get credentials
160
+ credential_id = config.get("credential_id")
161
+ if not credential_id:
162
+ self.print_error(
163
+ "No credential configured. Run 'manta config credentials add' first."
164
+ )
165
+ return None
166
+
167
+ token = self.credential_manager.get_credential(credential_id)
168
+ if not token:
169
+ self.print_error(f"Credential '{credential_id}' not found.")
170
+ return None
171
+
172
+ # Create API instance
173
+ connection = config.get("connection", {})
174
+ api = AsyncUserAPI(
175
+ token=token,
176
+ host=connection.get("host", "localhost"),
177
+ port=connection.get("port", 50052),
178
+ cert_folder=(
179
+ connection.get("cert_folder") if connection.get("use_tls") else None
180
+ ),
181
+ )
182
+
183
+ return api
184
+
185
+ except ImportError:
186
+ self.print_error(
187
+ "manta-sdk not installed. Install with: pip install manta-sdk[api]"
188
+ )
189
+ return None
190
+ except Exception as e:
191
+ self.print_error(f"Failed to create API instance: {e}")
192
+ return None
193
+
194
+ def _load_config(self):
195
+ """Load active configuration with fallbacks."""
196
+ # Use profile override if specified
197
+ if hasattr(self, "_profile_override") and self._profile_override:
198
+ profile = self.profile_manager.get_profile(self._profile_override)
199
+ if profile:
200
+ return profile.to_dict() if hasattr(profile, "to_dict") else profile
201
+ else:
202
+ self.print_warning(
203
+ f"Profile '{self._profile_override}' not found, using default"
204
+ )
205
+
206
+ # Get active profile
207
+ active_profile = self.config_manager.get_active_profile()
208
+ if active_profile:
209
+ profile = self.profile_manager.get_profile(active_profile)
210
+ if profile:
211
+ return profile.to_dict() if hasattr(profile, "to_dict") else profile
212
+
213
+ # No active profile, try default
214
+ default_profile = self.profile_manager.get_profile("default")
215
+ if default_profile:
216
+ return (
217
+ default_profile.to_dict()
218
+ if hasattr(default_profile, "to_dict")
219
+ else default_profile
220
+ )
221
+
222
+ return None
223
+
224
+ def get_user_info(self) -> int:
225
+ """Get current user information."""
226
+ api = self._get_user_api()
227
+ if not api:
228
+ return 1
229
+
230
+ try:
231
+
232
+ async def run():
233
+ user_info = await api.get_user()
234
+ return user_info
235
+
236
+ with Progress(
237
+ SpinnerColumn(),
238
+ TextColumn("[progress.description]{task.description}"),
239
+ console=self.console,
240
+ ) as progress:
241
+ task = progress.add_task("Getting user information...", total=None)
242
+ user_info = asyncio.run(run())
243
+ progress.update(task, completed=True)
244
+
245
+ # Display user info
246
+ panel = Panel.fit(
247
+ f"[cyan]User ID:[/cyan] {user_info.get('user_id', 'Unknown')}\n"
248
+ f"[cyan]Username:[/cyan] {user_info.get('username', 'Unknown')}\n"
249
+ f"[cyan]Email:[/cyan] {user_info.get('email', 'Unknown')}\n"
250
+ f"[cyan]Roles:[/cyan] {', '.join(user_info.get('roles', []))}",
251
+ title="User Information",
252
+ )
253
+ self.console.print(panel)
254
+ return 0
255
+
256
+ except Exception as e:
257
+ self.print_error(f"Failed to get user information: {e}")
258
+ return 1
259
+
260
+ def list_clusters(self) -> int:
261
+ """List available clusters."""
262
+ api = self._get_user_api()
263
+ if not api:
264
+ return 1
265
+
266
+ try:
267
+
268
+ async def run():
269
+ clusters = await api.stream_and_fetch_clusters()
270
+ return clusters
271
+
272
+ with Progress(
273
+ SpinnerColumn(),
274
+ TextColumn("[progress.description]{task.description}"),
275
+ console=self.console,
276
+ ) as progress:
277
+ task = progress.add_task("Fetching clusters...", total=None)
278
+ clusters = asyncio.run(run())
279
+ progress.update(task, completed=True)
280
+
281
+ # Display clusters
282
+ if not clusters:
283
+ self.print("No clusters found.")
284
+ return 0
285
+
286
+ table = Table(title="Available Clusters")
287
+ table.add_column("Cluster ID", style="cyan")
288
+ table.add_column("Name", style="blue")
289
+ table.add_column("Status", style="green")
290
+ table.add_column("Nodes", style="magenta")
291
+
292
+ for cluster in clusters:
293
+ table.add_row(
294
+ cluster.get("cluster_id", "Unknown"),
295
+ cluster.get("name", "Unknown"),
296
+ cluster.get("status", "Unknown"),
297
+ str(cluster.get("node_count", 0)),
298
+ )
299
+
300
+ self.console.print(table)
301
+
302
+ return 0
303
+
304
+ except Exception as e:
305
+ self.print_error(f"Failed to list clusters: {e}")
306
+ return 1
307
+
308
+ def list_swarms(self, cluster_id: str) -> int:
309
+ """List swarms for a cluster."""
310
+ api = self._get_user_api()
311
+ if not api:
312
+ return 1
313
+
314
+ try:
315
+
316
+ async def run():
317
+ swarms = await api.stream_and_fetch_swarms()
318
+ return swarms
319
+
320
+ with Progress(
321
+ SpinnerColumn(),
322
+ TextColumn("[progress.description]{task.description}"),
323
+ console=self.console,
324
+ ) as progress:
325
+ task = progress.add_task("Fetching swarms...", total=None)
326
+ swarms = asyncio.run(run())
327
+ progress.update(task, completed=True)
328
+
329
+ # Filter by cluster if specified
330
+ if cluster_id:
331
+ swarms = [s for s in swarms if s.get("cluster_id") == cluster_id]
332
+
333
+ # Display swarms
334
+ if not swarms:
335
+ msg = (
336
+ f"No swarms found for cluster '{cluster_id}'."
337
+ if cluster_id
338
+ else "No swarms found."
339
+ )
340
+ self.print(msg)
341
+ return 0
342
+
343
+ table = Table(
344
+ title=f"Swarms{' for cluster ' + cluster_id if cluster_id else ''}"
345
+ )
346
+ table.add_column("Swarm ID", style="cyan")
347
+ table.add_column("Name", style="blue")
348
+ table.add_column("Status", style="green")
349
+ table.add_column("Tasks", style="magenta")
350
+
351
+ for swarm in swarms:
352
+ table.add_row(
353
+ swarm.get("swarm_id", "Unknown"),
354
+ swarm.get("name", "Unknown"),
355
+ swarm.get("status", "Unknown"),
356
+ str(swarm.get("task_count", 0)),
357
+ )
358
+
359
+ self.console.print(table)
360
+ return 0
361
+
362
+ except Exception as e:
363
+ self.print_error(f"Failed to list swarms: {e}")
364
+ return 1
365
+
366
+ def show_cluster(self, cluster_id: str) -> int:
367
+ """Show detailed cluster information."""
368
+ api = self._get_user_api()
369
+ if not api:
370
+ return 1
371
+
372
+ try:
373
+
374
+ async def run():
375
+ cluster = await api.get_cluster(cluster_id)
376
+ return cluster
377
+
378
+ with Progress(
379
+ SpinnerColumn(),
380
+ TextColumn("[progress.description]{task.description}"),
381
+ console=self.console,
382
+ ) as progress:
383
+ task = progress.add_task("Fetching cluster details...", total=None)
384
+ cluster = asyncio.run(run())
385
+ progress.update(task, completed=True)
386
+
387
+ # Display cluster details
388
+ panel = Panel.fit(
389
+ f"[cyan]Cluster ID:[/cyan] {cluster.get('cluster_id', 'Unknown')}\n"
390
+ f"[cyan]Name:[/cyan] {cluster.get('name', 'Unknown')}\n"
391
+ f"[cyan]Description:[/cyan] {cluster.get('description', 'None')}\n"
392
+ f"[cyan]Status:[/cyan] {cluster.get('status', 'Unknown')}\n"
393
+ f"[cyan]Node Count:[/cyan] {cluster.get('node_count', 0)}\n"
394
+ f"[cyan]Created:[/cyan] {cluster.get('created_at', 'Unknown')}",
395
+ title=f"Cluster: {cluster_id}",
396
+ )
397
+ self.console.print(panel)
398
+
399
+ return 0
400
+
401
+ except Exception as e:
402
+ self.print_error(f"Failed to get cluster details: {e}")
403
+ return 1
404
+
405
+ return 0
406
+
407
+ except Exception as e:
408
+ self.print_error(f"Failed to get cluster details: {e}")
409
+ return 1
410
+
411
+ def list_simulations(self) -> int:
412
+ """List all simulations."""
413
+ # This is essentially the same as list_swarms without cluster filtering
414
+ return self.list_swarms(None)
415
+
416
+ def deploy_simulation(self, swarm_file: str, cluster_id: str) -> int:
417
+ """Deploy simulation from swarm file."""
418
+ self.print_error("Simulation deployment not yet implemented in CLI")
419
+ self.print("Use the SDK directly for now:")
420
+ self.print(" from manta.apis import AsyncUserAPI")
421
+ self.print(f" # Load and deploy your swarm from {swarm_file}")
422
+ return 1
423
+
424
+ # ============================================
425
+ # Service Status Operations
426
+ # ============================================
427
+
428
+ def check_user_service_status(self) -> int:
429
+ """Check if User service is available."""
430
+ api = self._get_user_api()
431
+ if not api:
432
+ return 1
433
+
434
+ try:
435
+
436
+ async def run():
437
+ return await api.is_available()
438
+
439
+ if self.console:
440
+ with Progress(
441
+ SpinnerColumn(),
442
+ TextColumn("[progress.description]{task.description}"),
443
+ console=self.console,
444
+ ) as progress:
445
+ task = progress.add_task(
446
+ "Checking service availability...", total=None
447
+ )
448
+ available = asyncio.run(run())
449
+ progress.update(task, completed=True)
450
+ else:
451
+ print("Checking service availability...")
452
+ available = asyncio.run(run())
453
+
454
+ if available:
455
+ self.print_success("User service is available")
456
+ return 0
457
+ else:
458
+ self.print_error("User service is not available")
459
+ return 1
460
+
461
+ except Exception as e:
462
+ self.print_error(f"Failed to check service status: {e}")
463
+ return 1
464
+
465
+ # ============================================
466
+ # Module Management Operations
467
+ # ============================================
468
+
469
+ def handle_module_commands(self, args) -> int:
470
+ """Handle module management commands."""
471
+ if not args.module_command:
472
+ self.print_error("Module command required")
473
+ return 1
474
+
475
+ if args.module_command == "list":
476
+ return self.list_modules()
477
+ elif args.module_command == "list-ids":
478
+ return self.list_module_ids()
479
+ elif args.module_command == "show":
480
+ return self.show_module(args.module_id)
481
+ elif args.module_command == "upload":
482
+ return self.upload_module(
483
+ args.module_file, getattr(args, "module_id", None)
484
+ )
485
+ elif args.module_command == "update":
486
+ return self.update_module(args.module_id, args.module_file)
487
+ elif args.module_command == "delete":
488
+ return self.delete_module(args.module_id, getattr(args, "force", False))
489
+ elif args.module_command == "download":
490
+ return self.download_module(
491
+ args.module_id, getattr(args, "output_file", None)
492
+ )
493
+ else:
494
+ self.print_error(f"Unknown module command: {args.module_command}")
495
+ return 1
496
+
497
+ def list_modules(self) -> int:
498
+ """List all modules for the user."""
499
+ api = self._get_user_api()
500
+ if not api:
501
+ return 1
502
+
503
+ try:
504
+
505
+ async def run():
506
+ modules = await api.stream_and_fetch_modules()
507
+ return modules
508
+
509
+ if self.console:
510
+ try:
511
+ with Progress(
512
+ SpinnerColumn(),
513
+ TextColumn("[progress.description]{task.description}"),
514
+ console=self.console,
515
+ ) as progress:
516
+ task = progress.add_task("Fetching modules...", total=None)
517
+ modules = asyncio.run(run())
518
+ progress.update(task, completed=True)
519
+ except Exception:
520
+ # Fallback to simple progress indication if Rich progress fails
521
+ self.print("Fetching modules...")
522
+ modules = asyncio.run(run())
523
+ else:
524
+ print("Fetching modules...")
525
+ modules = asyncio.run(run())
526
+
527
+ if not modules:
528
+ self.print("No modules found.")
529
+ return 0
530
+
531
+ # Display modules
532
+ if self.console:
533
+ table = Table(title="User Modules")
534
+ table.add_column("Module ID", style="cyan")
535
+ table.add_column("Name", style="blue")
536
+ table.add_column("Type", style="green")
537
+ table.add_column("Size", style="magenta")
538
+ table.add_column("Created", style="yellow")
539
+
540
+ for module in modules:
541
+ # Module is a Module object with attributes
542
+ module.__dict__ if hasattr(module, "__dict__") else {}
543
+
544
+ # Handle data attribute carefully for mock objects
545
+ data_attr = getattr(module, "data", b"")
546
+ if hasattr(data_attr, "_mock_name"): # It's a Mock object
547
+ data_size = 0
548
+ else:
549
+ data_size = len(data_attr) if data_attr else 0
550
+
551
+ table.add_row(
552
+ str(getattr(module, "module_id", "Unknown")),
553
+ str(getattr(module, "name", "Unknown")),
554
+ str(getattr(module, "module_type", "Unknown")),
555
+ self._format_size(data_size),
556
+ str(getattr(module, "created_at", "Unknown")),
557
+ )
558
+
559
+ self.console.print(table)
560
+ else:
561
+ print("Modules:")
562
+ print("-" * 80)
563
+ for module in modules:
564
+ module_id = str(getattr(module, "module_id", "Unknown"))
565
+ module_name = str(getattr(module, "name", "Unknown"))
566
+ module_type = str(getattr(module, "module_type", "Unknown"))
567
+ print(f"{module_id:30} {module_name:20} {module_type}")
568
+
569
+ return 0
570
+
571
+ except Exception as e:
572
+ self.print_error(f"Failed to list modules: {e}")
573
+ return 1
574
+
575
+ def list_module_ids(self) -> int:
576
+ """List module IDs only."""
577
+ api = self._get_user_api()
578
+ if not api:
579
+ return 1
580
+
581
+ try:
582
+
583
+ async def run():
584
+ return await api.list_module_ids()
585
+
586
+ module_ids = self._run_with_progress(run, "Fetching module IDs...")
587
+
588
+ if not module_ids:
589
+ self.print("No modules found.")
590
+ return 0
591
+
592
+ self.print(f"Found {len(module_ids)} modules:")
593
+ for module_id in module_ids:
594
+ self.print(f" {module_id}")
595
+
596
+ return 0
597
+
598
+ except Exception as e:
599
+ self.print_error(f"Failed to list module IDs: {e}")
600
+ return 1
601
+
602
+ def show_module(self, module_id: str) -> int:
603
+ """Show detailed module information."""
604
+ api = self._get_user_api()
605
+ if not api:
606
+ return 1
607
+
608
+ try:
609
+
610
+ async def run():
611
+ return await api.get_module(module_id)
612
+
613
+ module = self._run_with_progress(run, "Fetching module details...")
614
+
615
+ # Display module details
616
+ if self.console:
617
+ panel = Panel.fit(
618
+ f"[cyan]Module ID:[/cyan] {getattr(module, 'module_id', 'Unknown')}\n"
619
+ f"[cyan]Name:[/cyan] {getattr(module, 'name', 'Unknown')}\n"
620
+ f"[cyan]Type:[/cyan] {getattr(module, 'module_type', 'Unknown')}\n"
621
+ f"[cyan]Size:[/cyan] {self._format_size(len(getattr(module, 'data', b'')))}\n"
622
+ f"[cyan]Created:[/cyan] {getattr(module, 'created_at', 'Unknown')}",
623
+ title=f"Module: {module_id}",
624
+ )
625
+ self.console.print(panel)
626
+ else:
627
+ print(f"Module: {module_id}")
628
+ print(f" Name: {getattr(module, 'name', 'Unknown')}")
629
+ print(f" Type: {getattr(module, 'module_type', 'Unknown')}")
630
+ print(f" Size: {self._format_size(len(getattr(module, 'data', b'')))}")
631
+
632
+ return 0
633
+
634
+ except Exception as e:
635
+ self.print_error(f"Failed to get module details: {e}")
636
+ return 1
637
+
638
+ def upload_module(self, module_file: str, module_id: Optional[str] = None) -> int:
639
+ """Upload a module from file."""
640
+ api = self._get_user_api()
641
+ if not api:
642
+ return 1
643
+
644
+ try:
645
+ # Import Module class
646
+ from manta.apis.module import Module
647
+
648
+ # Validate file path
649
+ module_path = Path(module_file)
650
+ if not module_path.exists():
651
+ self.print_error(f"Module file not found: {module_file}")
652
+ return 1
653
+
654
+ # Get image name from user if not provided
655
+ if self.console:
656
+ default_image = "python:3.9-slim"
657
+ image = Prompt.ask("Container image", default=default_image)
658
+ else:
659
+ image = "python:3.9-slim"
660
+ self.print(f"Using default image: {image}")
661
+
662
+ # Create Module object
663
+ module = Module(
664
+ python_program=module_path,
665
+ image=image,
666
+ name=module_path.stem if module_path.is_file() else module_path.name,
667
+ )
668
+
669
+ async def run():
670
+ return await api.send_module(module)
671
+
672
+ result_id = self._run_with_progress(
673
+ run, f"Uploading module from {module_file}..."
674
+ )
675
+
676
+ self.print_success(f"Module uploaded successfully with ID: {result_id}")
677
+ return 0
678
+
679
+ except FileNotFoundError:
680
+ self.print_error(f"Module file not found: {module_file}")
681
+ return 1
682
+ except PermissionError:
683
+ self.print_error(f"Permission denied reading file: {module_file}")
684
+ return 1
685
+ except Exception as e:
686
+ self.print_error(f"Failed to upload module: {e}")
687
+ return 1
688
+
689
+ def update_module(self, module_id: str, module_file: str) -> int:
690
+ """Update an existing module."""
691
+ api = self._get_user_api()
692
+ if not api:
693
+ return 1
694
+
695
+ try:
696
+ # Import Module class
697
+ from manta.apis.module import Module
698
+
699
+ # Validate module_id parameter
700
+ if not module_id:
701
+ self.print_error("Module ID is required for update operation")
702
+ return 1
703
+
704
+ # Validate file path
705
+ module_path = Path(module_file)
706
+ if not module_path.exists():
707
+ self.print_error(f"Module file not found: {module_file}")
708
+ return 1
709
+
710
+ # First verify the module exists by trying to get it
711
+ async def check_module_exists():
712
+ try:
713
+ return await api.get_module(module_id)
714
+ except Exception:
715
+ return None
716
+
717
+ existing_module = self._run_with_progress(
718
+ check_module_exists, "Checking if module exists..."
719
+ )
720
+
721
+ if not existing_module:
722
+ self.print_error(f"Module with ID '{module_id}' not found")
723
+ return 1
724
+
725
+ # Get image name - use existing image as default or prompt for new one
726
+ existing_image = getattr(existing_module, "image", "python:3.9-slim")
727
+ if self.console:
728
+ image = Prompt.ask("Container image", default=existing_image)
729
+ else:
730
+ image = existing_image
731
+ self.print(f"Using existing image: {image}")
732
+
733
+ # Create updated Module object
734
+ module = Module(
735
+ python_program=module_path,
736
+ image=image,
737
+ name=module_path.stem if module_path.is_file() else module_path.name,
738
+ )
739
+
740
+ async def run():
741
+ return await api.update_module(module, module_id)
742
+
743
+ result_id = self._run_with_progress(run, f"Updating module {module_id}...")
744
+
745
+ self.print_success(f"Module updated successfully: {result_id}")
746
+ return 0
747
+
748
+ except FileNotFoundError:
749
+ self.print_error(f"Module file not found: {module_file}")
750
+ return 1
751
+ except PermissionError:
752
+ self.print_error(f"Permission denied reading file: {module_file}")
753
+ return 1
754
+ except Exception as e:
755
+ self.print_error(f"Failed to update module: {e}")
756
+ return 1
757
+
758
+ def delete_module(self, module_id: str, force: bool = False) -> int:
759
+ """Delete a module."""
760
+ if not force:
761
+ if self.console:
762
+ confirm = Confirm.ask(
763
+ f"Are you sure you want to delete module '{module_id}'?"
764
+ )
765
+ if not confirm:
766
+ self.print("Operation cancelled.")
767
+ return 0
768
+
769
+ api = self._get_user_api()
770
+ if not api:
771
+ return 1
772
+
773
+ try:
774
+
775
+ async def run():
776
+ return await api.remove_module(module_id)
777
+
778
+ result = self._run_with_progress(run, "Deleting module...")
779
+
780
+ self.print_success(f"Module deleted: {result}")
781
+ return 0
782
+
783
+ except Exception as e:
784
+ self.print_error(f"Failed to delete module: {e}")
785
+ return 1
786
+
787
+ def download_module(self, module_id: str, output_file: Optional[str] = None) -> int:
788
+ """Download a module to file."""
789
+ api = self._get_user_api()
790
+ if not api:
791
+ return 1
792
+
793
+ try:
794
+ # Validate module_id parameter
795
+ if not module_id:
796
+ self.print_error("Module ID is required for download operation")
797
+ return 1
798
+
799
+ # Fetch module from server
800
+ async def run():
801
+ return await api.get_module(module_id)
802
+
803
+ module = self._run_with_progress(run, f"Downloading module {module_id}...")
804
+
805
+ # Check if module has python_files (from Module.from_proto)
806
+ if not hasattr(module, "python_files") or not module.python_files:
807
+ self.print_error("Module contains no python files to download")
808
+ return 1
809
+
810
+ # Determine output path
811
+ if output_file:
812
+ output_path = Path(output_file)
813
+ else:
814
+ # Use module name or ID as default filename
815
+ module_name = getattr(module, "name", None) or module_id
816
+ if len(module.python_files) == 1:
817
+ # Single file - use the filename from the module
818
+ filename = list(module.python_files.keys())[0]
819
+ output_path = Path(filename)
820
+ else:
821
+ # Multiple files - create a directory
822
+ output_path = Path(f"{module_name}_module")
823
+
824
+ # Handle single file vs directory
825
+ if len(module.python_files) == 1:
826
+ # Single file download
827
+ filename, content = next(iter(module.python_files.items()))
828
+
829
+ # If output_file is a directory, put file inside it
830
+ if output_path.is_dir():
831
+ output_path = output_path / filename
832
+
833
+ # Check for conflicts
834
+ if output_path.exists():
835
+ if self.console:
836
+ overwrite = Confirm.ask(
837
+ f"File '{output_path}' already exists. Overwrite?"
838
+ )
839
+ if not overwrite:
840
+ self.print("Download cancelled.")
841
+ return 0
842
+ else:
843
+ self.print_error(
844
+ f"File '{output_path}' already exists. Use --force or specify a different output path."
845
+ )
846
+ return 1
847
+
848
+ # Write single file
849
+ try:
850
+ output_path.parent.mkdir(parents=True, exist_ok=True)
851
+ output_path.write_text(content, encoding="utf-8")
852
+ self.print_success(
853
+ f"Module downloaded to: {output_path.absolute()}"
854
+ )
855
+ except Exception as e:
856
+ self.print_error(f"Failed to write file: {e}")
857
+ return 1
858
+
859
+ else:
860
+ # Multiple files - create directory structure
861
+ if output_path.exists() and output_path.is_dir():
862
+ if self.console:
863
+ overwrite = Confirm.ask(
864
+ f"Directory '{output_path}' already exists. Continue?"
865
+ )
866
+ if not overwrite:
867
+ self.print("Download cancelled.")
868
+ return 0
869
+ elif output_path.exists():
870
+ self.print_error(
871
+ f"Path '{output_path}' exists and is not a directory"
872
+ )
873
+ return 1
874
+
875
+ # Create directory and write all files
876
+ try:
877
+ output_path.mkdir(parents=True, exist_ok=True)
878
+
879
+ files_written = 0
880
+ for filename, content in module.python_files.items():
881
+ file_path = output_path / filename
882
+
883
+ # Create subdirectories if needed
884
+ file_path.parent.mkdir(parents=True, exist_ok=True)
885
+
886
+ # Write file
887
+ file_path.write_text(content, encoding="utf-8")
888
+ files_written += 1
889
+
890
+ self.print_success(
891
+ f"Module downloaded: {files_written} files written to {output_path.absolute()}"
892
+ )
893
+
894
+ # Show file structure if console available
895
+ if self.console:
896
+ tree = Tree(f"📁 {output_path.name}")
897
+ for filename in sorted(module.python_files.keys()):
898
+ tree.add(f"📄 {filename}")
899
+ self.console.print(tree)
900
+
901
+ except Exception as e:
902
+ self.print_error(f"Failed to write files: {e}")
903
+ return 1
904
+
905
+ return 0
906
+
907
+ except Exception as e:
908
+ self.print_error(f"Failed to download module: {e}")
909
+ return 1
910
+
911
+ # ============================================
912
+ # Swarm Management Operations
913
+ # ============================================
914
+
915
+ def handle_swarm_commands(self, args) -> int:
916
+ """Handle swarm management commands."""
917
+ if not args.swarm_command:
918
+ self.print_error("Swarm command required")
919
+ return 1
920
+
921
+ if args.swarm_command == "list":
922
+ return self.list_swarms(getattr(args, "cluster_id", None))
923
+ elif args.swarm_command == "list-ids":
924
+ return self.list_swarm_ids()
925
+ elif args.swarm_command == "show":
926
+ return self.show_swarm(args.swarm_id)
927
+ elif args.swarm_command == "tasks":
928
+ return self.show_swarm_tasks(args.swarm_id)
929
+ elif args.swarm_command == "start":
930
+ return self.start_swarm(args.swarm_id)
931
+ elif args.swarm_command == "stop":
932
+ return self.stop_swarm(args.swarm_id, getattr(args, "force", False))
933
+ elif args.swarm_command == "delete":
934
+ return self.delete_swarm(args.swarm_id, getattr(args, "force", False))
935
+ elif args.swarm_command == "deploy":
936
+ return self.deploy_swarm_interactive()
937
+ elif args.swarm_command == "monitor":
938
+ return self.monitor_swarm(args.swarm_id)
939
+ else:
940
+ self.print_error(f"Unknown swarm command: {args.swarm_command}")
941
+ return 1
942
+
943
+ def list_swarm_ids(self) -> int:
944
+ """List swarm IDs only."""
945
+ api = self._get_user_api()
946
+ if not api:
947
+ return 1
948
+
949
+ try:
950
+
951
+ async def run():
952
+ return await api.list_swarm_ids()
953
+
954
+ if self.console:
955
+ with Progress(
956
+ SpinnerColumn(),
957
+ TextColumn("[progress.description]{task.description}"),
958
+ console=self.console,
959
+ ) as progress:
960
+ task = progress.add_task("Fetching swarm IDs...", total=None)
961
+ swarm_ids = asyncio.run(run())
962
+ progress.update(task, completed=True)
963
+ else:
964
+ print("Fetching swarm IDs...")
965
+ swarm_ids = asyncio.run(run())
966
+
967
+ if not swarm_ids:
968
+ self.print("No swarms found.")
969
+ return 0
970
+
971
+ self.print(f"Found {len(swarm_ids)} swarms:")
972
+ for swarm_id in swarm_ids:
973
+ self.print(f" {swarm_id}")
974
+
975
+ return 0
976
+
977
+ except Exception as e:
978
+ self.print_error(f"Failed to list swarm IDs: {e}")
979
+ return 1
980
+
981
+ def show_swarm(self, swarm_id: str) -> int:
982
+ """Show detailed swarm information."""
983
+ api = self._get_user_api()
984
+ if not api:
985
+ return 1
986
+
987
+ try:
988
+
989
+ async def run():
990
+ return await api.get_swarm(swarm_id)
991
+
992
+ if self.console:
993
+ with Progress(
994
+ SpinnerColumn(),
995
+ TextColumn("[progress.description]{task.description}"),
996
+ console=self.console,
997
+ ) as progress:
998
+ task = progress.add_task("Fetching swarm details...", total=None)
999
+ swarm = asyncio.run(run())
1000
+ progress.update(task, completed=True)
1001
+ else:
1002
+ print("Fetching swarm details...")
1003
+ swarm = asyncio.run(run())
1004
+
1005
+ # Display swarm details
1006
+ if self.console:
1007
+ panel = Panel.fit(
1008
+ f"[cyan]Swarm ID:[/cyan] {swarm.get('swarm_id', 'Unknown')}\n"
1009
+ f"[cyan]Name:[/cyan] {swarm.get('name', 'Unknown')}\n"
1010
+ f"[cyan]Status:[/cyan] {swarm.get('status', 'Unknown')}\n"
1011
+ f"[cyan]Cluster ID:[/cyan] {swarm.get('cluster_id', 'Unknown')}\n"
1012
+ f"[cyan]Task Count:[/cyan] {swarm.get('task_count', 0)}\n"
1013
+ f"[cyan]Created:[/cyan] {swarm.get('created_at', 'Unknown')}",
1014
+ title=f"Swarm: {swarm_id}",
1015
+ )
1016
+ self.console.print(panel)
1017
+ else:
1018
+ print(f"Swarm: {swarm_id}")
1019
+ print(f" Name: {swarm.get('name', 'Unknown')}")
1020
+ print(f" Status: {swarm.get('status', 'Unknown')}")
1021
+ print(f" Cluster ID: {swarm.get('cluster_id', 'Unknown')}")
1022
+ print(f" Task Count: {swarm.get('task_count', 0)}")
1023
+
1024
+ return 0
1025
+
1026
+ except Exception as e:
1027
+ self.print_error(f"Failed to get swarm details: {e}")
1028
+ return 1
1029
+
1030
+ def show_swarm_tasks(self, swarm_id: str) -> int:
1031
+ """Show tasks for a swarm."""
1032
+ api = self._get_user_api()
1033
+ if not api:
1034
+ return 1
1035
+
1036
+ try:
1037
+
1038
+ async def run():
1039
+ return await api.stream_and_fetch_tasks(swarm_id)
1040
+
1041
+ if self.console:
1042
+ with Progress(
1043
+ SpinnerColumn(),
1044
+ TextColumn("[progress.description]{task.description}"),
1045
+ console=self.console,
1046
+ ) as progress:
1047
+ task = progress.add_task("Fetching swarm tasks...", total=None)
1048
+ tasks = asyncio.run(run())
1049
+ progress.update(task, completed=True)
1050
+ else:
1051
+ print("Fetching swarm tasks...")
1052
+ tasks = asyncio.run(run())
1053
+
1054
+ if not tasks:
1055
+ self.print("No tasks found for this swarm.")
1056
+ return 0
1057
+
1058
+ # Display tasks
1059
+ if self.console:
1060
+ table = Table(title=f"Tasks for Swarm: {swarm_id}")
1061
+ table.add_column("Task ID", style="cyan")
1062
+ table.add_column("Status", style="green")
1063
+ table.add_column("Node ID", style="blue")
1064
+ table.add_column("Module", style="magenta")
1065
+ table.add_column("Progress", style="yellow")
1066
+
1067
+ for task in tasks:
1068
+ table.add_row(
1069
+ task.get("task_id", "Unknown"),
1070
+ task.get("status", "Unknown"),
1071
+ task.get("node_id", "N/A"),
1072
+ task.get("module_id", "Unknown"),
1073
+ f"{task.get('progress', 0)}%",
1074
+ )
1075
+
1076
+ self.console.print(table)
1077
+ else:
1078
+ print(f"Tasks for Swarm: {swarm_id}")
1079
+ print("-" * 80)
1080
+ for task in tasks:
1081
+ print(
1082
+ f"{task.get('task_id', 'Unknown'):30} {task.get('status', 'Unknown'):15} {task.get('node_id', 'N/A'):20}"
1083
+ )
1084
+
1085
+ return 0
1086
+
1087
+ except Exception as e:
1088
+ self.print_error(f"Failed to get swarm tasks: {e}")
1089
+ return 1
1090
+
1091
+ def start_swarm(self, swarm_id: str) -> int:
1092
+ """Start a swarm."""
1093
+ api = self._get_user_api()
1094
+ if not api:
1095
+ return 1
1096
+
1097
+ try:
1098
+
1099
+ async def run():
1100
+ return await api.start_swarm(swarm_id)
1101
+
1102
+ if self.console:
1103
+ with Progress(
1104
+ SpinnerColumn(),
1105
+ TextColumn("[progress.description]{task.description}"),
1106
+ console=self.console,
1107
+ ) as progress:
1108
+ task = progress.add_task("Starting swarm...", total=None)
1109
+ result = asyncio.run(run())
1110
+ progress.update(task, completed=True)
1111
+ else:
1112
+ print("Starting swarm...")
1113
+ result = asyncio.run(run())
1114
+
1115
+ self.print_success(f"Swarm started: {result}")
1116
+ return 0
1117
+
1118
+ except Exception as e:
1119
+ self.print_error(f"Failed to start swarm: {e}")
1120
+ return 1
1121
+
1122
+ def stop_swarm(self, swarm_id: str, force: bool = False) -> int:
1123
+ """Stop a swarm."""
1124
+ if not force:
1125
+ if self.console:
1126
+ confirm = Confirm.ask(
1127
+ f"Are you sure you want to stop swarm '{swarm_id}'?"
1128
+ )
1129
+ if not confirm:
1130
+ self.print("Operation cancelled.")
1131
+ return 0
1132
+
1133
+ api = self._get_user_api()
1134
+ if not api:
1135
+ return 1
1136
+
1137
+ try:
1138
+
1139
+ async def run():
1140
+ return await api.stop_swarm(swarm_id, force)
1141
+
1142
+ if self.console:
1143
+ with Progress(
1144
+ SpinnerColumn(),
1145
+ TextColumn("[progress.description]{task.description}"),
1146
+ console=self.console,
1147
+ ) as progress:
1148
+ task = progress.add_task("Stopping swarm...", total=None)
1149
+ result = asyncio.run(run())
1150
+ progress.update(task, completed=True)
1151
+ else:
1152
+ print("Stopping swarm...")
1153
+ result = asyncio.run(run())
1154
+
1155
+ self.print_success(f"Swarm stopped: {result}")
1156
+ return 0
1157
+
1158
+ except Exception as e:
1159
+ self.print_error(f"Failed to stop swarm: {e}")
1160
+ return 1
1161
+
1162
+ def delete_swarm(self, swarm_id: str, force: bool = False) -> int:
1163
+ """Delete a swarm."""
1164
+ if not force:
1165
+ if self.console:
1166
+ confirm = Confirm.ask(
1167
+ f"Are you sure you want to delete swarm '{swarm_id}'?"
1168
+ )
1169
+ if not confirm:
1170
+ self.print("Operation cancelled.")
1171
+ return 0
1172
+
1173
+ api = self._get_user_api()
1174
+ if not api:
1175
+ return 1
1176
+
1177
+ try:
1178
+
1179
+ async def run():
1180
+ return await api.remove_swarm(swarm_id)
1181
+
1182
+ if self.console:
1183
+ with Progress(
1184
+ SpinnerColumn(),
1185
+ TextColumn("[progress.description]{task.description}"),
1186
+ console=self.console,
1187
+ ) as progress:
1188
+ task = progress.add_task("Deleting swarm...", total=None)
1189
+ result = asyncio.run(run())
1190
+ progress.update(task, completed=True)
1191
+ else:
1192
+ print("Deleting swarm...")
1193
+ result = asyncio.run(run())
1194
+
1195
+ self.print_success(f"Swarm deleted: {result}")
1196
+ return 0
1197
+
1198
+ except Exception as e:
1199
+ self.print_error(f"Failed to delete swarm: {e}")
1200
+ return 1
1201
+
1202
+ def deploy_swarm_interactive(self) -> int:
1203
+ """Interactive swarm deployment wizard."""
1204
+ api = self._get_user_api()
1205
+ if not api:
1206
+ return 1
1207
+
1208
+ try:
1209
+ # Welcome message
1210
+ if self.console:
1211
+ self.console.print(
1212
+ Panel.fit(
1213
+ "[bold cyan]Welcome to the Swarm Deployment Wizard![/bold cyan]\n\n"
1214
+ "This wizard will guide you through deploying a swarm to your cluster.\n"
1215
+ "You can deploy from a Python swarm definition file or create one interactively.",
1216
+ title="Swarm Deployment Wizard",
1217
+ )
1218
+ )
1219
+ else:
1220
+ self.print("=== Swarm Deployment Wizard ===")
1221
+ self.print("This wizard will guide you through deploying a swarm.")
1222
+ self.print(
1223
+ "You can deploy from a Python file or create one interactively."
1224
+ )
1225
+
1226
+ # Step 1: Choose deployment mode
1227
+ deployment_mode = self._choose_deployment_mode()
1228
+ if deployment_mode == "cancel":
1229
+ return 0
1230
+
1231
+ # Step 2: Select cluster
1232
+ cluster_id = self._select_cluster_interactive(api)
1233
+ if not cluster_id:
1234
+ return 1
1235
+
1236
+ # Step 3: Deploy based on mode
1237
+ if deployment_mode == "file":
1238
+ return self._deploy_from_file(api, cluster_id)
1239
+ else:
1240
+ return self._deploy_interactive(api, cluster_id)
1241
+
1242
+ except KeyboardInterrupt:
1243
+ self.print("\nDeployment cancelled by user.")
1244
+ return 1
1245
+ except Exception as e:
1246
+ self.print_error(f"Deployment failed: {e}")
1247
+ return 1
1248
+
1249
+ def monitor_swarm(self, swarm_id: str) -> int:
1250
+ """Monitor swarm execution in real-time."""
1251
+ self.print_error("Real-time swarm monitoring not yet implemented")
1252
+ self.print("Use 'manta swarm tasks' to see current status")
1253
+ return 1
1254
+
1255
+ # ============================================
1256
+ # Node Management Operations
1257
+ # ============================================
1258
+
1259
+ def handle_node_commands(self, args) -> int:
1260
+ """Handle node management commands."""
1261
+ if not args.node_command:
1262
+ self.print_error("Node command required")
1263
+ return 1
1264
+
1265
+ if args.node_command == "list":
1266
+ return self.list_nodes(args.cluster_id, getattr(args, "available", False))
1267
+ elif args.node_command == "list-ids":
1268
+ return self.list_node_ids(
1269
+ args.cluster_id, getattr(args, "available", False)
1270
+ )
1271
+ elif args.node_command == "show":
1272
+ return self.show_node(args.cluster_id, args.node_id)
1273
+ elif args.node_command == "resources":
1274
+ return self.show_node_resources(args.cluster_id, args.node_id)
1275
+ elif args.node_command == "tasks":
1276
+ return self.show_node_tasks(args.cluster_id, args.node_id)
1277
+ elif args.node_command == "stop":
1278
+ return self.stop_node(args.cluster_id, args.node_id)
1279
+ elif args.node_command == "remove":
1280
+ return self.remove_node(
1281
+ args.cluster_id, args.node_id, getattr(args, "force", False)
1282
+ )
1283
+ elif args.node_command == "errors":
1284
+ return self.collect_node_errors(args.cluster_id)
1285
+ elif args.node_command == "monitor":
1286
+ return self.monitor_node(args.cluster_id, args.node_id)
1287
+ else:
1288
+ self.print_error(f"Unknown node command: {args.node_command}")
1289
+ return 1
1290
+
1291
+ def list_nodes(self, cluster_id: str, available_only: bool = False) -> int:
1292
+ """List nodes in cluster."""
1293
+ api = self._get_user_api()
1294
+ if not api:
1295
+ return 1
1296
+
1297
+ try:
1298
+
1299
+ async def run():
1300
+ return await api.stream_and_fetch_nodes(cluster_id)
1301
+
1302
+ if self.console:
1303
+ with Progress(
1304
+ SpinnerColumn(),
1305
+ TextColumn("[progress.description]{task.description}"),
1306
+ console=self.console,
1307
+ ) as progress:
1308
+ task = progress.add_task("Fetching nodes...", total=None)
1309
+ nodes = asyncio.run(run())
1310
+ progress.update(task, completed=True)
1311
+ else:
1312
+ print("Fetching nodes...")
1313
+ nodes = asyncio.run(run())
1314
+
1315
+ if not nodes:
1316
+ self.print("No nodes found.")
1317
+ return 0
1318
+
1319
+ # Filter available nodes if requested
1320
+ if available_only:
1321
+ nodes = [n for n in nodes if n.get("status") == "available"]
1322
+
1323
+ # Display nodes
1324
+ if self.console:
1325
+ table = Table(title=f"Nodes in Cluster: {cluster_id}")
1326
+ table.add_column("Node ID", style="cyan")
1327
+ table.add_column("Status", style="green")
1328
+ table.add_column("CPU", style="blue")
1329
+ table.add_column("Memory", style="magenta")
1330
+ table.add_column("Tasks", style="yellow")
1331
+
1332
+ for node in nodes:
1333
+ resources = node.get("resources", {})
1334
+ table.add_row(
1335
+ node.get("node_id", "Unknown"),
1336
+ node.get("status", "Unknown"),
1337
+ f"{resources.get('cpu', 0)}%",
1338
+ f"{resources.get('memory', 0)} MB",
1339
+ str(node.get("running_tasks", 0)),
1340
+ )
1341
+
1342
+ self.console.print(table)
1343
+ else:
1344
+ print(f"Nodes in Cluster: {cluster_id}")
1345
+ print("-" * 80)
1346
+ for node in nodes:
1347
+ print(
1348
+ f"{node.get('node_id', 'Unknown'):30} {node.get('status', 'Unknown'):15} Tasks: {node.get('running_tasks', 0)}"
1349
+ )
1350
+
1351
+ return 0
1352
+
1353
+ except Exception as e:
1354
+ self.print_error(f"Failed to list nodes: {e}")
1355
+ return 1
1356
+
1357
+ def list_node_ids(self, cluster_id: str, available_only: bool = False) -> int:
1358
+ """List node IDs only."""
1359
+ api = self._get_user_api()
1360
+ if not api:
1361
+ return 1
1362
+
1363
+ try:
1364
+
1365
+ async def run():
1366
+ return await api.list_node_ids(cluster_id, available_only)
1367
+
1368
+ if self.console:
1369
+ with Progress(
1370
+ SpinnerColumn(),
1371
+ TextColumn("[progress.description]{task.description}"),
1372
+ console=self.console,
1373
+ ) as progress:
1374
+ task = progress.add_task("Fetching node IDs...", total=None)
1375
+ node_ids = asyncio.run(run())
1376
+ progress.update(task, completed=True)
1377
+ else:
1378
+ print("Fetching node IDs...")
1379
+ node_ids = asyncio.run(run())
1380
+
1381
+ if not node_ids:
1382
+ self.print("No nodes found.")
1383
+ return 0
1384
+
1385
+ status_text = "available " if available_only else ""
1386
+ self.print(f"Found {len(node_ids)} {status_text}nodes:")
1387
+ for node_id in node_ids:
1388
+ self.print(f" {node_id}")
1389
+
1390
+ return 0
1391
+
1392
+ except Exception as e:
1393
+ self.print_error(f"Failed to list node IDs: {e}")
1394
+ return 1
1395
+
1396
+ def show_node(self, cluster_id: str, node_id: str) -> int:
1397
+ """Show detailed node information."""
1398
+ api = self._get_user_api()
1399
+ if not api:
1400
+ return 1
1401
+
1402
+ try:
1403
+
1404
+ async def run():
1405
+ return await api.get_node(cluster_id, node_id)
1406
+
1407
+ if self.console:
1408
+ with Progress(
1409
+ SpinnerColumn(),
1410
+ TextColumn("[progress.description]{task.description}"),
1411
+ console=self.console,
1412
+ ) as progress:
1413
+ task = progress.add_task("Fetching node details...", total=None)
1414
+ node = asyncio.run(run())
1415
+ progress.update(task, completed=True)
1416
+ else:
1417
+ print("Fetching node details...")
1418
+ node = asyncio.run(run())
1419
+
1420
+ # Display node details
1421
+ if self.console:
1422
+ resources = node.get("resources", {})
1423
+ panel = Panel.fit(
1424
+ f"[cyan]Node ID:[/cyan] {node.get('node_id', 'Unknown')}\n"
1425
+ f"[cyan]Status:[/cyan] {node.get('status', 'Unknown')}\n"
1426
+ f"[cyan]Cluster ID:[/cyan] {cluster_id}\n"
1427
+ f"[cyan]CPU Usage:[/cyan] {resources.get('cpu', 0)}%\n"
1428
+ f"[cyan]Memory Usage:[/cyan] {resources.get('memory', 0)} MB\n"
1429
+ f"[cyan]Running Tasks:[/cyan] {node.get('running_tasks', 0)}\n"
1430
+ f"[cyan]Last Seen:[/cyan] {node.get('last_seen', 'Unknown')}",
1431
+ title=f"Node: {node_id}",
1432
+ )
1433
+ self.console.print(panel)
1434
+ else:
1435
+ print(f"Node: {node_id}")
1436
+ print(f" Status: {node.get('status', 'Unknown')}")
1437
+ print(f" Cluster ID: {cluster_id}")
1438
+ print(f" Running Tasks: {node.get('running_tasks', 0)}")
1439
+
1440
+ return 0
1441
+
1442
+ except Exception as e:
1443
+ self.print_error(f"Failed to get node details: {e}")
1444
+ return 1
1445
+
1446
+ def show_node_resources(self, cluster_id: str, node_id: str) -> int:
1447
+ """Show current node resources."""
1448
+ api = self._get_user_api()
1449
+ if not api:
1450
+ return 1
1451
+
1452
+ try:
1453
+
1454
+ async def run():
1455
+ return await api.get_node_resources(cluster_id, node_id)
1456
+
1457
+ if self.console:
1458
+ with Progress(
1459
+ SpinnerColumn(),
1460
+ TextColumn("[progress.description]{task.description}"),
1461
+ console=self.console,
1462
+ ) as progress:
1463
+ task = progress.add_task("Fetching node resources...", total=None)
1464
+ resources = asyncio.run(run())
1465
+ progress.update(task, completed=True)
1466
+ else:
1467
+ print("Fetching node resources...")
1468
+ resources = asyncio.run(run())
1469
+
1470
+ # Display resources
1471
+ if self.console:
1472
+ panel = Panel.fit(
1473
+ f"[cyan]CPU Usage:[/cyan] {resources.get('cpu', 0)}%\n"
1474
+ f"[cyan]Memory Usage:[/cyan] {resources.get('memory', 0)} MB\n"
1475
+ f"[cyan]Disk Usage:[/cyan] {resources.get('disk', 0)} MB\n"
1476
+ f"[cyan]GPU Usage:[/cyan] {resources.get('gpu', 'N/A')}%\n"
1477
+ f"[cyan]Network I/O:[/cyan] {resources.get('network_io', 'N/A')} MB/s\n"
1478
+ f"[cyan]Last Updated:[/cyan] {resources.get('timestamp', 'Unknown')}",
1479
+ title=f"Resources for Node: {node_id}",
1480
+ )
1481
+ self.console.print(panel)
1482
+ else:
1483
+ print(f"Resources for Node: {node_id}")
1484
+ print(f" CPU Usage: {resources.get('cpu', 0)}%")
1485
+ print(f" Memory Usage: {resources.get('memory', 0)} MB")
1486
+ print(f" Disk Usage: {resources.get('disk', 0)} MB")
1487
+
1488
+ return 0
1489
+
1490
+ except Exception as e:
1491
+ self.print_error(f"Failed to get node resources: {e}")
1492
+ return 1
1493
+
1494
+ def show_node_tasks(self, cluster_id: str, node_id: str) -> int:
1495
+ """Show tasks assigned to a node."""
1496
+ api = self._get_user_api()
1497
+ if not api:
1498
+ return 1
1499
+
1500
+ try:
1501
+
1502
+ async def run():
1503
+ return await api.select_tasks(cluster_id, node_id)
1504
+
1505
+ if self.console:
1506
+ with Progress(
1507
+ SpinnerColumn(),
1508
+ TextColumn("[progress.description]{task.description}"),
1509
+ console=self.console,
1510
+ ) as progress:
1511
+ task = progress.add_task("Fetching node tasks...", total=None)
1512
+ tasks = asyncio.run(run())
1513
+ progress.update(task, completed=True)
1514
+ else:
1515
+ print("Fetching node tasks...")
1516
+ tasks = asyncio.run(run())
1517
+
1518
+ if not tasks:
1519
+ self.print(f"No tasks found for node '{node_id}'.")
1520
+ return 0
1521
+
1522
+ # Display tasks
1523
+ if self.console:
1524
+ table = Table(title=f"Tasks for Node: {node_id}")
1525
+ table.add_column("Task ID", style="cyan")
1526
+ table.add_column("Swarm ID", style="blue")
1527
+ table.add_column("Status", style="green")
1528
+ table.add_column("Progress", style="yellow")
1529
+
1530
+ for task in tasks:
1531
+ table.add_row(
1532
+ task.get("task_id", "Unknown"),
1533
+ task.get("swarm_id", "Unknown"),
1534
+ task.get("status", "Unknown"),
1535
+ f"{task.get('progress', 0)}%",
1536
+ )
1537
+
1538
+ self.console.print(table)
1539
+ else:
1540
+ print(f"Tasks for Node: {node_id}")
1541
+ print("-" * 80)
1542
+ for task in tasks:
1543
+ print(
1544
+ f"{task.get('task_id', 'Unknown'):30} {task.get('swarm_id', 'Unknown'):30} {task.get('status', 'Unknown')}"
1545
+ )
1546
+
1547
+ return 0
1548
+
1549
+ except Exception as e:
1550
+ self.print_error(f"Failed to get node tasks: {e}")
1551
+ return 1
1552
+
1553
+ def stop_node(self, cluster_id: str, node_id: str) -> int:
1554
+ """Stop a node."""
1555
+ api = self._get_user_api()
1556
+ if not api:
1557
+ return 1
1558
+
1559
+ try:
1560
+
1561
+ async def run():
1562
+ return await api.stop_node(cluster_id, node_id)
1563
+
1564
+ if self.console:
1565
+ with Progress(
1566
+ SpinnerColumn(),
1567
+ TextColumn("[progress.description]{task.description}"),
1568
+ console=self.console,
1569
+ ) as progress:
1570
+ task = progress.add_task("Stopping node...", total=None)
1571
+ result = asyncio.run(run())
1572
+ progress.update(task, completed=True)
1573
+ else:
1574
+ print("Stopping node...")
1575
+ result = asyncio.run(run())
1576
+
1577
+ self.print_success(f"Node stopped: {result}")
1578
+ return 0
1579
+
1580
+ except Exception as e:
1581
+ self.print_error(f"Failed to stop node: {e}")
1582
+ return 1
1583
+
1584
+ def remove_node(self, cluster_id: str, node_id: str, force: bool = False) -> int:
1585
+ """Remove a node from cluster."""
1586
+ if not force:
1587
+ if self.console:
1588
+ confirm = Confirm.ask(
1589
+ f"Are you sure you want to remove node '{node_id}' from cluster?"
1590
+ )
1591
+ if not confirm:
1592
+ self.print("Operation cancelled.")
1593
+ return 0
1594
+
1595
+ api = self._get_user_api()
1596
+ if not api:
1597
+ return 1
1598
+
1599
+ try:
1600
+
1601
+ async def run():
1602
+ return await api.remove_node(cluster_id, node_id)
1603
+
1604
+ if self.console:
1605
+ with Progress(
1606
+ SpinnerColumn(),
1607
+ TextColumn("[progress.description]{task.description}"),
1608
+ console=self.console,
1609
+ ) as progress:
1610
+ task = progress.add_task("Removing node...", total=None)
1611
+ result = asyncio.run(run())
1612
+ progress.update(task, completed=True)
1613
+ else:
1614
+ print("Removing node...")
1615
+ result = asyncio.run(run())
1616
+
1617
+ self.print_success(f"Node removed: {result}")
1618
+ return 0
1619
+
1620
+ except Exception as e:
1621
+ self.print_error(f"Failed to remove node: {e}")
1622
+ return 1
1623
+
1624
+ def collect_node_errors(self, cluster_id: str) -> int:
1625
+ """Collect errors from nodes in cluster."""
1626
+ api = self._get_user_api()
1627
+ if not api:
1628
+ return 1
1629
+
1630
+ try:
1631
+
1632
+ async def run():
1633
+ return await api.collect_errors(cluster_id)
1634
+
1635
+ if self.console:
1636
+ with Progress(
1637
+ SpinnerColumn(),
1638
+ TextColumn("[progress.description]{task.description}"),
1639
+ console=self.console,
1640
+ ) as progress:
1641
+ task = progress.add_task("Collecting errors...", total=None)
1642
+ errors = asyncio.run(run())
1643
+ progress.update(task, completed=True)
1644
+ else:
1645
+ print("Collecting errors...")
1646
+ errors = asyncio.run(run())
1647
+
1648
+ if not errors:
1649
+ self.print_success("No errors found.")
1650
+ return 0
1651
+
1652
+ # Display errors
1653
+ if self.console:
1654
+ table = Table(title=f"Errors in Cluster: {cluster_id}")
1655
+ table.add_column("Time", style="cyan")
1656
+ table.add_column("Node ID", style="blue")
1657
+ table.add_column("Error", style="red")
1658
+
1659
+ for error in errors:
1660
+ table.add_row(
1661
+ error.get("timestamp", "Unknown"),
1662
+ error.get("node_id", "Unknown"),
1663
+ error.get("error_message", "Unknown"),
1664
+ )
1665
+
1666
+ self.console.print(table)
1667
+ else:
1668
+ print(f"Errors in Cluster: {cluster_id}")
1669
+ print("-" * 80)
1670
+ for error in errors:
1671
+ print(
1672
+ f"{error.get('timestamp', 'Unknown'):20} {error.get('node_id', 'Unknown'):20} {error.get('error_message', 'Unknown')}"
1673
+ )
1674
+
1675
+ return 0
1676
+
1677
+ except Exception as e:
1678
+ self.print_error(f"Failed to collect errors: {e}")
1679
+ return 1
1680
+
1681
+ def monitor_node(self, cluster_id: str, node_id: str) -> int:
1682
+ """Monitor node resources in real-time."""
1683
+ self.print_error("Real-time node monitoring not yet implemented")
1684
+ self.print("Use 'manta node resources' to see current status")
1685
+ return 1
1686
+
1687
+ # ============================================
1688
+ # Results Management Operations
1689
+ # ============================================
1690
+
1691
+ def handle_results_commands(self, args) -> int:
1692
+ """Handle results management commands."""
1693
+ if not args.results_command:
1694
+ self.print_error("Results command required")
1695
+ return 1
1696
+
1697
+ if args.results_command == "list":
1698
+ return self.list_results(args.swarm_id, args.tag)
1699
+ elif args.results_command == "list-tags":
1700
+ return self.list_result_tags(args.swarm_id)
1701
+ elif args.results_command == "list-global-tags":
1702
+ return self.list_global_tags(args.swarm_id)
1703
+ elif args.results_command == "stream":
1704
+ return self.stream_results(args.swarm_id, args.tag)
1705
+ elif args.results_command == "export":
1706
+ return self.export_results(
1707
+ args.swarm_id, args.tag, getattr(args, "output_file", None)
1708
+ )
1709
+ elif args.results_command == "delete":
1710
+ return self.delete_results(
1711
+ args.swarm_id, args.tag, getattr(args, "force", False)
1712
+ )
1713
+ elif args.results_command == "global":
1714
+ return self.select_global_data(args.swarm_id, args.tag)
1715
+ else:
1716
+ self.print_error(f"Unknown results command: {args.results_command}")
1717
+ return 1
1718
+
1719
+ def list_results(self, swarm_id: str, tag: str) -> int:
1720
+ """List results for a swarm and tag."""
1721
+ api = self._get_user_api()
1722
+ if not api:
1723
+ return 1
1724
+
1725
+ try:
1726
+
1727
+ async def run():
1728
+ return await api.select_results(swarm_id, tag)
1729
+
1730
+ if self.console:
1731
+ with Progress(
1732
+ SpinnerColumn(),
1733
+ TextColumn("[progress.description]{task.description}"),
1734
+ console=self.console,
1735
+ ) as progress:
1736
+ task = progress.add_task("Fetching results...", total=None)
1737
+ results = asyncio.run(run())
1738
+ progress.update(task, completed=True)
1739
+ else:
1740
+ print("Fetching results...")
1741
+ results = asyncio.run(run())
1742
+
1743
+ if not results or swarm_id not in results:
1744
+ self.print("No results found.")
1745
+ return 0
1746
+
1747
+ result_data = results[swarm_id]
1748
+
1749
+ # Display results summary
1750
+ if self.console:
1751
+ panel = Panel.fit(
1752
+ f"[cyan]Swarm ID:[/cyan] {swarm_id}\n"
1753
+ f"[cyan]Tag:[/cyan] {tag}\n"
1754
+ f"[cyan]Iterations:[/cyan] {len(result_data.data)}\n"
1755
+ f"[cyan]Node Count:[/cyan] {len(set(node_id for iteration in result_data.data for node_id in iteration.keys()))}\n"
1756
+ f"[cyan]Total Results:[/cyan] {sum(len(nodes) for nodes in result_data.data)}",
1757
+ title=f"Results: {swarm_id}/{tag}",
1758
+ )
1759
+ self.console.print(panel)
1760
+
1761
+ # Show sample of results
1762
+ if result_data.data:
1763
+ table = Table(title="Sample Results (First Iteration)")
1764
+ table.add_column("Node ID", style="cyan")
1765
+ table.add_column("Value", style="green")
1766
+
1767
+ first_iteration = result_data.data[0]
1768
+ for node_id, node_data in first_iteration.items():
1769
+ value = str(node_data.get(tag, "N/A"))[
1770
+ :50
1771
+ ] # Truncate long values
1772
+ table.add_row(node_id, value)
1773
+
1774
+ self.console.print(table)
1775
+ else:
1776
+ print(f"Results: {swarm_id}/{tag}")
1777
+ print(f" Iterations: {len(result_data.data)}")
1778
+ print(
1779
+ f" Total Results: {sum(len(nodes) for nodes in result_data.data)}"
1780
+ )
1781
+
1782
+ return 0
1783
+
1784
+ except Exception as e:
1785
+ self.print_error(f"Failed to list results: {e}")
1786
+ return 1
1787
+
1788
+ def list_result_tags(self, swarm_id: str) -> int:
1789
+ """List available result tags for a swarm."""
1790
+ api = self._get_user_api()
1791
+ if not api:
1792
+ return 1
1793
+
1794
+ try:
1795
+
1796
+ async def run():
1797
+ return await api.list_result_tags(swarm_id)
1798
+
1799
+ if self.console:
1800
+ with Progress(
1801
+ SpinnerColumn(),
1802
+ TextColumn("[progress.description]{task.description}"),
1803
+ console=self.console,
1804
+ ) as progress:
1805
+ task = progress.add_task("Fetching result tags...", total=None)
1806
+ tags = asyncio.run(run())
1807
+ progress.update(task, completed=True)
1808
+ else:
1809
+ print("Fetching result tags...")
1810
+ tags = asyncio.run(run())
1811
+
1812
+ if not tags or not tags.get("tags"):
1813
+ self.print("No result tags found.")
1814
+ return 0
1815
+
1816
+ # Display tags
1817
+ if self.console:
1818
+ table = Table(title=f"Result Tags for Swarm: {swarm_id}")
1819
+ table.add_column("Tag", style="cyan")
1820
+ table.add_column("Count", style="blue")
1821
+ table.add_column("Last Updated", style="green")
1822
+
1823
+ for tag_name, tag_info in tags.get("tags", {}).items():
1824
+ table.add_row(
1825
+ tag_name,
1826
+ str(tag_info.get("count", 0)),
1827
+ tag_info.get("last_updated", "Unknown"),
1828
+ )
1829
+
1830
+ self.console.print(table)
1831
+ else:
1832
+ print(f"Result Tags for Swarm: {swarm_id}")
1833
+ print("-" * 60)
1834
+ for tag_name, tag_info in tags.get("tags", {}).items():
1835
+ print(f"{tag_name:30} Count: {tag_info.get('count', 0)}")
1836
+
1837
+ return 0
1838
+
1839
+ except Exception as e:
1840
+ self.print_error(f"Failed to list result tags: {e}")
1841
+ return 1
1842
+
1843
+ def list_global_tags(self, swarm_id: str) -> int:
1844
+ """List available global tags for a swarm."""
1845
+ api = self._get_user_api()
1846
+ if not api:
1847
+ return 1
1848
+
1849
+ try:
1850
+
1851
+ async def run():
1852
+ return await api.list_global_tags(swarm_id)
1853
+
1854
+ if self.console:
1855
+ with Progress(
1856
+ SpinnerColumn(),
1857
+ TextColumn("[progress.description]{task.description}"),
1858
+ console=self.console,
1859
+ ) as progress:
1860
+ task = progress.add_task("Fetching global tags...", total=None)
1861
+ tags = asyncio.run(run())
1862
+ progress.update(task, completed=True)
1863
+ else:
1864
+ print("Fetching global tags...")
1865
+ tags = asyncio.run(run())
1866
+
1867
+ if not tags or not tags.get("tags"):
1868
+ self.print("No global tags found.")
1869
+ return 0
1870
+
1871
+ # Display global tags
1872
+ if self.console:
1873
+ table = Table(title=f"Global Tags for Swarm: {swarm_id}")
1874
+ table.add_column("Tag", style="cyan")
1875
+ table.add_column("Type", style="blue")
1876
+ table.add_column("Size", style="green")
1877
+
1878
+ for tag_name, tag_info in tags.get("tags", {}).items():
1879
+ table.add_row(
1880
+ tag_name,
1881
+ tag_info.get("type", "Unknown"),
1882
+ self._format_size(tag_info.get("size", 0)),
1883
+ )
1884
+
1885
+ self.console.print(table)
1886
+ else:
1887
+ print(f"Global Tags for Swarm: {swarm_id}")
1888
+ print("-" * 60)
1889
+ for tag_name, tag_info in tags.get("tags", {}).items():
1890
+ print(f"{tag_name:30} Type: {tag_info.get('type', 'Unknown')}")
1891
+
1892
+ return 0
1893
+
1894
+ except Exception as e:
1895
+ self.print_error(f"Failed to list global tags: {e}")
1896
+ return 1
1897
+
1898
+ def stream_results(self, swarm_id: str, tag: str) -> int:
1899
+ """Stream results in real-time."""
1900
+ api = self._get_user_api()
1901
+ if not api:
1902
+ return 1
1903
+
1904
+ async def run_stream():
1905
+ try:
1906
+ # Set up signal handling for graceful shutdown
1907
+ stop_streaming = False
1908
+
1909
+ def signal_handler(_signum, _frame):
1910
+ nonlocal stop_streaming
1911
+ stop_streaming = True
1912
+
1913
+ signal.signal(signal.SIGINT, signal_handler)
1914
+ signal.signal(signal.SIGTERM, signal_handler)
1915
+
1916
+ # Create Rich components
1917
+ table = Table(title=f"Real-time Results: {swarm_id} (tag: {tag})")
1918
+ table.add_column("Timestamp", style="cyan")
1919
+ table.add_column("Node ID", style="blue")
1920
+ table.add_column("Task ID", style="magenta")
1921
+ table.add_column("Iteration", style="green")
1922
+ table.add_column("Data Size", style="yellow")
1923
+
1924
+ # Status panel for connection info
1925
+ status_panel = Panel(
1926
+ "[green]● Connected[/green] - Streaming results...\n"
1927
+ + "Press Ctrl+C to stop streaming",
1928
+ title="Streaming Status",
1929
+ border_style="green",
1930
+ )
1931
+
1932
+ # Counters for statistics
1933
+ result_count = 0
1934
+ start_time = time.time()
1935
+ last_result_time = None
1936
+
1937
+ # Display initial table with status
1938
+ if self.console:
1939
+ with Live(console=self.console, refresh_per_second=4) as live:
1940
+ # Start streaming
1941
+ try:
1942
+ async for result in api.stream_results(swarm_id, tag):
1943
+ if stop_streaming:
1944
+ break
1945
+
1946
+ # Process result data
1947
+ timestamp = result.get(
1948
+ "timestamp", datetime.now().strftime("%H:%M:%S")
1949
+ )
1950
+ node_id = result.get("node_id", "Unknown")
1951
+ task_id = result.get("task_id", "Unknown")
1952
+ iteration = result.get("iteration", 0)
1953
+ data = result.get("data", b"")
1954
+ data_size = (
1955
+ len(data) if isinstance(data, (bytes, str)) else 0
1956
+ )
1957
+
1958
+ # Add to table
1959
+ table.add_row(
1960
+ str(timestamp),
1961
+ str(node_id),
1962
+ str(task_id),
1963
+ str(iteration),
1964
+ self._format_size(data_size),
1965
+ )
1966
+
1967
+ # Update counters
1968
+ result_count += 1
1969
+ last_result_time = time.time()
1970
+ elapsed = last_result_time - start_time
1971
+
1972
+ # Update status panel with stats
1973
+ status_text = (
1974
+ f"[green]● Connected[/green] - Streaming results...\n"
1975
+ f"Results received: {result_count}\n"
1976
+ f"Elapsed time: {elapsed:.1f}s\n"
1977
+ f"Press Ctrl+C to stop streaming"
1978
+ )
1979
+ status_panel = Panel(
1980
+ status_text,
1981
+ title="Streaming Status",
1982
+ border_style="green",
1983
+ )
1984
+
1985
+ # Update live display
1986
+ display = Columns(
1987
+ [status_panel, table], equal=False, expand=True
1988
+ )
1989
+ live.update(display)
1990
+
1991
+ except KeyboardInterrupt:
1992
+ stop_streaming = True
1993
+ except Exception as e:
1994
+ # Update status to show error
1995
+ error_panel = Panel(
1996
+ f"[red]✗ Connection Error[/red]: {str(e)}\n"
1997
+ + "Attempting to reconnect...",
1998
+ title="Streaming Status",
1999
+ border_style="red",
2000
+ )
2001
+ display = Columns(
2002
+ [error_panel, table], equal=False, expand=True
2003
+ )
2004
+ live.update(display)
2005
+
2006
+ # Wait a bit before trying to reconnect
2007
+ await asyncio.sleep(2)
2008
+
2009
+ # Final status update
2010
+ final_status = (
2011
+ f"[yellow]● Disconnected[/yellow] - Streaming stopped.\n"
2012
+ f"Total results received: {result_count}\n"
2013
+ f"Total time: {time.time() - start_time:.1f}s"
2014
+ )
2015
+ final_panel = Panel(
2016
+ final_status,
2017
+ title="Streaming Complete",
2018
+ border_style="yellow",
2019
+ )
2020
+ display = Columns(
2021
+ [final_panel, table], equal=False, expand=True
2022
+ )
2023
+ live.update(display)
2024
+
2025
+ # Brief pause to show final status
2026
+ await asyncio.sleep(1)
2027
+ else:
2028
+ # Fallback for non-Rich console
2029
+ print(f"Streaming results for swarm {swarm_id}, tag {tag}...")
2030
+ print("Press Ctrl+C to stop streaming")
2031
+
2032
+ try:
2033
+ async for result in api.stream_results(swarm_id, tag):
2034
+ if stop_streaming:
2035
+ break
2036
+
2037
+ timestamp = result.get(
2038
+ "timestamp", datetime.now().strftime("%H:%M:%S")
2039
+ )
2040
+ node_id = result.get("node_id", "Unknown")
2041
+ task_id = result.get("task_id", "Unknown")
2042
+ iteration = result.get("iteration", 0)
2043
+
2044
+ print(
2045
+ f"[{timestamp}] Node: {node_id}, Task: {task_id}, Iteration: {iteration}"
2046
+ )
2047
+ result_count += 1
2048
+
2049
+ except KeyboardInterrupt:
2050
+ pass
2051
+
2052
+ print(
2053
+ f"\nStreaming stopped. Total results received: {result_count}"
2054
+ )
2055
+
2056
+ return 0
2057
+
2058
+ except Exception as e:
2059
+ if self.console:
2060
+ self.print_error(f"Failed to stream results: {e}")
2061
+ else:
2062
+ print(f"Error: Failed to stream results: {e}")
2063
+ return 1
2064
+
2065
+ try:
2066
+ if self.console:
2067
+ self.print(
2068
+ f"[cyan]Starting result streaming for swarm {swarm_id}, tag {tag}...[/cyan]"
2069
+ )
2070
+ else:
2071
+ print(f"Starting result streaming for swarm {swarm_id}, tag {tag}...")
2072
+
2073
+ return asyncio.run(run_stream())
2074
+
2075
+ except Exception as e:
2076
+ self.print_error(f"Failed to start result streaming: {e}")
2077
+ return 1
2078
+
2079
+ def export_results(
2080
+ self, swarm_id: str, tag: str, output_file: Optional[str] = None
2081
+ ) -> int:
2082
+ """Export results to file."""
2083
+ self.print_error("Results export not yet implemented in CLI")
2084
+ self.print("Use the SDK directly for now:")
2085
+ self.print(" from manta.apis import AsyncUserAPI")
2086
+ self.print(" # Export results to file")
2087
+ return 1
2088
+
2089
+ def delete_results(self, swarm_id: str, tag: str, force: bool = False) -> int:
2090
+ """Delete results for a swarm and tag."""
2091
+ if not force:
2092
+ if self.console:
2093
+ confirm = Confirm.ask(
2094
+ f"Are you sure you want to delete results '{tag}' for swarm '{swarm_id}'?"
2095
+ )
2096
+ if not confirm:
2097
+ self.print("Operation cancelled.")
2098
+ return 0
2099
+
2100
+ api = self._get_user_api()
2101
+ if not api:
2102
+ return 1
2103
+
2104
+ try:
2105
+
2106
+ async def run():
2107
+ return await api.delete_results(swarm_id, tag)
2108
+
2109
+ if self.console:
2110
+ with Progress(
2111
+ SpinnerColumn(),
2112
+ TextColumn("[progress.description]{task.description}"),
2113
+ console=self.console,
2114
+ ) as progress:
2115
+ task = progress.add_task("Deleting results...", total=None)
2116
+ result = asyncio.run(run())
2117
+ progress.update(task, completed=True)
2118
+ else:
2119
+ print("Deleting results...")
2120
+ result = asyncio.run(run())
2121
+
2122
+ self.print_success(f"Results deleted: {result}")
2123
+ return 0
2124
+
2125
+ except Exception as e:
2126
+ self.print_error(f"Failed to delete results: {e}")
2127
+ return 1
2128
+
2129
+ def select_global_data(self, swarm_id: str, tag: str) -> int:
2130
+ """Select global data for a swarm and tag."""
2131
+ api = self._get_user_api()
2132
+ if not api:
2133
+ return 1
2134
+
2135
+ try:
2136
+
2137
+ async def run():
2138
+ global_data = []
2139
+ async for data in api.select_global(swarm_id, tag):
2140
+ global_data.append(data)
2141
+ return global_data
2142
+
2143
+ if self.console:
2144
+ with Progress(
2145
+ SpinnerColumn(),
2146
+ TextColumn("[progress.description]{task.description}"),
2147
+ console=self.console,
2148
+ ) as progress:
2149
+ task = progress.add_task("Fetching global data...", total=None)
2150
+ global_data = asyncio.run(run())
2151
+ progress.update(task, completed=True)
2152
+ else:
2153
+ print("Fetching global data...")
2154
+ global_data = asyncio.run(run())
2155
+
2156
+ if not global_data:
2157
+ self.print("No global data found.")
2158
+ return 0
2159
+
2160
+ # Display global data
2161
+ self.print(f"Global data for {swarm_id}/{tag}:")
2162
+ if self.console:
2163
+ for i, data in enumerate(global_data[:5]): # Show first 5 entries
2164
+ panel = Panel.fit(
2165
+ json.dumps(data, indent=2)[:500]
2166
+ + ("..." if len(json.dumps(data, indent=2)) > 500 else ""),
2167
+ title=f"Entry {i + 1}",
2168
+ )
2169
+ self.console.print(panel)
2170
+
2171
+ if len(global_data) > 5:
2172
+ self.print(f"... and {len(global_data) - 5} more entries")
2173
+ else:
2174
+ for i, data in enumerate(global_data[:3]): # Show first 3 entries
2175
+ print(f"Entry {i + 1}: {str(data)[:200]}")
2176
+ if len(global_data) > 3:
2177
+ print(f"... and {len(global_data) - 3} more entries")
2178
+
2179
+ return 0
2180
+
2181
+ except Exception as e:
2182
+ self.print_error(f"Failed to select global data: {e}")
2183
+ return 1
2184
+
2185
+ # ============================================
2186
+ # Logging Operations
2187
+ # ============================================
2188
+
2189
+ def handle_logs_commands(self, args) -> int:
2190
+ """Handle logging commands."""
2191
+ if not args.logs_command:
2192
+ self.print_error("Logs command required")
2193
+ return 1
2194
+
2195
+ if args.logs_command == "collect":
2196
+ return self.collect_logs(
2197
+ args.swarm_id,
2198
+ getattr(args, "node_ids", None),
2199
+ getattr(args, "task_ids", None),
2200
+ getattr(args, "severity", None),
2201
+ )
2202
+ elif args.logs_command == "stream":
2203
+ return self.stream_logs(args.swarm_id)
2204
+ elif args.logs_command == "export":
2205
+ return self.export_logs(args.swarm_id, getattr(args, "output_file", None))
2206
+ else:
2207
+ self.print_error(f"Unknown logs command: {args.logs_command}")
2208
+ return 1
2209
+
2210
+ def collect_logs(
2211
+ self,
2212
+ swarm_id: str,
2213
+ node_ids: Optional[List[str]] = None,
2214
+ task_ids: Optional[List[str]] = None,
2215
+ severity: Optional[List[str]] = None,
2216
+ ) -> int:
2217
+ """Collect logs with filtering."""
2218
+ api = self._get_user_api()
2219
+ if not api:
2220
+ return 1
2221
+
2222
+ try:
2223
+
2224
+ async def run():
2225
+ return await api.collect_logs(
2226
+ swarm_id=swarm_id,
2227
+ node_ids=node_ids,
2228
+ task_ids=task_ids,
2229
+ severity=severity,
2230
+ )
2231
+
2232
+ if self.console:
2233
+ with Progress(
2234
+ SpinnerColumn(),
2235
+ TextColumn("[progress.description]{task.description}"),
2236
+ console=self.console,
2237
+ ) as progress:
2238
+ task = progress.add_task("Collecting logs...", total=None)
2239
+ logs = asyncio.run(run())
2240
+ progress.update(task, completed=True)
2241
+ else:
2242
+ print("Collecting logs...")
2243
+ logs = asyncio.run(run())
2244
+
2245
+ if not logs:
2246
+ self.print("No logs found.")
2247
+ return 0
2248
+
2249
+ # Display logs
2250
+ if self.console:
2251
+ table = Table(title=f"Logs for Swarm: {swarm_id}")
2252
+ table.add_column("Time", style="cyan")
2253
+ table.add_column("Node", style="blue")
2254
+ table.add_column("Task", style="magenta")
2255
+ table.add_column("Severity", style="yellow")
2256
+ table.add_column("Message", style="white")
2257
+
2258
+ for log_entry in logs[-50:]: # Show last 50 entries
2259
+ table.add_row(
2260
+ log_entry.get("timestamp", "Unknown"),
2261
+ (
2262
+ log_entry.get("node_id", "Unknown")[:8]
2263
+ if log_entry.get("node_id")
2264
+ else "N/A"
2265
+ ),
2266
+ (
2267
+ log_entry.get("task_id", "Unknown")[:8]
2268
+ if log_entry.get("task_id")
2269
+ else "N/A"
2270
+ ),
2271
+ log_entry.get("severity", "INFO"),
2272
+ log_entry.get("message", "")[:100], # Truncate long messages
2273
+ )
2274
+
2275
+ self.console.print(table)
2276
+
2277
+ if len(logs) > 50:
2278
+ self.print(
2279
+ f"Showing last 50 of {len(logs)} log entries. Use export for full logs."
2280
+ )
2281
+ else:
2282
+ print(f"Logs for Swarm: {swarm_id}")
2283
+ print("-" * 120)
2284
+ for log_entry in logs[-20:]: # Show last 20 entries
2285
+ timestamp = log_entry.get("timestamp", "Unknown")
2286
+ node_id = (
2287
+ log_entry.get("node_id", "Unknown")[:8]
2288
+ if log_entry.get("node_id")
2289
+ else "N/A"
2290
+ )
2291
+ severity = log_entry.get("severity", "INFO")
2292
+ message = log_entry.get("message", "")[:80]
2293
+ print(f"{timestamp:20} {node_id:10} {severity:8} {message}")
2294
+
2295
+ if len(logs) > 20:
2296
+ print(f"... showing last 20 of {len(logs)} log entries")
2297
+
2298
+ return 0
2299
+
2300
+ except Exception as e:
2301
+ self.print_error(f"Failed to collect logs: {e}")
2302
+ return 1
2303
+
2304
+ def stream_logs(self, swarm_id: str) -> int:
2305
+ """Stream logs in real-time."""
2306
+ api = self._get_user_api()
2307
+ if not api:
2308
+ return 1
2309
+
2310
+ async def run_stream():
2311
+ try:
2312
+ # Set up signal handling for graceful shutdown
2313
+ stop_streaming = False
2314
+
2315
+ def signal_handler(_signum, _frame):
2316
+ nonlocal stop_streaming
2317
+ stop_streaming = True
2318
+
2319
+ signal.signal(signal.SIGINT, signal_handler)
2320
+ signal.signal(signal.SIGTERM, signal_handler)
2321
+
2322
+ # Create Rich components for log display
2323
+ log_entries = []
2324
+ max_log_entries = 1000 # Keep last 1000 log entries
2325
+
2326
+ # Status panel for connection info
2327
+ status_panel = Panel(
2328
+ "[green]● Connected[/green] - Streaming logs...\n"
2329
+ + "Press Ctrl+C to stop streaming",
2330
+ title="Log Streaming Status",
2331
+ border_style="green",
2332
+ )
2333
+
2334
+ # Counters for statistics
2335
+ log_count = 0
2336
+ start_time = time.time()
2337
+ log_levels = {"ERROR": 0, "WARN": 0, "INFO": 0, "DEBUG": 0, "OTHER": 0}
2338
+
2339
+ def get_log_color(level):
2340
+ """Get color for log level."""
2341
+ colors = {
2342
+ "ERROR": "red",
2343
+ "WARN": "yellow",
2344
+ "WARNING": "yellow",
2345
+ "INFO": "blue",
2346
+ "DEBUG": "dim",
2347
+ "TRACE": "dim",
2348
+ }
2349
+ return colors.get(level.upper(), "white")
2350
+
2351
+ def format_log_entry(log):
2352
+ """Format a log entry for display."""
2353
+ timestamp = log.get(
2354
+ "timestamp", datetime.now().strftime("%H:%M:%S.%f")[:-3]
2355
+ )
2356
+ level = log.get("level", "INFO")
2357
+ message = log.get("message", str(log.get("data", "")))
2358
+ node_id = log.get("node_id", "Unknown")
2359
+ task_id = log.get("task_id", "Unknown")
2360
+
2361
+ # Truncate long messages for display
2362
+ if len(message) > 100:
2363
+ message = message[:97] + "..."
2364
+
2365
+ color = get_log_color(level)
2366
+ formatted = f"[dim]{timestamp}[/dim] [{color}]{level:5s}[/{color}] [cyan]{node_id}[/cyan] [magenta]{task_id}[/magenta] {message}"
2367
+ return formatted
2368
+
2369
+ # Display streaming logs
2370
+ if self.console:
2371
+ with Live(console=self.console, refresh_per_second=10) as live:
2372
+ try:
2373
+ async for log in api.stream_logs(swarm_id):
2374
+ if stop_streaming:
2375
+ break
2376
+
2377
+ # Process log entry
2378
+ level = log.get("level", "INFO").upper()
2379
+ if level in log_levels:
2380
+ log_levels[level] += 1
2381
+ else:
2382
+ log_levels["OTHER"] += 1
2383
+
2384
+ # Add to log entries list
2385
+ formatted_entry = format_log_entry(log)
2386
+ log_entries.append(formatted_entry)
2387
+
2388
+ # Keep only recent entries
2389
+ if len(log_entries) > max_log_entries:
2390
+ log_entries.pop(0)
2391
+
2392
+ # Update counters
2393
+ log_count += 1
2394
+ elapsed = time.time() - start_time
2395
+
2396
+ # Create log panel with recent entries
2397
+ recent_logs = log_entries[-20:] # Show last 20 entries
2398
+ log_display = "\n".join(recent_logs)
2399
+ if len(log_entries) > 20:
2400
+ log_display = (
2401
+ f"[dim]... {len(log_entries) - 20} older entries ...[/dim]\n"
2402
+ + log_display
2403
+ )
2404
+
2405
+ log_panel = Panel(
2406
+ log_display,
2407
+ title=f"Recent Log Entries ({len(log_entries)} total)",
2408
+ border_style="blue",
2409
+ height=15,
2410
+ )
2411
+
2412
+ # Update status panel with stats
2413
+ status_text = (
2414
+ f"[green]● Connected[/green] - Streaming logs...\n"
2415
+ f"Total logs: {log_count}\n"
2416
+ f"Elapsed: {elapsed:.1f}s\n"
2417
+ f"ERROR: {log_levels['ERROR']} | WARN: {log_levels['WARN']} | INFO: {log_levels['INFO']} | DEBUG: {log_levels['DEBUG']}\n"
2418
+ f"Press Ctrl+C to stop"
2419
+ )
2420
+ status_panel = Panel(
2421
+ status_text,
2422
+ title="Log Streaming Status",
2423
+ border_style="green",
2424
+ )
2425
+
2426
+ # Create combined display
2427
+ layout = Layout()
2428
+ layout.split_column(
2429
+ Layout(status_panel, size=8), Layout(log_panel)
2430
+ )
2431
+ live.update(layout)
2432
+
2433
+ except KeyboardInterrupt:
2434
+ stop_streaming = True
2435
+ except Exception as e:
2436
+ # Show error in status
2437
+ error_panel = Panel(
2438
+ f"[red]✗ Connection Error[/red]: {str(e)}\n"
2439
+ + "Attempting to reconnect...",
2440
+ title="Log Streaming Status",
2441
+ border_style="red",
2442
+ )
2443
+
2444
+ # Keep recent logs visible during error
2445
+ recent_logs = (
2446
+ log_entries[-20:]
2447
+ if log_entries
2448
+ else ["[dim]No logs received yet...[/dim]"]
2449
+ )
2450
+ log_display = "\n".join(recent_logs)
2451
+ log_panel = Panel(
2452
+ log_display,
2453
+ title=f"Recent Log Entries ({len(log_entries)} total)",
2454
+ border_style="red",
2455
+ height=15,
2456
+ )
2457
+
2458
+ layout = Layout()
2459
+ layout.split_column(
2460
+ Layout(error_panel, size=8), Layout(log_panel)
2461
+ )
2462
+ live.update(layout)
2463
+
2464
+ # Wait before retrying
2465
+ await asyncio.sleep(2)
2466
+
2467
+ # Final status update
2468
+ final_status = (
2469
+ f"[yellow]● Disconnected[/yellow] - Log streaming stopped.\n"
2470
+ f"Total logs received: {log_count}\n"
2471
+ f"Total time: {time.time() - start_time:.1f}s\n"
2472
+ f"Final counts - ERROR: {log_levels['ERROR']} | WARN: {log_levels['WARN']} | INFO: {log_levels['INFO']} | DEBUG: {log_levels['DEBUG']}"
2473
+ )
2474
+ final_panel = Panel(
2475
+ final_status,
2476
+ title="Log Streaming Complete",
2477
+ border_style="yellow",
2478
+ )
2479
+
2480
+ # Show final logs
2481
+ if log_entries:
2482
+ final_logs = log_entries[
2483
+ -30:
2484
+ ] # Show more entries in final view
2485
+ log_display = "\n".join(final_logs)
2486
+ if len(log_entries) > 30:
2487
+ log_display = (
2488
+ f"[dim]... {len(log_entries) - 30} older entries ...[/dim]\n"
2489
+ + log_display
2490
+ )
2491
+ else:
2492
+ log_display = "[dim]No logs were received during the streaming session.[/dim]"
2493
+
2494
+ final_log_panel = Panel(
2495
+ log_display,
2496
+ title=f"Final Log Summary ({len(log_entries)} total entries)",
2497
+ border_style="yellow",
2498
+ )
2499
+
2500
+ final_layout = Layout()
2501
+ final_layout.split_column(
2502
+ Layout(final_panel, size=8), Layout(final_log_panel)
2503
+ )
2504
+ live.update(final_layout)
2505
+
2506
+ # Brief pause to show final status
2507
+ await asyncio.sleep(2)
2508
+ else:
2509
+ # Fallback for non-Rich console
2510
+ print(f"Streaming logs for swarm {swarm_id}...")
2511
+ print("Press Ctrl+C to stop streaming")
2512
+
2513
+ try:
2514
+ async for log in api.stream_logs(swarm_id):
2515
+ if stop_streaming:
2516
+ break
2517
+
2518
+ timestamp = log.get(
2519
+ "timestamp", datetime.now().strftime("%H:%M:%S")
2520
+ )
2521
+ level = log.get("level", "INFO")
2522
+ message = log.get("message", str(log.get("data", "")))
2523
+ node_id = log.get("node_id", "Unknown")
2524
+
2525
+ print(f"[{timestamp}] {level:5s} [{node_id}] {message}")
2526
+ log_count += 1
2527
+
2528
+ except KeyboardInterrupt:
2529
+ pass
2530
+
2531
+ print(f"\nLog streaming stopped. Total logs received: {log_count}")
2532
+
2533
+ return 0
2534
+
2535
+ except Exception as e:
2536
+ if self.console:
2537
+ self.print_error(f"Failed to stream logs: {e}")
2538
+ else:
2539
+ print(f"Error: Failed to stream logs: {e}")
2540
+ return 1
2541
+
2542
+ try:
2543
+ if self.console:
2544
+ self.print(
2545
+ f"[cyan]Starting log streaming for swarm {swarm_id}...[/cyan]"
2546
+ )
2547
+ else:
2548
+ print(f"Starting log streaming for swarm {swarm_id}...")
2549
+
2550
+ return asyncio.run(run_stream())
2551
+
2552
+ except Exception as e:
2553
+ self.print_error(f"Failed to start log streaming: {e}")
2554
+ return 1
2555
+
2556
+ def export_logs(self, swarm_id: str, output_file: Optional[str] = None) -> int:
2557
+ """Export logs to file."""
2558
+ self.print_error("Log export not yet implemented in CLI")
2559
+ self.print("Use the SDK directly for now:")
2560
+ return 1
2561
+
2562
+ # ============================================
2563
+ # Swarm Deployment Wizard Helper Functions
2564
+ # ============================================
2565
+
2566
+ def _choose_deployment_mode(self) -> str:
2567
+ """Choose between interactive creation or file-based deployment."""
2568
+ if self.console:
2569
+ self.console.print(
2570
+ "\n[bold yellow]Step 1: Choose Deployment Mode[/bold yellow]"
2571
+ )
2572
+
2573
+ choices = [
2574
+ "1. Python - Deploy from Python swarm definition file",
2575
+ "2. Interactive - Create swarm configuration step-by-step",
2576
+ "3. Cancel",
2577
+ ]
2578
+
2579
+ for choice in choices:
2580
+ self.console.print(f" {choice}")
2581
+
2582
+ while True:
2583
+ mode = Prompt.ask("\nChoose deployment mode", choices=["1", "2", "3"])
2584
+ if mode == "1":
2585
+ return "file"
2586
+ elif mode == "2":
2587
+ return "interactive"
2588
+ elif mode == "3":
2589
+ return "cancel"
2590
+ else:
2591
+ self.print("\nChoose deployment mode:")
2592
+ self.print("1. Python - Deploy from Python swarm definition")
2593
+ self.print("2. Interactive - Create swarm configuration")
2594
+ self.print("3. Cancel")
2595
+
2596
+ while True:
2597
+ mode = input("Enter choice (1-3): ").strip()
2598
+ if mode == "1":
2599
+ return "file"
2600
+ elif mode == "2":
2601
+ return "interactive"
2602
+ elif mode == "3":
2603
+ return "cancel"
2604
+ else:
2605
+ self.print("Invalid choice. Please enter 1, 2, or 3.")
2606
+
2607
+ def _select_cluster_interactive(self, api) -> Optional[str]:
2608
+ """Interactively select a cluster for deployment."""
2609
+ if self.console:
2610
+ self.console.print(
2611
+ "\n[bold yellow]Step 2: Select Target Cluster[/bold yellow]"
2612
+ )
2613
+ else:
2614
+ self.print("\nStep 2: Select Target Cluster")
2615
+
2616
+ try:
2617
+ # Fetch clusters
2618
+ async def run():
2619
+ return await api.stream_and_fetch_clusters()
2620
+
2621
+ if self.console:
2622
+ with Progress(
2623
+ SpinnerColumn(),
2624
+ TextColumn("[progress.description]{task.description}"),
2625
+ console=self.console,
2626
+ ) as progress:
2627
+ task = progress.add_task(
2628
+ "Fetching available clusters...", total=None
2629
+ )
2630
+ clusters = asyncio.run(run())
2631
+ progress.update(task, completed=True)
2632
+ else:
2633
+ self.print("Fetching available clusters...")
2634
+ clusters = asyncio.run(run())
2635
+
2636
+ if not clusters:
2637
+ self.print_error(
2638
+ "No clusters available. Please create a cluster first."
2639
+ )
2640
+ return None
2641
+
2642
+ # Display clusters and let user choose
2643
+ if self.console:
2644
+ table = Table(title="Available Clusters")
2645
+ table.add_column("#", style="cyan", width=3)
2646
+ table.add_column("Cluster ID", style="blue")
2647
+ table.add_column("Name", style="green")
2648
+ table.add_column("Status", style="yellow")
2649
+ table.add_column("Nodes", style="magenta")
2650
+
2651
+ for i, cluster in enumerate(clusters, 1):
2652
+ table.add_row(
2653
+ str(i),
2654
+ cluster.get("cluster_id", "Unknown"),
2655
+ cluster.get("name", "Unknown"),
2656
+ cluster.get("status", "Unknown"),
2657
+ str(cluster.get("node_count", 0)),
2658
+ )
2659
+
2660
+ self.console.print(table)
2661
+
2662
+ while True:
2663
+ try:
2664
+ choice = IntPrompt.ask("Select cluster number", default=1)
2665
+ if 1 <= choice <= len(clusters):
2666
+ selected_cluster = clusters[choice - 1]
2667
+ cluster_id = selected_cluster.get("cluster_id")
2668
+ self.console.print(
2669
+ f"Selected cluster: [cyan]{cluster_id}[/cyan]"
2670
+ )
2671
+ return cluster_id
2672
+ else:
2673
+ self.console.print(
2674
+ f"[red]Please enter a number between 1 and {len(clusters)}[/red]"
2675
+ )
2676
+ except KeyboardInterrupt:
2677
+ return None
2678
+ else:
2679
+ self.print("Available clusters:")
2680
+ for i, cluster in enumerate(clusters, 1):
2681
+ self.print(
2682
+ f"{i}. {cluster.get('cluster_id', 'Unknown')} - {cluster.get('name', 'Unknown')} ({cluster.get('node_count', 0)} nodes)"
2683
+ )
2684
+
2685
+ while True:
2686
+ try:
2687
+ choice = input(
2688
+ f"Select cluster number (1-{len(clusters)}): "
2689
+ ).strip()
2690
+ choice = int(choice)
2691
+ if 1 <= choice <= len(clusters):
2692
+ selected_cluster = clusters[choice - 1]
2693
+ cluster_id = selected_cluster.get("cluster_id")
2694
+ self.print(f"Selected cluster: {cluster_id}")
2695
+ return cluster_id
2696
+ else:
2697
+ self.print(
2698
+ f"Please enter a number between 1 and {len(clusters)}"
2699
+ )
2700
+ except (ValueError, KeyboardInterrupt):
2701
+ return None
2702
+
2703
+ except Exception as e:
2704
+ self.print_error(f"Failed to fetch clusters: {e}")
2705
+ return None
2706
+
2707
+ def _deploy_from_file(self, api, cluster_id: str) -> int:
2708
+ """Deploy swarm from Python definition file."""
2709
+ if self.console:
2710
+ self.console.print(
2711
+ "\n[bold yellow]Step 3: Deploy from Python Swarm Definition[/bold yellow]"
2712
+ )
2713
+
2714
+ file_path = Prompt.ask("Enter path to Python swarm file (e.g., swarm.py)")
2715
+ else:
2716
+ self.print("\nStep 3: Deploy from Python Swarm Definition")
2717
+ file_path = input("Enter path to Python swarm file: ").strip()
2718
+
2719
+ if not file_path:
2720
+ self.print_error("No file path provided.")
2721
+ return 1
2722
+
2723
+ config_path = Path(file_path)
2724
+ if not config_path.exists():
2725
+ self.print_error(f"Swarm file not found: {file_path}")
2726
+ return 1
2727
+
2728
+ if not config_path.suffix == ".py":
2729
+ self.print_error(
2730
+ "Please provide a Python file (.py) containing the swarm definition."
2731
+ )
2732
+ return 1
2733
+
2734
+ try:
2735
+ # Load the Python swarm definition
2736
+ swarm = self._load_python_swarm(config_path)
2737
+ if not swarm:
2738
+ return 1
2739
+
2740
+ # Preview swarm configuration
2741
+ if not self._preview_python_swarm(swarm):
2742
+ return 0 # User cancelled
2743
+
2744
+ # Deploy the swarm
2745
+ return self._execute_deployment(api, cluster_id, swarm)
2746
+
2747
+ except ImportError as e:
2748
+ self.print_error(f"Failed to import swarm module: {e}")
2749
+ return 1
2750
+ except AttributeError as e:
2751
+ self.print_error(f"Invalid swarm definition: {e}")
2752
+ return 1
2753
+ except Exception as e:
2754
+ self.print_error(f"Failed to load swarm: {e}")
2755
+ return 1
2756
+
2757
+ def _deploy_interactive(self, api, cluster_id: str) -> int:
2758
+ """Deploy swarm with interactive configuration."""
2759
+ if self.console:
2760
+ self.console.print(
2761
+ "\n[bold yellow]Step 3: Interactive Swarm Configuration[/bold yellow]"
2762
+ )
2763
+ else:
2764
+ self.print("\nStep 3: Interactive Swarm Configuration")
2765
+
2766
+ # Get available modules
2767
+ modules = self._fetch_available_modules(api)
2768
+ if modules is None:
2769
+ return 1
2770
+
2771
+ # Build swarm configuration interactively
2772
+ swarm_config = self._build_swarm_config_interactive(modules)
2773
+ if not swarm_config:
2774
+ return 1
2775
+
2776
+ # Create swarm object
2777
+ swarm = self._create_swarm_from_config(swarm_config)
2778
+ if not swarm:
2779
+ return 1
2780
+
2781
+ # Preview configuration
2782
+ if not self._preview_swarm_config(swarm, swarm_config):
2783
+ return 0 # User cancelled
2784
+
2785
+ # Deploy the swarm
2786
+ return self._execute_deployment(api, cluster_id, swarm)
2787
+
2788
+ def _fetch_available_modules(self, api) -> Optional[List[Any]]:
2789
+ """Fetch available modules for swarm configuration."""
2790
+ try:
2791
+
2792
+ async def run():
2793
+ return await api.stream_and_fetch_modules()
2794
+
2795
+ if self.console:
2796
+ with Progress(
2797
+ SpinnerColumn(),
2798
+ TextColumn("[progress.description]{task.description}"),
2799
+ console=self.console,
2800
+ ) as progress:
2801
+ task = progress.add_task(
2802
+ "Fetching available modules...", total=None
2803
+ )
2804
+ modules = asyncio.run(run())
2805
+ progress.update(task, completed=True)
2806
+ else:
2807
+ self.print("Fetching available modules...")
2808
+ modules = asyncio.run(run())
2809
+
2810
+ return modules
2811
+
2812
+ except Exception as e:
2813
+ self.print_error(f"Failed to fetch modules: {e}")
2814
+ return None
2815
+
2816
+ def _build_swarm_config_interactive(
2817
+ self, modules: List[Any]
2818
+ ) -> Optional[Dict[str, Any]]:
2819
+ """Build swarm configuration through interactive prompts."""
2820
+ config = {
2821
+ "name": "InteractiveSwarm",
2822
+ "modules": [],
2823
+ "globals": {},
2824
+ "networks": [],
2825
+ "tasks": [],
2826
+ }
2827
+
2828
+ # Step 3a: Basic configuration
2829
+ if self.console:
2830
+ self.console.print("\n[bold blue]Basic Configuration[/bold blue]")
2831
+ config["name"] = Prompt.ask("Swarm name", default="InteractiveSwarm")
2832
+ else:
2833
+ self.print("\nBasic Configuration:")
2834
+ name = input("Swarm name (default: InteractiveSwarm): ").strip()
2835
+ config["name"] = name if name else "InteractiveSwarm"
2836
+
2837
+ # Step 3b: Select modules
2838
+ if not self._select_modules_interactive(config, modules):
2839
+ return None
2840
+
2841
+ # Step 3c: Configure global parameters
2842
+ if not self._configure_globals_interactive(config):
2843
+ return None
2844
+
2845
+ # Step 3d: Configure networks (optional)
2846
+ self._configure_networks_interactive(config)
2847
+
2848
+ # Step 3e: Configure task execution order
2849
+ if not self._configure_tasks_interactive(config):
2850
+ return None
2851
+
2852
+ return config
2853
+
2854
+ def _validate_swarm_config(self, config: Dict[str, Any]) -> bool:
2855
+ """Validate the structure of a swarm configuration."""
2856
+ required_fields = ["name", "modules"]
2857
+
2858
+ for field in required_fields:
2859
+ if field not in config:
2860
+ self.print_error(f"Missing required field '{field}' in configuration")
2861
+ return False
2862
+
2863
+ # Validate modules structure
2864
+ modules = config.get("modules", [])
2865
+ if not isinstance(modules, list) or len(modules) == 0:
2866
+ self.print_error("Configuration must contain at least one module")
2867
+ return False
2868
+
2869
+ for i, module in enumerate(modules):
2870
+ if not isinstance(module, dict):
2871
+ self.print_error(
2872
+ f"Module {i + 1}: must be an object with 'id' and 'alias' fields"
2873
+ )
2874
+ return False
2875
+ if "id" not in module or "alias" not in module:
2876
+ self.print_error(
2877
+ f"Module {i + 1}: must have both 'id' and 'alias' fields"
2878
+ )
2879
+ return False
2880
+
2881
+ # Validate optional fields if present
2882
+ if "globals" in config and not isinstance(config["globals"], dict):
2883
+ self.print_error("'globals' field must be an object")
2884
+ return False
2885
+
2886
+ if "networks" in config:
2887
+ networks = config["networks"]
2888
+ if not isinstance(networks, list):
2889
+ self.print_error("'networks' field must be an array")
2890
+ return False
2891
+
2892
+ for i, network in enumerate(networks):
2893
+ if not isinstance(network, dict) or "name" not in network:
2894
+ self.print_error(f"Network {i + 1}: must have a 'name' field")
2895
+ return False
2896
+
2897
+ if "tasks" in config:
2898
+ tasks = config["tasks"]
2899
+ if not isinstance(tasks, list):
2900
+ self.print_error("'tasks' field must be an array")
2901
+ return False
2902
+
2903
+ for i, task in enumerate(tasks):
2904
+ if not isinstance(task, dict) or "type" not in task:
2905
+ self.print_error(f"Task {i + 1}: must have a 'type' field")
2906
+ return False
2907
+
2908
+ return True
2909
+
2910
+ def _select_modules_interactive(
2911
+ self, config: Dict[str, Any], modules: List[Any]
2912
+ ) -> bool:
2913
+ """Interactively select and configure modules."""
2914
+ if self.console:
2915
+ self.console.print("\n[bold blue]Module Selection[/bold blue]")
2916
+ else:
2917
+ self.print("\nModule Selection:")
2918
+
2919
+ if not modules:
2920
+ self.print_error(
2921
+ "No modules available. Please upload modules first using 'manta module upload'."
2922
+ )
2923
+ return False
2924
+
2925
+ # Display available modules
2926
+ if self.console:
2927
+ table = Table(title="Available Modules")
2928
+ table.add_column("#", style="cyan", width=3)
2929
+ table.add_column("Module ID", style="blue")
2930
+ table.add_column("Name", style="green")
2931
+ table.add_column("Type", style="yellow")
2932
+
2933
+ for i, module in enumerate(modules, 1):
2934
+ table.add_row(
2935
+ str(i),
2936
+ str(getattr(module, "module_id", "Unknown")),
2937
+ str(getattr(module, "name", "Unknown")),
2938
+ str(getattr(module, "module_type", "Unknown")),
2939
+ )
2940
+
2941
+ self.console.print(table)
2942
+ else:
2943
+ self.print("Available modules:")
2944
+ for i, module in enumerate(modules, 1):
2945
+ self.print(
2946
+ f"{i}. {getattr(module, 'module_id', 'Unknown')} - {getattr(module, 'name', 'Unknown')}"
2947
+ )
2948
+
2949
+ # Select modules
2950
+ selected_modules = []
2951
+ while True:
2952
+ if self.console:
2953
+ if selected_modules:
2954
+ self.console.print(
2955
+ f"\nCurrently selected: {len(selected_modules)} modules"
2956
+ )
2957
+ choice = Prompt.ask(
2958
+ "Select module number (or 'done' to finish, 'cancel' to abort)"
2959
+ )
2960
+ else:
2961
+ if selected_modules:
2962
+ self.print(f"\nCurrently selected: {len(selected_modules)} modules")
2963
+ choice = input("Select module number (or 'done'/'cancel'): ").strip()
2964
+
2965
+ if choice.lower() == "done":
2966
+ if selected_modules:
2967
+ break
2968
+ else:
2969
+ self.print_error("You must select at least one module.")
2970
+ continue
2971
+ elif choice.lower() == "cancel":
2972
+ return False
2973
+
2974
+ try:
2975
+ choice_num = int(choice)
2976
+ if 1 <= choice_num <= len(modules):
2977
+ module = modules[choice_num - 1]
2978
+ module_id = str(getattr(module, "module_id", "Unknown"))
2979
+
2980
+ # Check if already selected
2981
+ if any(m["id"] == module_id for m in selected_modules):
2982
+ self.print("Module already selected.")
2983
+ continue
2984
+
2985
+ # Add module with alias
2986
+ if self.console:
2987
+ alias = Prompt.ask(
2988
+ f"Alias for module {getattr(module, 'name', 'Unknown')}",
2989
+ default=getattr(
2990
+ module, "name", f"module_{len(selected_modules) + 1}"
2991
+ ),
2992
+ )
2993
+ else:
2994
+ alias = input(
2995
+ f"Alias for module (default: {getattr(module, 'name', f'module_{len(selected_modules) + 1}')}): "
2996
+ ).strip()
2997
+ if not alias:
2998
+ alias = getattr(
2999
+ module, "name", f"module_{len(selected_modules) + 1}"
3000
+ )
3001
+
3002
+ selected_modules.append(
3003
+ {
3004
+ "id": module_id,
3005
+ "alias": alias,
3006
+ "name": getattr(module, "name", "Unknown"),
3007
+ }
3008
+ )
3009
+
3010
+ self.print_success(f"Added module: {alias}")
3011
+ else:
3012
+ self.print_error(
3013
+ f"Please enter a number between 1 and {len(modules)}"
3014
+ )
3015
+ except ValueError:
3016
+ self.print_error("Please enter a valid number, 'done', or 'cancel'")
3017
+
3018
+ config["modules"] = selected_modules
3019
+ return True
3020
+
3021
+ def _configure_globals_interactive(self, config: Dict[str, Any]) -> bool:
3022
+ """Interactively configure global parameters."""
3023
+ if self.console:
3024
+ self.console.print("\n[bold blue]Global Parameters (Optional)[/bold blue]")
3025
+ self.console.print(
3026
+ "Global parameters are shared across all tasks in the swarm."
3027
+ )
3028
+
3029
+ add_globals = Confirm.ask(
3030
+ "Would you like to add global parameters?", default=False
3031
+ )
3032
+ else:
3033
+ self.print("\nGlobal Parameters (Optional):")
3034
+ self.print("Global parameters are shared across all tasks in the swarm.")
3035
+ add_globals_input = input("Add global parameters? (y/N): ").strip().lower()
3036
+ add_globals = add_globals_input in ["y", "yes"]
3037
+
3038
+ if not add_globals:
3039
+ return True
3040
+
3041
+ globals_config = {}
3042
+ while True:
3043
+ if self.console:
3044
+ if globals_config:
3045
+ self.console.print(
3046
+ f"\nCurrent globals: {list(globals_config.keys())}"
3047
+ )
3048
+
3049
+ param_name = Prompt.ask("Parameter name (or 'done' to finish)").strip()
3050
+ else:
3051
+ if globals_config:
3052
+ self.print(f"\nCurrent globals: {list(globals_config.keys())}")
3053
+ param_name = input("Parameter name (or 'done'): ").strip()
3054
+
3055
+ if param_name.lower() == "done":
3056
+ break
3057
+
3058
+ if not param_name:
3059
+ self.print_error("Parameter name cannot be empty.")
3060
+ continue
3061
+
3062
+ # Get parameter value
3063
+ if self.console:
3064
+ param_value = Prompt.ask(f"Value for '{param_name}' (JSON format)")
3065
+ else:
3066
+ param_value = input(f"Value for '{param_name}' (JSON format): ")
3067
+
3068
+ try:
3069
+ # Try to parse as JSON
3070
+ parsed_value = json.loads(param_value)
3071
+ globals_config[param_name] = parsed_value
3072
+ self.print_success(f"Added global parameter: {param_name}")
3073
+ except json.JSONDecodeError:
3074
+ # Store as string if not valid JSON
3075
+ globals_config[param_name] = param_value
3076
+ self.print_success(f"Added global parameter: {param_name} (as string)")
3077
+
3078
+ config["globals"] = globals_config
3079
+ return True
3080
+
3081
+ def _configure_networks_interactive(self, config: Dict[str, Any]):
3082
+ """Interactively configure networks (optional)."""
3083
+ if self.console:
3084
+ self.console.print(
3085
+ "\n[bold blue]Network Configuration (Optional)[/bold blue]"
3086
+ )
3087
+ self.console.print("Networks enable communication between tasks.")
3088
+
3089
+ add_networks = Confirm.ask(
3090
+ "Would you like to add custom networks?", default=False
3091
+ )
3092
+ else:
3093
+ self.print("\nNetwork Configuration (Optional):")
3094
+ self.print("Networks enable communication between tasks.")
3095
+ add_networks_input = input("Add custom networks? (y/N): ").strip().lower()
3096
+ add_networks = add_networks_input in ["y", "yes"]
3097
+
3098
+ if not add_networks:
3099
+ return
3100
+
3101
+ networks = []
3102
+ drivers = ["overlay", "bridge", "host"]
3103
+
3104
+ while True:
3105
+ if self.console:
3106
+ if networks:
3107
+ self.console.print(
3108
+ f"\nCurrent networks: {[n['name'] for n in networks]}"
3109
+ )
3110
+
3111
+ network_name = Prompt.ask("Network name (or 'done' to finish)").strip()
3112
+ else:
3113
+ if networks:
3114
+ self.print(f"\nCurrent networks: {[n['name'] for n in networks]}")
3115
+ network_name = input("Network name (or 'done'): ").strip()
3116
+
3117
+ if network_name.lower() == "done":
3118
+ break
3119
+
3120
+ if not network_name:
3121
+ self.print_error("Network name cannot be empty.")
3122
+ continue
3123
+
3124
+ # Select driver
3125
+ if self.console:
3126
+ self.console.print("Available drivers:")
3127
+ for i, driver in enumerate(drivers, 1):
3128
+ self.console.print(f" {i}. {driver}")
3129
+
3130
+ driver_choice = IntPrompt.ask(
3131
+ "Select driver", default=1, choices=["1", "2", "3"]
3132
+ )
3133
+ selected_driver = drivers[driver_choice - 1]
3134
+ else:
3135
+ self.print("Available drivers:")
3136
+ for i, driver in enumerate(drivers, 1):
3137
+ self.print(f" {i}. {driver}")
3138
+
3139
+ while True:
3140
+ try:
3141
+ driver_choice = input(
3142
+ "Select driver (1-3, default=1): "
3143
+ ).strip()
3144
+ if not driver_choice:
3145
+ driver_choice = "1"
3146
+ driver_choice = int(driver_choice)
3147
+ if 1 <= driver_choice <= 3:
3148
+ selected_driver = drivers[driver_choice - 1]
3149
+ break
3150
+ else:
3151
+ self.print("Please enter 1, 2, or 3")
3152
+ except ValueError:
3153
+ self.print("Please enter a valid number")
3154
+
3155
+ networks.append({"name": network_name, "driver": selected_driver})
3156
+
3157
+ self.print_success(f"Added network: {network_name} ({selected_driver})")
3158
+
3159
+ config["networks"] = networks
3160
+
3161
+ def _configure_tasks_interactive(self, config: Dict[str, Any]) -> bool:
3162
+ """Configure task execution flow."""
3163
+ if self.console:
3164
+ self.console.print("\n[bold blue]Task Execution Configuration[/bold blue]")
3165
+ self.console.print("Define how your modules are executed and connected.")
3166
+ else:
3167
+ self.print("\nTask Execution Configuration:")
3168
+ self.print("Define how your modules are executed and connected.")
3169
+
3170
+ modules = config.get("modules", [])
3171
+ if len(modules) == 1:
3172
+ # Simple single module execution
3173
+ config["tasks"] = [{"type": "single", "module": modules[0]["alias"]}]
3174
+ if self.console:
3175
+ self.console.print(f"Single module execution: {modules[0]['alias']}")
3176
+ else:
3177
+ self.print(f"Single module execution: {modules[0]['alias']}")
3178
+ return True
3179
+
3180
+ # Multiple modules - ask for execution pattern
3181
+ if self.console:
3182
+ self.console.print("Execution patterns:")
3183
+ self.console.print(" 1. Sequential - Execute modules one after another")
3184
+ self.console.print(" 2. Parallel - Execute modules simultaneously")
3185
+ self.console.print(" 3. Custom - Define custom execution flow")
3186
+
3187
+ pattern = Prompt.ask(
3188
+ "Select execution pattern", choices=["1", "2", "3"], default="1"
3189
+ )
3190
+ else:
3191
+ self.print("Execution patterns:")
3192
+ self.print(" 1. Sequential - Execute modules one after another")
3193
+ self.print(" 2. Parallel - Execute modules simultaneously")
3194
+ self.print(" 3. Custom - Define custom execution flow")
3195
+
3196
+ pattern = input("Select pattern (1-3, default=1): ").strip()
3197
+ if not pattern:
3198
+ pattern = "1"
3199
+
3200
+ if pattern == "1":
3201
+ # Sequential execution
3202
+ config["tasks"] = [
3203
+ {"type": "sequential", "modules": [m["alias"] for m in modules]}
3204
+ ]
3205
+ self.print_success("Configured sequential execution")
3206
+ elif pattern == "2":
3207
+ # Parallel execution
3208
+ config["tasks"] = [
3209
+ {"type": "parallel", "modules": [m["alias"] for m in modules]}
3210
+ ]
3211
+ self.print_success("Configured parallel execution")
3212
+ else:
3213
+ # Custom execution - simplified for now
3214
+ self.print("Custom execution flow not yet implemented. Using sequential.")
3215
+ config["tasks"] = [
3216
+ {"type": "sequential", "modules": [m["alias"] for m in modules]}
3217
+ ]
3218
+
3219
+ return True
3220
+
3221
+ def _load_python_swarm(self, swarm_path: Path) -> Optional[Any]:
3222
+ """Load a swarm from a Python file."""
3223
+ import importlib.util
3224
+ import inspect
3225
+
3226
+ try:
3227
+ # Add the parent directory to sys.path for imports
3228
+ parent_dir = str(swarm_path.parent.resolve())
3229
+ if parent_dir not in sys.path:
3230
+ sys.path.insert(0, parent_dir)
3231
+
3232
+ # Create a proper module name based on the file structure
3233
+ # This helps with relative imports
3234
+ module_name = swarm_path.stem
3235
+ if swarm_path.parent.name != ".":
3236
+ # Use the parent directory name as package name
3237
+ package_name = swarm_path.parent.name
3238
+ full_module_name = f"{package_name}.{module_name}"
3239
+ else:
3240
+ full_module_name = module_name
3241
+
3242
+ # Load the Python module
3243
+ spec = importlib.util.spec_from_file_location(full_module_name, swarm_path)
3244
+ if spec is None or spec.loader is None:
3245
+ self.print_error(f"Failed to load Python module from {swarm_path}")
3246
+ return None
3247
+
3248
+ swarm_module = importlib.util.module_from_spec(spec)
3249
+ sys.modules[full_module_name] = swarm_module
3250
+
3251
+ # For relative imports to work, we need to set up the package structure
3252
+ if "." in full_module_name:
3253
+ parent_package = full_module_name.rsplit(".", 1)[0]
3254
+ if parent_package not in sys.modules:
3255
+ # Create a dummy parent package
3256
+ parent_spec = importlib.util.spec_from_file_location(
3257
+ parent_package, "__init__.py"
3258
+ )
3259
+ parent_module = (
3260
+ importlib.util.module_from_spec(parent_spec)
3261
+ if parent_spec
3262
+ else type(sys)("module")
3263
+ )
3264
+ parent_module.__path__ = [str(swarm_path.parent)]
3265
+ sys.modules[parent_package] = parent_module
3266
+ swarm_module.__package__ = parent_package
3267
+
3268
+ spec.loader.exec_module(swarm_module)
3269
+
3270
+ # Find the Swarm class in the module
3271
+ from manta.apis.swarm import Swarm
3272
+
3273
+ swarm_instance = None
3274
+ for name, obj in inspect.getmembers(swarm_module):
3275
+ if inspect.isclass(obj) and issubclass(obj, Swarm) and obj != Swarm:
3276
+ # Found a Swarm subclass, instantiate it
3277
+ try:
3278
+ swarm_instance = obj()
3279
+ self.print(f"Loaded swarm class: {name}")
3280
+ break
3281
+ except TypeError:
3282
+ # Try with default arguments if constructor requires them
3283
+ try:
3284
+ swarm_instance = obj(image="manta-light:pytorch")
3285
+ self.print(
3286
+ f"Loaded swarm class: {name} (with default image)"
3287
+ )
3288
+ break
3289
+ except Exception:
3290
+ continue
3291
+
3292
+ if not swarm_instance:
3293
+ # No Swarm subclass found, try to find a swarm instance directly
3294
+ for name, obj in inspect.getmembers(swarm_module):
3295
+ if isinstance(obj, Swarm):
3296
+ swarm_instance = obj
3297
+ self.print(f"Found swarm instance: {name}")
3298
+ break
3299
+
3300
+ if not swarm_instance:
3301
+ self.print_error("No Swarm class or instance found in the file.")
3302
+ self.print_error(
3303
+ "Make sure your file defines a class that inherits from Swarm."
3304
+ )
3305
+ return None
3306
+
3307
+ return swarm_instance
3308
+
3309
+ except Exception as e:
3310
+ self.print_error(f"Failed to load swarm from {swarm_path}: {e}")
3311
+ import traceback
3312
+
3313
+ if self.console:
3314
+ self.console.print(f"[red]{traceback.format_exc()}[/red]")
3315
+ else:
3316
+ print(traceback.format_exc())
3317
+ return None
3318
+ finally:
3319
+ # Clean up sys.path
3320
+ if "parent_dir" in locals() and parent_dir in sys.path:
3321
+ sys.path.remove(parent_dir)
3322
+ # Remove the temporary modules
3323
+ if "full_module_name" in locals() and full_module_name in sys.modules:
3324
+ del sys.modules[full_module_name]
3325
+ if "parent_package" in locals() and parent_package in sys.modules:
3326
+ del sys.modules[parent_package]
3327
+
3328
+ def _preview_python_swarm(self, swarm: Any) -> bool:
3329
+ """Preview Python swarm definition and ask for confirmation."""
3330
+ if self.console:
3331
+ self.console.print("\n[bold yellow]Step 4: Swarm Preview[/bold yellow]")
3332
+
3333
+ # Create preview content
3334
+ preview_content = []
3335
+
3336
+ # Get swarm name
3337
+ swarm_name = getattr(swarm, "name", swarm.__class__.__name__)
3338
+ preview_content.append(f"[cyan]Swarm:[/cyan] {swarm_name}")
3339
+
3340
+ # List tasks
3341
+ task_count = 0
3342
+ task_names = []
3343
+ for attr_name in dir(swarm):
3344
+ attr = getattr(swarm, attr_name)
3345
+ if hasattr(attr, "__class__") and "Task" in attr.__class__.__name__:
3346
+ task_count += 1
3347
+ task_names.append(attr_name)
3348
+
3349
+ if task_count > 0:
3350
+ preview_content.append(f"[cyan]Tasks:[/cyan] {task_count}")
3351
+ for task_name in task_names:
3352
+ task = getattr(swarm, task_name)
3353
+ module = getattr(task, "module", None)
3354
+ if module:
3355
+ module_info = getattr(module, "image", "unknown")
3356
+ preview_content.append(
3357
+ f" - {task_name} (image: {module_info})"
3358
+ )
3359
+ else:
3360
+ preview_content.append(f" - {task_name}")
3361
+
3362
+ # Check for execute method
3363
+ if hasattr(swarm, "execute"):
3364
+ preview_content.append("[cyan]Execute Method:[/cyan] Defined")
3365
+ else:
3366
+ preview_content.append(
3367
+ "[yellow]Warning:[/yellow] No execute() method defined"
3368
+ )
3369
+
3370
+ # Check for globals
3371
+ if hasattr(swarm, "_globals"):
3372
+ globals_count = len(swarm._globals) if swarm._globals else 0
3373
+ if globals_count > 0:
3374
+ preview_content.append(
3375
+ f"[cyan]Global Parameters:[/cyan] {globals_count}"
3376
+ )
3377
+ for key in list(swarm._globals.keys())[:5]: # Show first 5
3378
+ preview_content.append(f" - {key}")
3379
+ if globals_count > 5:
3380
+ preview_content.append(f" ... and {globals_count - 5} more")
3381
+
3382
+ panel = Panel.fit("\n".join(preview_content), title="Python Swarm Preview")
3383
+ self.console.print(panel)
3384
+
3385
+ return Confirm.ask("\nDeploy this swarm?", default=True)
3386
+ else:
3387
+ self.print("\nStep 4: Swarm Preview")
3388
+ self.print("=" * 40)
3389
+
3390
+ swarm_name = getattr(swarm, "name", swarm.__class__.__name__)
3391
+ self.print(f"Swarm: {swarm_name}")
3392
+
3393
+ # List tasks
3394
+ task_count = 0
3395
+ for attr_name in dir(swarm):
3396
+ attr = getattr(swarm, attr_name)
3397
+ if hasattr(attr, "__class__") and "Task" in attr.__class__.__name__:
3398
+ task_count += 1
3399
+ self.print(f" - Task: {attr_name}")
3400
+
3401
+ if task_count > 0:
3402
+ self.print(f"Total Tasks: {task_count}")
3403
+
3404
+ # Check for execute method
3405
+ if hasattr(swarm, "execute"):
3406
+ self.print("Execute Method: Defined")
3407
+ else:
3408
+ self.print("Warning: No execute() method defined")
3409
+
3410
+ self.print("=" * 40)
3411
+
3412
+ confirm = input("\nDeploy this swarm? (y/n): ").strip().lower()
3413
+ return confirm in ["y", "yes"]
3414
+
3415
+ def _create_swarm_from_config(self, config: Dict[str, Any]) -> Optional[Any]:
3416
+ """Create a Swarm object from configuration."""
3417
+ # Note: This is a simplified implementation for the CLI wizard
3418
+ # Full configuration-based deployment with module ID references
3419
+ # requires additional development to properly resolve module IDs to Module objects
3420
+
3421
+ self.print_warning("Configuration-based swarm deployment is currently limited.")
3422
+ self.print_warning(
3423
+ "For production deployments, use the manta-sdk API directly."
3424
+ )
3425
+
3426
+ try:
3427
+ # Import required classes
3428
+ from manta.apis.graph import Task
3429
+ from manta.apis.module import Module
3430
+ from manta.apis.swarm import Driver, Swarm
3431
+
3432
+ # Create a simple swarm class for basic functionality
3433
+ class ConfigurableSwarm(Swarm):
3434
+ def __init__(self, swarm_config):
3435
+ super().__init__()
3436
+ self.name = swarm_config.get("name", "ConfigurableSwarm")
3437
+ self.config = swarm_config
3438
+
3439
+ # Set up globals
3440
+ for key, value in swarm_config.get("globals", {}).items():
3441
+ self.set_global(key, value)
3442
+
3443
+ # Set up networks
3444
+ for network in swarm_config.get("networks", []):
3445
+ driver_name = network.get("driver", "overlay").upper()
3446
+ driver = getattr(Driver, driver_name, Driver.OVERLAY)
3447
+ self.add_network(network["name"], driver)
3448
+
3449
+ def execute(self):
3450
+ # Simplified execution - this is a placeholder implementation
3451
+ # In a real scenario, you would need to:
3452
+ # 1. Fetch actual Module objects from the server using module IDs
3453
+ # 2. Build the proper task graph based on the configuration
3454
+ # 3. Handle different execution patterns (sequential, parallel, custom)
3455
+
3456
+ modules_config = self.config.get("modules", [])
3457
+ if not modules_config:
3458
+ raise NotImplementedError("No modules configured in swarm")
3459
+
3460
+ # For now, create a placeholder module to demonstrate structure
3461
+ # This would need to be replaced with proper module resolution
3462
+ placeholder_module = Module("placeholder.py", "python:3.9-slim")
3463
+ placeholder_module.name = "ConfigurationPlaceholder"
3464
+
3465
+ return Task(placeholder_module)
3466
+
3467
+ return ConfigurableSwarm(config)
3468
+
3469
+ except ImportError as e:
3470
+ self.print_error(f"Failed to import required modules: {e}")
3471
+ self.print_error("Make sure manta-sdk[api] is installed")
3472
+ return None
3473
+ except Exception as e:
3474
+ self.print_error(f"Failed to create swarm from configuration: {e}")
3475
+ return None
3476
+
3477
+ def _preview_swarm_config(self, swarm: Any, config: Dict[str, Any]) -> bool:
3478
+ """Preview swarm configuration and ask for confirmation."""
3479
+ if self.console:
3480
+ self.console.print(
3481
+ "\n[bold yellow]Step 4: Configuration Preview[/bold yellow]"
3482
+ )
3483
+
3484
+ # Create preview panel
3485
+ preview_content = []
3486
+ preview_content.append(
3487
+ f"[cyan]Swarm Name:[/cyan] {config.get('name', 'Unknown')}"
3488
+ )
3489
+ preview_content.append(
3490
+ f"[cyan]Modules:[/cyan] {len(config.get('modules', []))}"
3491
+ )
3492
+
3493
+ modules = config.get("modules", [])
3494
+ for module in modules:
3495
+ preview_content.append(
3496
+ f" - {module.get('alias', 'Unknown')} ({module.get('id', 'Unknown')})"
3497
+ )
3498
+
3499
+ globals_config = config.get("globals", {})
3500
+ if globals_config:
3501
+ preview_content.append(
3502
+ f"[cyan]Global Parameters:[/cyan] {len(globals_config)}"
3503
+ )
3504
+ for key in globals_config.keys():
3505
+ preview_content.append(f" - {key}")
3506
+
3507
+ networks = config.get("networks", [])
3508
+ if networks:
3509
+ preview_content.append(f"[cyan]Networks:[/cyan] {len(networks)}")
3510
+ for network in networks:
3511
+ preview_content.append(
3512
+ f" - {network.get('name', 'Unknown')} ({network.get('driver', 'overlay')})"
3513
+ )
3514
+
3515
+ tasks = config.get("tasks", [])
3516
+ if tasks:
3517
+ preview_content.append(
3518
+ f"[cyan]Execution:[/cyan] {tasks[0].get('type', 'Unknown')}"
3519
+ )
3520
+
3521
+ panel = Panel.fit(
3522
+ "\n".join(preview_content), title="Swarm Configuration Preview"
3523
+ )
3524
+ self.console.print(panel)
3525
+
3526
+ return Confirm.ask("\nDeploy this swarm configuration?", default=True)
3527
+ else:
3528
+ self.print("\nStep 4: Configuration Preview")
3529
+ self.print("=" * 40)
3530
+ self.print(f"Swarm Name: {config.get('name', 'Unknown')}")
3531
+ self.print(f"Modules: {len(config.get('modules', []))}")
3532
+
3533
+ modules = config.get("modules", [])
3534
+ for module in modules:
3535
+ self.print(
3536
+ f" - {module.get('alias', 'Unknown')} ({module.get('id', 'Unknown')})"
3537
+ )
3538
+
3539
+ globals_config = config.get("globals", {})
3540
+ if globals_config:
3541
+ self.print(f"Global Parameters: {len(globals_config)}")
3542
+ for key in globals_config.keys():
3543
+ self.print(f" - {key}")
3544
+
3545
+ networks = config.get("networks", [])
3546
+ if networks:
3547
+ self.print(f"Networks: {len(networks)}")
3548
+ for network in networks:
3549
+ self.print(
3550
+ f" - {network.get('name', 'Unknown')} ({network.get('driver', 'overlay')})"
3551
+ )
3552
+
3553
+ self.print("=" * 40)
3554
+
3555
+ confirm = input("Deploy this swarm configuration? (Y/n): ").strip().lower()
3556
+ return confirm in ["", "y", "yes"]
3557
+
3558
+ def _execute_deployment(self, api, cluster_id: str, swarm: Any) -> int:
3559
+ """Execute the swarm deployment."""
3560
+ try:
3561
+ if self.console:
3562
+ self.console.print(
3563
+ "\n[bold yellow]Step 5: Deploying Swarm[/bold yellow]"
3564
+ )
3565
+ else:
3566
+ self.print("\nStep 5: Deploying Swarm")
3567
+
3568
+ # Deploy swarm
3569
+ async def run():
3570
+ return await api.send_swarm(cluster_id, swarm)
3571
+
3572
+ if self.console:
3573
+ with Progress(
3574
+ SpinnerColumn(),
3575
+ TextColumn("[progress.description]{task.description}"),
3576
+ console=self.console,
3577
+ ) as progress:
3578
+ task = progress.add_task("Sending swarm to cluster...", total=None)
3579
+ result = asyncio.run(run())
3580
+ progress.update(task, completed=True)
3581
+ else:
3582
+ self.print("Sending swarm to cluster...")
3583
+ result = asyncio.run(run())
3584
+
3585
+ swarm_id = result.get("swarm_id")
3586
+ if not swarm_id:
3587
+ self.print_error("Failed to get swarm ID from deployment result")
3588
+ return 1
3589
+
3590
+ # Start the swarm
3591
+ async def start_run():
3592
+ return await api.start_swarm(swarm_id)
3593
+
3594
+ if self.console:
3595
+ with Progress(
3596
+ SpinnerColumn(),
3597
+ TextColumn("[progress.description]{task.description}"),
3598
+ console=self.console,
3599
+ ) as progress:
3600
+ task = progress.add_task("Starting swarm execution...", total=None)
3601
+ asyncio.run(start_run())
3602
+ progress.update(task, completed=True)
3603
+ else:
3604
+ self.print("Starting swarm execution...")
3605
+ asyncio.run(start_run())
3606
+
3607
+ # Display success message
3608
+ if self.console:
3609
+ success_panel = Panel.fit(
3610
+ f"[green]Swarm deployed successfully![/green]\n\n"
3611
+ f"[cyan]Swarm ID:[/cyan] {swarm_id}\n"
3612
+ f"[cyan]Cluster ID:[/cyan] {cluster_id}\n"
3613
+ f"[cyan]Status:[/cyan] {result.get('status', 'Unknown')}\n\n"
3614
+ f"Use the following commands to monitor your swarm:\n"
3615
+ f" [yellow]manta swarm show {swarm_id}[/yellow]\n"
3616
+ f" [yellow]manta swarm tasks {swarm_id}[/yellow]\n"
3617
+ f" [yellow]manta results list {swarm_id} <tag>[/yellow]",
3618
+ title="Deployment Complete",
3619
+ )
3620
+ self.console.print(success_panel)
3621
+ else:
3622
+ self.print("\n" + "=" * 50)
3623
+ self.print("DEPLOYMENT SUCCESSFUL!")
3624
+ self.print("=" * 50)
3625
+ self.print(f"Swarm ID: {swarm_id}")
3626
+ self.print(f"Cluster ID: {cluster_id}")
3627
+ self.print(f"Status: {result.get('status', 'Unknown')}")
3628
+ self.print("\nMonitoring commands:")
3629
+ self.print(f" manta swarm show {swarm_id}")
3630
+ self.print(f" manta swarm tasks {swarm_id}")
3631
+ self.print(f" manta results list {swarm_id} <tag>")
3632
+ self.print("=" * 50)
3633
+
3634
+ return 0
3635
+
3636
+ except Exception as e:
3637
+ self.print_error(f"Deployment failed: {e}")
3638
+ return 1
3639
+
3640
+ def _run_async(self, coroutine):
3641
+ """Run an async coroutine and return the result."""
3642
+ try:
3643
+ return asyncio.run(coroutine)
3644
+ except Exception as e:
3645
+ self.print_error(f"Async operation failed: {e}")
3646
+ return None
3647
+
3648
+ def _format_user_info_table(self, user_info: Dict) -> Table:
3649
+ """Format user info as Rich table."""
3650
+ table = Table(
3651
+ title="User Information", show_header=True, header_style="bold blue"
3652
+ )
3653
+ table.add_column("Field", style="cyan", no_wrap=True)
3654
+ table.add_column("Value", style="white")
3655
+
3656
+ # Add user info rows
3657
+ for key, value in user_info.items():
3658
+ if key == "id":
3659
+ table.add_row("User ID", str(value))
3660
+ elif key == "username":
3661
+ table.add_row("Username", str(value))
3662
+ elif key == "created_at":
3663
+ table.add_row("Created At", str(value))
3664
+ elif key == "email":
3665
+ table.add_row("Email", str(value))
3666
+ elif key == "clusters":
3667
+ table.add_row("Active Clusters", str(value))
3668
+ elif key == "swarms":
3669
+ table.add_row("Total Swarms", str(value))
3670
+ else:
3671
+ # Convert underscores to spaces and capitalize
3672
+ field_name = key.replace("_", " ").title()
3673
+ table.add_row(field_name, str(value))
3674
+
3675
+ return table
3676
+
3677
+ def _format_cluster_table(self, clusters: List[Dict]) -> Table:
3678
+ """Format clusters as Rich table."""
3679
+ table = Table(title="Clusters", show_header=True, header_style="bold blue")
3680
+ table.add_column("ID", style="cyan", no_wrap=True)
3681
+ table.add_column("Name", style="white")
3682
+ table.add_column("Status", style="green")
3683
+ table.add_column("Nodes", justify="right", style="yellow")
3684
+ table.add_column("Created At", style="dim")
3685
+
3686
+ for cluster in clusters:
3687
+ status_color = "green" if cluster.get("status") == "active" else "red"
3688
+ table.add_row(
3689
+ cluster.get("id", ""),
3690
+ cluster.get("name", ""),
3691
+ f"[{status_color}]{cluster.get('status', 'unknown')}[/{status_color}]",
3692
+ str(cluster.get("nodes", 0)),
3693
+ cluster.get("created_at", ""),
3694
+ )
3695
+
3696
+ return table
3697
+
3698
+ def _format_swarm_table(self, swarms: List[Dict]) -> Table:
3699
+ """Format swarms as Rich table."""
3700
+ table = Table(title="Swarms", show_header=True, header_style="bold blue")
3701
+ table.add_column("ID", style="cyan", no_wrap=True)
3702
+ table.add_column("Cluster ID", style="white")
3703
+ table.add_column("Status", style="green")
3704
+ table.add_column("Progress", justify="right", style="yellow")
3705
+ table.add_column("Created At", style="dim")
3706
+
3707
+ for swarm in swarms:
3708
+ status_color = {
3709
+ "running": "green",
3710
+ "completed": "blue",
3711
+ "failed": "red",
3712
+ "pending": "yellow",
3713
+ }.get(swarm.get("status"), "white")
3714
+
3715
+ progress = swarm.get("progress", 0)
3716
+ if isinstance(progress, (int, float)):
3717
+ progress_text = f"{progress * 100:.1f}%"
3718
+ else:
3719
+ progress_text = str(progress)
3720
+
3721
+ table.add_row(
3722
+ swarm.get("id", ""),
3723
+ swarm.get("cluster_id", ""),
3724
+ f"[{status_color}]{swarm.get('status', 'unknown')}[/{status_color}]",
3725
+ progress_text,
3726
+ swarm.get("created_at", ""),
3727
+ )
3728
+
3729
+ return table
3730
+
3731
+ def _format_module_table(self, modules: List[Dict]) -> Table:
3732
+ """Format modules as Rich table."""
3733
+ table = Table(title="Modules", show_header=True, header_style="bold blue")
3734
+ table.add_column("ID", style="cyan", no_wrap=True)
3735
+ table.add_column("Name", style="white")
3736
+ table.add_column("Version", style="green")
3737
+ table.add_column("Size", justify="right", style="yellow")
3738
+
3739
+ for module in modules:
3740
+ size = module.get("size", 0)
3741
+ if isinstance(size, int):
3742
+ size_text = self._format_size(size)
3743
+ else:
3744
+ size_text = str(size)
3745
+
3746
+ table.add_row(
3747
+ module.get("id", ""),
3748
+ module.get("name", ""),
3749
+ module.get("version", ""),
3750
+ size_text,
3751
+ )
3752
+
3753
+ return table
3754
+
3755
+ def _interactive_swarm_deploy(self, api) -> int:
3756
+ """Interactive swarm deployment."""
3757
+ try:
3758
+ # Prompt for cluster ID
3759
+ cluster_id = Prompt.ask("Enter cluster ID")
3760
+
3761
+ # Prompt for swarm config
3762
+ config_path = Prompt.ask("Enter path to swarm configuration file")
3763
+
3764
+ # Check if file exists
3765
+ config_file = Path(config_path)
3766
+ if not config_file.exists():
3767
+ self.print_error(f"Configuration file not found: {config_path}")
3768
+ return 1
3769
+
3770
+ # Load config
3771
+ try:
3772
+ config_content = config_file.read_text()
3773
+ swarm_config = json.loads(config_content)
3774
+ except Exception as e:
3775
+ self.print_error(f"Failed to load configuration: {e}")
3776
+ return 1
3777
+
3778
+ # Confirm deployment
3779
+ confirm = Confirm.ask(f"Deploy swarm to cluster {cluster_id}?")
3780
+ if not confirm:
3781
+ self.print("Deployment cancelled.")
3782
+ return 0
3783
+
3784
+ # Deploy swarm
3785
+ async def deploy():
3786
+ return await api.deploy_swarm(cluster_id, swarm_config)
3787
+
3788
+ result = self._run_async(deploy())
3789
+ if result:
3790
+ self.print_success(
3791
+ f"Swarm deployed successfully: {result.get('id', 'unknown')}"
3792
+ )
3793
+ return 0
3794
+ else:
3795
+ return 1
3796
+
3797
+ except Exception as e:
3798
+ self.print_error(f"Interactive deployment failed: {e}")
3799
+ return 1
3800
+
3801
+ def _stream_results(self, api, swarm_id: str, tag: str) -> int:
3802
+ """Stream results from a swarm."""
3803
+ try:
3804
+ self.print(f"Streaming results for swarm {swarm_id}, tag: {tag}")
3805
+ self.print("Press Ctrl+C to stop streaming...")
3806
+
3807
+ while True:
3808
+ try:
3809
+
3810
+ async def get_results():
3811
+ return await api.get_results(swarm_id, tag)
3812
+
3813
+ results = self._run_async(get_results())
3814
+ if results:
3815
+ for result in results:
3816
+ timestamp = result.get("created_at", "unknown")
3817
+ data = result.get("data", {})
3818
+ self.print(f"[{timestamp}] {json.dumps(data, indent=2)}")
3819
+
3820
+ time.sleep(5) # Wait 5 seconds before next poll
3821
+
3822
+ except KeyboardInterrupt:
3823
+ self.print("\nStreaming stopped.")
3824
+ return 130 # KeyboardInterrupt exit code
3825
+
3826
+ except Exception as e:
3827
+ self.print_error(f"Streaming failed: {e}")
3828
+ return 1
3829
+
3830
+ def _export_results(self, api, swarm_id: str, tag: str, output_file: str) -> int:
3831
+ """Export results to a file."""
3832
+ try:
3833
+
3834
+ async def get_results():
3835
+ return await api.get_results(swarm_id, tag)
3836
+
3837
+ results = self._run_async(get_results())
3838
+ if not results:
3839
+ self.print_error("No results found")
3840
+ return 1
3841
+
3842
+ # Write to file
3843
+ with open(output_file, "w") as f:
3844
+ json.dump(results, f, indent=2)
3845
+
3846
+ self.print_success(f"Results exported to {output_file}")
3847
+ return 0
3848
+
3849
+ except Exception as e:
3850
+ self.print_error(f"Export failed: {e}")
3851
+ return 1
3852
+
3853
+ # ============================================
3854
+ # Utility Functions
3855
+ # ============================================
3856
+
3857
+ def _format_size(self, size_bytes: int) -> str:
3858
+ """Format byte size in human readable format."""
3859
+ if size_bytes == 0:
3860
+ return "0 B"
3861
+
3862
+ for unit in ["B", "KB", "MB", "GB", "TB"]:
3863
+ if size_bytes < 1024.0:
3864
+ return f"{size_bytes:.1f} {unit}"
3865
+ size_bytes /= 1024.0
3866
+ return f"{size_bytes:.1f} PB"
3867
+
3868
+ def print_warning(self, message: str):
3869
+ """Print warning message."""
3870
+ if self.console:
3871
+ self.console.print(f"[yellow]Warning:[/yellow] {message}")
3872
+ else:
3873
+ print(f"Warning: {message}")