unitlab 2.3.29__tar.gz → 2.3.33__tar.gz
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-2.3.29/src/unitlab.egg-info → unitlab-2.3.33}/PKG-INFO +1 -1
- {unitlab-2.3.29 → unitlab-2.3.33}/setup.py +1 -1
- {unitlab-2.3.29 → unitlab-2.3.33}/src/unitlab/client.py +9 -12
- {unitlab-2.3.29 → unitlab-2.3.33}/src/unitlab/main.py +5 -16
- {unitlab-2.3.29 → unitlab-2.3.33}/src/unitlab/persistent_tunnel.py +103 -33
- {unitlab-2.3.29 → unitlab-2.3.33/src/unitlab.egg-info}/PKG-INFO +1 -1
- {unitlab-2.3.29 → unitlab-2.3.33}/LICENSE.md +0 -0
- {unitlab-2.3.29 → unitlab-2.3.33}/README.md +0 -0
- {unitlab-2.3.29 → unitlab-2.3.33}/setup.cfg +0 -0
- {unitlab-2.3.29 → unitlab-2.3.33}/src/unitlab/__init__.py +0 -0
- {unitlab-2.3.29 → unitlab-2.3.33}/src/unitlab/__main__.py +0 -0
- {unitlab-2.3.29 → unitlab-2.3.33}/src/unitlab/api_tunnel.py +0 -0
- {unitlab-2.3.29 → unitlab-2.3.33}/src/unitlab/auto_tunnel.py +0 -0
- {unitlab-2.3.29 → unitlab-2.3.33}/src/unitlab/binary_manager.py +0 -0
- {unitlab-2.3.29 → unitlab-2.3.33}/src/unitlab/cloudflare_api_tunnel.py +0 -0
- {unitlab-2.3.29 → unitlab-2.3.33}/src/unitlab/cloudflare_api_tunnel_backup.py +0 -0
- {unitlab-2.3.29 → unitlab-2.3.33}/src/unitlab/dynamic_tunnel.py +0 -0
- {unitlab-2.3.29 → unitlab-2.3.33}/src/unitlab/easy_tunnel.py +0 -0
- {unitlab-2.3.29 → unitlab-2.3.33}/src/unitlab/exceptions.py +0 -0
- {unitlab-2.3.29 → unitlab-2.3.33}/src/unitlab/simple_tunnel.py +0 -0
- {unitlab-2.3.29 → unitlab-2.3.33}/src/unitlab/tunnel_config.py +0 -0
- {unitlab-2.3.29 → unitlab-2.3.33}/src/unitlab/tunnel_service_token.py +0 -0
- {unitlab-2.3.29 → unitlab-2.3.33}/src/unitlab/utils.py +0 -0
- {unitlab-2.3.29 → unitlab-2.3.33}/src/unitlab.egg-info/SOURCES.txt +0 -0
- {unitlab-2.3.29 → unitlab-2.3.33}/src/unitlab.egg-info/dependency_links.txt +0 -0
- {unitlab-2.3.29 → unitlab-2.3.33}/src/unitlab.egg-info/entry_points.txt +0 -0
- {unitlab-2.3.29 → unitlab-2.3.33}/src/unitlab.egg-info/requires.txt +0 -0
- {unitlab-2.3.29 → unitlab-2.3.33}/src/unitlab.egg-info/top_level.txt +0 -0
@@ -286,21 +286,14 @@ class UnitlabClient:
|
|
286
286
|
from .persistent_tunnel import PersistentTunnel
|
287
287
|
logger.info("Using Persistent Tunnel with Cloudflare API")
|
288
288
|
self.tunnel_manager = PersistentTunnel(device_id=self.device_id)
|
289
|
-
#
|
290
|
-
self.jupyter_url =
|
291
|
-
self.ssh_url =
|
289
|
+
# Don't call run() here - it has infinite loop. Call start() in setup_tunnels()
|
290
|
+
self.jupyter_url = None
|
291
|
+
self.ssh_url = None
|
292
|
+
|
292
293
|
except ImportError as e:
|
293
294
|
logger.warning(f"Could not import PersistentTunnel: {e}")
|
294
295
|
# Fallback to easy tunnel
|
295
|
-
|
296
|
-
from .easy_tunnel import EasyTunnelAdapter
|
297
|
-
logger.info("Using Easy Dynamic Tunnel (random URL)")
|
298
|
-
self.tunnel_manager = EasyTunnelAdapter(device_id=self.device_id)
|
299
|
-
self.jupyter_url = None
|
300
|
-
self.ssh_url = None
|
301
|
-
except ImportError:
|
302
|
-
logger.error("No tunnel implementation available!")
|
303
|
-
raise
|
296
|
+
|
304
297
|
|
305
298
|
# Setup signal handlers
|
306
299
|
signal.signal(signal.SIGINT, self._handle_shutdown)
|
@@ -410,6 +403,10 @@ class UnitlabClient:
|
|
410
403
|
if self.tunnel_manager.start():
|
411
404
|
# Store the processes for monitoring
|
412
405
|
self.jupyter_proc = self.tunnel_manager.jupyter_process
|
406
|
+
# Update URLs after tunnel starts successfully
|
407
|
+
self.jupyter_url = self.tunnel_manager.jupyter_url
|
408
|
+
self.ssh_url = self.tunnel_manager.jupyter_url
|
409
|
+
logger.info(f"Tunnel started successfully at {self.jupyter_url}")
|
413
410
|
self.tunnel_proc = self.tunnel_manager.tunnel_process
|
414
411
|
self.jupyter_port = "8888" # Both use fixed port
|
415
412
|
|
@@ -151,22 +151,11 @@ def run_agent(
|
|
151
151
|
# Try environment variable first
|
152
152
|
device_id = os.getenv('DEVICE_ID')
|
153
153
|
if not device_id:
|
154
|
-
#
|
155
|
-
|
156
|
-
|
157
|
-
|
158
|
-
|
159
|
-
device_id = device_id_file.read_text().strip()
|
160
|
-
print(f"📌 Using saved device ID: {device_id}")
|
161
|
-
else:
|
162
|
-
# Generate a unique ID based on hostname and random UUID
|
163
|
-
hostname = platform.node().replace('.', '-').replace(' ', '-')[:20]
|
164
|
-
random_suffix = str(uuid.uuid4())[:8]
|
165
|
-
device_id = f"{hostname}-{random_suffix}"
|
166
|
-
|
167
|
-
# Save for future runs
|
168
|
-
device_id_file.write_text(device_id)
|
169
|
-
print(f"📝 Generated and saved device ID: {device_id}")
|
154
|
+
# Always generate a unique device ID (no saving/reusing)
|
155
|
+
hostname = platform.node().replace('.', '-').replace(' ', '-')[:20]
|
156
|
+
random_suffix = str(uuid.uuid4())[:8]
|
157
|
+
device_id = f"{hostname}-{random_suffix}"
|
158
|
+
print(f"📝 Generated unique device ID: {device_id}")
|
170
159
|
|
171
160
|
|
172
161
|
# Create client and initialize device agent
|
@@ -67,9 +67,38 @@ class PersistentTunnel:
|
|
67
67
|
"Content-Type": "application/json"
|
68
68
|
}
|
69
69
|
|
70
|
-
def
|
71
|
-
"""
|
72
|
-
|
70
|
+
def get_or_create_tunnel(self):
|
71
|
+
"""Get existing tunnel or create a new one"""
|
72
|
+
# First, check if tunnel already exists
|
73
|
+
print("🔍 Checking for existing tunnel: {}...".format(self.tunnel_name))
|
74
|
+
|
75
|
+
list_url = "https://api.cloudflare.com/client/v4/accounts/{}/cfd_tunnel".format(self.cf_account_id)
|
76
|
+
headers = self._get_headers()
|
77
|
+
|
78
|
+
# Check if tunnel exists
|
79
|
+
response = requests.get(list_url, headers=headers)
|
80
|
+
if response.status_code == 200:
|
81
|
+
tunnels = response.json().get("result", [])
|
82
|
+
for tunnel in tunnels:
|
83
|
+
if tunnel["name"] == self.tunnel_name:
|
84
|
+
print("✅ Found existing tunnel: {}".format(tunnel["id"]))
|
85
|
+
self.tunnel_id = tunnel["id"]
|
86
|
+
|
87
|
+
# Tunnel exists, create a new one with unique name
|
88
|
+
print("⚠️ Tunnel with this name already exists")
|
89
|
+
import uuid
|
90
|
+
unique_suffix = str(uuid.uuid4())[:8]
|
91
|
+
self.tunnel_name = "agent-{}-{}".format(self.device_id, unique_suffix)
|
92
|
+
print("🔄 Creating new tunnel with unique name: {}".format(self.tunnel_name))
|
93
|
+
# Don't break, let it continue to create new tunnel
|
94
|
+
return self.create_new_tunnel()
|
95
|
+
|
96
|
+
# Create new tunnel
|
97
|
+
return self.create_new_tunnel()
|
98
|
+
|
99
|
+
def create_new_tunnel(self):
|
100
|
+
"""Create a brand new tunnel"""
|
101
|
+
print("🔧 Creating new tunnel: {}...".format(self.tunnel_name))
|
73
102
|
|
74
103
|
# Generate random tunnel secret (32 bytes)
|
75
104
|
import secrets
|
@@ -96,8 +125,8 @@ class PersistentTunnel:
|
|
96
125
|
"TunnelID": self.tunnel_id
|
97
126
|
}
|
98
127
|
|
99
|
-
# Save credentials to file
|
100
|
-
cred_file = "/tmp/tunnel-{}.json".format(self.
|
128
|
+
# Save credentials to file with tunnel name (not ID) for consistency
|
129
|
+
cred_file = "/tmp/tunnel-{}.json".format(self.tunnel_name)
|
101
130
|
with open(cred_file, 'w') as f:
|
102
131
|
json.dump(self.tunnel_credentials, f)
|
103
132
|
|
@@ -143,19 +172,7 @@ class PersistentTunnel:
|
|
143
172
|
|
144
173
|
def create_tunnel_config(self, cred_file):
|
145
174
|
"""Create tunnel config file"""
|
146
|
-
|
147
|
-
"ingress": [
|
148
|
-
{
|
149
|
-
"hostname": "{}.{}".format(self.subdomain, self.domain),
|
150
|
-
"service": "http://localhost:8888"
|
151
|
-
},
|
152
|
-
{
|
153
|
-
"service": "http_status:404"
|
154
|
-
}
|
155
|
-
]
|
156
|
-
}
|
157
|
-
|
158
|
-
config_file = "/tmp/tunnel-config-{}.yml".format(self.tunnel_id)
|
175
|
+
config_file = "/tmp/tunnel-config-{}.yml".format(self.tunnel_name)
|
159
176
|
with open(config_file, 'w') as f:
|
160
177
|
f.write("tunnel: {}\n".format(self.tunnel_id))
|
161
178
|
f.write("credentials-file: {}\n\n".format(cred_file))
|
@@ -167,25 +184,75 @@ class PersistentTunnel:
|
|
167
184
|
return config_file
|
168
185
|
|
169
186
|
def get_cloudflared_path(self):
|
170
|
-
"""Get or download cloudflared"""
|
187
|
+
"""Get or download cloudflared for any platform"""
|
171
188
|
import shutil
|
189
|
+
import platform
|
190
|
+
|
191
|
+
# Check if already in system PATH
|
172
192
|
if shutil.which("cloudflared"):
|
173
193
|
return "cloudflared"
|
174
194
|
|
175
|
-
|
195
|
+
# Determine binary location based on OS
|
196
|
+
system = platform.system().lower()
|
197
|
+
machine = platform.machine().lower()
|
198
|
+
|
199
|
+
if system == "windows":
|
200
|
+
local_bin = os.path.expanduser("~/cloudflared/cloudflared.exe")
|
201
|
+
else:
|
202
|
+
local_bin = os.path.expanduser("~/.local/bin/cloudflared")
|
203
|
+
|
204
|
+
# Check if already downloaded
|
176
205
|
if os.path.exists(local_bin):
|
177
206
|
return local_bin
|
178
207
|
|
179
|
-
# Download
|
180
|
-
print("📦 Downloading cloudflared...")
|
181
|
-
|
182
|
-
system
|
183
|
-
|
184
|
-
|
208
|
+
# Download based on platform
|
209
|
+
print("📦 Downloading cloudflared for {}...".format(system))
|
210
|
+
|
211
|
+
if system == "linux":
|
212
|
+
# Linux: detect architecture
|
213
|
+
if "arm" in machine or "aarch64" in machine:
|
214
|
+
arch = "arm64"
|
215
|
+
elif "386" in machine or "i686" in machine:
|
216
|
+
arch = "386"
|
217
|
+
else:
|
218
|
+
arch = "amd64"
|
219
|
+
url = "https://github.com/cloudflare/cloudflared/releases/latest/download/cloudflared-linux-{}".format(arch)
|
220
|
+
|
221
|
+
os.makedirs(os.path.dirname(local_bin), exist_ok=True)
|
222
|
+
subprocess.run("curl -L {} -o {}".format(url, local_bin), shell=True, capture_output=True)
|
223
|
+
subprocess.run("chmod +x {}".format(local_bin), shell=True)
|
224
|
+
|
225
|
+
elif system == "darwin":
|
226
|
+
# macOS: supports both Intel and Apple Silicon
|
227
|
+
if "arm" in machine:
|
228
|
+
url = "https://github.com/cloudflare/cloudflared/releases/latest/download/cloudflared-darwin-arm64.tgz"
|
229
|
+
else:
|
230
|
+
url = "https://github.com/cloudflare/cloudflared/releases/latest/download/cloudflared-darwin-amd64.tgz"
|
231
|
+
|
232
|
+
os.makedirs(os.path.dirname(local_bin), exist_ok=True)
|
233
|
+
# Download and extract tar.gz
|
234
|
+
subprocess.run("curl -L {} | tar xz -C {}".format(url, os.path.dirname(local_bin)), shell=True, capture_output=True)
|
235
|
+
subprocess.run("chmod +x {}".format(local_bin), shell=True)
|
236
|
+
|
237
|
+
elif system == "windows":
|
238
|
+
# Windows: typically amd64
|
239
|
+
if "arm" in machine:
|
240
|
+
arch = "arm64"
|
241
|
+
elif "386" in machine:
|
242
|
+
arch = "386"
|
243
|
+
else:
|
244
|
+
arch = "amd64"
|
245
|
+
url = "https://github.com/cloudflare/cloudflared/releases/latest/download/cloudflared-windows-{}.exe".format(arch)
|
246
|
+
|
247
|
+
os.makedirs(os.path.dirname(local_bin), exist_ok=True)
|
248
|
+
# Use PowerShell on Windows to download
|
249
|
+
subprocess.run("powershell -Command \"Invoke-WebRequest -Uri {} -OutFile {}\"".format(url, local_bin), shell=True, capture_output=True)
|
250
|
+
|
251
|
+
else:
|
252
|
+
print("❌ Unsupported platform: {}".format(system))
|
253
|
+
raise Exception("Platform {} not supported".format(system))
|
185
254
|
|
186
|
-
|
187
|
-
subprocess.run("curl -L {} -o {}".format(url, local_bin), shell=True, capture_output=True)
|
188
|
-
subprocess.run("chmod +x {}".format(local_bin), shell=True)
|
255
|
+
print("✅ cloudflared downloaded successfully")
|
189
256
|
return local_bin
|
190
257
|
|
191
258
|
def start_jupyter(self):
|
@@ -198,7 +265,10 @@ class PersistentTunnel:
|
|
198
265
|
"--no-browser",
|
199
266
|
"--ip", "0.0.0.0",
|
200
267
|
"--NotebookApp.token=''",
|
201
|
-
"--NotebookApp.password=''"
|
268
|
+
"--NotebookApp.password=''",
|
269
|
+
"--NotebookApp.allow_origin='*'"
|
270
|
+
|
271
|
+
|
202
272
|
]
|
203
273
|
|
204
274
|
self.jupyter_process = subprocess.Popen(
|
@@ -241,8 +311,8 @@ class PersistentTunnel:
|
|
241
311
|
|
242
312
|
# API credentials are hardcoded, so we're ready to go
|
243
313
|
|
244
|
-
# 1.
|
245
|
-
cred_file = self.
|
314
|
+
# 1. Get existing or create new tunnel via API
|
315
|
+
cred_file = self.get_or_create_tunnel()
|
246
316
|
if not cred_file:
|
247
317
|
print("⚠️ Falling back to quick tunnel")
|
248
318
|
return self.start_quick_tunnel()
|
@@ -316,7 +386,7 @@ class PersistentTunnel:
|
|
316
386
|
)
|
317
387
|
requests.delete(url, headers=self._get_headers())
|
318
388
|
print("🗑️ Tunnel deleted")
|
319
|
-
except Exception
|
389
|
+
except Exception:
|
320
390
|
pass # Ignore cleanup errors
|
321
391
|
|
322
392
|
def run(self):
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|