unitlab 2.3.42__tar.gz → 2.3.44__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.42/src/unitlab.egg-info → unitlab-2.3.44}/PKG-INFO +1 -1
- {unitlab-2.3.42 → unitlab-2.3.44}/setup.py +1 -1
- {unitlab-2.3.42 → unitlab-2.3.44}/src/unitlab/persistent_tunnel.py +242 -140
- {unitlab-2.3.42 → unitlab-2.3.44/src/unitlab.egg-info}/PKG-INFO +1 -1
- {unitlab-2.3.42 → unitlab-2.3.44}/LICENSE.md +0 -0
- {unitlab-2.3.42 → unitlab-2.3.44}/README.md +0 -0
- {unitlab-2.3.42 → unitlab-2.3.44}/setup.cfg +0 -0
- {unitlab-2.3.42 → unitlab-2.3.44}/src/unitlab/__init__.py +0 -0
- {unitlab-2.3.42 → unitlab-2.3.44}/src/unitlab/__main__.py +0 -0
- {unitlab-2.3.42 → unitlab-2.3.44}/src/unitlab/client.py +0 -0
- {unitlab-2.3.42 → unitlab-2.3.44}/src/unitlab/exceptions.py +0 -0
- {unitlab-2.3.42 → unitlab-2.3.44}/src/unitlab/main.py +0 -0
- {unitlab-2.3.42 → unitlab-2.3.44}/src/unitlab/utils.py +0 -0
- {unitlab-2.3.42 → unitlab-2.3.44}/src/unitlab.egg-info/SOURCES.txt +0 -0
- {unitlab-2.3.42 → unitlab-2.3.44}/src/unitlab.egg-info/dependency_links.txt +0 -0
- {unitlab-2.3.42 → unitlab-2.3.44}/src/unitlab.egg-info/entry_points.txt +0 -0
- {unitlab-2.3.42 → unitlab-2.3.44}/src/unitlab.egg-info/requires.txt +0 -0
- {unitlab-2.3.42 → unitlab-2.3.44}/src/unitlab.egg-info/top_level.txt +0 -0
@@ -38,25 +38,37 @@ class PersistentTunnel:
|
|
38
38
|
|
39
39
|
# Clean device ID for subdomain
|
40
40
|
if device_id:
|
41
|
-
self.device_id = device_id.replace('_', '').replace('.', '').lower()[:
|
41
|
+
self.device_id = device_id.replace('-', '').replace('_', '').replace('.', '').lower()[:20]
|
42
42
|
else:
|
43
43
|
import uuid
|
44
44
|
self.device_id = str(uuid.uuid4())[:8]
|
45
45
|
|
46
|
-
|
46
|
+
# Main tunnel for Jupyter/API
|
47
|
+
self.main_tunnel_name = "agent-{}".format(self.device_id)
|
48
|
+
self.main_tunnel_id = None
|
49
|
+
self.main_tunnel_process = None
|
50
|
+
self.main_tunnel_credentials = None
|
51
|
+
|
52
|
+
# SSH tunnel
|
53
|
+
self.ssh_tunnel_name = "ssh-{}".format(self.device_id)
|
54
|
+
self.ssh_tunnel_id = None
|
55
|
+
self.ssh_tunnel_process = None
|
56
|
+
self.ssh_tunnel_credentials = None
|
57
|
+
|
58
|
+
# URLs
|
47
59
|
self.subdomain = self.device_id
|
48
60
|
self.domain = "unitlab-ai.com"
|
49
61
|
self.jupyter_url = "https://{}.{}".format(self.subdomain, self.domain)
|
50
62
|
self.api_expose_url = "https://{}.{}/api-agent/".format(self.subdomain, self.domain)
|
51
|
-
self.ssh_subdomain = "
|
52
|
-
self.ssh_url = "{}.{}".format(self.ssh_subdomain, self.domain)
|
63
|
+
self.ssh_subdomain = "ssh{}".format(self.device_id)
|
64
|
+
self.ssh_url = "{}.{}".format(self.ssh_subdomain, self.domain)
|
53
65
|
|
54
|
-
self.tunnel_id = None
|
55
|
-
self.tunnel_credentials = None
|
56
66
|
self.jupyter_process = None
|
57
|
-
self.tunnel_process = None
|
58
|
-
|
59
67
|
|
68
|
+
@property
|
69
|
+
def tunnel_process(self):
|
70
|
+
"""Compatibility property for backward compatibility"""
|
71
|
+
return self.main_tunnel_process
|
60
72
|
|
61
73
|
def _get_headers(self):
|
62
74
|
"""Get API headers for Global API Key"""
|
@@ -79,9 +91,49 @@ class PersistentTunnel:
|
|
79
91
|
# return self.create_new_tunnel()
|
80
92
|
|
81
93
|
|
82
|
-
def
|
83
|
-
"""Create
|
84
|
-
print("🔧 Creating
|
94
|
+
def create_main_tunnel(self):
|
95
|
+
"""Create main tunnel for Jupyter/API"""
|
96
|
+
print("🔧 Creating main tunnel: {}...".format(self.main_tunnel_name))
|
97
|
+
|
98
|
+
# Generate random tunnel secret (32 bytes)
|
99
|
+
tunnel_secret = base64.b64encode(secrets.token_bytes(32)).decode()
|
100
|
+
|
101
|
+
url = "https://api.cloudflare.com/client/v4/accounts/{}/cfd_tunnel".format(self.cf_account_id)
|
102
|
+
headers = self._get_headers()
|
103
|
+
|
104
|
+
data = {
|
105
|
+
"name": self.main_tunnel_name,
|
106
|
+
"tunnel_secret": tunnel_secret
|
107
|
+
}
|
108
|
+
|
109
|
+
response = requests.post(url, headers=headers, json=data)
|
110
|
+
|
111
|
+
if response.status_code in [200, 201]:
|
112
|
+
result = response.json()["result"]
|
113
|
+
self.main_tunnel_id = result["id"]
|
114
|
+
|
115
|
+
# Create credentials JSON
|
116
|
+
self.main_tunnel_credentials = {
|
117
|
+
"AccountTag": self.cf_account_id,
|
118
|
+
"TunnelSecret": tunnel_secret,
|
119
|
+
"TunnelID": self.main_tunnel_id
|
120
|
+
}
|
121
|
+
|
122
|
+
# Save credentials to file
|
123
|
+
cred_file = "/tmp/tunnel-{}.json".format(self.main_tunnel_name)
|
124
|
+
with open(cred_file, 'w') as f:
|
125
|
+
json.dump(self.main_tunnel_credentials, f)
|
126
|
+
|
127
|
+
print("✅ Main tunnel created: {}".format(self.main_tunnel_id))
|
128
|
+
print("✅ Credentials saved to: {}".format(cred_file))
|
129
|
+
return cred_file
|
130
|
+
else:
|
131
|
+
print("❌ Failed to create main tunnel: {}".format(response.text))
|
132
|
+
return None
|
133
|
+
|
134
|
+
def create_ssh_tunnel(self):
|
135
|
+
"""Create SSH tunnel"""
|
136
|
+
print("🔧 Creating SSH tunnel: {}...".format(self.ssh_tunnel_name))
|
85
137
|
|
86
138
|
# Generate random tunnel secret (32 bytes)
|
87
139
|
tunnel_secret = base64.b64encode(secrets.token_bytes(32)).decode()
|
@@ -90,7 +142,7 @@ class PersistentTunnel:
|
|
90
142
|
headers = self._get_headers()
|
91
143
|
|
92
144
|
data = {
|
93
|
-
"name": self.
|
145
|
+
"name": self.ssh_tunnel_name,
|
94
146
|
"tunnel_secret": tunnel_secret
|
95
147
|
}
|
96
148
|
|
@@ -98,36 +150,34 @@ class PersistentTunnel:
|
|
98
150
|
|
99
151
|
if response.status_code in [200, 201]:
|
100
152
|
result = response.json()["result"]
|
101
|
-
self.
|
153
|
+
self.ssh_tunnel_id = result["id"]
|
102
154
|
|
103
155
|
# Create credentials JSON
|
104
|
-
self.
|
156
|
+
self.ssh_tunnel_credentials = {
|
105
157
|
"AccountTag": self.cf_account_id,
|
106
158
|
"TunnelSecret": tunnel_secret,
|
107
|
-
"TunnelID": self.
|
159
|
+
"TunnelID": self.ssh_tunnel_id
|
108
160
|
}
|
109
161
|
|
110
162
|
# Save credentials to file
|
111
|
-
cred_file = "/tmp/tunnel-{}.json".format(self.
|
163
|
+
cred_file = "/tmp/tunnel-{}.json".format(self.ssh_tunnel_name)
|
112
164
|
with open(cred_file, 'w') as f:
|
113
|
-
json.dump(self.
|
165
|
+
json.dump(self.ssh_tunnel_credentials, f)
|
114
166
|
|
115
|
-
print("✅
|
167
|
+
print("✅ SSH tunnel created: {}".format(self.ssh_tunnel_id))
|
116
168
|
print("✅ Credentials saved to: {}".format(cred_file))
|
117
169
|
return cred_file
|
118
170
|
else:
|
119
|
-
print("❌ Failed to create tunnel: {}".format(response.text))
|
171
|
+
print("❌ Failed to create SSH tunnel: {}".format(response.text))
|
120
172
|
return None
|
121
173
|
|
122
|
-
def
|
123
|
-
"""Create DNS CNAME records for
|
124
|
-
if not self.
|
174
|
+
def create_dns_records(self):
|
175
|
+
"""Create DNS CNAME records for both tunnels"""
|
176
|
+
if not self.main_tunnel_id or not self.ssh_tunnel_id:
|
125
177
|
return False
|
126
178
|
|
127
179
|
print("🔧 Creating DNS records...")
|
128
180
|
|
129
|
-
# self.get_zone_id()
|
130
|
-
|
131
181
|
url = "https://api.cloudflare.com/client/v4/zones/{}/dns_records".format(self.cf_zone_id)
|
132
182
|
headers = self._get_headers()
|
133
183
|
|
@@ -135,7 +185,7 @@ class PersistentTunnel:
|
|
135
185
|
data = {
|
136
186
|
"type": "CNAME",
|
137
187
|
"name": self.subdomain,
|
138
|
-
"content": "{}.cfargotunnel.com".format(self.
|
188
|
+
"content": "{}.cfargotunnel.com".format(self.main_tunnel_id),
|
139
189
|
"proxied": True,
|
140
190
|
"ttl": 1
|
141
191
|
}
|
@@ -150,58 +200,31 @@ class PersistentTunnel:
|
|
150
200
|
print("❌ Failed to create main DNS: {}".format(response.text[:200]))
|
151
201
|
return False
|
152
202
|
|
153
|
-
#
|
154
|
-
# print("🔍 Checking for existing SSH DNS record: {}.{}".format(self.ssh_subdomain, self.domain))
|
155
|
-
# list_url = "{}?name={}.{}".format(url, self.ssh_subdomain, self.domain)
|
156
|
-
# list_response = requests.get(list_url, headers=headers)
|
157
|
-
|
158
|
-
# if list_response.status_code == 200:
|
159
|
-
# records = list_response.json().get("result", [])
|
160
|
-
# print("Found {} existing DNS records".format(len(records)))
|
161
|
-
# print('this is new version')
|
162
|
-
# for record in records:
|
163
|
-
# if record["name"] == "{}.{}".format(self.ssh_subdomain, self.domain):
|
164
|
-
# record_id = record["id"]
|
165
|
-
# print("🗑️ Deleting old SSH DNS record: {}".format(record_id))
|
166
|
-
# delete_url = "{}/{}".format(url, record_id)
|
167
|
-
# delete_response = requests.delete(delete_url, headers=headers)
|
168
|
-
# if delete_response.status_code in [200, 204]:
|
169
|
-
# print("✅ Deleted old SSH DNS record")
|
170
|
-
# else:
|
171
|
-
# print("⚠️ Could not delete old SSH DNS record: {}".format(delete_response.text[:200]))
|
172
|
-
# else:
|
173
|
-
# print("⚠️ Could not list DNS records: {}".format(list_response.text[:200]))
|
174
|
-
|
175
|
-
# Wait a moment for DNS deletion to propagate
|
203
|
+
# Wait a moment for DNS propagation
|
176
204
|
time.sleep(2)
|
177
205
|
|
178
|
-
# Create
|
206
|
+
# Create SSH subdomain record pointing to SSH tunnel
|
179
207
|
ssh_data = {
|
180
208
|
"type": "CNAME",
|
181
209
|
"name": self.ssh_subdomain,
|
182
|
-
"content": "{}.cfargotunnel.com".format(self.
|
210
|
+
"content": "{}.cfargotunnel.com".format(self.ssh_tunnel_id),
|
183
211
|
"proxied": True,
|
184
212
|
"ttl": 1
|
185
213
|
}
|
186
214
|
|
187
|
-
print("📝 Creating SSH DNS record: {} -> {}".format(self.ssh_subdomain, self.
|
215
|
+
print("📝 Creating SSH DNS record: {} -> {}".format(self.ssh_subdomain, self.ssh_tunnel_id))
|
188
216
|
ssh_response = requests.post(url, headers=headers, json=ssh_data)
|
189
217
|
|
190
218
|
if ssh_response.status_code in [200, 201]:
|
191
219
|
print("✅ SSH DNS record created: {}.{}".format(self.ssh_subdomain, self.domain))
|
192
|
-
|
220
|
+
return True
|
221
|
+
elif "already exists" in ssh_response.text:
|
222
|
+
print("⚠️ SSH DNS record already exists")
|
223
|
+
return True
|
193
224
|
else:
|
194
|
-
print("❌ Failed to create SSH DNS:
|
195
|
-
|
196
|
-
|
197
|
-
error_data = ssh_response.json()
|
198
|
-
if "errors" in error_data:
|
199
|
-
for error in error_data["errors"]:
|
200
|
-
print(" Error: {}".format(error.get("message", error)))
|
201
|
-
except:
|
202
|
-
pass
|
203
|
-
|
204
|
-
return True
|
225
|
+
print("❌ Failed to create SSH DNS: {}".format(ssh_response.text[:200]))
|
226
|
+
return False
|
227
|
+
|
205
228
|
|
206
229
|
def create_access_application(self):
|
207
230
|
"""Create Cloudflare Access application for SSH with bypass policy"""
|
@@ -255,34 +278,45 @@ class PersistentTunnel:
|
|
255
278
|
print("⚠️ Could not create Access application: {}".format(app_response.text[:200]))
|
256
279
|
return False
|
257
280
|
|
258
|
-
def
|
259
|
-
"""Create tunnel config file"""
|
260
|
-
config_file = "/tmp/tunnel-config-{}.yml".format(self.
|
281
|
+
def create_main_tunnel_config(self, cred_file):
|
282
|
+
"""Create main tunnel config file for Jupyter/API"""
|
283
|
+
config_file = "/tmp/tunnel-config-{}.yml".format(self.main_tunnel_name)
|
261
284
|
with open(config_file, 'w') as f:
|
262
|
-
f.write("tunnel: {}\n".format(self.
|
285
|
+
f.write("tunnel: {}\n".format(self.main_tunnel_id))
|
263
286
|
f.write("credentials-file: {}\n\n".format(cred_file))
|
264
287
|
f.write("ingress:\n")
|
265
288
|
|
266
|
-
# SSH service on dedicated subdomain (s{deviceid}.unitlab-ai.com)
|
267
|
-
f.write(" - hostname: {}.{}\n".format(self.ssh_subdomain, self.domain))
|
268
|
-
f.write(" service: ssh://localhost:22\n")
|
269
|
-
|
270
|
-
|
271
|
-
|
272
|
-
|
273
289
|
# API (more specific path goes first)
|
274
290
|
f.write(" - hostname: {}.{}\n".format(self.subdomain, self.domain))
|
275
|
-
f.write(" path: /api-agent
|
291
|
+
f.write(" path: /api-agent\n")
|
276
292
|
f.write(" service: http://localhost:8001\n")
|
277
293
|
|
278
|
-
|
279
|
-
|
280
294
|
# Jupyter (general hostname for HTTP)
|
281
295
|
f.write(" - hostname: {}.{}\n".format(self.subdomain, self.domain))
|
282
296
|
f.write(" service: http://localhost:8888\n")
|
283
297
|
|
284
298
|
# Catch-all 404 (MUST be last!)
|
285
299
|
f.write(" - service: http_status:404\n")
|
300
|
+
|
301
|
+
print("✅ Main tunnel config created: {}".format(config_file))
|
302
|
+
return config_file
|
303
|
+
|
304
|
+
def create_ssh_tunnel_config(self, cred_file):
|
305
|
+
"""Create SSH tunnel config file"""
|
306
|
+
config_file = "/tmp/tunnel-config-{}.yml".format(self.ssh_tunnel_name)
|
307
|
+
with open(config_file, 'w') as f:
|
308
|
+
f.write("tunnel: {}\n".format(self.ssh_tunnel_id))
|
309
|
+
f.write("credentials-file: {}\n\n".format(cred_file))
|
310
|
+
f.write("ingress:\n")
|
311
|
+
|
312
|
+
# SSH service
|
313
|
+
f.write(" - hostname: {}.{}\n".format(self.ssh_subdomain, self.domain))
|
314
|
+
f.write(" service: ssh://localhost:22\n")
|
315
|
+
|
316
|
+
# Catch-all 404 (MUST be last!)
|
317
|
+
f.write(" - service: http_status:404\n")
|
318
|
+
|
319
|
+
print("✅ SSH tunnel config created: {}".format(config_file))
|
286
320
|
return config_file
|
287
321
|
|
288
322
|
|
@@ -357,10 +391,7 @@ class PersistentTunnel:
|
|
357
391
|
|
358
392
|
print("✅ cloudflared downloaded successfully")
|
359
393
|
return local_bin
|
360
|
-
|
361
|
-
|
362
|
-
|
363
|
-
|
394
|
+
|
364
395
|
def start_jupyter(self):
|
365
396
|
"""Start Jupyter"""
|
366
397
|
print("🚀 Starting Jupyter...")
|
@@ -373,8 +404,6 @@ class PersistentTunnel:
|
|
373
404
|
"--NotebookApp.token=''",
|
374
405
|
"--NotebookApp.password=''",
|
375
406
|
"--NotebookApp.allow_origin='*'"
|
376
|
-
|
377
|
-
|
378
407
|
]
|
379
408
|
|
380
409
|
self.jupyter_process = subprocess.Popen(
|
@@ -394,77 +423,126 @@ class PersistentTunnel:
|
|
394
423
|
|
395
424
|
api_thread = threading.Thread(target=run_api, daemon=True)
|
396
425
|
api_thread.start()
|
397
|
-
print('API
|
426
|
+
print('✅ API started')
|
398
427
|
|
399
|
-
def
|
400
|
-
"""Start tunnel
|
401
|
-
print("🔧 Starting tunnel...")
|
402
|
-
|
403
|
-
cloudflared = self.get_cloudflared_path()
|
404
|
-
|
405
|
-
cmd = [
|
406
|
-
cloudflared,
|
407
|
-
"tunnel",
|
408
|
-
"--config", config_file,
|
409
|
-
"run"
|
410
|
-
]
|
411
|
-
|
412
|
-
self.
|
413
|
-
cmd, stdout=subprocess.PIPE, stderr=subprocess.PIPE
|
414
|
-
)
|
415
|
-
|
416
|
-
time.sleep(2)
|
417
|
-
|
418
|
-
# Check if process is still running
|
419
|
-
if self.
|
420
|
-
print("❌
|
421
|
-
# Try to read error output
|
422
|
-
try:
|
423
|
-
stdout, stderr = self.
|
424
|
-
if stderr:
|
425
|
-
print(f"Error: {stderr.decode()}")
|
426
|
-
if stdout:
|
427
|
-
print(f"Output: {stdout.decode()}")
|
428
|
-
except:
|
429
|
-
pass
|
430
|
-
return False
|
431
|
-
|
432
|
-
print("✅
|
428
|
+
def start_main_tunnel(self, config_file):
|
429
|
+
"""Start main tunnel for Jupyter/API"""
|
430
|
+
print("🔧 Starting main tunnel...")
|
431
|
+
|
432
|
+
cloudflared = self.get_cloudflared_path()
|
433
|
+
|
434
|
+
cmd = [
|
435
|
+
cloudflared,
|
436
|
+
"tunnel",
|
437
|
+
"--config", config_file,
|
438
|
+
"run"
|
439
|
+
]
|
440
|
+
|
441
|
+
self.main_tunnel_process = subprocess.Popen(
|
442
|
+
cmd, stdout=subprocess.PIPE, stderr=subprocess.PIPE
|
443
|
+
)
|
444
|
+
|
445
|
+
time.sleep(2)
|
446
|
+
|
447
|
+
# Check if process is still running
|
448
|
+
if self.main_tunnel_process.poll() is not None:
|
449
|
+
print("❌ Main tunnel process died!")
|
450
|
+
# Try to read error output
|
451
|
+
try:
|
452
|
+
stdout, stderr = self.main_tunnel_process.communicate(timeout=1)
|
453
|
+
if stderr:
|
454
|
+
print(f"Error: {stderr.decode()}")
|
455
|
+
if stdout:
|
456
|
+
print(f"Output: {stdout.decode()}")
|
457
|
+
except:
|
458
|
+
pass
|
459
|
+
return False
|
460
|
+
|
461
|
+
print("✅ Main tunnel running at {}".format(self.jupyter_url))
|
433
462
|
print("✅ API running at {}".format(self.api_expose_url))
|
434
|
-
|
463
|
+
return True
|
464
|
+
|
465
|
+
def start_ssh_tunnel(self, config_file):
|
466
|
+
"""Start SSH tunnel"""
|
467
|
+
print("🔧 Starting SSH tunnel...")
|
468
|
+
|
469
|
+
cloudflared = self.get_cloudflared_path()
|
470
|
+
|
471
|
+
cmd = [
|
472
|
+
cloudflared,
|
473
|
+
"tunnel",
|
474
|
+
"--config", config_file,
|
475
|
+
"run"
|
476
|
+
]
|
477
|
+
|
478
|
+
self.ssh_tunnel_process = subprocess.Popen(
|
479
|
+
cmd, stdout=subprocess.PIPE, stderr=subprocess.PIPE
|
480
|
+
)
|
481
|
+
|
482
|
+
time.sleep(2)
|
483
|
+
|
484
|
+
# Check if process is still running
|
485
|
+
if self.ssh_tunnel_process.poll() is not None:
|
486
|
+
print("❌ SSH tunnel process died!")
|
487
|
+
# Try to read error output
|
488
|
+
try:
|
489
|
+
stdout, stderr = self.ssh_tunnel_process.communicate(timeout=1)
|
490
|
+
if stderr:
|
491
|
+
print(f"Error: {stderr.decode()}")
|
492
|
+
if stdout:
|
493
|
+
print(f"Output: {stdout.decode()}")
|
494
|
+
except:
|
495
|
+
pass
|
496
|
+
return False
|
497
|
+
|
498
|
+
print("✅ SSH tunnel running at {}".format(self.ssh_url))
|
435
499
|
return True
|
436
500
|
|
437
501
|
def start(self):
|
438
502
|
"""Main entry point"""
|
439
503
|
try:
|
440
504
|
print("="*50)
|
441
|
-
print("🌐 Persistent Tunnel with
|
505
|
+
print("🌐 Persistent Tunnel with Separate SSH")
|
442
506
|
print("Device: {}".format(self.device_id))
|
443
|
-
print("
|
507
|
+
print("Main: {}.{}".format(self.subdomain, self.domain))
|
508
|
+
print("SSH: {}.{}".format(self.ssh_subdomain, self.domain))
|
444
509
|
print("="*50)
|
445
510
|
|
446
|
-
#
|
511
|
+
# 1. Create main tunnel for Jupyter/API
|
512
|
+
main_cred_file = self.create_main_tunnel()
|
513
|
+
if not main_cred_file:
|
514
|
+
print("❌ Failed to create main tunnel")
|
515
|
+
return False
|
447
516
|
|
448
|
-
#
|
449
|
-
|
450
|
-
|
517
|
+
# 2. Create SSH tunnel
|
518
|
+
ssh_cred_file = self.create_ssh_tunnel()
|
519
|
+
if not ssh_cred_file:
|
520
|
+
print("❌ Failed to create SSH tunnel")
|
521
|
+
return False
|
451
522
|
|
452
|
-
#
|
453
|
-
self.
|
523
|
+
# 3. Create DNS records for both tunnels
|
524
|
+
self.create_dns_records()
|
454
525
|
|
455
|
-
#
|
526
|
+
# 4. Create Access application for SSH
|
456
527
|
self.create_access_application()
|
457
528
|
|
458
|
-
#
|
459
|
-
|
529
|
+
# 5. Create config files
|
530
|
+
main_config_file = self.create_main_tunnel_config(main_cred_file)
|
531
|
+
ssh_config_file = self.create_ssh_tunnel_config(ssh_cred_file)
|
460
532
|
|
461
|
-
#
|
533
|
+
# 6. Start services (Jupyter and API)
|
462
534
|
self.start_jupyter()
|
463
535
|
self.start_api()
|
464
|
-
|
536
|
+
|
537
|
+
# 7. Start both tunnels
|
538
|
+
if not self.start_main_tunnel(main_config_file):
|
539
|
+
return False
|
540
|
+
|
541
|
+
if not self.start_ssh_tunnel(ssh_config_file):
|
542
|
+
return False
|
465
543
|
|
466
544
|
print("\n" + "="*50)
|
467
|
-
print("🎉 SUCCESS!
|
545
|
+
print("🎉 SUCCESS! All services running:")
|
468
546
|
print("📔 Jupyter: {}".format(self.jupyter_url))
|
469
547
|
print("🔧 API: {}".format(self.api_expose_url))
|
470
548
|
print("🔐 SSH: {}".format(self.ssh_url))
|
@@ -475,7 +553,8 @@ class PersistentTunnel:
|
|
475
553
|
print("ssh -o ProxyCommand='cloudflared access ssh --hostname {}' {}@{}".format(
|
476
554
|
self.ssh_url, current_user, self.ssh_url))
|
477
555
|
print("")
|
478
|
-
print("Tunnel ID: {}".format(self.
|
556
|
+
print("Main Tunnel ID: {}".format(self.main_tunnel_id))
|
557
|
+
print("SSH Tunnel ID: {}".format(self.ssh_tunnel_id))
|
479
558
|
print("="*50)
|
480
559
|
|
481
560
|
|
@@ -494,14 +573,30 @@ class PersistentTunnel:
|
|
494
573
|
"""Stop everything"""
|
495
574
|
if self.jupyter_process:
|
496
575
|
self.jupyter_process.terminate()
|
497
|
-
if self.tunnel_process:
|
498
|
-
self.tunnel_process.terminate()
|
499
576
|
try:
|
500
|
-
self.
|
577
|
+
self.jupyter_process.wait(timeout=5)
|
578
|
+
except subprocess.TimeoutExpired:
|
579
|
+
self.jupyter_process.kill()
|
580
|
+
self.jupyter_process.wait()
|
581
|
+
print("✅ Jupyter stopped")
|
582
|
+
|
583
|
+
if self.main_tunnel_process:
|
584
|
+
self.main_tunnel_process.terminate()
|
585
|
+
try:
|
586
|
+
self.main_tunnel_process.wait(timeout=5)
|
587
|
+
except subprocess.TimeoutExpired:
|
588
|
+
self.main_tunnel_process.kill()
|
589
|
+
self.main_tunnel_process.wait()
|
590
|
+
print("✅ Main tunnel stopped")
|
591
|
+
|
592
|
+
if self.ssh_tunnel_process:
|
593
|
+
self.ssh_tunnel_process.terminate()
|
594
|
+
try:
|
595
|
+
self.ssh_tunnel_process.wait(timeout=5)
|
501
596
|
except subprocess.TimeoutExpired:
|
502
|
-
self.
|
503
|
-
self.
|
504
|
-
print("✅
|
597
|
+
self.ssh_tunnel_process.kill()
|
598
|
+
self.ssh_tunnel_process.wait()
|
599
|
+
print("✅ SSH tunnel stopped")
|
505
600
|
|
506
601
|
# # Optionally delete tunnel when stopping
|
507
602
|
# if self.tunnel_id:
|
@@ -521,6 +616,13 @@ class PersistentTunnel:
|
|
521
616
|
print("\nPress Ctrl+C to stop...")
|
522
617
|
while True:
|
523
618
|
time.sleep(1)
|
619
|
+
# Check if processes are still running
|
620
|
+
if self.main_tunnel_process and self.main_tunnel_process.poll() is not None:
|
621
|
+
print("❌ Main tunnel process died!")
|
622
|
+
break
|
623
|
+
if self.ssh_tunnel_process and self.ssh_tunnel_process.poll() is not None:
|
624
|
+
print("❌ SSH tunnel process died!")
|
625
|
+
break
|
524
626
|
except KeyboardInterrupt:
|
525
627
|
print("\n⏹️ Shutting down...")
|
526
628
|
self.stop()
|
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
|