cook: two default dishes (phyllomeos, phyllomeos-headless) + guest/experimental tiers; CI builds raw images
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>
This commit is contained in:
co-authored by
Claude Sonnet 5
parent
35d8879bcc
commit
67316d9852
+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__':
|
||||
|
||||
@@ -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
|
||||
@@ -68,6 +68,7 @@ features:
|
||||
- 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
|
||||
|
||||
+89
-36
@@ -2,52 +2,87 @@
|
||||
# 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
|
||||
hardware-support: true
|
||||
guest-agents: false
|
||||
hypervisor: base
|
||||
hypervisor_type: intelcpu
|
||||
# Desktop-hypervisor variants
|
||||
- name: desktop-hypervisor
|
||||
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
|
||||
@@ -58,14 +93,32 @@ recipes:
|
||||
hardware-support: true
|
||||
guest-agents: false
|
||||
hypervisor: desktop
|
||||
hypervisor_type: intelcpu
|
||||
hypervisor_type: any
|
||||
|
||||
# Live desktop ISO variants (built with livemedia-creator --make-iso)
|
||||
- name: live-desktop
|
||||
# Encrypted root
|
||||
- name: exp-encrypted-headless
|
||||
tier: experimental
|
||||
variants:
|
||||
- repository: "44"
|
||||
desktop: gnome
|
||||
storage: livefs
|
||||
bootloader: grub
|
||||
storage: encrypted
|
||||
bootloader: systemd-boot
|
||||
security: enabled
|
||||
initial-setup: server
|
||||
hardware-support: true
|
||||
guest-agents: true
|
||||
live: true
|
||||
hypervisor: base
|
||||
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
|
||||
|
||||
@@ -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()
|
||||
|
||||
Reference in New Issue
Block a user