Skip to content

Commit 23efeb9

Browse files
author
dylanwongtencent
committed
added real playbooks
1 parent 764380d commit 23efeb9

18 files changed

Lines changed: 688 additions & 0 deletions

File tree

pvm-firecracker-ansible/README.md

Lines changed: 86 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,86 @@
1+
# PVM Firecracker — Ansible playbook
2+
3+
Run **Firecracker microVMs without KVM / hardware virtualization** on a regular
4+
cloud VM, using **PVM** (Pagetable-based Virtual Machine). This playbook codifies
5+
the whole manual build: a PVM-patched Firecracker, a PVM host kernel, a PVM guest
6+
kernel, an Ubuntu rootfs, GRUB config, reboot, and an end-to-end microVM smoke test.
7+
8+
## The one hard requirement
9+
10+
PVM needs the **`fsgsbase`** CPU feature exposed to the guest vCPU. Many cloud
11+
hosts hide it (for live-migration compatibility), and PVM **cannot** run there.
12+
Check first:
13+
14+
```bash
15+
grep -o fsgsbase /proc/cpuinfo # must print "fsgsbase"
16+
```
17+
18+
The playbook asserts this in preflight and stops immediately if it's missing.
19+
20+
Other requirements: x86_64, RHEL family (Rocky/Alma/RHEL **10** tested), root via SSH,
21+
~10 GB free disk, internet access. First run compiles two kernels + Firecracker
22+
(~20–40 min on 32 vCPUs).
23+
24+
## Usage
25+
26+
```bash
27+
# 1. point inventory.ini at your host
28+
# 2. run it
29+
ansible-playbook playbook.yml
30+
31+
# stage everything but don't reboot / test (e.g. you want to control the reboot)
32+
ansible-playbook playbook.yml -e pvm_reboot=false -e pvm_run_smoke_test=false
33+
34+
# re-run only part of it (every expensive step is idempotent via `creates:`)
35+
ansible-playbook playbook.yml --tags firecracker
36+
ansible-playbook playbook.yml --tags host_kernel,guest_kernel
37+
```
38+
39+
## What it does (stages / tags)
40+
41+
| Tag | Stage |
42+
|-----|-------|
43+
| `preflight` | assert x86_64 + RHEL + **fsgsbase**, make work dirs |
44+
| `packages` | install build + runtime dependencies |
45+
| `firecracker` | install Rust, clone the fork, **re-apply the PVM source patches**, build, install `firecracker`/`jailer` |
46+
| `host_kernel` | build + install the `pvm-612` host kernel (`CONFIG_KVM_PVM=m`, `pti=off` fixes) |
47+
| `guest_kernel` | build the PVM guest `vmlinux` (stripped) |
48+
| `rootfs` | build an Ubuntu ext4 rootfs with an injected SSH key |
49+
| `boot` | `pti=off`, auto-load `kvm-pvm`, set default kernel, **reboot**, verify `/dev/kvm` |
50+
| `smoke_test` | boot a microVM, ping + SSH into the guest, assert it's up |
51+
52+
## Key variables (`roles/pvm_firecracker/defaults/main.yml`)
53+
54+
| Variable | Default | Notes |
55+
|----------|---------|-------|
56+
| `pvm_reboot` | `true` | reboot into the PVM kernel and verify |
57+
| `pvm_set_default_kernel` | `true` | make the PVM kernel the persistent default |
58+
| `pvm_run_smoke_test` | `true` | boot a microVM and prove it |
59+
| `firecracker_repo` / `firecracker_version` | rusternetes-labs / `main` | patches re-applied regardless |
60+
| `pvm_linux_branch` | `pvm-612` | Linux 6.12.33 base |
61+
| `rootfs_squashfs_url` | FC CI Ubuntu 24.04 | userland is PVM-independent |
62+
| `make_jobs` | all vCPUs | parallel build |
63+
64+
## Result
65+
66+
After a successful run the target has, in `/root/pvm/`: `firecracker`, `jailer`,
67+
`vmlinux-pvm`, `rootfs.ext4`, `id_rsa`, `vmconfig.json`, `run-test.sh`. It boots the
68+
PVM kernel by default with `/dev/kvm` provided by `kvm-pvm`, and:
69+
70+
```bash
71+
/root/pvm/run-test.sh # boots a microVM (full Ubuntu guest) over PVM, no KVM hardware
72+
```
73+
74+
## Notes & caveats
75+
76+
- **Custom kernel + reboot.** Have console access to your cloud VM in case the
77+
kernel fails to boot. `pvm_reboot=false` lets you stage everything and reboot
78+
yourself. (`saved_entry` keeps your stock kernel as a fallback.)
79+
- **gnu build target.** Firecracker is built for `x86_64-unknown-linux-gnu`, which
80+
uses an empty seccomp filter (no `libseccomp` at runtime). For a hardened
81+
musl/static build, build via the upstream `tools/devtool` in a container.
82+
- **Idempotent.** Safe to re-run; finished steps are skipped. To force a kernel
83+
rebuild, remove its `creates:` marker (e.g. the `bzImage`) or wipe `build_root`.
84+
- Live-migration support from the fork is intentionally out of scope (not needed
85+
to run microVMs without KVM).
86+
```
Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,13 @@
1+
[defaults]
2+
inventory = inventory.ini
3+
roles_path = roles
4+
host_key_checking = False
5+
retry_files_enabled = False
6+
stdout_callback = yaml
7+
# Kernel builds are long-running; keep the SSH connection alive.
8+
timeout = 60
9+
10+
[ssh_connection]
11+
pipelining = True
12+
# Keep long-running tasks (kernel compiles) from dropping the control socket.
13+
ssh_args = -o ControlMaster=auto -o ControlPersist=120m -o ServerAliveInterval=30 -o ServerAliveCountMax=120
Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,10 @@
1+
# Target host(s) that will run the no-KVM PVM Firecracker stack.
2+
# Requirements: x86_64, a cloud VM whose vCPU EXPOSES the `fsgsbase` flag
3+
# (check with: grep -o fsgsbase /proc/cpuinfo). RHEL/Rocky/Alma 10 family.
4+
#
5+
# Example:
6+
[pvm]
7+
deployment ansible_host=170.106.40.67 ansible_user=root
8+
9+
[pvm:vars]
10+
ansible_python_interpreter=/usr/bin/python3
Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,21 @@
1+
---
2+
# =============================================================================
3+
# Run Firecracker microVMs WITHOUT KVM / hardware virtualization, using PVM
4+
# (Pagetable-based Virtual Machine) on a regular cloud VM.
5+
#
6+
# Usage:
7+
# ansible-playbook playbook.yml
8+
# ansible-playbook playbook.yml -e pvm_reboot=false # stage, don't reboot
9+
# ansible-playbook playbook.yml -e pvm_run_smoke_test=false
10+
# ansible-playbook playbook.yml --tags firecracker,host_kernel
11+
#
12+
# NOTE: This compiles two Linux kernels and Firecracker from source. On a
13+
# 32-vCPU box budget ~20-40 min for a first run. It is safe to re-run: every
14+
# expensive step is guarded with `creates:` and will be skipped if already done.
15+
# =============================================================================
16+
- name: Set up the no-KVM PVM Firecracker stack
17+
hosts: pvm
18+
become: true
19+
gather_facts: true
20+
roles:
21+
- role: pvm_firecracker
Lines changed: 41 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,41 @@
1+
---
2+
# ---------- Source repositories ----------
3+
# The Firecracker fork carrying the PVM forward-port (this role re-applies the
4+
# two essential source changes itself, so any reasonably-recent commit works).
5+
firecracker_repo: "https://github.com/rusternetes-labs/firecracker"
6+
firecracker_version: "main"
7+
8+
# PVM host + guest kernel source (Linux 6.12.33 base).
9+
pvm_linux_repo: "https://github.com/virt-pvm/linux"
10+
pvm_linux_branch: "pvm-612"
11+
pvm_guest_config_url: "https://raw.githubusercontent.com/virt-pvm/misc/main/pvm-guest-6.12.33.config"
12+
13+
# Ubuntu rootfs published by the Firecracker CI (userland is PVM-independent).
14+
rootfs_squashfs_url: "https://s3.amazonaws.com/spec.ccfc.min/firecracker-ci/v1.12/x86_64/ubuntu-24.04.squashfs"
15+
rootfs_size: "1G"
16+
17+
# ---------- Working directories ----------
18+
build_root: "/root/pvm-build" # scratch space for clones + compiles
19+
host_kernel_src: "{{ build_root }}/host-linux"
20+
guest_kernel_src: "{{ build_root }}/guest-linux"
21+
firecracker_src: "{{ build_root }}/firecracker"
22+
pvm_dir: "/root/pvm" # runtime artifacts live here
23+
24+
# ---------- Build knobs ----------
25+
make_jobs: "{{ ansible_processor_vcpus | default(ansible_processor_cores) | default(4) }}"
26+
host_localversion: "-pvm" # host kernel becomes 6.12.33-pvm+
27+
guest_localversion: "-pvmguest"
28+
fc_target: "x86_64-unknown-linux-gnu" # gnu build => empty seccomp filter, no libseccomp at runtime
29+
30+
# ---------- Behaviour toggles ----------
31+
pvm_reboot: true # reboot into the PVM kernel at the end
32+
pvm_set_default_kernel: true # make the PVM kernel the persistent default
33+
pvm_run_smoke_test: true # boot a microVM and prove it after reboot
34+
35+
# ---------- microVM smoke-test settings ----------
36+
vm_vcpus: 2
37+
vm_mem_mib: 1024
38+
vm_tap_dev: "tap0"
39+
vm_host_ip: "172.16.0.1"
40+
vm_guest_ip: "172.16.0.2"
41+
vm_guest_mac: "06:00:AC:10:00:02" # derives 172.16.0.2 in the CI rootfs
Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,3 @@
1+
---
2+
# Reboot is handled inline in tasks/60-boot.yml (it must be followed by
3+
# verification tasks in the same play), so no handlers are required here.
Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,17 @@
1+
---
2+
galaxy_info:
3+
role_name: pvm_firecracker
4+
author: deployment
5+
description: Run Firecracker microVMs without KVM using PVM on regular cloud VMs.
6+
license: Apache-2.0
7+
min_ansible_version: "2.14"
8+
platforms:
9+
- name: EL
10+
versions:
11+
- "10"
12+
galaxy_tags:
13+
- firecracker
14+
- pvm
15+
- virtualization
16+
- microvm
17+
dependencies: []
Lines changed: 47 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,47 @@
1+
---
2+
# PVM has hard requirements. Fail loudly NOW rather than after a 30-minute build.
3+
4+
- name: Require x86_64
5+
ansible.builtin.assert:
6+
that: ansible_architecture == "x86_64"
7+
fail_msg: "PVM is x86_64-only (found {{ ansible_architecture }})."
8+
9+
- name: Require a RHEL-family distro (Rocky/Alma/RHEL 10 tested)
10+
ansible.builtin.assert:
11+
that: ansible_os_family == "RedHat"
12+
fail_msg: "This role targets the RHEL family (found {{ ansible_distribution }})."
13+
14+
- name: Read CPU flags
15+
ansible.builtin.command: grep -m1 '^flags' /proc/cpuinfo
16+
register: cpuflags
17+
changed_when: false
18+
19+
- name: >-
20+
Require FSGSBASE (the show-stopper). PVM's kvm-pvm module refuses to load
21+
without it, and a cloud host that hides it from guest CPUID cannot run PVM.
22+
ansible.builtin.assert:
23+
that: "'fsgsbase' in cpuflags.stdout"
24+
fail_msg: >-
25+
This VM's vCPU does NOT expose `fsgsbase`, so PVM cannot run here.
26+
Move to an instance type / CPU model that exposes it
27+
(verify with: grep -o fsgsbase /proc/cpuinfo).
28+
success_msg: "FSGSBASE present — PVM is viable on this host."
29+
30+
- name: Detect pre-existing /dev/kvm (real KVM present — PVM not strictly needed)
31+
ansible.builtin.stat:
32+
path: /dev/kvm
33+
register: _preexisting_kvm
34+
35+
- name: Note if hardware KVM is already available
36+
ansible.builtin.debug:
37+
msg: "Note: /dev/kvm already exists; this host may have real KVM and not need PVM."
38+
when: _preexisting_kvm.stat.exists
39+
40+
- name: Ensure working directories exist
41+
ansible.builtin.file:
42+
path: "{{ item }}"
43+
state: directory
44+
mode: "0755"
45+
loop:
46+
- "{{ build_root }}"
47+
- "{{ pvm_dir }}"
Lines changed: 35 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,35 @@
1+
---
2+
- name: Install toolchain, kernel-build, Firecracker-build and rootfs dependencies
3+
ansible.builtin.dnf:
4+
name:
5+
# base toolchain
6+
- gcc
7+
- gcc-c++
8+
- make
9+
- git
10+
- perl
11+
- python3
12+
- curl
13+
- wget
14+
- xz
15+
- rsync
16+
- hostname
17+
# kernel build
18+
- bison
19+
- flex
20+
- openssl-devel
21+
- elfutils-libelf-devel
22+
- bc
23+
- dwarves
24+
- ncurses-devel
25+
- dracut
26+
# Firecracker build (aws-lc-sys/bindgen/seccompiler)
27+
- cmake
28+
- clang
29+
- clang-devel
30+
- libseccomp-devel
31+
# rootfs + microVM networking
32+
- squashfs-tools
33+
- e2fsprogs
34+
- iptables
35+
state: present
Lines changed: 69 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,69 @@
1+
---
2+
# Build the PVM-aware Firecracker. The fork's `main` tracks upstream and the
3+
# PVM source changes are tiny + surgical, so we re-apply them here idempotently
4+
# instead of depending on them being committed.
5+
6+
- name: Install rustup + toolchain (user-space, no root toolchain needed)
7+
ansible.builtin.shell: |
8+
set -e
9+
curl -sSf --proto '=https' --tlsv1.2 https://sh.rustup.rs -o /tmp/rustup-init.sh
10+
sh /tmp/rustup-init.sh -y --default-toolchain stable --profile minimal
11+
args:
12+
creates: "{{ ansible_env.HOME }}/.cargo/bin/cargo"
13+
14+
- name: Clone the Firecracker fork
15+
ansible.builtin.git:
16+
repo: "{{ firecracker_repo }}"
17+
dest: "{{ firecracker_src }}"
18+
version: "{{ firecracker_version }}"
19+
depth: 1
20+
single_branch: true
21+
force: false
22+
23+
# ---- PVM forward-port: change #1 ----------------------------------------
24+
# Disable per-vCPU TSC scaling. PVM does not support KVM_CAP_TSC_CONTROL, so
25+
# the upstream call would fail and Firecracker would refuse to boot a guest.
26+
- name: "PVM patch: make set_tsc_khz() a no-op"
27+
ansible.builtin.replace:
28+
path: "{{ firecracker_src }}/src/vmm/src/arch/x86_64/vcpu.rs"
29+
regexp: 'self\.fd\.set_tsc_khz\(tsc_freq\)\.map_err\(SetTscError\)'
30+
replace: '{ let _ = tsc_freq; Ok(()) } // PVM: TSC scaling unsupported'
31+
32+
# ---- PVM forward-port: change #2 ----------------------------------------
33+
# Relax the static-CPU-template model check so templates work across CPUs.
34+
# `return Err(InvalidCpuModel);` (note: no enum prefix) is unique to the live
35+
# code path; the test-only references use GetCpuTemplateError::InvalidCpuModel.
36+
- name: "PVM patch: relax static CPU-template model check"
37+
ansible.builtin.replace:
38+
path: "{{ firecracker_src }}/src/vmm/src/cpu_config/x86_64/custom_cpu_template.rs"
39+
regexp: '^\s*return Err\(InvalidCpuModel\);'
40+
replace: ' { /* PVM: skip CPU-model compatibility check */ }'
41+
42+
# A gnu-target build has no matching seccomp JSON, so Firecracker's build.rs
43+
# falls back to an empty filter automatically (no libseccomp at runtime).
44+
- name: Build Firecracker + jailer ({{ fc_target }}, release)
45+
ansible.builtin.command:
46+
cmd: cargo build --release --target {{ fc_target }} --package firecracker --package jailer
47+
chdir: "{{ firecracker_src }}"
48+
creates: "{{ firecracker_src }}/build/cargo_target/{{ fc_target }}/release/firecracker"
49+
environment:
50+
PATH: "{{ ansible_env.HOME }}/.cargo/bin:{{ ansible_env.PATH }}"
51+
CARGO_TERM_PROGRESS_WHEN: "never"
52+
async: 1800
53+
poll: 20
54+
55+
- name: Install firecracker + jailer binaries
56+
ansible.builtin.copy:
57+
src: "{{ firecracker_src }}/build/cargo_target/{{ fc_target }}/release/{{ item }}"
58+
dest: "{{ pvm_dir }}/{{ item }}"
59+
mode: "0755"
60+
remote_src: true
61+
loop:
62+
- firecracker
63+
- jailer
64+
65+
- name: Symlink firecracker into PATH
66+
ansible.builtin.file:
67+
src: "{{ pvm_dir }}/firecracker"
68+
dest: /usr/local/bin/firecracker
69+
state: link

0 commit comments

Comments
 (0)