Files
computer-playbook/scripts/install/filesystem.sh
T
kevinveenbirkenbachandClaude Opus 5 d0b0b464e7 fix(images): make a dnf retry fetch again instead of replaying its cache
Both image-build jobs of run 37494541850 failed, and only centos broke; arch,
debian, ubuntu and fedora built. The headline reads like an outage:

    Error: Failed to download metadata for repo 'baseos':
      Cannot download repomd.xml ... All mirrors were tried

The line above it says otherwise:

    Errors during downloading metadata for repository 'baseos':
      - Downloading successful, but checksum doesn't match.
        Calculated: 76995e25878232aba76bc22834280007be1baf40...

The download succeeds; the repomd.xml served does not match the checksum the
metalink advertises. The decisive detail is that this digest is byte-identical
on attempts 1, 2 and 3. A real re-fetch reaches a different mirror and yields a
different digest, so attempts 2 and 3 fetched nothing: dnf had written the bad
metadata to /var/cache/dnf and retry only slept and re-invoked the same command
against the same file. Three attempts, one download, and the same for all seven
build attempts.

retry_dnf is retry plus a metadata clean before each re-run, so the next
attempt is a fetch and can land on a mirror that is not mid-sync. All ten dnf
call sites use it, not just the one that failed: the zfs builddep and install
paths carry the identical weakness. apt and pacman keep retry, having no
equivalent cache trap.

The success path is unchanged, so the common case behaves identically. On the
failure path the script runs under set -e, which aborts if the clean itself
returns non-zero: measured, and it fails closed and loud rather than continuing
against a stale cache.

This fixes the retry, not the mirror. If the EL mirrors served stale metadata
for the whole window, three real fetches still fail.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-10-06 19:15:52 +02:00

320 lines
11 KiB
Bash
Executable File

#!/usr/bin/env bash
# Bake the userlands for every filesystem the docker data root can run on into
# the distro image: ext4, btrfs and zfs. The kernel side always comes from the
# host, but the tools have to sit in the image - dockerd resolves `zfs` on PATH
# and opens /dev/zfs before it accepts a data root, and mkfs.ext4 / mkfs.btrfs
# on the host are equally useless to a container that cannot call them. A pick
# the kernel could serve still fails when the image lacks the userland.
#
# The three differ sharply in what that costs:
#
# * ext4 and btrfs are ordinary packages everywhere except the EL family,
# where RHEL dropped btrfs and btrfs-progs exists only in EPEL. A plain
# `dnf install btrfs-progs` there fails the whole transaction and takes
# e2fsprogs down with it, so EPEL goes in first. The split is by distro
# rather than by a failed first attempt, because on Fedora there is no
# epel-release to fall back to: routing a transient failure there would
# turn it into a fatal one and hide the cause behind an EPEL message.
# * The OpenZFS `zfs` RPM carries an ungated `Requires: %{name}-kmod`, so a
# plain `dnf install zfs` resolves through zfs-dkms and drags in
# kernel-devel, kernel-debug-core and a DKMS build against a kernel the
# container never boots: measured at 389 extra packages and 1.44 GB. A
# dummy package that only *provides* zfs-kmod keeps the same install at 60
# packages with no kernel package at all. Its version must equal the exact
# %{version} dnf resolves for `zfs`; a mismatch silently falls back to DKMS.
# Because that version is pinned, the repository is removed again once the
# userland is in: leaving it enabled means the next `dnf upgrade` finds a
# newer zfs whose kmod dependency the dummy no longer satisfies, and the
# whole DKMS pull returns at deploy time rather than at build time.
# zfsonlinux publishes Fedora and EL binaries for x86_64 only, and on any
# other architecture the binary repository has no metadata at all: dnf5
# skips it, dnf4 aborts every transaction that loads it. There the zfs
# source RPM from the same release is rebuilt instead, with the binary
# repository disabled and zfs-release gone before any further dnf call.
# Its build dependencies include the protected systemd, which `dnf remove`
# refuses to touch, so they are marked as dependencies and autoremoved once
# the userland holds on to whatever it still needs.
# * Debian keeps zfsutils-linux in contrib, which the base image does not
# enable. Without the extra source apt reports no installation candidate.
# * Arch has no zfs userland in core/extra. archzfs publishes a prebuilt
# zfs-utils, but only on a rolling release tag whose asset filename carries
# the version, so any pinned URL 404s on the next upstream bump. Building
# the AUR PKGBUILD (which configures --with-config=user, userland only) is
# the reproducible route and costs about six minutes. That build runs with
# --skippgpcheck rather than --skipinteg: the release tarball's sha256 and
# b2 sums stay verified, and only the detached OpenZFS signature is skipped,
# because checking it means carrying the release key in the builder's
# keyring while the same PKGBUILD already supplies both the sums and the
# list of accepted keys, so the signature adds no trust the sums do not.
set -euo pipefail
RETRIES=3
AUR_BUILDER=pkgbuild
ZFS_RELEASE_FEDORA="3-1 3-0 2-8"
ZFS_RELEASE_EL="3-0 2-8 2-3"
log() { echo "[filesystem] $*"; }
# Param: $@ command to run, retried on failure with a widening backoff
retry() {
local attempt=1
until "$@"; do
if [ "${attempt}" -ge "${RETRIES}" ]; then
log "ERROR: '$*' failed after ${attempt} attempts"
return 1
fi
log "attempt ${attempt} of '$1' failed; retrying"
sleep $((attempt * 5))
attempt=$((attempt + 1))
done
}
# Param: $@ dnf command, retried with the metadata cache dropped between tries
retry_dnf() {
local attempt=1
until "$@"; do
if [ "${attempt}" -ge "${RETRIES}" ]; then
log "ERROR: '$*' failed after ${attempt} attempts"
return 1
fi
log "attempt ${attempt} of '$1' failed; dropping dnf metadata and retrying"
dnf clean metadata
sleep $((attempt * 5))
attempt=$((attempt + 1))
done
}
install_apt() {
export DEBIAN_FRONTEND=noninteractive
local apt_opts=(-o Acquire::Retries="${RETRIES}" -qq)
if [ "${ID:-}" = debian ]; then
log "enabling contrib for ${VERSION_CODENAME:?}, which is where zfsutils-linux lives"
echo "deb http://deb.debian.org/debian ${VERSION_CODENAME} contrib" \
>/etc/apt/sources.list.d/contrib.list
fi
retry apt-get "${apt_opts[@]}" update
retry apt-get "${apt_opts[@]}" install -y --no-install-recommends \
e2fsprogs btrfs-progs zfsutils-linux
rm -rf /var/lib/apt/lists/*
}
install_pacman() {
retry pacman -Syu --noconfirm --needed base-devel git e2fsprogs btrfs-progs
if ! id "${AUR_BUILDER}" >/dev/null 2>&1; then
useradd -m -s /bin/bash "${AUR_BUILDER}"
fi
install -d -m 0755 /etc/sudoers.d
printf '%s ALL=(ALL) NOPASSWD: /usr/bin/pacman\n' "${AUR_BUILDER}" \
>/etc/sudoers.d/99-"${AUR_BUILDER}"-pacman
chmod 0440 /etc/sudoers.d/99-"${AUR_BUILDER}"-pacman
local build_root=/tmp/zfs-utils-build
rm -rf "${build_root}"
install -d -o "${AUR_BUILDER}" -g "${AUR_BUILDER}" "${build_root}"
retry su "${AUR_BUILDER}" -c \
"git clone --depth 1 https://aur.archlinux.org/zfs-utils.git '${build_root}'" # nocheck: url - AUR serves this path to git clients only; a plain GET is a 404 by design
su "${AUR_BUILDER}" -c \
"cd '${build_root}' && makepkg --syncdeps --noconfirm --cleanbuild --skippgpcheck"
local pkg
pkg="$(find "${build_root}" -maxdepth 1 -type f -name 'zfs-utils-*.pkg.tar.*' \
! -name '*-debug-*' | head -n1)"
if [ -z "${pkg}" ]; then
log "ERROR: makepkg produced no zfs-utils package"
return 1
fi
pacman -U --noconfirm "${pkg}"
rm -rf "${build_root}" /var/cache/pacman/pkg
}
install_dnf_native() {
case "${ID:-}" in
fedora)
retry_dnf dnf install -y e2fsprogs btrfs-progs
return
;;
esac
log "adding EPEL, the only place the EL family carries btrfs-progs"
retry_dnf dnf install -y epel-release
retry_dnf dnf install -y e2fsprogs btrfs-progs
}
# Param: $1 path segment of the zfsonlinux repository, `fedora` or `epel`
# Param: $2 space-separated zfs-release versions to try, newest first
release_install() {
local base="$1" candidates="$2" dist version url
dist="$(rpm --eval '%{dist}')"
for version in ${candidates}; do
url="https://zfsonlinux.org/${base}/zfs-release-${version}${dist}.noarch.rpm"
if dnf install -y "${url}"; then
log "zfs-release: ${url}"
return 0
fi
log "no zfs-release ${version} for ${dist}"
done
log "ERROR: none of the zfs-release versions '${candidates}' exists for ${dist}"
return 1
}
# Param: $1 directory the zfs source RPM is downloaded into
# Output: path of the downloaded source RPM
download_zfs_source() {
local dir="$1" srpm
rm -rf "${dir}"
mkdir -p "${dir}"
if command -v dnf5 >/dev/null 2>&1; then
retry_dnf dnf install -y --disablerepo=zfs dnf5-plugins >&2
else
retry_dnf dnf install -y --disablerepo=zfs dnf-plugins-core >&2
fi
retry_dnf dnf download --source --disablerepo=zfs --enablerepo=zfs-source --destdir="${dir}" zfs >&2
srpm="$(find "${dir}" -maxdepth 1 -name 'zfs-*.src.rpm' | head -n1)"
if [ -z "${srpm}" ]; then
log "ERROR: the zfs-source repository offers no zfs source RPM" >&2
return 1
fi
printf '%s\n' "${srpm}"
}
# Param: $1 zfs source RPM to rebuild for the running architecture
# Param: $2 sorted package names installed before any zfs build tooling
install_zfs_from_source() {
local srpm="$1" before="$2" topdir=/tmp/zfs-rebuild
local -a added runtime crb=()
if [ "${ID:-}" != fedora ]; then
crb=(--enablerepo=crb)
fi
retry_dnf dnf builddep -y "${crb[@]}" "${srpm}"
mapfile -t added < <(rpm -qa --qf '%{NAME}\n' | sort | comm -13 "${before}" -)
rm -rf "${topdir}"
rpmbuild --define "_topdir ${topdir}" --rebuild "${srpm}"
mapfile -t runtime < <(find "${topdir}/RPMS" -name '*.rpm' \
! -name '*-debug*' ! -name '*-devel-*' ! -name 'zfs-test*' \
! -name 'zfs-dracut*' ! -name 'python3-pyzfs*' ! -name 'pam_zfs_key*' \
! -name 'zfs-dkms*' ! -name 'zfs-kmod*')
retry_dnf dnf install -y "${runtime[@]}"
log "dropping ${#added[@]} build dependencies of the zfs rebuild"
if command -v dnf5 >/dev/null 2>&1; then
dnf mark -y dependency "${added[@]}"
else
dnf mark remove "${added[@]}"
fi
dnf autoremove -y
rm -rf "${topdir}" "$(dirname "${srpm}")"
}
install_dnf() {
install_dnf_native
case "${ID:-}" in
fedora) retry release_install fedora "${ZFS_RELEASE_FEDORA}" ;;
*) retry release_install epel "${ZFS_RELEASE_EL}" ;;
esac
local zfs_version source_rpm="" before=/tmp/zfs-packages-before
rpm -qa --qf '%{NAME}\n' | sort >"${before}"
zfs_version="$(dnf --quiet --setopt=zfs.skip_if_unavailable=True repoquery --qf '%{version}\n' zfs | sort -V | tail -n1)"
if [ -z "${zfs_version}" ]; then
log "the zfs repository carries no $(uname -m) build; rebuilding its source RPM"
source_rpm="$(download_zfs_source /tmp/zfs-source)"
zfs_version="$(rpm -qp --qf '%{version}' "${source_rpm}")"
dnf remove -y --disablerepo=zfs zfs-release
fi
log "zfs userland version: ${zfs_version}"
if ! command -v rpmbuild >/dev/null 2>&1; then
retry_dnf dnf install -y rpm-build
fi
local topdir=/tmp/zfs-kmod-container
rm -rf "${topdir}"
mkdir -p "${topdir}/SPECS"
cat >"${topdir}/SPECS/zfs-kmod-container.spec" <<SPEC
Name: zfs-kmod-container
Version: ${zfs_version}
Release: 1
Summary: Satisfies the zfs kernel module dependency inside a container
License: CDDL
BuildArch: noarch
Provides: zfs-kmod = ${zfs_version}
%description
The container never loads a zfs module; the host supplies it and the container
reaches it through /dev/zfs. This package exists only so the zfs userland
resolves without pulling a DKMS toolchain and a kernel it will never boot.
%files
SPEC
rpmbuild --define "_topdir ${topdir}" -bb "${topdir}/SPECS/zfs-kmod-container.spec"
dnf install -y "${topdir}"/RPMS/noarch/zfs-kmod-container-*.rpm
if [ -n "${source_rpm}" ]; then
install_zfs_from_source "${source_rpm}" "${before}"
else
retry_dnf dnf install -y zfs
dnf remove -y zfs-release
fi
if rpm -qa 'kernel-devel*' 'kernel-debug*' 'zfs-dkms' | grep -q .; then
log "ERROR: the transaction pulled kernel or DKMS packages, so the dummy did not take"
rpm -qa 'kernel-devel*' 'kernel-debug*' 'zfs-dkms' >&2
return 1
fi
rm -rf "${topdir}" "${before}"
dnf clean all
}
verify() {
local tool
for tool in mkfs.ext4 mkfs.btrfs zfs zpool; do
if ! command -v "${tool}" >/dev/null 2>&1; then
log "ERROR: ${tool} is missing after the install"
return 1
fi
done
if ldd "$(command -v zpool)" | grep -q 'not found'; then
log "ERROR: zpool has unresolved shared libraries"
ldd "$(command -v zpool)" | grep 'not found' >&2
return 1
fi
if command -v dnf >/dev/null 2>&1 && dnf repolist --enabled 2>/dev/null | grep -q '^zfs'; then
log "ERROR: a zfs repository is still enabled, so an upgrade would re-resolve the kmod"
return 1
fi
log "installed mkfs.ext4, mkfs.btrfs and $(command -v zpool); all libraries resolved"
}
main() {
# shellcheck source=/dev/null
. /etc/os-release
if command -v apt-get >/dev/null 2>&1; then
install_apt
elif command -v pacman >/dev/null 2>&1; then
install_pacman
elif command -v dnf >/dev/null 2>&1; then
install_dnf
else
log "ERROR: no supported package manager (apt/pacman/dnf)"
exit 1
fi
verify
}
main "$@"