From c272050890033fa3f25db76767511794971c9db6 Mon Sep 17 00:00:00 2001 From: s8n-ru <279801990+s8n-ru@users.noreply.github.com> Date: Fri, 8 May 2026 00:51:16 +0100 Subject: [PATCH] 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// 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 --- .github/workflows/build-installer-iso.yml | 44 +++- .../install-ostreecontainer-installer.ks | 43 +++ .../veilor-os/scripts/persist-install-logs.sh | 249 ++++++++++++++++++ 3 files changed, 328 insertions(+), 8 deletions(-) create mode 100755 overlay/usr/share/veilor-os/scripts/persist-install-logs.sh diff --git a/.github/workflows/build-installer-iso.yml b/.github/workflows/build-installer-iso.yml index c8f8c50..1bf1e1d 100644 --- a/.github/workflows/build-installer-iso.yml +++ b/.github/workflows/build-installer-iso.yml @@ -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 \ diff --git a/kickstart/install-ostreecontainer-installer.ks b/kickstart/install-ostreecontainer-installer.ks index 4dccd93..f964284 100644 --- a/kickstart/install-ostreecontainer-installer.ks +++ b/kickstart/install-ostreecontainer-installer.ks @@ -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 diff --git a/overlay/usr/share/veilor-os/scripts/persist-install-logs.sh b/overlay/usr/share/veilor-os/scripts/persist-install-logs.sh new file mode 100755 index 0000000..24d5572 --- /dev/null +++ b/overlay/usr/share/veilor-os/scripts/persist-install-logs.sh @@ -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// 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