Skip to content

Commit f5052c5

Browse files
release: v0.1.0 readiness — pipeline, networking quickstart, ForkVm RPC (#70)
1 parent 894337a commit f5052c5

17 files changed

Lines changed: 236 additions & 13 deletions

File tree

.github/workflows/release.yml

Lines changed: 32 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -38,16 +38,18 @@ jobs:
3838
run: |
3939
version="${GITHUB_REF_NAME#v}"
4040
41-
# The tag is the single source of truth: stamp it into both manifests.
42-
# Cargo.toml's [package] version is the only line starting with
43-
# `version = `; mix.exs has exactly one `version: "..."` (the project
44-
# version). `0,/re/` rewrites just the first match.
45-
sed -i -E "0,/^version = \"[^\"]*\"/s//version = \"${version}\"/" native/suidhelper/Cargo.toml
41+
# The tag is the single source of truth: stamp it into every package
42+
# manifest. `0,/re/` rewrites only the first match, which for each
43+
# Cargo.toml is its own `[package] version` (xtask is intentionally
44+
# left at 0.0.0 — it is publish = false).
45+
for m in native/suidhelper/Cargo.toml native/suidhelper/meta/Cargo.toml native/guest-agent/Cargo.toml; do
46+
sed -i -E "0,/^version = \"[^\"]*\"/s//version = \"${version}\"/" "$m"
47+
done
4648
sed -i -E "0,/version: \"[^\"]*\"/s//version: \"${version}\"/" mix.exs
4749
4850
git config user.name "github-actions[bot]"
4951
git config user.email "41898282+github-actions[bot]@users.noreply.github.com"
50-
git add native/suidhelper/Cargo.toml mix.exs
52+
git add native/suidhelper/Cargo.toml native/suidhelper/meta/Cargo.toml native/guest-agent/Cargo.toml mix.exs
5153
5254
# Idempotent: a re-run after the bump already landed has nothing to commit.
5355
if git diff --cached --quiet; then
@@ -133,6 +135,18 @@ jobs:
133135
- name: Install dependencies
134136
run: mix deps.get
135137

138+
# The gitignored gRPC/guest-agent/suidhelper artifacts are regenerated by
139+
# Mix compilers on every compile; hex.build compiles, so the runner needs
140+
# their toolchain. Mirrors ci.yml.
141+
- name: Install protoc
142+
run: sudo apt-get update && sudo apt-get install -y protobuf-compiler
143+
144+
- name: Install protoc-gen-elixir
145+
run: mix escript.install hex protobuf 0.17.0 --force
146+
147+
- name: Install musl target for the guest-agent build
148+
run: rustup target add x86_64-unknown-linux-musl
149+
136150
- name: Build package tarball
137151
run: mix hex.build --output "hypervm-${{ needs.prepare-release.outputs.version }}.tar"
138152

@@ -191,6 +205,18 @@ jobs:
191205
- name: Install dependencies
192206
run: mix deps.get
193207

208+
# The gitignored gRPC/guest-agent/suidhelper artifacts are regenerated by
209+
# Mix compilers on every compile; hex.build compiles, so the runner needs
210+
# their toolchain. Mirrors ci.yml.
211+
- name: Install protoc
212+
run: sudo apt-get update && sudo apt-get install -y protobuf-compiler
213+
214+
- name: Install protoc-gen-elixir
215+
run: mix escript.install hex protobuf 0.17.0 --force
216+
217+
- name: Install musl target for the guest-agent build
218+
run: rustup target add x86_64-unknown-linux-musl
219+
194220
# HEX_API_KEY authenticates non-interactively; --yes skips the prompt.
195221
# Publishes both the package and its docs.
196222
- name: hex publish

CHANGELOG.md

Lines changed: 28 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,28 @@
1+
# Changelog
2+
3+
All notable changes to this project are documented here. The format is based on
4+
[Keep a Changelog](https://keepachangelog.com/en/1.1.0/), and this project
5+
adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
6+
7+
## [Unreleased]
8+
9+
## [0.1.0]
10+
11+
First public release: a distributed orchestrator for Firecracker microVMs on the
12+
BEAM.
13+
14+
### Added
15+
- Firecracker microVM lifecycle: create, stop, locate, and list VMs across a
16+
cluster, with automatic layer-affinity placement.
17+
- Content-addressed OCI image loading into a shared media store + image database.
18+
- Copy-on-write disk **forking** (`Hyper.Vm.fork/1` and the `ForkVm` gRPC RPC):
19+
colocated thin-snapshot forks with a cluster-wide delta-layer fallback.
20+
- Per-VM egress networking: netns + TAP + nftables NAT.
21+
- Granular per-VM compute metering (CPU-time, from the cgroup) for billing.
22+
- Guest exec over vsock (`Hyper.exec/3`).
23+
- gRPC interface (`hyper.grpc.v0`, UNSTABLE) — off by default, unauthenticated.
24+
- Privileged Rust setuid helper for the host operations the BEAM cannot do safely.
25+
- OpenTelemetry tracing across the subsystems.
26+
27+
[Unreleased]: https://github.com/harmont-dev/hyper/compare/v0.1.0...HEAD
28+
[0.1.0]: https://github.com/harmont-dev/hyper/releases/tag/v0.1.0

README.md

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -39,7 +39,7 @@ Then load an OCI image and boot it:
3939

4040
```elixir
4141
{:ok, img_id} = Hyper.Img.OciLoader.load("docker.io/library/alpine:3.19")
42-
{:ok, vm} = Hyper.create_vm(%Hyper.Vm.Spec{img_id: img_id})
42+
{:ok, vm} = Hyper.create_vm(%Hyper.Vm.Spec{img_id: img_id, type: :micro})
4343
{:ok, %{stdout: "hello\n"}} = Hyper.exec(vm, ["/bin/echo", "hello"])
4444
```
4545

@@ -84,6 +84,7 @@ integrating Hyper.
8484
BEAM-native interfaces, we recognize this may not be ideal for all languages
8585
and existing stacks. For that reason, Hyper has a GRPC interface, so you can
8686
call it from any language you already use.
87+
⚠️ The gRPC API is unauthenticated and off by default — bind it to a trusted network or front it with an authenticating proxy. See [the gRPC guide](docs/grpc.md).
8788

8889
## Docs
8990

docs/cookbook/config.md

Lines changed: 26 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -108,6 +108,32 @@ uid_gid_range = [900000, 999999]
108108
```
109109
<!-- tabs close -->
110110

111+
## VM Networking Configuration
112+
113+
**Required.** Every VM gets NAT'd egress out through a physical host interface,
114+
and a node refuses to start without a `[network]` table (see the
115+
[install guide](install.md#vm-networking-required) for the host prerequisites:
116+
`iproute2`/`nftables` and `net.ipv4.ip_forward=1`). `config.toml`-only — the
117+
setuid helper reads the same `uplink` and `clone_pool` to build each VM's netns,
118+
veth pair, and NAT rules, so it cannot live in `config.exs`.
119+
120+
### `config.toml`
121+
122+
```toml
123+
[network]
124+
# **required**. The physical uplink interface guest egress is NAT'd out through.
125+
# On a single-uplink host, the default-route NIC is the usual choice:
126+
# ip route show default | awk '{print $5; exit}'
127+
uplink = "eth0"
128+
129+
# optional -- defaults shown. The IPv4 CIDR the per-VM /30s are carved from.
130+
clone_pool = "172.31.0.0/16"
131+
132+
# optional -- defaults shown. DNS resolver handed to guests via the kernel
133+
# cmdline (the guest agent writes it to /etc/resolv.conf).
134+
resolver = "1.1.1.1"
135+
```
136+
111137
## gRPC Configuration
112138

113139
Hyper supports a [gRPC](https://grpc.io/) interface enabling you to interface

docs/cookbook/install.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -45,7 +45,7 @@ You can install the required packages by running:
4545
sudo dnf install -y \
4646
coreutils \
4747
device-mapper-persistent-data \
48-
e2fsprogs \
48+
e2fsprogs \
4949
glibc-common \
5050
kernel-modules-extra-$(uname -r) \
5151
lvm2 \

docs/cookbook/intro.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -65,7 +65,7 @@ interface, or through [gRPC](../grpc.md):
6565
With the image loaded, and an `img_id` in hand, you can boot it:
6666

6767
```elixir
68-
{:ok, vm} = Hyper.create_vm(%Hyper.Vm.Spec{img_id: img_id})
68+
{:ok, vm} = Hyper.create_vm(%Hyper.Vm.Spec{img_id: img_id, type: :micro})
6969
```
7070

7171
The VM is scheduled onto the most available node in the cluster, preferring

docs/grpc.md

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,15 @@ create, stop, locate, and list microVMs.
88
> **v0 -- unstable.** The contract may change without notice during early
99
> development. Pin to a commit if you depend on it.
1010
11+
> #### No authentication {: .warning}
12+
>
13+
> The gRPC API has **no authentication**. Any client that can reach the port can
14+
> create and stop VMs and load images. It is **off by default** (`grpc.enabled =
15+
> false`). When you enable it, bind it to loopback or a trusted network, or put
16+
> it behind a proxy that terminates TLS and authenticates callers. TLS (`cred`)
17+
> encrypts the channel but does not authenticate the client. Authentication is
18+
> planned for a later release; the `v0` contract is UNSTABLE.
19+
1120
## Configuration
1221

1322
By default, the gRPC interface is **disabled**. The simplest way to enable it

lib/hyper.ex

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -166,6 +166,19 @@ defmodule Hyper do
166166
:error, {:erpc, _} -> {:error, :machine_unreachable}
167167
end
168168

169+
@doc """
170+
Fork the running VM `vm_id`: boot a child from a copy-on-write clone of its
171+
disk, colocated when the parent's node has room and re-placed cluster-wide
172+
otherwise. Returns the child VM. See `Hyper.Vm.fork/1` for the semantics.
173+
"""
174+
@spec fork_vm(Hyper.Vm.Id.t()) :: {:ok, Hyper.Vm.t()} | {:error, term()}
175+
def fork_vm(vm_id) when is_binary(vm_id) do
176+
case Hyper.Cluster.Routing.supervisor_of(vm_id) do
177+
nil -> {:error, :not_found}
178+
pid -> Hyper.Vm.fork(pid)
179+
end
180+
end
181+
169182
@doc """
170183
The vm id for a VM handle -- the pid returned by `create_vm/1`. Resolves on the
171184
pid's owning node, so a VM just placed on a remote node is found immediately

lib/hyper/grpc/codec.ex

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,7 @@ defmodule Hyper.Grpc.Codec do
1313
alias Hyper.Grpc.V0.{
1414
CreateVmRequest,
1515
CreateVmResponse,
16+
ForkVmResponse,
1617
GetVmResponse,
1718
GetVmUsageResponse,
1819
ListVmsResponse,
@@ -75,6 +76,10 @@ defmodule Hyper.Grpc.Codec do
7576
def to_grpc({:created, vm_id, node}) when is_binary(vm_id),
7677
do: %CreateVmResponse{vm_id: vm_id, node: to_string(node)}
7778

79+
@spec to_grpc({:forked, Hyper.Vm.Id.t(), node()}) :: ForkVmResponse.t()
80+
def to_grpc({:forked, vm_id, node}) when is_binary(vm_id),
81+
do: %ForkVmResponse{vm_id: vm_id, node: to_string(node)}
82+
7883
@spec to_grpc({:located, Hyper.Vm.Id.t(), node()}) :: GetVmResponse.t()
7984
def to_grpc({:located, vm_id, node}),
8085
do: %GetVmResponse{vm_id: vm_id, node: to_string(node)}
@@ -126,6 +131,9 @@ defmodule Hyper.Grpc.Codec do
126131
defp rpc_error(:machine_unreachable),
127132
do: GRPC.RPCError.exception(:unavailable, "VM's host node is unreachable")
128133

134+
defp rpc_error(:node_unreachable),
135+
do: GRPC.RPCError.exception(:unavailable, "VM's host node is unreachable")
136+
129137
defp rpc_error(reason) when reason in [:no_capacity, :exhausted],
130138
do: GRPC.RPCError.exception(:resource_exhausted, "no capacity")
131139

lib/hyper/grpc/server.ex

Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,8 @@ defmodule Hyper.Grpc.Server do
1515
alias Hyper.Grpc.V0.{
1616
CreateVmRequest,
1717
CreateVmResponse,
18+
ForkVmRequest,
19+
ForkVmResponse,
1820
GetVmRequest,
1921
GetVmResponse,
2022
GetVmUsageRequest,
@@ -50,6 +52,20 @@ defmodule Hyper.Grpc.Server do
5052
end
5153
end
5254

55+
@spec fork_vm(ForkVmRequest.t(), GRPC.Server.Stream.t()) :: ForkVmResponse.t()
56+
@decorate with_span("Hyper.Grpc.Server.fork_vm", include: [:vm_id])
57+
def fork_vm(%ForkVmRequest{vm_id: vm_id}, _stream) do
58+
with {:ok, child} <- Hyper.fork_vm(vm_id),
59+
child_id when is_binary(child_id) <- Hyper.id(child) do
60+
Codec.to_grpc({:forked, child_id, node(child)})
61+
else
62+
# The child was placed but its host became unreachable before its id
63+
# could be confirmed — same shape as create_vm/2.
64+
nil -> raise Codec.to_grpc({:error, :machine_unreachable})
65+
{:error, reason} -> raise Codec.to_grpc({:error, reason})
66+
end
67+
end
68+
5369
@spec stop_vm(StopVmRequest.t(), GRPC.Server.Stream.t()) :: Empty.t()
5470
@decorate with_span("Hyper.Grpc.Server.stop_vm", include: [:vm_id])
5571
def stop_vm(%StopVmRequest{vm_id: vm_id}, _stream) do

0 commit comments

Comments
 (0)