unitlab 2.3.3__tar.gz → 2.3.4__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.3/src/unitlab.egg-info → unitlab-2.3.4}/PKG-INFO +2 -1
- {unitlab-2.3.3 → unitlab-2.3.4}/setup.py +3 -1
- unitlab-2.3.4/src/unitlab/tunnel_config.py +199 -0
- unitlab-2.3.4/src/unitlab/tunnel_service_token.py +104 -0
- {unitlab-2.3.3 → unitlab-2.3.4/src/unitlab.egg-info}/PKG-INFO +2 -1
- {unitlab-2.3.3 → unitlab-2.3.4}/src/unitlab.egg-info/SOURCES.txt +1 -0
- {unitlab-2.3.3 → unitlab-2.3.4}/src/unitlab.egg-info/requires.txt +1 -0
- unitlab-2.3.3/src/unitlab/tunnel_config.py +0 -238
- {unitlab-2.3.3 → unitlab-2.3.4}/LICENSE.md +0 -0
- {unitlab-2.3.3 → unitlab-2.3.4}/README.md +0 -0
- {unitlab-2.3.3 → unitlab-2.3.4}/setup.cfg +0 -0
- {unitlab-2.3.3 → unitlab-2.3.4}/src/unitlab/__init__.py +0 -0
- {unitlab-2.3.3 → unitlab-2.3.4}/src/unitlab/__main__.py +0 -0
- {unitlab-2.3.3 → unitlab-2.3.4}/src/unitlab/client.py +0 -0
- {unitlab-2.3.3 → unitlab-2.3.4}/src/unitlab/exceptions.py +0 -0
- {unitlab-2.3.3 → unitlab-2.3.4}/src/unitlab/main.py +0 -0
- {unitlab-2.3.3 → unitlab-2.3.4}/src/unitlab/utils.py +0 -0
- {unitlab-2.3.3 → unitlab-2.3.4}/src/unitlab.egg-info/dependency_links.txt +0 -0
- {unitlab-2.3.3 → unitlab-2.3.4}/src/unitlab.egg-info/entry_points.txt +0 -0
- {unitlab-2.3.3 → unitlab-2.3.4}/src/unitlab.egg-info/top_level.txt +0 -0
@@ -1,6 +1,6 @@
|
|
1
1
|
Metadata-Version: 2.1
|
2
2
|
Name: unitlab
|
3
|
-
Version: 2.3.
|
3
|
+
Version: 2.3.4
|
4
4
|
Home-page: https://github.com/teamunitlab/unitlab-sdk
|
5
5
|
Author: Unitlab Inc.
|
6
6
|
Author-email: team@unitlab.ai
|
@@ -23,3 +23,4 @@ Requires-Dist: typer
|
|
23
23
|
Requires-Dist: validators
|
24
24
|
Requires-Dist: psutil
|
25
25
|
Requires-Dist: pyyaml
|
26
|
+
Requires-Dist: jupyter
|
@@ -2,7 +2,7 @@ from setuptools import find_packages, setup
|
|
2
2
|
|
3
3
|
setup(
|
4
4
|
name="unitlab",
|
5
|
-
version="2.3.
|
5
|
+
version="2.3.4",
|
6
6
|
license="MIT",
|
7
7
|
author="Unitlab Inc.",
|
8
8
|
author_email="team@unitlab.ai",
|
@@ -31,6 +31,8 @@ setup(
|
|
31
31
|
"validators",
|
32
32
|
'psutil',
|
33
33
|
'pyyaml',
|
34
|
+
'jupyter',
|
35
|
+
|
34
36
|
],
|
35
37
|
entry_points={
|
36
38
|
"console_scripts": ["unitlab=unitlab.main:app"],
|
@@ -0,0 +1,199 @@
|
|
1
|
+
"""
|
2
|
+
Cloudflare Tunnel Configuration using Service Token
|
3
|
+
No user login required - uses pre-generated service token
|
4
|
+
"""
|
5
|
+
|
6
|
+
import os
|
7
|
+
import subprocess
|
8
|
+
import socket
|
9
|
+
import time
|
10
|
+
import logging
|
11
|
+
|
12
|
+
logger = logging.getLogger(__name__)
|
13
|
+
|
14
|
+
|
15
|
+
class CloudflareTunnel:
|
16
|
+
def __init__(self, base_domain, device_id): # base_domain kept for compatibility
|
17
|
+
"""
|
18
|
+
Initialize tunnel with service token
|
19
|
+
No login or credential files needed
|
20
|
+
"""
|
21
|
+
# Configuration
|
22
|
+
self.base_domain = "1scan.uz" # Hardcoded domain
|
23
|
+
self.device_id = device_id
|
24
|
+
self.hostname = socket.gethostname()
|
25
|
+
|
26
|
+
# Service token - replace with your actual token from cloudflared tunnel token command
|
27
|
+
# This token can ONLY run the tunnel, cannot modify or delete it
|
28
|
+
# To generate: cloudflared tunnel token unitlab-shared
|
29
|
+
self.service_token = os.getenv(
|
30
|
+
"CLOUDFLARE_TUNNEL_TOKEN",
|
31
|
+
"eyJhIjoiYzkxMTkyYWUyMGE1ZDQzZjY1ZTA4NzU1MGQ4ZGM4OWIiLCJzIjoiZmdnSHowbFJFRnBHa05TZzIzV3JKMVBiaDVROGVUd0oyYWtJWThXdjhtTT0iLCJ0IjoiYjMzZGFhOGYtMmNjMy00Y2FkLWEyMjgtOTdlMDYwNzBlNjAwIn0=" # Replace this with your actual token from step 1
|
32
|
+
)
|
33
|
+
|
34
|
+
if self.service_token == "YOUR_SERVICE_TOKEN_HERE":
|
35
|
+
logger.warning(
|
36
|
+
"⚠️ No service token configured. "
|
37
|
+
"Set CLOUDFLARE_TUNNEL_TOKEN env var or update the token in tunnel_config.py"
|
38
|
+
)
|
39
|
+
|
40
|
+
# Subdomain names
|
41
|
+
self.jupyter_subdomain = f"jupyter-{device_id}"
|
42
|
+
self.ssh_subdomain = f"ssh-{device_id}"
|
43
|
+
|
44
|
+
# Full URLs
|
45
|
+
self.jupyter_url = f"https://{self.jupyter_subdomain}.{self.base_domain}"
|
46
|
+
self.ssh_url = f"https://{self.ssh_subdomain}.{self.base_domain}"
|
47
|
+
|
48
|
+
self.tunnel_process = None
|
49
|
+
|
50
|
+
def check_cloudflared_installed(self):
|
51
|
+
"""Check if cloudflared is installed"""
|
52
|
+
try:
|
53
|
+
result = subprocess.run(
|
54
|
+
["cloudflared", "--version"],
|
55
|
+
capture_output=True,
|
56
|
+
text=True
|
57
|
+
)
|
58
|
+
return result.returncode == 0
|
59
|
+
except FileNotFoundError:
|
60
|
+
return False
|
61
|
+
|
62
|
+
def install_cloudflared(self):
|
63
|
+
"""Auto-install cloudflared if not present"""
|
64
|
+
import platform
|
65
|
+
|
66
|
+
system = platform.system().lower()
|
67
|
+
machine = platform.machine().lower()
|
68
|
+
|
69
|
+
print("📦 Installing cloudflared...")
|
70
|
+
|
71
|
+
try:
|
72
|
+
if system == "linux":
|
73
|
+
# Determine architecture
|
74
|
+
if machine in ["x86_64", "amd64"]:
|
75
|
+
arch = "amd64"
|
76
|
+
elif machine in ["aarch64", "arm64"]:
|
77
|
+
arch = "arm64"
|
78
|
+
else:
|
79
|
+
arch = "386"
|
80
|
+
|
81
|
+
# Download and install
|
82
|
+
url = f"https://github.com/cloudflare/cloudflared/releases/latest/download/cloudflared-linux-{arch}"
|
83
|
+
|
84
|
+
commands = [
|
85
|
+
f"curl -L {url} -o /tmp/cloudflared",
|
86
|
+
"chmod +x /tmp/cloudflared",
|
87
|
+
"sudo mv /tmp/cloudflared /usr/local/bin/cloudflared 2>/dev/null || "
|
88
|
+
"mkdir -p ~/.local/bin && mv /tmp/cloudflared ~/.local/bin/cloudflared"
|
89
|
+
]
|
90
|
+
|
91
|
+
for cmd in commands:
|
92
|
+
subprocess.run(cmd, shell=True, check=False)
|
93
|
+
|
94
|
+
# Add ~/.local/bin to PATH if needed
|
95
|
+
local_bin = os.path.expanduser("~/.local/bin")
|
96
|
+
if local_bin not in os.environ.get("PATH", ""):
|
97
|
+
os.environ["PATH"] = f"{local_bin}:{os.environ['PATH']}"
|
98
|
+
|
99
|
+
return self.check_cloudflared_installed()
|
100
|
+
|
101
|
+
elif system == "darwin":
|
102
|
+
# Try homebrew first
|
103
|
+
result = subprocess.run(["brew", "install", "cloudflared"], capture_output=True)
|
104
|
+
if result.returncode != 0:
|
105
|
+
# Fallback to direct download
|
106
|
+
url = "https://github.com/cloudflare/cloudflared/releases/latest/download/cloudflared-darwin-amd64.tgz"
|
107
|
+
subprocess.run(f"curl -L {url} | tar xz", shell=True)
|
108
|
+
subprocess.run("sudo mv cloudflared /usr/local/bin/", shell=True)
|
109
|
+
|
110
|
+
return self.check_cloudflared_installed()
|
111
|
+
|
112
|
+
elif system == "windows":
|
113
|
+
print("⚠️ Please install cloudflared manually on Windows")
|
114
|
+
print(" Download from: https://github.com/cloudflare/cloudflared/releases")
|
115
|
+
return False
|
116
|
+
|
117
|
+
except Exception as e:
|
118
|
+
logger.error(f"Failed to install cloudflared: {e}")
|
119
|
+
return False
|
120
|
+
|
121
|
+
def setup(self, jupyter_port): # jupyter_port kept for compatibility
|
122
|
+
"""
|
123
|
+
Setup and start tunnel with service token
|
124
|
+
No login required!
|
125
|
+
"""
|
126
|
+
print("🚀 Setting up Cloudflare tunnel with service token...")
|
127
|
+
|
128
|
+
# Check if cloudflared is installed
|
129
|
+
if not self.check_cloudflared_installed():
|
130
|
+
print("cloudflared not found, attempting to install...")
|
131
|
+
if not self.install_cloudflared():
|
132
|
+
print("❌ Failed to install cloudflared. Please install it manually:")
|
133
|
+
print(" https://developers.cloudflare.com/cloudflare-one/connections/connect-apps/install-and-setup/installation/")
|
134
|
+
return None
|
135
|
+
|
136
|
+
# Start tunnel with service token
|
137
|
+
return self.start_tunnel_with_token()
|
138
|
+
|
139
|
+
def start_tunnel_with_token(self):
|
140
|
+
"""
|
141
|
+
Start the tunnel using service token
|
142
|
+
This is much simpler than the credential file approach
|
143
|
+
"""
|
144
|
+
try:
|
145
|
+
print("🚀 Starting Cloudflare tunnel...")
|
146
|
+
|
147
|
+
# Simple command with service token
|
148
|
+
cmd = [
|
149
|
+
"cloudflared",
|
150
|
+
"tunnel",
|
151
|
+
"--no-autoupdate", # Prevent auto-updates during run
|
152
|
+
"run",
|
153
|
+
"--token",
|
154
|
+
self.service_token
|
155
|
+
]
|
156
|
+
|
157
|
+
# Start the tunnel process
|
158
|
+
self.tunnel_process = subprocess.Popen(
|
159
|
+
cmd,
|
160
|
+
stdout=subprocess.PIPE,
|
161
|
+
stderr=subprocess.STDOUT,
|
162
|
+
text=True,
|
163
|
+
bufsize=1
|
164
|
+
)
|
165
|
+
|
166
|
+
# Wait for tunnel to establish
|
167
|
+
print("⏳ Waiting for tunnel to connect...")
|
168
|
+
time.sleep(5)
|
169
|
+
|
170
|
+
# Check if process is still running
|
171
|
+
if self.tunnel_process.poll() is None:
|
172
|
+
print("✅ Tunnel is running!")
|
173
|
+
print(f"📌 Jupyter URL: {self.jupyter_url}")
|
174
|
+
print(f"📌 SSH URL: {self.ssh_url}")
|
175
|
+
return self.tunnel_process
|
176
|
+
else:
|
177
|
+
# Read any error output
|
178
|
+
output = self.tunnel_process.stdout.read()
|
179
|
+
print("❌ Tunnel failed to start")
|
180
|
+
print(f"Error output: {output}")
|
181
|
+
return None
|
182
|
+
|
183
|
+
except Exception as e:
|
184
|
+
print(f"❌ Error starting tunnel: {e}")
|
185
|
+
return None
|
186
|
+
|
187
|
+
def stop(self):
|
188
|
+
"""Stop the tunnel if running"""
|
189
|
+
if self.tunnel_process and self.tunnel_process.poll() is None:
|
190
|
+
print("Stopping tunnel...")
|
191
|
+
self.tunnel_process.terminate()
|
192
|
+
self.tunnel_process.wait(timeout=5)
|
193
|
+
print("Tunnel stopped")
|
194
|
+
|
195
|
+
# Removed all the old methods that are no longer needed:
|
196
|
+
# - login() - not needed with service token
|
197
|
+
# - create_tunnel() - tunnel already exists
|
198
|
+
# - configure_dns() - already configured
|
199
|
+
# - create_config_file() - not needed with service token
|
@@ -0,0 +1,104 @@
|
|
1
|
+
"""
|
2
|
+
Service Token implementation for Cloudflare Tunnel
|
3
|
+
More secure than embedding full credentials
|
4
|
+
"""
|
5
|
+
|
6
|
+
import os
|
7
|
+
import subprocess
|
8
|
+
import time
|
9
|
+
from pathlib import Path
|
10
|
+
|
11
|
+
|
12
|
+
class ServiceTokenTunnel:
|
13
|
+
"""
|
14
|
+
Use Cloudflare Service Token instead of credentials file
|
15
|
+
This is more secure and doesn't require login
|
16
|
+
"""
|
17
|
+
|
18
|
+
# Embed the service token (generated once by admin)
|
19
|
+
# This token can ONLY run the tunnel, cannot modify it
|
20
|
+
DEFAULT_SERVICE_TOKEN = "eyJhIjoiYzkxMTkyYWUyMGE1ZDQzZjY1ZTA4NzU1MGQ4ZGM4OWIiLCJzIjoiZmdnSHowbFJFRnBHa05TZzIzV3JKMVBiaDVROGVUd0oyYWtJWThXdjhtTT0iLCJ0IjoiYjMzZGFhOGYtMmNjMy00Y2FkLWEyMjgtOTdlMDYwNzBlNjAwIn0="
|
21
|
+
|
22
|
+
def __init__(self, device_id, service_token=None):
|
23
|
+
self.device_id = device_id
|
24
|
+
# Allow override via environment variable or parameter
|
25
|
+
self.service_token = (
|
26
|
+
service_token or
|
27
|
+
os.getenv("CLOUDFLARE_TUNNEL_TOKEN") or
|
28
|
+
self.DEFAULT_SERVICE_TOKEN
|
29
|
+
)
|
30
|
+
|
31
|
+
# With service token, we don't need credential files
|
32
|
+
# The token contains all necessary information
|
33
|
+
|
34
|
+
def start_tunnel_with_token(self):
|
35
|
+
"""
|
36
|
+
Start tunnel using service token
|
37
|
+
No login, no credential files needed
|
38
|
+
"""
|
39
|
+
print("🚀 Starting tunnel with service token...")
|
40
|
+
|
41
|
+
# Service token method - super simple!
|
42
|
+
cmd = [
|
43
|
+
"cloudflared",
|
44
|
+
"tunnel",
|
45
|
+
"run",
|
46
|
+
"--token",
|
47
|
+
self.service_token
|
48
|
+
]
|
49
|
+
|
50
|
+
try:
|
51
|
+
# Start the tunnel
|
52
|
+
process = subprocess.Popen(
|
53
|
+
cmd,
|
54
|
+
stdout=subprocess.PIPE,
|
55
|
+
stderr=subprocess.PIPE,
|
56
|
+
text=True
|
57
|
+
)
|
58
|
+
|
59
|
+
# Wait for tunnel to establish
|
60
|
+
time.sleep(3)
|
61
|
+
|
62
|
+
if process.poll() is None:
|
63
|
+
print("✅ Tunnel running with service token")
|
64
|
+
return process
|
65
|
+
else:
|
66
|
+
print("❌ Failed to start tunnel")
|
67
|
+
return None
|
68
|
+
|
69
|
+
except Exception as e:
|
70
|
+
print(f"❌ Error: {e}")
|
71
|
+
return None
|
72
|
+
|
73
|
+
def get_tunnel_info(self):
|
74
|
+
"""
|
75
|
+
Service tokens use predetermined URLs
|
76
|
+
The admin sets these up when creating the tunnel
|
77
|
+
"""
|
78
|
+
# These URLs are configured when creating the tunnel
|
79
|
+
base_domain = "1scan.uz"
|
80
|
+
return {
|
81
|
+
"jupyter_url": f"https://jupyter-{self.device_id}.{base_domain}",
|
82
|
+
"ssh_url": f"https://ssh-{self.device_id}.{base_domain}"
|
83
|
+
}
|
84
|
+
|
85
|
+
|
86
|
+
# Usage example
|
87
|
+
def run_with_service_token(device_id):
|
88
|
+
"""
|
89
|
+
Example of how simple it is with service token
|
90
|
+
"""
|
91
|
+
tunnel = ServiceTokenTunnel(device_id)
|
92
|
+
|
93
|
+
# No login needed!
|
94
|
+
# No credential files needed!
|
95
|
+
# Just run with the token
|
96
|
+
process = tunnel.start_tunnel_with_token()
|
97
|
+
|
98
|
+
if process:
|
99
|
+
urls = tunnel.get_tunnel_info()
|
100
|
+
print(f"Jupyter: {urls['jupyter_url']}")
|
101
|
+
print(f"SSH: {urls['ssh_url']}")
|
102
|
+
return process
|
103
|
+
|
104
|
+
return None
|
@@ -1,6 +1,6 @@
|
|
1
1
|
Metadata-Version: 2.1
|
2
2
|
Name: unitlab
|
3
|
-
Version: 2.3.
|
3
|
+
Version: 2.3.4
|
4
4
|
Home-page: https://github.com/teamunitlab/unitlab-sdk
|
5
5
|
Author: Unitlab Inc.
|
6
6
|
Author-email: team@unitlab.ai
|
@@ -23,3 +23,4 @@ Requires-Dist: typer
|
|
23
23
|
Requires-Dist: validators
|
24
24
|
Requires-Dist: psutil
|
25
25
|
Requires-Dist: pyyaml
|
26
|
+
Requires-Dist: jupyter
|
@@ -1,238 +0,0 @@
|
|
1
|
-
"""
|
2
|
-
Cloudflare Tunnel Configuration for persistent subdomains
|
3
|
-
"""
|
4
|
-
|
5
|
-
import json
|
6
|
-
import subprocess
|
7
|
-
import socket
|
8
|
-
import time
|
9
|
-
import yaml
|
10
|
-
from pathlib import Path
|
11
|
-
|
12
|
-
|
13
|
-
class CloudflareTunnel:
|
14
|
-
def __init__(self, base_domain, device_id):
|
15
|
-
# Hardcode the base domain here
|
16
|
-
self.base_domain = "1scan.uz" # HARDCODED - ignore the passed base_domain
|
17
|
-
self.device_id = device_id
|
18
|
-
self.hostname = socket.gethostname()
|
19
|
-
self.tunnel_name = f"device-{device_id}"
|
20
|
-
self.config_dir = Path.home() / ".cloudflared"
|
21
|
-
self.config_dir.mkdir(exist_ok=True)
|
22
|
-
|
23
|
-
# Subdomain names
|
24
|
-
self.jupyter_subdomain = f"jupyter-{device_id}"
|
25
|
-
self.ssh_subdomain = f"ssh-{device_id}"
|
26
|
-
|
27
|
-
# Full URLs - using hardcoded base_domain
|
28
|
-
self.jupyter_url = f"https://{self.jupyter_subdomain}.{self.base_domain}"
|
29
|
-
self.ssh_url = f"https://{self.ssh_subdomain}.{self.base_domain}"
|
30
|
-
|
31
|
-
self.tunnel_uuid = None
|
32
|
-
self.credentials_file = None
|
33
|
-
|
34
|
-
def login(self):
|
35
|
-
"""Login to Cloudflare (one-time setup)"""
|
36
|
-
try:
|
37
|
-
print("🔐 Checking Cloudflare authentication...")
|
38
|
-
result = subprocess.run(
|
39
|
-
["cloudflared", "tunnel", "login"],
|
40
|
-
capture_output=True,
|
41
|
-
text=True
|
42
|
-
)
|
43
|
-
if result.returncode == 0:
|
44
|
-
print("✅ Cloudflare authentication successful")
|
45
|
-
return True
|
46
|
-
else:
|
47
|
-
print("❌ Cloudflare authentication failed")
|
48
|
-
return False
|
49
|
-
except Exception as e:
|
50
|
-
print(f"❌ Error during Cloudflare login: {e}")
|
51
|
-
return False
|
52
|
-
|
53
|
-
def create_tunnel(self):
|
54
|
-
"""Create a named tunnel"""
|
55
|
-
try:
|
56
|
-
print(f"🚇 Creating tunnel: {self.tunnel_name}")
|
57
|
-
|
58
|
-
# Check if tunnel already exists
|
59
|
-
list_result = subprocess.run(
|
60
|
-
["cloudflared", "tunnel", "list", "--output", "json"],
|
61
|
-
capture_output=True,
|
62
|
-
text=True
|
63
|
-
)
|
64
|
-
|
65
|
-
if list_result.returncode == 0:
|
66
|
-
tunnels = json.loads(list_result.stdout)
|
67
|
-
for tunnel in tunnels:
|
68
|
-
if tunnel.get("name") == self.tunnel_name:
|
69
|
-
self.tunnel_uuid = tunnel.get("id")
|
70
|
-
print(f"✅ Tunnel already exists with ID: {self.tunnel_uuid}")
|
71
|
-
self.credentials_file = self.config_dir / f"{self.tunnel_uuid}.json"
|
72
|
-
return True
|
73
|
-
|
74
|
-
# Create new tunnel
|
75
|
-
result = subprocess.run(
|
76
|
-
["cloudflared", "tunnel", "create", self.tunnel_name],
|
77
|
-
capture_output=True,
|
78
|
-
text=True
|
79
|
-
)
|
80
|
-
|
81
|
-
if result.returncode == 0:
|
82
|
-
for line in result.stdout.split('\n'):
|
83
|
-
if "Created tunnel" in line and "with id" in line:
|
84
|
-
self.tunnel_uuid = line.split("with id")[1].strip()
|
85
|
-
break
|
86
|
-
|
87
|
-
if not self.tunnel_uuid:
|
88
|
-
list_result = subprocess.run(
|
89
|
-
["cloudflared", "tunnel", "list", "--output", "json"],
|
90
|
-
capture_output=True,
|
91
|
-
text=True
|
92
|
-
)
|
93
|
-
if list_result.returncode == 0:
|
94
|
-
tunnels = json.loads(list_result.stdout)
|
95
|
-
for tunnel in tunnels:
|
96
|
-
if tunnel.get("name") == self.tunnel_name:
|
97
|
-
self.tunnel_uuid = tunnel.get("id")
|
98
|
-
break
|
99
|
-
|
100
|
-
if self.tunnel_uuid:
|
101
|
-
self.credentials_file = self.config_dir / f"{self.tunnel_uuid}.json"
|
102
|
-
print(f"✅ Tunnel created with ID: {self.tunnel_uuid}")
|
103
|
-
return True
|
104
|
-
|
105
|
-
print(f"❌ Failed to create tunnel: {result.stderr}")
|
106
|
-
return False
|
107
|
-
|
108
|
-
except Exception as e:
|
109
|
-
print(f"❌ Error creating tunnel: {e}")
|
110
|
-
return False
|
111
|
-
|
112
|
-
def configure_dns(self):
|
113
|
-
"""Configure DNS routes for the tunnel"""
|
114
|
-
try:
|
115
|
-
print("🌐 Configuring DNS routes...")
|
116
|
-
|
117
|
-
# Route for Jupyter
|
118
|
-
jupyter_result = subprocess.run(
|
119
|
-
["cloudflared", "tunnel", "route", "dns",
|
120
|
-
self.tunnel_name, f"{self.jupyter_subdomain}.{self.base_domain}"],
|
121
|
-
capture_output=True,
|
122
|
-
text=True
|
123
|
-
)
|
124
|
-
|
125
|
-
if jupyter_result.returncode == 0:
|
126
|
-
print(f"✅ Jupyter route configured: {self.jupyter_url}")
|
127
|
-
else:
|
128
|
-
print(f"⚠️ Jupyter route may already exist or failed: {jupyter_result.stderr}")
|
129
|
-
|
130
|
-
# Route for SSH
|
131
|
-
ssh_result = subprocess.run(
|
132
|
-
["cloudflared", "tunnel", "route", "dns",
|
133
|
-
self.tunnel_name, f"{self.ssh_subdomain}.{self.base_domain}"],
|
134
|
-
capture_output=True,
|
135
|
-
text=True
|
136
|
-
)
|
137
|
-
|
138
|
-
if ssh_result.returncode == 0:
|
139
|
-
print(f"✅ SSH route configured: {self.ssh_url}")
|
140
|
-
else:
|
141
|
-
print(f"⚠️ SSH route may already exist or failed: {ssh_result.stderr}")
|
142
|
-
|
143
|
-
return True
|
144
|
-
|
145
|
-
except Exception as e:
|
146
|
-
print(f"❌ Error configuring DNS: {e}")
|
147
|
-
return False
|
148
|
-
|
149
|
-
def create_config_file(self, jupyter_port):
|
150
|
-
"""Create tunnel configuration file"""
|
151
|
-
config = {
|
152
|
-
"tunnel": self.tunnel_uuid,
|
153
|
-
"credentials-file": str(self.credentials_file),
|
154
|
-
"ingress": [
|
155
|
-
{
|
156
|
-
"hostname": f"{self.jupyter_subdomain}.{self.base_domain}",
|
157
|
-
"service": f"http://localhost:{jupyter_port}",
|
158
|
-
"originRequest": {
|
159
|
-
"noTLSVerify": True
|
160
|
-
}
|
161
|
-
},
|
162
|
-
{
|
163
|
-
"hostname": f"{self.ssh_subdomain}.{self.base_domain}",
|
164
|
-
"service": "ssh://localhost:22",
|
165
|
-
"originRequest": {
|
166
|
-
"noTLSVerify": True
|
167
|
-
}
|
168
|
-
},
|
169
|
-
{
|
170
|
-
"service": "http_status:404"
|
171
|
-
}
|
172
|
-
]
|
173
|
-
}
|
174
|
-
|
175
|
-
config_file = self.config_dir / f"config-{self.device_id}.yml"
|
176
|
-
with open(config_file, 'w') as f:
|
177
|
-
yaml.dump(config, f, default_flow_style=False)
|
178
|
-
|
179
|
-
print(f"📝 Configuration saved to: {config_file}")
|
180
|
-
return config_file
|
181
|
-
|
182
|
-
def start_tunnel(self, config_file):
|
183
|
-
"""Start the tunnel with the configuration"""
|
184
|
-
try:
|
185
|
-
print("🚀 Starting Cloudflare tunnel...")
|
186
|
-
|
187
|
-
cmd = ["cloudflared", "tunnel", "--config", str(config_file), "run"]
|
188
|
-
|
189
|
-
process = subprocess.Popen(
|
190
|
-
cmd,
|
191
|
-
stdout=subprocess.PIPE,
|
192
|
-
stderr=subprocess.STDOUT,
|
193
|
-
text=True,
|
194
|
-
bufsize=1
|
195
|
-
)
|
196
|
-
|
197
|
-
# Wait for tunnel to establish
|
198
|
-
time.sleep(5)
|
199
|
-
|
200
|
-
if process.poll() is None:
|
201
|
-
print("✅ Tunnel is running")
|
202
|
-
return process
|
203
|
-
else:
|
204
|
-
print("❌ Tunnel failed to start")
|
205
|
-
return None
|
206
|
-
|
207
|
-
except Exception as e:
|
208
|
-
print(f"❌ Error starting tunnel: {e}")
|
209
|
-
return None
|
210
|
-
|
211
|
-
def setup(self, jupyter_port):
|
212
|
-
"""Complete setup process"""
|
213
|
-
# Check if we need to login
|
214
|
-
if not (self.config_dir / "cert.pem").exists():
|
215
|
-
if not self.login():
|
216
|
-
return None
|
217
|
-
|
218
|
-
# Create tunnel
|
219
|
-
if not self.create_tunnel():
|
220
|
-
return None
|
221
|
-
|
222
|
-
# Configure DNS
|
223
|
-
if not self.configure_dns():
|
224
|
-
return None
|
225
|
-
|
226
|
-
# Create config file
|
227
|
-
config_file = self.create_config_file(jupyter_port)
|
228
|
-
|
229
|
-
# Start tunnel
|
230
|
-
tunnel_process = self.start_tunnel(config_file)
|
231
|
-
|
232
|
-
if tunnel_process:
|
233
|
-
print("\n✅ Tunnel setup complete!")
|
234
|
-
print(f"📌 Jupyter URL: {self.jupyter_url}")
|
235
|
-
print(f"📌 SSH URL: {self.ssh_url}")
|
236
|
-
return tunnel_process
|
237
|
-
|
238
|
-
return None
|
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
|