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.
- manta/__init__.light.py +22 -0
- manta/__init__.py +83 -0
- manta/__main__.py +21 -0
- manta/apis/__init__.py +7 -0
- manta/apis/async_user_api.py +6458 -0
- manta/apis/graph.py +498 -0
- manta/apis/module.py +316 -0
- manta/apis/results.py +251 -0
- manta/apis/swarm.py +206 -0
- manta/apis/user_api.py +1016 -0
- manta/cli/__init__.py +1 -0
- manta/cli/commands/__init__.py +1 -0
- manta/cli/commands/base_handler.py +229 -0
- manta/cli/commands/doc.py +192 -0
- manta/cli/commands/install.py +346 -0
- manta/cli/commands/sdk.py +9 -0
- manta/cli/commands/sdk_cluster.py +211 -0
- manta/cli/commands/sdk_config.py +347 -0
- manta/cli/commands/sdk_globals.py +280 -0
- manta/cli/commands/sdk_logs.py +174 -0
- manta/cli/commands/sdk_main.py +167 -0
- manta/cli/commands/sdk_module.py +516 -0
- manta/cli/commands/sdk_nodes.py +168 -0
- manta/cli/commands/sdk_original.py +3873 -0
- manta/cli/commands/sdk_results.py +265 -0
- manta/cli/commands/sdk_swarm.py +454 -0
- manta/cli/commands/sdk_user.py +234 -0
- manta/cli/commands/status.py +292 -0
- manta/cli/component_detector.py +112 -0
- manta/cli/config_manager.py +445 -0
- manta/cli/main.py +265 -0
- manta/cli/utils/__init__.py +27 -0
- manta/cli/utils/converters.py +140 -0
- manta/clients/cluster_management_client.py +486 -0
- manta/clients/local_client.py +149 -0
- manta/clients/module_management_client.py +217 -0
- manta/clients/swarm_management_client.py +562 -0
- manta/clients/user_management_client.py +395 -0
- manta/clients/world_client.py +195 -0
- manta/light/__init__.py +31 -0
- manta/light/globals.py +245 -0
- manta/light/local.py +407 -0
- manta/light/logging_config.py +39 -0
- manta/light/path.py +116 -0
- manta/light/results.py +236 -0
- manta/light/task.py +100 -0
- manta/light/utils.py +217 -0
- manta/light/world.py +177 -0
- mantatech_sdk-0.5b0.dev65.dist-info/METADATA +1039 -0
- mantatech_sdk-0.5b0.dev65.dist-info/RECORD +54 -0
- mantatech_sdk-0.5b0.dev65.dist-info/WHEEL +5 -0
- mantatech_sdk-0.5b0.dev65.dist-info/entry_points.txt +2 -0
- mantatech_sdk-0.5b0.dev65.dist-info/licenses/LICENSE +683 -0
- 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}")
|