gitarsenal-cli 1.0.5 → 1.0.6
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 +383 -61
- package/test_modalSandboxScript.py +420 -48
package/package.json
CHANGED
@@ -8,6 +8,8 @@ import re
|
|
8
8
|
import datetime
|
9
9
|
import getpass
|
10
10
|
import requests
|
11
|
+
import secrets
|
12
|
+
import string
|
11
13
|
|
12
14
|
def handle_interactive_input(prompt, is_password=False):
|
13
15
|
"""Handle interactive input from the user with optional password masking"""
|
@@ -1808,6 +1810,324 @@ cd "{current_dir}"
|
|
1808
1810
|
"sandbox_id": sandbox_id
|
1809
1811
|
}
|
1810
1812
|
|
1813
|
+
|
1814
|
+
def handle_interactive_input(prompt, is_password=False):
|
1815
|
+
"""Handle interactive input from the user with optional password masking"""
|
1816
|
+
print("\n" + "="*60)
|
1817
|
+
print(f"{prompt}")
|
1818
|
+
print("="*60)
|
1819
|
+
|
1820
|
+
try:
|
1821
|
+
if is_password:
|
1822
|
+
user_input = getpass.getpass("Input (hidden): ").strip()
|
1823
|
+
else:
|
1824
|
+
user_input = input("Input: ").strip()
|
1825
|
+
|
1826
|
+
if not user_input:
|
1827
|
+
print("❌ No input provided.")
|
1828
|
+
return None
|
1829
|
+
print("✅ Input received successfully!")
|
1830
|
+
return user_input
|
1831
|
+
except KeyboardInterrupt:
|
1832
|
+
print("\n❌ Input cancelled by user.")
|
1833
|
+
return None
|
1834
|
+
except Exception as e:
|
1835
|
+
print(f"❌ Error getting input: {e}")
|
1836
|
+
return None
|
1837
|
+
|
1838
|
+
def generate_random_password(length=16):
|
1839
|
+
"""Generate a random password for SSH access"""
|
1840
|
+
alphabet = string.ascii_letters + string.digits + "!@#$%^&*"
|
1841
|
+
password = ''.join(secrets.choice(alphabet) for i in range(length))
|
1842
|
+
return password
|
1843
|
+
|
1844
|
+
|
1845
|
+
|
1846
|
+
def create_modal_ssh_container(gpu_type, repo_url=None, repo_name=None, setup_commands=None,
|
1847
|
+
volume_name=None, timeout_minutes=60, ssh_password=None):
|
1848
|
+
"""Create a Modal SSH container with GPU support"""
|
1849
|
+
|
1850
|
+
# Generate a unique app name with timestamp to avoid conflicts
|
1851
|
+
timestamp = datetime.datetime.now().strftime("%Y%m%d-%H%M%S")
|
1852
|
+
app_name = f"ssh-container-{timestamp}"
|
1853
|
+
|
1854
|
+
gpu_configs = {
|
1855
|
+
'A10G': {'gpu': 'a10g', 'memory': 24},
|
1856
|
+
'A100': {'gpu': 'a100', 'memory': 40},
|
1857
|
+
'H100': {'gpu': 'h100', 'memory': 80},
|
1858
|
+
'T4': {'gpu': 't4', 'memory': 16},
|
1859
|
+
'V100': {'gpu': 'v100', 'memory': 16}
|
1860
|
+
}
|
1861
|
+
|
1862
|
+
if gpu_type not in gpu_configs:
|
1863
|
+
print(f"⚠️ Unknown GPU type: {gpu_type}. Using A10G as default.")
|
1864
|
+
gpu_type = 'A10G'
|
1865
|
+
|
1866
|
+
gpu_spec = gpu_configs[gpu_type]
|
1867
|
+
print(f"🚀 Creating Modal SSH container with {gpu_spec['gpu']} GPU ({gpu_spec['memory']}GB VRAM)")
|
1868
|
+
|
1869
|
+
# Generate or use provided SSH password
|
1870
|
+
if not ssh_password:
|
1871
|
+
ssh_password = generate_random_password()
|
1872
|
+
print(f"🔐 Generated SSH password: {ssh_password}")
|
1873
|
+
|
1874
|
+
# Setup volume if specified
|
1875
|
+
volume = None
|
1876
|
+
volume_mount_path = "/persistent"
|
1877
|
+
|
1878
|
+
if volume_name:
|
1879
|
+
print(f"📦 Setting up volume: {volume_name}")
|
1880
|
+
try:
|
1881
|
+
volume = modal.Volume.from_name(volume_name, create_if_missing=True)
|
1882
|
+
print(f"✅ Volume '{volume_name}' ready for use")
|
1883
|
+
except Exception as e:
|
1884
|
+
print(f"⚠️ Could not setup volume '{volume_name}': {e}")
|
1885
|
+
print("⚠️ Continuing without persistent volume")
|
1886
|
+
volume = None
|
1887
|
+
else:
|
1888
|
+
# Create a default volume for this session
|
1889
|
+
default_volume_name = f"ssh-vol-{timestamp}"
|
1890
|
+
print(f"📦 Creating default volume: {default_volume_name}")
|
1891
|
+
try:
|
1892
|
+
volume = modal.Volume.from_name(default_volume_name, create_if_missing=True)
|
1893
|
+
volume_name = default_volume_name
|
1894
|
+
print(f"✅ Default volume '{default_volume_name}' created")
|
1895
|
+
except Exception as e:
|
1896
|
+
print(f"⚠️ Could not create default volume: {e}")
|
1897
|
+
print("⚠️ Continuing without persistent volume")
|
1898
|
+
volume = None
|
1899
|
+
|
1900
|
+
# Create SSH-enabled image
|
1901
|
+
ssh_image = (
|
1902
|
+
modal.Image.debian_slim()
|
1903
|
+
.apt_install(
|
1904
|
+
"openssh-server", "sudo", "curl", "wget", "vim", "htop", "git",
|
1905
|
+
"python3", "python3-pip", "build-essential", "tmux", "screen", "nano",
|
1906
|
+
"gpg", "ca-certificates", "software-properties-common"
|
1907
|
+
)
|
1908
|
+
.pip_install("uv") # Fast Python package installer
|
1909
|
+
.run_commands(
|
1910
|
+
# Create SSH directory
|
1911
|
+
"mkdir -p /var/run/sshd",
|
1912
|
+
"mkdir -p /root/.ssh",
|
1913
|
+
"chmod 700 /root/.ssh",
|
1914
|
+
|
1915
|
+
# Configure SSH server
|
1916
|
+
"sed -i 's/#PermitRootLogin prohibit-password/PermitRootLogin yes/' /etc/ssh/sshd_config",
|
1917
|
+
"sed -i 's/#PasswordAuthentication yes/PasswordAuthentication yes/' /etc/ssh/sshd_config",
|
1918
|
+
"sed -i 's/#PubkeyAuthentication yes/PubkeyAuthentication yes/' /etc/ssh/sshd_config",
|
1919
|
+
|
1920
|
+
# SSH keep-alive settings
|
1921
|
+
"echo 'ClientAliveInterval 60' >> /etc/ssh/sshd_config",
|
1922
|
+
"echo 'ClientAliveCountMax 3' >> /etc/ssh/sshd_config",
|
1923
|
+
|
1924
|
+
# Generate SSH host keys
|
1925
|
+
"ssh-keygen -A",
|
1926
|
+
|
1927
|
+
# Set up a nice bash prompt
|
1928
|
+
"echo 'export PS1=\"\\[\\e[1;32m\\]modal:\\[\\e[1;34m\\]\\w\\[\\e[0m\\]$ \"' >> /root/.bashrc",
|
1929
|
+
)
|
1930
|
+
)
|
1931
|
+
|
1932
|
+
# Create Modal app
|
1933
|
+
with modal.enable_output():
|
1934
|
+
print(f"🚀 Creating Modal app: {app_name}")
|
1935
|
+
app = modal.App.lookup(app_name, create_if_missing=True)
|
1936
|
+
|
1937
|
+
# Setup volume mount if available
|
1938
|
+
volumes = {}
|
1939
|
+
if volume:
|
1940
|
+
volumes[volume_mount_path] = volume
|
1941
|
+
print(f"📦 Mounting volume '{volume_name}' at {volume_mount_path}")
|
1942
|
+
|
1943
|
+
@app.function(
|
1944
|
+
image=ssh_image,
|
1945
|
+
timeout=timeout_minutes * 60, # Convert to seconds
|
1946
|
+
gpu=gpu_spec['gpu'],
|
1947
|
+
cpu=2,
|
1948
|
+
memory=8192,
|
1949
|
+
serialized=True,
|
1950
|
+
volumes=volumes if volumes else None,
|
1951
|
+
)
|
1952
|
+
def ssh_container():
|
1953
|
+
import subprocess
|
1954
|
+
import time
|
1955
|
+
import os
|
1956
|
+
|
1957
|
+
# Set root password
|
1958
|
+
subprocess.run(["bash", "-c", f"echo 'root:{ssh_password}' | chpasswd"], check=True)
|
1959
|
+
|
1960
|
+
# Start SSH service
|
1961
|
+
subprocess.run(["service", "ssh", "start"], check=True)
|
1962
|
+
|
1963
|
+
# Setup environment
|
1964
|
+
os.environ['PS1'] = r'\[\e[1;32m\]modal:\[\e[1;34m\]\w\[\e[0m\]$ '
|
1965
|
+
|
1966
|
+
# Clone repository if provided
|
1967
|
+
if repo_url:
|
1968
|
+
repo_name_from_url = repo_name or repo_url.split('/')[-1].replace('.git', '')
|
1969
|
+
print(f"📥 Cloning repository: {repo_url}")
|
1970
|
+
|
1971
|
+
try:
|
1972
|
+
subprocess.run(["git", "clone", repo_url], check=True, cwd="/root")
|
1973
|
+
print(f"✅ Repository cloned successfully: {repo_name_from_url}")
|
1974
|
+
|
1975
|
+
# Change to repository directory
|
1976
|
+
repo_dir = f"/root/{repo_name_from_url}"
|
1977
|
+
if os.path.exists(repo_dir):
|
1978
|
+
os.chdir(repo_dir)
|
1979
|
+
print(f"📂 Changed to repository directory: {repo_dir}")
|
1980
|
+
|
1981
|
+
except subprocess.CalledProcessError as e:
|
1982
|
+
print(f"❌ Failed to clone repository: {e}")
|
1983
|
+
|
1984
|
+
# Run setup commands if provided
|
1985
|
+
if setup_commands:
|
1986
|
+
print(f"⚙️ Running {len(setup_commands)} setup commands...")
|
1987
|
+
for i, cmd in enumerate(setup_commands, 1):
|
1988
|
+
print(f"📋 Executing command {i}/{len(setup_commands)}: {cmd}")
|
1989
|
+
try:
|
1990
|
+
result = subprocess.run(cmd, shell=True, check=True,
|
1991
|
+
capture_output=True, text=True)
|
1992
|
+
if result.stdout:
|
1993
|
+
print(f"✅ Output: {result.stdout}")
|
1994
|
+
except subprocess.CalledProcessError as e:
|
1995
|
+
print(f"❌ Command failed: {e}")
|
1996
|
+
if e.stderr:
|
1997
|
+
print(f"❌ Error: {e.stderr}")
|
1998
|
+
|
1999
|
+
# Get container info
|
2000
|
+
print("🔍 Container started successfully!")
|
2001
|
+
print(f"🆔 Container ID: {os.environ.get('MODAL_TASK_ID', 'unknown')}")
|
2002
|
+
|
2003
|
+
# Keep the container running
|
2004
|
+
while True:
|
2005
|
+
time.sleep(30)
|
2006
|
+
# Check if SSH service is still running
|
2007
|
+
try:
|
2008
|
+
subprocess.run(["service", "ssh", "status"], check=True,
|
2009
|
+
capture_output=True)
|
2010
|
+
except subprocess.CalledProcessError:
|
2011
|
+
print("⚠️ SSH service stopped, restarting...")
|
2012
|
+
subprocess.run(["service", "ssh", "start"], check=True)
|
2013
|
+
|
2014
|
+
# Start the container
|
2015
|
+
print("🚀 Starting SSH container...")
|
2016
|
+
|
2017
|
+
# Use spawn to run the container in the background
|
2018
|
+
container_handle = ssh_container.spawn()
|
2019
|
+
|
2020
|
+
# Wait a moment for the container to start
|
2021
|
+
print("⏳ Waiting for container to initialize...")
|
2022
|
+
time.sleep(10)
|
2023
|
+
|
2024
|
+
# Get container information
|
2025
|
+
try:
|
2026
|
+
# Try to get the container ID from Modal
|
2027
|
+
container_id = None
|
2028
|
+
|
2029
|
+
# Get container list to find our container
|
2030
|
+
print("🔍 Looking for container information...")
|
2031
|
+
result = subprocess.run(["modal", "container", "list", "--json"],
|
2032
|
+
capture_output=True, text=True)
|
2033
|
+
|
2034
|
+
if result.returncode == 0:
|
2035
|
+
try:
|
2036
|
+
containers = json.loads(result.stdout)
|
2037
|
+
if containers and isinstance(containers, list):
|
2038
|
+
# Find the most recent container
|
2039
|
+
for container in containers:
|
2040
|
+
if container.get("App") == app_name:
|
2041
|
+
container_id = container.get("Container ID")
|
2042
|
+
break
|
2043
|
+
|
2044
|
+
if not container_id and containers:
|
2045
|
+
# Fall back to the first container
|
2046
|
+
container_id = containers[0].get("Container ID")
|
2047
|
+
|
2048
|
+
except json.JSONDecodeError:
|
2049
|
+
pass
|
2050
|
+
|
2051
|
+
if not container_id:
|
2052
|
+
# Try text parsing
|
2053
|
+
result = subprocess.run(["modal", "container", "list"],
|
2054
|
+
capture_output=True, text=True)
|
2055
|
+
if result.returncode == 0:
|
2056
|
+
lines = result.stdout.split('\n')
|
2057
|
+
for line in lines:
|
2058
|
+
if app_name in line or ('ta-' in line and '│' in line):
|
2059
|
+
parts = line.split('│')
|
2060
|
+
if len(parts) >= 2:
|
2061
|
+
possible_id = parts[1].strip()
|
2062
|
+
if possible_id.startswith('ta-'):
|
2063
|
+
container_id = possible_id
|
2064
|
+
break
|
2065
|
+
|
2066
|
+
if container_id:
|
2067
|
+
print(f"📋 Container ID: {container_id}")
|
2068
|
+
|
2069
|
+
# Get the external IP for SSH access
|
2070
|
+
print("🔍 Getting container connection info...")
|
2071
|
+
|
2072
|
+
# Try to get SSH connection details
|
2073
|
+
try:
|
2074
|
+
# Modal containers typically expose SSH on port 22
|
2075
|
+
ssh_info = f"ssh root@{container_id}.modal.run"
|
2076
|
+
|
2077
|
+
print("\n" + "="*80)
|
2078
|
+
print("🚀 SSH CONTAINER READY!")
|
2079
|
+
print("="*80)
|
2080
|
+
print(f"🆔 Container ID: {container_id}")
|
2081
|
+
print(f"🔐 SSH Password: {ssh_password}")
|
2082
|
+
print(f"📱 App Name: {app_name}")
|
2083
|
+
if volume:
|
2084
|
+
print(f"💾 Volume: {volume_name} (mounted at {volume_mount_path})")
|
2085
|
+
print("\n🔗 SSH Connection:")
|
2086
|
+
print(f" {ssh_info}")
|
2087
|
+
print(f" Password: {ssh_password}")
|
2088
|
+
print("\n💡 Alternative connection methods:")
|
2089
|
+
print(f" modal container exec --pty {container_id} bash")
|
2090
|
+
print(f" modal shell {container_id}")
|
2091
|
+
print("="*80)
|
2092
|
+
|
2093
|
+
# Try to open SSH connection in a new terminal
|
2094
|
+
try:
|
2095
|
+
terminal_script = f'''
|
2096
|
+
tell application "Terminal"
|
2097
|
+
do script "echo 'Connecting to Modal SSH container...'; echo 'Password: {ssh_password}'; {ssh_info}"
|
2098
|
+
activate
|
2099
|
+
end tell
|
2100
|
+
'''
|
2101
|
+
|
2102
|
+
subprocess.run(['osascript', '-e', terminal_script],
|
2103
|
+
capture_output=True, text=True, timeout=30)
|
2104
|
+
print("✅ New terminal window opened with SSH connection")
|
2105
|
+
|
2106
|
+
except Exception as e:
|
2107
|
+
print(f"⚠️ Could not open terminal window: {e}")
|
2108
|
+
print("📝 You can manually connect using the SSH command above")
|
2109
|
+
|
2110
|
+
except Exception as e:
|
2111
|
+
print(f"⚠️ Error getting SSH connection info: {e}")
|
2112
|
+
print("📝 You can connect using:")
|
2113
|
+
print(f" modal container exec --pty {container_id} bash")
|
2114
|
+
else:
|
2115
|
+
print("⚠️ Could not determine container ID")
|
2116
|
+
print("📝 Check running containers with: modal container list")
|
2117
|
+
|
2118
|
+
except Exception as e:
|
2119
|
+
print(f"❌ Error getting container information: {e}")
|
2120
|
+
|
2121
|
+
# Return container information
|
2122
|
+
return {
|
2123
|
+
"container_handle": container_handle,
|
2124
|
+
"container_id": container_id,
|
2125
|
+
"app_name": app_name,
|
2126
|
+
"ssh_password": ssh_password,
|
2127
|
+
"volume_name": volume_name,
|
2128
|
+
"volume_mount_path": volume_mount_path if volume else None
|
2129
|
+
}
|
2130
|
+
|
1811
2131
|
def fetch_setup_commands_from_api(repo_url):
|
1812
2132
|
"""Fetch setup commands from the GitIngest API using real repository analysis."""
|
1813
2133
|
import tempfile
|
@@ -2157,7 +2477,7 @@ if __name__ == "__main__":
|
|
2157
2477
|
# Parse command line arguments when script is run directly
|
2158
2478
|
import argparse
|
2159
2479
|
|
2160
|
-
parser = argparse.ArgumentParser(description='Create a Modal
|
2480
|
+
parser = argparse.ArgumentParser(description='Create a Modal SSH container with GPU')
|
2161
2481
|
parser.add_argument('--gpu', type=str, default='A10G', help='GPU type (default: A10G)')
|
2162
2482
|
parser.add_argument('--repo-url', type=str, help='Repository URL to clone')
|
2163
2483
|
parser.add_argument('--repo-name', type=str, help='Repository name override')
|
@@ -2167,6 +2487,8 @@ if __name__ == "__main__":
|
|
2167
2487
|
parser.add_argument('--setup-script', type=str, help='Path to bash script containing setup commands')
|
2168
2488
|
parser.add_argument('--working-dir', type=str, help='Working directory for the setup script')
|
2169
2489
|
parser.add_argument('--volume-name', type=str, help='Name of the Modal volume for persistent storage')
|
2490
|
+
parser.add_argument('--timeout', type=int, default=60, help='Container timeout in minutes (default: 60)')
|
2491
|
+
parser.add_argument('--ssh-password', type=str, help='SSH password (random if not provided)')
|
2170
2492
|
parser.add_argument('--use-api', action='store_true', help='Fetch setup commands from API')
|
2171
2493
|
|
2172
2494
|
args = parser.parse_args()
|
@@ -2207,6 +2529,7 @@ if __name__ == "__main__":
|
|
2207
2529
|
for i, cmd in enumerate(setup_commands, 1):
|
2208
2530
|
print(f" {i}. {cmd}")
|
2209
2531
|
|
2532
|
+
# Load commands from file if specified
|
2210
2533
|
if args.commands_file and os.path.exists(args.commands_file):
|
2211
2534
|
try:
|
2212
2535
|
with open(args.commands_file, 'r') as f:
|
@@ -2257,7 +2580,7 @@ if __name__ == "__main__":
|
|
2257
2580
|
working_dir = args.working_dir or os.getcwd()
|
2258
2581
|
print(f"📂 Using working directory: {working_dir}")
|
2259
2582
|
|
2260
|
-
# Execute the script directly instead of through
|
2583
|
+
# Execute the script directly instead of through container
|
2261
2584
|
try:
|
2262
2585
|
print(f"🔄 Executing script directly: bash {args.setup_script} {working_dir}")
|
2263
2586
|
result = subprocess.run(['bash', args.setup_script, working_dir],
|
@@ -2276,9 +2599,9 @@ if __name__ == "__main__":
|
|
2276
2599
|
setup_commands = []
|
2277
2600
|
except Exception as e:
|
2278
2601
|
print(f"❌ Failed to execute script: {e}")
|
2279
|
-
# Fall back to running the script through
|
2602
|
+
# Fall back to running the script through container
|
2280
2603
|
setup_commands = [f"bash {args.setup_script} {working_dir}"]
|
2281
|
-
print("🔄 Falling back to running script through
|
2604
|
+
print("🔄 Falling back to running script through container")
|
2282
2605
|
else:
|
2283
2606
|
print(f"❌ Script not found at: {args.setup_script}")
|
2284
2607
|
# Try to find the script in common locations
|
@@ -2301,32 +2624,34 @@ if __name__ == "__main__":
|
|
2301
2624
|
setup_commands = []
|
2302
2625
|
|
2303
2626
|
try:
|
2304
|
-
result =
|
2305
|
-
args.gpu,
|
2306
|
-
args.repo_url,
|
2307
|
-
args.repo_name,
|
2627
|
+
result = create_modal_ssh_container(
|
2628
|
+
args.gpu,
|
2629
|
+
args.repo_url,
|
2630
|
+
args.repo_name,
|
2308
2631
|
setup_commands,
|
2309
|
-
getattr(args, 'volume_name', None)
|
2632
|
+
getattr(args, 'volume_name', None),
|
2633
|
+
args.timeout,
|
2634
|
+
args.ssh_password
|
2310
2635
|
)
|
2311
2636
|
|
2312
|
-
print("\n⏳ Keeping the
|
2637
|
+
print("\n⏳ Keeping the SSH container alive. Press Ctrl+C to exit (container will continue running)...")
|
2313
2638
|
try:
|
2314
2639
|
while True:
|
2315
2640
|
time.sleep(90)
|
2316
2641
|
print(".", end="", flush=True)
|
2317
2642
|
except KeyboardInterrupt:
|
2318
|
-
print("\n👋 Script exited. The
|
2643
|
+
print("\n👋 Script exited. The SSH container will continue running.")
|
2319
2644
|
if 'result' in locals() and result:
|
2320
2645
|
container_id = None
|
2321
|
-
|
2646
|
+
ssh_password = None
|
2647
|
+
|
2648
|
+
# Try to get container ID and SSH password from the result dictionary
|
2322
2649
|
if isinstance(result, dict):
|
2323
|
-
|
2324
|
-
|
2325
|
-
|
2326
|
-
|
2327
|
-
|
2328
|
-
elif hasattr(result, 'container_id'):
|
2329
|
-
container_id = result.container_id
|
2650
|
+
container_id = result.get('container_id')
|
2651
|
+
ssh_password = result.get('ssh_password')
|
2652
|
+
elif hasattr(result, 'container_id'):
|
2653
|
+
container_id = result.container_id
|
2654
|
+
ssh_password = getattr(result, 'ssh_password', None)
|
2330
2655
|
|
2331
2656
|
# If we still don't have the container ID, try to read it from the file
|
2332
2657
|
if not container_id:
|
@@ -2338,66 +2663,63 @@ if __name__ == "__main__":
|
|
2338
2663
|
print(f"⚠️ Could not read container ID from file: {e}")
|
2339
2664
|
|
2340
2665
|
if container_id:
|
2341
|
-
print(f"🚀
|
2342
|
-
|
2343
|
-
|
2344
|
-
|
2345
|
-
tell application "Terminal"
|
2346
|
-
do script "modal shell {container_id}"
|
2347
|
-
activate
|
2348
|
-
end tell
|
2349
|
-
'''
|
2666
|
+
print(f"🚀 SSH connection information:")
|
2667
|
+
print(f" ssh root@{container_id}.modal.run")
|
2668
|
+
if ssh_password:
|
2669
|
+
print(f" Password: {ssh_password}")
|
2350
2670
|
|
2671
|
+
# Try to open a new terminal window with SSH connection
|
2351
2672
|
try:
|
2352
|
-
|
2673
|
+
terminal_script = f'''
|
2674
|
+
tell application "Terminal"
|
2675
|
+
do script "echo 'Connecting to Modal SSH container...'; echo 'Password: {ssh_password or 'unknown'}'; ssh root@{container_id}.modal.run"
|
2676
|
+
activate
|
2677
|
+
end tell
|
2678
|
+
'''
|
2679
|
+
|
2353
2680
|
subprocess.run(['osascript', '-e', terminal_script],
|
2354
2681
|
capture_output=True, text=True, timeout=30)
|
2355
|
-
print("✅ New terminal window opened
|
2682
|
+
print("✅ New terminal window opened with SSH connection")
|
2683
|
+
|
2356
2684
|
except Exception as e:
|
2357
2685
|
print(f"⚠️ Failed to open terminal window: {e}")
|
2358
2686
|
|
2359
|
-
# Try alternative approach with iTerm2
|
2360
|
-
print("🔄 Trying with iTerm2 instead...")
|
2361
|
-
iterm_script = f'''
|
2362
|
-
tell application "iTerm"
|
2363
|
-
create window with default profile
|
2364
|
-
tell current session of current window
|
2365
|
-
write text "modal shell {container_id}"
|
2366
|
-
end tell
|
2367
|
-
end tell
|
2368
|
-
'''
|
2369
|
-
|
2687
|
+
# Try alternative approach with iTerm2
|
2370
2688
|
try:
|
2371
|
-
|
2372
|
-
|
2373
|
-
|
2689
|
+
iterm_script = f'''
|
2690
|
+
tell application "iTerm"
|
2691
|
+
create window with default profile
|
2692
|
+
tell current session of current window
|
2693
|
+
write text "echo 'Connecting to Modal SSH container...'; echo 'Password: {ssh_password or 'unknown'}'; ssh root@{container_id}.modal.run"
|
2694
|
+
end tell
|
2695
|
+
end tell
|
2696
|
+
'''
|
2697
|
+
|
2698
|
+
subprocess.run(['osascript', '-e', iterm_script],
|
2699
|
+
capture_output=True, text=True, timeout=30)
|
2700
|
+
print("✅ New iTerm2 window opened with SSH connection")
|
2701
|
+
|
2374
2702
|
except Exception as e2:
|
2375
|
-
# As a last resort, try to run the modal shell command directly
|
2376
2703
|
print(f"⚠️ Failed to open iTerm2 window: {e2}")
|
2377
|
-
print("
|
2378
|
-
|
2379
|
-
|
2380
|
-
|
2381
|
-
|
2382
|
-
|
2383
|
-
subprocess.run(shell_cmd, shell=True)
|
2384
|
-
print("✅ Shell session completed")
|
2385
|
-
except Exception as e3:
|
2386
|
-
print(f"❌ Error starting shell: {e3}")
|
2387
|
-
print("📝 You can manually connect using:")
|
2388
|
-
print(f" modal shell {container_id}")
|
2704
|
+
print("📝 You can manually connect using:")
|
2705
|
+
print(f" ssh root@{container_id}.modal.run")
|
2706
|
+
if ssh_password:
|
2707
|
+
print(f" Password: {ssh_password}")
|
2708
|
+
print(" Or use Modal exec:")
|
2709
|
+
print(f" modal container exec --pty {container_id} bash")
|
2389
2710
|
else:
|
2390
2711
|
print("⚠️ Could not determine container ID")
|
2391
2712
|
print("📝 You can manually connect using:")
|
2392
2713
|
print(" modal container list")
|
2393
|
-
print(" modal
|
2714
|
+
print(" modal container exec --pty <CONTAINER_ID> bash")
|
2394
2715
|
|
2395
2716
|
# Exit cleanly
|
2396
2717
|
sys.exit(0)
|
2718
|
+
|
2397
2719
|
except KeyboardInterrupt:
|
2398
|
-
# Handle Ctrl+C during
|
2399
|
-
print("\n👋 Script interrupted during
|
2400
|
-
print("📝 You may need to check if a
|
2720
|
+
# Handle Ctrl+C during container creation
|
2721
|
+
print("\n👋 Script interrupted during container creation.")
|
2722
|
+
print("📝 You may need to check if a container was created with: modal container list")
|
2401
2723
|
sys.exit(0)
|
2402
2724
|
except Exception as e:
|
2403
2725
|
print(f"❌ Error: {e}")
|
@@ -8,6 +8,8 @@ import re
|
|
8
8
|
import datetime
|
9
9
|
import getpass
|
10
10
|
import requests
|
11
|
+
import secrets
|
12
|
+
import string
|
11
13
|
|
12
14
|
def handle_interactive_input(prompt, is_password=False):
|
13
15
|
"""Handle interactive input from the user with optional password masking"""
|
@@ -1808,6 +1810,324 @@ cd "{current_dir}"
|
|
1808
1810
|
"sandbox_id": sandbox_id
|
1809
1811
|
}
|
1810
1812
|
|
1813
|
+
|
1814
|
+
def handle_interactive_input(prompt, is_password=False):
|
1815
|
+
"""Handle interactive input from the user with optional password masking"""
|
1816
|
+
print("\n" + "="*60)
|
1817
|
+
print(f"{prompt}")
|
1818
|
+
print("="*60)
|
1819
|
+
|
1820
|
+
try:
|
1821
|
+
if is_password:
|
1822
|
+
user_input = getpass.getpass("Input (hidden): ").strip()
|
1823
|
+
else:
|
1824
|
+
user_input = input("Input: ").strip()
|
1825
|
+
|
1826
|
+
if not user_input:
|
1827
|
+
print("❌ No input provided.")
|
1828
|
+
return None
|
1829
|
+
print("✅ Input received successfully!")
|
1830
|
+
return user_input
|
1831
|
+
except KeyboardInterrupt:
|
1832
|
+
print("\n❌ Input cancelled by user.")
|
1833
|
+
return None
|
1834
|
+
except Exception as e:
|
1835
|
+
print(f"❌ Error getting input: {e}")
|
1836
|
+
return None
|
1837
|
+
|
1838
|
+
def generate_random_password(length=16):
|
1839
|
+
"""Generate a random password for SSH access"""
|
1840
|
+
alphabet = string.ascii_letters + string.digits + "!@#$%^&*"
|
1841
|
+
password = ''.join(secrets.choice(alphabet) for i in range(length))
|
1842
|
+
return password
|
1843
|
+
|
1844
|
+
|
1845
|
+
|
1846
|
+
def create_modal_ssh_container(gpu_type, repo_url=None, repo_name=None, setup_commands=None,
|
1847
|
+
volume_name=None, timeout_minutes=60, ssh_password=None):
|
1848
|
+
"""Create a Modal SSH container with GPU support"""
|
1849
|
+
|
1850
|
+
# Generate a unique app name with timestamp to avoid conflicts
|
1851
|
+
timestamp = datetime.datetime.now().strftime("%Y%m%d-%H%M%S")
|
1852
|
+
app_name = f"ssh-container-{timestamp}"
|
1853
|
+
|
1854
|
+
gpu_configs = {
|
1855
|
+
'A10G': {'gpu': 'a10g', 'memory': 24},
|
1856
|
+
'A100': {'gpu': 'a100', 'memory': 40},
|
1857
|
+
'H100': {'gpu': 'h100', 'memory': 80},
|
1858
|
+
'T4': {'gpu': 't4', 'memory': 16},
|
1859
|
+
'V100': {'gpu': 'v100', 'memory': 16}
|
1860
|
+
}
|
1861
|
+
|
1862
|
+
if gpu_type not in gpu_configs:
|
1863
|
+
print(f"⚠️ Unknown GPU type: {gpu_type}. Using A10G as default.")
|
1864
|
+
gpu_type = 'A10G'
|
1865
|
+
|
1866
|
+
gpu_spec = gpu_configs[gpu_type]
|
1867
|
+
print(f"🚀 Creating Modal SSH container with {gpu_spec['gpu']} GPU ({gpu_spec['memory']}GB VRAM)")
|
1868
|
+
|
1869
|
+
# Generate or use provided SSH password
|
1870
|
+
if not ssh_password:
|
1871
|
+
ssh_password = generate_random_password()
|
1872
|
+
print(f"🔐 Generated SSH password: {ssh_password}")
|
1873
|
+
|
1874
|
+
# Setup volume if specified
|
1875
|
+
volume = None
|
1876
|
+
volume_mount_path = "/persistent"
|
1877
|
+
|
1878
|
+
if volume_name:
|
1879
|
+
print(f"📦 Setting up volume: {volume_name}")
|
1880
|
+
try:
|
1881
|
+
volume = modal.Volume.from_name(volume_name, create_if_missing=True)
|
1882
|
+
print(f"✅ Volume '{volume_name}' ready for use")
|
1883
|
+
except Exception as e:
|
1884
|
+
print(f"⚠️ Could not setup volume '{volume_name}': {e}")
|
1885
|
+
print("⚠️ Continuing without persistent volume")
|
1886
|
+
volume = None
|
1887
|
+
else:
|
1888
|
+
# Create a default volume for this session
|
1889
|
+
default_volume_name = f"ssh-vol-{timestamp}"
|
1890
|
+
print(f"📦 Creating default volume: {default_volume_name}")
|
1891
|
+
try:
|
1892
|
+
volume = modal.Volume.from_name(default_volume_name, create_if_missing=True)
|
1893
|
+
volume_name = default_volume_name
|
1894
|
+
print(f"✅ Default volume '{default_volume_name}' created")
|
1895
|
+
except Exception as e:
|
1896
|
+
print(f"⚠️ Could not create default volume: {e}")
|
1897
|
+
print("⚠️ Continuing without persistent volume")
|
1898
|
+
volume = None
|
1899
|
+
|
1900
|
+
# Create SSH-enabled image
|
1901
|
+
ssh_image = (
|
1902
|
+
modal.Image.debian_slim()
|
1903
|
+
.apt_install(
|
1904
|
+
"openssh-server", "sudo", "curl", "wget", "vim", "htop", "git",
|
1905
|
+
"python3", "python3-pip", "build-essential", "tmux", "screen", "nano",
|
1906
|
+
"gpg", "ca-certificates", "software-properties-common"
|
1907
|
+
)
|
1908
|
+
.pip_install("uv") # Fast Python package installer
|
1909
|
+
.run_commands(
|
1910
|
+
# Create SSH directory
|
1911
|
+
"mkdir -p /var/run/sshd",
|
1912
|
+
"mkdir -p /root/.ssh",
|
1913
|
+
"chmod 700 /root/.ssh",
|
1914
|
+
|
1915
|
+
# Configure SSH server
|
1916
|
+
"sed -i 's/#PermitRootLogin prohibit-password/PermitRootLogin yes/' /etc/ssh/sshd_config",
|
1917
|
+
"sed -i 's/#PasswordAuthentication yes/PasswordAuthentication yes/' /etc/ssh/sshd_config",
|
1918
|
+
"sed -i 's/#PubkeyAuthentication yes/PubkeyAuthentication yes/' /etc/ssh/sshd_config",
|
1919
|
+
|
1920
|
+
# SSH keep-alive settings
|
1921
|
+
"echo 'ClientAliveInterval 60' >> /etc/ssh/sshd_config",
|
1922
|
+
"echo 'ClientAliveCountMax 3' >> /etc/ssh/sshd_config",
|
1923
|
+
|
1924
|
+
# Generate SSH host keys
|
1925
|
+
"ssh-keygen -A",
|
1926
|
+
|
1927
|
+
# Set up a nice bash prompt
|
1928
|
+
"echo 'export PS1=\"\\[\\e[1;32m\\]modal:\\[\\e[1;34m\\]\\w\\[\\e[0m\\]$ \"' >> /root/.bashrc",
|
1929
|
+
)
|
1930
|
+
)
|
1931
|
+
|
1932
|
+
# Create Modal app
|
1933
|
+
with modal.enable_output():
|
1934
|
+
print(f"🚀 Creating Modal app: {app_name}")
|
1935
|
+
app = modal.App.lookup(app_name, create_if_missing=True)
|
1936
|
+
|
1937
|
+
# Setup volume mount if available
|
1938
|
+
volumes = {}
|
1939
|
+
if volume:
|
1940
|
+
volumes[volume_mount_path] = volume
|
1941
|
+
print(f"📦 Mounting volume '{volume_name}' at {volume_mount_path}")
|
1942
|
+
|
1943
|
+
@app.function(
|
1944
|
+
image=ssh_image,
|
1945
|
+
timeout=timeout_minutes * 60, # Convert to seconds
|
1946
|
+
gpu=gpu_spec['gpu'],
|
1947
|
+
cpu=2,
|
1948
|
+
memory=8192,
|
1949
|
+
serialized=True,
|
1950
|
+
volumes=volumes if volumes else None,
|
1951
|
+
)
|
1952
|
+
def ssh_container():
|
1953
|
+
import subprocess
|
1954
|
+
import time
|
1955
|
+
import os
|
1956
|
+
|
1957
|
+
# Set root password
|
1958
|
+
subprocess.run(["bash", "-c", f"echo 'root:{ssh_password}' | chpasswd"], check=True)
|
1959
|
+
|
1960
|
+
# Start SSH service
|
1961
|
+
subprocess.run(["service", "ssh", "start"], check=True)
|
1962
|
+
|
1963
|
+
# Setup environment
|
1964
|
+
os.environ['PS1'] = r'\[\e[1;32m\]modal:\[\e[1;34m\]\w\[\e[0m\]$ '
|
1965
|
+
|
1966
|
+
# Clone repository if provided
|
1967
|
+
if repo_url:
|
1968
|
+
repo_name_from_url = repo_name or repo_url.split('/')[-1].replace('.git', '')
|
1969
|
+
print(f"📥 Cloning repository: {repo_url}")
|
1970
|
+
|
1971
|
+
try:
|
1972
|
+
subprocess.run(["git", "clone", repo_url], check=True, cwd="/root")
|
1973
|
+
print(f"✅ Repository cloned successfully: {repo_name_from_url}")
|
1974
|
+
|
1975
|
+
# Change to repository directory
|
1976
|
+
repo_dir = f"/root/{repo_name_from_url}"
|
1977
|
+
if os.path.exists(repo_dir):
|
1978
|
+
os.chdir(repo_dir)
|
1979
|
+
print(f"📂 Changed to repository directory: {repo_dir}")
|
1980
|
+
|
1981
|
+
except subprocess.CalledProcessError as e:
|
1982
|
+
print(f"❌ Failed to clone repository: {e}")
|
1983
|
+
|
1984
|
+
# Run setup commands if provided
|
1985
|
+
if setup_commands:
|
1986
|
+
print(f"⚙️ Running {len(setup_commands)} setup commands...")
|
1987
|
+
for i, cmd in enumerate(setup_commands, 1):
|
1988
|
+
print(f"📋 Executing command {i}/{len(setup_commands)}: {cmd}")
|
1989
|
+
try:
|
1990
|
+
result = subprocess.run(cmd, shell=True, check=True,
|
1991
|
+
capture_output=True, text=True)
|
1992
|
+
if result.stdout:
|
1993
|
+
print(f"✅ Output: {result.stdout}")
|
1994
|
+
except subprocess.CalledProcessError as e:
|
1995
|
+
print(f"❌ Command failed: {e}")
|
1996
|
+
if e.stderr:
|
1997
|
+
print(f"❌ Error: {e.stderr}")
|
1998
|
+
|
1999
|
+
# Get container info
|
2000
|
+
print("🔍 Container started successfully!")
|
2001
|
+
print(f"🆔 Container ID: {os.environ.get('MODAL_TASK_ID', 'unknown')}")
|
2002
|
+
|
2003
|
+
# Keep the container running
|
2004
|
+
while True:
|
2005
|
+
time.sleep(30)
|
2006
|
+
# Check if SSH service is still running
|
2007
|
+
try:
|
2008
|
+
subprocess.run(["service", "ssh", "status"], check=True,
|
2009
|
+
capture_output=True)
|
2010
|
+
except subprocess.CalledProcessError:
|
2011
|
+
print("⚠️ SSH service stopped, restarting...")
|
2012
|
+
subprocess.run(["service", "ssh", "start"], check=True)
|
2013
|
+
|
2014
|
+
# Start the container
|
2015
|
+
print("🚀 Starting SSH container...")
|
2016
|
+
|
2017
|
+
# Use spawn to run the container in the background
|
2018
|
+
container_handle = ssh_container.spawn()
|
2019
|
+
|
2020
|
+
# Wait a moment for the container to start
|
2021
|
+
print("⏳ Waiting for container to initialize...")
|
2022
|
+
time.sleep(10)
|
2023
|
+
|
2024
|
+
# Get container information
|
2025
|
+
try:
|
2026
|
+
# Try to get the container ID from Modal
|
2027
|
+
container_id = None
|
2028
|
+
|
2029
|
+
# Get container list to find our container
|
2030
|
+
print("🔍 Looking for container information...")
|
2031
|
+
result = subprocess.run(["modal", "container", "list", "--json"],
|
2032
|
+
capture_output=True, text=True)
|
2033
|
+
|
2034
|
+
if result.returncode == 0:
|
2035
|
+
try:
|
2036
|
+
containers = json.loads(result.stdout)
|
2037
|
+
if containers and isinstance(containers, list):
|
2038
|
+
# Find the most recent container
|
2039
|
+
for container in containers:
|
2040
|
+
if container.get("App") == app_name:
|
2041
|
+
container_id = container.get("Container ID")
|
2042
|
+
break
|
2043
|
+
|
2044
|
+
if not container_id and containers:
|
2045
|
+
# Fall back to the first container
|
2046
|
+
container_id = containers[0].get("Container ID")
|
2047
|
+
|
2048
|
+
except json.JSONDecodeError:
|
2049
|
+
pass
|
2050
|
+
|
2051
|
+
if not container_id:
|
2052
|
+
# Try text parsing
|
2053
|
+
result = subprocess.run(["modal", "container", "list"],
|
2054
|
+
capture_output=True, text=True)
|
2055
|
+
if result.returncode == 0:
|
2056
|
+
lines = result.stdout.split('\n')
|
2057
|
+
for line in lines:
|
2058
|
+
if app_name in line or ('ta-' in line and '│' in line):
|
2059
|
+
parts = line.split('│')
|
2060
|
+
if len(parts) >= 2:
|
2061
|
+
possible_id = parts[1].strip()
|
2062
|
+
if possible_id.startswith('ta-'):
|
2063
|
+
container_id = possible_id
|
2064
|
+
break
|
2065
|
+
|
2066
|
+
if container_id:
|
2067
|
+
print(f"📋 Container ID: {container_id}")
|
2068
|
+
|
2069
|
+
# Get the external IP for SSH access
|
2070
|
+
print("🔍 Getting container connection info...")
|
2071
|
+
|
2072
|
+
# Try to get SSH connection details
|
2073
|
+
try:
|
2074
|
+
# Modal containers typically expose SSH on port 22
|
2075
|
+
ssh_info = f"ssh root@{container_id}.modal.run"
|
2076
|
+
|
2077
|
+
print("\n" + "="*80)
|
2078
|
+
print("🚀 SSH CONTAINER READY!")
|
2079
|
+
print("="*80)
|
2080
|
+
print(f"🆔 Container ID: {container_id}")
|
2081
|
+
print(f"🔐 SSH Password: {ssh_password}")
|
2082
|
+
print(f"📱 App Name: {app_name}")
|
2083
|
+
if volume:
|
2084
|
+
print(f"💾 Volume: {volume_name} (mounted at {volume_mount_path})")
|
2085
|
+
print("\n🔗 SSH Connection:")
|
2086
|
+
print(f" {ssh_info}")
|
2087
|
+
print(f" Password: {ssh_password}")
|
2088
|
+
print("\n💡 Alternative connection methods:")
|
2089
|
+
print(f" modal container exec --pty {container_id} bash")
|
2090
|
+
print(f" modal shell {container_id}")
|
2091
|
+
print("="*80)
|
2092
|
+
|
2093
|
+
# Try to open SSH connection in a new terminal
|
2094
|
+
try:
|
2095
|
+
terminal_script = f'''
|
2096
|
+
tell application "Terminal"
|
2097
|
+
do script "echo 'Connecting to Modal SSH container...'; echo 'Password: {ssh_password}'; {ssh_info}"
|
2098
|
+
activate
|
2099
|
+
end tell
|
2100
|
+
'''
|
2101
|
+
|
2102
|
+
subprocess.run(['osascript', '-e', terminal_script],
|
2103
|
+
capture_output=True, text=True, timeout=30)
|
2104
|
+
print("✅ New terminal window opened with SSH connection")
|
2105
|
+
|
2106
|
+
except Exception as e:
|
2107
|
+
print(f"⚠️ Could not open terminal window: {e}")
|
2108
|
+
print("📝 You can manually connect using the SSH command above")
|
2109
|
+
|
2110
|
+
except Exception as e:
|
2111
|
+
print(f"⚠️ Error getting SSH connection info: {e}")
|
2112
|
+
print("📝 You can connect using:")
|
2113
|
+
print(f" modal container exec --pty {container_id} bash")
|
2114
|
+
else:
|
2115
|
+
print("⚠️ Could not determine container ID")
|
2116
|
+
print("📝 Check running containers with: modal container list")
|
2117
|
+
|
2118
|
+
except Exception as e:
|
2119
|
+
print(f"❌ Error getting container information: {e}")
|
2120
|
+
|
2121
|
+
# Return container information
|
2122
|
+
return {
|
2123
|
+
"container_handle": container_handle,
|
2124
|
+
"container_id": container_id,
|
2125
|
+
"app_name": app_name,
|
2126
|
+
"ssh_password": ssh_password,
|
2127
|
+
"volume_name": volume_name,
|
2128
|
+
"volume_mount_path": volume_mount_path if volume else None
|
2129
|
+
}
|
2130
|
+
|
1811
2131
|
def fetch_setup_commands_from_api(repo_url):
|
1812
2132
|
"""Fetch setup commands from the GitIngest API using real repository analysis."""
|
1813
2133
|
import tempfile
|
@@ -2157,7 +2477,7 @@ if __name__ == "__main__":
|
|
2157
2477
|
# Parse command line arguments when script is run directly
|
2158
2478
|
import argparse
|
2159
2479
|
|
2160
|
-
parser = argparse.ArgumentParser(description='Create a Modal
|
2480
|
+
parser = argparse.ArgumentParser(description='Create a Modal SSH container with GPU')
|
2161
2481
|
parser.add_argument('--gpu', type=str, default='A10G', help='GPU type (default: A10G)')
|
2162
2482
|
parser.add_argument('--repo-url', type=str, help='Repository URL to clone')
|
2163
2483
|
parser.add_argument('--repo-name', type=str, help='Repository name override')
|
@@ -2167,6 +2487,8 @@ if __name__ == "__main__":
|
|
2167
2487
|
parser.add_argument('--setup-script', type=str, help='Path to bash script containing setup commands')
|
2168
2488
|
parser.add_argument('--working-dir', type=str, help='Working directory for the setup script')
|
2169
2489
|
parser.add_argument('--volume-name', type=str, help='Name of the Modal volume for persistent storage')
|
2490
|
+
parser.add_argument('--timeout', type=int, default=60, help='Container timeout in minutes (default: 60)')
|
2491
|
+
parser.add_argument('--ssh-password', type=str, help='SSH password (random if not provided)')
|
2170
2492
|
parser.add_argument('--use-api', action='store_true', help='Fetch setup commands from API')
|
2171
2493
|
|
2172
2494
|
args = parser.parse_args()
|
@@ -2207,6 +2529,7 @@ if __name__ == "__main__":
|
|
2207
2529
|
for i, cmd in enumerate(setup_commands, 1):
|
2208
2530
|
print(f" {i}. {cmd}")
|
2209
2531
|
|
2532
|
+
# Load commands from file if specified
|
2210
2533
|
if args.commands_file and os.path.exists(args.commands_file):
|
2211
2534
|
try:
|
2212
2535
|
with open(args.commands_file, 'r') as f:
|
@@ -2257,7 +2580,7 @@ if __name__ == "__main__":
|
|
2257
2580
|
working_dir = args.working_dir or os.getcwd()
|
2258
2581
|
print(f"📂 Using working directory: {working_dir}")
|
2259
2582
|
|
2260
|
-
# Execute the script directly instead of through
|
2583
|
+
# Execute the script directly instead of through container
|
2261
2584
|
try:
|
2262
2585
|
print(f"🔄 Executing script directly: bash {args.setup_script} {working_dir}")
|
2263
2586
|
result = subprocess.run(['bash', args.setup_script, working_dir],
|
@@ -2276,9 +2599,9 @@ if __name__ == "__main__":
|
|
2276
2599
|
setup_commands = []
|
2277
2600
|
except Exception as e:
|
2278
2601
|
print(f"❌ Failed to execute script: {e}")
|
2279
|
-
# Fall back to running the script through
|
2602
|
+
# Fall back to running the script through container
|
2280
2603
|
setup_commands = [f"bash {args.setup_script} {working_dir}"]
|
2281
|
-
print("🔄 Falling back to running script through
|
2604
|
+
print("🔄 Falling back to running script through container")
|
2282
2605
|
else:
|
2283
2606
|
print(f"❌ Script not found at: {args.setup_script}")
|
2284
2607
|
# Try to find the script in common locations
|
@@ -2301,54 +2624,103 @@ if __name__ == "__main__":
|
|
2301
2624
|
setup_commands = []
|
2302
2625
|
|
2303
2626
|
try:
|
2304
|
-
result =
|
2305
|
-
args.gpu,
|
2306
|
-
args.repo_url,
|
2307
|
-
args.repo_name,
|
2627
|
+
result = create_modal_ssh_container(
|
2628
|
+
args.gpu,
|
2629
|
+
args.repo_url,
|
2630
|
+
args.repo_name,
|
2308
2631
|
setup_commands,
|
2309
|
-
getattr(args, 'volume_name', None)
|
2632
|
+
getattr(args, 'volume_name', None),
|
2633
|
+
args.timeout,
|
2634
|
+
args.ssh_password
|
2310
2635
|
)
|
2311
2636
|
|
2312
|
-
print("\n⏳ Keeping the
|
2313
|
-
|
2314
|
-
|
2315
|
-
|
2316
|
-
|
2317
|
-
|
2318
|
-
|
2319
|
-
|
2320
|
-
|
2321
|
-
|
2322
|
-
|
2323
|
-
#
|
2324
|
-
|
2325
|
-
|
2326
|
-
|
2637
|
+
print("\n⏳ Keeping the SSH container alive. Press Ctrl+C to exit (container will continue running)...")
|
2638
|
+
try:
|
2639
|
+
while True:
|
2640
|
+
time.sleep(90)
|
2641
|
+
print(".", end="", flush=True)
|
2642
|
+
except KeyboardInterrupt:
|
2643
|
+
print("\n👋 Script exited. The SSH container will continue running.")
|
2644
|
+
if 'result' in locals() and result:
|
2645
|
+
container_id = None
|
2646
|
+
ssh_password = None
|
2647
|
+
|
2648
|
+
# Try to get container ID and SSH password from the result dictionary
|
2649
|
+
if isinstance(result, dict):
|
2650
|
+
container_id = result.get('container_id')
|
2651
|
+
ssh_password = result.get('ssh_password')
|
2327
2652
|
elif hasattr(result, 'container_id'):
|
2328
2653
|
container_id = result.container_id
|
2654
|
+
ssh_password = getattr(result, 'ssh_password', None)
|
2655
|
+
|
2656
|
+
# If we still don't have the container ID, try to read it from the file
|
2657
|
+
if not container_id:
|
2658
|
+
try:
|
2659
|
+
with open(os.path.expanduser("~/.modal_last_container_id"), "r") as f:
|
2660
|
+
container_id = f.read().strip()
|
2661
|
+
print(f"📋 Retrieved container ID from file: {container_id}")
|
2662
|
+
except Exception as e:
|
2663
|
+
print(f"⚠️ Could not read container ID from file: {e}")
|
2664
|
+
|
2665
|
+
if container_id:
|
2666
|
+
print(f"🚀 SSH connection information:")
|
2667
|
+
print(f" ssh root@{container_id}.modal.run")
|
2668
|
+
if ssh_password:
|
2669
|
+
print(f" Password: {ssh_password}")
|
2670
|
+
|
2671
|
+
# Try to open a new terminal window with SSH connection
|
2672
|
+
try:
|
2673
|
+
terminal_script = f'''
|
2674
|
+
tell application "Terminal"
|
2675
|
+
do script "echo 'Connecting to Modal SSH container...'; echo 'Password: {ssh_password or 'unknown'}'; ssh root@{container_id}.modal.run"
|
2676
|
+
activate
|
2677
|
+
end tell
|
2678
|
+
'''
|
2679
|
+
|
2680
|
+
subprocess.run(['osascript', '-e', terminal_script],
|
2681
|
+
capture_output=True, text=True, timeout=30)
|
2682
|
+
print("✅ New terminal window opened with SSH connection")
|
2683
|
+
|
2684
|
+
except Exception as e:
|
2685
|
+
print(f"⚠️ Failed to open terminal window: {e}")
|
2686
|
+
|
2687
|
+
# Try alternative approach with iTerm2
|
2688
|
+
try:
|
2689
|
+
iterm_script = f'''
|
2690
|
+
tell application "iTerm"
|
2691
|
+
create window with default profile
|
2692
|
+
tell current session of current window
|
2693
|
+
write text "echo 'Connecting to Modal SSH container...'; echo 'Password: {ssh_password or 'unknown'}'; ssh root@{container_id}.modal.run"
|
2694
|
+
end tell
|
2695
|
+
end tell
|
2696
|
+
'''
|
2697
|
+
|
2698
|
+
subprocess.run(['osascript', '-e', iterm_script],
|
2699
|
+
capture_output=True, text=True, timeout=30)
|
2700
|
+
print("✅ New iTerm2 window opened with SSH connection")
|
2701
|
+
|
2702
|
+
except Exception as e2:
|
2703
|
+
print(f"⚠️ Failed to open iTerm2 window: {e2}")
|
2704
|
+
print("📝 You can manually connect using:")
|
2705
|
+
print(f" ssh root@{container_id}.modal.run")
|
2706
|
+
if ssh_password:
|
2707
|
+
print(f" Password: {ssh_password}")
|
2708
|
+
print(" Or use Modal exec:")
|
2709
|
+
print(f" modal container exec --pty {container_id} bash")
|
2710
|
+
else:
|
2711
|
+
print("⚠️ Could not determine container ID")
|
2712
|
+
print("📝 You can manually connect using:")
|
2713
|
+
print(" modal container list")
|
2714
|
+
print(" modal container exec --pty <CONTAINER_ID> bash")
|
2329
2715
|
|
2330
|
-
#
|
2331
|
-
|
2332
|
-
try:
|
2333
|
-
with open(os.path.expanduser("~/.modal_last_container_id"), "r") as f:
|
2334
|
-
container_id = f.read().strip()
|
2335
|
-
print(f"📋 Retrieved container ID from file: {container_id}")
|
2336
|
-
except Exception as e:
|
2337
|
-
print(f"⚠️ Could not read container ID from file: {e}")
|
2716
|
+
# Exit cleanly
|
2717
|
+
sys.exit(0)
|
2338
2718
|
|
2339
|
-
|
2340
|
-
|
2341
|
-
|
2342
|
-
|
2343
|
-
|
2344
|
-
|
2345
|
-
|
2346
|
-
|
2347
|
-
print(f"❌ Error starting shell: {e}")
|
2348
|
-
print(f"📝 You can manually connect using:")
|
2349
|
-
print(f" modal shell {container_id}")
|
2350
|
-
else:
|
2351
|
-
print("⚠️ Could not determine container ID")
|
2352
|
-
print("📝 You can manually connect using:")
|
2353
|
-
print(" modal container list")
|
2354
|
-
print(" modal shell <CONTAINER_ID>")
|
2719
|
+
except KeyboardInterrupt:
|
2720
|
+
# Handle Ctrl+C during container creation
|
2721
|
+
print("\n👋 Script interrupted during container creation.")
|
2722
|
+
print("📝 You may need to check if a container was created with: modal container list")
|
2723
|
+
sys.exit(0)
|
2724
|
+
except Exception as e:
|
2725
|
+
print(f"❌ Error: {e}")
|
2726
|
+
sys.exit(1)
|