Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
14 changes: 14 additions & 0 deletions Cargo.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

16 changes: 8 additions & 8 deletions architecture/podman-rootless-networking.md
Original file line number Diff line number Diff line change
Expand Up @@ -35,7 +35,7 @@ For rootful bridge networking:
6. Netavark configures iptables/nftables rules -- masquerade for outbound, DNAT for port mappings
7. Netavark starts aardvark-dns if DNS is enabled, listening on the bridge gateway address

```
```text
Host Kernel
|
+-- Bridge interface (e.g., "podman0") <-- created by Netavark
Expand All @@ -60,7 +60,7 @@ Unprivileged users cannot create network interfaces on the host. They cannot cre

Pasta (part of the `passt` project -- same binary, different command name) operates entirely in userspace, translating between the container's L2 TAP interface and the host's L4 sockets. It requires no capabilities or privileges.

```
```text
Container Network Namespace
|
+-- TAP device (e.g., "eth0")
Expand Down Expand Up @@ -131,7 +131,7 @@ Unlike bridge networking, pasta containers are isolated from each other by defau

The Podman compute driver creates three layers of network isolation:

```
```text
Namespace 1: Host
|
pasta manages port forwarding (127.0.0.1:<ephemeral>)
Expand Down Expand Up @@ -164,7 +164,7 @@ client.ensure_network(&config.network_name).await?;

This creates a bridge network named `"openshell"` (default from `DEFAULT_NETWORK_NAME` in `openshell-core/src/config.rs`) with `dns_enabled: true`. In rootless mode, this bridge exists inside a user namespace managed by pasta. The bridge IP range (e.g., `10.89.x.x`) is not routable from the host.

```
```text
Host (your machine)
|
127.0.0.1:<ephemeral> <--- pasta binds this on the host
Expand Down Expand Up @@ -212,7 +212,7 @@ The bridge gateway IP does NOT work for this purpose in rootless mode because it

Inside the container, the supervisor creates another network namespace (`netns.rs:53-178`, setup at lines 53-63, `ip netns add` at line 77) for the user workload:

```
```text
Container (10.89.1.2 on the Podman bridge)
|
[Supervisor process - runs in container's default netns]
Expand Down Expand Up @@ -247,7 +247,7 @@ A tmpfs is mounted at `/run/netns` in the container spec (`container.rs:458-463`

### SSH Session: Client to Sandbox Shell

```
```text
Client (CLI on user's machine)
|
1. gRPC: CreateSshSession -> gateway (returns token, connect_path)
Expand Down Expand Up @@ -281,7 +281,7 @@ The SSH daemon listens on a Unix socket (not a TCP port) with 0600 permissions.

### Outbound HTTP Request from Sandbox Process

```
```text
User's code (inner netns, 10.200.0.2)
|
1. curl https://api.example.com
Expand All @@ -306,7 +306,7 @@ Supervisor proxy (10.200.0.1:3128 in container netns)

### Supervisor gRPC Callback to Gateway

```
```text
Supervisor (container netns, 10.89.x.2)
|
1. gRPC connect to http://host.containers.internal:8080
Expand Down
8 changes: 8 additions & 0 deletions crates/openshell-cli/src/main.rs
Original file line number Diff line number Diff line change
Expand Up @@ -1138,6 +1138,11 @@ enum SandboxCommands {
#[arg(long)]
gpu: bool,

/// Target a specific GPU by PCI address (e.g. "0000:2d:00.0") or index (e.g. "0", "1").
/// Only valid with --gpu. When omitted with --gpu, the first available GPU is assigned.
#[arg(long, requires = "gpu")]
gpu_device: Option<String>,
Comment on lines +1141 to +1144
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Just to clarify, this is not specific to the VM driver and could be mapped to requests in k8s, Docker, or Podman?

As a follow up question: Does it make sense to allow gpu_device to be specified multiple times to allow for multiple devices, or should validation (e.g. a comma-separated list) be delegated to the driver?

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Good question on both points. Yes, --gpu and --gpu-device are intentionally driver-agnostic — the proto defines them on CreateSandboxRequest and DriverSandboxSpec, so k8s/Docker/Podman drivers can map them to their native GPU request mechanisms. For multi-device: today the proto field is a single string, so multi-GPU per sandbox would need a proto change (repeated string gpu_devices) plus inventory updates. I propose to update this in a follow-up PR.

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I'm fine with a follow-up. Would an issue to discuss how users are expected to request GPUs be a good place to have a follow-up discussion? Some of the basic use cases that I can see are:

  1. A user wants a sandbox with any GPU. (count == 1)
  2. A user wants a sandbox with a specific number of GPUs. (count > 1).
  3. A user wants a sandbox with a SPECIFIC set of GPUs. (Specified by driver-specific IDs).

A more advanced use case that one could also start discussing is when a user wants a sandbox with access to one or more GPUs with specific properties. I would assume that this could also be reduced to a set of driver-specific IDs though, so maybe it is sufficient to demonstrate this transform.


/// SSH destination for remote bootstrap (e.g., user@hostname).
/// Only used when no cluster exists yet; ignored if a cluster is
/// already active.
Expand Down Expand Up @@ -2307,6 +2312,7 @@ async fn main() -> Result<()> {
no_keep,
editor,
gpu,
gpu_device,
remote,
ssh_key,
providers,
Expand Down Expand Up @@ -2402,6 +2408,7 @@ async fn main() -> Result<()> {
upload_spec.as_ref(),
keep,
gpu,
gpu_device.as_deref(),
editor,
remote.as_deref(),
ssh_key.as_deref(),
Expand All @@ -2425,6 +2432,7 @@ async fn main() -> Result<()> {
upload_spec.as_ref(),
keep,
gpu,
gpu_device.as_deref(),
editor,
remote.as_deref(),
ssh_key.as_deref(),
Expand Down
4 changes: 4 additions & 0 deletions crates/openshell-cli/src/run.rs
Original file line number Diff line number Diff line change
Expand Up @@ -1923,6 +1923,7 @@ pub async fn sandbox_create_with_bootstrap(
upload: Option<&(String, Option<String>, bool)>,
keep: bool,
gpu: bool,
gpu_device: Option<&str>,
editor: Option<Editor>,
remote: Option<&str>,
ssh_key: Option<&str>,
Expand Down Expand Up @@ -1954,6 +1955,7 @@ pub async fn sandbox_create_with_bootstrap(
upload,
keep,
gpu,
gpu_device,
editor,
remote,
ssh_key,
Expand Down Expand Up @@ -2010,6 +2012,7 @@ pub async fn sandbox_create(
upload: Option<&(String, Option<String>, bool)>,
keep: bool,
gpu: bool,
gpu_device: Option<&str>,
editor: Option<Editor>,
remote: Option<&str>,
ssh_key: Option<&str>,
Expand Down Expand Up @@ -2117,6 +2120,7 @@ pub async fn sandbox_create(
let request = CreateSandboxRequest {
spec: Some(SandboxSpec {
gpu: requested_gpu,
gpu_device: gpu_device.unwrap_or_default().to_string(),
policy,
providers: configured_providers,
template,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -574,6 +574,7 @@ async fn sandbox_create_keeps_command_sessions_by_default() {
None,
None,
None,
None,
&[],
None,
None,
Expand Down Expand Up @@ -615,6 +616,7 @@ async fn sandbox_create_deletes_command_sessions_with_no_keep() {
None,
None,
None,
None,
&[],
None,
None,
Expand Down Expand Up @@ -659,6 +661,7 @@ async fn sandbox_create_deletes_shell_sessions_with_no_keep() {
None,
None,
None,
None,
&[],
None,
None,
Expand Down Expand Up @@ -703,6 +706,7 @@ async fn sandbox_create_keeps_sandbox_with_hidden_keep_flag() {
None,
None,
None,
None,
&[],
None,
None,
Expand Down Expand Up @@ -744,6 +748,7 @@ async fn sandbox_create_keeps_sandbox_with_forwarding() {
None,
None,
None,
None,
&[],
None,
Some(openshell_core::forward::ForwardSpec::new(8080)),
Expand Down
1 change: 1 addition & 0 deletions crates/openshell-driver-docker/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -231,6 +231,7 @@ impl DockerComputeDriver {
driver_version: self.config.daemon_version.clone(),
default_image: self.config.default_image.clone(),
supports_gpu: false,
gpu_count: 0,
}
}

Expand Down
1 change: 1 addition & 0 deletions crates/openshell-driver-docker/src/tests.rs
Original file line number Diff line number Diff line change
Expand Up @@ -30,6 +30,7 @@ fn test_sandbox() -> DriverSandbox {
platform_config: None,
}),
gpu: false,
gpu_device: String::new(),
}),
status: None,
}
Expand Down
1 change: 1 addition & 0 deletions crates/openshell-driver-kubernetes/src/driver.rs
Original file line number Diff line number Diff line change
Expand Up @@ -150,6 +150,7 @@ impl KubernetesComputeDriver {
driver_version: openshell_core::VERSION.to_string(),
default_image: self.config.default_image.clone(),
supports_gpu: self.has_gpu_capacity().await.unwrap_or(false),
gpu_count: 0,
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Question: is a raw int rich enough here? Should a driver expose the valid names of devices that are available, for example?

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Agreed, a raw int is limited. A richer repeated GpuDeviceInfo message (with BDF, device name, availability) on GetCapabilitiesResponse would let the CLI show available devices and validate --gpu-device client-side. I propose to address this, along with the previous one, in a follow-up PR.

})
}

Expand Down
1 change: 1 addition & 0 deletions crates/openshell-driver-podman/src/driver.rs
Original file line number Diff line number Diff line change
Expand Up @@ -161,6 +161,7 @@ impl PodmanComputeDriver {
driver_version: openshell_core::VERSION.to_string(),
default_image: self.config.default_image.clone(),
supports_gpu,
gpu_count: 0,
})
}

Expand Down
3 changes: 3 additions & 0 deletions crates/openshell-driver-vm/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,7 @@ path = "src/main.rs"

[dependencies]
openshell-core = { path = "../openshell-core" }
openshell-vfio = { path = "../openshell-vfio" }

tokio = { workspace = true }
tonic = { workspace = true, features = ["transport"] }
Expand All @@ -32,6 +33,8 @@ tracing = { workspace = true }
tracing-subscriber = { workspace = true }
miette = { workspace = true }
url = { workspace = true }
serde = { workspace = true }
serde_json = { workspace = true }
libc = "0.2"
libloading = "0.8"
tar = "0.4"
Expand Down
18 changes: 12 additions & 6 deletions crates/openshell-driver-vm/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -36,9 +36,17 @@ mise run gateway:vm
```

First run takes a few minutes while `mise run vm:setup` stages libkrun/libkrunfw/gvproxy and `mise run vm:rootfs -- --base` builds the embedded rootfs. Subsequent runs are cached. To keep the Unix socket path under macOS `SUN_LEN`, `mise run gateway:vm` and `start.sh` default the state dir to `/tmp/openshell-vm-driver-dev-$USER-port-$PORT/` (SQLite DB + per-sandbox rootfs + `compute-driver.sock`) unless `OPENSHELL_VM_DRIVER_STATE_DIR` is set.
The wrapper also prints the recommended gateway name (`vm-driver-port-$PORT` by default) plus the exact repo-local `scripts/bin/openshell gateway add` and `scripts/bin/openshell gateway select` commands to use from another terminal. This avoids accidentally hitting an older `openshell` binary elsewhere on your `PATH`.
The wrapper auto-registers the gateway with the CLI (`gateway destroy` + `gateway add`) so no manual registration step is needed. When running under `sudo`, it uses `sudo -u $SUDO_USER` for the registration so the config is written under the invoking user's home directory. Re-runs are idempotent.
It also exports `OPENSHELL_DRIVER_DIR=$PWD/target/debug` before starting the gateway so local dev runs use the freshly built `openshell-driver-vm` instead of an older installed copy from `~/.local/libexec/openshell` or `/usr/local/libexec`.

For GPU passthrough (VFIO), pass `-- --gpu` and run with root privileges:

```shell
sudo -E env "PATH=$PATH" mise run gateway:vm -- --gpu
```

See [`architecture/vm-gpu-sandbox-guide.md`](../../architecture/vm-gpu-sandbox-guide.md) for full GPU prerequisites and usage.

Override via environment:

```shell
Expand Down Expand Up @@ -129,13 +137,11 @@ See [`openshell-gateway --help`](../openshell-server/src/cli.rs) for the full fl

## Verifying the gateway

In another terminal:
The gateway is auto-registered by `start.sh`. In another terminal:

```shell
export OPENSHELL_GATEWAY_URL=http://127.0.0.1:8080
cargo run -p openshell-cli -- gateway register local --url $OPENSHELL_GATEWAY_URL --no-tls
cargo run -p openshell-cli -- sandbox create --name demo
cargo run -p openshell-cli -- sandbox connect demo
scripts/bin/openshell sandbox create --name demo
scripts/bin/openshell sandbox connect demo
```

First sandbox takes 10–30 seconds to boot (rootfs extraction + libkrun + guest init). Subsequent creates reuse the prepared sandbox rootfs.
Expand Down
20 changes: 18 additions & 2 deletions crates/openshell-driver-vm/build.rs
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,7 @@ fn main() {
"libkrunfw.5.dylib.zst",
"gvproxy.zst",
"rootfs.tar.zst",
"rootfs-gpu.tar.zst",
] {
println!("cargo:rerun-if-changed={dir}/{name}");
}
Expand All @@ -36,7 +37,15 @@ fn main() {
"linux" => ("libkrun.so", "libkrunfw.so.5"),
_ => {
println!("cargo:warning=VM runtime not available for {target_os}-{target_arch}");
generate_stub_resources(&out_dir, &["libkrun", "libkrunfw", "rootfs.tar.zst"]);
generate_stub_resources(
&out_dir,
&[
"libkrun",
"libkrunfw",
"rootfs.tar.zst",
"rootfs-gpu.tar.zst",
],
);
return;
}
};
Expand All @@ -53,6 +62,7 @@ fn main() {
&format!("{libkrunfw_name}.zst"),
"gvproxy.zst",
"rootfs.tar.zst",
"rootfs-gpu.tar.zst",
],
);
return;
Expand All @@ -71,6 +81,7 @@ fn main() {
&format!("{libkrunfw_name}.zst"),
"gvproxy.zst",
"rootfs.tar.zst",
"rootfs-gpu.tar.zst",
],
);
return;
Expand All @@ -84,6 +95,10 @@ fn main() {
),
("gvproxy.zst".to_string(), "gvproxy.zst".to_string()),
("rootfs.tar.zst".to_string(), "rootfs.tar.zst".to_string()),
(
"rootfs-gpu.tar.zst".to_string(),
"rootfs-gpu.tar.zst".to_string(),
),
];

let mut all_found = true;
Expand Down Expand Up @@ -124,12 +139,13 @@ fn main() {
&format!("{libkrunfw_name}.zst"),
"gvproxy.zst",
"rootfs.tar.zst",
"rootfs-gpu.tar.zst",
],
);
}
}

fn generate_stub_resources(out_dir: &PathBuf, names: &[&str]) {
fn generate_stub_resources(out_dir: &std::path::Path, names: &[&str]) {
for name in names {
let path = out_dir.join(name);
if !path.exists() {
Expand Down
Loading
Loading