Files
computer-playbook/scripts/install/filesystem.sh
T
kevinveenbirkenbach 4c4b00a0e9 Merge branch 'main' into feature/store-i18n
# Conflicts:
#	roles/update/README.md
#	roles/web-app-bluesky/templates/extra_locations.conf.j2
#	roles/web-app-opentalk/templates/nginx/extra.conf.j2
#	scripts/install/filesystem.sh
2026-10-22 10:57:53 +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 --refresh install -y e2fsprogs btrfs-progs
return
;;
esac
log "adding EPEL, the only place the EL family carries btrfs-progs"
retry_dnf dnf --refresh install -y epel-release
retry_dnf dnf --refresh 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 --refresh 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 --refresh 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 "$@"