chapman-sip 0.1.0__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.
@@ -0,0 +1,53 @@
1
+ Metadata-Version: 2.4
2
+ Name: chapman-sip
3
+ Version: 0.1.0
4
+ Summary: A CLI tool to run steerable models on the Chapman cluster with SSH port forwarding
5
+ Author-email: Developer <developer@example.com>
6
+ Classifier: Programming Language :: Python :: 3
7
+ Classifier: License :: OSI Approved :: MIT License
8
+ Classifier: Operating System :: OS Independent
9
+ Requires-Python: >=3.8
10
+ Description-Content-Type: text/markdown
11
+ Requires-Dist: paramiko>=3.0.0
12
+ Requires-Dist: click>=8.0.0
13
+ Requires-Dist: rich>=12.0.0
14
+
15
+ # Chapman-Sip CLI
16
+
17
+ A pip-installable Python command-line utility to connect to the Chapman cluster (`dgx0.chapman.edu`), pull the `steerable-model-runner` repository, find an open port, launch the model server, and tunnel the port back to your local machine.
18
+
19
+ ## Features
20
+
21
+ - **Automated Host Setup**: Checks if the remote repository is cloned at `~/.steerable-models` on the cluster, cloning or pulling changes automatically.
22
+ - **Dynamic Port Hunting**: Generates a random port and verifies it is not currently bound on the cluster before initiating.
23
+ - **SSH Port Forwarding**: Automatically forwards the selected remote port to `localhost:[PORT]` locally.
24
+ - **Interactive Log Streaming**: Streams stderr and stdout from the model serving script in real-time.
25
+ - **Safe Shutdown**: Automatically cleans up the remote process, closes the port-forwarding server, and closes SSH channels on Ctrl+C.
26
+
27
+ ## Installation
28
+
29
+ Install in editable mode for development or directly from the source directory:
30
+
31
+ ```bash
32
+ pip install -e .
33
+ ```
34
+
35
+ ## Usage
36
+
37
+ Simply run:
38
+
39
+ ```bash
40
+ sip
41
+ ```
42
+
43
+ You will be prompted for:
44
+ - SSH username
45
+ - SSH password
46
+ - Hugging Face API token (for model download)
47
+ - Hugging Face repository name (e.g., the model repository to run)
48
+
49
+ Once connected, the utility will output the endpoint URL, e.g.:
50
+ ```
51
+ https://localhost:[PORT]/v1/
52
+ ```
53
+ Keep the CLI running while you query the endpoint. Press `Ctrl+C` to terminate the model server and close the tunnel.
@@ -0,0 +1,39 @@
1
+ # Chapman-Sip CLI
2
+
3
+ A pip-installable Python command-line utility to connect to the Chapman cluster (`dgx0.chapman.edu`), pull the `steerable-model-runner` repository, find an open port, launch the model server, and tunnel the port back to your local machine.
4
+
5
+ ## Features
6
+
7
+ - **Automated Host Setup**: Checks if the remote repository is cloned at `~/.steerable-models` on the cluster, cloning or pulling changes automatically.
8
+ - **Dynamic Port Hunting**: Generates a random port and verifies it is not currently bound on the cluster before initiating.
9
+ - **SSH Port Forwarding**: Automatically forwards the selected remote port to `localhost:[PORT]` locally.
10
+ - **Interactive Log Streaming**: Streams stderr and stdout from the model serving script in real-time.
11
+ - **Safe Shutdown**: Automatically cleans up the remote process, closes the port-forwarding server, and closes SSH channels on Ctrl+C.
12
+
13
+ ## Installation
14
+
15
+ Install in editable mode for development or directly from the source directory:
16
+
17
+ ```bash
18
+ pip install -e .
19
+ ```
20
+
21
+ ## Usage
22
+
23
+ Simply run:
24
+
25
+ ```bash
26
+ sip
27
+ ```
28
+
29
+ You will be prompted for:
30
+ - SSH username
31
+ - SSH password
32
+ - Hugging Face API token (for model download)
33
+ - Hugging Face repository name (e.g., the model repository to run)
34
+
35
+ Once connected, the utility will output the endpoint URL, e.g.:
36
+ ```
37
+ https://localhost:[PORT]/v1/
38
+ ```
39
+ Keep the CLI running while you query the endpoint. Press `Ctrl+C` to terminate the model server and close the tunnel.
@@ -0,0 +1,26 @@
1
+ [build-system]
2
+ requires = ["setuptools>=61.0.0", "wheel"]
3
+ build-backend = "setuptools.build_meta"
4
+
5
+ [project]
6
+ name = "chapman-sip"
7
+ version = "0.1.0"
8
+ description = "A CLI tool to run steerable models on the Chapman cluster with SSH port forwarding"
9
+ readme = "README.md"
10
+ authors = [
11
+ { name = "Developer", email = "developer@example.com" }
12
+ ]
13
+ classifiers = [
14
+ "Programming Language :: Python :: 3",
15
+ "License :: OSI Approved :: MIT License",
16
+ "Operating System :: OS Independent",
17
+ ]
18
+ requires-python = ">=3.8"
19
+ dependencies = [
20
+ "paramiko>=3.0.0",
21
+ "click>=8.0.0",
22
+ "rich>=12.0.0",
23
+ ]
24
+
25
+ [project.scripts]
26
+ sip = "chapman_runner.cli:main"
@@ -0,0 +1,4 @@
1
+ [egg_info]
2
+ tag_build =
3
+ tag_date = 0
4
+
@@ -0,0 +1,5 @@
1
+ """
2
+ Chapman Cluster Model Runner package.
3
+ """
4
+
5
+ __version__ = "0.1.0"
@@ -0,0 +1,409 @@
1
+ import os
2
+ import json
3
+ import sys
4
+ import time
5
+ import click
6
+ import shutil
7
+ import socket
8
+ import subprocess
9
+ import webbrowser
10
+ from rich.console import Console
11
+ from rich.panel import Panel
12
+ from rich.text import Text
13
+
14
+ from chapman_runner.ssh import (
15
+ connect_ssh,
16
+ find_free_remote_port,
17
+ setup_remote_repo,
18
+ start_model_server,
19
+ )
20
+ from chapman_runner.tunnel import SSHTunnel
21
+
22
+ console = Console()
23
+ CACHE_FILE = os.path.expanduser("~/.sip_cache.json")
24
+
25
+ def is_docker_running():
26
+ """
27
+ Checks if Docker is installed and the daemon is active.
28
+ """
29
+ if shutil.which("docker") is None:
30
+ return False
31
+ try:
32
+ result = subprocess.run(
33
+ ["docker", "info"],
34
+ stdout=subprocess.DEVNULL,
35
+ stderr=subprocess.DEVNULL,
36
+ timeout=5
37
+ )
38
+ return result.returncode == 0
39
+ except Exception:
40
+ return False
41
+
42
+ def is_local_port_free(port):
43
+ """
44
+ Checks if a port is free on the local loopback.
45
+ """
46
+ with socket.socket(socket.AF_INET, socket.SOCK_STREAM) as s:
47
+ try:
48
+ s.bind(("127.0.0.1", port))
49
+ return True
50
+ except OSError:
51
+ return False
52
+
53
+ def find_free_local_port(start_port=3000):
54
+ """
55
+ Finds the first free local port starting at start_port.
56
+ """
57
+ port = start_port
58
+ while port < 65535:
59
+ if is_local_port_free(port):
60
+ return port
61
+ port += 1
62
+ raise RuntimeError("No free local ports found!")
63
+
64
+ def start_open_webui(model_port):
65
+ """
66
+ Finds a local port, restarts the Open WebUI docker container,
67
+ and opens the URL in the browser.
68
+ """
69
+ if not is_docker_running():
70
+ console.print("[yellow]* Docker is not running or not installed. Skipping Open WebUI startup.[/yellow]")
71
+ return None
72
+
73
+ try:
74
+ webui_port = find_free_local_port(3000)
75
+ console.print(f"[cyan]Starting Open WebUI on local port {webui_port}...[/cyan]")
76
+
77
+ # 1. Clean up any existing 'sip-open-webui' container
78
+ subprocess.run(
79
+ ["docker", "rm", "-f", "sip-open-webui"],
80
+ stdout=subprocess.DEVNULL,
81
+ stderr=subprocess.DEVNULL
82
+ )
83
+
84
+ # 2. Run the new container
85
+ cmd = [
86
+ "docker", "run", "-d",
87
+ "--name", "sip-open-webui",
88
+ "-p", f"{webui_port}:8080",
89
+ "-e", f"OPENAI_API_BASE_URL=http://host.docker.internal:{model_port}/v1",
90
+ "-v", "open-webui:/app/backend/data",
91
+ "--restart", "always",
92
+ "ghcr.io/open-webui/open-webui:main"
93
+ ]
94
+
95
+ result = subprocess.run(cmd, stdout=subprocess.PIPE, stderr=subprocess.PIPE, text=True)
96
+ if result.returncode != 0:
97
+ console.print(f"[red]Failed to start Open WebUI container: {result.stderr.strip()}[/red]")
98
+ return None
99
+
100
+ console.print(f"[green]* Open WebUI container started successfully (ID: {result.stdout.strip()[:12]}).[/green]")
101
+
102
+ # 3. Open in browser
103
+ webui_url = f"http://localhost:{webui_port}"
104
+ console.print(f"[green]* Opening {webui_url} in default browser...[/green]")
105
+ webbrowser.open(webui_url)
106
+
107
+ return webui_port
108
+ except Exception as e:
109
+ console.print(f"[red]Error starting Open WebUI: {e}[/red]")
110
+ return None
111
+
112
+ def load_cache():
113
+ """
114
+ Loads saved configurations from the local cache file.
115
+ """
116
+ if os.path.exists(CACHE_FILE):
117
+ try:
118
+ with open(CACHE_FILE, "r") as f:
119
+ data = json.load(f)
120
+ if isinstance(data, dict) and "history" in data:
121
+ return data
122
+ except Exception:
123
+ pass
124
+ return {"history": []}
125
+
126
+ def save_cache(host, username, hf_token, hf_repo):
127
+ """
128
+ Saves a successful configuration run to the local cache file.
129
+ """
130
+ cache = load_cache()
131
+ history = cache.get("history", [])
132
+
133
+ # Create the config object (passwords are NEVER cached)
134
+ config = {
135
+ "host": host,
136
+ "username": username,
137
+ "hf_token": hf_token,
138
+ "hf_repo": hf_repo
139
+ }
140
+
141
+ # Remove any existing configuration for the same host, username, and repo to avoid duplication
142
+ history = [
143
+ c for c in history
144
+ if not (c.get("host") == host and c.get("username") == username and c.get("hf_repo") == hf_repo)
145
+ ]
146
+
147
+ # Insert new run configuration at the front
148
+ history.insert(0, config)
149
+
150
+ # Keep only the last 10 configurations
151
+ cache["history"] = history[:10]
152
+
153
+ try:
154
+ os.makedirs(os.path.dirname(CACHE_FILE), exist_ok=True)
155
+ with open(CACHE_FILE, "w") as f:
156
+ json.dump(cache, f, indent=4)
157
+ except Exception:
158
+ pass
159
+
160
+ def print_banner():
161
+ banner = Text()
162
+ banner.append("\n", style="bold")
163
+ banner.append("+--------------------------------------------------------+\n", style="bold cyan")
164
+ banner.append("| CHAPMAN CLUSTER MODEL RUNNER CLI |\n", style="bold cyan")
165
+ banner.append("+--------------------------------------------------------+\n", style="bold cyan")
166
+ console.print(banner)
167
+
168
+ @click.command()
169
+ @click.option("--host", default="dgx0.chapman.edu", help="The Chapman cluster host address.")
170
+ @click.option("--username", help="Your SSH username for the Chapman cluster.")
171
+ def main(host, username):
172
+ """
173
+ CLI utility to connect to the Chapman cluster, set up steerable-model-runner,
174
+ and tunnel the serving port back to your local machine.
175
+ """
176
+ print_banner()
177
+
178
+ cache = load_cache()
179
+ history = cache.get("history", [])
180
+ selected_config = None
181
+ edit_config = False
182
+
183
+ while True:
184
+ selected_config = None
185
+ edit_config = False
186
+
187
+ # Check if there are cached configurations
188
+ if history:
189
+ console.print("[bold yellow]Saved configurations found:[/bold yellow]")
190
+ for idx, config in enumerate(history, 1):
191
+ console.print(f"[{idx}] [green]{config['hf_repo']}[/green] (on {config['host']} as {config['username']})")
192
+ console.print(f"[{len(history) + 1}] Start a new custom run...")
193
+
194
+ choice = click.prompt(
195
+ click.style("Select an option", fg="green", bold=True),
196
+ type=click.IntRange(1, len(history) + 1),
197
+ default=1
198
+ )
199
+
200
+ if choice <= len(history):
201
+ selected_config = history[choice - 1]
202
+ else:
203
+ selected_config = None
204
+
205
+ if selected_config:
206
+ console.print(f"\n[cyan]Selected:[/cyan] [green]{selected_config['hf_repo']}[/green] on {selected_config['host']} as {selected_config['username']}")
207
+ console.print("[1] Run directly")
208
+ console.print("[2] Edit this configuration")
209
+ console.print("[3] Cancel (Go back)")
210
+
211
+ action = click.prompt(
212
+ click.style("Select action", fg="green", bold=True),
213
+ type=click.IntRange(1, 3),
214
+ default=1
215
+ )
216
+
217
+ if action == 3:
218
+ console.print()
219
+ continue
220
+ elif action == 2:
221
+ edit_config = True
222
+
223
+ break
224
+
225
+ # Resolve details based on selection or prompts
226
+ if selected_config and not edit_config:
227
+ host = selected_config.get("host")
228
+ username = selected_config.get("username")
229
+ hf_token = selected_config.get("hf_token")
230
+ hf_repo = selected_config.get("hf_repo")
231
+
232
+ # Only prompt for the password
233
+ password = click.prompt(
234
+ click.style(f"Enter SSH Password for {username}", fg="green", bold=True),
235
+ hide_input=True,
236
+ type=str
237
+ )
238
+
239
+ # If there is no hf_token in the cache, ask for it!
240
+ if not hf_token:
241
+ hf_token = click.prompt(
242
+ click.style("Enter Hugging Face API Token", fg="green", bold=True),
243
+ hide_input=True,
244
+ type=str
245
+ )
246
+ else:
247
+ # Defaults based on selection or the most recent run in history
248
+ default_host = selected_config.get("host") if selected_config else (host or "dgx0.chapman.edu")
249
+ default_username = selected_config.get("username") if selected_config else (username or (history[0]["username"] if history else None))
250
+ default_token = selected_config.get("hf_token") if selected_config else (history[0]["hf_token"] if history else None)
251
+ default_repo = selected_config.get("hf_repo") if selected_config else (history[0]["hf_repo"] if history else None)
252
+
253
+ host = click.prompt(
254
+ click.style("Enter Chapman Cluster Host", fg="green", bold=True),
255
+ default=default_host,
256
+ type=str
257
+ )
258
+
259
+ username = click.prompt(
260
+ click.style("Enter SSH Username", fg="green", bold=True),
261
+ default=default_username,
262
+ type=str
263
+ )
264
+
265
+ password = click.prompt(
266
+ click.style(f"Enter SSH Password for {username}", fg="green", bold=True),
267
+ hide_input=True,
268
+ type=str
269
+ )
270
+
271
+ hf_token = click.prompt(
272
+ click.style("Enter Hugging Face API Token", fg="green", bold=True),
273
+ default=default_token,
274
+ hide_input=True,
275
+ type=str
276
+ )
277
+
278
+ hf_repo = click.prompt(
279
+ click.style("Enter Hugging Face Model Repository", fg="green", bold=True),
280
+ default=default_repo,
281
+ type=str
282
+ )
283
+
284
+ ssh_client = None
285
+ tunnel = None
286
+ channel = None
287
+
288
+ try:
289
+ # Step 1: Connect to the cluster
290
+ with console.status(f"[bold cyan]Connecting to {host}...[/bold cyan]") as status:
291
+ ssh_client = connect_ssh(host, username, password)
292
+ console.print(f"[green]* Connected to {host} successfully.[/green]")
293
+
294
+ # Step 2: Generate and verify remote port
295
+ with console.status("[bold cyan]Searching for an open port on the cluster...[/bold cyan]") as status:
296
+ port = find_free_remote_port(ssh_client)
297
+ console.print(f"[green]* Reserved free remote port: [bold]{port}[/bold][/green]")
298
+
299
+ # Step 3: Setup steerable-model-runner repo
300
+ with console.status("[bold cyan]Cloning/updating repository 'steerable-model-runner' on the cluster...[/bold cyan]") as status:
301
+ setup_remote_repo(
302
+ ssh_client,
303
+ repo_url="https://github.com/Alignment-Faking-Chapman/steerable-model-runner.git",
304
+ target_dir="~/.steerable-models"
305
+ )
306
+ console.print("[green]* Repository is set up and up-to-date in ~/.steerable-models.[/green]")
307
+
308
+ # Configuration succeeded, save to cache
309
+ save_cache(host, username, hf_token, hf_repo)
310
+
311
+ # Step 4: Setup local tunnel
312
+ tunnel = SSHTunnel(ssh_client, port)
313
+ tunnel.start()
314
+ console.print(f"[green]* Local SSH port forwarding active on port {port}.[/green]")
315
+
316
+ # Step 5: Start model server
317
+ console.print("[cyan]Starting model server on remote cluster...[/cyan]")
318
+ channel = start_model_server(
319
+ ssh_client,
320
+ repo_dir="~/.steerable-models",
321
+ hf_token=hf_token,
322
+ hf_repo=hf_repo,
323
+ port=port
324
+ )
325
+
326
+ # Trigger Open WebUI startup if Docker is running
327
+ webui_port = start_open_webui(port)
328
+
329
+ # Output the OpenAI endpoint
330
+ endpoint_url = f"https://localhost:{port}/v1/"
331
+
332
+ # Display the endpoint beautifully
333
+ endpoint_text = Text()
334
+ endpoint_text.append("\nModel server is launching!\n\n", style="bold green")
335
+ endpoint_text.append("Your OpenAI-compatible local endpoint is:\n", style="white")
336
+ endpoint_text.append(f" {endpoint_url}\n\n", style="bold underline yellow")
337
+
338
+ if webui_port:
339
+ webui_url = f"http://localhost:{webui_port}"
340
+ endpoint_text.append("Open WebUI is running at:\n", style="white")
341
+ endpoint_text.append(f" {webui_url}\n\n", style="bold underline yellow")
342
+ endpoint_text.append("Note: The model server is still downloading and loading weights.\n", style="cyan")
343
+ endpoint_text.append("The WebUI will become active once startup is complete.\n\n", style="cyan")
344
+
345
+ endpoint_text.append("Streaming remote logs below. Press ", style="white")
346
+ endpoint_text.append("Ctrl+C", style="bold red")
347
+ endpoint_text.append(" to stop the server and close the tunnel.\n", style="white")
348
+
349
+ console.print(Panel(endpoint_text, title="Endpoint Ready", border_style="cyan"))
350
+
351
+ # Keep reading remote logs and forward to local terminal
352
+ while True:
353
+ if channel.recv_ready():
354
+ data = channel.recv(4096)
355
+ if len(data) == 0:
356
+ break
357
+ sys.stdout.buffer.write(data)
358
+ sys.stdout.flush()
359
+
360
+ if channel.recv_stderr_ready():
361
+ data = channel.recv_stderr(4096)
362
+ if len(data) == 0:
363
+ break
364
+ sys.stderr.buffer.write(data)
365
+ sys.stderr.flush()
366
+
367
+ if channel.exit_status_ready():
368
+ exit_code = channel.recv_exit_status()
369
+ console.print(f"\n[yellow]Remote process exited with status {exit_code}.[/yellow]")
370
+ break
371
+
372
+ time.sleep(0.01)
373
+
374
+ sys.exit(0)
375
+
376
+ except KeyboardInterrupt:
377
+ console.print("\n[yellow]Received KeyboardInterrupt. Shutting down...[/yellow]")
378
+ sys.exit(0)
379
+ except Exception as e:
380
+ console.print(f"\n[bold red]Error: {e}[/bold red]")
381
+ sys.exit(1)
382
+ finally:
383
+ # Clean up
384
+ if channel:
385
+ console.print("[cyan]Terminating remote model server process...[/cyan]")
386
+ try:
387
+ # Send Ctrl+C (INT) to the pty
388
+ channel.send("\x03")
389
+ # Wait briefly
390
+ for _ in range(15):
391
+ if channel.exit_status_ready():
392
+ break
393
+ time.sleep(0.1)
394
+ channel.close()
395
+ except Exception:
396
+ pass
397
+
398
+ if tunnel:
399
+ console.print("[cyan]Stopping SSH tunnel...[/cyan]")
400
+ tunnel.stop()
401
+
402
+ if ssh_client:
403
+ console.print("[cyan]Closing SSH connection...[/cyan]")
404
+ ssh_client.close()
405
+
406
+ console.print("[green]* Cleaned up successfully. Goodbye![/green]")
407
+
408
+ if __name__ == "__main__":
409
+ main()
@@ -0,0 +1,100 @@
1
+ import random
2
+ import time
3
+ import paramiko
4
+ from rich.console import Console
5
+
6
+ console = Console()
7
+
8
+ def connect_ssh(host, username, password, port=22):
9
+ """
10
+ Establishes an SSH connection to the Chapman cluster.
11
+ """
12
+ client = paramiko.SSHClient()
13
+ client.set_missing_host_key_policy(paramiko.AutoAddPolicy())
14
+
15
+ try:
16
+ client.connect(
17
+ hostname=host,
18
+ port=port,
19
+ username=username,
20
+ password=password,
21
+ timeout=15,
22
+ look_for_keys=False,
23
+ allow_agent=False
24
+ )
25
+ return client
26
+ except Exception as e:
27
+ console.print(f"[red]Error connecting to {host}: {e}[/red]")
28
+ raise
29
+
30
+ def is_port_free_remote(ssh_client, port):
31
+ """
32
+ Checks if a port is free on the remote machine by attempting to bind to it via Python.
33
+ """
34
+ # Run a quick python bind test on remote
35
+ cmd = f"python3 -c \"import socket; s = socket.socket(); s.bind(('127.0.0.1', {port}))\""
36
+ stdin, stdout, stderr = ssh_client.exec_command(cmd)
37
+
38
+ # Wait for the command to finish
39
+ exit_status = stdout.channel.recv_exit_status()
40
+ return exit_status == 0
41
+
42
+ def find_free_remote_port(ssh_client, min_port=15000, max_port=60000, max_attempts=20):
43
+ """
44
+ Finds a random free port on the remote machine.
45
+ """
46
+ for _ in range(max_attempts):
47
+ port = random.randint(min_port, max_port)
48
+ if is_port_free_remote(ssh_client, port):
49
+ return port
50
+ raise RuntimeError("Could not find a free port on the remote host after several attempts.")
51
+
52
+ def setup_remote_repo(ssh_client, repo_url, target_dir="~/.steerable-models"):
53
+ """
54
+ Ensures the target repository is cloned and updated on the cluster.
55
+ """
56
+ # Shell commands to check if git repo exists, if not clone it, else pull it.
57
+ setup_cmd = f"""
58
+ if [ ! -d {target_dir}/.git ]; then
59
+ rm -rf {target_dir}
60
+ git clone {repo_url} {target_dir}
61
+ else
62
+ cd {target_dir} && git pull
63
+ fi
64
+ """
65
+
66
+ stdin, stdout, stderr = ssh_client.exec_command(setup_cmd)
67
+
68
+ # Read output to print if needed and wait for completion
69
+ exit_status = stdout.channel.recv_exit_status()
70
+ if exit_status != 0:
71
+ err_msg = stderr.read().decode().strip()
72
+ out_msg = stdout.read().decode().strip()
73
+ console.print(f"[red]Failed to setup repository on remote machine.[/red]")
74
+ if out_msg:
75
+ console.print(f"[yellow]Stdout:[/yellow]\n{out_msg}")
76
+ if err_msg:
77
+ console.print(f"[red]Stderr:[/red]\n{err_msg}")
78
+ raise RuntimeError("Failed to set up remote repository.")
79
+
80
+ def start_model_server(ssh_client, repo_dir, hf_token, hf_repo, port):
81
+ """
82
+ Starts the model server remotely using a PTY session and returns the channel.
83
+ """
84
+ transport = ssh_client.get_transport()
85
+ if not transport:
86
+ raise RuntimeError("SSH Transport is not active.")
87
+
88
+ channel = transport.open_session()
89
+ # Request a PTY to ensure child processes are terminated when the session is closed,
90
+ # and to enable real-time ANSI log formatting.
91
+ channel.get_pty()
92
+
93
+ cmd = (
94
+ f"chmod +x {repo_dir}/run.sh && "
95
+ f"cd {repo_dir} && "
96
+ f"./run.sh --hf_token '{hf_token}' --hf_repo '{hf_repo}' --port {port}"
97
+ )
98
+
99
+ channel.exec_command(cmd)
100
+ return channel
@@ -0,0 +1,80 @@
1
+ import select
2
+ import socketserver
3
+ import threading
4
+ from rich.console import Console
5
+
6
+ console = Console()
7
+
8
+ class ForwardServer(socketserver.ThreadingTCPServer):
9
+ daemon_threads = True
10
+ allow_reuse_address = True
11
+
12
+ class Handler(socketserver.BaseRequestHandler):
13
+ """
14
+ Handles local requests and tunnels them through the Paramiko SSH transport channel.
15
+ """
16
+ def handle(self):
17
+ try:
18
+ chan = self.ssh_transport.open_channel(
19
+ "direct-tcpip",
20
+ ("127.0.0.1", self.chain_port),
21
+ self.request.getpeername(),
22
+ )
23
+ except Exception as e:
24
+ # Log connection issues at verbose/debug levels if needed
25
+ return
26
+
27
+ if chan is None:
28
+ return
29
+
30
+ try:
31
+ while True:
32
+ r, w, x = select.select([self.request, chan], [], [])
33
+ if self.request in r:
34
+ data = self.request.recv(4096)
35
+ if len(data) == 0:
36
+ break
37
+ chan.sendall(data)
38
+ if chan in r:
39
+ data = chan.recv(4096)
40
+ if len(data) == 0:
41
+ break
42
+ self.request.sendall(data)
43
+ except Exception:
44
+ pass
45
+ finally:
46
+ chan.close()
47
+ self.request.close()
48
+
49
+ class SSHTunnel:
50
+ """
51
+ Manager for setting up and tearing down the SSH port-forwarding tunnel.
52
+ """
53
+ def __init__(self, ssh_client, port):
54
+ self.ssh_transport = ssh_client.get_transport()
55
+ self.port = port
56
+ self.server = None
57
+ self.thread = None
58
+
59
+ def start(self):
60
+ # Create a dynamically subclassed handler to inject the transport and target port
61
+ class CustomHandler(Handler):
62
+ ssh_transport = self.ssh_transport
63
+ chain_port = self.port
64
+
65
+ try:
66
+ self.server = ForwardServer(("127.0.0.1", self.port), CustomHandler)
67
+ self.thread = threading.Thread(target=self.server.serve_forever, daemon=True)
68
+ self.thread.start()
69
+ except Exception as e:
70
+ console.print(f"[red]Failed to start local forwarding server on port {self.port}: {e}[/red]")
71
+ raise
72
+
73
+ def stop(self):
74
+ if self.server:
75
+ self.server.shutdown()
76
+ self.server.server_close()
77
+ self.server = None
78
+ if self.thread:
79
+ self.thread.join(timeout=1.0)
80
+ self.thread = None
@@ -0,0 +1,53 @@
1
+ Metadata-Version: 2.4
2
+ Name: chapman-sip
3
+ Version: 0.1.0
4
+ Summary: A CLI tool to run steerable models on the Chapman cluster with SSH port forwarding
5
+ Author-email: Developer <developer@example.com>
6
+ Classifier: Programming Language :: Python :: 3
7
+ Classifier: License :: OSI Approved :: MIT License
8
+ Classifier: Operating System :: OS Independent
9
+ Requires-Python: >=3.8
10
+ Description-Content-Type: text/markdown
11
+ Requires-Dist: paramiko>=3.0.0
12
+ Requires-Dist: click>=8.0.0
13
+ Requires-Dist: rich>=12.0.0
14
+
15
+ # Chapman-Sip CLI
16
+
17
+ A pip-installable Python command-line utility to connect to the Chapman cluster (`dgx0.chapman.edu`), pull the `steerable-model-runner` repository, find an open port, launch the model server, and tunnel the port back to your local machine.
18
+
19
+ ## Features
20
+
21
+ - **Automated Host Setup**: Checks if the remote repository is cloned at `~/.steerable-models` on the cluster, cloning or pulling changes automatically.
22
+ - **Dynamic Port Hunting**: Generates a random port and verifies it is not currently bound on the cluster before initiating.
23
+ - **SSH Port Forwarding**: Automatically forwards the selected remote port to `localhost:[PORT]` locally.
24
+ - **Interactive Log Streaming**: Streams stderr and stdout from the model serving script in real-time.
25
+ - **Safe Shutdown**: Automatically cleans up the remote process, closes the port-forwarding server, and closes SSH channels on Ctrl+C.
26
+
27
+ ## Installation
28
+
29
+ Install in editable mode for development or directly from the source directory:
30
+
31
+ ```bash
32
+ pip install -e .
33
+ ```
34
+
35
+ ## Usage
36
+
37
+ Simply run:
38
+
39
+ ```bash
40
+ sip
41
+ ```
42
+
43
+ You will be prompted for:
44
+ - SSH username
45
+ - SSH password
46
+ - Hugging Face API token (for model download)
47
+ - Hugging Face repository name (e.g., the model repository to run)
48
+
49
+ Once connected, the utility will output the endpoint URL, e.g.:
50
+ ```
51
+ https://localhost:[PORT]/v1/
52
+ ```
53
+ Keep the CLI running while you query the endpoint. Press `Ctrl+C` to terminate the model server and close the tunnel.
@@ -0,0 +1,12 @@
1
+ README.md
2
+ pyproject.toml
3
+ src/chapman_runner/__init__.py
4
+ src/chapman_runner/cli.py
5
+ src/chapman_runner/ssh.py
6
+ src/chapman_runner/tunnel.py
7
+ src/chapman_sip.egg-info/PKG-INFO
8
+ src/chapman_sip.egg-info/SOURCES.txt
9
+ src/chapman_sip.egg-info/dependency_links.txt
10
+ src/chapman_sip.egg-info/entry_points.txt
11
+ src/chapman_sip.egg-info/requires.txt
12
+ src/chapman_sip.egg-info/top_level.txt
@@ -0,0 +1,2 @@
1
+ [console_scripts]
2
+ sip = chapman_runner.cli:main
@@ -0,0 +1,3 @@
1
+ paramiko>=3.0.0
2
+ click>=8.0.0
3
+ rich>=12.0.0
@@ -0,0 +1 @@
1
+ chapman_runner