Compare commits
22
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
dad1642705 | ||
|
|
f7b6623b25 | ||
|
|
6d521930f4 | ||
|
|
c611bbf2c2 | ||
|
|
6931039809 | ||
|
|
41eb18cc70 | ||
|
|
b8cd2966f4 | ||
|
|
67316d9852 | ||
|
|
35d8879bcc | ||
|
|
bb23fed54a | ||
|
|
d3189bee94 | ||
|
|
435ce41c02 | ||
|
|
7197ed9be9 | ||
|
|
da5bcf6b04 | ||
|
|
f246dec765 | ||
|
|
bddd05f3a8 | ||
|
|
c491aeb635 | ||
|
|
dceb82f8f6 | ||
|
|
a8228df780 | ||
|
|
0a553429ba | ||
|
|
cd4302cb09 | ||
|
|
fff901ef1a |
@@ -0,0 +1,147 @@
|
||||
name: build-image
|
||||
|
||||
# A full build takes ~25 min on the runner and executes the checked-out code
|
||||
# as root there, so it never runs on a plain push. Recipes are still linted
|
||||
# and validated on every push and PR by ci.yml. Images are built:
|
||||
# - on a release tag (v*.*.*): build, smoke test, publish as a release;
|
||||
# - on a PR into main carrying the `build-image` label, i.e. once a
|
||||
# maintainer has reviewed it (Gitea does not let authors approve their
|
||||
# own PRs, so approval cannot be the gate). Re-runs on every new push to
|
||||
# the PR while the label stays. Only for changes that affect images;
|
||||
# - weekly, to catch breakage from upstream Fedora package changes;
|
||||
# - by hand (workflow_dispatch) on any branch.
|
||||
on:
|
||||
push:
|
||||
tags: ["v*.*.*"]
|
||||
pull_request:
|
||||
branches: [main]
|
||||
types: [labeled, synchronize, reopened]
|
||||
paths:
|
||||
- "cook/**"
|
||||
- "build-image.sh"
|
||||
- "smoke-test.sh"
|
||||
- ".gitea/workflows/build-image.yaml"
|
||||
schedule:
|
||||
- cron: "0 3 * * 1" # Mondays 03:00
|
||||
workflow_dispatch:
|
||||
|
||||
# Builds raw disk images of the two default-tier editions (see
|
||||
# cook/recipes_manifest.yaml) with build-image.sh, i.e.
|
||||
# `livemedia-creator --no-virt --make-disk`. Other tiers (guest, experimental)
|
||||
# are built on demand with `./build-image.sh --tier <tier>`.
|
||||
#
|
||||
# The build job runs on the registered `fedora:host` runner (a Fedora VM, jobs
|
||||
# run as root), not in a container: --make-disk needs real loop devices and a
|
||||
# live systemd-udevd. The runner needs lorax-lmc-novirt, qemu-img, make,
|
||||
# python3-pip, pykickstart and xz installed, plus qemu-system-x86_64,
|
||||
# /dev/kvm and edk2-ovmf for smoke-test.sh. It must run with SELinux enabled:
|
||||
# anaconda inherits the runner's own selinux=0, if any (smoke-test.sh fails
|
||||
# such images).
|
||||
|
||||
jobs:
|
||||
validate:
|
||||
# On PRs, only once the `build-image` label is set (see `on:` above), and
|
||||
# not again when some other label is added to an already-labeled PR.
|
||||
if: >-
|
||||
github.event_name != 'pull_request' ||
|
||||
(contains(github.event.pull_request.labels.*.name, 'build-image') &&
|
||||
(github.event.action != 'labeled' || github.event.label.name == 'build-image'))
|
||||
runs-on: fedora
|
||||
defaults:
|
||||
run:
|
||||
shell: bash
|
||||
container:
|
||||
image: git.phyllo.me/devops/fedora-runner-image:latest
|
||||
|
||||
steps:
|
||||
- uses: https://git.phyllo.me/devops/checkout@v5
|
||||
with:
|
||||
fetch-depth: 0
|
||||
|
||||
- name: Install Python deps
|
||||
run: pip install -r cook/requirements.txt
|
||||
|
||||
- name: Generate, lint, and validate dishes
|
||||
run: |
|
||||
cd cook
|
||||
make all
|
||||
|
||||
build-image:
|
||||
needs: validate
|
||||
runs-on: fedora
|
||||
strategy:
|
||||
fail-fast: false
|
||||
# NB: Gitea runs every matrix entry as its own task, so `max-parallel`
|
||||
# does not serialize them; build-image.sh takes a host-wide flock instead.
|
||||
matrix:
|
||||
include:
|
||||
- edition: phyllomeos
|
||||
root_size: 16384
|
||||
- edition: phyllomeos-headless
|
||||
root_size: 8192
|
||||
defaults:
|
||||
run:
|
||||
shell: bash
|
||||
env:
|
||||
# /tmp is a small tmpfs on the runner; livemedia-creator writes the whole
|
||||
# disk image under $TMPDIR
|
||||
TMPDIR: /var/tmp
|
||||
|
||||
steps:
|
||||
- uses: https://git.phyllo.me/devops/checkout@v5
|
||||
with:
|
||||
fetch-depth: 0
|
||||
|
||||
- name: Check runner prerequisites
|
||||
run: |
|
||||
missing=0
|
||||
for tool in livemedia-creator qemu-img make pip xz sudo qemu-system-x86_64 debugfs; do
|
||||
command -v "$tool" >/dev/null || { echo "missing on runner: $tool"; missing=1; }
|
||||
done
|
||||
[ -e /dev/loop-control ] || { echo "missing on runner: /dev/loop-control"; missing=1; }
|
||||
[ -e /dev/kvm ] || { echo "missing on runner: /dev/kvm"; missing=1; }
|
||||
[ -e /usr/share/edk2/ovmf/OVMF_CODE.fd ] || { echo "missing on runner: edk2-ovmf"; missing=1; }
|
||||
[ "$missing" = 0 ] || exit 1
|
||||
|
||||
- name: Install Python deps
|
||||
run: pip install -r cook/requirements.txt
|
||||
|
||||
- name: Build raw image
|
||||
run: |
|
||||
# Editions are named "<edition>_<values...>.cfg"; the trailing "_" keeps
|
||||
# "phyllomeos" from matching "phyllomeos-headless".
|
||||
(cd cook && make all)
|
||||
dish="$(basename "$(ls cook/dishes/${{ matrix.edition }}_*.cfg)" .cfg)"
|
||||
echo "DISH=$dish" >> "$GITHUB_ENV"
|
||||
./build-image.sh --dish "$dish" --root-size ${{ matrix.root_size }}
|
||||
|
||||
- name: Smoke test (static checks + boot)
|
||||
run: ./smoke-test.sh "build/$DISH.img"
|
||||
|
||||
- name: Compress image
|
||||
run: xz -T0 "build/$DISH.img"
|
||||
|
||||
- name: Upload image and kickstart as artifact
|
||||
if: "!startsWith(github.ref, 'refs/tags/')"
|
||||
uses: actions/upload-artifact@v3
|
||||
with:
|
||||
name: ${{ matrix.edition }}
|
||||
path: |
|
||||
build/${{ env.DISH }}.img.xz
|
||||
cook/dishes/${{ env.DISH }}.cfg
|
||||
if-no-files-found: error
|
||||
retention-days: 7
|
||||
|
||||
- name: Publish image and kickstart as release
|
||||
if: startsWith(github.ref, 'refs/tags/')
|
||||
uses: https://git.phyllo.me/devops/action-gh-release@v2
|
||||
with:
|
||||
files: |
|
||||
build/${{ env.DISH }}.img.xz
|
||||
cook/dishes/${{ env.DISH }}.cfg
|
||||
draft: false
|
||||
prerelease: false
|
||||
|
||||
- name: Clean up build products
|
||||
if: always()
|
||||
run: rm -rf build /var/tmp/lmc-result.*
|
||||
@@ -1,74 +0,0 @@
|
||||
name: build-iso
|
||||
|
||||
on:
|
||||
push:
|
||||
branches: [main]
|
||||
release:
|
||||
types: [published]
|
||||
|
||||
jobs:
|
||||
validate:
|
||||
runs-on: fedora
|
||||
container:
|
||||
image: git.phyllo.me/devops/fedora-runner-image:latest
|
||||
|
||||
steps:
|
||||
- uses: https://git.phyllo.me/devops/checkout@v5
|
||||
with:
|
||||
fetch-depth: 0
|
||||
|
||||
- name: Install dependencies
|
||||
run: |
|
||||
pip install PyYAML pykickstart
|
||||
|
||||
- name: Generate, lint, and validate dishes
|
||||
run: |
|
||||
cd cook
|
||||
make all
|
||||
|
||||
build-iso:
|
||||
needs: validate
|
||||
runs-on: fedora-cloud-42
|
||||
container:
|
||||
image: git.phyllo.me/devops/fedora-runner-image:latest
|
||||
|
||||
steps:
|
||||
- uses: https://git.phyllo.me/devops/checkout@v5
|
||||
with:
|
||||
fetch-depth: 0
|
||||
|
||||
- name: Initialize mock
|
||||
run: |
|
||||
mock -r fedora-44-x86_64 --init
|
||||
|
||||
- name: Install required packages
|
||||
run: |
|
||||
mock -r fedora-44-x86_64 --install lorax-lmc-novirt vim-minimal pykickstart livecd-tools
|
||||
|
||||
- name: Generate dishes
|
||||
run: |
|
||||
cd cook
|
||||
make all
|
||||
|
||||
- name: Copy configuration file to mock
|
||||
run: |
|
||||
mock -r fedora-44-x86_64 --copyin dishes/desktop_44_standard_grub_gnome_guest-agents.cfg /builddir
|
||||
|
||||
- name: Build ISO with livemedia-creator
|
||||
run: |
|
||||
mock -r fedora-44-x86_64 --shell --enable-network --isolation=simple << 'EOF'
|
||||
cd /builddir
|
||||
livemedia-creator --ks desktop_44_standard_grub_gnome_guest-agents.cfg --no-virt --resultdir /var/lmc --project phyllomeos --make-iso --volid phyllomeos --iso-only --iso-name desktop-44.iso --releasever 44 --macboot
|
||||
EOF
|
||||
|
||||
- name: Upload ISO as artifact
|
||||
uses: actions/upload-artifact@v3
|
||||
with:
|
||||
name: desktop-44.iso
|
||||
path: /var/lmc/desktop-44.iso
|
||||
if-no-files-found: error
|
||||
|
||||
- name: Cleanup mock environment
|
||||
if: always()
|
||||
run: |
|
||||
mock -r fedora-44-x86_64 --clean
|
||||
@@ -1,59 +0,0 @@
|
||||
name: release
|
||||
|
||||
on:
|
||||
push:
|
||||
tags:
|
||||
- "v*.*.*"
|
||||
|
||||
env:
|
||||
FEDORA_VERSION: 44
|
||||
KICKSTART_FILE: desktop_44_standard_grub_gnome_guest-agents
|
||||
|
||||
jobs:
|
||||
checkout:
|
||||
runs-on: fedora
|
||||
defaults:
|
||||
run:
|
||||
shell: bash
|
||||
|
||||
steps:
|
||||
- uses: https://git.phyllo.me/devops/checkout@v5
|
||||
with:
|
||||
fetch-depth: 0
|
||||
|
||||
- name: Initialize mock
|
||||
run: |
|
||||
mock -r fedora-${{ env.FEDORA_VERSION }}-x86_64 --init
|
||||
|
||||
- name: Install required packages
|
||||
run: |
|
||||
mock -r fedora-${{ env.FEDORA_VERSION }}-x86_64 --install lorax-lmc-novirt vim-minimal pykickstart livecd-tools
|
||||
|
||||
- name: Generate dishes
|
||||
run: |
|
||||
cd cook
|
||||
make all
|
||||
|
||||
- name: Copy configuration file to mock
|
||||
run: |
|
||||
mock -r fedora-${{ env.FEDORA_VERSION }}-x86_64 --copyin dishes/${{ env.KICKSTART_FILE }} /builddir
|
||||
|
||||
- name: Build ISO with livemedia-creator
|
||||
run: |
|
||||
mock -r fedora-${{ env.FEDORA_VERSION }}-x86_64 --shell --enable-network --isolation=simple << 'EOF'
|
||||
cd /builddir
|
||||
livemedia-creator --ks ${{ env.KICKSTART_FILE }}.cfg --no-virt --resultdir /var/lmc --project ${{ env.KICKSTART_FILE }} --make-iso --volid ${{ env.KICKSTART_FILE }} --iso-only --iso-name ${{ env.KICKSTART_FILE }}-${{ env.FEDORA_VERSION }}.iso --releasever ${{ env.FEDORA_VERSION }} --macboot
|
||||
EOF
|
||||
|
||||
- name: Release
|
||||
uses: https://git.phyllo.me/devops/action-gh-release@v2
|
||||
if: github.ref_type == 'tag'
|
||||
with:
|
||||
files: /var/lib/mock/fedora-${{ env.FEDORA_VERSION }}-x86_64/root/var/lmc/${{ env.KICKSTART_FILE }}-${{ env.FEDORA_VERSION }}.iso
|
||||
draft: false
|
||||
prerelease: false
|
||||
|
||||
- name: Cleanup mock environment
|
||||
if: always()
|
||||
run: |
|
||||
mock -r fedora-${{ env.FEDORA_VERSION }}-x86_64 --clean
|
||||
@@ -0,0 +1,27 @@
|
||||
name: ci
|
||||
|
||||
on:
|
||||
push:
|
||||
pull_request:
|
||||
|
||||
jobs:
|
||||
# Bump when the recipe language changes; both jobs share the runner.
|
||||
cook:
|
||||
runs-on: fedora
|
||||
defaults:
|
||||
run:
|
||||
shell: bash
|
||||
container:
|
||||
image: git.phyllo.me/devops/fedora-runner-image:latest
|
||||
steps:
|
||||
- uses: https://git.phyllo.me/devops/checkout@v5
|
||||
with:
|
||||
fetch-depth: 0
|
||||
- name: Install Python deps
|
||||
run: pip install -r cook/requirements.txt
|
||||
- name: Lint manifest + templates
|
||||
run: make -C cook lint
|
||||
- name: Generate recipes + dishes, validate every dish
|
||||
run: make -C cook all
|
||||
- name: Run the pytest suite
|
||||
run: make -C cook test
|
||||
+8
-1
@@ -2,7 +2,14 @@
|
||||
cook/recipes/*.cfg
|
||||
cook/dishes/*.cfg
|
||||
|
||||
# Generated build products (created by build-image.sh)
|
||||
build/
|
||||
|
||||
# Python
|
||||
__pycache__/
|
||||
*.py[cod]
|
||||
.pytest_cache/
|
||||
.pytest_cache/
|
||||
# Build artifacts (livemedia-creator side effects)
|
||||
anaconda/
|
||||
livemedia.log
|
||||
program.log
|
||||
|
||||
@@ -10,7 +10,7 @@ Provided that some dependencies are met (`libvirt` is running on your computer,
|
||||
chmod +x deploy.sh
|
||||
```
|
||||
|
||||
- Execute it and pick a dish, e.g. `desktop_44_standard_grub_gnome_guest-agents`, when prompted:
|
||||
- Execute it and pick a dish when prompted (by default the two Phyllome OS editions; see [Editions and tiers](#editions-and-tiers)). The example below is from an earlier, larger matrix:
|
||||
|
||||
```
|
||||
./deploy.sh
|
||||
@@ -35,9 +35,65 @@ Allocating 'desktop_44_standard_grub_gnome_guest-agents.img'
|
||||
Creating domain...
|
||||
```
|
||||
|
||||
`deploy.sh` regenerates the `recipes` and `dishes` (they are build products) before deploying.
|
||||
`deploy.sh` regenerates the `recipes` and `dishes` (they are build products) before deploying. Pass `--tier guest` to deploy a plain Fedora server or desktop VM on a Phyllome OS host instead.
|
||||
After a successfull installation, the virtual machine will shutdown and be ready to use when powered on again.
|
||||
|
||||
## Editions and tiers
|
||||
|
||||
The repo ships a recipe to deploy Phyllome OS on a target host, not a general-purpose kickstart collection. Only two dishes are generated by default (Fedora 44, UEFI, systemd-boot, both with hardware support and guest agents so they also run as VMs):
|
||||
|
||||
| Dish (prefix) | What it is |
|
||||
|---|---|
|
||||
| `phyllomeos` | Phyllome OS with a GUI (GNOME + virt-manager) |
|
||||
| `phyllomeos-headless` | Headless Phyllome OS |
|
||||
|
||||
Every group in `cook/recipes_manifest.yaml` belongs to a *tier*, and only the requested tier is generated:
|
||||
|
||||
| Tier | Contents | Build with |
|
||||
|---|---|---|
|
||||
| `default` | the two editions above | `make all` |
|
||||
| `guest` | `guest-server`, `guest-desktop`: Fedora VMs to deploy on a Phyllome OS host | `make all TIER=guest` |
|
||||
| `experimental` | single hand-written variants to try new dishes (BIOS/GRUB, encrypted root, rawhide) | `make all TIER=experimental` |
|
||||
|
||||
`TIER=all` (or `TIER="default guest"`) combines tiers; `deploy.sh` and `build-image.sh` take `--tier`. Lint always covers every tier, so inactive tiers cannot rot. To test a new dish, add one variant (no list-valued matrices) to an `experimental` group.
|
||||
|
||||
## Building a disk image (no libvirt/virt-install)
|
||||
|
||||
`deploy.sh` drives a live `virt-install`/libvirt session. If you just want a
|
||||
raw or qcow2 disk image — e.g. to hand to another VM manager such as
|
||||
wonder-vm via virtpull's `virtpull local ... --wonder-vm NAME` —
|
||||
use `build-image.sh` instead. It runs `livemedia-creator --no-virt
|
||||
--make-disk` directly on the host, producing a disk image instead of a
|
||||
bootable ISO:
|
||||
|
||||
```
|
||||
./build-image.sh [--dish NAME] [--tier TIER] [--as qcow2] [--root-size MiB] [--extra-ks FILE]
|
||||
```
|
||||
|
||||
It picks a dish the same way `deploy.sh` does (or takes `--dish NAME`) and writes the result under
|
||||
`./build/`. `smoke-test.sh IMAGE` then checks the result: read-only static checks (no `selinux=0` on any kernel command line and labeled files when the image declares SELinux; builds on a host with SELinux disabled fail this), then a KVM boot with `-snapshot` that waits for qemu-guest-agent to answer.
|
||||
|
||||
The `build-image` CI workflow builds, smoke-tests and compresses both default editions. It does not run on plain pushes (a build takes ~25 min and runs as root on the runner); `ci.yml` lints and validates every push instead. It runs on:
|
||||
|
||||
* `v*.*.*` tags: attaches the compressed raw images (`.img.xz`) and the flattened kickstart files to the release;
|
||||
* PRs into `main` labeled `build-image` (set it once the PR is reviewed; it re-runs on every new push while the label stays), for changes under `cook/`, to `build-image.sh`, `smoke-test.sh` or the workflow itself;
|
||||
* a weekly schedule (Mondays 03:00), to catch breakage from upstream Fedora packages;
|
||||
* manual dispatch from the Actions tab, on any branch.
|
||||
|
||||
Non-tag runs keep the images as 7-day workflow artifacts. Requires `lorax-lmc-novirt` (heavier than `deploy.sh`'s plain
|
||||
QEMU/libvirt prerequisites, not installed by
|
||||
`deploy/install-prerequisites-on-linux.sh`) and root — the `--no-virt` disk
|
||||
install needs real loop-device access (`/dev/loop-control`), which a
|
||||
`mock` chroot cannot provide (no live `systemd-udevd` inside it, which
|
||||
breaks blivet's device scan) and neither can a rootless container.
|
||||
`--root-size` (default 8192 MiB) sizes the root partition, needed because
|
||||
livemedia-creator computes the image size up front, unlike virt-install
|
||||
which pre-creates the disk at a known size. `--extra-ks FILE` appends
|
||||
local content (e.g. a bespoke `user`/`rootpw` override) to the dish before
|
||||
building, without touching the tracked ingredients — see the "bespoke
|
||||
dish" pattern below; never commit a file used here if it carries real
|
||||
credentials.
|
||||
|
||||
## Repository structure
|
||||
|
||||
This repository contains such files broken down as:
|
||||
@@ -95,7 +151,7 @@ cd cook && make all
|
||||
Alternatively, for a bespoke dish not part of the matrix, append the ingredient directly to an existing recipe and re-flatten it:
|
||||
|
||||
```
|
||||
echo "%include ../ingredients/extra-luanti.ks # Sandbox video game engine" >> cook/recipes/desktop_44_standard_grub_gnome.cfg
|
||||
echo "%include ../ingredients/extra-luanti.ks # Sandbox video game engine" >> cook/recipes/<recipe>.cfg
|
||||
cd cook && make generate
|
||||
```
|
||||
|
||||
@@ -107,12 +163,13 @@ cd cook && make generate
|
||||
cd cook && make inventory
|
||||
```
|
||||
|
||||
- Define a new edition (a group named e.g. `server`) in `cook/recipes_manifest.yaml` with the desired variants; the cartesian product of its list values generates one dish per combination.
|
||||
- Define a new group in `cook/recipes_manifest.yaml` with a `tier` (use `experimental` for anything that should not be built by default); the cartesian product of its list values generates one dish per combination, so keep it to a single variant.
|
||||
|
||||
### Useful targets (in `cook/`)
|
||||
|
||||
```
|
||||
make all # generate recipes + dishes, lint, and validate (default)
|
||||
make all # generate the default tier's dishes, lint, and validate
|
||||
make all TIER=guest # ... or the guest / experimental / all tiers
|
||||
make lint # check the manifest/templates without writing files
|
||||
make validate # validate existing dishes
|
||||
make inventory # print the ingredient catalog derived from the templates
|
||||
|
||||
Executable
+233
@@ -0,0 +1,233 @@
|
||||
#!/bin/bash
|
||||
# Build a raw (or qcow2) disk image from a dish, without libvirt/virt-install.
|
||||
#
|
||||
# Runs `livemedia-creator --no-virt --make-disk` directly on the host, so
|
||||
# the result is a disk image you can hand to another VM manager directly
|
||||
# (e.g. `virtpull local ./build/<dish>.img --wonder-vm NAME`) instead of a
|
||||
# bootable ISO.
|
||||
#
|
||||
# Usage: ./build-image.sh [--dish NAME] [--tier TIER] [--as qcow2]
|
||||
# [--root-size MiB] [--extra-ks FILE]
|
||||
#
|
||||
# --dish skips the interactive picker (used by CI); --tier selects which
|
||||
# manifest tier of dishes to generate (default: default; see
|
||||
# cook/recipes_manifest.yaml).
|
||||
#
|
||||
# Requires: lorax-lmc-novirt (livemedia-creator), qemu-img, and root (the
|
||||
# --no-virt disk install needs real loop-device access via
|
||||
# /dev/loop-control; individual commands below run under sudo — passwordless
|
||||
# sudo, or run this whole script as root).
|
||||
#
|
||||
# Earlier iteration wrapped this in `mock` (mirroring
|
||||
# .gitea/workflows/build-iso.yaml's ISO build) to pin the target release's
|
||||
# toolchain. That does not work for --make-disk: mock's chroot has no live
|
||||
# systemd-udevd, so udev never populates properties for the loop device
|
||||
# livemedia-creator creates for the image, and blivet's device scan
|
||||
# crashes (blivet/udev.py device_get_name: "TypeError: argument of type
|
||||
# 'NoneType' is not a container or iterable") — reproduced identically on
|
||||
# Fedora 44 and 43 mock chroots, and confirmed absent when run directly on
|
||||
# the host instead. --make-iso never hits this: ISO building doesn't touch
|
||||
# block devices at all, only --make-disk (and other disk/appliance modes)
|
||||
# does. The consequence: this script now builds using whatever
|
||||
# anaconda/lorax the host has installed — the kickstart's own repo/url
|
||||
# lines still pin the actual package set to the target Fedora release
|
||||
# regardless, same as any other direct (non-mock) livemedia-creator use.
|
||||
|
||||
set -euo pipefail
|
||||
|
||||
AS_FORMAT=""
|
||||
DISH=""
|
||||
TIER="${TIER:-default}"
|
||||
ROOT_SIZE=8192
|
||||
EXTRA_KS=""
|
||||
while [[ $# -gt 0 ]]; do
|
||||
case "$1" in
|
||||
--dish)
|
||||
DISH="$2"
|
||||
shift 2
|
||||
;;
|
||||
--tier)
|
||||
TIER="$2"
|
||||
shift 2
|
||||
;;
|
||||
--as)
|
||||
AS_FORMAT="$2"
|
||||
shift 2
|
||||
;;
|
||||
--root-size)
|
||||
ROOT_SIZE="$2"
|
||||
shift 2
|
||||
;;
|
||||
--extra-ks)
|
||||
EXTRA_KS="$2"
|
||||
shift 2
|
||||
;;
|
||||
*)
|
||||
echo "Unknown argument: $1"
|
||||
echo "Usage: $0 [--dish NAME] [--tier TIER] [--as qcow2] [--root-size MiB] [--extra-ks FILE]"
|
||||
exit 1
|
||||
;;
|
||||
esac
|
||||
done
|
||||
if [[ -n "$AS_FORMAT" && "$AS_FORMAT" != "qcow2" ]]; then
|
||||
echo "--as only supports 'qcow2' (the build itself always produces raw)"
|
||||
exit 1
|
||||
fi
|
||||
if ! [[ "$ROOT_SIZE" =~ ^[0-9]+$ ]]; then
|
||||
echo "--root-size must be a whole number of MiB, got '$ROOT_SIZE'"
|
||||
exit 1
|
||||
fi
|
||||
if [[ -n "$EXTRA_KS" && ! -f "$EXTRA_KS" ]]; then
|
||||
echo "--extra-ks file not found: $EXTRA_KS"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
for tool in livemedia-creator qemu-img; do
|
||||
if ! command -v "$tool" &> /dev/null; then
|
||||
echo "Missing required tool: $tool"
|
||||
echo "Fedora: sudo dnf install lorax-lmc-novirt qemu-img"
|
||||
exit 1
|
||||
fi
|
||||
done
|
||||
if [[ $EUID -ne 0 ]] && ! sudo -n true 2>/dev/null; then
|
||||
echo "This needs root (loop-device access for the --no-virt disk install)."
|
||||
echo "Re-run as root, or ensure passwordless sudo is available."
|
||||
exit 1
|
||||
fi
|
||||
|
||||
# Regenerate recipes and dishes before building (they are build products)
|
||||
if ! (cd cook && make all TIER="$TIER"); then
|
||||
echo "Failed to generate dishes in cook/ (see 'make all' in cook/)"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
# Pick a dish: --dish, or interactively (shared with deploy/deploy-distro.sh)
|
||||
if [[ -n "$DISH" ]]; then
|
||||
dish="${DISH%.cfg}"
|
||||
if [[ ! -f "cook/dishes/${dish}.cfg" ]]; then
|
||||
echo "Dish not found: cook/dishes/${dish}.cfg (is --tier '$TIER' right?)"
|
||||
exit 1
|
||||
fi
|
||||
else
|
||||
# shellcheck source=./deploy/select-dish.sh
|
||||
source "$(dirname "$0")/deploy/select-dish.sh"
|
||||
dish="$selected_dish"
|
||||
fi
|
||||
|
||||
# Derive the target release, same convention as
|
||||
# deploy/deploy-distro.sh's find_fedora_iso() (used for --releasever, which
|
||||
# substitutes @VERSION@ in bootloader/template text — the kickstart's own
|
||||
# repo/url lines are what actually pin the package set).
|
||||
if [[ "$dish" == *"rawhide"* ]]; then
|
||||
releasever="rawhide"
|
||||
elif [[ "$dish" =~ _([0-9]+) ]]; then
|
||||
releasever="${BASH_REMATCH[1]}"
|
||||
else
|
||||
echo "Could not determine a Fedora release version from dish name '$dish'."
|
||||
echo "Expected a '_<NN>' segment (e.g. desktop_44_standard...) or 'rawhide'."
|
||||
exit 1
|
||||
fi
|
||||
|
||||
out_dir="./build"
|
||||
mkdir -p "$out_dir"
|
||||
out_dir="$(cd "$out_dir" && pwd)"
|
||||
|
||||
# Adjustments needed only for this --no-virt disk-image path, applied to a
|
||||
# scratch copy rather than the shared dish:
|
||||
#
|
||||
# 1. cook/ingredients/core/base.ks sets `text` for deploy/deploy-distro.sh's
|
||||
# virt-install netinstall console; livemedia-creator manages the display
|
||||
# mode itself and refuses a kickstart that also sets one ("The kickstart
|
||||
# must not set a display mode").
|
||||
#
|
||||
# 2. cook/ingredients' storage layout uses `part / --grow` with no explicit
|
||||
# --size: fine for virt-install, which pre-creates the disk at a known
|
||||
# size for anaconda to grow into. livemedia-creator instead computes the
|
||||
# total image size up front by summing partition sizes, and crashes
|
||||
# (TypeError: unsupported operand type(s) for +: 'int' and 'NoneType')
|
||||
# on a --grow partition with no size to add. Give it one via --root-size
|
||||
# (default 8192 MiB; the desktop dishes likely need more).
|
||||
#
|
||||
# 3. --extra-ks appends arbitrary local content (e.g. a bespoke `user`/
|
||||
# `rootpw`/`security` override) after the above. Kickstart's singular
|
||||
# commands take the last occurrence, so this can override anything the
|
||||
# dish already set without touching the shared ingredients — see the
|
||||
# README's "bespoke dish not part of the matrix" pattern. Never commit
|
||||
# a file used here if it carries real credentials.
|
||||
disk_ks="$(mktemp "${TMPDIR:-/tmp}/${dish}.disk-build.XXXXXX.cfg")"
|
||||
trap 'rm -f "$disk_ks"' EXIT
|
||||
grep -v -E '^(text|cmdline|graphical)([[:space:]]|$)' "cook/dishes/${dish}.cfg" \
|
||||
| awk -v size="$ROOT_SIZE" '
|
||||
/^part / && /--grow/ && !/--size=/ { $0 = $0 " --size=" size }
|
||||
{ print }
|
||||
' > "$disk_ks"
|
||||
if [[ -n "$EXTRA_KS" ]]; then
|
||||
{
|
||||
echo
|
||||
cat "$EXTRA_KS"
|
||||
} >> "$disk_ks"
|
||||
fi
|
||||
|
||||
if command -v ksvalidator &> /dev/null; then
|
||||
echo "Validating kickstart (informational; a build failure is the real test)..."
|
||||
ksvalidator "$disk_ks" || true
|
||||
fi
|
||||
|
||||
resultdir="$(mktemp -u "${TMPDIR:-/tmp}/lmc-result.XXXXXX")"
|
||||
|
||||
# Only one livemedia-creator run per host at a time. It and anaconda use
|
||||
# host-global resources (/mnt/sysroot, /mnt/sysimage, loop and device-mapper
|
||||
# devices), so concurrent builds corrupt each other -- e.g. bootctl failing with
|
||||
# "Couldn't find EFI system partition" when the CI matrix started both editions
|
||||
# at once on a shared runner (Gitea expands each matrix entry into its own
|
||||
# task, so `max-parallel` does not serialize them). A host-wide flock does.
|
||||
# Under the lock, first clear mounts/devices a previously failed build left
|
||||
# behind, which would otherwise poison this one.
|
||||
LOCK=/run/lock/phyllomeos-build-image.lock
|
||||
echo "Building disk image for '$dish' (target release $releasever)..."
|
||||
echo "Waiting for the host build lock ($LOCK) if another build is running..."
|
||||
sudo flock "$LOCK" env DISH="$dish" bash -c '
|
||||
for mnt in /mnt/sysimage /mnt/sysroot; do
|
||||
if findmnt -rn "$mnt" >/dev/null 2>&1; then
|
||||
echo "Cleaning stale mount $mnt from a failed build"
|
||||
umount -R "$mnt" || true
|
||||
fi
|
||||
done
|
||||
for dm in $(dmsetup ls 2>/dev/null | awk -v d="$DISH" "index(\$1, d) == 1 {print \$1}" | sort -r); do
|
||||
echo "Removing stale device-mapper device $dm"
|
||||
dmsetup remove --retry "$dm" || true
|
||||
done
|
||||
for lo in $(losetup -a | awk -F: "/lmc-result\\./ {print \$1}"); do
|
||||
echo "Detaching stale loop device $lo"
|
||||
losetup -d "$lo" || true
|
||||
done
|
||||
exec livemedia-creator "$@"
|
||||
' _ --ks "$disk_ks" --no-virt --resultdir "$resultdir" \
|
||||
--make-disk --image-name "${dish}.img" --releasever "$releasever"
|
||||
|
||||
# livemedia-creator's exact output filename depends on the installed lorax
|
||||
# version; don't assume --image-name is honored verbatim, take whatever
|
||||
# single disk image landed in the resultdir.
|
||||
produced=$(sudo find "$resultdir" -maxdepth 1 -type f \( -iname '*.img' -o -iname '*disk*' \) | head -n 1)
|
||||
if [[ -z "$produced" ]]; then
|
||||
echo "livemedia-creator did not produce a recognizable disk image under $resultdir"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
raw_out="${out_dir}/${dish}.img"
|
||||
sudo cp --reflink=auto "$produced" "$raw_out"
|
||||
sudo chown "$(id -u):$(id -g)" "$raw_out"
|
||||
sudo rm -rf "$resultdir"
|
||||
echo "Built: $raw_out"
|
||||
|
||||
if [[ "$AS_FORMAT" == "qcow2" ]]; then
|
||||
qcow2_out="${out_dir}/${dish}.qcow2"
|
||||
qemu-img convert -O qcow2 "$raw_out" "$qcow2_out"
|
||||
echo "Converted: $qcow2_out"
|
||||
final="$qcow2_out"
|
||||
else
|
||||
final="$raw_out"
|
||||
fi
|
||||
|
||||
echo
|
||||
echo "Next: virtpull local $final --wonder-vm <NAME>"
|
||||
+6
-2
@@ -1,6 +1,9 @@
|
||||
.PHONY: help generate lint validate inventory test install-deps clean clean-recipes clean-dishes all
|
||||
|
||||
PYTHON ?= python3
|
||||
# Manifest tier(s) to generate: default, guest, experimental, all (space-separated)
|
||||
TIER ?= default
|
||||
TIER_ARGS = $(foreach t,$(TIER),--tier $(t))
|
||||
|
||||
default: all
|
||||
|
||||
@@ -9,6 +12,7 @@ help:
|
||||
@echo ""
|
||||
@echo "Available targets:"
|
||||
@echo " all - Generate recipes and dishes, lint, and validate (default)"
|
||||
@echo " TIER=default|guest|experimental|all picks the manifest tier(s)"
|
||||
@echo " generate - Same as 'all'"
|
||||
@echo " lint - Lint manifest and templates without writing files"
|
||||
@echo " validate - Validate existing dishes without regenerating"
|
||||
@@ -22,13 +26,13 @@ help:
|
||||
all: generate
|
||||
|
||||
generate:
|
||||
@$(PYTHON) generate_recipe.py
|
||||
@$(PYTHON) generate_recipe.py $(TIER_ARGS)
|
||||
|
||||
lint:
|
||||
@$(PYTHON) generate_recipe.py --no-generate --no-validate
|
||||
|
||||
validate:
|
||||
@$(PYTHON) generate_recipe.py --no-generate --no-lint
|
||||
@$(PYTHON) generate_recipe.py $(TIER_ARGS) --no-generate --no-lint
|
||||
|
||||
inventory:
|
||||
@$(PYTHON) generate_recipe.py --inventory
|
||||
|
||||
+43
-4
@@ -204,9 +204,42 @@ def validate_dish(dish_path):
|
||||
return False, str(exc)
|
||||
|
||||
|
||||
DEFAULT_TIER = 'default'
|
||||
|
||||
|
||||
def group_tier(group):
|
||||
"""Return the tier of a manifest group (groups default to 'default')."""
|
||||
return group.get('tier', DEFAULT_TIER)
|
||||
|
||||
|
||||
def select_groups(manifest, tiers):
|
||||
"""Return the manifest groups belonging to the requested tiers.
|
||||
|
||||
tiers is a collection of tier names; 'all' selects every group.
|
||||
"""
|
||||
groups = manifest.get('recipes', [])
|
||||
if 'all' in tiers:
|
||||
return groups
|
||||
return [g for g in groups if group_tier(g) in tiers]
|
||||
|
||||
|
||||
def clean_output(directory):
|
||||
"""Remove previously generated .cfg files so stale dishes never linger."""
|
||||
if not os.path.isdir(directory):
|
||||
return
|
||||
for entry in os.listdir(directory):
|
||||
if entry.endswith('.cfg'):
|
||||
os.remove(os.path.join(directory, entry))
|
||||
|
||||
|
||||
def generate(manifest_path, templates_path, recipes_dir, dishes_dir,
|
||||
ingredients_dir, do_generate=True, do_lint=True, do_validate=True):
|
||||
"""Full cooking pipeline. Returns an exit code (0 on success)."""
|
||||
ingredients_dir, do_generate=True, do_lint=True, do_validate=True,
|
||||
tiers=(DEFAULT_TIER,)):
|
||||
"""Full cooking pipeline. Returns an exit code (0 on success).
|
||||
|
||||
Only groups in the requested tiers are written; lint always covers the
|
||||
whole manifest so inactive tiers cannot rot.
|
||||
"""
|
||||
manifest = load_yaml(manifest_path)
|
||||
templates = load_yaml(templates_path)
|
||||
ingredients_root = os.path.join(os.path.dirname(os.path.abspath(manifest_path)),
|
||||
@@ -225,8 +258,10 @@ def generate(manifest_path, templates_path, recipes_dir, dishes_dir,
|
||||
if do_generate:
|
||||
os.makedirs(recipe_dir, exist_ok=True)
|
||||
os.makedirs(dish_dir, exist_ok=True)
|
||||
clean_output(recipe_dir)
|
||||
clean_output(dish_dir)
|
||||
|
||||
for group in manifest.get('recipes', []):
|
||||
for group in select_groups(manifest, tiers):
|
||||
group_name = group.get('name', 'unknown')
|
||||
for config in group.get('variants', []):
|
||||
for variant in expand_variants(config):
|
||||
@@ -315,6 +350,9 @@ def main(argv=None):
|
||||
help='output directory for flattened dishes (default: dishes)')
|
||||
parser.add_argument('--ingredients-dir', default='ingredients',
|
||||
help='ingredient fragments directory (default: ingredients)')
|
||||
parser.add_argument('--tier', action='append', metavar='TIER',
|
||||
help="manifest tier to generate: default, guest, experimental "
|
||||
"or all; repeatable (default: default)")
|
||||
parser.add_argument('--no-generate', action='store_true',
|
||||
help='do not write recipes or dishes (lint/validate only)')
|
||||
parser.add_argument('--no-lint', action='store_true',
|
||||
@@ -342,7 +380,8 @@ def main(argv=None):
|
||||
args.ingredients_dir,
|
||||
do_generate=not args.no_generate,
|
||||
do_lint=not args.no_lint,
|
||||
do_validate=not args.no_validate)
|
||||
do_validate=not args.no_validate,
|
||||
tiers=tuple(args.tier or (DEFAULT_TIER,)))
|
||||
|
||||
|
||||
if __name__ == '__main__':
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
# Keyboard, language, and timezone configuration
|
||||
|
||||
keyboard --xlayouts='ch (fr)' # Set keyboard layouts for Romandy
|
||||
keyboard --vckeymap=ch-fr --xlayouts='ch (fr)' # Set keyboard layouts for Romandy: --vckeymap for the text console (tty login, headless), --xlayouts for graphical sessions
|
||||
lang en_US.UTF-8 # Set system language to American English. More languages could be supported: --addsupport=cs_CZ,de_DE,en_UK
|
||||
timezone Europe/Zurich --utc # Set system timezone to Zurich
|
||||
|
||||
@@ -1,3 +1,3 @@
|
||||
# Network configuration
|
||||
|
||||
network --onboot=yes --bootproto=dhcp --device=link --activate --hostname=phyllome-alpha # Configure network devices, enable them at boot time device and sets a particular hostname. "link" selects the first device reaching an up state
|
||||
network --onboot=yes --bootproto=dhcp --device=link --activate --hostname=phyllomeos # Configure network devices, enable them at boot time device and sets a generic hostname (rename per machine after install). "link" selects the first device reaching an up state
|
||||
|
||||
@@ -2,8 +2,27 @@
|
||||
|
||||
%post --nochroot --log=/mnt/sysimage/root/hypervisor-amdcpu-post.log # Beginning of %post section. Those commands are executed outside the chroot environment. Logging is enabled to help with post-installation troubleshooting
|
||||
|
||||
sed -i 's/\(quiet\)/\1 iommu=pt rd.driver.pre=vfio-pci/i' /mnt/sysimage/etc/default/grub # Load kernel modules in GRUB.
|
||||
# Kernel arguments cannot be attached to a single canonical location at %post time:
|
||||
# anaconda always creates /etc/kernel/cmdline (kernel-install source for both
|
||||
# systemd-boot and GRUB+BLS layouts), but GRUB+BLS boot entries have already been
|
||||
# generated from it by the kernel package scriptlets, and non-BLS flows still read
|
||||
# /etc/default/grub. Update every surface idempotently, guarded by file existence.
|
||||
|
||||
echo "options kvm_amd nested=1" >> /mnt/sysimage/etc/modprobe.d/kvm.conf # Add support for nested virtualization
|
||||
IOMMU_ARGS="iommu=pt rd.driver.pre=vfio-pci"
|
||||
|
||||
if [ -f /mnt/sysimage/etc/kernel/cmdline ]; then # kernel-install source (systemd-boot now, GRUB+BLS for future kernels)
|
||||
grep -q "iommu=pt" /mnt/sysimage/etc/kernel/cmdline || echo " $IOMMU_ARGS" >> /mnt/sysimage/etc/kernel/cmdline # Append IOMMU arguments once
|
||||
fi
|
||||
|
||||
if [ -f /mnt/sysimage/etc/default/grub ]; then # GRUB: arguments for grub.cfg-based boot flows
|
||||
grep -q "iommu=pt" /mnt/sysimage/etc/default/grub || sed -i "s/\(quiet\)/\1 $IOMMU_ARGS/i" /mnt/sysimage/etc/default/grub # Append IOMMU arguments once
|
||||
fi
|
||||
|
||||
for bls_entry in /mnt/sysimage/boot/loader/entries/*.conf; do # Already-generated boot entries share one "options" line shape
|
||||
[ -e "$bls_entry" ] || continue # Nothing matched: the glob stays literal
|
||||
grep -q "iommu=pt" "$bls_entry" || sed -i "s/^\(options .*\)/\1 $IOMMU_ARGS/" "$bls_entry" # Patch the options line of each entry once
|
||||
done
|
||||
|
||||
grep -q "nested=1" /mnt/sysimage/etc/modprobe.d/kvm.conf 2>/dev/null || echo "options kvm_amd nested=1" >> /mnt/sysimage/etc/modprobe.d/kvm.conf # Add support for nested virtualization on AMD CPUs, exactly once
|
||||
|
||||
%end # End of the %post section
|
||||
|
||||
@@ -0,0 +1,33 @@
|
||||
# CPU-agnostic optimization for hypervisor (Intel and AMD)
|
||||
#
|
||||
# One ingredient for both CPU vendors, so a single dish can be installed on either:
|
||||
# * intel_iommu=on is ignored by AMD kernels (amd_iommu is enabled by default).
|
||||
# * "options kvm_intel"/"options kvm_amd" only apply to the module that actually loads.
|
||||
|
||||
%post --nochroot --log=/mnt/sysimage/root/hypervisor-cpu-post.log # Beginning of %post section. Those commands are executed outside the chroot environment. Logging is enabled to help with post-installation troubleshooting
|
||||
|
||||
# Kernel arguments cannot be attached to a single canonical location at %post time:
|
||||
# anaconda always creates /etc/kernel/cmdline (kernel-install source for both
|
||||
# systemd-boot and GRUB+BLS layouts), but GRUB+BLS boot entries have already been
|
||||
# generated from it by the kernel package scriptlets, and non-BLS flows still read
|
||||
# /etc/default/grub. Update every surface idempotently, guarded by file existence.
|
||||
|
||||
IOMMU_ARGS="intel_iommu=on iommu=pt rd.driver.pre=vfio-pci"
|
||||
|
||||
if [ -f /mnt/sysimage/etc/kernel/cmdline ]; then # kernel-install source (systemd-boot now, GRUB+BLS for future kernels)
|
||||
grep -q "iommu=pt" /mnt/sysimage/etc/kernel/cmdline || echo " $IOMMU_ARGS" >> /mnt/sysimage/etc/kernel/cmdline # Append IOMMU arguments once
|
||||
fi
|
||||
|
||||
if [ -f /mnt/sysimage/etc/default/grub ]; then # GRUB: arguments for grub.cfg-based boot flows
|
||||
grep -q "iommu=pt" /mnt/sysimage/etc/default/grub || sed -i "s/\(quiet\)/\1 $IOMMU_ARGS/i" /mnt/sysimage/etc/default/grub # Append IOMMU arguments once
|
||||
fi
|
||||
|
||||
for bls_entry in /mnt/sysimage/boot/loader/entries/*.conf; do # Already-generated boot entries share one "options" line shape
|
||||
[ -e "$bls_entry" ] || continue # Nothing matched: the glob stays literal
|
||||
grep -q "iommu=pt" "$bls_entry" || sed -i "s/^\(options .*\)/\1 $IOMMU_ARGS/" "$bls_entry" # Patch the options line of each entry once
|
||||
done
|
||||
|
||||
grep -q "kvm_intel" /mnt/sysimage/etc/modprobe.d/kvm.conf 2>/dev/null || echo "options kvm_intel nested=1" >> /mnt/sysimage/etc/modprobe.d/kvm.conf # Add support for nested virtualization on Intel CPUs, exactly once
|
||||
grep -q "kvm_amd" /mnt/sysimage/etc/modprobe.d/kvm.conf 2>/dev/null || echo "options kvm_amd nested=1" >> /mnt/sysimage/etc/modprobe.d/kvm.conf # Add support for nested virtualization on AMD CPUs, exactly once
|
||||
|
||||
%end # End of the %post section
|
||||
@@ -2,12 +2,27 @@
|
||||
|
||||
%post --nochroot --log=/mnt/sysimage/root/hypervisor-intelcpu-post.log # Beginning of %post section. Those commands are executed outside the chroot environment. Logging is enabled to help with post-installation troubleshooting
|
||||
|
||||
if [ -f /mnt/sysimage/etc/kernel/cmdline ]; then # systemd-boot: kernel arguments live in /etc/kernel/cmdline
|
||||
grep -q "intel_iommu=on" /mnt/sysimage/etc/kernel/cmdline || sed -i 's/$/ intel_iommu=on iommu=pt rd.driver.pre=vfio-pci/' /mnt/sysimage/etc/kernel/cmdline # Append IOMMU arguments once
|
||||
else # GRUB: kernel arguments live in /etc/default/grub
|
||||
sed -i 's/\(quiet\)/\1 intel_iommu=on iommu=pt rd.driver.pre=vfio-pci/i' /mnt/sysimage/etc/default/grub # Load kernel modules in GRUB.
|
||||
# Kernel arguments cannot be attached to a single canonical location at %post time:
|
||||
# anaconda always creates /etc/kernel/cmdline (kernel-install source for both
|
||||
# systemd-boot and GRUB+BLS layouts), but GRUB+BLS boot entries have already been
|
||||
# generated from it by the kernel package scriptlets, and non-BLS flows still read
|
||||
# /etc/default/grub. Update every surface idempotently, guarded by file existence.
|
||||
|
||||
IOMMU_ARGS="intel_iommu=on iommu=pt rd.driver.pre=vfio-pci"
|
||||
|
||||
if [ -f /mnt/sysimage/etc/kernel/cmdline ]; then # kernel-install source (systemd-boot now, GRUB+BLS for future kernels)
|
||||
grep -q "intel_iommu=on" /mnt/sysimage/etc/kernel/cmdline || echo " $IOMMU_ARGS" >> /mnt/sysimage/etc/kernel/cmdline # Append IOMMU arguments once
|
||||
fi
|
||||
|
||||
echo "options kvm_intel nested=1" >> /mnt/sysimage/etc/modprobe.d/kvm.conf # Add support for nested virtualization on Intel CPUs
|
||||
if [ -f /mnt/sysimage/etc/default/grub ]; then # GRUB: arguments for grub.cfg-based boot flows
|
||||
grep -q "intel_iommu=on" /mnt/sysimage/etc/default/grub || sed -i "s/\(quiet\)/\1 $IOMMU_ARGS/i" /mnt/sysimage/etc/default/grub # Append IOMMU arguments once
|
||||
fi
|
||||
|
||||
for bls_entry in /mnt/sysimage/boot/loader/entries/*.conf; do # Already-generated boot entries share one "options" line shape
|
||||
[ -e "$bls_entry" ] || continue # Nothing matched: the glob stays literal
|
||||
grep -q "intel_iommu=on" "$bls_entry" || sed -i "s/^\(options .*\)/\1 $IOMMU_ARGS/" "$bls_entry" # Patch the options line of each entry once
|
||||
done
|
||||
|
||||
grep -q "nested=1" /mnt/sysimage/etc/modprobe.d/kvm.conf 2>/dev/null || echo "options kvm_intel nested=1" >> /mnt/sysimage/etc/modprobe.d/kvm.conf # Add support for nested virtualization on Intel CPUs, exactly once
|
||||
|
||||
%end # End of the %post section
|
||||
|
||||
@@ -10,16 +10,16 @@ gnome-initial-setup # Add GNOME initial setup too to let user create local accou
|
||||
|
||||
%end # End of the packages section
|
||||
|
||||
# %post --nochroot --log=/mnt/sysimage/root/initial-setup-gnome.log # Beginning of %post section. Those commands are executed outside the chroot environment. Add logging.
|
||||
#
|
||||
# truncate -s 0 /mnt/sysimage/usr/share/gnome-initial-setup/vendor.conf # remove content of vendor.conf so that all options are made available
|
||||
#
|
||||
# ## Append lines to existing vendor.conf file, so that options are skipped upon reboot
|
||||
# cat >> /mnt/sysimage/usr/share/gnome-initial-setup/vendor.conf<< EOF
|
||||
# [pages]
|
||||
# skip=privacy
|
||||
# [goa]
|
||||
# providers=local-first!
|
||||
# EOF
|
||||
#
|
||||
# %end # End of the %post section
|
||||
%post --nochroot --log=/mnt/sysimage/root/initial-setup-gnome.log # Beginning of %post section. Those commands are executed outside the chroot environment. Add logging.
|
||||
|
||||
truncate -s 0 /mnt/sysimage/usr/share/gnome-initial-setup/vendor.conf # remove content of vendor.conf so that all options are made available
|
||||
|
||||
## Append lines to existing vendor.conf file, so that options are skipped upon reboot
|
||||
cat >> /mnt/sysimage/usr/share/gnome-initial-setup/vendor.conf<< EOF
|
||||
[pages]
|
||||
skip=privacy
|
||||
[goa]
|
||||
providers=local-first!
|
||||
EOF
|
||||
|
||||
%end # End of the %post section
|
||||
|
||||
@@ -0,0 +1,8 @@
|
||||
# BIOS boot storage configuration (GPT disk label with a BIOS boot partition)
|
||||
|
||||
zerombr # Destroy all the contents of disks with invalid partition tables or other formatting unrecognizable to the installer
|
||||
clearpart --all --initlabel --disklabel=gpt # Erase all partitions, initialize a GPT disk label, and initialize the disk label to the default for the target architecture
|
||||
|
||||
part biosboot --fstype="biosboot" --size=2 --label=biosboot # Creates a 2 MiB BIOS boot partition, required to install GRUB on a GPT-labelled disk under legacy BIOS firmware
|
||||
part /boot --fstype="ext4" --size=2048 --label=boot # Creates a 2048 MiB ext4 boot partition
|
||||
part / --fstype="ext4" --grow --label=root --mkfsoptions="-O encrypt,fast_commit" # Create a single root partition with the remaining space
|
||||
@@ -6,3 +6,70 @@ clearpart --all --initlabel # Erase all partitions and Initializes the disk labe
|
||||
part /boot/efi --fstype="efi" --size=512 --fsoptions="umask=0077,shortname=winnt" --label=efi # Creates a 512 MiB EFI system partition
|
||||
part /boot --fstype="ext4" --size=2048 --label=boot # Creates a 2048 MiB ext4 boot partition
|
||||
part / --fstype="ext4" --grow --label=root --mkfsoptions="-O encrypt,fast_commit" --encrypted --passphrase= # Create a single encrypted root partition with the remaining space.
|
||||
|
||||
# Tools for LUKS management and TPM2-based auto-unlock
|
||||
%packages --exclude-weakdeps
|
||||
|
||||
cryptsetup # Userspace tool for dm-crypt, required to open and manage the LUKS root device
|
||||
tpm2-tools # TPM2 tools, provides the dracut tpm2-tss module needed to rebuild the initrd for TPM2 auto-unlock
|
||||
tpm2-tss # TPM2 TSS libraries, runtime dependency of tpm2-tools and systemd-cryptenroll
|
||||
|
||||
%end # End of the packages section
|
||||
|
||||
# Stash a preseeded LUKS passphrase so the %post below can enroll TPM2-based
|
||||
# auto-unlock. Only acts when `--passphrase=` was preseeded on the encrypted
|
||||
# `part /` line; with the interactive default (empty passphrase) nothing is
|
||||
# stashed and the %post enrollment is skipped cleanly.
|
||||
%pre --logfile=/tmp/luks-tpm-pre.log
|
||||
|
||||
KS_FILE=""
|
||||
for candidate in /run/install/ks.cfg /tmp/ks.cfg; do
|
||||
if [ -f "$candidate" ]; then
|
||||
KS_FILE="$candidate"
|
||||
break
|
||||
fi
|
||||
done
|
||||
|
||||
if [ -n "$KS_FILE" ]; then
|
||||
LINE=$(grep -E '^[[:space:]]*part[[:space:]]+/[[:space:]].*--encrypted' "$KS_FILE" | grep -- '--passphrase=' | head -n 1)
|
||||
if [ -n "$LINE" ]; then
|
||||
PW=${LINE#*--passphrase=}
|
||||
case "$PW" in
|
||||
\"*) PW=${PW#\"}; PW=${PW%%\"*} ;;
|
||||
*) PW=${PW%%[[:space:]]*} ;;
|
||||
esac
|
||||
if [ -n "$PW" ]; then
|
||||
printf '%s' "$PW" > /tmp/.luks-passphrase
|
||||
chmod 600 /tmp/.luks-passphrase
|
||||
fi
|
||||
fi
|
||||
fi
|
||||
|
||||
%end
|
||||
|
||||
# Enroll TPM2-based LUKS auto-unlock (PCR 7-bound) when both guards hold: a
|
||||
# TPM2 device is available at install time and a passphrase was preseeded in
|
||||
# the kickstart (stashed by the %pre above). Otherwise the system keeps the
|
||||
# regular passphrase prompt at boot. On any failure the LUKS configuration is
|
||||
# left untouched, so the worst case is a passphrase prompt on every boot.
|
||||
%post --logfile=/root/luks-tpm-post.log
|
||||
|
||||
if [ -e /dev/tpmrm0 ] && [ -s /tmp/.luks-passphrase ]; then
|
||||
set -x
|
||||
LUKSUUID=$(awk '$2 ~ /^UUID=/ {sub(/^UUID=/, "", $2); print $2}' /etc/crypttab)
|
||||
LUKSDEV="/dev/disk/by-uuid/$LUKSUUID"
|
||||
if systemd-cryptenroll --tpm2-device=auto --tpm2-pcrs=7 --unlock-key-file=/tmp/.luks-passphrase "$LUKSDEV"; then
|
||||
sed -i 's| none discard,x-initrd.attach| tpm2-device=auto,discard,x-initrd.attach|' /etc/crypttab
|
||||
if ! grep -q 'tpm2-device=auto' /etc/crypttab; then
|
||||
sed -i -E 's|[[:space:]]none([[:space:]]|$)| tpm2-device=auto,discard\1|' /etc/crypttab
|
||||
fi
|
||||
if grep -q 'tpm2-device=auto' /etc/crypttab; then
|
||||
MID=$(cat /etc/machine-id)
|
||||
KVER=$(ls /usr/lib/modules | head -n 1)
|
||||
dracut -f --kver "$KVER" "/boot/efi/$MID/$KVER/initrd"
|
||||
fi
|
||||
fi
|
||||
shred -u /tmp/.luks-passphrase 2>/dev/null || true
|
||||
fi
|
||||
|
||||
%end
|
||||
|
||||
@@ -35,6 +35,8 @@ choices:
|
||||
storage:
|
||||
standard: storage/standard.ks
|
||||
encrypted: storage/encrypted.ks
|
||||
biosboot: storage/biosboot.ks
|
||||
livefs: live/core/storage.ks
|
||||
bootloader:
|
||||
grub: bootloader/grub.ks
|
||||
systemd-boot: bootloader/systemd-boot.ks
|
||||
@@ -60,11 +62,21 @@ features:
|
||||
- hypervisor/base/services.ks
|
||||
- hypervisor/base/post-scripts.ks
|
||||
desktop:
|
||||
- hypervisor/base/packages.ks
|
||||
- hypervisor/base/services.ks
|
||||
- hypervisor/base/post-scripts.ks
|
||||
- packages/virtual-machine-manager/packages.ks
|
||||
- packages/virtual-machine-manager/post-scripts.ks
|
||||
hypervisor_type:
|
||||
any: hypervisor/cpu-agnostic.ks
|
||||
amdcpu: hypervisor/amdcpu.ks
|
||||
intelcpu: hypervisor/intelcpu.ks
|
||||
intelgpu: hypervisor/intelgpu.ks
|
||||
hardware-support: packages/hardware-support.ks
|
||||
guest-agents: guest-agents/base.ks
|
||||
guest-agents: guest-agents/base.ks
|
||||
live:
|
||||
true:
|
||||
- live/core/base.ks
|
||||
- live/core/packages.ks
|
||||
- live/post/base.ks
|
||||
- live/post/session.ks
|
||||
+100
-36
@@ -2,59 +2,123 @@
|
||||
# Define all recipe variants using ingredients from recipe_templates.yaml.
|
||||
# Edit this file to add new variants, then run: make all
|
||||
#
|
||||
# Tiers: every group belongs to a tier (default: "default"). Only the requested
|
||||
# tier is generated, so the default build stays tiny:
|
||||
# default -- the two shipping editions (phyllomeos, phyllomeos-headless)
|
||||
# guest -- Fedora VM guests to deploy on a Phyllome OS host
|
||||
# experimental -- one-off variants for testing new dishes
|
||||
# Build a tier with: make all TIER=guest (or: TIER="default guest", TIER=all)
|
||||
# Lint always covers every tier.
|
||||
#
|
||||
# Syntax:
|
||||
# - Single values: repository: "43"
|
||||
# - List values: repository: ["43", "44", "rawhide"] (expands to multiple variants)
|
||||
# - Single values: repository: "44"
|
||||
# - List values: repository: ["43", "44"] (expands to multiple variants)
|
||||
# - Booleans: hardware-support: true / false (feature flags)
|
||||
#
|
||||
# NOTE: List values create a cartesian product.
|
||||
# Example: repository: ["43", "44", "rawhide"] + storage: ["standard", "encrypted"]
|
||||
# creates 6 variants: (43,standard), (43,encrypted), (44,standard), (44,encrypted),
|
||||
# (rawhide,standard), (rawhide,encrypted)
|
||||
# NOTE: List values create a cartesian product. Keep it out of the default and
|
||||
# guest tiers; prefer one hand-written variant per experimental dish.
|
||||
#
|
||||
# NOTE: The 'name' field prefixes every generated dish/recipe filename, e.g.
|
||||
# the group below yields dishes like desktop_43_gnome_standard_grub_guest-agents.cfg.
|
||||
# the "phyllomeos" group yields phyllomeos_44_standard_systemd-boot_...cfg.
|
||||
|
||||
recipes:
|
||||
# Desktop variants
|
||||
- name: desktop
|
||||
variants:
|
||||
- repository: ["43", "44", "rawhide"]
|
||||
desktop: gnome
|
||||
storage: ["standard", "encrypted"]
|
||||
bootloader: ["grub", "systemd-boot"]
|
||||
hardware-support: [true, false]
|
||||
guest-agents: [true, false]
|
||||
# --- default tier: the two shipping editions -------------------------------
|
||||
|
||||
# Server variants
|
||||
- name: server
|
||||
# Phyllome OS with a GUI (GNOME + virt-manager)
|
||||
- name: phyllomeos
|
||||
tier: default
|
||||
variants:
|
||||
- repository: ["44"]
|
||||
- repository: "44"
|
||||
desktop: gnome
|
||||
storage: standard
|
||||
bootloader: systemd-boot
|
||||
security: enabled
|
||||
initial-setup: gnome
|
||||
hardware-support: true
|
||||
guest-agents: true
|
||||
hypervisor: desktop
|
||||
hypervisor_type: any
|
||||
|
||||
# Headless Phyllome OS
|
||||
- name: phyllomeos-headless
|
||||
tier: default
|
||||
variants:
|
||||
- repository: "44"
|
||||
storage: standard
|
||||
bootloader: systemd-boot
|
||||
security: enabled
|
||||
initial-setup: server
|
||||
hardware-support: true
|
||||
guest-agents: true
|
||||
hypervisor: base
|
||||
hypervisor_type: any
|
||||
|
||||
# --- guest tier: VMs to deploy on a Phyllome OS host -------------------------
|
||||
|
||||
- name: guest-server
|
||||
tier: guest
|
||||
variants:
|
||||
- repository: "44"
|
||||
storage: standard
|
||||
bootloader: systemd-boot
|
||||
security: enabled
|
||||
initial-setup: server
|
||||
hardware-support: false
|
||||
guest-agents: true
|
||||
initial-setup: server
|
||||
|
||||
# Hypervisor variants
|
||||
- name: hypervisor
|
||||
- name: guest-desktop
|
||||
tier: guest
|
||||
variants:
|
||||
- repository: ["44"]
|
||||
- repository: "44"
|
||||
desktop: gnome
|
||||
storage: standard
|
||||
bootloader: systemd-boot
|
||||
security: enabled
|
||||
initial-setup: gnome
|
||||
hardware-support: false
|
||||
guest-agents: true
|
||||
|
||||
# --- experimental tier: single variants, no matrices -------------------------
|
||||
|
||||
# BIOS (legacy firmware) laptop: GPT + biosboot partition, GRUB
|
||||
- name: exp-bios-desktop-hypervisor
|
||||
tier: experimental
|
||||
variants:
|
||||
- repository: "44"
|
||||
desktop: gnome
|
||||
storage: biosboot
|
||||
bootloader: grub
|
||||
security: enabled
|
||||
initial-setup: gnome
|
||||
hardware-support: true
|
||||
guest-agents: false
|
||||
hypervisor: desktop
|
||||
hypervisor_type: any
|
||||
|
||||
# Encrypted root
|
||||
- name: exp-encrypted-headless
|
||||
tier: experimental
|
||||
variants:
|
||||
- repository: "44"
|
||||
storage: encrypted
|
||||
bootloader: systemd-boot
|
||||
security: enabled
|
||||
initial-setup: server
|
||||
hardware-support: true
|
||||
guest-agents: true
|
||||
hypervisor: base
|
||||
hypervisor_type: intelcpu
|
||||
#
|
||||
# # Desktop-hypervisor variants
|
||||
# - name: desktop-hypervisor
|
||||
# variants:
|
||||
# - repository: 43
|
||||
# desktop: gnome
|
||||
# storage: standard
|
||||
# bootloader: grub
|
||||
# hardware-support: false
|
||||
# guest-agents: true
|
||||
# hypervisor: desktop
|
||||
# hypervisor_type: ["amdcpu", "intelcpu"]
|
||||
hypervisor_type: any
|
||||
|
||||
# Rawhide
|
||||
- name: exp-rawhide-headless
|
||||
tier: experimental
|
||||
variants:
|
||||
- repository: rawhide
|
||||
storage: standard
|
||||
bootloader: systemd-boot
|
||||
security: enabled
|
||||
initial-setup: server
|
||||
hardware-support: true
|
||||
guest-agents: true
|
||||
hypervisor: base
|
||||
hypervisor_type: any
|
||||
|
||||
@@ -1,2 +1,3 @@
|
||||
PyYAML>=6.0
|
||||
pykickstart>=1.99
|
||||
pytest>=7.0
|
||||
|
||||
@@ -20,7 +20,7 @@ BASE_FRAGMENTS = [
|
||||
"core/locale.ks",
|
||||
"core/network.ks",
|
||||
"core/services.ks",
|
||||
"packages/core.ks",
|
||||
"packages/core-explicit.ks",
|
||||
"packages/fedora-remix.ks",
|
||||
"packages/hand-picked.ks",
|
||||
]
|
||||
@@ -278,4 +278,77 @@ def test_generate_writes_recipes_and_dishes(tmp_path, templates):
|
||||
assert len(list(recipes.glob("*.cfg"))) == 1
|
||||
assert len(list(dishes.glob("*.cfg"))) == 1
|
||||
dish = (dishes / "desktop_43_standard_grub_gnome.cfg").read_text()
|
||||
assert "%include" not in dish
|
||||
assert "%include" not in dish
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# tiers and the real manifest
|
||||
# ---------------------------------------------------------------------------
|
||||
|
||||
def real_manifest():
|
||||
return gen.load_yaml(os.path.join(COOK_DIR, "recipes_manifest.yaml"))
|
||||
|
||||
|
||||
def dish_names(manifest, templates, tiers):
|
||||
return sorted(
|
||||
gen.render_filename(templates, group["name"], variant)
|
||||
for group in gen.select_groups(manifest, tiers)
|
||||
for config in group["variants"]
|
||||
for variant in gen.expand_variants(config))
|
||||
|
||||
|
||||
def test_select_groups_defaults_untiered_groups_to_default():
|
||||
manifest = {"recipes": [{"name": "a"}, {"name": "b", "tier": "guest"}]}
|
||||
assert [g["name"] for g in gen.select_groups(manifest, ("default",))] == ["a"]
|
||||
assert [g["name"] for g in gen.select_groups(manifest, ("guest",))] == ["b"]
|
||||
assert [g["name"] for g in gen.select_groups(manifest, ("all",))] == ["a", "b"]
|
||||
|
||||
|
||||
def test_real_manifest_lints_clean_across_all_tiers(templates):
|
||||
problems = gen.lint_manifest(real_manifest(), templates,
|
||||
os.path.join(COOK_DIR, "ingredients"))
|
||||
assert problems == []
|
||||
|
||||
|
||||
def test_default_tier_is_the_two_editions(templates):
|
||||
names = dish_names(real_manifest(), templates, ("default",))
|
||||
assert len(names) == 2
|
||||
assert sorted(n.split("_")[0] for n in names) == ["phyllomeos", "phyllomeos-headless"]
|
||||
|
||||
|
||||
def test_guest_tier_is_server_and_desktop(templates):
|
||||
names = dish_names(real_manifest(), templates, ("guest",))
|
||||
assert sorted(n.split("_")[0] for n in names) == ["guest-desktop", "guest-server"]
|
||||
assert all("guest-agents" in n for n in names)
|
||||
|
||||
|
||||
def test_shipping_dishes_contents(templates):
|
||||
manifest = real_manifest()
|
||||
for group in gen.select_groups(manifest, ("default",)):
|
||||
variant = gen.expand_variants(group["variants"][0])[0]
|
||||
fragments, problems = gen.collect_fragments(templates, variant)
|
||||
assert problems == []
|
||||
assert "hypervisor/cpu-agnostic.ks" in fragments
|
||||
assert "bootloader/systemd-boot.ks" in fragments
|
||||
assert "storage/standard.ks" in fragments
|
||||
assert ("desktop/gnome/config.ks" in fragments) == (group["name"] == "phyllomeos")
|
||||
|
||||
|
||||
def test_generate_only_writes_requested_tier_and_clears_stale(tmp_path):
|
||||
(tmp_path / "ingredients").symlink_to(os.path.join(COOK_DIR, "ingredients"),
|
||||
target_is_directory=True)
|
||||
recipes = tmp_path / "recipes"
|
||||
dishes = tmp_path / "dishes"
|
||||
recipes.mkdir()
|
||||
dishes.mkdir()
|
||||
(dishes / "stale.cfg").write_text("stale")
|
||||
(recipes / "stale.cfg").write_text("stale")
|
||||
templates_path = os.path.join(COOK_DIR, "recipe_templates.yaml")
|
||||
manifest_path = os.path.join(COOK_DIR, "recipes_manifest.yaml")
|
||||
|
||||
code = gen.generate(manifest_path, templates_path, str(recipes), str(dishes),
|
||||
"ingredients", tiers=("guest",))
|
||||
assert code == 0
|
||||
names = sorted(p.name.split("_")[0] for p in dishes.glob("*.cfg"))
|
||||
assert names == ["guest-desktop", "guest-server"]
|
||||
assert not (dishes / "stale.cfg").exists()
|
||||
assert not (recipes / "stale.cfg").exists()
|
||||
|
||||
@@ -11,8 +11,26 @@ execute_script() {
|
||||
return 0 # Indicate success
|
||||
}
|
||||
|
||||
# Usage: ./deploy.sh [--tier default|guest|experimental|all]
|
||||
# Tier of dishes to generate and offer (default: default). Use "guest" to
|
||||
# deploy a plain Fedora server/desktop VM on a Phyllome OS host.
|
||||
TIER="${TIER:-default}"
|
||||
while [[ $# -gt 0 ]]; do
|
||||
case "$1" in
|
||||
--tier)
|
||||
TIER="$2"
|
||||
shift 2
|
||||
;;
|
||||
*)
|
||||
echo "Unknown argument: $1"
|
||||
echo "Usage: $0 [--tier default|guest|experimental|all]"
|
||||
exit 1
|
||||
;;
|
||||
esac
|
||||
done
|
||||
|
||||
# Generate recipes and dishes before deployment (they are build products)
|
||||
if ! (cd cook && make all); then
|
||||
if ! (cd cook && make all TIER="$TIER"); then
|
||||
echo "Failed to generate dishes in cook/ (see 'make all' in cook/)"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
+4
-29
@@ -141,35 +141,10 @@ fi
|
||||
# Display the selected option (optional)
|
||||
echo "You selected: $uri"
|
||||
|
||||
# Get a list of files in "dishes" directory
|
||||
mapfile -t dish_name < <(find "cook/dishes/" -maxdepth 1 -type f \( -name "*guest-agents*" \) -printf "%f\n" | sed 's/\.[^.]*$//')
|
||||
|
||||
# Check if there are any files
|
||||
if [ ${#dish_name[@]} -eq 0 ]; then
|
||||
echo "No files found in the directory ../dishes."
|
||||
exit 1
|
||||
fi
|
||||
|
||||
# Display the files with numbered options
|
||||
echo "Available files:"
|
||||
for i in "${!dish_name[@]}"; do
|
||||
echo "$((i + 1)). ${dish_name[$i]}"
|
||||
done
|
||||
|
||||
# Prompt the user to select a file
|
||||
read -r -p "Enter the number of the file you want to select: " choice
|
||||
|
||||
# Validate the user's choice
|
||||
if ! [[ "$choice" =~ ^[0-9]+$ ]] || (( choice < 1 )) || (( choice > ${#dish_name[@]} )); then
|
||||
echo "Invalid choice. Please enter a number from 1 to ${#dish_name[@]}."
|
||||
exit 1
|
||||
fi
|
||||
|
||||
# Get the selected filename
|
||||
vm_name="${dish_name[$((choice - 1))]}"
|
||||
|
||||
# Output the selected filename
|
||||
echo "You selected: $vm_name"
|
||||
# Pick a dish (shared with build-image.sh)
|
||||
# shellcheck source=./select-dish.sh
|
||||
source "$(dirname "$0")/select-dish.sh"
|
||||
vm_name="$selected_dish"
|
||||
|
||||
# Find Fedora ISO based on the dish name
|
||||
fedora_iso=$(find_fedora_iso "$vm_name")
|
||||
|
||||
@@ -0,0 +1,27 @@
|
||||
#!/bin/bash
|
||||
# Lists cook/dishes/*guest-agents*.cfg and prompts the user to pick one.
|
||||
# Meant to be sourced (not executed) from repo root, after `cook && make all`
|
||||
# has regenerated dishes. Sets $selected_dish to the chosen dish's basename
|
||||
# (no extension) on success, or exits non-zero.
|
||||
|
||||
mapfile -t dish_name < <(find "cook/dishes/" -maxdepth 1 -type f \( -name "*guest-agents*" \) -printf "%f\n" | sed 's/\.[^.]*$//')
|
||||
|
||||
if [ ${#dish_name[@]} -eq 0 ]; then
|
||||
echo "No files found in cook/dishes/."
|
||||
exit 1
|
||||
fi
|
||||
|
||||
echo "Available dishes:"
|
||||
for i in "${!dish_name[@]}"; do
|
||||
echo "$((i + 1)). ${dish_name[$i]}"
|
||||
done
|
||||
|
||||
read -r -p "Enter the number of the dish you want to select: " choice
|
||||
|
||||
if ! [[ "$choice" =~ ^[0-9]+$ ]] || (( choice < 1 )) || (( choice > ${#dish_name[@]} )); then
|
||||
echo "Invalid choice. Please enter a number from 1 to ${#dish_name[@]}."
|
||||
exit 1
|
||||
fi
|
||||
|
||||
selected_dish="${dish_name[$((choice - 1))]}"
|
||||
echo "You selected: $selected_dish"
|
||||
Executable
+206
@@ -0,0 +1,206 @@
|
||||
#!/bin/bash
|
||||
# Smoke-test a disk image produced by build-image.sh.
|
||||
#
|
||||
# Usage: ./smoke-test.sh [--timeout SECONDS] IMAGE
|
||||
#
|
||||
# 1. Static checks, read-only, without booting: if the image declares
|
||||
# SELinux (SELINUX= in /etc/selinux/config is not "disabled"), no boot
|
||||
# entry and not /etc/kernel/cmdline may carry selinux=0, and files must be
|
||||
# labeled. Catches builds on a host with SELinux disabled: anaconda then
|
||||
# inherits the host's selinux=0 and overrides the kickstart's
|
||||
# `selinux --enforcing` (see devices/runner.md in inventory-of-devices).
|
||||
# 2. Boot test: boots the image under KVM with -snapshot (the image is never
|
||||
# written) and no network, and waits for qemu-guest-agent to answer
|
||||
# guest-sync. The agent only starts once userspace is up, so this proves
|
||||
# firmware, bootloader, kernel, initramfs, root mount and systemd all work,
|
||||
# without depending on a serial console being configured in the image.
|
||||
#
|
||||
# Requires: root (loop devices, mount), qemu-system-x86_64 with /dev/kvm,
|
||||
# edk2-ovmf (UEFI images), e2fsprogs (debugfs), python3. The image must
|
||||
# include qemu-guest-agent (the guest-agents ingredient).
|
||||
|
||||
set -euo pipefail
|
||||
|
||||
TIMEOUT=300
|
||||
while [[ $# -gt 0 ]]; do
|
||||
case "$1" in
|
||||
--timeout)
|
||||
TIMEOUT="$2"
|
||||
shift 2
|
||||
;;
|
||||
-h|--help)
|
||||
echo "Usage: $0 [--timeout SECONDS] IMAGE"
|
||||
exit 0
|
||||
;;
|
||||
*)
|
||||
break
|
||||
;;
|
||||
esac
|
||||
done
|
||||
IMAGE="${1:?Usage: $0 [--timeout SECONDS] IMAGE}"
|
||||
[ -f "$IMAGE" ] || { echo "no such image: $IMAGE"; exit 1; }
|
||||
|
||||
OVMF_CODE=/usr/share/edk2/ovmf/OVMF_CODE.fd
|
||||
OVMF_VARS=/usr/share/edk2/ovmf/OVMF_VARS.fd
|
||||
|
||||
WORK="$(mktemp -d "${TMPDIR:-/tmp}/smoke-test.XXXXXX")"
|
||||
LOOP=""
|
||||
QEMU_PID=""
|
||||
cleanup() {
|
||||
[ -n "$QEMU_PID" ] && kill "$QEMU_PID" 2>/dev/null || true
|
||||
mountpoint -q "$WORK/esp" && umount "$WORK/esp" || true
|
||||
[ -n "$LOOP" ] && losetup -d "$LOOP" || true
|
||||
rm -rf "$WORK"
|
||||
}
|
||||
trap cleanup EXIT
|
||||
|
||||
fail() { echo "FAIL: $*"; exit 1; }
|
||||
|
||||
# --- 1. static checks ---------------------------------------------------------
|
||||
|
||||
LOOP="$(losetup --read-only --partscan --find --show "$IMAGE")"
|
||||
udevadm settle
|
||||
# Filesystem labels come from the storage ingredients (root, boot); the ESP is
|
||||
# matched by partition type instead, as its FAT label ends up upper-cased.
|
||||
part_by_label() { lsblk -rno PATH,LABEL "$LOOP" | awk -v l="$1" '$2 == l { print $1; exit }'; }
|
||||
ROOT_PART="$(part_by_label root)"
|
||||
BOOT_PART="$(part_by_label boot)"
|
||||
ESP_PART="$(lsblk -rno PATH,PARTTYPE "$LOOP" | awk '$2 == "c12a7328-f81f-11d2-ba4b-00a0c93ec93b" { print $1; exit }')"
|
||||
[ -n "$ROOT_PART" ] || fail "no partition labeled 'root' in $IMAGE"
|
||||
|
||||
rootcat() { debugfs -R "cat $1" "$ROOT_PART" 2>/dev/null; }
|
||||
selinux_mode="$(rootcat /etc/selinux/config | sed -n 's/^SELINUX=//p')"
|
||||
echo "SELINUX=${selinux_mode:-<unset>} in /etc/selinux/config"
|
||||
|
||||
# Collect every kernel command line the image boots with.
|
||||
cmdlines="$WORK/cmdlines"
|
||||
: > "$cmdlines"
|
||||
{ echo "== /etc/kernel/cmdline"; rootcat /etc/kernel/cmdline; } >> "$cmdlines"
|
||||
if [ -n "$ESP_PART" ]; then
|
||||
mkdir -p "$WORK/esp"
|
||||
mount -o ro "$ESP_PART" "$WORK/esp"
|
||||
for f in "$WORK"/esp/loader/entries/*.conf; do
|
||||
[ -e "$f" ] && { echo "== ESP ${f#"$WORK"/esp/}"; grep '^options' "$f"; } >> "$cmdlines"
|
||||
done
|
||||
fi
|
||||
if [ -n "$BOOT_PART" ]; then
|
||||
for f in $(debugfs -R "ls /loader/entries" "$BOOT_PART" 2>/dev/null | grep -o '[^ ]*\.conf'); do
|
||||
{ echo "== /boot/loader/entries/$f"; debugfs -R "cat /loader/entries/$f" "$BOOT_PART" 2>/dev/null | grep '^options'; } >> "$cmdlines"
|
||||
done
|
||||
fi
|
||||
cat "$cmdlines"
|
||||
|
||||
if [ -n "$selinux_mode" ] && [ "$selinux_mode" != disabled ]; then
|
||||
if grep -qw 'selinux=0' "$cmdlines"; then
|
||||
fail "image declares SELINUX=$selinux_mode but boots with selinux=0 (built on a host with SELinux disabled?)"
|
||||
fi
|
||||
for f in /etc/shadow /usr/bin/bash; do
|
||||
label="$(debugfs -R "ea_get $f security.selinux" "$ROOT_PART" 2>/dev/null | sed -n 's/.*= "\(.*\)\\000"$/\1/p')"
|
||||
echo "label $f: ${label:-<none>}"
|
||||
[ -n "$label" ] || fail "$f has no SELinux label (built on a host with SELinux disabled?)"
|
||||
done
|
||||
fi
|
||||
echo "static checks: OK"
|
||||
|
||||
mountpoint -q "$WORK/esp" && umount "$WORK/esp"
|
||||
losetup -d "$LOOP"
|
||||
LOOP=""
|
||||
|
||||
# --- 2. boot test -------------------------------------------------------------
|
||||
|
||||
firmware=()
|
||||
if [ -n "$ESP_PART" ]; then
|
||||
cp "$OVMF_VARS" "$WORK/vars.fd"
|
||||
firmware=(-drive "if=pflash,format=raw,readonly=on,file=$OVMF_CODE"
|
||||
-drive "if=pflash,format=raw,file=$WORK/vars.fd")
|
||||
fi
|
||||
|
||||
qemu-system-x86_64 \
|
||||
-name smoke-test -machine q35,accel=kvm -cpu host -smp 2 -m 2048 \
|
||||
"${firmware[@]}" \
|
||||
-drive "file=$IMAGE,format=raw,if=virtio,snapshot=on" \
|
||||
-nic none -display none -monitor none \
|
||||
-serial "file:$WORK/serial.log" \
|
||||
-chardev "socket,id=qga0,path=$WORK/qga.sock,server=on,wait=off" \
|
||||
-device virtio-serial \
|
||||
-device virtserialport,chardev=qga0,name=org.qemu.guest_agent.0 &
|
||||
QEMU_PID=$!
|
||||
|
||||
echo "booting (timeout ${TIMEOUT}s), waiting for qemu-guest-agent..."
|
||||
if python3 - "$WORK/qga.sock" "$TIMEOUT" "$QEMU_PID" <<'EOF'
|
||||
import json, os, socket, sys, time
|
||||
|
||||
path, timeout, qemu_pid = sys.argv[1], int(sys.argv[2]), int(sys.argv[3])
|
||||
deadline = time.monotonic() + timeout
|
||||
|
||||
def qemu_alive():
|
||||
try:
|
||||
os.kill(qemu_pid, 0)
|
||||
return True
|
||||
except ProcessLookupError:
|
||||
return False
|
||||
|
||||
while not os.path.exists(path):
|
||||
if not qemu_alive():
|
||||
sys.exit("qemu exited before creating the guest agent socket")
|
||||
if time.monotonic() > deadline:
|
||||
sys.exit("qemu did not create the guest agent socket within %ds" % timeout)
|
||||
time.sleep(0.5)
|
||||
sock = socket.socket(socket.AF_UNIX)
|
||||
sock.connect(path)
|
||||
buf = b""
|
||||
sync_id = 0
|
||||
while time.monotonic() < deadline:
|
||||
if not qemu_alive():
|
||||
sys.exit("qemu exited before the guest agent answered")
|
||||
# guest-sync with a fresh id flushes anything stale in the channel; the
|
||||
# agent echoes the id back once it runs.
|
||||
sync_id += 1
|
||||
sock.sendall(json.dumps({"execute": "guest-sync", "arguments": {"id": sync_id}}).encode() + b"\n")
|
||||
sock.settimeout(5)
|
||||
try:
|
||||
while b"\n" not in buf:
|
||||
chunk = sock.recv(4096)
|
||||
if not chunk:
|
||||
break
|
||||
buf += chunk
|
||||
except socket.timeout:
|
||||
continue
|
||||
line, _, buf = buf.partition(b"\n")
|
||||
try:
|
||||
reply = json.loads(line)
|
||||
except ValueError:
|
||||
continue
|
||||
# Replies can lag behind: syncs sent while the agent was still starting
|
||||
# are answered in order, so accept any id we have sent.
|
||||
ret = reply.get("return") if isinstance(reply, dict) else None
|
||||
if isinstance(ret, int) and 1 <= ret <= sync_id:
|
||||
# Drain whatever else is queued before asking for osinfo.
|
||||
sock.settimeout(1)
|
||||
try:
|
||||
while True:
|
||||
chunk = sock.recv(4096)
|
||||
if not chunk:
|
||||
break
|
||||
except socket.timeout:
|
||||
pass
|
||||
buf = b""
|
||||
sock.sendall(b'{"execute": "guest-get-osinfo"}\n')
|
||||
sock.settimeout(10)
|
||||
while b"\n" not in buf:
|
||||
buf += sock.recv(4096)
|
||||
info = json.loads(buf.partition(b"\n")[0]).get("return", {})
|
||||
print("guest agent answered after %ds: %s, kernel %s"
|
||||
% (timeout - (deadline - time.monotonic()), info.get("pretty-name"), info.get("kernel-release")))
|
||||
sys.exit(0)
|
||||
sys.exit("guest agent did not answer within %ds" % timeout)
|
||||
EOF
|
||||
then
|
||||
echo "boot test: OK"
|
||||
else
|
||||
echo "--- qemu-guest-agent on the serial console:"
|
||||
grep -a 'qemu-guest-agent\|Guest Agent' "$WORK/serial.log" 2>/dev/null || echo "(no mention)"
|
||||
echo "--- last 40 lines of the serial console:"
|
||||
tail -40 "$WORK/serial.log" 2>/dev/null || true
|
||||
fail "boot test"
|
||||
fi
|
||||
Reference in New Issue
Block a user