Skip to content

Commit f13916a

Browse files
authored
feat: add snapshots example (#753)
1 parent da5faa4 commit f13916a

4 files changed

Lines changed: 175 additions & 0 deletions

File tree

EXAMPLES.md

Lines changed: 32 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,7 @@ Runnable examples live in [`examples/`](./examples).
99

1010
- [Blueprint with Build Context](#blueprint-with-build-context)
1111
- [Devbox From Blueprint (Run Command, Shutdown)](#devbox-from-blueprint-lifecycle)
12+
- [Devbox Snapshot and Resume](#devbox-snapshot-resume)
1213
- [MCP Hub + Claude Code + GitHub](#mcp-github-tools)
1314

1415
<a id="blueprint-with-build-context"></a>
@@ -72,6 +73,37 @@ uv run pytest -m smoketest tests/smoketests/examples/
7273

7374
**Source:** [`examples/devbox_from_blueprint_lifecycle.py`](./examples/devbox_from_blueprint_lifecycle.py)
7475

76+
<a id="devbox-snapshot-resume"></a>
77+
## Devbox Snapshot and Resume
78+
79+
**Use case:** Create a devbox, snapshot its disk, resume from the snapshot, and demonstrate that changes in the original devbox do not affect the clone. Uses the async SDK.
80+
81+
**Tags:** `devbox`, `snapshot`, `resume`, `cleanup`, `async`
82+
83+
### Workflow
84+
- Create a devbox
85+
- Write a file to the devbox
86+
- Create a disk snapshot
87+
- Create a new devbox from the snapshot
88+
- Modify the file on the original devbox
89+
- Verify the clone has the original content
90+
- Shutdown both devboxes and delete the snapshot
91+
92+
### Prerequisites
93+
- `RUNLOOP_API_KEY`
94+
95+
### Run
96+
```sh
97+
uv run python -m examples.devbox_snapshot_resume
98+
```
99+
100+
### Test
101+
```sh
102+
uv run pytest -m smoketest tests/smoketests/examples/
103+
```
104+
105+
**Source:** [`examples/devbox_snapshot_resume.py`](./examples/devbox_snapshot_resume.py)
106+
75107
<a id="mcp-github-tools"></a>
76108
## MCP Hub + Claude Code + GitHub
77109

examples/devbox_snapshot_resume.py

Lines changed: 134 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,134 @@
1+
#!/usr/bin/env -S uv run python
2+
"""
3+
---
4+
title: Devbox Snapshot and Resume
5+
slug: devbox-snapshot-resume
6+
use_case: Create a devbox, snapshot its disk, resume from the snapshot, and demonstrate that changes in the original devbox do not affect the clone. Uses the async SDK.
7+
workflow:
8+
- Create a devbox
9+
- Write a file to the devbox
10+
- Create a disk snapshot
11+
- Create a new devbox from the snapshot
12+
- Modify the file on the original devbox
13+
- Verify the clone has the original content
14+
- Shutdown both devboxes and delete the snapshot
15+
tags:
16+
- devbox
17+
- snapshot
18+
- resume
19+
- cleanup
20+
- async
21+
prerequisites:
22+
- RUNLOOP_API_KEY
23+
run: uv run python -m examples.devbox_snapshot_resume
24+
test: uv run pytest -m smoketest tests/smoketests/examples/
25+
---
26+
"""
27+
28+
from __future__ import annotations
29+
30+
from runloop_api_client import AsyncRunloopSDK
31+
32+
from ._harness import run_as_cli, wrap_recipe
33+
from .example_types import ExampleCheck, RecipeOutput, RecipeContext
34+
35+
FILE_PATH = "/home/user/welcome.txt"
36+
ORIGINAL_CONTENT = "hello world!"
37+
MODIFIED_CONTENT = "original devbox has changed the welcome message"
38+
39+
40+
async def recipe(ctx: RecipeContext) -> RecipeOutput:
41+
"""Create a devbox, snapshot it, resume from snapshot, and verify state isolation."""
42+
cleanup = ctx.cleanup
43+
44+
sdk = AsyncRunloopSDK()
45+
46+
# Create a devbox
47+
dbx_original = await sdk.devbox.create(
48+
name="dbx_original",
49+
launch_parameters={
50+
"resource_size_request": "X_SMALL",
51+
"keep_alive_time_seconds": 60 * 5,
52+
},
53+
)
54+
cleanup.add(f"devbox:{dbx_original.id}", dbx_original.shutdown)
55+
56+
# Write a file to the original devbox
57+
await dbx_original.file.write(file_path=FILE_PATH, contents=ORIGINAL_CONTENT)
58+
59+
# Read and display the file contents
60+
cat_original_before = await dbx_original.cmd.exec(f"cat {FILE_PATH}")
61+
original_content_before = await cat_original_before.stdout()
62+
63+
# Create a disk snapshot of the original devbox
64+
snapshot = await dbx_original.snapshot_disk(name="my-snapshot")
65+
cleanup.add(f"snapshot:{snapshot.id}", snapshot.delete)
66+
67+
# Create a new devbox from the snapshot
68+
dbx_clone = await sdk.devbox.create_from_snapshot(
69+
snapshot.id,
70+
name="dbx_clone",
71+
launch_parameters={
72+
"resource_size_request": "X_SMALL",
73+
"keep_alive_time_seconds": 60 * 5,
74+
},
75+
)
76+
cleanup.add(f"devbox:{dbx_clone.id}", dbx_clone.shutdown)
77+
78+
# Modify the file on the original devbox
79+
await dbx_original.file.write(file_path=FILE_PATH, contents=MODIFIED_CONTENT)
80+
81+
# Read the file contents from both devboxes
82+
cat_clone = await dbx_clone.cmd.exec(f"cat {FILE_PATH}")
83+
clone_content = await cat_clone.stdout()
84+
85+
# now the original devbox has been modified but the clone has the original message
86+
cat_original_after = await dbx_original.cmd.exec(f"cat {FILE_PATH}")
87+
original_content_after = await cat_original_after.stdout()
88+
89+
return RecipeOutput(
90+
resources_created=[
91+
f"devbox:{dbx_original.id}",
92+
f"snapshot:{snapshot.id}",
93+
f"devbox:{dbx_clone.id}",
94+
],
95+
checks=[
96+
ExampleCheck(
97+
name="original devbox file created successfully",
98+
passed=cat_original_before.exit_code == 0 and original_content_before.strip() == ORIGINAL_CONTENT,
99+
details=f'content="{original_content_before.strip()}"',
100+
),
101+
ExampleCheck(
102+
name="snapshot created successfully",
103+
passed=bool(snapshot.id),
104+
details=f"snapshotId={snapshot.id}",
105+
),
106+
ExampleCheck(
107+
name="clone devbox created from snapshot",
108+
passed=bool(dbx_clone.id),
109+
details=f"cloneId={dbx_clone.id}",
110+
),
111+
ExampleCheck(
112+
name="clone has original file content (before modification)",
113+
passed=cat_clone.exit_code == 0 and clone_content.strip() == ORIGINAL_CONTENT,
114+
details=f'cloneContent="{clone_content.strip()}"',
115+
),
116+
ExampleCheck(
117+
name="original devbox has modified content",
118+
passed=cat_original_after.exit_code == 0 and original_content_after.strip() == MODIFIED_CONTENT,
119+
details=f'originalContent="{original_content_after.strip()}"',
120+
),
121+
ExampleCheck(
122+
name="clone and original have divergent state",
123+
passed=clone_content.strip() != original_content_after.strip(),
124+
details=f'clone="{clone_content.strip()}" vs original="{original_content_after.strip()}"',
125+
),
126+
],
127+
)
128+
129+
130+
run_devbox_snapshot_resume_example = wrap_recipe(recipe)
131+
132+
133+
if __name__ == "__main__":
134+
run_as_cli(run_devbox_snapshot_resume_example)

examples/registry.py

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,7 @@
99

1010
from .example_types import ExampleResult
1111
from .mcp_github_tools import run_mcp_github_tools_example
12+
from .devbox_snapshot_resume import run_devbox_snapshot_resume_example
1213
from .blueprint_with_build_context import run_blueprint_with_build_context_example
1314
from .devbox_from_blueprint_lifecycle import run_devbox_from_blueprint_lifecycle_example
1415

@@ -29,6 +30,13 @@
2930
"required_env": ["RUNLOOP_API_KEY"],
3031
"run": run_devbox_from_blueprint_lifecycle_example,
3132
},
33+
{
34+
"slug": "devbox-snapshot-resume",
35+
"title": "Devbox Snapshot and Resume",
36+
"file_name": "devbox_snapshot_resume.py",
37+
"required_env": ["RUNLOOP_API_KEY"],
38+
"run": run_devbox_snapshot_resume_example,
39+
},
3240
{
3341
"slug": "mcp-github-tools",
3442
"title": "MCP Hub + Claude Code + GitHub",

llms.txt

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,7 @@
1111
## Core Patterns
1212

1313
- [Devbox lifecycle example](examples/devbox_from_blueprint_lifecycle.py): Create blueprint, launch devbox, run commands, cleanup
14+
- [Devbox snapshot and resume example](examples/devbox_snapshot_resume.py): Snapshot disk, resume from snapshot, verify state isolation
1415
- [MCP GitHub example](examples/mcp_github_tools.py): MCP Hub integration with Claude Code
1516

1617
## API Reference

0 commit comments

Comments
 (0)