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())