unitlab 2.3.3__py3-none-any.whl → 2.3.4__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/tunnel_config.py CHANGED
@@ -1,192 +1,161 @@
1
1
  """
2
- Cloudflare Tunnel Configuration for persistent subdomains
2
+ Cloudflare Tunnel Configuration using Service Token
3
+ No user login required - uses pre-generated service token
3
4
  """
4
5
 
5
- import json
6
+ import os
6
7
  import subprocess
7
8
  import socket
8
9
  import time
9
- import yaml
10
- from pathlib import Path
10
+ import logging
11
+
12
+ logger = logging.getLogger(__name__)
11
13
 
12
14
 
13
15
  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
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
17
23
  self.device_id = device_id
18
24
  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)
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
+ )
22
39
 
23
40
  # Subdomain names
24
41
  self.jupyter_subdomain = f"jupyter-{device_id}"
25
42
  self.ssh_subdomain = f"ssh-{device_id}"
26
43
 
27
- # Full URLs - using hardcoded base_domain
44
+ # Full URLs
28
45
  self.jupyter_url = f"https://{self.jupyter_subdomain}.{self.base_domain}"
29
46
  self.ssh_url = f"https://{self.ssh_subdomain}.{self.base_domain}"
30
47
 
31
- self.tunnel_uuid = None
32
- self.credentials_file = None
48
+ self.tunnel_process = None
33
49
 
34
- def login(self):
35
- """Login to Cloudflare (one-time setup)"""
50
+ def check_cloudflared_installed(self):
51
+ """Check if cloudflared is installed"""
36
52
  try:
37
- print("🔐 Checking Cloudflare authentication...")
38
53
  result = subprocess.run(
39
- ["cloudflared", "tunnel", "login"],
54
+ ["cloudflared", "--version"],
40
55
  capture_output=True,
41
56
  text=True
42
57
  )
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}")
58
+ return result.returncode == 0
59
+ except FileNotFoundError:
51
60
  return False
52
61
 
53
- def create_tunnel(self):
54
- """Create a named tunnel"""
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
+
55
71
  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
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"
86
80
 
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
81
+ # Download and install
82
+ url = f"https://github.com/cloudflare/cloudflared/releases/latest/download/cloudflared-linux-{arch}"
99
83
 
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}")
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
142
116
 
143
- return True
144
-
145
117
  except Exception as e:
146
- print(f" Error configuring DNS: {e}")
118
+ logger.error(f"Failed to install cloudflared: {e}")
147
119
  return False
148
120
 
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
- }
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...")
174
127
 
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)
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
178
135
 
179
- print(f"📝 Configuration saved to: {config_file}")
180
- return config_file
136
+ # Start tunnel with service token
137
+ return self.start_tunnel_with_token()
181
138
 
182
- def start_tunnel(self, config_file):
183
- """Start the tunnel with the configuration"""
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
+ """
184
144
  try:
185
145
  print("🚀 Starting Cloudflare tunnel...")
186
146
 
187
- cmd = ["cloudflared", "tunnel", "--config", str(config_file), "run"]
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
+ ]
188
156
 
189
- process = subprocess.Popen(
157
+ # Start the tunnel process
158
+ self.tunnel_process = subprocess.Popen(
190
159
  cmd,
191
160
  stdout=subprocess.PIPE,
192
161
  stderr=subprocess.STDOUT,
@@ -195,44 +164,36 @@ class CloudflareTunnel:
195
164
  )
196
165
 
197
166
  # Wait for tunnel to establish
167
+ print("⏳ Waiting for tunnel to connect...")
198
168
  time.sleep(5)
199
169
 
200
- if process.poll() is None:
201
- print("✅ Tunnel is running")
202
- return process
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
203
176
  else:
177
+ # Read any error output
178
+ output = self.tunnel_process.stdout.read()
204
179
  print("❌ Tunnel failed to start")
180
+ print(f"Error output: {output}")
205
181
  return None
206
182
 
207
183
  except Exception as e:
208
184
  print(f"❌ Error starting tunnel: {e}")
209
185
  return None
210
186
 
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
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
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,4 +23,5 @@ Requires-Dist: typer
23
23
  Requires-Dist: validators
24
24
  Requires-Dist: psutil
25
25
  Requires-Dist: pyyaml
26
+ Requires-Dist: jupyter
26
27
 
@@ -3,11 +3,12 @@ unitlab/__main__.py,sha256=6Hs2PV7EYc5Tid4g4OtcLXhqVHiNYTGzSBdoOnW2HXA,29
3
3
  unitlab/client.py,sha256=LGbjto3jisF5LojlYjJyLKRc55wxfZTO_tkoipZBKd4,22910
4
4
  unitlab/exceptions.py,sha256=68Tr6LreEzjQ3Vns8HAaWdtewtkNUJOvPazbf6NSnXU,950
5
5
  unitlab/main.py,sha256=XD8PF_EJ1gAKqHcRT_bWy62LWrODGWMx2roKoaQaoY4,5253
6
- unitlab/tunnel_config.py,sha256=lfWXKTPKlhHOxECUA0_HhynrKfOZ345nLo0qA7XtfbE,8571
6
+ unitlab/tunnel_config.py,sha256=WhxjuOjvOXz16KbMAgpvbCpRD0Y5y53y9jVizQ7CxR8,7841
7
+ unitlab/tunnel_service_token.py,sha256=ji96a4s4W2cFJrHZle0zBD85Ac_T862-gCKzBUomrxM,3125
7
8
  unitlab/utils.py,sha256=83ekAxxfXecFTg76Z62BGDybC_skKJHYoLyawCD9wGM,1920
8
- unitlab-2.3.3.dist-info/LICENSE.md,sha256=Gn7RRvByorAcAaM-WbyUpsgi5ED1-bKFFshbWfYYz2Y,1069
9
- unitlab-2.3.3.dist-info/METADATA,sha256=pO2UGvjS3FoVgAXZKlHUeBONoWAnLgVkZ2I55UyiSGs,791
10
- unitlab-2.3.3.dist-info/WHEEL,sha256=iAkIy5fosb7FzIOwONchHf19Qu7_1wCWyFNR5gu9nU0,91
11
- unitlab-2.3.3.dist-info/entry_points.txt,sha256=ig-PjKEqSCj3UTdyANgEi4tsAU84DyXdaOJ02NHX4bY,45
12
- unitlab-2.3.3.dist-info/top_level.txt,sha256=Al4ZlTYE3fTJK2o6YLCDMH5_DjuQkffRBMxgmWbKaqQ,8
13
- unitlab-2.3.3.dist-info/RECORD,,
9
+ unitlab-2.3.4.dist-info/LICENSE.md,sha256=Gn7RRvByorAcAaM-WbyUpsgi5ED1-bKFFshbWfYYz2Y,1069
10
+ unitlab-2.3.4.dist-info/METADATA,sha256=4GHYHBRrtcbbHw06gcZ5mfucnjJvOGFC4Y8DvRqw-28,814
11
+ unitlab-2.3.4.dist-info/WHEEL,sha256=iAkIy5fosb7FzIOwONchHf19Qu7_1wCWyFNR5gu9nU0,91
12
+ unitlab-2.3.4.dist-info/entry_points.txt,sha256=ig-PjKEqSCj3UTdyANgEi4tsAU84DyXdaOJ02NHX4bY,45
13
+ unitlab-2.3.4.dist-info/top_level.txt,sha256=Al4ZlTYE3fTJK2o6YLCDMH5_DjuQkffRBMxgmWbKaqQ,8
14
+ unitlab-2.3.4.dist-info/RECORD,,