feat(installer): persist install logs to USB by default

- new helper overlay/usr/share/veilor-os/scripts/persist-install-logs.sh
  detects boot USB (BOOT=/findfs, /run/install/repo, /sys/block removable),
  copies /tmp/anaconda.log + program/storage/packaging/dnf/syslog/X +
  journalctl -b + dmesg + lsblk/blkid/mount + /proc/cmdline into
  /veilor-install-logs/<UTC-ts>/ on the stick; mirrors backup into
  /mnt/sysroot/var/log/veilor-install-logs/ so logs survive even on RO
  USB or detect failure
- toggle: kernel cmdline veilor.install_logs=on|off (default ON until
  v1.0 final); never fails install on log persistence error
- kickstart/install-ostreecontainer-installer.ks: add %post --nochroot
  block calling helper with toggle-aware inline fallback if helper
  missing
- .github/workflows/build-installer-iso.yml: switch bib config from
  [customizations.user] to [customizations.installer.kickstart] so our
  new %post --nochroot actually lands in the produced ISO; admin user
  now created by ks user directive (locked + chage 0); ostreecontainer
  line stripped (bib auto-appends it); kernel-cmdline-default
  limitation documented (osbuild/bootc-image-builder#899)

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
This commit is contained in:
s8n-ru 2026-05-08 00:51:16 +01:00
parent 865c9507af
commit c272050890
3 changed files with 328 additions and 8 deletions

View file

@ -69,14 +69,42 @@ jobs:
# it locally to compose the installer ISO.
podman pull ghcr.io/veilor-org/veilor-os:43 || \
podman pull git.s8n.ru/veilor-org/veilor-os:43
# Generate a minimal config.toml for bootc-image-builder that
# tells Anaconda to ask for LUKS pw + admin pw.
cat > /tmp/bib-config.toml <<'TOML'
[[customizations.user]]
name = "admin"
password = ""
groups = ["wheel"]
TOML
# Generate config.toml for bootc-image-builder.
#
# We use [customizations.installer.kickstart] (NOT
# [customizations.user]) because we need our own %post --nochroot
# block to persist install logs back to the boot USB. Per upstream
# docs, [customizations.user] and [customizations.installer.kickstart]
# are mutually exclusive (see osbuild/bootc-image-builder#528) — so
# the admin user is now created by a kickstart `user` directive
# below, locked + chage 0 so first SDDM login forces a real pw.
#
# bootc-image-builder auto-appends `ostreecontainer ...` to the
# contents we provide; we MUST NOT include that line ourselves
# (we strip it from the source kickstart with sed).
#
# NOTE on kernel cmdline default: ideally we'd set
# `veilor.install_logs=on` as an installer-kernel default, but
# `[customizations.kernel].append` targets the INSTALLED system's
# kargs.d, not the live ISO's grub.cfg (osbuild/bootc-image-builder
# #899 still open). The persist-install-logs.sh helper defaults to
# ON when the toggle is absent, so the desired default is achieved
# without needing installer-cmdline injection. Operators flip to
# off at boot via GRUB edit: append `veilor.install_logs=off`.
KS_SRC="kickstart/install-ostreecontainer-installer.ks"
KS_FILTERED="$(grep -v '^ostreecontainer' "$KS_SRC")"
# Insert a locked admin user directive under the rootpw block —
# Anaconda's interactive Users spoke is unavailable in unattended
# bib mode, so we pre-create admin and let chage -d 0 force a pw
# change at first login.
USER_LINE='user --name=admin --groups=wheel --plaintext --password="" --lock'
KS_FILTERED="$(printf '%s\n' "$KS_FILTERED" | awk -v ul="$USER_LINE" '/^rootpw --lock$/ { print; print ul; next } { print }')"
{
echo '[customizations.installer.kickstart]'
echo 'contents = """'
printf '%s\n' "$KS_FILTERED"
echo '"""'
} > /tmp/bib-config.toml
podman run --rm \
--privileged \
--pull=newer \

View file

@ -78,3 +78,46 @@ set -uo pipefail
echo veilor-install > /etc/hostname
chage -d 0 admin 2>/dev/null || true
%end
# ── %post --nochroot — persist install logs to USB (toggle: veilor.install_logs=on|off) ──
#
# Runs OUTSIDE the target chroot so /tmp/anaconda.log etc. on the live
# ramdisk are accessible alongside /mnt/sysroot. Calls the helper that
# ships in the veilor-os OCI image overlay; if the helper is missing
# (corrupt overlay, stripped image, etc.) we fall back to a minimal
# inline copy. NEVER fail the install over log persistence.
#
# Default: ON until v1.0 final. Disable per-boot:
# edit GRUB / press 'e', append: veilor.install_logs=off
%post --nochroot --erroronfail=no
set -uo pipefail
VEILOR_HELPER="/mnt/sysroot/usr/share/veilor-os/scripts/persist-install-logs.sh"
[ -x "$VEILOR_HELPER" ] || VEILOR_HELPER="/mnt/sysimage/usr/share/veilor-os/scripts/persist-install-logs.sh"
if [ -x "$VEILOR_HELPER" ]; then
"$VEILOR_HELPER" || true
else
# Inline fallback — toggle-aware, backup-only (no USB write attempt).
TS="$(date -u +%Y-%m-%dT%H-%M-%SZ)"
SR=/mnt/sysroot; [ -d "$SR" ] || SR=/mnt/sysimage
DST="${SR}/var/log/veilor-install-logs/${TS}"
TOGGLE=on
for tok in $(cat /proc/cmdline 2>/dev/null); do
case "$tok" in veilor.install_logs=off|veilor.install_logs=0|veilor.install_logs=false|veilor.install_logs=no) TOGGLE=off ;; esac
done
if [ "$TOGGLE" = "on" ]; then
mkdir -p "$DST" 2>/dev/null || true
for f in /tmp/anaconda.log /tmp/program.log /tmp/storage.log \
/tmp/packaging.log /tmp/syslog /tmp/dnf.log \
/tmp/ks.cfg /run/veilor-installer.log; do
[ -e "$f" ] && cp -a "$f" "$DST/" 2>/dev/null || true
done
dmesg > "$DST/dmesg.txt" 2>/dev/null || true
journalctl --no-pager -b > "$DST/journalctl-b.txt" 2>/dev/null || true
echo "[veilor] inline fallback used — helper missing at $VEILOR_HELPER" \
> "$DST/manifest.txt"
fi
fi
exit 0
%end

View file

@ -0,0 +1,249 @@
#!/usr/bin/env bash
# persist-install-logs.sh — copy Anaconda install logs back to the boot USB
#
# Runs from %post --nochroot near the end of the Anaconda install. At that
# point /tmp/*.log on the live ramdisk has the full evidence trail
# (anaconda.log, program.log, storage.log, packaging.log, dnf.log,
# syslog, etc.) — and is about to be lost forever when the user reboots
# into the freshly installed system.
#
# We:
# 1. Honour the kernel cmdline toggle veilor.install_logs=on|off
# (default: on, until v1.0 final flips the default to off).
# 2. Detect the boot USB device (BOOT=, BOOT_IMAGE=, /run/install/repo,
# then /sys/block/*/removable=1 fallback).
# 3. Try to remount it rw and copy logs into
# /veilor-install-logs/<UTC-ISO8601>/ on the USB.
# 4. ALSO copy a backup into /mnt/sysroot/var/log/veilor-install-logs/
# so logs survive in the installed system even if the USB is RO,
# missing, or write-failed.
# 5. NEVER fail the install over this. Every error is logged + ignored.
#
# Disable at boot: edit GRUB / press 'e', append: veilor.install_logs=off
# Disable in kickstart: comment out the call in install-ostreecontainer-installer.ks
#
# Author: veilor-os agent A2 (2026-05-08)
# License: AGPLv3 — same as veilor-os
set -uo pipefail
# ── trace ── everything to stderr; Anaconda captures stderr to program.log
log() { printf '[persist-install-logs] %s\n' "$*" >&2; }
trap 'log "WARN: line $LINENO failed (rc=$?) — continuing"' ERR
TS="$(date -u +%Y-%m-%dT%H-%M-%SZ)"
SYSROOT="${VEILOR_SYSROOT:-/mnt/sysroot}"
[ -d "$SYSROOT" ] || SYSROOT="/mnt/sysimage" # legacy Anaconda path
BACKUP_DIR="${SYSROOT}/var/log/veilor-install-logs/${TS}"
mkdir -p "$BACKUP_DIR" 2>/dev/null || true
# ── 1. toggle ──────────────────────────────────────────────────────────────
parse_toggle() {
# default ON until v1.0 final
local cmdline val
cmdline="$(cat /proc/cmdline 2>/dev/null || true)"
for tok in $cmdline; do
case "$tok" in
veilor.install_logs=*) val="${tok#veilor.install_logs=}" ;;
esac
done
val="${val:-on}"
case "$val" in
on|true|1|yes) echo on ;;
off|false|0|no) echo off ;;
*) log "unknown veilor.install_logs=$val — defaulting to on"; echo on ;;
esac
}
TOGGLE="$(parse_toggle)"
if [ "$TOGGLE" = "off" ]; then
log "veilor.install_logs=off — log persistence skipped"
exit 0
fi
log "veilor.install_logs=on — persisting install logs (ts=${TS})"
# ── 2. collect log payload into staging dir ───────────────────────────────
STAGE="$(mktemp -d -t veilor-install-logs.XXXXXX 2>/dev/null || echo /tmp/veilor-install-logs-stage)"
mkdir -p "$STAGE"
collect() {
local src="$1" dst="$2"
if [ -e "$src" ]; then
cp -a "$src" "$STAGE/$dst" 2>/dev/null || \
log "could not copy $src"
fi
}
# Anaconda /tmp logs (live env)
for f in anaconda.log program.log storage.log packaging.log syslog \
dnf.log dnf.librepo.log dnf.rpm.log dnf.hawkey.log \
X.log ifcfg.log lvm.log yum.log; do
collect "/tmp/$f" "$f"
done
# Kickstart-related
collect /tmp/ks.cfg ks.cfg
collect /tmp/ks-script.log ks-script.log
collect /tmp/kickstart_pre.log kickstart_pre.log
collect /tmp/kickstart_post.log kickstart_post.log
# veilor TUI installer log (live ISO writes this to /run)
collect /run/veilor-installer.log veilor-installer.log
# Runtime evidence
{
echo "── /proc/cmdline ──"
cat /proc/cmdline 2>/dev/null
echo
echo "── /proc/version ──"
cat /proc/version 2>/dev/null
echo
echo "── /etc/os-release ──"
cat /etc/os-release 2>/dev/null
echo
echo "── timestamp (UTC) ──"
date -u
} > "$STAGE/system-info.txt" 2>/dev/null || true
dmesg --ctime > "$STAGE/dmesg.txt" 2>/dev/null || \
dmesg > "$STAGE/dmesg.txt" 2>/dev/null || true
journalctl --no-pager -b > "$STAGE/journalctl-b.txt" 2>/dev/null || true
lsblk -fJ > "$STAGE/lsblk.json" 2>/dev/null || true
blkid > "$STAGE/blkid.txt" 2>/dev/null || true
mount > "$STAGE/mount.txt" 2>/dev/null || true
# Manifest
{
echo "veilor-os install log bundle"
echo "timestamp_utc=${TS}"
echo "host_uname=$(uname -a 2>/dev/null)"
echo "files:"
(cd "$STAGE" && ls -la 2>/dev/null)
} > "$STAGE/manifest.txt"
# Backup copy regardless of USB success
cp -a "$STAGE/." "$BACKUP_DIR/" 2>/dev/null && \
log "backup written to ${BACKUP_DIR}" || \
log "WARN: could not write backup to ${BACKUP_DIR}"
# ── 3. detect boot USB ─────────────────────────────────────────────────────
detect_usb_dev() {
local cmdline tok val dev
cmdline="$(cat /proc/cmdline 2>/dev/null || true)"
# 3a) BOOT=LABEL=... or BOOT=UUID=... explicit
for tok in $cmdline; do
case "$tok" in
BOOT=*)
val="${tok#BOOT=}"
dev="$(findfs "$val" 2>/dev/null || true)"
[ -n "$dev" ] && [ -b "$dev" ] && { echo "$dev"; return 0; }
;;
esac
done
# 3b) Anaconda mounts the install medium at /run/install/repo
if mountpoint -q /run/install/repo 2>/dev/null; then
dev="$(findmnt -no SOURCE /run/install/repo 2>/dev/null || true)"
[ -n "$dev" ] && [ -b "$dev" ] && { echo "$dev"; return 0; }
fi
if mountpoint -q /run/install/sources/mount-0000-iso 2>/dev/null; then
dev="$(findmnt -no SOURCE /run/install/sources/mount-0000-iso 2>/dev/null || true)"
[ -n "$dev" ] && [ -b "$dev" ] && { echo "$dev"; return 0; }
fi
# 3c) BOOT_IMAGE=(hdX,Y)/path — extract base device from kernel arg via
# /run/initramfs/livedev (dracut-live writes this)
if [ -r /run/initramfs/livedev ]; then
dev="$(cat /run/initramfs/livedev 2>/dev/null)"
[ -n "$dev" ] && [ -b "$dev" ] && { echo "$dev"; return 0; }
fi
# 3d) /sys/block walk for first removable device with mounted partition
local d part
for d in /sys/block/*/removable; do
[ "$(cat "$d" 2>/dev/null)" = "1" ] || continue
local base
base="$(basename "$(dirname "$d")")"
for part in /sys/block/"$base"/"$base"*; do
[ -d "$part" ] || continue
local pname="/dev/$(basename "$part")"
[ -b "$pname" ] && { echo "$pname"; return 0; }
done
done
return 1
}
USB_DEV="$(detect_usb_dev || true)"
if [ -z "${USB_DEV:-}" ]; then
log "could not detect boot USB device — backup-only mode (see ${BACKUP_DIR})"
exit 0
fi
log "detected boot USB partition: ${USB_DEV}"
# Walk to parent disk if we got a partition — we want the data partition not
# the ESP. For an Anaconda-spun installer USB the ISO is hybrid: the ISO9660
# partition holds the squashfs (RO), and there's usually an ESP. Strategy:
# try mounting the partition we got first; if it's RO we accept that and
# attempt remount; if remount fails we give up gracefully.
# ── 4. mount USB and write logs ────────────────────────────────────────────
MOUNT_POINT="/run/veilor-install-logs-mount"
mkdir -p "$MOUNT_POINT"
mount_rw() {
local dev="$1"
if mount "$dev" "$MOUNT_POINT" 2>/dev/null; then
# check if rw
if touch "$MOUNT_POINT/.veilor-write-test" 2>/dev/null; then
rm -f "$MOUNT_POINT/.veilor-write-test"
return 0
fi
# try remount rw
if mount -o remount,rw "$MOUNT_POINT" 2>/dev/null && \
touch "$MOUNT_POINT/.veilor-write-test" 2>/dev/null; then
rm -f "$MOUNT_POINT/.veilor-write-test"
return 0
fi
log "USB mounted RO and remount-rw failed: ${dev}"
umount "$MOUNT_POINT" 2>/dev/null || true
return 1
fi
return 1
}
if mount_rw "$USB_DEV"; then
DEST="${MOUNT_POINT}/veilor-install-logs/${TS}"
if mkdir -p "$DEST" 2>/dev/null && cp -a "$STAGE/." "$DEST/" 2>/dev/null; then
sync
log "logs persisted to USB: ${USB_DEV}:/veilor-install-logs/${TS}"
else
log "WARN: USB mounted rw but write failed — keeping backup at ${BACKUP_DIR}"
fi
umount "$MOUNT_POINT" 2>/dev/null || true
else
# Try the parent disk's other partitions (some installer USBs have a
# writable data partition separate from the ISO9660 squashfs partition).
parent="$(echo "$USB_DEV" | sed -E 's/[0-9]+$//; s/p$//')"
if [ -b "$parent" ]; then
for cand in "$parent"*[0-9]; do
[ -b "$cand" ] || continue
[ "$cand" = "$USB_DEV" ] && continue
if mount_rw "$cand"; then
DEST="${MOUNT_POINT}/veilor-install-logs/${TS}"
if mkdir -p "$DEST" 2>/dev/null && cp -a "$STAGE/." "$DEST/" 2>/dev/null; then
sync
log "logs persisted to USB partition: ${cand}:/veilor-install-logs/${TS}"
fi
umount "$MOUNT_POINT" 2>/dev/null || true
break
fi
done
fi
log "USB write path unavailable — relying on backup at ${BACKUP_DIR}"
fi
rmdir "$MOUNT_POINT" 2>/dev/null || true
rm -rf "$STAGE" 2>/dev/null || true
exit 0