main.py
1 # Copyright 2025 Alibaba Group Holding Ltd. 2 # 3 # Licensed under the Apache License, Version 2.0 (the "License"); 4 # you may not use this file except in compliance with the License. 5 # You may obtain a copy of the License at 6 # 7 # http://www.apache.org/licenses/LICENSE-2.0 8 # 9 # Unless required by applicable law or agreed to in writing, software 10 # distributed under the License is distributed on an "AS IS" BASIS, 11 # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 # See the License for the specific language governing permissions and 13 # limitations under the License. 14 15 """ 16 Host Volume Mount Example 17 ========================= 18 19 Demonstrates how to mount a host directory into a sandbox container using 20 the OpenSandbox Volume API. This enables sharing files, datasets, or model 21 checkpoints between the host machine and sandbox environments. 22 23 Three scenarios are demonstrated: 24 25 1. **Read-write mount** - Share a working directory for bidirectional file exchange. 26 2. **Read-only mount** - Provide shared datasets or configs that sandboxes should 27 not modify. 28 3. **SubPath mount** - Mount a specific subdirectory from the host path. 29 30 Prerequisites: 31 - OpenSandbox server running with Docker runtime 32 - Server config includes `[storage]` section with appropriate `allowed_host_paths` 33 - Host directories created before running this script (see README.md) 34 """ 35 36 import asyncio 37 import os 38 import tempfile 39 from datetime import timedelta 40 from pathlib import Path 41 42 from opensandbox import Sandbox 43 from opensandbox.config import ConnectionConfig 44 45 try: 46 from opensandbox.models.sandboxes import Host, Volume 47 except ImportError: 48 print( 49 "ERROR: Your installed opensandbox SDK does not include Volume/Host models.\n" 50 " Volume support requires the latest SDK from source.\n" 51 " Please install from the local repository:\n" 52 "\n" 53 " pip install -e sdks/sandbox/python\n" 54 "\n" 55 " See README.md for details." 56 ) 57 raise SystemExit(1) 58 59 60 async def print_exec(sandbox: Sandbox, command: str) -> str | None: 61 """Run a command in the sandbox and print/return stdout.""" 62 result = await sandbox.commands.run(command) 63 if result.error: 64 print(f" [error] {result.error.name}: {result.error.value}") 65 return None 66 text = "\n".join(msg.text for msg in result.logs.stdout) 67 if text: 68 print(f" {text}") 69 return text 70 71 72 async def demo_readwrite_mount(config: ConnectionConfig, image: str, host_dir: str) -> None: 73 """ 74 Scenario 1: Read-write mount. 75 76 Mount a host directory into the sandbox at /mnt/shared. Write a file from 77 inside the sandbox, then verify it appears on the host. 78 """ 79 print("\n" + "=" * 60) 80 print("Scenario 1: Read-Write Host Volume Mount") 81 print("=" * 60) 82 print(f" Host path : {host_dir}") 83 print(f" Mount path: /mnt/shared") 84 85 sandbox = await Sandbox.create( 86 image=image, 87 connection_config=config, 88 timeout=timedelta(minutes=2), 89 volumes=[ 90 Volume( 91 name="shared-data", 92 host=Host(path=host_dir), 93 mountPath="/mnt/shared", 94 readOnly=False, 95 ), 96 ], 97 ) 98 99 async with sandbox: 100 try: 101 # Read existing files from host 102 print("\n [1] Listing files visible from inside the sandbox:") 103 await print_exec(sandbox, "ls -la /mnt/shared/") 104 105 # Write a file from inside the sandbox 106 print("\n [2] Writing a file from inside the sandbox:") 107 await print_exec( 108 sandbox, 109 "echo 'Hello from sandbox!' > /mnt/shared/sandbox-greeting.txt", 110 ) 111 print(" -> Written: /mnt/shared/sandbox-greeting.txt") 112 113 # Verify the file content 114 print("\n [3] Reading back the file:") 115 await print_exec(sandbox, "cat /mnt/shared/sandbox-greeting.txt") 116 117 # Check host-side: the file should now exist on the host 118 host_file = Path(host_dir) / "sandbox-greeting.txt" 119 if host_file.exists(): 120 print(f"\n [4] Verified on host: {host_file}") 121 print(f" Content: {host_file.read_text().strip()}") 122 else: 123 print(f"\n [4] Note: {host_file} not directly visible (expected on remote Docker)") 124 125 finally: 126 await sandbox.kill() 127 128 print("\n Scenario 1 completed.") 129 130 131 async def demo_readonly_mount(config: ConnectionConfig, image: str, host_dir: str) -> None: 132 """ 133 Scenario 2: Read-only mount. 134 135 Mount the same host directory as read-only. Verify reads work but writes 136 are rejected by the container runtime. 137 """ 138 print("\n" + "=" * 60) 139 print("Scenario 2: Read-Only Host Volume Mount") 140 print("=" * 60) 141 print(f" Host path : {host_dir}") 142 print(f" Mount path: /mnt/readonly") 143 144 sandbox = await Sandbox.create( 145 image=image, 146 connection_config=config, 147 timeout=timedelta(minutes=2), 148 volumes=[ 149 Volume( 150 name="readonly-data", 151 host=Host(path=host_dir), 152 mountPath="/mnt/readonly", 153 readOnly=True, 154 ), 155 ], 156 ) 157 158 async with sandbox: 159 try: 160 # Read existing files 161 print("\n [1] Reading files from read-only mount:") 162 await print_exec(sandbox, "ls -la /mnt/readonly/") 163 164 # Read the marker file 165 print("\n [2] Reading marker.txt:") 166 await print_exec(sandbox, "cat /mnt/readonly/marker.txt") 167 168 # Attempt to write (should fail) 169 print("\n [3] Attempting to write (should fail):") 170 result = await sandbox.commands.run( 171 "touch /mnt/readonly/should-fail.txt 2>&1 || echo 'Write denied (expected)'" 172 ) 173 for msg in result.logs.stdout: 174 print(f" {msg.text}") 175 for msg in result.logs.stderr: 176 print(f" {msg.text}") 177 178 finally: 179 await sandbox.kill() 180 181 print("\n Scenario 2 completed.") 182 183 184 async def demo_subpath_mount(config: ConnectionConfig, image: str, host_dir: str) -> None: 185 """ 186 Scenario 3: SubPath mount. 187 188 Mount only a specific subdirectory from the host path. This is useful when 189 the host path contains multiple datasets or project directories, and you 190 want to expose only one of them. 191 """ 192 print("\n" + "=" * 60) 193 print("Scenario 3: SubPath Host Volume Mount") 194 print("=" * 60) 195 196 # Ensure subdirectory exists on host 197 sub_dir = Path(host_dir) / "datasets" / "train" 198 sub_dir.mkdir(parents=True, exist_ok=True) 199 (sub_dir / "data.csv").write_text("id,value\n1,100\n2,200\n3,300\n") 200 201 print(f" Host path : {host_dir}") 202 print(f" SubPath : datasets/train") 203 print(f" Mount path: /mnt/training-data") 204 205 sandbox = await Sandbox.create( 206 image=image, 207 connection_config=config, 208 timeout=timedelta(minutes=2), 209 volumes=[ 210 Volume( 211 name="training-data", 212 host=Host(path=host_dir), 213 mountPath="/mnt/training-data", 214 subPath="datasets/train", 215 readOnly=True, 216 ), 217 ], 218 ) 219 220 async with sandbox: 221 try: 222 # List the mounted subdirectory 223 print("\n [1] Listing mounted subpath content:") 224 await print_exec(sandbox, "ls -la /mnt/training-data/") 225 226 # Read the CSV data 227 print("\n [2] Reading data.csv:") 228 await print_exec(sandbox, "cat /mnt/training-data/data.csv") 229 230 finally: 231 await sandbox.kill() 232 233 print("\n Scenario 3 completed.") 234 235 236 async def main() -> None: 237 domain = os.getenv("SANDBOX_DOMAIN", "localhost:8080") 238 api_key = os.getenv("SANDBOX_API_KEY") 239 image = os.getenv("SANDBOX_IMAGE", "ubuntu") 240 host_dir = os.getenv("HOST_VOLUME_PATH", "") 241 242 # If no host path specified, create a temporary directory with sample data 243 if not host_dir: 244 host_dir = tempfile.mkdtemp(prefix="opensandbox-vol-") 245 print(f"No HOST_VOLUME_PATH set, using temporary directory: {host_dir}") 246 marker = Path(host_dir) / "marker.txt" 247 marker.write_text("hello-from-host\n") 248 print(f"Created marker file: {marker}") 249 else: 250 print(f"Using HOST_VOLUME_PATH: {host_dir}") 251 252 config = ConnectionConfig( 253 domain=domain, 254 api_key=api_key, 255 request_timeout=timedelta(minutes=3), 256 ) 257 258 print(f"\nOpenSandbox server : {config.domain}") 259 print(f"Sandbox image : {image}") 260 print(f"Host volume path : {host_dir}") 261 262 await demo_readwrite_mount(config, image, host_dir) 263 await demo_readonly_mount(config, image, host_dir) 264 await demo_subpath_mount(config, image, host_dir) 265 266 print("\n" + "=" * 60) 267 print("All scenarios completed successfully!") 268 print("=" * 60) 269 270 271 if __name__ == "__main__": 272 asyncio.run(main())