mirror of
https://github.com/kevinveenbirkenbach/computer-playbook.git
synced 2026-11-10 12:47:04 +00:00
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>
320 lines
11 KiB
Bash
Executable File
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 "$@"
|