unitlab 2.3.26__py3-none-any.whl → 2.3.28__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.
- unitlab/client.py +48 -56
- unitlab/simple_tunnel.py +205 -0
- {unitlab-2.3.26.dist-info → unitlab-2.3.28.dist-info}/METADATA +1 -1
- {unitlab-2.3.26.dist-info → unitlab-2.3.28.dist-info}/RECORD +8 -7
- {unitlab-2.3.26.dist-info → unitlab-2.3.28.dist-info}/LICENSE.md +0 -0
- {unitlab-2.3.26.dist-info → unitlab-2.3.28.dist-info}/WHEEL +0 -0
- {unitlab-2.3.26.dist-info → unitlab-2.3.28.dist-info}/entry_points.txt +0 -0
- {unitlab-2.3.26.dist-info → unitlab-2.3.28.dist-info}/top_level.txt +0 -0
unitlab/client.py
CHANGED
@@ -17,6 +17,7 @@ import psutil
|
|
17
17
|
from datetime import datetime, timezone
|
18
18
|
from .tunnel_config import CloudflareTunnel
|
19
19
|
from .cloudflare_api_tunnel import CloudflareAPITunnel
|
20
|
+
from .simple_tunnel import SimpleTunnel
|
20
21
|
from .utils import get_api_url, handle_exceptions
|
21
22
|
from pathlib import Path
|
22
23
|
|
@@ -37,9 +38,6 @@ except ImportError:
|
|
37
38
|
pass # dotenv not installed, use system env vars only
|
38
39
|
|
39
40
|
|
40
|
-
|
41
|
-
|
42
|
-
|
43
41
|
logger = logging.getLogger(__name__)
|
44
42
|
|
45
43
|
class UnitlabClient:
|
@@ -283,24 +281,12 @@ class UnitlabClient:
|
|
283
281
|
self.device_id = device_id
|
284
282
|
self.base_domain = base_domain
|
285
283
|
|
286
|
-
#
|
287
|
-
|
288
|
-
|
289
|
-
|
290
|
-
|
291
|
-
|
292
|
-
self.ssh_url = self.tunnel_manager.ssh_url
|
293
|
-
except Exception as e:
|
294
|
-
logger.warning(f"Failed to initialize API tunnel, falling back: {e}")
|
295
|
-
if CloudflareTunnel:
|
296
|
-
logger.info("Using service token Cloudflare tunnel")
|
297
|
-
self.tunnel_manager = CloudflareTunnel(base_domain, device_id)
|
298
|
-
self.jupyter_url = self.tunnel_manager.jupyter_url
|
299
|
-
self.ssh_url = self.tunnel_manager.ssh_url
|
300
|
-
else:
|
301
|
-
self.tunnel_manager = None
|
302
|
-
self.jupyter_url = f"https://jupyter-{device_id}.{base_domain}"
|
303
|
-
self.ssh_url = f"https://ssh-{device_id}.{base_domain}"
|
284
|
+
# Use the simple hardcoded tunnel approach
|
285
|
+
logger.info("Using simple hardcoded Cloudflare tunnel")
|
286
|
+
self.tunnel_manager = SimpleTunnel(device_id=self.device_id)
|
287
|
+
# SimpleTunnel generates the URL internally based on device_id
|
288
|
+
self.jupyter_url = self.tunnel_manager.jupyter_url
|
289
|
+
self.ssh_url = self.jupyter_url # Same URL for both services
|
304
290
|
|
305
291
|
# Setup signal handlers
|
306
292
|
signal.signal(signal.SIGINT, self._handle_shutdown)
|
@@ -400,23 +386,27 @@ class UnitlabClient:
|
|
400
386
|
def setup_tunnels(self) -> bool:
|
401
387
|
"""Setup Cloudflare tunnels"""
|
402
388
|
try:
|
403
|
-
if not self.jupyter_port:
|
404
|
-
logger.error("Jupyter port not available")
|
405
|
-
return False
|
406
|
-
|
407
389
|
if not self.tunnel_manager:
|
408
|
-
logger.warning("
|
390
|
+
logger.warning("Tunnel manager not available, skipping tunnel setup")
|
409
391
|
return True
|
410
392
|
|
411
|
-
logger.info("Setting up Cloudflare
|
412
|
-
self.tunnel_proc = self.tunnel_manager.setup(self.jupyter_port)
|
393
|
+
logger.info("Setting up Cloudflare tunnel...")
|
413
394
|
|
414
|
-
|
415
|
-
|
395
|
+
# SimpleTunnel handles both Jupyter and tunnel startup internally
|
396
|
+
# It doesn't need the jupyter_port parameter since it starts its own Jupyter
|
397
|
+
if self.tunnel_manager.start():
|
398
|
+
# Store the processes for monitoring
|
399
|
+
self.jupyter_proc = self.tunnel_manager.jupyter_process
|
400
|
+
self.tunnel_proc = self.tunnel_manager.tunnel_process
|
401
|
+
self.jupyter_port = "8888" # SimpleTunnel uses fixed port
|
402
|
+
|
403
|
+
# The tunnel is now running
|
404
|
+
logger.info("✅ Tunnel and Jupyter established")
|
416
405
|
self.report_services()
|
417
406
|
return True
|
418
|
-
|
419
|
-
|
407
|
+
else:
|
408
|
+
logger.error("Failed to start tunnel")
|
409
|
+
return False
|
420
410
|
|
421
411
|
except Exception as e:
|
422
412
|
logger.error(f"Tunnel setup failed: {e}")
|
@@ -603,13 +593,9 @@ class UnitlabClient:
|
|
603
593
|
# Check SSH
|
604
594
|
self.check_ssh()
|
605
595
|
|
606
|
-
#
|
607
|
-
|
608
|
-
|
609
|
-
return
|
610
|
-
|
611
|
-
# Wait a moment for Jupyter to fully initialize
|
612
|
-
time.sleep(1)
|
596
|
+
# SimpleTunnel handles Jupyter internally, so we don't start it separately
|
597
|
+
# Just setup the tunnels which will also start Jupyter
|
598
|
+
logger.info("Starting integrated Jupyter and tunnel...")
|
613
599
|
|
614
600
|
# Setup tunnels
|
615
601
|
if not self.setup_tunnels():
|
@@ -648,22 +634,28 @@ class UnitlabClient:
|
|
648
634
|
|
649
635
|
self.running = False
|
650
636
|
|
651
|
-
#
|
652
|
-
if self.
|
653
|
-
logger.info("Stopping Jupyter...")
|
654
|
-
self.
|
655
|
-
|
656
|
-
|
657
|
-
|
658
|
-
|
659
|
-
|
660
|
-
|
661
|
-
|
662
|
-
|
663
|
-
|
664
|
-
|
665
|
-
|
666
|
-
|
667
|
-
|
637
|
+
# With SimpleTunnel, we just call stop() method
|
638
|
+
if self.tunnel_manager and hasattr(self.tunnel_manager, 'stop'):
|
639
|
+
logger.info("Stopping tunnel and Jupyter...")
|
640
|
+
self.tunnel_manager.stop()
|
641
|
+
else:
|
642
|
+
# Fallback to individual process cleanup
|
643
|
+
# Stop Jupyter
|
644
|
+
if self.jupyter_proc:
|
645
|
+
logger.info("Stopping Jupyter...")
|
646
|
+
self.jupyter_proc.terminate()
|
647
|
+
try:
|
648
|
+
self.jupyter_proc.wait(timeout=5)
|
649
|
+
except subprocess.TimeoutExpired:
|
650
|
+
self.jupyter_proc.kill()
|
651
|
+
|
652
|
+
# Stop tunnel
|
653
|
+
if self.tunnel_proc:
|
654
|
+
logger.info("Stopping tunnel...")
|
655
|
+
self.tunnel_proc.terminate()
|
656
|
+
try:
|
657
|
+
self.tunnel_proc.wait(timeout=5)
|
658
|
+
except subprocess.TimeoutExpired:
|
659
|
+
self.tunnel_proc.kill()
|
668
660
|
|
669
661
|
logger.info("Cleanup complete")
|
unitlab/simple_tunnel.py
ADDED
@@ -0,0 +1,205 @@
|
|
1
|
+
#!/usr/bin/env python3
|
2
|
+
"""
|
3
|
+
Simple Cloudflare Tunnel for Jupyter - MVP Version
|
4
|
+
Uses hardcoded tunnel credentials
|
5
|
+
"""
|
6
|
+
|
7
|
+
import subprocess
|
8
|
+
from pathlib import Path
|
9
|
+
import time
|
10
|
+
|
11
|
+
|
12
|
+
class SimpleTunnel:
|
13
|
+
def __init__(self, device_id=None):
|
14
|
+
"""
|
15
|
+
Initialize SimpleTunnel with hardcoded everything
|
16
|
+
device_id: Unique device identifier for subdomain generation
|
17
|
+
"""
|
18
|
+
# Everything hardcoded for simplicity
|
19
|
+
self.tunnel_uuid = "c6caf64a-7499-4aa5-8702-0d1870388114"
|
20
|
+
self.domain = "1scan.uz"
|
21
|
+
|
22
|
+
# Generate unique subdomain from device_id
|
23
|
+
if device_id:
|
24
|
+
# Clean device_id: remove special chars, lowercase, limit length
|
25
|
+
clean_id = device_id.replace('-', '').replace('_', '').replace('.', '').lower()[:30]
|
26
|
+
self.subdomain = clean_id
|
27
|
+
else:
|
28
|
+
# Fallback to a random subdomain if no device_id
|
29
|
+
import uuid
|
30
|
+
self.subdomain = str(uuid.uuid4())[:8]
|
31
|
+
|
32
|
+
# The unique URL for this device
|
33
|
+
self.jupyter_url = "https://{}.{}".format(self.subdomain, self.domain)
|
34
|
+
|
35
|
+
self.jupyter_process = None
|
36
|
+
self.tunnel_process = None
|
37
|
+
|
38
|
+
def start_jupyter(self, port=8888):
|
39
|
+
"""Start Jupyter notebook server"""
|
40
|
+
print("🚀 Starting Jupyter on port {}...".format(port))
|
41
|
+
|
42
|
+
cmd = [
|
43
|
+
"jupyter", "notebook",
|
44
|
+
"--port", '8888',
|
45
|
+
"--no-browser",
|
46
|
+
"--ip", "0.0.0.0",
|
47
|
+
"--ServerApp.token=''",
|
48
|
+
"--ServerApp.password=''",
|
49
|
+
"--ServerApp.allow_origin='*'"
|
50
|
+
]
|
51
|
+
|
52
|
+
self.jupyter_process = subprocess.Popen(
|
53
|
+
cmd,
|
54
|
+
stdout=subprocess.PIPE,
|
55
|
+
stderr=subprocess.PIPE
|
56
|
+
)
|
57
|
+
|
58
|
+
# Wait for Jupyter to start
|
59
|
+
time.sleep(3)
|
60
|
+
|
61
|
+
print("✅ Jupyter started on port {}".format(port))
|
62
|
+
return True
|
63
|
+
|
64
|
+
def get_cloudflared_path(self):
|
65
|
+
"""Get cloudflared binary, download if needed"""
|
66
|
+
import os
|
67
|
+
import platform
|
68
|
+
|
69
|
+
# Check if cloudflared exists in system
|
70
|
+
try:
|
71
|
+
import shutil
|
72
|
+
if shutil.which("cloudflared"):
|
73
|
+
print("✅ Using system cloudflared")
|
74
|
+
return "cloudflared"
|
75
|
+
except:
|
76
|
+
pass
|
77
|
+
|
78
|
+
# Check local binary
|
79
|
+
local_bin = os.path.expanduser("~/.local/bin/cloudflared")
|
80
|
+
if os.path.exists(local_bin):
|
81
|
+
print("✅ Using existing cloudflared from ~/.local/bin")
|
82
|
+
return local_bin
|
83
|
+
|
84
|
+
# Download it
|
85
|
+
print("📦 Downloading cloudflared (this may take a moment)...")
|
86
|
+
system = platform.system().lower()
|
87
|
+
if system == "linux":
|
88
|
+
arch = "amd64" if "x86" in platform.machine() else "arm64"
|
89
|
+
url = "https://github.com/cloudflare/cloudflared/releases/latest/download/cloudflared-linux-{}".format(arch)
|
90
|
+
|
91
|
+
os.makedirs(os.path.expanduser("~/.local/bin"), exist_ok=True)
|
92
|
+
result = subprocess.run("curl -L {} -o {}".format(url, local_bin), shell=True, capture_output=True)
|
93
|
+
if result.returncode == 0:
|
94
|
+
subprocess.run("chmod +x {}".format(local_bin), shell=True)
|
95
|
+
print("✅ cloudflared downloaded successfully")
|
96
|
+
return local_bin
|
97
|
+
else:
|
98
|
+
print("❌ Failed to download cloudflared")
|
99
|
+
raise Exception("Could not download cloudflared")
|
100
|
+
|
101
|
+
raise Exception("Could not find or download cloudflared")
|
102
|
+
|
103
|
+
def start_tunnel(self, local_port=8888):
|
104
|
+
"""Start cloudflared tunnel - simple and direct"""
|
105
|
+
print("🔧 Starting Cloudflare tunnel...")
|
106
|
+
|
107
|
+
# Get cloudflared (downloads if needed)
|
108
|
+
try:
|
109
|
+
cloudflared = self.get_cloudflared_path()
|
110
|
+
except Exception as e:
|
111
|
+
print("⚠️ Error getting cloudflared: {}".format(e))
|
112
|
+
# Fallback - try to use system cloudflared anyway
|
113
|
+
cloudflared = "cloudflared"
|
114
|
+
|
115
|
+
# Simple command - just run the tunnel
|
116
|
+
cmd = [
|
117
|
+
cloudflared,
|
118
|
+
"tunnel",
|
119
|
+
"run",
|
120
|
+
"--url", "http://127.0.0.1:{}".format(local_port),
|
121
|
+
self.tunnel_uuid
|
122
|
+
]
|
123
|
+
|
124
|
+
self.tunnel_process = subprocess.Popen(
|
125
|
+
cmd,
|
126
|
+
stdout=subprocess.PIPE,
|
127
|
+
stderr=subprocess.PIPE
|
128
|
+
)
|
129
|
+
|
130
|
+
# Wait for tunnel to establish
|
131
|
+
time.sleep(3)
|
132
|
+
|
133
|
+
print("✅ Tunnel running at {}".format(self.jupyter_url))
|
134
|
+
|
135
|
+
def start(self):
|
136
|
+
"""Start tunnel and Jupyter (non-blocking)"""
|
137
|
+
try:
|
138
|
+
print("="*50)
|
139
|
+
print("🌐 Simple Cloudflare Tunnel for Jupyter - MVP")
|
140
|
+
print("="*50)
|
141
|
+
|
142
|
+
# 1. Cloudflared should be installed on system
|
143
|
+
|
144
|
+
# 2. Start Jupyter
|
145
|
+
if not self.start_jupyter():
|
146
|
+
raise Exception("Failed to start Jupyter")
|
147
|
+
|
148
|
+
# 3. Start tunnel
|
149
|
+
self.start_tunnel()
|
150
|
+
|
151
|
+
# 4. Print access info
|
152
|
+
print("\n" + "="*50)
|
153
|
+
print("🎉 SUCCESS! Your Jupyter is now accessible at:")
|
154
|
+
print(" {}".format(self.jupyter_url))
|
155
|
+
print(" Device subdomain: {}".format(self.subdomain))
|
156
|
+
print("="*50)
|
157
|
+
|
158
|
+
return True
|
159
|
+
|
160
|
+
except Exception as e:
|
161
|
+
print("❌ Error: {}".format(e))
|
162
|
+
self.stop()
|
163
|
+
return False
|
164
|
+
|
165
|
+
def stop(self):
|
166
|
+
"""Stop tunnel and Jupyter"""
|
167
|
+
if self.jupyter_process:
|
168
|
+
self.jupyter_process.terminate()
|
169
|
+
self.jupyter_process = None
|
170
|
+
if self.tunnel_process:
|
171
|
+
self.tunnel_process.terminate()
|
172
|
+
self.tunnel_process = None
|
173
|
+
|
174
|
+
def run(self):
|
175
|
+
"""Main entry point for standalone use - sets up everything and blocks"""
|
176
|
+
try:
|
177
|
+
if self.start():
|
178
|
+
# Keep running
|
179
|
+
while True:
|
180
|
+
time.sleep(1)
|
181
|
+
except KeyboardInterrupt:
|
182
|
+
print("\n⏹️ Shutting down...")
|
183
|
+
self.stop()
|
184
|
+
print("👋 Goodbye!")
|
185
|
+
|
186
|
+
|
187
|
+
def main():
|
188
|
+
"""Example usage with device ID"""
|
189
|
+
|
190
|
+
# Generate a unique device ID (in real usage, this comes from main.py)
|
191
|
+
import platform
|
192
|
+
import uuid
|
193
|
+
hostname = platform.node().replace('.', '-').replace(' ', '-')[:20]
|
194
|
+
random_suffix = str(uuid.uuid4())[:8]
|
195
|
+
device_id = "{}-{}".format(hostname, random_suffix)
|
196
|
+
|
197
|
+
print("Device ID: {}".format(device_id))
|
198
|
+
|
199
|
+
# Create tunnel with device ID for unique subdomain
|
200
|
+
tunnel = SimpleTunnel(device_id=device_id)
|
201
|
+
tunnel.run()
|
202
|
+
|
203
|
+
|
204
|
+
if __name__ == "__main__":
|
205
|
+
main()
|
@@ -1,17 +1,18 @@
|
|
1
1
|
unitlab/__init__.py,sha256=Wtk5kQ_MTlxtd3mxJIn2qHVK5URrVcasMMPjD3BtrVM,214
|
2
2
|
unitlab/__main__.py,sha256=6Hs2PV7EYc5Tid4g4OtcLXhqVHiNYTGzSBdoOnW2HXA,29
|
3
3
|
unitlab/binary_manager.py,sha256=Q1v2Odm0hk_3g7jfDUJQfkjEbUbSjtuyo2JDUyWjDrk,5468
|
4
|
-
unitlab/client.py,sha256=
|
4
|
+
unitlab/client.py,sha256=TAv9ePzs8gGAgqGXkiGCxD-cI5dEGKWnyGKAU6UiR0M,24635
|
5
5
|
unitlab/cloudflare_api_tunnel.py,sha256=XgDOQ-ISNDAJOlbKp96inGix3An_eBnAQ2pORcGBM40,14061
|
6
6
|
unitlab/cloudflare_api_tunnel_backup.py,sha256=dG5Vax0JqrF2i-zxAFB-kNGyVSFR01-ovalwuJELqpo,28489
|
7
7
|
unitlab/exceptions.py,sha256=68Tr6LreEzjQ3Vns8HAaWdtewtkNUJOvPazbf6NSnXU,950
|
8
8
|
unitlab/main.py,sha256=7gPZ_2n90sxDnq9oGZVKOkuifr-k7w2Tq3ZIldAUE8I,5877
|
9
|
+
unitlab/simple_tunnel.py,sha256=vWgVYFEbPoGCHmumujNrfBnDPuUCZgQJkVO3IvdygQA,6812
|
9
10
|
unitlab/tunnel_config.py,sha256=7CiAqasfg26YQfJYXapCBQPSoqw4jIx6yR64saybLLo,8312
|
10
11
|
unitlab/tunnel_service_token.py,sha256=ji96a4s4W2cFJrHZle0zBD85Ac_T862-gCKzBUomrxM,3125
|
11
12
|
unitlab/utils.py,sha256=83ekAxxfXecFTg76Z62BGDybC_skKJHYoLyawCD9wGM,1920
|
12
|
-
unitlab-2.3.
|
13
|
-
unitlab-2.3.
|
14
|
-
unitlab-2.3.
|
15
|
-
unitlab-2.3.
|
16
|
-
unitlab-2.3.
|
17
|
-
unitlab-2.3.
|
13
|
+
unitlab-2.3.28.dist-info/LICENSE.md,sha256=Gn7RRvByorAcAaM-WbyUpsgi5ED1-bKFFshbWfYYz2Y,1069
|
14
|
+
unitlab-2.3.28.dist-info/METADATA,sha256=Ig_y0Z7LQzzQkeE2NIKX6lq46EuKP1VVd1TqURBqfDI,844
|
15
|
+
unitlab-2.3.28.dist-info/WHEEL,sha256=iAkIy5fosb7FzIOwONchHf19Qu7_1wCWyFNR5gu9nU0,91
|
16
|
+
unitlab-2.3.28.dist-info/entry_points.txt,sha256=ig-PjKEqSCj3UTdyANgEi4tsAU84DyXdaOJ02NHX4bY,45
|
17
|
+
unitlab-2.3.28.dist-info/top_level.txt,sha256=Al4ZlTYE3fTJK2o6YLCDMH5_DjuQkffRBMxgmWbKaqQ,8
|
18
|
+
unitlab-2.3.28.dist-info/RECORD,,
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|