Author SHA1 Message Date
lukas 6d521930f4 Merge pull request 'Fix/image hardening' (#8) from fix/image-hardening into main
build-image / validate (push) Successful in 9s
ci / cook (push) Successful in 9s
build-image / build-image (phyllomeos-headless, 8192) (push) Failing after 8m54s
build-image / build-image (phyllomeos, 16384) (push) Failing after 17m56s
Reviewed-on: #8
2026-09-23 17:10:08 +00:00
Lukas GreveandClaude Opus 5.5 c611bbf2c2 core/network: generic default hostname instead of phyllome-alpha
ci / cook (push) Successful in 10s
ci / cook (pull_request) Successful in 9s
phyllome-alpha is the name of one specific deployed machine; baking it
into every image gives each new install a name that collides with it on
the LAN. Default to a generic 'phyllomeos', renamed per machine after
install.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-09-23 13:43:00 +02:00
Lukas GreveandClaude Opus 5.5 6931039809 core/locale: set the console keymap too (--vckeymap=ch-fr)
Only --xlayouts was set, so images got no /etc/vconsole.conf and the text
console used a US keymap. On the headless edition that is the login
prompt: a password typed in ch-fr during Initial Setup did not match
what the US-keymap tty login produced. Found testing the
phyllomeos-headless image from CI run 237 under wonder-vm.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-09-23 13:43:00 +02:00
Lukas GreveandClaude Opus 5.5 41eb18cc70 ci: build images only on release tags, not on every push to main
A full build of both editions takes ~25 min on the fedora:host runner
(run 237: 10:46-11:10), too costly to repeat on every push to main.
build-image.yaml now triggers only on v*.*.* tags; the tag path already
publishes the image and kickstart as a release, so the main-only
upload-artifact step is dropped. Recipes are still linted and validated
on every push by ci.yml.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-09-23 13:42:39 +02:00
Lukas GreveandClaude Sonnet 5 b8cd2966f4 build-image: serialize host-wide with flock, clean up stale mounts/devices
build-image / validate (push) Successful in 10s
ci / cook (push) Successful in 11s
build-image / build-image (phyllomeos-headless, 8192) (push) Successful in 23m38s
build-image / build-image (phyllomeos, 16384) (push) Successful in 32m1s
Gitea runs each matrix entry as its own task, so build-image.yaml's
`max-parallel: 1` never serialized the two editions -- both builds started
at once on the fedora:host runner and corrupted each other's global
anaconda/livemedia-creator state (/mnt/sysroot, /mnt/sysimage, loop and
device-mapper devices), surfacing as bootctl failing with "Couldn't find
EFI system partition" even though the dish's EFI partition was fine.

build-image.sh now takes a host-wide flock around livemedia-creator, and
under the lock first clears any stale mount/dm/loop device a previously
failed build left behind. Verified on the runner: both editions launched
concurrently now run serially under the lock and build successfully.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-09-23 12:46:08 +02:00
Lukas GreveandClaude Sonnet 5 67316d9852 cook: two default dishes (phyllomeos, phyllomeos-headless) + guest/experimental tiers; CI builds raw images
build-image / validate (push) Successful in 8s
ci / cook (push) Successful in 10s
build-image / build-image (phyllomeos, 16384) (push) Failing after 1m13s
build-image / build-image (phyllomeos-headless, 8192) (push) Failing after 11m10s
Manifest groups get a tier; only `default` is generated (make all TIER=...,
--tier). Default: phyllomeos (GNOME + virt-manager) and phyllomeos-headless,
Fedora 44, systemd-boot, CPU-agnostic hypervisor ingredient (hypervisor_type:
any). guest tier: guest-server, guest-desktop. experimental: biosboot/grub,
encrypted, rawhide. Generator clears stale recipes/dishes before writing.

build-image.sh: --dish and --tier; deploy.sh: --tier. build-iso.yaml replaced
by build-image.yaml: raw images for both editions on the fedora:host runner
(TMPDIR=/var/tmp, max-parallel 1), attached with the flattened kickstarts to
tagged releases. Live edition no longer built.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-09-20 11:35:28 +02:00
Lukas Greve 35d8879bcc ci: build live ISO with livemedia-creator; add live-desktop recipe
build-iso / validate (push) Successful in 12s
ci / cook (push) Successful in 13s
build-iso / build-iso (push) Successful in 25m11s
2026-09-19 17:53:06 +02:00
Lukas Greve bb23fed54a ci: run livemedia-creator directly in runner image (drop mock)
build-iso / validate (push) Successful in 13s
ci / cook (push) Successful in 14s
build-iso / build-iso (push) Failing after 1m54s
mock --init fails on repo GPG signature verification in the runner
container. The container is already fedora:44 with anaconda/lorax
installed, so the mock chroot adds nothing but a failing GPG wall.
Run dnf-installed lorax-lmc-novirt/pykickstart/livecd-tools and
livemedia-creator directly in the job container; artifacts now live
under build/ in the workspace.
2026-09-19 17:20:52 +02:00
Lukas Greve d3189bee94 ci: consolidate ISO workflows into single dual-mode build-iso
build-iso / validate (push) Successful in 13s
ci / cook (push) Successful in 13s
build-iso / build-iso (push) Failing after 32s
Merge build-iso.yaml and build-iso_more-recent.yaml into one workflow.
- triggers: push to main (build + artifact) and v*.*.* tags (build + release)
- parametrize Fedora version and dish via env
- fix copyin source (cook/dishes/... .cfg) and artifact/release path
  (host path into the mock chroot, /var/lib/mock//root/var/lmc)
- install cook Python deps in the build job before make all
- drop build-iso_more-recent.yaml (superseded)
2026-09-19 14:29:05 +02:00
Lukas Greve 435ce41c02 ci: re-trigger after pytest requirements fix
build-iso / validate (push) Successful in 11s
ci / cook (push) Successful in 13s
build-iso / build-iso (push) Failing after 27s
2026-09-19 14:10:52 +02:00
Lukas Greve 7197ed9be9 cook: add pytest to requirements (CI test step)
build-iso / validate (push) Successful in 10s
ci / cook (push) Successful in 13s
build-iso / build-iso (push) Failing after 28s
2026-09-19 14:10:02 +02:00
Lukas Greve da5bcf6b04 ci: install Python deps before lint
ci / cook (push) Failing after 13s
build-iso / validate (push) Successful in 13s
build-iso / build-iso (push) Failing after 38s
2026-09-19 14:08:07 +02:00
Lukas Greve f246dec765 runner disk extended to 100G; re-trigger CI
ci / cook (push) Failing after 1m41s
build-iso / validate (push) Successful in 1m47s
build-iso / build-iso (push) Failing after 29s
2026-09-19 14:02:04 +02:00
Lukas Greve bddd05f3a8 ci: bump (actions were disabled at last push)
build-iso / validate (push) Failing after 56s
ci / cook (push) Failing after 56s
build-iso / build-iso (push) Skipped
2026-09-19 12:10:52 +02:00
Lukas Greve c491aeb635 ci: add lint/test/validate workflow; fix stale fedora-cloud-42 label -> fedora 2026-09-19 12:08:18 +02:00
Lukas GreveandClaude Sonnet 5 dceb82f8f6 Add build-image.sh: raw/qcow2 disk images without libvirt/virt-install
Runs `livemedia-creator --no-virt --make-disk` directly on the host to
produce a disk image any VM manager can consume (e.g. via virtpull's
`virtpull local ... --wonder-vm NAME`), instead of deploy.sh's live
virt-install/libvirt session.

An earlier version wrapped this in `mock`, mirroring
.gitea/workflows/build-iso.yaml's ISO build. That doesn't work for
--make-disk: mock's chroot has no live systemd-udevd, so udev never
populates properties for the loop device livemedia-creator creates,
and blivet's device scan crashes. --make-iso never hits this since it
never touches block devices. Reproduced identically on Fedora 44 and
43 mock chroots, confirmed fixed by running directly on the host
instead (root required, for /dev/loop-control access).

Also fixes two real kickstart incompatibilities with the --no-virt
disk-image path (applied to a scratch copy, not the shared
ingredients): the `text` display-mode directive (needed for
virt-install's netinstall console) conflicts with livemedia-creator's
own display handling, and `part / --grow` with no explicit --size
crashes livemedia-creator's upfront disk-size calculation (works fine
under virt-install, which pre-creates the disk at a known size
instead) — now configurable via --root-size.

New --extra-ks FILE flag layers one-off local content (e.g. a bespoke
user/rootpw override) onto the scratch copy without touching tracked
ingredients, following the README's existing "bespoke dish not part
of the matrix" pattern.

Factored deploy/deploy-distro.sh's dish-picker into deploy/select-dish.sh,
shared by both scripts.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-09-18 20:04:49 +02:00
Lukas Greve a8228df780 feat: add TPM2 auto-unlock support to encrypted storage ingredient
Add cryptsetup, tpm2-tools and tpm2-tss to every encrypted dish so the
initrd can be rebuilt with TPM2 support. A guarded %pre stashes a
preseeded LUKS passphrase from the kickstart, and a guarded %post
enrolls PCR 7-bound auto-unlock via systemd-cryptenroll, switches
crypttab to tpm2-device=auto and regenerates the initrd. Without a
preseeded passphrase or a TPM device the enrollment is skipped and the
system keeps the regular passphrase prompt.
2026-09-15 11:04:19 +02:00
Lukas Greve 0a553429ba fix: make amdcpu IOMMU %post layout-aware and idempotent
Same fix as cd4302c for intelcpu: the bare sed against /etc/default/grub
missed systemd-boot and already-generated GRUB+BLS boot entries. Update
every kernel-argument surface idempotently, guarded by file existence:
/etc/kernel/cmdline, /etc/default/grub, and the options line of every
/boot/loader/entries/*.conf. Guard the nested=1 append as well.
2026-09-12 20:11:02 +02:00
Lukas Greve cd4302cb09 fix: make intelcpu IOMMU %post layout-aware and idempotent
Anaconda always creates /etc/kernel/cmdline, even on GRUB+BLS installs, so
the previous if/else misdetected systemd-boot on GRUB systems: the IOMMU
arguments landed in /etc/kernel/cmdline after GRUB+BLS boot entries had
already been generated from it, and never reached the running kernel
(observed on thinkpad and nuc6i7kyb).

Instead of branching on one file, update every kernel-argument surface
idempotently, guarded by existence: /etc/kernel/cmdline (kernel-install
source for systemd-boot and GRUB+BLS future kernels), /etc/default/grub
(grub.cfg flows), and the options line of every already-generated
/boot/loader/entries/*.conf (same shape under both systemd-boot and
GRUB+BLS). Guard the nested=1 append as well.
2026-09-12 20:09:16 +02:00
Lukas Greve fff901ef1a feat: add GNOME desktop-hypervisor dish for BIOS laptops
- storage/biosboot.ks: GPT disk label with a 2 MiB BIOS boot partition,
  no ESP (pure legacy layout); registered as the 'biosboot' storage choice
- recipe_templates.yaml: make the desktop hypervisor a superset of the
  base hypervisor so GNOME hypervisor dishes get libvirtd + virt-manager
- recipes_manifest.yaml: enable the desktop-hypervisor group targeting
  Fedora 44 (gnome, biosboot, grub, security enabled, gnome initial-setup,
  hardware-support, intelcpu)
- initial-setup/gnome/config.ks: apply the vendor.conf fix (truncate and
  append skip=privacy / providers=local-first!) as an enabled %post
- tests: update BASE_FRAGMENTS fixture to core-explicit.ks (follow-up to
  ccf4f9d)
2026-09-12 10:31:39 +02:00
24 changed files with 875 additions and 236 deletions
+102
View File
@@ -0,0 +1,102 @@
name: build-image
# Only a release tag builds images: a full build takes ~25 min on the runner,
# too costly to repeat on every push to main. Recipes are still linted and
# validated on every push by ci.yml.
on:
push:
tags: ["v*.*.*"]
# 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.
jobs:
validate:
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; 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; }
[ "$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 }}
xz -T0 "build/$dish.img"
- 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.*
-74
View File
@@ -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
+27
View File
@@ -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
View File
@@ -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
+53 -5
View File
@@ -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,56 @@ 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/`. The `build-image` CI workflow runs it for both default editions on every push to `main`, and on `v*.*.*` tags it attaches the compressed raw images (`.img.xz`) and the flattened kickstart files to the release. 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 +142,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 +154,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
View File
@@ -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
View File
@@ -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
View File
@@ -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 -1
View File
@@ -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 -1
View File
@@ -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
+21 -2
View File
@@ -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
+20 -5
View File
@@ -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
+13 -13
View File
@@ -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
+8
View File
@@ -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
+67
View File
@@ -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
+13 -1
View File
@@ -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
View File
@@ -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
View File
@@ -1,2 +1,3 @@
PyYAML>=6.0
pykickstart>=1.99
pytest>=7.0
+75 -2
View File
@@ -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()
+19 -1
View File
@@ -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
View File
@@ -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")
+27
View File
@@ -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"