gitarsenal-cli 1.0.7 → 1.0.8
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.
- package/package.json +1 -1
- package/python/test_modalSandboxScript.py +307 -114
package/package.json
CHANGED
@@ -1947,7 +1947,7 @@ def ssh_container_function(ssh_password, repo_url=None, repo_name=None, setup_co
|
|
1947
1947
|
|
1948
1948
|
def create_modal_ssh_container(gpu_type, repo_url=None, repo_name=None, setup_commands=None,
|
1949
1949
|
volume_name=None, timeout_minutes=60, ssh_password=None):
|
1950
|
-
"""Create a Modal SSH container with GPU support"""
|
1950
|
+
"""Create a Modal SSH container with GPU support and tunneling"""
|
1951
1951
|
|
1952
1952
|
# Generate a unique app name with timestamp to avoid conflicts
|
1953
1953
|
timestamp = datetime.datetime.now().strftime("%Y%m%d-%H%M%S")
|
@@ -1998,137 +1998,212 @@ def create_modal_ssh_container(gpu_type, repo_url=None, repo_name=None, setup_co
|
|
1998
1998
|
print(f"⚠️ Could not create default volume: {e}")
|
1999
1999
|
print("⚠️ Continuing without persistent volume")
|
2000
2000
|
volume = None
|
2001
|
-
|
2002
|
-
# Create
|
2003
|
-
|
2004
|
-
|
2005
|
-
|
2006
|
-
|
2007
|
-
|
2008
|
-
|
2009
|
-
|
2010
|
-
|
2011
|
-
|
2012
|
-
|
2013
|
-
|
2001
|
+
|
2002
|
+
# Create SSH-enabled image
|
2003
|
+
ssh_image = (
|
2004
|
+
modal.Image.debian_slim()
|
2005
|
+
.apt_install(
|
2006
|
+
"openssh-server", "sudo", "curl", "wget", "vim", "htop", "git",
|
2007
|
+
"python3", "python3-pip", "build-essential", "tmux", "screen", "nano",
|
2008
|
+
"gpg", "ca-certificates", "software-properties-common"
|
2009
|
+
)
|
2010
|
+
.pip_install("uv") # Fast Python package installer
|
2011
|
+
.run_commands(
|
2012
|
+
# Create SSH directory
|
2013
|
+
"mkdir -p /var/run/sshd",
|
2014
|
+
"mkdir -p /root/.ssh",
|
2015
|
+
"chmod 700 /root/.ssh",
|
2016
|
+
|
2017
|
+
# Configure SSH server
|
2018
|
+
"sed -i 's/#PermitRootLogin prohibit-password/PermitRootLogin yes/' /etc/ssh/sshd_config",
|
2019
|
+
"sed -i 's/#PasswordAuthentication yes/PasswordAuthentication yes/' /etc/ssh/sshd_config",
|
2020
|
+
"sed -i 's/#PubkeyAuthentication yes/PubkeyAuthentication yes/' /etc/ssh/sshd_config",
|
2021
|
+
|
2022
|
+
# SSH keep-alive settings
|
2023
|
+
"echo 'ClientAliveInterval 60' >> /etc/ssh/sshd_config",
|
2024
|
+
"echo 'ClientAliveCountMax 3' >> /etc/ssh/sshd_config",
|
2025
|
+
|
2026
|
+
# Generate SSH host keys
|
2027
|
+
"ssh-keygen -A",
|
2028
|
+
|
2029
|
+
# Set up a nice bash prompt
|
2030
|
+
"echo 'export PS1=\"\\[\\e[1;32m\\]modal:\\[\\e[1;34m\\]\\w\\[\\e[0m\\]$ \"' >> /root/.bashrc",
|
2031
|
+
)
|
2032
|
+
)
|
2033
|
+
|
2034
|
+
# Create the Modal app
|
2035
|
+
app = modal.App(app_name)
|
2036
|
+
|
2037
|
+
# Configure volumes if available
|
2038
|
+
volumes_config = {}
|
2039
|
+
if volume:
|
2040
|
+
volumes_config[volume_mount_path] = volume
|
2041
|
+
|
2042
|
+
@app.function(
|
2043
|
+
image=ssh_image,
|
2044
|
+
timeout=timeout_minutes * 60, # Convert to seconds
|
2045
|
+
gpu=gpu_spec['gpu'],
|
2046
|
+
cpu=2,
|
2047
|
+
memory=8192,
|
2048
|
+
serialized=True,
|
2049
|
+
volumes=volumes_config if volumes_config else None,
|
2050
|
+
)
|
2051
|
+
def run_ssh_container():
|
2052
|
+
"""Start SSH container with password authentication and optional setup."""
|
2053
|
+
import subprocess
|
2054
|
+
import time
|
2055
|
+
import os
|
2056
|
+
|
2057
|
+
# Set root password
|
2058
|
+
print("🔐 Setting up authentication...")
|
2059
|
+
subprocess.run(
|
2060
|
+
["bash", "-c", f"echo 'root:{ssh_password}' | chpasswd"],
|
2061
|
+
check=True
|
2014
2062
|
)
|
2015
2063
|
|
2016
|
-
|
2017
|
-
print("🚀 Starting SSH container...")
|
2064
|
+
print("✅ Root password configured")
|
2018
2065
|
|
2019
|
-
#
|
2020
|
-
|
2066
|
+
# Start SSH daemon in background
|
2067
|
+
print("🔄 Starting SSH daemon...")
|
2068
|
+
ssh_process = subprocess.Popen(["/usr/sbin/sshd", "-D"])
|
2021
2069
|
|
2022
|
-
#
|
2023
|
-
|
2024
|
-
time.sleep(10)
|
2070
|
+
# Give SSH daemon a moment to start
|
2071
|
+
time.sleep(2)
|
2025
2072
|
|
2026
|
-
#
|
2027
|
-
|
2028
|
-
|
2029
|
-
|
2030
|
-
|
2031
|
-
# Get container list to find our container
|
2032
|
-
print("🔍 Looking for container information...")
|
2033
|
-
result = subprocess.run(["modal", "container", "list", "--json"],
|
2034
|
-
capture_output=True, text=True)
|
2035
|
-
|
2036
|
-
if result.returncode == 0:
|
2037
|
-
try:
|
2038
|
-
containers = json.loads(result.stdout)
|
2039
|
-
if containers and isinstance(containers, list):
|
2040
|
-
# Find the most recent container
|
2041
|
-
for container in containers:
|
2042
|
-
if container.get("App") == "ssh-container-app":
|
2043
|
-
container_id = container.get("Container ID")
|
2044
|
-
break
|
2045
|
-
|
2046
|
-
if not container_id and containers:
|
2047
|
-
# Fall back to the first container
|
2048
|
-
container_id = containers[0].get("Container ID")
|
2049
|
-
|
2050
|
-
except json.JSONDecodeError:
|
2051
|
-
pass
|
2052
|
-
|
2053
|
-
if not container_id:
|
2054
|
-
# Try text parsing
|
2055
|
-
result = subprocess.run(["modal", "container", "list"],
|
2056
|
-
capture_output=True, text=True)
|
2057
|
-
if result.returncode == 0:
|
2058
|
-
lines = result.stdout.split('\n')
|
2059
|
-
for line in lines:
|
2060
|
-
if "ssh-container-app" in line or ('ta-' in line and '│' in line):
|
2061
|
-
parts = line.split('│')
|
2062
|
-
if len(parts) >= 2:
|
2063
|
-
possible_id = parts[1].strip()
|
2064
|
-
if possible_id.startswith('ta-'):
|
2065
|
-
container_id = possible_id
|
2066
|
-
break
|
2073
|
+
# Clone repository if provided
|
2074
|
+
if repo_url:
|
2075
|
+
repo_name_from_url = repo_name or repo_url.split('/')[-1].replace('.git', '')
|
2076
|
+
print(f"📥 Cloning repository: {repo_url}")
|
2067
2077
|
|
2068
|
-
|
2069
|
-
|
2078
|
+
try:
|
2079
|
+
subprocess.run(["git", "clone", repo_url, "/root/repo"], check=True)
|
2080
|
+
print(f"✅ Repository cloned successfully: {repo_name_from_url}")
|
2081
|
+
|
2082
|
+
# Change to repository directory
|
2083
|
+
repo_dir = f"/root/repo"
|
2084
|
+
os.chdir(repo_dir)
|
2085
|
+
print(f"📂 Changed to repository directory: {repo_dir}")
|
2070
2086
|
|
2071
|
-
#
|
2072
|
-
|
2087
|
+
# Initialize git submodules if they exist
|
2088
|
+
if os.path.exists(".gitmodules"):
|
2089
|
+
print("📦 Initializing git submodules...")
|
2090
|
+
subprocess.run(["git", "submodule", "update", "--init", "--recursive"], check=True)
|
2073
2091
|
|
2074
|
-
|
2092
|
+
except subprocess.CalledProcessError as e:
|
2093
|
+
print(f"❌ Failed to clone repository: {e}")
|
2094
|
+
|
2095
|
+
# Run setup commands if provided
|
2096
|
+
if setup_commands:
|
2097
|
+
print(f"⚙️ Running {len(setup_commands)} setup commands...")
|
2098
|
+
for i, cmd in enumerate(setup_commands, 1):
|
2099
|
+
print(f"📋 Executing command {i}/{len(setup_commands)}: {cmd}")
|
2075
2100
|
try:
|
2076
|
-
#
|
2077
|
-
|
2078
|
-
|
2079
|
-
|
2080
|
-
|
2081
|
-
|
2082
|
-
|
2083
|
-
|
2084
|
-
|
2085
|
-
|
2086
|
-
|
2087
|
-
|
2088
|
-
|
2089
|
-
print(f"
|
2090
|
-
|
2091
|
-
|
2092
|
-
|
2093
|
-
|
2101
|
+
# Run in repository directory if it exists, otherwise root
|
2102
|
+
work_dir = "/root/repo" if os.path.exists("/root/repo") else "/root"
|
2103
|
+
result = subprocess.run(
|
2104
|
+
cmd,
|
2105
|
+
shell=True,
|
2106
|
+
check=True,
|
2107
|
+
cwd=work_dir,
|
2108
|
+
capture_output=True,
|
2109
|
+
text=True
|
2110
|
+
)
|
2111
|
+
if result.stdout:
|
2112
|
+
print(f"✅ Output: {result.stdout}")
|
2113
|
+
except subprocess.CalledProcessError as e:
|
2114
|
+
print(f"❌ Command failed: {e}")
|
2115
|
+
if e.stderr:
|
2116
|
+
print(f"❌ Error: {e.stderr}")
|
2117
|
+
|
2118
|
+
# Create unencrypted tunnel for SSH (port 22)
|
2119
|
+
print("🌐 Creating SSH tunnel...")
|
2120
|
+
with modal.forward(22, unencrypted=True) as tunnel:
|
2121
|
+
# Print connection information
|
2122
|
+
host, port = tunnel.tcp_socket
|
2123
|
+
|
2124
|
+
print("\n" + "=" * 80)
|
2125
|
+
print("🎉 SSH CONTAINER IS READY!")
|
2126
|
+
print("=" * 80)
|
2127
|
+
print(f"🌐 SSH Host: {host}")
|
2128
|
+
print(f"🔌 SSH Port: {port}")
|
2129
|
+
print(f"👤 Username: root")
|
2130
|
+
print(f"🔐 Password: {ssh_password}")
|
2131
|
+
print()
|
2132
|
+
print("🔗 CONNECT USING THIS COMMAND:")
|
2133
|
+
print(f"ssh -p {port} root@{host}")
|
2134
|
+
print()
|
2135
|
+
print("💡 Connection Tips:")
|
2136
|
+
print("• Copy the password above and paste when prompted")
|
2137
|
+
print("• Use Ctrl+C to disconnect (container will keep running)")
|
2138
|
+
print("• Type 'exit' in the SSH session to close the connection")
|
2139
|
+
if repo_url:
|
2140
|
+
print("• Your repository is in: /root/repo")
|
2141
|
+
if volume:
|
2142
|
+
print(f"• Persistent storage is mounted at: {volume_mount_path}")
|
2143
|
+
print(f"• Container will auto-terminate in {timeout_minutes} minutes")
|
2144
|
+
print()
|
2145
|
+
print("🛠️ Available Tools:")
|
2146
|
+
print("• Python 3 with pip and uv package manager")
|
2147
|
+
print("• Git, curl, wget, vim, nano, htop")
|
2148
|
+
print("• tmux and screen for session management")
|
2149
|
+
print(f"• {gpu_type} GPU access")
|
2150
|
+
print("=" * 80)
|
2151
|
+
|
2152
|
+
# Keep the container running and monitor SSH daemon
|
2153
|
+
try:
|
2154
|
+
start_time = time.time()
|
2155
|
+
last_check = start_time
|
2156
|
+
|
2157
|
+
while True:
|
2158
|
+
current_time = time.time()
|
2094
2159
|
|
2095
|
-
#
|
2096
|
-
|
2097
|
-
|
2098
|
-
|
2099
|
-
|
2100
|
-
|
2101
|
-
end tell
|
2102
|
-
'''
|
2160
|
+
# Check SSH daemon every 30 seconds
|
2161
|
+
if current_time - last_check >= 30:
|
2162
|
+
if ssh_process.poll() is not None:
|
2163
|
+
print("⚠️ SSH daemon stopped unexpectedly, restarting...")
|
2164
|
+
ssh_process = subprocess.Popen(["/usr/sbin/sshd", "-D"])
|
2165
|
+
time.sleep(2)
|
2103
2166
|
|
2104
|
-
|
2105
|
-
|
2106
|
-
|
2167
|
+
# Print alive status every 5 minutes
|
2168
|
+
elapsed_minutes = (current_time - start_time) / 60
|
2169
|
+
if int(elapsed_minutes) % 5 == 0 and elapsed_minutes > 0:
|
2170
|
+
remaining_minutes = timeout_minutes - elapsed_minutes
|
2171
|
+
print(f"⏱️ Container alive for {elapsed_minutes:.0f} minutes, {remaining_minutes:.0f} minutes remaining")
|
2107
2172
|
|
2108
|
-
|
2109
|
-
print(f"⚠️ Could not open terminal window: {e}")
|
2110
|
-
print("📝 You can manually connect using the SSH command above")
|
2173
|
+
last_check = current_time
|
2111
2174
|
|
2112
|
-
|
2113
|
-
|
2114
|
-
|
2115
|
-
|
2116
|
-
|
2117
|
-
|
2118
|
-
|
2119
|
-
|
2120
|
-
|
2121
|
-
|
2122
|
-
|
2123
|
-
|
2175
|
+
time.sleep(10)
|
2176
|
+
|
2177
|
+
except KeyboardInterrupt:
|
2178
|
+
print("\n👋 Received shutdown signal...")
|
2179
|
+
print("🔄 Stopping SSH daemon...")
|
2180
|
+
ssh_process.terminate()
|
2181
|
+
ssh_process.wait()
|
2182
|
+
print("✅ Container shutting down gracefully")
|
2183
|
+
return
|
2184
|
+
|
2185
|
+
# Run the container
|
2186
|
+
print("⏳ Starting container... This may take 1-2 minutes...")
|
2187
|
+
print("📦 Building image and allocating resources...")
|
2188
|
+
|
2189
|
+
try:
|
2190
|
+
# Start the container and run the function
|
2191
|
+
with modal.enable_output():
|
2192
|
+
with app.run():
|
2193
|
+
run_ssh_container.remote()
|
2194
|
+
|
2124
2195
|
return {
|
2125
|
-
"
|
2126
|
-
"container_id": container_id,
|
2127
|
-
"app_name": "ssh-container-app",
|
2196
|
+
"app_name": app_name,
|
2128
2197
|
"ssh_password": ssh_password,
|
2129
2198
|
"volume_name": volume_name,
|
2130
2199
|
"volume_mount_path": volume_mount_path if volume else None
|
2131
2200
|
}
|
2201
|
+
except KeyboardInterrupt:
|
2202
|
+
print("\n👋 Container startup interrupted")
|
2203
|
+
return None
|
2204
|
+
except Exception as e:
|
2205
|
+
print(f"❌ Error running container: {e}")
|
2206
|
+
return None
|
2132
2207
|
|
2133
2208
|
def fetch_setup_commands_from_api(repo_url):
|
2134
2209
|
"""Fetch setup commands from the GitIngest API using real repository analysis."""
|
@@ -2475,6 +2550,124 @@ def get_setup_commands_from_local_api(repo_url, gitingest_data):
|
|
2475
2550
|
|
2476
2551
|
return None
|
2477
2552
|
|
2553
|
+
# Define a function to create and return a properly configured ssh container function
|
2554
|
+
def create_ssh_container_function(gpu_type="a10g", timeout_minutes=60, volume=None, volume_mount_path="/persistent"):
|
2555
|
+
# Create a new app for this specific container
|
2556
|
+
app_name = f"ssh-container-{datetime.datetime.now().strftime('%Y%m%d-%H%M%S')}"
|
2557
|
+
ssh_app = modal.App.lookup(app_name, create_if_missing=True)
|
2558
|
+
|
2559
|
+
# Create SSH-enabled image
|
2560
|
+
ssh_image = (
|
2561
|
+
modal.Image.debian_slim()
|
2562
|
+
.apt_install(
|
2563
|
+
"openssh-server", "sudo", "curl", "wget", "vim", "htop", "git",
|
2564
|
+
"python3", "python3-pip", "build-essential", "tmux", "screen", "nano",
|
2565
|
+
"gpg", "ca-certificates", "software-properties-common"
|
2566
|
+
)
|
2567
|
+
.pip_install("uv")
|
2568
|
+
.run_commands(
|
2569
|
+
# Create SSH directory
|
2570
|
+
"mkdir -p /var/run/sshd",
|
2571
|
+
"mkdir -p /root/.ssh",
|
2572
|
+
"chmod 700 /root/.ssh",
|
2573
|
+
|
2574
|
+
# Configure SSH server
|
2575
|
+
"sed -i 's/#PermitRootLogin prohibit-password/PermitRootLogin yes/' /etc/ssh/sshd_config",
|
2576
|
+
"sed -i 's/#PasswordAuthentication yes/PasswordAuthentication yes/' /etc/ssh/sshd_config",
|
2577
|
+
"sed -i 's/#PubkeyAuthentication yes/PubkeyAuthentication yes/' /etc/ssh/sshd_config",
|
2578
|
+
|
2579
|
+
# SSH keep-alive settings
|
2580
|
+
"echo 'ClientAliveInterval 60' >> /etc/ssh/sshd_config",
|
2581
|
+
"echo 'ClientAliveCountMax 3' >> /etc/ssh/sshd_config",
|
2582
|
+
|
2583
|
+
# Generate SSH host keys
|
2584
|
+
"ssh-keygen -A",
|
2585
|
+
|
2586
|
+
# Set up a nice bash prompt
|
2587
|
+
"echo 'export PS1=\"\\[\\e[1;32m\\]modal:\\[\\e[1;34m\\]\\w\\[\\e[0m\\]$ \"' >> /root/.bashrc",
|
2588
|
+
)
|
2589
|
+
)
|
2590
|
+
|
2591
|
+
# Setup volume mount if available
|
2592
|
+
volumes = {}
|
2593
|
+
if volume:
|
2594
|
+
volumes[volume_mount_path] = volume
|
2595
|
+
|
2596
|
+
# Define the function with the specific configuration
|
2597
|
+
@ssh_app.function(
|
2598
|
+
image=ssh_image,
|
2599
|
+
timeout=timeout_minutes * 60, # Convert to seconds
|
2600
|
+
gpu=gpu_type,
|
2601
|
+
cpu=2,
|
2602
|
+
memory=8192,
|
2603
|
+
serialized=True,
|
2604
|
+
volumes=volumes if volumes else None,
|
2605
|
+
)
|
2606
|
+
def ssh_container(ssh_password, repo_url=None, repo_name=None, setup_commands=None):
|
2607
|
+
import subprocess
|
2608
|
+
import time
|
2609
|
+
import os
|
2610
|
+
|
2611
|
+
# Set root password
|
2612
|
+
subprocess.run(["bash", "-c", f"echo 'root:{ssh_password}' | chpasswd"], check=True)
|
2613
|
+
|
2614
|
+
# Start SSH service
|
2615
|
+
subprocess.run(["service", "ssh", "start"], check=True)
|
2616
|
+
|
2617
|
+
# Setup environment
|
2618
|
+
os.environ['PS1'] = r'\[\e[1;32m\]modal:\[\e[1;34m\]\w\[\e[0m\]$ '
|
2619
|
+
|
2620
|
+
# Clone repository if provided
|
2621
|
+
if repo_url:
|
2622
|
+
repo_name_from_url = repo_name or repo_url.split('/')[-1].replace('.git', '')
|
2623
|
+
print(f"📥 Cloning repository: {repo_url}")
|
2624
|
+
|
2625
|
+
try:
|
2626
|
+
subprocess.run(["git", "clone", repo_url], check=True, cwd="/root")
|
2627
|
+
print(f"✅ Repository cloned successfully: {repo_name_from_url}")
|
2628
|
+
|
2629
|
+
# Change to repository directory
|
2630
|
+
repo_dir = f"/root/{repo_name_from_url}"
|
2631
|
+
if os.path.exists(repo_dir):
|
2632
|
+
os.chdir(repo_dir)
|
2633
|
+
print(f"📂 Changed to repository directory: {repo_dir}")
|
2634
|
+
|
2635
|
+
except subprocess.CalledProcessError as e:
|
2636
|
+
print(f"❌ Failed to clone repository: {e}")
|
2637
|
+
|
2638
|
+
# Run setup commands if provided
|
2639
|
+
if setup_commands:
|
2640
|
+
print(f"⚙️ Running {len(setup_commands)} setup commands...")
|
2641
|
+
for i, cmd in enumerate(setup_commands, 1):
|
2642
|
+
print(f"📋 Executing command {i}/{len(setup_commands)}: {cmd}")
|
2643
|
+
try:
|
2644
|
+
result = subprocess.run(cmd, shell=True, check=True,
|
2645
|
+
capture_output=True, text=True)
|
2646
|
+
if result.stdout:
|
2647
|
+
print(f"✅ Output: {result.stdout}")
|
2648
|
+
except subprocess.CalledProcessError as e:
|
2649
|
+
print(f"❌ Command failed: {e}")
|
2650
|
+
if e.stderr:
|
2651
|
+
print(f"❌ Error: {e.stderr}")
|
2652
|
+
|
2653
|
+
# Get container info
|
2654
|
+
print("🔍 Container started successfully!")
|
2655
|
+
print(f"🆔 Container ID: {os.environ.get('MODAL_TASK_ID', 'unknown')}")
|
2656
|
+
|
2657
|
+
# Keep the container running
|
2658
|
+
while True:
|
2659
|
+
time.sleep(30)
|
2660
|
+
# Check if SSH service is still running
|
2661
|
+
try:
|
2662
|
+
subprocess.run(["service", "ssh", "status"], check=True,
|
2663
|
+
capture_output=True)
|
2664
|
+
except subprocess.CalledProcessError:
|
2665
|
+
print("⚠️ SSH service stopped, restarting...")
|
2666
|
+
subprocess.run(["service", "ssh", "start"], check=True)
|
2667
|
+
|
2668
|
+
# Return the configured function
|
2669
|
+
return ssh_container, app_name
|
2670
|
+
|
2478
2671
|
if __name__ == "__main__":
|
2479
2672
|
# Parse command line arguments when script is run directly
|
2480
2673
|
import argparse
|