flash-sandbox 0.1.0__tar.gz
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.
- flash_sandbox-0.1.0/PKG-INFO +217 -0
- flash_sandbox-0.1.0/README.md +204 -0
- flash_sandbox-0.1.0/flash_sandbox/__init__.py +12 -0
- flash_sandbox-0.1.0/flash_sandbox/client.py +220 -0
- flash_sandbox-0.1.0/flash_sandbox/http_client.py +402 -0
- flash_sandbox-0.1.0/flash_sandbox/proto/__init__.py +0 -0
- flash_sandbox-0.1.0/flash_sandbox/proto/sandbox_pb2.py +65 -0
- flash_sandbox-0.1.0/flash_sandbox/proto/sandbox_pb2_grpc.py +355 -0
- flash_sandbox-0.1.0/flash_sandbox.egg-info/PKG-INFO +217 -0
- flash_sandbox-0.1.0/flash_sandbox.egg-info/SOURCES.txt +15 -0
- flash_sandbox-0.1.0/flash_sandbox.egg-info/dependency_links.txt +1 -0
- flash_sandbox-0.1.0/flash_sandbox.egg-info/requires.txt +7 -0
- flash_sandbox-0.1.0/flash_sandbox.egg-info/top_level.txt +1 -0
- flash_sandbox-0.1.0/pyproject.toml +24 -0
- flash_sandbox-0.1.0/setup.cfg +4 -0
- flash_sandbox-0.1.0/tests/test_http_client.py +642 -0
- flash_sandbox-0.1.0/tests/test_sdk.py +83 -0
|
@@ -0,0 +1,217 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: flash-sandbox
|
|
3
|
+
Version: 0.1.0
|
|
4
|
+
Summary: A Python SDK for interacting with the Sandbox Orchestrator.
|
|
5
|
+
Requires-Python: >=3.8
|
|
6
|
+
Description-Content-Type: text/markdown
|
|
7
|
+
Requires-Dist: grpcio>=1.50.0
|
|
8
|
+
Requires-Dist: protobuf>=4.21.0
|
|
9
|
+
Requires-Dist: requests>=2.28.0
|
|
10
|
+
Provides-Extra: dev
|
|
11
|
+
Requires-Dist: grpcio-tools>=1.50.0; extra == "dev"
|
|
12
|
+
Requires-Dist: pytest; extra == "dev"
|
|
13
|
+
|
|
14
|
+
# Sandbox SDK
|
|
15
|
+
|
|
16
|
+
A Python SDK for interacting with the Sandbox Orchestrator. Supports both
|
|
17
|
+
**gRPC** and **HTTP** transports with an identical public interface, so you can swap protocols without changing application logic.
|
|
18
|
+
|
|
19
|
+
## Installation
|
|
20
|
+
|
|
21
|
+
```bash
|
|
22
|
+
pip install flash-sandbox
|
|
23
|
+
```
|
|
24
|
+
|
|
25
|
+
The package pulls in `grpcio`, `protobuf`, and `requests` automatically.
|
|
26
|
+
|
|
27
|
+
## Quick Start
|
|
28
|
+
|
|
29
|
+
### HTTP transport (recommended for simplicity)
|
|
30
|
+
|
|
31
|
+
```python
|
|
32
|
+
from flash_sandbox import HTTPClient
|
|
33
|
+
|
|
34
|
+
client = HTTPClient(host="localhost", port=8080)
|
|
35
|
+
|
|
36
|
+
# Start a Docker sandbox
|
|
37
|
+
sandbox_id = client.start_sandbox(
|
|
38
|
+
type="docker",
|
|
39
|
+
image="alpine:latest",
|
|
40
|
+
command=["tail", "-f", "/dev/null"],
|
|
41
|
+
memory_mb=128,
|
|
42
|
+
cpu_cores=0.5,
|
|
43
|
+
)
|
|
44
|
+
print(f"Sandbox ID: {sandbox_id}")
|
|
45
|
+
|
|
46
|
+
# Execute a command
|
|
47
|
+
result = client.exec_command(sandbox_id, ["echo", "Hello from HTTP!"])
|
|
48
|
+
print(f"Output: {result.stdout.strip()}")
|
|
49
|
+
print(f"Exit code: {result.exit_code}")
|
|
50
|
+
|
|
51
|
+
# Check status
|
|
52
|
+
status = client.get_status(sandbox_id)
|
|
53
|
+
print(f"Status: {status}")
|
|
54
|
+
|
|
55
|
+
# Get resource metrics
|
|
56
|
+
metrics = client.get_metrics(sandbox_id)
|
|
57
|
+
print(f"Memory: {metrics.memory_usage_bytes} / {metrics.memory_limit_bytes}")
|
|
58
|
+
print(f"CPU: {metrics.cpu_percent}%")
|
|
59
|
+
|
|
60
|
+
# Snapshot and resume
|
|
61
|
+
snap = client.snapshot_sandbox(sandbox_id)
|
|
62
|
+
print(f"Snapshot: {snap['snapshot_path']}")
|
|
63
|
+
client.resume_sandbox(sandbox_id)
|
|
64
|
+
|
|
65
|
+
# Stop
|
|
66
|
+
client.stop_sandbox(sandbox_id)
|
|
67
|
+
client.close()
|
|
68
|
+
```
|
|
69
|
+
|
|
70
|
+
### gRPC transport
|
|
71
|
+
|
|
72
|
+
```python
|
|
73
|
+
from flash_sandbox import SandboxClient
|
|
74
|
+
|
|
75
|
+
client = SandboxClient(host="localhost", port=50051)
|
|
76
|
+
|
|
77
|
+
# Start a Docker sandbox
|
|
78
|
+
docker_id = client.start_sandbox(
|
|
79
|
+
type="docker",
|
|
80
|
+
image="alpine:latest",
|
|
81
|
+
command=["tail", "-f", "/dev/null"],
|
|
82
|
+
memory_mb=128,
|
|
83
|
+
cpu_cores=0.5,
|
|
84
|
+
)
|
|
85
|
+
print(f"Docker Sandbox ID: {docker_id}")
|
|
86
|
+
|
|
87
|
+
# Start a Firecracker sandbox
|
|
88
|
+
fc_id = client.start_sandbox(
|
|
89
|
+
type="firecracker",
|
|
90
|
+
image="alpine:latest",
|
|
91
|
+
command=["tail", "-f", "/dev/null"],
|
|
92
|
+
memory_mb=512,
|
|
93
|
+
cpu_cores=1.0,
|
|
94
|
+
)
|
|
95
|
+
print(f"Firecracker Sandbox ID: {fc_id}")
|
|
96
|
+
|
|
97
|
+
# Start a gVisor sandbox
|
|
98
|
+
gvisor_id = client.start_sandbox(
|
|
99
|
+
type="gvisor",
|
|
100
|
+
image="alpine:latest",
|
|
101
|
+
command=["tail", "-f", "/dev/null"],
|
|
102
|
+
memory_mb=256,
|
|
103
|
+
cpu_cores=1.0,
|
|
104
|
+
)
|
|
105
|
+
print(f"gVisor Sandbox ID: {gvisor_id}")
|
|
106
|
+
|
|
107
|
+
# Check status
|
|
108
|
+
status = client.get_status(fc_id)
|
|
109
|
+
print(f"Firecracker Status: {status}")
|
|
110
|
+
|
|
111
|
+
# Snapshot and Resume
|
|
112
|
+
snap_res = client.snapshot_sandbox(fc_id)
|
|
113
|
+
print(f"Snapshot Data: {snap_res}")
|
|
114
|
+
client.resume_sandbox(fc_id)
|
|
115
|
+
|
|
116
|
+
# Execute commands
|
|
117
|
+
exec_res = client.exec_command(gvisor_id, ["echo", "Hello from gVisor!"])
|
|
118
|
+
print(f"gVisor Output: {exec_res.stdout.strip()}")
|
|
119
|
+
print(f"gVisor Exit Code: {exec_res.exit_code}")
|
|
120
|
+
|
|
121
|
+
# Stop sandboxes
|
|
122
|
+
client.stop_sandbox(docker_id)
|
|
123
|
+
client.stop_sandbox(fc_id)
|
|
124
|
+
client.stop_sandbox(gvisor_id)
|
|
125
|
+
client.close()
|
|
126
|
+
```
|
|
127
|
+
|
|
128
|
+
## API Reference
|
|
129
|
+
|
|
130
|
+
Both `HTTPClient` and `SandboxClient` expose the same set of methods:
|
|
131
|
+
|
|
132
|
+
| Method | Description |
|
|
133
|
+
|---|---|
|
|
134
|
+
| `start_sandbox(type, image, command, memory_mb, cpu_cores, ...)` | Start a new sandbox and return its ID. |
|
|
135
|
+
| `stop_sandbox(sandbox_id)` | Stop and remove a running sandbox. |
|
|
136
|
+
| `exec_command(sandbox_id, command)` | Execute a command in a sandbox. Returns an object with `stdout`, `stderr`, and `exit_code`. |
|
|
137
|
+
| `get_status(sandbox_id)` | Return the status string (`"running"`, `"stopped"`, etc.). |
|
|
138
|
+
| `get_metrics(sandbox_id)` | Return point-in-time resource-usage metrics (memory, CPU, network, block I/O). |
|
|
139
|
+
| `snapshot_sandbox(sandbox_id)` | Create a snapshot. Returns `{"snapshot_path": ..., "mem_file_path": ...}`. |
|
|
140
|
+
| `resume_sandbox(sandbox_id)` | Resume a paused / snapshotted sandbox. |
|
|
141
|
+
| `close()` | Release the underlying connection / session. |
|
|
142
|
+
|
|
143
|
+
### Constructor options
|
|
144
|
+
|
|
145
|
+
#### HTTPClient
|
|
146
|
+
|
|
147
|
+
```python
|
|
148
|
+
HTTPClient(
|
|
149
|
+
host="localhost", # Orchestrator hostname
|
|
150
|
+
port=8080, # HTTP port (default 8080)
|
|
151
|
+
address=None, # Full URL, overrides host/port (e.g. "http://proxy:9090/v1/service/sandbox")
|
|
152
|
+
timeout=30.0, # Request timeout in seconds (None = no timeout)
|
|
153
|
+
session=None, # Optional requests.Session for custom TLS / auth / retries
|
|
154
|
+
)
|
|
155
|
+
```
|
|
156
|
+
|
|
157
|
+
#### SandboxClient (gRPC)
|
|
158
|
+
|
|
159
|
+
```python
|
|
160
|
+
SandboxClient(
|
|
161
|
+
host="localhost", # Orchestrator hostname
|
|
162
|
+
port=50051, # gRPC port (default 50051)
|
|
163
|
+
address=None, # Full address for proxy routing (e.g. "localhost:8092/v1/service/sandbox")
|
|
164
|
+
)
|
|
165
|
+
```
|
|
166
|
+
|
|
167
|
+
### HTTPClient-only extras
|
|
168
|
+
|
|
169
|
+
The HTTP transport includes a few additional features:
|
|
170
|
+
|
|
171
|
+
- **Firecracker fields** on `start_sandbox`: `kernel_image`, `initrd_path`, `snapshot_path`, `mem_file_path`.
|
|
172
|
+
- **Custom timeouts** per-client via the `timeout` parameter.
|
|
173
|
+
- **Session injection** – pass your own `requests.Session` for connection pooling, mutual TLS, retry policies, or authentication headers.
|
|
174
|
+
- **Typed exceptions**: `SandboxHTTPError` (with `.status_code` and `.detail`) and the more specific `SandboxNotFoundError` for 404 responses.
|
|
175
|
+
|
|
176
|
+
### Response types (HTTP client)
|
|
177
|
+
|
|
178
|
+
| Class | Fields |
|
|
179
|
+
|---|---|
|
|
180
|
+
| `ExecResult` | `stdout: str`, `stderr: str`, `exit_code: int` |
|
|
181
|
+
| `MetricsResult` | `memory_usage_bytes`, `memory_limit_bytes`, `memory_percent`, `cpu_percent`, `pids_current`, `net_rx_bytes`, `net_tx_bytes`, `block_read_bytes`, `block_write_bytes` |
|
|
182
|
+
| `SnapshotResult` | `snapshot_path: str`, `mem_file_path: str` |
|
|
183
|
+
|
|
184
|
+
All response dataclasses are **frozen** (immutable).
|
|
185
|
+
|
|
186
|
+
## Context manager
|
|
187
|
+
|
|
188
|
+
Both clients support the context-manager protocol:
|
|
189
|
+
|
|
190
|
+
```python
|
|
191
|
+
from flash_sandbox import HTTPClient
|
|
192
|
+
|
|
193
|
+
with HTTPClient(host="localhost") as client:
|
|
194
|
+
sid = client.start_sandbox(type="docker", image="alpine:latest")
|
|
195
|
+
client.stop_sandbox(sid)
|
|
196
|
+
# Connection is automatically closed here.
|
|
197
|
+
```
|
|
198
|
+
|
|
199
|
+
## Proxy / reverse-proxy support
|
|
200
|
+
|
|
201
|
+
Both clients support routing through a reverse proxy that uses path-based
|
|
202
|
+
routing. Pass the full address including the path prefix:
|
|
203
|
+
|
|
204
|
+
```python
|
|
205
|
+
# HTTP through a proxy
|
|
206
|
+
http_client = HTTPClient(address="http://proxy.example.com:8092/v1/service/sandbox")
|
|
207
|
+
|
|
208
|
+
# gRPC through a proxy
|
|
209
|
+
grpc_client = SandboxClient(address="proxy.example.com:8092/v1/service/sandbox")
|
|
210
|
+
```
|
|
211
|
+
|
|
212
|
+
## Running tests
|
|
213
|
+
|
|
214
|
+
```bash
|
|
215
|
+
pip install -e ".[dev]"
|
|
216
|
+
pytest tests/
|
|
217
|
+
```
|
|
@@ -0,0 +1,204 @@
|
|
|
1
|
+
# Sandbox SDK
|
|
2
|
+
|
|
3
|
+
A Python SDK for interacting with the Sandbox Orchestrator. Supports both
|
|
4
|
+
**gRPC** and **HTTP** transports with an identical public interface, so you can swap protocols without changing application logic.
|
|
5
|
+
|
|
6
|
+
## Installation
|
|
7
|
+
|
|
8
|
+
```bash
|
|
9
|
+
pip install flash-sandbox
|
|
10
|
+
```
|
|
11
|
+
|
|
12
|
+
The package pulls in `grpcio`, `protobuf`, and `requests` automatically.
|
|
13
|
+
|
|
14
|
+
## Quick Start
|
|
15
|
+
|
|
16
|
+
### HTTP transport (recommended for simplicity)
|
|
17
|
+
|
|
18
|
+
```python
|
|
19
|
+
from flash_sandbox import HTTPClient
|
|
20
|
+
|
|
21
|
+
client = HTTPClient(host="localhost", port=8080)
|
|
22
|
+
|
|
23
|
+
# Start a Docker sandbox
|
|
24
|
+
sandbox_id = client.start_sandbox(
|
|
25
|
+
type="docker",
|
|
26
|
+
image="alpine:latest",
|
|
27
|
+
command=["tail", "-f", "/dev/null"],
|
|
28
|
+
memory_mb=128,
|
|
29
|
+
cpu_cores=0.5,
|
|
30
|
+
)
|
|
31
|
+
print(f"Sandbox ID: {sandbox_id}")
|
|
32
|
+
|
|
33
|
+
# Execute a command
|
|
34
|
+
result = client.exec_command(sandbox_id, ["echo", "Hello from HTTP!"])
|
|
35
|
+
print(f"Output: {result.stdout.strip()}")
|
|
36
|
+
print(f"Exit code: {result.exit_code}")
|
|
37
|
+
|
|
38
|
+
# Check status
|
|
39
|
+
status = client.get_status(sandbox_id)
|
|
40
|
+
print(f"Status: {status}")
|
|
41
|
+
|
|
42
|
+
# Get resource metrics
|
|
43
|
+
metrics = client.get_metrics(sandbox_id)
|
|
44
|
+
print(f"Memory: {metrics.memory_usage_bytes} / {metrics.memory_limit_bytes}")
|
|
45
|
+
print(f"CPU: {metrics.cpu_percent}%")
|
|
46
|
+
|
|
47
|
+
# Snapshot and resume
|
|
48
|
+
snap = client.snapshot_sandbox(sandbox_id)
|
|
49
|
+
print(f"Snapshot: {snap['snapshot_path']}")
|
|
50
|
+
client.resume_sandbox(sandbox_id)
|
|
51
|
+
|
|
52
|
+
# Stop
|
|
53
|
+
client.stop_sandbox(sandbox_id)
|
|
54
|
+
client.close()
|
|
55
|
+
```
|
|
56
|
+
|
|
57
|
+
### gRPC transport
|
|
58
|
+
|
|
59
|
+
```python
|
|
60
|
+
from flash_sandbox import SandboxClient
|
|
61
|
+
|
|
62
|
+
client = SandboxClient(host="localhost", port=50051)
|
|
63
|
+
|
|
64
|
+
# Start a Docker sandbox
|
|
65
|
+
docker_id = client.start_sandbox(
|
|
66
|
+
type="docker",
|
|
67
|
+
image="alpine:latest",
|
|
68
|
+
command=["tail", "-f", "/dev/null"],
|
|
69
|
+
memory_mb=128,
|
|
70
|
+
cpu_cores=0.5,
|
|
71
|
+
)
|
|
72
|
+
print(f"Docker Sandbox ID: {docker_id}")
|
|
73
|
+
|
|
74
|
+
# Start a Firecracker sandbox
|
|
75
|
+
fc_id = client.start_sandbox(
|
|
76
|
+
type="firecracker",
|
|
77
|
+
image="alpine:latest",
|
|
78
|
+
command=["tail", "-f", "/dev/null"],
|
|
79
|
+
memory_mb=512,
|
|
80
|
+
cpu_cores=1.0,
|
|
81
|
+
)
|
|
82
|
+
print(f"Firecracker Sandbox ID: {fc_id}")
|
|
83
|
+
|
|
84
|
+
# Start a gVisor sandbox
|
|
85
|
+
gvisor_id = client.start_sandbox(
|
|
86
|
+
type="gvisor",
|
|
87
|
+
image="alpine:latest",
|
|
88
|
+
command=["tail", "-f", "/dev/null"],
|
|
89
|
+
memory_mb=256,
|
|
90
|
+
cpu_cores=1.0,
|
|
91
|
+
)
|
|
92
|
+
print(f"gVisor Sandbox ID: {gvisor_id}")
|
|
93
|
+
|
|
94
|
+
# Check status
|
|
95
|
+
status = client.get_status(fc_id)
|
|
96
|
+
print(f"Firecracker Status: {status}")
|
|
97
|
+
|
|
98
|
+
# Snapshot and Resume
|
|
99
|
+
snap_res = client.snapshot_sandbox(fc_id)
|
|
100
|
+
print(f"Snapshot Data: {snap_res}")
|
|
101
|
+
client.resume_sandbox(fc_id)
|
|
102
|
+
|
|
103
|
+
# Execute commands
|
|
104
|
+
exec_res = client.exec_command(gvisor_id, ["echo", "Hello from gVisor!"])
|
|
105
|
+
print(f"gVisor Output: {exec_res.stdout.strip()}")
|
|
106
|
+
print(f"gVisor Exit Code: {exec_res.exit_code}")
|
|
107
|
+
|
|
108
|
+
# Stop sandboxes
|
|
109
|
+
client.stop_sandbox(docker_id)
|
|
110
|
+
client.stop_sandbox(fc_id)
|
|
111
|
+
client.stop_sandbox(gvisor_id)
|
|
112
|
+
client.close()
|
|
113
|
+
```
|
|
114
|
+
|
|
115
|
+
## API Reference
|
|
116
|
+
|
|
117
|
+
Both `HTTPClient` and `SandboxClient` expose the same set of methods:
|
|
118
|
+
|
|
119
|
+
| Method | Description |
|
|
120
|
+
|---|---|
|
|
121
|
+
| `start_sandbox(type, image, command, memory_mb, cpu_cores, ...)` | Start a new sandbox and return its ID. |
|
|
122
|
+
| `stop_sandbox(sandbox_id)` | Stop and remove a running sandbox. |
|
|
123
|
+
| `exec_command(sandbox_id, command)` | Execute a command in a sandbox. Returns an object with `stdout`, `stderr`, and `exit_code`. |
|
|
124
|
+
| `get_status(sandbox_id)` | Return the status string (`"running"`, `"stopped"`, etc.). |
|
|
125
|
+
| `get_metrics(sandbox_id)` | Return point-in-time resource-usage metrics (memory, CPU, network, block I/O). |
|
|
126
|
+
| `snapshot_sandbox(sandbox_id)` | Create a snapshot. Returns `{"snapshot_path": ..., "mem_file_path": ...}`. |
|
|
127
|
+
| `resume_sandbox(sandbox_id)` | Resume a paused / snapshotted sandbox. |
|
|
128
|
+
| `close()` | Release the underlying connection / session. |
|
|
129
|
+
|
|
130
|
+
### Constructor options
|
|
131
|
+
|
|
132
|
+
#### HTTPClient
|
|
133
|
+
|
|
134
|
+
```python
|
|
135
|
+
HTTPClient(
|
|
136
|
+
host="localhost", # Orchestrator hostname
|
|
137
|
+
port=8080, # HTTP port (default 8080)
|
|
138
|
+
address=None, # Full URL, overrides host/port (e.g. "http://proxy:9090/v1/service/sandbox")
|
|
139
|
+
timeout=30.0, # Request timeout in seconds (None = no timeout)
|
|
140
|
+
session=None, # Optional requests.Session for custom TLS / auth / retries
|
|
141
|
+
)
|
|
142
|
+
```
|
|
143
|
+
|
|
144
|
+
#### SandboxClient (gRPC)
|
|
145
|
+
|
|
146
|
+
```python
|
|
147
|
+
SandboxClient(
|
|
148
|
+
host="localhost", # Orchestrator hostname
|
|
149
|
+
port=50051, # gRPC port (default 50051)
|
|
150
|
+
address=None, # Full address for proxy routing (e.g. "localhost:8092/v1/service/sandbox")
|
|
151
|
+
)
|
|
152
|
+
```
|
|
153
|
+
|
|
154
|
+
### HTTPClient-only extras
|
|
155
|
+
|
|
156
|
+
The HTTP transport includes a few additional features:
|
|
157
|
+
|
|
158
|
+
- **Firecracker fields** on `start_sandbox`: `kernel_image`, `initrd_path`, `snapshot_path`, `mem_file_path`.
|
|
159
|
+
- **Custom timeouts** per-client via the `timeout` parameter.
|
|
160
|
+
- **Session injection** – pass your own `requests.Session` for connection pooling, mutual TLS, retry policies, or authentication headers.
|
|
161
|
+
- **Typed exceptions**: `SandboxHTTPError` (with `.status_code` and `.detail`) and the more specific `SandboxNotFoundError` for 404 responses.
|
|
162
|
+
|
|
163
|
+
### Response types (HTTP client)
|
|
164
|
+
|
|
165
|
+
| Class | Fields |
|
|
166
|
+
|---|---|
|
|
167
|
+
| `ExecResult` | `stdout: str`, `stderr: str`, `exit_code: int` |
|
|
168
|
+
| `MetricsResult` | `memory_usage_bytes`, `memory_limit_bytes`, `memory_percent`, `cpu_percent`, `pids_current`, `net_rx_bytes`, `net_tx_bytes`, `block_read_bytes`, `block_write_bytes` |
|
|
169
|
+
| `SnapshotResult` | `snapshot_path: str`, `mem_file_path: str` |
|
|
170
|
+
|
|
171
|
+
All response dataclasses are **frozen** (immutable).
|
|
172
|
+
|
|
173
|
+
## Context manager
|
|
174
|
+
|
|
175
|
+
Both clients support the context-manager protocol:
|
|
176
|
+
|
|
177
|
+
```python
|
|
178
|
+
from flash_sandbox import HTTPClient
|
|
179
|
+
|
|
180
|
+
with HTTPClient(host="localhost") as client:
|
|
181
|
+
sid = client.start_sandbox(type="docker", image="alpine:latest")
|
|
182
|
+
client.stop_sandbox(sid)
|
|
183
|
+
# Connection is automatically closed here.
|
|
184
|
+
```
|
|
185
|
+
|
|
186
|
+
## Proxy / reverse-proxy support
|
|
187
|
+
|
|
188
|
+
Both clients support routing through a reverse proxy that uses path-based
|
|
189
|
+
routing. Pass the full address including the path prefix:
|
|
190
|
+
|
|
191
|
+
```python
|
|
192
|
+
# HTTP through a proxy
|
|
193
|
+
http_client = HTTPClient(address="http://proxy.example.com:8092/v1/service/sandbox")
|
|
194
|
+
|
|
195
|
+
# gRPC through a proxy
|
|
196
|
+
grpc_client = SandboxClient(address="proxy.example.com:8092/v1/service/sandbox")
|
|
197
|
+
```
|
|
198
|
+
|
|
199
|
+
## Running tests
|
|
200
|
+
|
|
201
|
+
```bash
|
|
202
|
+
pip install -e ".[dev]"
|
|
203
|
+
pytest tests/
|
|
204
|
+
```
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
from .client import SandboxClient
|
|
2
|
+
from .http_client import HTTPClient, ExecResult, MetricsResult, SnapshotResult, SandboxHTTPError, SandboxNotFoundError
|
|
3
|
+
|
|
4
|
+
__all__ = [
|
|
5
|
+
"SandboxClient",
|
|
6
|
+
"HTTPClient",
|
|
7
|
+
"ExecResult",
|
|
8
|
+
"MetricsResult",
|
|
9
|
+
"SnapshotResult",
|
|
10
|
+
"SandboxHTTPError",
|
|
11
|
+
"SandboxNotFoundError",
|
|
12
|
+
]
|
|
@@ -0,0 +1,220 @@
|
|
|
1
|
+
import grpc
|
|
2
|
+
from typing import List, Optional
|
|
3
|
+
from urllib.parse import urlparse
|
|
4
|
+
|
|
5
|
+
from .proto import sandbox_pb2
|
|
6
|
+
from .proto import sandbox_pb2_grpc
|
|
7
|
+
|
|
8
|
+
|
|
9
|
+
class _OCFProxyInterceptor(
|
|
10
|
+
grpc.UnaryUnaryClientInterceptor,
|
|
11
|
+
grpc.UnaryStreamClientInterceptor,
|
|
12
|
+
grpc.StreamUnaryClientInterceptor,
|
|
13
|
+
grpc.StreamStreamClientInterceptor,
|
|
14
|
+
):
|
|
15
|
+
"""Interceptor that prepends an OCF proxy path prefix to gRPC method paths.
|
|
16
|
+
|
|
17
|
+
Python's grpc library does NOT include the URL path portion of the channel
|
|
18
|
+
target in the HTTP/2 :path pseudo-header. It always sends
|
|
19
|
+
:path = /package.Service/Method. The OCF proxy needs the full path
|
|
20
|
+
(e.g. /v1/service/sandbox/package.Service/Method) to route the request.
|
|
21
|
+
This interceptor rewrites every outgoing RPC's method string to include
|
|
22
|
+
the prefix.
|
|
23
|
+
"""
|
|
24
|
+
|
|
25
|
+
def __init__(self, path_prefix: str):
|
|
26
|
+
# Ensure the prefix starts with / and does not end with /
|
|
27
|
+
self._prefix = "/" + path_prefix.strip("/")
|
|
28
|
+
|
|
29
|
+
def _rewrite(self, client_call_details):
|
|
30
|
+
"""Return a new ClientCallDetails with the rewritten method path."""
|
|
31
|
+
new_method = self._prefix + client_call_details.method
|
|
32
|
+
return _new_client_call_details(client_call_details, new_method)
|
|
33
|
+
|
|
34
|
+
def intercept_unary_unary(self, continuation, client_call_details, request):
|
|
35
|
+
return continuation(self._rewrite(client_call_details), request)
|
|
36
|
+
|
|
37
|
+
def intercept_unary_stream(self, continuation, client_call_details, request):
|
|
38
|
+
return continuation(self._rewrite(client_call_details), request)
|
|
39
|
+
|
|
40
|
+
def intercept_stream_unary(self, continuation, client_call_details, request_iterator):
|
|
41
|
+
return continuation(self._rewrite(client_call_details), request_iterator)
|
|
42
|
+
|
|
43
|
+
def intercept_stream_stream(self, continuation, client_call_details, request_iterator):
|
|
44
|
+
return continuation(self._rewrite(client_call_details), request_iterator)
|
|
45
|
+
|
|
46
|
+
|
|
47
|
+
class _ClientCallDetails(
|
|
48
|
+
grpc.ClientCallDetails,
|
|
49
|
+
):
|
|
50
|
+
"""Concrete implementation of ClientCallDetails for rewriting."""
|
|
51
|
+
|
|
52
|
+
def __init__(self, method, timeout, metadata, credentials, wait_for_ready, compression):
|
|
53
|
+
self.method = method
|
|
54
|
+
self.timeout = timeout
|
|
55
|
+
self.metadata = metadata
|
|
56
|
+
self.credentials = credentials
|
|
57
|
+
self.wait_for_ready = wait_for_ready
|
|
58
|
+
self.compression = compression
|
|
59
|
+
|
|
60
|
+
|
|
61
|
+
def _new_client_call_details(original, new_method):
|
|
62
|
+
return _ClientCallDetails(
|
|
63
|
+
method=new_method,
|
|
64
|
+
timeout=original.timeout,
|
|
65
|
+
metadata=original.metadata,
|
|
66
|
+
credentials=original.credentials,
|
|
67
|
+
wait_for_ready=original.wait_for_ready,
|
|
68
|
+
compression=original.compression,
|
|
69
|
+
)
|
|
70
|
+
|
|
71
|
+
|
|
72
|
+
class SandboxClient:
|
|
73
|
+
"""Client for interacting with the Agent Sandbox Orchestrator via gRPC."""
|
|
74
|
+
|
|
75
|
+
def __init__(self, host: Optional[str] = None, port: int = 50051, address: Optional[str] = None):
|
|
76
|
+
"""
|
|
77
|
+
Initialize the SandboxClient.
|
|
78
|
+
|
|
79
|
+
Args:
|
|
80
|
+
host: The hostname or IP address of the orchestrator.
|
|
81
|
+
port: The port number of the orchestrator gRPC service.
|
|
82
|
+
address: Full address for proxy-based routing (e.g.
|
|
83
|
+
``localhost:8092/v1/service/sandbox``). The path portion
|
|
84
|
+
is extracted and prepended to each gRPC method via an
|
|
85
|
+
interceptor so the OCF proxy can route the request.
|
|
86
|
+
Overrides *host* and *port*.
|
|
87
|
+
"""
|
|
88
|
+
if address:
|
|
89
|
+
# Strip scheme if the user passed one — gRPC channels don't use schemes.
|
|
90
|
+
addr = address
|
|
91
|
+
for scheme in ("http://", "https://"):
|
|
92
|
+
if addr.startswith(scheme):
|
|
93
|
+
addr = addr[len(scheme):]
|
|
94
|
+
break
|
|
95
|
+
# Split "host:port/path..." into the channel target and the proxy path.
|
|
96
|
+
# urlparse needs a scheme, so we add one temporarily.
|
|
97
|
+
parsed = urlparse(f"http://{addr}")
|
|
98
|
+
channel_target = f"{parsed.hostname}:{parsed.port or 80}"
|
|
99
|
+
proxy_path = parsed.path # e.g. "/v1/service/sandbox"
|
|
100
|
+
elif host:
|
|
101
|
+
channel_target = f"{host}:{port}"
|
|
102
|
+
proxy_path = ""
|
|
103
|
+
else:
|
|
104
|
+
raise ValueError("Must provide either an address or a host")
|
|
105
|
+
|
|
106
|
+
self.address = channel_target
|
|
107
|
+
self.channel = grpc.insecure_channel(channel_target)
|
|
108
|
+
|
|
109
|
+
if proxy_path and proxy_path != "/":
|
|
110
|
+
interceptor = _OCFProxyInterceptor(proxy_path)
|
|
111
|
+
self.channel = grpc.intercept_channel(self.channel, interceptor)
|
|
112
|
+
|
|
113
|
+
self.stub = sandbox_pb2_grpc.SandboxServiceStub(self.channel)
|
|
114
|
+
|
|
115
|
+
def close(self):
|
|
116
|
+
"""Close the gRPC channel."""
|
|
117
|
+
self.channel.close()
|
|
118
|
+
|
|
119
|
+
def __enter__(self):
|
|
120
|
+
return self
|
|
121
|
+
|
|
122
|
+
def __exit__(self, exc_type, exc_value, traceback):
|
|
123
|
+
self.close()
|
|
124
|
+
|
|
125
|
+
def start_sandbox(
|
|
126
|
+
self,
|
|
127
|
+
type: str,
|
|
128
|
+
image: str,
|
|
129
|
+
command: Optional[List[str]] = None,
|
|
130
|
+
memory_mb: int = 512,
|
|
131
|
+
cpu_cores: float = 1.0,
|
|
132
|
+
) -> str:
|
|
133
|
+
"""
|
|
134
|
+
Start a new sandbox.
|
|
135
|
+
|
|
136
|
+
Args:
|
|
137
|
+
type: The type of sandbox (e.g., "docker", "firecracker").
|
|
138
|
+
image: The image to use for the sandbox.
|
|
139
|
+
command: The command to run in the sandbox.
|
|
140
|
+
memory_mb: Memory limit in MB.
|
|
141
|
+
cpu_cores: CPU cores limit.
|
|
142
|
+
|
|
143
|
+
Returns:
|
|
144
|
+
The ID of the started sandbox.
|
|
145
|
+
"""
|
|
146
|
+
request = sandbox_pb2.StartRequest(
|
|
147
|
+
type=type,
|
|
148
|
+
image=image,
|
|
149
|
+
command=command or [],
|
|
150
|
+
memory_mb=memory_mb,
|
|
151
|
+
cpu_cores=cpu_cores,
|
|
152
|
+
)
|
|
153
|
+
response = self.stub.StartSandbox(request)
|
|
154
|
+
return response.id
|
|
155
|
+
|
|
156
|
+
def stop_sandbox(self, sandbox_id: str) -> None:
|
|
157
|
+
"""
|
|
158
|
+
Stop a running sandbox.
|
|
159
|
+
|
|
160
|
+
Args:
|
|
161
|
+
sandbox_id: The ID of the sandbox to stop.
|
|
162
|
+
"""
|
|
163
|
+
request = sandbox_pb2.StopRequest(id=sandbox_id)
|
|
164
|
+
self.stub.StopSandbox(request)
|
|
165
|
+
|
|
166
|
+
def exec_command(self, sandbox_id: str, command: List[str]) -> sandbox_pb2.ExecResponse:
|
|
167
|
+
"""
|
|
168
|
+
Execute a command in a running sandbox.
|
|
169
|
+
|
|
170
|
+
Args:
|
|
171
|
+
sandbox_id: The ID of the sandbox.
|
|
172
|
+
command: The command to execute.
|
|
173
|
+
|
|
174
|
+
Returns:
|
|
175
|
+
The execution response containing stdout, stderr, and exit code.
|
|
176
|
+
"""
|
|
177
|
+
request = sandbox_pb2.ExecRequest(id=sandbox_id, command=command)
|
|
178
|
+
return self.stub.ExecCommand(request)
|
|
179
|
+
|
|
180
|
+
def get_status(self, sandbox_id: str) -> str:
|
|
181
|
+
"""
|
|
182
|
+
Get the status of a sandbox.
|
|
183
|
+
|
|
184
|
+
Args:
|
|
185
|
+
sandbox_id: The ID of the sandbox.
|
|
186
|
+
|
|
187
|
+
Returns:
|
|
188
|
+
The status of the sandbox (e.g., "running", "stopped").
|
|
189
|
+
"""
|
|
190
|
+
request = sandbox_pb2.StatusRequest(id=sandbox_id)
|
|
191
|
+
response = self.stub.GetStatus(request)
|
|
192
|
+
return response.status
|
|
193
|
+
|
|
194
|
+
def snapshot_sandbox(self, sandbox_id: str) -> dict:
|
|
195
|
+
"""
|
|
196
|
+
Snapshot a running sandbox.
|
|
197
|
+
|
|
198
|
+
Args:
|
|
199
|
+
sandbox_id: The ID of the sandbox to snapshot.
|
|
200
|
+
|
|
201
|
+
Returns:
|
|
202
|
+
A dictionary containing the paths to the snapshot file and memory file.
|
|
203
|
+
"""
|
|
204
|
+
request = sandbox_pb2.SnapshotRequest(id=sandbox_id)
|
|
205
|
+
response = self.stub.SnapshotSandbox(request)
|
|
206
|
+
return {
|
|
207
|
+
"snapshot_path": response.snapshot_path,
|
|
208
|
+
"mem_file_path": response.mem_file_path,
|
|
209
|
+
}
|
|
210
|
+
|
|
211
|
+
def resume_sandbox(self, sandbox_id: str) -> None:
|
|
212
|
+
"""
|
|
213
|
+
Resume a paused or snapshotted sandbox.
|
|
214
|
+
|
|
215
|
+
Args:
|
|
216
|
+
sandbox_id: The ID of the sandbox to resume.
|
|
217
|
+
"""
|
|
218
|
+
request = sandbox_pb2.ResumeRequest(id=sandbox_id)
|
|
219
|
+
self.stub.ResumeSandbox(request)
|
|
220
|
+
|