alphai 0.0.7__py3-none-any.whl → 0.1.0__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.
- alphai/__init__.py +8 -4
- alphai/auth.py +362 -0
- alphai/cli.py +1015 -0
- alphai/client.py +400 -0
- alphai/config.py +88 -0
- alphai/docker.py +764 -0
- alphai/utils.py +192 -0
- alphai-0.1.0.dist-info/METADATA +394 -0
- alphai-0.1.0.dist-info/RECORD +12 -0
- {alphai-0.0.7.dist-info → alphai-0.1.0.dist-info}/WHEEL +2 -1
- alphai-0.1.0.dist-info/entry_points.txt +2 -0
- alphai-0.1.0.dist-info/top_level.txt +1 -0
- alphai/alphai.py +0 -786
- alphai/api/client.py +0 -0
- alphai/benchmarking/benchmarker.py +0 -37
- alphai/client/__init__.py +0 -0
- alphai/client/client.py +0 -382
- alphai/profilers/__init__.py +0 -0
- alphai/profilers/configs_base.py +0 -7
- alphai/profilers/jax.py +0 -37
- alphai/profilers/pytorch.py +0 -83
- alphai/profilers/pytorch_utils.py +0 -419
- alphai/util.py +0 -19
- alphai-0.0.7.dist-info/LICENSE +0 -201
- alphai-0.0.7.dist-info/METADATA +0 -125
- alphai-0.0.7.dist-info/RECORD +0 -16
alphai/docker.py
ADDED
|
@@ -0,0 +1,764 @@
|
|
|
1
|
+
"""Docker management for alphai CLI."""
|
|
2
|
+
|
|
3
|
+
import sys
|
|
4
|
+
from typing import Optional, Dict, Any, List
|
|
5
|
+
import subprocess
|
|
6
|
+
import shutil
|
|
7
|
+
from rich.console import Console
|
|
8
|
+
from rich.progress import Progress, SpinnerColumn, TextColumn
|
|
9
|
+
from rich.panel import Panel
|
|
10
|
+
|
|
11
|
+
|
|
12
|
+
class DockerManager:
|
|
13
|
+
"""Manage Docker operations for the alphai CLI."""
|
|
14
|
+
|
|
15
|
+
def __init__(self, console: Console):
|
|
16
|
+
"""Initialize the Docker manager."""
|
|
17
|
+
self.console = console
|
|
18
|
+
self._docker_available = None
|
|
19
|
+
|
|
20
|
+
def is_docker_available(self) -> bool:
|
|
21
|
+
"""Check if Docker is available and running."""
|
|
22
|
+
if self._docker_available is not None:
|
|
23
|
+
return self._docker_available
|
|
24
|
+
|
|
25
|
+
# Check if docker command exists
|
|
26
|
+
if not shutil.which("docker"):
|
|
27
|
+
self._docker_available = False
|
|
28
|
+
return False
|
|
29
|
+
|
|
30
|
+
# Check if Docker daemon is running
|
|
31
|
+
try:
|
|
32
|
+
result = subprocess.run(
|
|
33
|
+
["docker", "info"],
|
|
34
|
+
capture_output=True,
|
|
35
|
+
text=True,
|
|
36
|
+
timeout=10
|
|
37
|
+
)
|
|
38
|
+
self._docker_available = result.returncode == 0
|
|
39
|
+
return self._docker_available
|
|
40
|
+
except (subprocess.TimeoutExpired, subprocess.SubprocessError):
|
|
41
|
+
self._docker_available = False
|
|
42
|
+
return False
|
|
43
|
+
|
|
44
|
+
def pull_image(self, image: str) -> bool:
|
|
45
|
+
"""Pull a Docker image."""
|
|
46
|
+
if not self.is_docker_available():
|
|
47
|
+
self.console.print("[red]Error: Docker is not available or not running[/red]")
|
|
48
|
+
return False
|
|
49
|
+
|
|
50
|
+
try:
|
|
51
|
+
with Progress(
|
|
52
|
+
SpinnerColumn(),
|
|
53
|
+
TextColumn("[progress.description]{task.description}"),
|
|
54
|
+
console=self.console
|
|
55
|
+
) as progress:
|
|
56
|
+
task = progress.add_task(f"Pulling image {image}...", total=None)
|
|
57
|
+
|
|
58
|
+
result = subprocess.run(
|
|
59
|
+
["docker", "pull", image],
|
|
60
|
+
capture_output=True,
|
|
61
|
+
text=True,
|
|
62
|
+
timeout=300 # 5 minutes timeout
|
|
63
|
+
)
|
|
64
|
+
|
|
65
|
+
progress.update(task, completed=1)
|
|
66
|
+
|
|
67
|
+
if result.returncode == 0:
|
|
68
|
+
self.console.print(f"[green]✓ Successfully pulled image {image}[/green]")
|
|
69
|
+
return True
|
|
70
|
+
else:
|
|
71
|
+
self.console.print(f"[red]Error pulling image {image}: {result.stderr}[/red]")
|
|
72
|
+
return False
|
|
73
|
+
|
|
74
|
+
except subprocess.TimeoutExpired:
|
|
75
|
+
self.console.print(f"[red]Timeout pulling image {image}[/red]")
|
|
76
|
+
return False
|
|
77
|
+
except Exception as e:
|
|
78
|
+
self.console.print(f"[red]Error pulling image {image}: {e}[/red]")
|
|
79
|
+
return False
|
|
80
|
+
|
|
81
|
+
def run_container(
|
|
82
|
+
self,
|
|
83
|
+
image: str,
|
|
84
|
+
name: Optional[str] = None,
|
|
85
|
+
ports: Optional[Dict[int, int]] = None,
|
|
86
|
+
environment: Optional[Dict[str, str]] = None,
|
|
87
|
+
volumes: Optional[Dict[str, str]] = None,
|
|
88
|
+
detach: bool = False,
|
|
89
|
+
command: Optional[str] = None
|
|
90
|
+
) -> Optional[Any]:
|
|
91
|
+
"""Run a Docker container with the specified configuration."""
|
|
92
|
+
if not self.is_docker_available():
|
|
93
|
+
self.console.print("[red]Error: Docker is not available or not running[/red]")
|
|
94
|
+
self.console.print("[yellow]Please install Docker and ensure it's running[/yellow]")
|
|
95
|
+
return None
|
|
96
|
+
|
|
97
|
+
# Build docker run command
|
|
98
|
+
cmd = ["docker", "run"]
|
|
99
|
+
|
|
100
|
+
# Add name if specified
|
|
101
|
+
if name:
|
|
102
|
+
cmd.extend(["--name", name])
|
|
103
|
+
|
|
104
|
+
# Add port mappings
|
|
105
|
+
if ports:
|
|
106
|
+
for host_port, container_port in ports.items():
|
|
107
|
+
cmd.extend(["-p", f"{host_port}:{container_port}"])
|
|
108
|
+
|
|
109
|
+
# Add environment variables
|
|
110
|
+
if environment:
|
|
111
|
+
for key, value in environment.items():
|
|
112
|
+
cmd.extend(["-e", f"{key}={value}"])
|
|
113
|
+
|
|
114
|
+
# Add volume mounts
|
|
115
|
+
if volumes:
|
|
116
|
+
for host_path, container_path in volumes.items():
|
|
117
|
+
cmd.extend(["-v", f"{host_path}:{container_path}"])
|
|
118
|
+
|
|
119
|
+
# Add detach flag
|
|
120
|
+
if detach:
|
|
121
|
+
cmd.append("-d")
|
|
122
|
+
else:
|
|
123
|
+
cmd.extend(["-it"])
|
|
124
|
+
|
|
125
|
+
# Remove container when it exits (unless detached)
|
|
126
|
+
if not detach:
|
|
127
|
+
cmd.append("--rm")
|
|
128
|
+
|
|
129
|
+
# Add the image
|
|
130
|
+
cmd.append(image)
|
|
131
|
+
|
|
132
|
+
# Add custom command if specified
|
|
133
|
+
if command:
|
|
134
|
+
cmd.extend(["bash", "-c", command])
|
|
135
|
+
|
|
136
|
+
try:
|
|
137
|
+
# Check if image exists locally, pull if not
|
|
138
|
+
check_result = subprocess.run(
|
|
139
|
+
["docker", "images", "-q", image],
|
|
140
|
+
capture_output=True,
|
|
141
|
+
text=True
|
|
142
|
+
)
|
|
143
|
+
|
|
144
|
+
if not check_result.stdout.strip():
|
|
145
|
+
self.console.print(f"[yellow]Image {image} not found locally, pulling...[/yellow]")
|
|
146
|
+
if not self.pull_image(image):
|
|
147
|
+
return None
|
|
148
|
+
|
|
149
|
+
# Run the container
|
|
150
|
+
if detach:
|
|
151
|
+
with Progress(
|
|
152
|
+
SpinnerColumn(),
|
|
153
|
+
TextColumn("[progress.description]{task.description}"),
|
|
154
|
+
console=self.console
|
|
155
|
+
) as progress:
|
|
156
|
+
task = progress.add_task("Starting container...", total=None)
|
|
157
|
+
|
|
158
|
+
result = subprocess.run(
|
|
159
|
+
cmd,
|
|
160
|
+
capture_output=True,
|
|
161
|
+
text=True,
|
|
162
|
+
timeout=30
|
|
163
|
+
)
|
|
164
|
+
|
|
165
|
+
progress.update(task, completed=1)
|
|
166
|
+
|
|
167
|
+
if result.returncode == 0:
|
|
168
|
+
container_id = result.stdout.strip()
|
|
169
|
+
return MockContainer(container_id)
|
|
170
|
+
else:
|
|
171
|
+
self.console.print(f"[red]Error starting container: {result.stderr}[/red]")
|
|
172
|
+
return None
|
|
173
|
+
else:
|
|
174
|
+
# Interactive mode
|
|
175
|
+
self.console.print(f"[green]Starting interactive container from {image}...[/green]")
|
|
176
|
+
self.console.print("[dim]Press Ctrl+C to stop the container[/dim]")
|
|
177
|
+
|
|
178
|
+
try:
|
|
179
|
+
# Run interactively without capturing output
|
|
180
|
+
result = subprocess.run(cmd)
|
|
181
|
+
return MockContainer("interactive")
|
|
182
|
+
except KeyboardInterrupt:
|
|
183
|
+
self.console.print("\n[yellow]Container stopped by user[/yellow]")
|
|
184
|
+
return MockContainer("interactive")
|
|
185
|
+
|
|
186
|
+
except subprocess.TimeoutExpired:
|
|
187
|
+
self.console.print("[red]Timeout starting container[/red]")
|
|
188
|
+
return None
|
|
189
|
+
except Exception as e:
|
|
190
|
+
self.console.print(f"[red]Error running container: {e}[/red]")
|
|
191
|
+
return None
|
|
192
|
+
|
|
193
|
+
def list_containers(self, all_containers: bool = False) -> list:
|
|
194
|
+
"""List Docker containers."""
|
|
195
|
+
if not self.is_docker_available():
|
|
196
|
+
return []
|
|
197
|
+
|
|
198
|
+
try:
|
|
199
|
+
cmd = ["docker", "ps"]
|
|
200
|
+
if all_containers:
|
|
201
|
+
cmd.append("-a")
|
|
202
|
+
|
|
203
|
+
result = subprocess.run(
|
|
204
|
+
cmd,
|
|
205
|
+
capture_output=True,
|
|
206
|
+
text=True,
|
|
207
|
+
timeout=10
|
|
208
|
+
)
|
|
209
|
+
|
|
210
|
+
if result.returncode == 0:
|
|
211
|
+
# Parse output (this is a simplified version)
|
|
212
|
+
lines = result.stdout.strip().split('\n')
|
|
213
|
+
if len(lines) > 1:
|
|
214
|
+
return lines[1:] # Skip header
|
|
215
|
+
return []
|
|
216
|
+
else:
|
|
217
|
+
return []
|
|
218
|
+
|
|
219
|
+
except Exception:
|
|
220
|
+
return []
|
|
221
|
+
|
|
222
|
+
def stop_container(self, container_id: str) -> bool:
|
|
223
|
+
"""Stop a running container."""
|
|
224
|
+
if not self.is_docker_available():
|
|
225
|
+
return False
|
|
226
|
+
|
|
227
|
+
try:
|
|
228
|
+
result = subprocess.run(
|
|
229
|
+
["docker", "stop", container_id],
|
|
230
|
+
capture_output=True,
|
|
231
|
+
text=True,
|
|
232
|
+
timeout=30
|
|
233
|
+
)
|
|
234
|
+
|
|
235
|
+
return result.returncode == 0
|
|
236
|
+
|
|
237
|
+
except Exception:
|
|
238
|
+
return False
|
|
239
|
+
|
|
240
|
+
def remove_container(self, container_id: str, force: bool = False) -> bool:
|
|
241
|
+
"""Remove a container."""
|
|
242
|
+
if not self.is_docker_available():
|
|
243
|
+
return False
|
|
244
|
+
|
|
245
|
+
try:
|
|
246
|
+
cmd = ["docker", "rm"]
|
|
247
|
+
if force:
|
|
248
|
+
cmd.append("-f")
|
|
249
|
+
cmd.append(container_id)
|
|
250
|
+
|
|
251
|
+
result = subprocess.run(
|
|
252
|
+
cmd,
|
|
253
|
+
capture_output=True,
|
|
254
|
+
text=True,
|
|
255
|
+
timeout=30
|
|
256
|
+
)
|
|
257
|
+
|
|
258
|
+
return result.returncode == 0
|
|
259
|
+
|
|
260
|
+
except Exception:
|
|
261
|
+
return False
|
|
262
|
+
|
|
263
|
+
def install_cloudflared_in_container(self, container_id: str) -> bool:
|
|
264
|
+
"""Install cloudflared in a running container."""
|
|
265
|
+
if not self.is_docker_available():
|
|
266
|
+
return False
|
|
267
|
+
|
|
268
|
+
try:
|
|
269
|
+
# Detect the container's package manager and architecture
|
|
270
|
+
package_manager = self._detect_package_manager(container_id)
|
|
271
|
+
architecture = self._detect_architecture(container_id)
|
|
272
|
+
|
|
273
|
+
if not package_manager:
|
|
274
|
+
self.console.print("[red]Unsupported container: No compatible package manager found[/red]")
|
|
275
|
+
return False
|
|
276
|
+
|
|
277
|
+
# Commands to install cloudflared based on package manager
|
|
278
|
+
install_commands = self._get_install_commands(package_manager, architecture)
|
|
279
|
+
|
|
280
|
+
with Progress(
|
|
281
|
+
SpinnerColumn(),
|
|
282
|
+
TextColumn("[progress.description]{task.description}"),
|
|
283
|
+
console=self.console
|
|
284
|
+
) as progress:
|
|
285
|
+
task = progress.add_task("Installing cloudflared in container...", total=len(install_commands))
|
|
286
|
+
|
|
287
|
+
for i, command in enumerate(install_commands):
|
|
288
|
+
result = subprocess.run(
|
|
289
|
+
["docker", "exec", "--user", "root", container_id, "bash", "-c", command],
|
|
290
|
+
capture_output=True,
|
|
291
|
+
text=True,
|
|
292
|
+
timeout=60
|
|
293
|
+
)
|
|
294
|
+
|
|
295
|
+
if result.returncode != 0:
|
|
296
|
+
self.console.print(f"[red]Error running command '{command}': {result.stderr}[/red]")
|
|
297
|
+
return False
|
|
298
|
+
|
|
299
|
+
progress.update(task, advance=1)
|
|
300
|
+
|
|
301
|
+
self.console.print("[green]✓ cloudflared installed successfully[/green]")
|
|
302
|
+
return True
|
|
303
|
+
|
|
304
|
+
except subprocess.TimeoutExpired:
|
|
305
|
+
self.console.print("[red]Timeout installing cloudflared[/red]")
|
|
306
|
+
return False
|
|
307
|
+
except Exception as e:
|
|
308
|
+
self.console.print(f"[red]Error installing cloudflared: {e}[/red]")
|
|
309
|
+
return False
|
|
310
|
+
|
|
311
|
+
def _detect_package_manager(self, container_id: str) -> Optional[str]:
|
|
312
|
+
"""Detect the package manager available in the container."""
|
|
313
|
+
package_managers = {
|
|
314
|
+
'apt': 'which apt',
|
|
315
|
+
'apt-get': 'which apt-get',
|
|
316
|
+
'yum': 'which yum',
|
|
317
|
+
'dnf': 'which dnf',
|
|
318
|
+
'apk': 'which apk',
|
|
319
|
+
'zypper': 'which zypper'
|
|
320
|
+
}
|
|
321
|
+
|
|
322
|
+
for pm_name, check_cmd in package_managers.items():
|
|
323
|
+
try:
|
|
324
|
+
result = subprocess.run(
|
|
325
|
+
["docker", "exec", container_id, "bash", "-c", check_cmd],
|
|
326
|
+
capture_output=True,
|
|
327
|
+
text=True,
|
|
328
|
+
timeout=10
|
|
329
|
+
)
|
|
330
|
+
if result.returncode == 0:
|
|
331
|
+
return pm_name
|
|
332
|
+
except:
|
|
333
|
+
continue
|
|
334
|
+
|
|
335
|
+
return None
|
|
336
|
+
|
|
337
|
+
def _detect_architecture(self, container_id: str) -> str:
|
|
338
|
+
"""Detect the container's architecture."""
|
|
339
|
+
try:
|
|
340
|
+
result = subprocess.run(
|
|
341
|
+
["docker", "exec", container_id, "uname", "-m"],
|
|
342
|
+
capture_output=True,
|
|
343
|
+
text=True,
|
|
344
|
+
timeout=10
|
|
345
|
+
)
|
|
346
|
+
if result.returncode == 0:
|
|
347
|
+
arch = result.stdout.strip()
|
|
348
|
+
# Map to cloudflared architecture names
|
|
349
|
+
arch_map = {
|
|
350
|
+
'x86_64': 'amd64',
|
|
351
|
+
'aarch64': 'arm64',
|
|
352
|
+
'armv7l': 'arm',
|
|
353
|
+
'i386': '386'
|
|
354
|
+
}
|
|
355
|
+
return arch_map.get(arch, 'amd64')
|
|
356
|
+
except:
|
|
357
|
+
pass
|
|
358
|
+
|
|
359
|
+
return 'amd64' # Default fallback
|
|
360
|
+
|
|
361
|
+
def _get_install_commands(self, package_manager: str, architecture: str) -> List[str]:
|
|
362
|
+
"""Get installation commands based on package manager and architecture."""
|
|
363
|
+
cloudflared_url = f"https://github.com/cloudflare/cloudflared/releases/latest/download/cloudflared-linux-{architecture}"
|
|
364
|
+
|
|
365
|
+
if package_manager == 'apt-get':
|
|
366
|
+
return [
|
|
367
|
+
"apt-get update",
|
|
368
|
+
"apt-get install -y wget",
|
|
369
|
+
f"wget -q https://github.com/cloudflare/cloudflared/releases/latest/download/cloudflared-linux-{architecture}.deb",
|
|
370
|
+
f"dpkg -i cloudflared-linux-{architecture}.deb",
|
|
371
|
+
f"rm cloudflared-linux-{architecture}.deb"
|
|
372
|
+
]
|
|
373
|
+
|
|
374
|
+
elif package_manager in ['yum', 'dnf']:
|
|
375
|
+
return [
|
|
376
|
+
f"{package_manager} update -y",
|
|
377
|
+
f"{package_manager} install -y wget",
|
|
378
|
+
f"wget -q {cloudflared_url} -O /usr/local/bin/cloudflared",
|
|
379
|
+
"chmod +x /usr/local/bin/cloudflared"
|
|
380
|
+
]
|
|
381
|
+
|
|
382
|
+
elif package_manager == 'apk':
|
|
383
|
+
return [
|
|
384
|
+
"apk update",
|
|
385
|
+
"apk add --no-cache wget",
|
|
386
|
+
f"wget -q {cloudflared_url} -O /usr/local/bin/cloudflared",
|
|
387
|
+
"chmod +x /usr/local/bin/cloudflared"
|
|
388
|
+
]
|
|
389
|
+
|
|
390
|
+
elif package_manager == 'zypper':
|
|
391
|
+
return [
|
|
392
|
+
"zypper refresh",
|
|
393
|
+
"zypper install -y wget",
|
|
394
|
+
f"wget -q {cloudflared_url} -O /usr/local/bin/cloudflared",
|
|
395
|
+
"chmod +x /usr/local/bin/cloudflared"
|
|
396
|
+
]
|
|
397
|
+
|
|
398
|
+
else:
|
|
399
|
+
# Generic approach - download binary directly
|
|
400
|
+
return [
|
|
401
|
+
f"wget -q {cloudflared_url} -O /usr/local/bin/cloudflared",
|
|
402
|
+
"chmod +x /usr/local/bin/cloudflared"
|
|
403
|
+
]
|
|
404
|
+
|
|
405
|
+
def setup_tunnel_in_container(self, container_id: str, tunnel_token: str) -> bool:
|
|
406
|
+
"""Set up cloudflared tunnel service in a running container."""
|
|
407
|
+
if not self.is_docker_available():
|
|
408
|
+
return False
|
|
409
|
+
|
|
410
|
+
try:
|
|
411
|
+
with Progress(
|
|
412
|
+
SpinnerColumn(),
|
|
413
|
+
TextColumn("[progress.description]{task.description}"),
|
|
414
|
+
console=self.console
|
|
415
|
+
) as progress:
|
|
416
|
+
task = progress.add_task("Setting up tunnel service...", total=None)
|
|
417
|
+
|
|
418
|
+
# Install the tunnel service with the token
|
|
419
|
+
result = subprocess.run(
|
|
420
|
+
["docker", "exec", "--user", "root", container_id, "cloudflared", "service", "install", tunnel_token],
|
|
421
|
+
capture_output=True,
|
|
422
|
+
text=True,
|
|
423
|
+
timeout=30
|
|
424
|
+
)
|
|
425
|
+
|
|
426
|
+
progress.update(task, completed=1)
|
|
427
|
+
|
|
428
|
+
if result.returncode == 0:
|
|
429
|
+
self.console.print("[green]✓ Tunnel service installed successfully[/green]")
|
|
430
|
+
return True
|
|
431
|
+
else:
|
|
432
|
+
self.console.print(f"[red]Error setting up tunnel service: {result.stderr}[/red]")
|
|
433
|
+
return False
|
|
434
|
+
|
|
435
|
+
except subprocess.TimeoutExpired:
|
|
436
|
+
self.console.print("[red]Timeout setting up tunnel service[/red]")
|
|
437
|
+
return False
|
|
438
|
+
except Exception as e:
|
|
439
|
+
self.console.print(f"[red]Error setting up tunnel service: {e}[/red]")
|
|
440
|
+
return False
|
|
441
|
+
|
|
442
|
+
def exec_command(self, container_id: str, command: str) -> Optional[str]:
|
|
443
|
+
"""Execute a command in a running container and return output."""
|
|
444
|
+
if not self.is_docker_available():
|
|
445
|
+
return None
|
|
446
|
+
|
|
447
|
+
try:
|
|
448
|
+
result = subprocess.run(
|
|
449
|
+
["docker", "exec", container_id, "bash", "-c", command],
|
|
450
|
+
capture_output=True,
|
|
451
|
+
text=True,
|
|
452
|
+
timeout=30
|
|
453
|
+
)
|
|
454
|
+
|
|
455
|
+
if result.returncode == 0:
|
|
456
|
+
return result.stdout.strip()
|
|
457
|
+
else:
|
|
458
|
+
self.console.print(f"[red]Command failed: {result.stderr}[/red]")
|
|
459
|
+
return None
|
|
460
|
+
|
|
461
|
+
except Exception as e:
|
|
462
|
+
self.console.print(f"[red]Error executing command: {e}[/red]")
|
|
463
|
+
return None
|
|
464
|
+
|
|
465
|
+
def get_container_logs(self, container_id: str, tail: int = 50) -> Optional[str]:
|
|
466
|
+
"""Get recent logs from a container."""
|
|
467
|
+
if not self.is_docker_available():
|
|
468
|
+
return None
|
|
469
|
+
|
|
470
|
+
try:
|
|
471
|
+
result = subprocess.run(
|
|
472
|
+
["docker", "logs", "--tail", str(tail), container_id],
|
|
473
|
+
capture_output=True,
|
|
474
|
+
text=True,
|
|
475
|
+
timeout=10
|
|
476
|
+
)
|
|
477
|
+
|
|
478
|
+
if result.returncode == 0:
|
|
479
|
+
return result.stdout + result.stderr
|
|
480
|
+
else:
|
|
481
|
+
return None
|
|
482
|
+
|
|
483
|
+
except Exception as e:
|
|
484
|
+
self.console.print(f"[red]Error getting container logs: {e}[/red]")
|
|
485
|
+
return None
|
|
486
|
+
|
|
487
|
+
def is_container_running(self, container_id: str) -> bool:
|
|
488
|
+
"""Check if a container is currently running."""
|
|
489
|
+
if not self.is_docker_available():
|
|
490
|
+
return False
|
|
491
|
+
|
|
492
|
+
try:
|
|
493
|
+
result = subprocess.run(
|
|
494
|
+
["docker", "ps", "-q", "--filter", f"id={container_id}"],
|
|
495
|
+
capture_output=True,
|
|
496
|
+
text=True,
|
|
497
|
+
timeout=10
|
|
498
|
+
)
|
|
499
|
+
|
|
500
|
+
return result.returncode == 0 and result.stdout.strip() != ""
|
|
501
|
+
|
|
502
|
+
except Exception as e:
|
|
503
|
+
self.console.print(f"[red]Error checking container status: {e}[/red]")
|
|
504
|
+
return False
|
|
505
|
+
|
|
506
|
+
def get_container_status(self, container_id: str) -> Optional[str]:
|
|
507
|
+
"""Get the status of a container."""
|
|
508
|
+
if not self.is_docker_available():
|
|
509
|
+
return None
|
|
510
|
+
|
|
511
|
+
try:
|
|
512
|
+
result = subprocess.run(
|
|
513
|
+
["docker", "ps", "-a", "--format", "{{.Status}}", "--filter", f"id={container_id}"],
|
|
514
|
+
capture_output=True,
|
|
515
|
+
text=True,
|
|
516
|
+
timeout=10
|
|
517
|
+
)
|
|
518
|
+
|
|
519
|
+
if result.returncode == 0:
|
|
520
|
+
return result.stdout.strip()
|
|
521
|
+
else:
|
|
522
|
+
return None
|
|
523
|
+
|
|
524
|
+
except Exception as e:
|
|
525
|
+
self.console.print(f"[red]Error getting container status: {e}[/red]")
|
|
526
|
+
return None
|
|
527
|
+
|
|
528
|
+
def ensure_jupyter_running(self, container_id: str, jupyter_port: int = 8888, jupyter_token: Optional[str] = None, force_restart: bool = False) -> tuple[bool, Optional[str]]:
|
|
529
|
+
"""Ensure Jupyter Lab is running in the container.
|
|
530
|
+
|
|
531
|
+
Args:
|
|
532
|
+
container_id: Docker container ID
|
|
533
|
+
jupyter_port: Port for Jupyter to listen on
|
|
534
|
+
jupyter_token: Token to use for Jupyter authentication
|
|
535
|
+
force_restart: If True, skip the "already running" check and start Jupyter with our token
|
|
536
|
+
|
|
537
|
+
Returns:
|
|
538
|
+
tuple: (success, jupyter_token) where success is bool and jupyter_token is the token used
|
|
539
|
+
"""
|
|
540
|
+
if not self.is_docker_available():
|
|
541
|
+
return False, None
|
|
542
|
+
|
|
543
|
+
# Use provided token or generate one
|
|
544
|
+
if not jupyter_token:
|
|
545
|
+
jupyter_token = self.generate_jupyter_token()
|
|
546
|
+
|
|
547
|
+
try:
|
|
548
|
+
# Check if Jupyter is already running (unless we want to force restart)
|
|
549
|
+
if not force_restart and self._is_jupyter_running(container_id, jupyter_port):
|
|
550
|
+
self.console.print("[yellow]⚠ Jupyter is already running with unknown token[/yellow]")
|
|
551
|
+
self.console.print("[dim]Consider using --force to restart with your token[/dim]")
|
|
552
|
+
return True, jupyter_token
|
|
553
|
+
|
|
554
|
+
# Try to start Jupyter Lab
|
|
555
|
+
self.console.print("[yellow]Starting Jupyter Lab with custom token...[/yellow]")
|
|
556
|
+
|
|
557
|
+
# Improved Jupyter startup commands with better compatibility
|
|
558
|
+
jupyter_commands = [
|
|
559
|
+
# Jupyter Lab (modern preferred)
|
|
560
|
+
f"jupyter lab --ip=0.0.0.0 --port={jupyter_port} --no-browser --allow-root --ServerApp.token={jupyter_token} --ServerApp.allow_origin='*' --ServerApp.base_url=/ --ServerApp.terminado_settings='{{\"shell_command\":[\"/bin/bash\"]}}'",
|
|
561
|
+
# Jupyter Lab with python -m (fallback)
|
|
562
|
+
f"python -m jupyter lab --ip=0.0.0.0 --port={jupyter_port} --no-browser --allow-root --ServerApp.token={jupyter_token} --ServerApp.allow_origin='*'",
|
|
563
|
+
# Jupyter Notebook (legacy compatibility)
|
|
564
|
+
f"jupyter notebook --ip=0.0.0.0 --port={jupyter_port} --no-browser --allow-root --NotebookApp.token={jupyter_token} --NotebookApp.allow_origin='*'",
|
|
565
|
+
# Jupyter Notebook with python -m
|
|
566
|
+
f"python -m jupyter notebook --ip=0.0.0.0 --port={jupyter_port} --no-browser --allow-root --NotebookApp.token={jupyter_token} --NotebookApp.allow_origin='*'"
|
|
567
|
+
]
|
|
568
|
+
|
|
569
|
+
for i, cmd in enumerate(jupyter_commands):
|
|
570
|
+
try:
|
|
571
|
+
# Start Jupyter in background
|
|
572
|
+
result = subprocess.run(
|
|
573
|
+
["docker", "exec", "-d", container_id, "bash", "-c", cmd],
|
|
574
|
+
capture_output=True,
|
|
575
|
+
text=True,
|
|
576
|
+
timeout=10
|
|
577
|
+
)
|
|
578
|
+
|
|
579
|
+
if result.returncode == 0:
|
|
580
|
+
# Wait a moment and check if it's running
|
|
581
|
+
import time
|
|
582
|
+
time.sleep(5) # Increased wait time for Jupyter to start
|
|
583
|
+
if self._is_jupyter_running(container_id, jupyter_port):
|
|
584
|
+
self.console.print("[green]✓ Jupyter Lab started successfully[/green]")
|
|
585
|
+
return True, jupyter_token
|
|
586
|
+
elif i == 0: # Only show this message on first attempt
|
|
587
|
+
self.console.print("[yellow]Trying alternative startup command...[/yellow]")
|
|
588
|
+
|
|
589
|
+
except subprocess.TimeoutExpired:
|
|
590
|
+
continue
|
|
591
|
+
except Exception:
|
|
592
|
+
continue
|
|
593
|
+
|
|
594
|
+
self.console.print("[yellow]⚠ Could not start Jupyter automatically[/yellow]")
|
|
595
|
+
self.console.print("[dim]Tip: Ensure Jupyter is installed in your container[/dim]")
|
|
596
|
+
return False, None
|
|
597
|
+
|
|
598
|
+
except Exception as e:
|
|
599
|
+
self.console.print(f"[red]Error ensuring Jupyter is running: {e}[/red]")
|
|
600
|
+
return False, None
|
|
601
|
+
|
|
602
|
+
def _is_jupyter_running(self, container_id: str, port: int) -> bool:
|
|
603
|
+
"""Check if Jupyter is running on the specified port."""
|
|
604
|
+
try:
|
|
605
|
+
# Check if the port is listening
|
|
606
|
+
result = subprocess.run(
|
|
607
|
+
["docker", "exec", container_id, "netstat", "-tln"],
|
|
608
|
+
capture_output=True,
|
|
609
|
+
text=True,
|
|
610
|
+
timeout=5
|
|
611
|
+
)
|
|
612
|
+
|
|
613
|
+
if result.returncode == 0:
|
|
614
|
+
return f":{port}" in result.stdout
|
|
615
|
+
|
|
616
|
+
# Fallback: check for Jupyter processes
|
|
617
|
+
result = subprocess.run(
|
|
618
|
+
["docker", "exec", container_id, "pgrep", "-f", "jupyter"],
|
|
619
|
+
capture_output=True,
|
|
620
|
+
text=True,
|
|
621
|
+
timeout=5
|
|
622
|
+
)
|
|
623
|
+
|
|
624
|
+
return result.returncode == 0
|
|
625
|
+
|
|
626
|
+
except Exception:
|
|
627
|
+
return False
|
|
628
|
+
|
|
629
|
+
def get_jupyter_startup_command(self, jupyter_port: int = 8888, jupyter_token: Optional[str] = None) -> str:
|
|
630
|
+
"""Get a universal Jupyter startup command."""
|
|
631
|
+
if not jupyter_token:
|
|
632
|
+
jupyter_token = self.generate_jupyter_token()
|
|
633
|
+
|
|
634
|
+
return (
|
|
635
|
+
f"jupyter lab --ip=0.0.0.0 --port={jupyter_port} "
|
|
636
|
+
f"--no-browser --allow-root --token={jupyter_token} "
|
|
637
|
+
f"--NotebookApp.allow_origin='*' "
|
|
638
|
+
f"--ServerApp.terminado_settings='{{\"shell_command\":[\"/bin/bash\"]}}'"
|
|
639
|
+
)
|
|
640
|
+
|
|
641
|
+
def generate_jupyter_token(self) -> str:
|
|
642
|
+
"""Generate a secure Jupyter token."""
|
|
643
|
+
import secrets
|
|
644
|
+
return secrets.token_hex(32) # 64-character hex token
|
|
645
|
+
|
|
646
|
+
def uninstall_cloudflared_service(self, container_id: str) -> bool:
|
|
647
|
+
"""Uninstall cloudflared service from a running container."""
|
|
648
|
+
if not self.is_docker_available():
|
|
649
|
+
return False
|
|
650
|
+
|
|
651
|
+
try:
|
|
652
|
+
with Progress(
|
|
653
|
+
SpinnerColumn(),
|
|
654
|
+
TextColumn("[progress.description]{task.description}"),
|
|
655
|
+
console=self.console
|
|
656
|
+
) as progress:
|
|
657
|
+
task = progress.add_task("Uninstalling cloudflared service...", total=None)
|
|
658
|
+
|
|
659
|
+
# Uninstall the cloudflared service
|
|
660
|
+
result = subprocess.run(
|
|
661
|
+
["docker", "exec", "--user", "root", container_id, "cloudflared", "service", "uninstall"],
|
|
662
|
+
capture_output=True,
|
|
663
|
+
text=True,
|
|
664
|
+
timeout=30
|
|
665
|
+
)
|
|
666
|
+
|
|
667
|
+
progress.update(task, completed=1)
|
|
668
|
+
|
|
669
|
+
if result.returncode == 0:
|
|
670
|
+
self.console.print("[green]✓ Cloudflared service uninstalled successfully[/green]")
|
|
671
|
+
return True
|
|
672
|
+
else:
|
|
673
|
+
# Don't treat this as a hard error since the service might not be installed
|
|
674
|
+
self.console.print(f"[yellow]Warning: Could not uninstall cloudflared service: {result.stderr}[/yellow]")
|
|
675
|
+
return True
|
|
676
|
+
|
|
677
|
+
except subprocess.TimeoutExpired:
|
|
678
|
+
self.console.print("[yellow]Warning: Timeout uninstalling cloudflared service[/yellow]")
|
|
679
|
+
return True
|
|
680
|
+
except Exception as e:
|
|
681
|
+
self.console.print(f"[yellow]Warning: Error uninstalling cloudflared service: {e}[/yellow]")
|
|
682
|
+
return True
|
|
683
|
+
|
|
684
|
+
def stop_and_remove_container(self, container_id: str, force: bool = False) -> bool:
|
|
685
|
+
"""Stop and remove a container."""
|
|
686
|
+
if not self.is_docker_available():
|
|
687
|
+
return False
|
|
688
|
+
|
|
689
|
+
try:
|
|
690
|
+
with Progress(
|
|
691
|
+
SpinnerColumn(),
|
|
692
|
+
TextColumn("[progress.description]{task.description}"),
|
|
693
|
+
console=self.console
|
|
694
|
+
) as progress:
|
|
695
|
+
task = progress.add_task("Stopping and removing container...", total=2)
|
|
696
|
+
|
|
697
|
+
# Stop the container
|
|
698
|
+
stop_result = self.stop_container(container_id)
|
|
699
|
+
progress.update(task, advance=1)
|
|
700
|
+
|
|
701
|
+
# Remove the container
|
|
702
|
+
remove_result = self.remove_container(container_id, force=force)
|
|
703
|
+
progress.update(task, advance=1)
|
|
704
|
+
|
|
705
|
+
if stop_result and remove_result:
|
|
706
|
+
self.console.print(f"[green]✓ Container {container_id[:12]} stopped and removed[/green]")
|
|
707
|
+
return True
|
|
708
|
+
else:
|
|
709
|
+
self.console.print(f"[yellow]Warning: Issues stopping/removing container {container_id[:12]}[/yellow]")
|
|
710
|
+
return False
|
|
711
|
+
|
|
712
|
+
except Exception as e:
|
|
713
|
+
self.console.print(f"[red]Error stopping/removing container: {e}[/red]")
|
|
714
|
+
return False
|
|
715
|
+
|
|
716
|
+
def cleanup_container_and_tunnel(
|
|
717
|
+
self,
|
|
718
|
+
container_id: str,
|
|
719
|
+
tunnel_id: Optional[str] = None,
|
|
720
|
+
project_id: Optional[str] = None,
|
|
721
|
+
force: bool = False
|
|
722
|
+
) -> bool:
|
|
723
|
+
"""Comprehensive cleanup of container, tunnel service, and optionally tunnel/project."""
|
|
724
|
+
if not self.is_docker_available():
|
|
725
|
+
return False
|
|
726
|
+
|
|
727
|
+
success = True
|
|
728
|
+
|
|
729
|
+
try:
|
|
730
|
+
# Check if container is running
|
|
731
|
+
if self.is_container_running(container_id):
|
|
732
|
+
# Step 1: Uninstall cloudflared service if container is running
|
|
733
|
+
self.console.print("[yellow]Cleaning up cloudflared service...[/yellow]")
|
|
734
|
+
if not self.uninstall_cloudflared_service(container_id):
|
|
735
|
+
success = False
|
|
736
|
+
|
|
737
|
+
# Give it a moment for the service to stop
|
|
738
|
+
import time
|
|
739
|
+
time.sleep(2)
|
|
740
|
+
|
|
741
|
+
# Step 2: Stop and remove container
|
|
742
|
+
self.console.print("[yellow]Stopping and removing container...[/yellow]")
|
|
743
|
+
if not self.stop_and_remove_container(container_id, force=force):
|
|
744
|
+
success = False
|
|
745
|
+
|
|
746
|
+
if success:
|
|
747
|
+
self.console.print("[green]✓ Container cleanup completed successfully[/green]")
|
|
748
|
+
else:
|
|
749
|
+
self.console.print("[yellow]⚠ Container cleanup completed with warnings[/yellow]")
|
|
750
|
+
|
|
751
|
+
return success
|
|
752
|
+
|
|
753
|
+
except Exception as e:
|
|
754
|
+
self.console.print(f"[red]Error during container cleanup: {e}[/red]")
|
|
755
|
+
return False
|
|
756
|
+
|
|
757
|
+
|
|
758
|
+
class MockContainer:
|
|
759
|
+
"""Mock container object to simulate Docker container."""
|
|
760
|
+
|
|
761
|
+
def __init__(self, container_id: str):
|
|
762
|
+
"""Initialize mock container."""
|
|
763
|
+
self.id = container_id
|
|
764
|
+
self.short_id = container_id[:12] if len(container_id) > 12 else container_id
|