Skip to content

Commit f123573

Browse files
Merge pull request #224 from jacobweinstock/main
Add GHA manual vagrant box build:
2 parents 898e16a + 800fe6a commit f123573

12 files changed

Lines changed: 624 additions & 4 deletions

File tree

Lines changed: 83 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,83 @@
1+
name: Build & publish tinkerbell/pxe box
2+
3+
on:
4+
workflow_dispatch:
5+
inputs:
6+
version:
7+
description: "Version string written into metadata.json (e.g. 0.1.0)"
8+
required: true
9+
default: "0.1.0"
10+
type: string
11+
12+
permissions:
13+
contents: write
14+
15+
jobs:
16+
build-and-release:
17+
runs-on: ubuntu-latest
18+
env:
19+
RELEASE_TAG: pxe-box-latest
20+
steps:
21+
- name: Checkout
22+
uses: actions/checkout@v4
23+
24+
- name: Install build dependencies
25+
run: |
26+
sudo apt-get update
27+
sudo apt-get install -y --no-install-recommends \
28+
dosfstools mtools qemu-utils gdisk uuid-runtime
29+
30+
- name: Compute BASE_URL
31+
id: base
32+
run: |
33+
echo "url=https://github.com/${GITHUB_REPOSITORY}/releases/download/${RELEASE_TAG}" >> "$GITHUB_OUTPUT"
34+
35+
- name: Build all box artifacts
36+
run: |
37+
make -C stack/pxe-box \
38+
NAME=tinkerbell/pxe \
39+
VERSION='${{ inputs.version }}' \
40+
BASE_URL='${{ steps.base.outputs.url }}'
41+
42+
- name: Show outputs
43+
run: |
44+
ls -lh stack/pxe-box/out/
45+
echo "--- metadata.json ---"
46+
cat stack/pxe-box/out/metadata.json
47+
48+
- name: Remove previous rolling release & tag
49+
env:
50+
GH_TOKEN: ${{ github.token }}
51+
run: |
52+
gh release delete "$RELEASE_TAG" \
53+
--repo "$GITHUB_REPOSITORY" \
54+
--cleanup-tag --yes || true
55+
56+
- name: Publish to rolling GitHub Release
57+
uses: softprops/action-gh-release@v2
58+
with:
59+
tag_name: ${{ env.RELEASE_TAG }}
60+
target_commitish: ${{ github.sha }}
61+
name: "tinkerbell/pxe (rolling)"
62+
make_latest: "false"
63+
prerelease: false
64+
draft: false
65+
body: |
66+
Rolling release of the `tinkerbell/pxe` Vagrant box.
67+
68+
- version in `metadata.json`: `${{ inputs.version }}`
69+
- commit: `${{ github.sha }}`
70+
- built by: ${{ github.workflow }} (${{ github.run_id }})
71+
72+
Use it from a `Vagrantfile`:
73+
74+
```ruby
75+
config.vm.box = "tinkerbell/pxe"
76+
config.vm.box_url = "https://github.com/${{ github.repository }}/releases/download/${{ env.RELEASE_TAG }}/metadata.json"
77+
```
78+
files: |
79+
stack/pxe-box/out/pxe-amd64-virtualbox.box
80+
stack/pxe-box/out/pxe-arm64-virtualbox.box
81+
stack/pxe-box/out/pxe-amd64-libvirt.box
82+
stack/pxe-box/out/pxe-arm64-libvirt.box
83+
stack/pxe-box/out/metadata.json

stack/pxe-box/.gitignore

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
out/

stack/pxe-box/Makefile

Lines changed: 55 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,55 @@
1+
# Build minimal "PXE" Vagrant boxes (iPXE-on-disk) for
2+
# {virtualbox, libvirt} x {amd64, arm64}, plus a top-level Vagrant Cloud-style
3+
# metadata.json that maps a single box name to all four artifacts.
4+
#
5+
# Usage:
6+
# make # build everything into ./out
7+
# make disks # just the raw FAT32 + iPXE disk images
8+
# make vbox libvirt # provider-specific .box files
9+
# make metadata # top-level metadata.json
10+
# make clean
11+
#
12+
# Required tools (macOS: brew install dosfstools mtools qemu coreutils gptfdisk):
13+
# curl, dd, sgdisk, mkfs.vfat, mtools (mformat, mmd, mcopy), qemu-img, tar, uuidgen.
14+
# All steps are host-arch agnostic; a single linux/amd64 runner can build
15+
# every (provider x architecture) combination.
16+
17+
SHELL := /usr/bin/env bash
18+
.SHELLFLAGS := -euo pipefail -c
19+
20+
NAME ?= tinkerbell/pxe
21+
VERSION ?= 0.1.0
22+
BASE_URL ?= https://example.invalid/pxe
23+
ARCHES ?= amd64 arm64
24+
OUT := out
25+
26+
DISKS := $(addprefix $(OUT)/disk-,$(addsuffix .img,$(ARCHES)))
27+
VBOXES := $(addprefix $(OUT)/pxe-,$(addsuffix -virtualbox.box,$(ARCHES)))
28+
LVBOXES := $(addprefix $(OUT)/pxe-,$(addsuffix -libvirt.box,$(ARCHES)))
29+
30+
.PHONY: all clean disks vbox libvirt metadata
31+
all: vbox libvirt metadata
32+
33+
clean:
34+
rm -rf $(OUT)
35+
36+
disks: $(DISKS)
37+
38+
$(OUT)/disk-%.img: scripts/build-disk.sh
39+
@mkdir -p $(OUT)
40+
scripts/build-disk.sh $* $@
41+
42+
vbox: $(VBOXES)
43+
44+
$(OUT)/pxe-%-virtualbox.box: $(OUT)/disk-%.img scripts/pack-virtualbox.sh templates/box.ovf.tmpl templates/Vagrantfile.vbox
45+
scripts/pack-virtualbox.sh $* $< $@
46+
47+
libvirt: $(LVBOXES)
48+
49+
$(OUT)/pxe-%-libvirt.box: $(OUT)/disk-%.img scripts/pack-libvirt.sh templates/Vagrantfile.libvirt
50+
scripts/pack-libvirt.sh $* $< $@
51+
52+
metadata: $(OUT)/metadata.json
53+
54+
$(OUT)/metadata.json: $(VBOXES) $(LVBOXES) scripts/make-metadata.sh
55+
BASE_URL=$(BASE_URL) scripts/make-metadata.sh $(NAME) $(VERSION) > $@

stack/pxe-box/README.md

Lines changed: 104 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,104 @@
1+
# pxe-box
2+
3+
Builds a tiny "iPXE on disk" Vagrant box — the cross-arch / cross-provider
4+
replacement for [`jtyr/pxe`][jtyr]. Each artifact is a ~64 MB virtual disk
5+
whose only contents are an EFI system partition with iPXE as the default
6+
loader (`\EFI\BOOT\BOOTX64.EFI` on amd64, `\EFI\BOOT\BOOTAA64.EFI` on arm64).
7+
At boot the firmware launches iPXE, which DHCPs and chains to whatever
8+
PXE/HTTP infrastructure is on the network — for the playground that's Smee.
9+
10+
Targets produced:
11+
12+
| provider | amd64 | arm64 |
13+
| ---------- | :---: | :---: |
14+
| virtualbox |||
15+
| libvirt |||
16+
17+
Plus a top-level Vagrant Cloud-style `metadata.json` that resolves a single
18+
box name (default `tinkerbell/pxe`) to the right artifact based on the host's
19+
provider and architecture.
20+
21+
## Build
22+
23+
Host requirements (macOS): `brew install dosfstools mtools qemu coreutils gptfdisk`.
24+
Linux distros generally ship these. No VirtualBox installation is required
25+
on the build host — `qemu-img` produces the VDI directly, so a single
26+
`linux/amd64` runner can build every `(provider x architecture)` combination.
27+
28+
```sh
29+
make # build everything into ./out
30+
make ARCHES=arm64 vbox # subset
31+
make BASE_URL=https://example.com/pxe metadata
32+
```
33+
34+
Output:
35+
36+
```
37+
out/
38+
├── disk-amd64.img
39+
├── disk-arm64.img
40+
├── pxe-amd64-virtualbox.box
41+
├── pxe-arm64-virtualbox.box
42+
├── pxe-amd64-libvirt.box
43+
├── pxe-arm64-libvirt.box
44+
└── metadata.json
45+
```
46+
47+
## Local-only use (no server)
48+
49+
You don't need to host the boxes anywhere. Build, then `vagrant box add` the
50+
single artifact you need by direct file path. The box name is whatever you
51+
pick at `add` time:
52+
53+
```sh
54+
# 1. Build (only the artifact you need is fine).
55+
make ARCHES=arm64 vbox # -> out/pxe-arm64-virtualbox.box
56+
57+
# 2. Register it locally under a name of your choice.
58+
vagrant box add --provider virtualbox --architecture arm64 \
59+
--name tinkerbell/pxe out/pxe-arm64-virtualbox.box
60+
61+
# 3. Reference it like any other box. No box_url, no metadata.json.
62+
# In a Vagrantfile:
63+
# config.vm.box = "tinkerbell/pxe"
64+
```
65+
66+
Repeat step 2 for additional `(provider, architecture)` combos as needed —
67+
Vagrant stores them under the same name and picks the right one based on the
68+
provider you use and the host's architecture.
69+
70+
To remove or refresh a locally added box:
71+
72+
```sh
73+
vagrant box list
74+
vagrant box remove tinkerbell/pxe --provider virtualbox --architecture arm64
75+
```
76+
77+
## Publish (optional, multi-user setup)
78+
79+
If you want one Vagrantfile to resolve to the right artifact for any
80+
contributor automatically, upload the four `.box` files and `metadata.json`
81+
to a static host (S3, GCS, GitHub Releases, ghcr.io OCI artifacts, anywhere
82+
with HTTP). The `url` fields in `metadata.json` must resolve to the box
83+
files.
84+
85+
In a `Vagrantfile`:
86+
87+
```ruby
88+
config.vm.box = "tinkerbell/pxe"
89+
config.vm.box_url = "https://your-host/path/metadata.json"
90+
```
91+
92+
Vagrant picks the right `(provider, architecture)` automatically.
93+
94+
## Caveats
95+
96+
- The OVF template in `templates/box.ovf.tmpl` targets VirtualBox 7.1+.
97+
If a future VBox release rejects it, regenerate by exporting a working
98+
VM (`VBoxManage export <vm> -o ref.ovf`) and copying the structure back.
99+
- `qemu-img convert -O vdi` produces a sparse VDI from the 64 MB raw; the
100+
resulting `.box` is a few MB compressed.
101+
- The arm64 box requires a host able to run arm64 VirtualBox VMs (Apple
102+
Silicon with VirtualBox ≥ 7.1).
103+
104+
[jtyr]: https://app.vagrantup.com/jtyr/boxes/pxe
Lines changed: 58 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,58 @@
1+
#!/usr/bin/env bash
2+
# Build a 64MB GPT raw disk image with a single EFI System Partition whose
3+
# default UEFI loader is iPXE.
4+
#
5+
# Usage: build-disk.sh <amd64|arm64> <output.img>
6+
#
7+
# Requires: curl, dd, sgdisk (gdisk / gptfdisk), mtools >= 4.0 (mformat,mmd,mcopy
8+
# with @@offset syntax).
9+
set -euo pipefail
10+
11+
arch=${1:?arch (amd64|arm64) required}
12+
out=${2:?output path required}
13+
14+
case "$arch" in
15+
amd64)
16+
url="https://boot.ipxe.org/x86_64-efi/snponly.efi"
17+
loader="BOOTX64.EFI"
18+
;;
19+
arm64)
20+
url="https://boot.ipxe.org/arm64-efi/snponly.efi"
21+
loader="BOOTAA64.EFI"
22+
;;
23+
*)
24+
echo "unknown arch: $arch" >&2
25+
exit 1
26+
;;
27+
esac
28+
29+
tmp=$(mktemp -d)
30+
trap 'rm -rf "$tmp"' EXIT
31+
32+
echo "==> downloading iPXE EFI for $arch"
33+
curl -fsSL -o "$tmp/$loader" "$url"
34+
35+
echo "==> creating 64MB raw image"
36+
dd if=/dev/zero of="$out" bs=1048576 count=64 status=none
37+
38+
echo "==> writing GPT with single ESP (type EF00)"
39+
# Partition starts at sector 2048 (1MB) and runs to end-34 (sgdisk default).
40+
sgdisk --clear \
41+
--new=1:2048:0 \
42+
--typecode=1:EF00 \
43+
--change-name=1:"EFI System Partition" \
44+
"$out" >/dev/null
45+
46+
# Byte offset of the ESP for mtools.
47+
offset=$((2048 * 512))
48+
49+
echo "==> formatting ESP as FAT32 (offset=$offset)"
50+
# mformat needs a drive letter mapping; supply via -i image@@offset.
51+
# -F = FAT32, -v = label.
52+
mformat -i "$out@@${offset}" -F -v IPXE ::
53+
54+
echo "==> populating \\EFI\\BOOT\\$loader"
55+
mmd -i "$out@@${offset}" ::/EFI ::/EFI/BOOT
56+
mcopy -i "$out@@${offset}" "$tmp/$loader" "::/EFI/BOOT/$loader"
57+
58+
echo "==> done: $out"
Lines changed: 47 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,47 @@
1+
#!/usr/bin/env bash
2+
# Emit a Vagrant Cloud-style top-level metadata.json that maps a single box
3+
# name to all four (provider x architecture) artifacts.
4+
#
5+
# Usage: BASE_URL=https://host/path make-metadata.sh <name> <version>
6+
set -euo pipefail
7+
8+
name=${1:?name required}
9+
version=${2:?version required}
10+
base_url=${BASE_URL:-https://example.invalid/pxe}
11+
12+
cat <<JSON
13+
{
14+
"name": "$name",
15+
"versions": [
16+
{
17+
"version": "$version",
18+
"providers": [
19+
{
20+
"name": "virtualbox",
21+
"architecture": "amd64",
22+
"default_architecture": true,
23+
"url": "$base_url/$version/pxe-amd64-virtualbox.box"
24+
},
25+
{
26+
"name": "virtualbox",
27+
"architecture": "arm64",
28+
"default_architecture": false,
29+
"url": "$base_url/$version/pxe-arm64-virtualbox.box"
30+
},
31+
{
32+
"name": "libvirt",
33+
"architecture": "amd64",
34+
"default_architecture": true,
35+
"url": "$base_url/$version/pxe-amd64-libvirt.box"
36+
},
37+
{
38+
"name": "libvirt",
39+
"architecture": "arm64",
40+
"default_architecture": false,
41+
"url": "$base_url/$version/pxe-arm64-libvirt.box"
42+
}
43+
]
44+
}
45+
]
46+
}
47+
JSON
Lines changed: 34 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,34 @@
1+
#!/usr/bin/env bash
2+
# Pack a raw FAT32 disk image as a Vagrant libvirt .box for the given arch.
3+
#
4+
# Usage: pack-libvirt.sh <amd64|arm64> <input.img> <output.box>
5+
set -euo pipefail
6+
7+
arch=${1:?arch required}
8+
raw=${2:?input img required}
9+
out=${3:?output .box required}
10+
here=$(cd "$(dirname "$0")/.." && pwd)
11+
12+
work=$(mktemp -d)
13+
trap 'rm -rf "$work"' EXIT
14+
15+
echo "==> converting raw -> qcow2"
16+
qemu-img convert -f raw -O qcow2 "$raw" "$work/box.img"
17+
18+
# virtual_size is reported in GB; round up from raw byte size.
19+
disk_bytes=$(wc -c <"$raw" | tr -d ' ')
20+
virtual_size_gb=$(((disk_bytes + 1024 * 1024 * 1024 - 1) / (1024 * 1024 * 1024)))
21+
[[ $virtual_size_gb -lt 1 ]] && virtual_size_gb=1
22+
23+
cp "$here/templates/Vagrantfile.libvirt" "$work/Vagrantfile"
24+
cat >"$work/metadata.json" <<JSON
25+
{
26+
"provider": "libvirt",
27+
"format": "qcow2",
28+
"virtual_size": $virtual_size_gb
29+
}
30+
JSON
31+
32+
echo "==> archiving $out"
33+
tar -C "$work" -czf "$out" box.img Vagrantfile metadata.json
34+
echo "==> done: $out"

0 commit comments

Comments
 (0)