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.
@@ -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
- # Use service token - simple and reliable
226
- # The dashboard must have *.1scan.uz -> localhost:8888 configured
227
- service_token = "eyJhIjoiYzkxMTkyYWUyMGE1ZDQzZjY1ZTA4NzU1MGQ4ZGM4OWIiLCJ0IjoiMDc3N2ZjMTAtNDljNC00NzJkLTg2NjEtZjYwZDgwZDYxODRkIiwicyI6Ik9XRTNaak5tTVdVdE1tWTRaUzAwTmpoakxTazBaalF0WXpjek1tSm1ZVGt4WlRRMCJ9"
347
+ # Create or get existing tunnel for this device
348
+ device_tunnel = self.create_device_tunnel()
228
349
 
229
- cmd = [
230
- cloudflared_path,
231
- "tunnel",
232
- "--no-autoupdate",
233
- "run",
234
- "--token",
235
- service_token
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
- self.tunnel_process.wait(timeout=5)
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
  """
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.1
2
2
  Name: unitlab
3
- Version: 2.3.12
3
+ Version: 2.3.14
4
4
  Home-page: https://github.com/teamunitlab/unitlab-sdk
5
5
  Author: Unitlab Inc.
6
6
  Author-email: team@unitlab.ai
@@ -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=d95e4c4SvjpCtnQz_aGTvP-ma9fkxsbvV-oIq3aWxIk,15533
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.dist-info/LICENSE.md,sha256=Gn7RRvByorAcAaM-WbyUpsgi5ED1-bKFFshbWfYYz2Y,1069
12
- unitlab-2.3.12.dist-info/METADATA,sha256=bMIkAY0bQjPXwYU6D1Rlrl47DoqwPZqW78OJP5NXIPA,844
13
- unitlab-2.3.12.dist-info/WHEEL,sha256=iAkIy5fosb7FzIOwONchHf19Qu7_1wCWyFNR5gu9nU0,91
14
- unitlab-2.3.12.dist-info/entry_points.txt,sha256=ig-PjKEqSCj3UTdyANgEi4tsAU84DyXdaOJ02NHX4bY,45
15
- unitlab-2.3.12.dist-info/top_level.txt,sha256=Al4ZlTYE3fTJK2o6YLCDMH5_DjuQkffRBMxgmWbKaqQ,8
16
- unitlab-2.3.12.dist-info/RECORD,,
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,,