unitlab 2.3.12__py3-none-any.whl → 2.3.14__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/cloudflare_api_tunnel.py +198 -12
- {unitlab-2.3.12.dist-info → unitlab-2.3.14.dist-info}/METADATA +1 -1
- {unitlab-2.3.12.dist-info → unitlab-2.3.14.dist-info}/RECORD +7 -7
- {unitlab-2.3.12.dist-info → unitlab-2.3.14.dist-info}/LICENSE.md +0 -0
- {unitlab-2.3.12.dist-info → unitlab-2.3.14.dist-info}/WHEEL +0 -0
- {unitlab-2.3.12.dist-info → unitlab-2.3.14.dist-info}/entry_points.txt +0 -0
- {unitlab-2.3.12.dist-info → unitlab-2.3.14.dist-info}/top_level.txt +0 -0
unitlab/cloudflare_api_tunnel.py
CHANGED
@@ -64,6 +64,7 @@ class CloudflareAPITunnel:
|
|
64
64
|
|
65
65
|
self.tunnel_process = None
|
66
66
|
self.created_dns_records = []
|
67
|
+
self.tunnel_config_file = None
|
67
68
|
|
68
69
|
# Try to initialize binary manager, but don't fail if it doesn't work
|
69
70
|
try:
|
@@ -205,6 +206,127 @@ class CloudflareAPITunnel:
|
|
205
206
|
print(" Assuming routes are configured in dashboard.")
|
206
207
|
return True
|
207
208
|
|
209
|
+
def create_device_tunnel(self):
|
210
|
+
"""
|
211
|
+
Create a unique tunnel for this device if it doesn't exist
|
212
|
+
"""
|
213
|
+
tunnel_name = f"device-{self.clean_device_id}"
|
214
|
+
print(f"🔍 Checking for tunnel: {tunnel_name}")
|
215
|
+
|
216
|
+
# Check if tunnel already exists
|
217
|
+
list_url = f"{self.api_base}/accounts/{self.account_id}/tunnels"
|
218
|
+
response = requests.get(list_url, headers=self.headers)
|
219
|
+
|
220
|
+
if response.status_code == 200:
|
221
|
+
tunnels = response.json().get('result', [])
|
222
|
+
existing_tunnel = None
|
223
|
+
|
224
|
+
for tunnel in tunnels:
|
225
|
+
if tunnel['name'] == tunnel_name:
|
226
|
+
existing_tunnel = tunnel
|
227
|
+
print(f"✅ Found existing tunnel: {tunnel_name}")
|
228
|
+
break
|
229
|
+
|
230
|
+
if not existing_tunnel:
|
231
|
+
# Create new tunnel
|
232
|
+
print(f"📦 Creating new tunnel: {tunnel_name}")
|
233
|
+
create_url = f"{self.api_base}/accounts/{self.account_id}/tunnels"
|
234
|
+
create_data = {
|
235
|
+
"name": tunnel_name,
|
236
|
+
"tunnel_secret": os.urandom(32).hex() # Generate random secret
|
237
|
+
}
|
238
|
+
|
239
|
+
create_response = requests.post(create_url, headers=self.headers, json=create_data)
|
240
|
+
|
241
|
+
if create_response.status_code in [200, 201]:
|
242
|
+
existing_tunnel = create_response.json()['result']
|
243
|
+
print(f"✅ Created tunnel: {tunnel_name}")
|
244
|
+
|
245
|
+
# Save credentials for this tunnel
|
246
|
+
self._save_tunnel_credentials(existing_tunnel)
|
247
|
+
|
248
|
+
# Configure tunnel routes
|
249
|
+
self._configure_tunnel_routes(existing_tunnel['id'])
|
250
|
+
|
251
|
+
# Create DNS records for this device
|
252
|
+
self.create_dns_records()
|
253
|
+
else:
|
254
|
+
print(f"❌ Failed to create tunnel: {create_response.text}")
|
255
|
+
return None
|
256
|
+
else:
|
257
|
+
# Tunnel exists - update config file in case settings changed
|
258
|
+
print(f"♻️ Updating configuration for existing tunnel")
|
259
|
+
self._configure_tunnel_routes(existing_tunnel['id'])
|
260
|
+
|
261
|
+
# Ensure DNS records exist
|
262
|
+
self.create_dns_records()
|
263
|
+
|
264
|
+
return existing_tunnel
|
265
|
+
|
266
|
+
return None
|
267
|
+
|
268
|
+
def _configure_tunnel_routes(self, tunnel_id):
|
269
|
+
"""
|
270
|
+
Configure ingress routes for the device tunnel
|
271
|
+
The tunnel needs to be configured with a config file, not via API
|
272
|
+
So we'll create a config file for it
|
273
|
+
"""
|
274
|
+
import yaml
|
275
|
+
|
276
|
+
# Create config file for this tunnel
|
277
|
+
config_dir = Path.home() / '.cloudflared'
|
278
|
+
config_dir.mkdir(exist_ok=True)
|
279
|
+
config_file = config_dir / f'config-{tunnel_id}.yml'
|
280
|
+
|
281
|
+
config = {
|
282
|
+
"tunnel": tunnel_id,
|
283
|
+
"credentials-file": str(config_dir / f"{tunnel_id}.json"),
|
284
|
+
"ingress": [
|
285
|
+
{
|
286
|
+
"hostname": f"{self.jupyter_subdomain}.{self.base_domain}",
|
287
|
+
"service": "http://localhost:8888",
|
288
|
+
"originRequest": {
|
289
|
+
"noTLSVerify": True
|
290
|
+
}
|
291
|
+
},
|
292
|
+
{
|
293
|
+
"hostname": f"{self.ssh_subdomain}.{self.base_domain}",
|
294
|
+
"service": "ssh://localhost:22"
|
295
|
+
},
|
296
|
+
{
|
297
|
+
"service": "http_status:404"
|
298
|
+
}
|
299
|
+
]
|
300
|
+
}
|
301
|
+
|
302
|
+
with open(config_file, 'w') as f:
|
303
|
+
yaml.dump(config, f)
|
304
|
+
|
305
|
+
print(f"✅ Created tunnel config: {config_file}")
|
306
|
+
self.tunnel_config_file = config_file
|
307
|
+
|
308
|
+
def _save_tunnel_credentials(self, tunnel_info):
|
309
|
+
"""
|
310
|
+
Save tunnel credentials locally for this device
|
311
|
+
"""
|
312
|
+
creds_dir = Path.home() / '.cloudflared'
|
313
|
+
creds_dir.mkdir(exist_ok=True)
|
314
|
+
|
315
|
+
creds_file = creds_dir / f"{tunnel_info['id']}.json"
|
316
|
+
|
317
|
+
credentials = {
|
318
|
+
"AccountTag": self.account_id,
|
319
|
+
"TunnelSecret": tunnel_info.get('tunnel_secret') or tunnel_info.get('secret'),
|
320
|
+
"TunnelID": tunnel_info['id']
|
321
|
+
}
|
322
|
+
|
323
|
+
import json
|
324
|
+
with open(creds_file, 'w') as f:
|
325
|
+
json.dump(credentials, f)
|
326
|
+
|
327
|
+
print(f"💾 Saved credentials to: {creds_file}")
|
328
|
+
return creds_file
|
329
|
+
|
208
330
|
def start_tunnel_with_token(self):
|
209
331
|
"""
|
210
332
|
Start tunnel using the existing service token
|
@@ -222,18 +344,77 @@ class CloudflareAPITunnel:
|
|
222
344
|
if not cloudflared_path:
|
223
345
|
raise RuntimeError("Failed to obtain cloudflared binary")
|
224
346
|
|
225
|
-
#
|
226
|
-
|
227
|
-
service_token = "eyJhIjoiYzkxMTkyYWUyMGE1ZDQzZjY1ZTA4NzU1MGQ4ZGM4OWIiLCJ0IjoiMDc3N2ZjMTAtNDljNC00NzJkLTg2NjEtZjYwZDgwZDYxODRkIiwicyI6Ik9XRTNaak5tTVdVdE1tWTRaUzAwTmpoakxTazBaalF0WXpjek1tSm1ZVGt4WlRRMCJ9"
|
347
|
+
# Create or get existing tunnel for this device
|
348
|
+
device_tunnel = self.create_device_tunnel()
|
228
349
|
|
229
|
-
|
230
|
-
|
231
|
-
|
232
|
-
"
|
233
|
-
"
|
234
|
-
|
235
|
-
|
236
|
-
|
350
|
+
if not device_tunnel:
|
351
|
+
print("❌ Could not create/find device tunnel")
|
352
|
+
# Fallback to shared tunnel if API fails
|
353
|
+
print("⚠️ Falling back to shared tunnel...")
|
354
|
+
service_token = "eyJhIjoiYzkxMTkyYWUyMGE1ZDQzZjY1ZTA4NzU1MGQ4ZGM4OWIiLCJ0IjoiMDc3N2ZjMTAtNDljNC00NzJkLTg2NjEtZjYwZDgwZDYxODRkIiwicyI6Ik9XRTNaak5tTVdVdE1tWTRaUzAwTmpoakLTazBaalF0WXpjek1tSm1ZVGt4WlRRMCJ9"
|
355
|
+
cmd = [
|
356
|
+
cloudflared_path,
|
357
|
+
"tunnel",
|
358
|
+
"--no-autoupdate",
|
359
|
+
"run",
|
360
|
+
"--token",
|
361
|
+
service_token
|
362
|
+
]
|
363
|
+
else:
|
364
|
+
tunnel_id = device_tunnel['id']
|
365
|
+
tunnel_name = device_tunnel['name']
|
366
|
+
|
367
|
+
print(f"🚇 Starting tunnel: {tunnel_name} ({tunnel_id})")
|
368
|
+
|
369
|
+
# Check if credentials file exists
|
370
|
+
creds_file = Path.home() / '.cloudflared' / f"{tunnel_id}.json"
|
371
|
+
|
372
|
+
if not creds_file.exists():
|
373
|
+
# Try to recreate credentials from stored secret
|
374
|
+
if device_tunnel.get('tunnel_secret'):
|
375
|
+
self._save_tunnel_credentials(device_tunnel)
|
376
|
+
else:
|
377
|
+
print("⚠️ No credentials found, requesting from API...")
|
378
|
+
# Get token for this tunnel
|
379
|
+
token_url = f"{self.api_base}/accounts/{self.account_id}/tunnels/{tunnel_id}/token"
|
380
|
+
token_response = requests.get(token_url, headers=self.headers)
|
381
|
+
if token_response.status_code == 200:
|
382
|
+
token = token_response.json()['result']
|
383
|
+
# Use token directly
|
384
|
+
cmd = [
|
385
|
+
cloudflared_path,
|
386
|
+
"tunnel",
|
387
|
+
"--no-autoupdate",
|
388
|
+
"run",
|
389
|
+
"--token",
|
390
|
+
token
|
391
|
+
]
|
392
|
+
else:
|
393
|
+
print("❌ Could not get tunnel token")
|
394
|
+
return None
|
395
|
+
|
396
|
+
if creds_file.exists():
|
397
|
+
# Check if config file exists
|
398
|
+
config_file = Path.home() / '.cloudflared' / f'config-{tunnel_id}.yml'
|
399
|
+
if config_file.exists():
|
400
|
+
# Run tunnel with config file (includes routes)
|
401
|
+
cmd = [
|
402
|
+
cloudflared_path,
|
403
|
+
"tunnel",
|
404
|
+
"--no-autoupdate",
|
405
|
+
"--config", str(config_file),
|
406
|
+
"run"
|
407
|
+
]
|
408
|
+
else:
|
409
|
+
# Fallback to credentials file only
|
410
|
+
cmd = [
|
411
|
+
cloudflared_path,
|
412
|
+
"tunnel",
|
413
|
+
"--no-autoupdate",
|
414
|
+
"--credentials-file", str(creds_file),
|
415
|
+
"run",
|
416
|
+
tunnel_id
|
417
|
+
]
|
237
418
|
|
238
419
|
self.tunnel_process = subprocess.Popen(
|
239
420
|
cmd,
|
@@ -271,12 +452,17 @@ class CloudflareAPITunnel:
|
|
271
452
|
def stop(self):
|
272
453
|
"""
|
273
454
|
Stop the tunnel if running
|
455
|
+
Note: We keep the tunnel configuration for next run
|
274
456
|
"""
|
275
457
|
if self.tunnel_process and self.tunnel_process.poll() is None:
|
276
458
|
print("Stopping tunnel...")
|
277
459
|
self.tunnel_process.terminate()
|
278
|
-
|
460
|
+
try:
|
461
|
+
self.tunnel_process.wait(timeout=5)
|
462
|
+
except subprocess.TimeoutExpired:
|
463
|
+
self.tunnel_process.kill()
|
279
464
|
print("Tunnel stopped")
|
465
|
+
print("ℹ️ Tunnel configuration preserved for next run")
|
280
466
|
|
281
467
|
def _ensure_cloudflared(self):
|
282
468
|
"""
|
@@ -2,15 +2,15 @@ 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
4
|
unitlab/client.py,sha256=V5fTgbprmMsnMwD_FPn7oZh0KK6hdnqB4BYuY4D-JRw,24558
|
5
|
-
unitlab/cloudflare_api_tunnel.py,sha256=
|
5
|
+
unitlab/cloudflare_api_tunnel.py,sha256=b3qYgI5mWuU3yiFZLNRPBk1iu27P0xq36eEqGzaeays,23309
|
6
6
|
unitlab/exceptions.py,sha256=68Tr6LreEzjQ3Vns8HAaWdtewtkNUJOvPazbf6NSnXU,950
|
7
7
|
unitlab/main.py,sha256=h1WG6up6STt-2fJZAkqnxenwJ7kmkqnkfq5Zs4xLSeI,5257
|
8
8
|
unitlab/tunnel_config.py,sha256=7CiAqasfg26YQfJYXapCBQPSoqw4jIx6yR64saybLLo,8312
|
9
9
|
unitlab/tunnel_service_token.py,sha256=ji96a4s4W2cFJrHZle0zBD85Ac_T862-gCKzBUomrxM,3125
|
10
10
|
unitlab/utils.py,sha256=83ekAxxfXecFTg76Z62BGDybC_skKJHYoLyawCD9wGM,1920
|
11
|
-
unitlab-2.3.
|
12
|
-
unitlab-2.3.
|
13
|
-
unitlab-2.3.
|
14
|
-
unitlab-2.3.
|
15
|
-
unitlab-2.3.
|
16
|
-
unitlab-2.3.
|
11
|
+
unitlab-2.3.14.dist-info/LICENSE.md,sha256=Gn7RRvByorAcAaM-WbyUpsgi5ED1-bKFFshbWfYYz2Y,1069
|
12
|
+
unitlab-2.3.14.dist-info/METADATA,sha256=xojV4DdJOJiEikB6fOw4Sr16sfqb7tsBS897suaISLU,844
|
13
|
+
unitlab-2.3.14.dist-info/WHEEL,sha256=iAkIy5fosb7FzIOwONchHf19Qu7_1wCWyFNR5gu9nU0,91
|
14
|
+
unitlab-2.3.14.dist-info/entry_points.txt,sha256=ig-PjKEqSCj3UTdyANgEi4tsAU84DyXdaOJ02NHX4bY,45
|
15
|
+
unitlab-2.3.14.dist-info/top_level.txt,sha256=Al4ZlTYE3fTJK2o6YLCDMH5_DjuQkffRBMxgmWbKaqQ,8
|
16
|
+
unitlab-2.3.14.dist-info/RECORD,,
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|