Skip to content
Merged
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
7 changes: 4 additions & 3 deletions .github/scripts/provision-kvm-host.sh
Original file line number Diff line number Diff line change
@@ -1,10 +1,11 @@
#!/usr/bin/env bash
# Provision a GitHub-hosted ubuntu runner as a single-node Firecracker host
# for the `:integration` E2E suite. Mirrors docs/cookbook/install.md; keep
# the two in sync. Assumes: passwordless sudo, repo compiled (the firecracker
# the two (and setup.sh, the dev-host equivalent) in sync. Assumes:
# passwordless sudo, repo compiled (the firecracker
# and suidhelper install tasks are mix tasks), MIX_ENV matching the test run.
#
# Deliberate CI-only deltas from install.md do NOT remove these in a future
# Deliberate CI-only deltas from install.md -- do NOT remove these in a future
# sync pass:
# - the 0666 udev kvm rule (install.md assumes a real host with a `kvm`
# group an operator is added to; the ephemeral runner has neither)
Expand Down Expand Up @@ -72,7 +73,7 @@ sudo chmod 0644 /etc/hyper/config.toml
sudo mkdir -p /sys/fs/cgroup/hyper
echo '+cpu +memory' | sudo tee /sys/fs/cgroup/hyper/cgroup.subtree_control >/dev/null

# The BEAM (the `runner` user Hyper refuses to run as root) owns work_dir.
# The BEAM (the `runner` user -- Hyper refuses to run as root) owns work_dir.
# layers/ must pre-exist: boot validation (Layer.Repo.test_system) checks it,
# and the node only creates it lazily on first image load.
sudo mkdir -p /srv/hyper
Expand Down
18 changes: 15 additions & 3 deletions .github/workflows/ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -152,6 +152,18 @@ jobs:
- name: Dialyzer
run: mix dialyzer

shellcheck:
name: ShellCheck
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v7

# shellcheck ships preinstalled on GitHub-hosted ubuntu runners. Lint
# every tracked shell script; `git ls-files` keeps new scripts covered
# without a hand-maintained list and skips untracked _build/deps.
- name: ShellCheck
run: git ls-files -z '*.sh' | xargs -0 -r shellcheck

rust:
name: Rust (suidhelper)
runs-on: ubuntu-latest
Expand Down Expand Up @@ -271,14 +283,14 @@ jobs:
integration:
name: Integration (KVM E2E)
# ubuntu-latest x64 exposes /dev/kvm (all Linux runners with 2+ vCPUs,
# per GitHub's Apr 2024 changelog); arm64 runners do NOT keep this x64.
# per GitHub's Apr 2024 changelog); arm64 runners do NOT -- keep this x64.
runs-on: ubuntu-latest
# Worst case: three 10-minute tests (vm_lifecycle + crash_recovery + fork),
# plus the gRPC contract wrapper's own 25-minute cap (20-min lifecycle
# vitest test + errors suite + npm ci/gen), plus ~5 minutes of
# provisioning/compile/npm setup. Budget with headroom above that sum: a
# tight budget could cancel the job right as it finishes, and
# `!cancelled()` steps then skip result uploads exactly when flake
# `!cancelled()` steps then skip result uploads -- exactly when flake
# history matters most.
timeout-minutes: 75
env:
Expand Down Expand Up @@ -379,7 +391,7 @@ jobs:
# Multiple --only flags OR together: this runs ONLY the gated tags.
# coveralls.json wraps `mix test`, so the tag filters and
# --warnings-as-errors pass through; it writes cover/excoveralls.json,
# which is this job's Codecov upload without it the integration-only
# which is this job's Codecov upload -- without it the integration-only
# modules (e.g. Hyper.Metering.Usage) report zero coverage.
- name: Integration + external tests
run: mix coveralls.json --only integration --only external --warnings-as-errors
Expand Down
37 changes: 24 additions & 13 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -26,29 +26,40 @@ same niche as [Daytona](https://github.com/daytonaio/daytona),

## Quick Start

Add Hyper to your Mix project:
On an Ubuntu/Debian machine with KVM (bare metal, or a cloud instance with
nested virtualization):

```elixir
def deps do
[
{:hypervm, "~> 0.1"}
]
end
```sh
git clone https://github.com/harmont-dev/hyper && cd hyper
./setup.sh # one-time host provisioning (asks for sudo)
iex -S mix # boot a Hyper node
```

Prepare the host (KVM, device-mapper, PostgreSQL, the Firecracker binaries and
the setuid helper — the [installation
guide](https://hexdocs.pm/hypervm/install.html) walks through each step), then
load an OCI image and boot it:
Then load an OCI image and boot it:

```elixir
{:ok, img_id} = Hyper.Img.OciLoader.load("docker.io/library/alpine:3.19")
{:ok, vm} = Hyper.create_vm(%Hyper.Vm.Spec{img_id: img_id})
{:ok, %{stdout: "hello\n"}} = Hyper.exec(vm, ["/bin/echo", "hello"])
```

Please read the [Hexdocs](https://hexdocs.pm/hypervm/) for guides on using,
deploying and integrating Hyper.
That's a real Firecracker microVM with a copy-on-write rootfs -- the
[quickstart guide](https://hexdocs.pm/hypervm/quickstart.html) has the
details. To embed Hyper in your own application instead, add it to your Mix
project:

```elixir
def deps do
[
{:hypervm, "~> 0.1"}
]
end
```

and follow the [installation
guide](https://hexdocs.pm/hypervm/install.html). The
[Hexdocs](https://hexdocs.pm/hypervm/) cover using, deploying and
integrating Hyper.

## Features

Expand Down
30 changes: 16 additions & 14 deletions docs/cookbook/install.md
Original file line number Diff line number Diff line change
@@ -1,6 +1,8 @@
# Quick Start
# Installation

This document provides the quickest start available to get Hyper running.
This document walks through a full installation of Hyper on a node. For the
fastest dev/eval path, see the [quickstart](quickstart.md), which automates
this guide via `setup.sh`.

## Configuration

Expand Down Expand Up @@ -67,8 +69,8 @@ sudo dnf install -y \

### Build Toolchain

Hyper compiles two Rust components as part of `mix compile` the setuid
helper and the in-guest agent and generates its Firecracker/gRPC bindings
Hyper compiles two Rust components as part of `mix compile` -- the setuid
helper and the in-guest agent -- and generates its Firecracker/gRPC bindings
from the shipped specs. Every machine that **compiles** Hyper (including as a
Mix dependency) therefore needs, besides Elixir `~> 1.20` on OTP 28+:

Expand All @@ -84,7 +86,7 @@ sudo apt install -y protobuf-compiler # dnf install protobuf-compiler
mix escript.install hex protobuf 0.17.0
```

`~/.mix/escripts` does not need to be on your `PATH` the build finds
`~/.mix/escripts` does not need to be on your `PATH` -- the build finds
`protoc-gen-elixir` there itself.

### Device Mapper Config
Expand Down Expand Up @@ -233,14 +235,14 @@ as this user; every operation that genuinely needs root is routed through the
setuid helper (see [SUID Helper](#suid-helper)), so the node itself never holds
privilege.

Create the user system account, no login shell:
Create the user -- system account, no login shell:

```sh
sudo useradd --system --shell /usr/sbin/nologin --home-dir /srv/hyper hyper
```

Start Hyper as this user (for example `sudo -u hyper ...`, or `User=hyper` in a
systemd unit). The rest of this section covers the few permissions it needs
systemd unit). The rest of this section covers the few permissions it needs --
and the ones it deliberately does **not**.

#### Working directory
Expand All @@ -249,7 +251,7 @@ The node builds its entire on-disk tree (`jails`, `socks`, `scratch`, `layers`,
`redist`) under `work_dir` (from `/etc/hyper/config.toml`, default `/srv/hyper`)
**as this user**. It must therefore own that directory. Boot validation
(`Hyper.Node.Layer.Repo.test_system/0`) refuses to start unless the `layers`
subdirectory already exists the node only creates it lazily on first image
subdirectory already exists -- the node only creates it lazily on first image
load, so pre-create it now:

```sh
Expand All @@ -274,7 +276,7 @@ end
```

Then `mix deps.get && mix compile`. Alternatively, work from a source
checkout of the [repository](https://github.com/harmont-dev/hyper) every
checkout of the [repository](https://github.com/harmont-dev/hyper) -- every
step below is identical.

### Firecracker
Expand All @@ -288,7 +290,7 @@ mix firecracker.install # installs to /opt/firecracker
```

The task installs the binaries under their bare basenames (`firecracker`,
`jailer` the setuid helper rejects version-stamped names), marks them
`jailer` -- the setuid helper rejects version-stamped names), marks them
executable, and prints the `[tools]` snippet for `/etc/hyper/config.toml`.
After installing, make both binaries root-owned and not group- or
world-writable (the task prints the exact `chown` command); the helper refuses
Expand All @@ -307,7 +309,7 @@ operations. This is achieved through a side-car binary called
> build produced**: `mix compile` stamps the helper with a BLAKE3 checksum and
> bakes that identity into the release, and a deployed helper whose version or
> checksum differs is refused. A binary from another machine or build will not
> pass always install the helper from the same tree you compiled.
> pass -- always install the helper from the same tree you compiled.

Build and install it with:

Expand All @@ -328,7 +330,7 @@ sudo install -o root -g root -m 4755 \

With PostgreSQL reachable (see above) and your database credentials configured
(see the [configuration guide](config.md)), create and migrate the image
database once per cluster, from any node:
database -- once per cluster, from any node:

```sh
mix ecto.create -r Hyper.Img.Db.Repo
Expand All @@ -337,7 +339,7 @@ mix ecto.migrate -r Hyper.Img.Db.Repo

## Booting

Start your application as the `hyper` user Hyper's supervision tree boots
Start your application as the `hyper` user -- Hyper's supervision tree boots
with it and the node becomes a VM runner. From a source checkout, an
interactive session is the quickest smoke test:

Expand All @@ -348,7 +350,7 @@ sudo -u hyper iex -S mix
At boot Hyper validates the node: config file ownership, the setuid helper's
build identity, and the device-mapper targets. A misconfigured node refuses to
start with a specific error rather than limping along. Once up, load an image
and boot a VM see the [intro](intro.md#usage) for the walkthrough.
and boot a VM -- see the [intro](intro.md#usage) for the walkthrough.

For production, run it under a supervisor of your choice (e.g. a systemd unit
with `User=hyper`). Nodes that join the BEAM cluster become additional VM
Expand Down
57 changes: 57 additions & 0 deletions docs/cookbook/quickstart.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,57 @@
# Quick Start

Boot a real Firecracker microVM on your own Linux machine in about five
minutes.

This is the fastest path to a running Hyper node. It is a dev/eval setup -- for
a production install (dedicated system user, managed PostgreSQL, systemd)
follow the [installation guide](install.md).

## What you need

- An Ubuntu/Debian host (x86_64 or aarch64) with KVM -- `stat /dev/kvm` must
succeed. (Other distros work too, via the manual [installation
guide](install.md).)
- cgroups v2 (the default on any modern distro).
- `sudo` -- host provisioning (device-mapper, the setuid helper) needs root.
Hyper itself never runs as root.
- [Docker](https://docs.docker.com/engine/install/) -- for the side-car
Postgres.
- Elixir `~> 1.20` OTP 28+ ([install](https://elixir-lang.org/install.html)).
- Rust via [rustup](https://rustup.rs) -- the build compiles the setuid helper
and the in-guest agent.

`setup.sh` checks every requirement up front and stops with an actionable
error before touching the host.

## Run it

```sh
git clone https://github.com/harmont-dev/hyper && cd hyper
./setup.sh # one-time host provisioning (asks for sudo)
# NOTE: please read this script and ensure you're comfortable
# with it

iex -S mix # boot a Hyper node
```

## Boot a VM

In the `iex` shell:

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

That is a real Firecracker microVM with a copy-on-write rootfs. The
[intro](intro.md#usage) walks through loading images, booting VMs, and
running commands; for other languages there is a
[gRPC interface](../grpc.md).

## Where next

- [Intro](intro.md) -- concepts and the usage walkthrough.
- [Installation guide](install.md) -- the manual, production-grade setup.
- [Configuration guide](config.md) -- every `/etc/hyper/config.toml` knob.
7 changes: 4 additions & 3 deletions mix.exs
Original file line number Diff line number Diff line change
Expand Up @@ -118,6 +118,7 @@ defmodule Hyper.MixProject do
# Narrative/guide pages rendered alongside the API reference.
extras: [
"README.md",
"docs/cookbook/quickstart.md",
"docs/cookbook/intro.md",
"docs/cookbook/install.md",
"docs/cookbook/config.md",
Expand Down Expand Up @@ -195,7 +196,7 @@ defmodule Hyper.MixProject do
# the bindings in a consumer's build (they are gitignored, not in `lib`).
# priv/repo ships the migrations so a consumer can `mix ecto.migrate`.
# native/ ships the Rust crate *sources* because the `:suidhelper_stamp` and
# `:guest_agent_build` compilers build them wherever hyper compiles the
# `:guest_agent_build` compilers build them wherever hyper compiles -- the
# suidhelper must be built locally anyway (`Hyper.SuidHelper.verify_version/0`
# checks the deployed helper's BLAKE3 against this build's stamp, so a
# prebuilt binary can never match). Crate subpaths are listed explicitly to
Expand All @@ -216,7 +217,7 @@ defmodule Hyper.MixProject do
),
# `lib` is included wholesale, so on a dev box the gitignored generated
# outputs (bindings, expected.ex) exist on disk and would leak into the
# tarball with a build identity from the packaging machine. Keep the
# tarball -- with a build identity from the packaging machine. Keep the
# package deterministic: consumers regenerate all of these at compile.
exclude_patterns: [
~r{^lib/hyper/firecracker/api/(operations|schemas)/},
Expand All @@ -239,7 +240,7 @@ defmodule Hyper.MixProject do
# zsh (it only de-selects the `$ ` prompt; everything else is plain text)
# from `ExDoc.Application.start`, which runs during the `docs` task.
#
# So we start :ex_doc and :makeup_syntect here first (idempotent the later
# So we start :ex_doc and :makeup_syntect here first (idempotent -- the later
# `docs` task won't re-run their `start/2`), then register our shell aliases
# LAST so they win. Dev-only; runs as the `docs` alias's first step.
defp register_doc_lexers(_args) do
Expand Down
Loading
Loading