Root cause of a fresh-install "cannot reach the Podman API socket" report (a friend's Unraid box, cache pool present and mounted): unlike Docker-for-Unraid's docker.img path, this plugin never auto-created STORAGE_PATH itself — only podman.img inside it. A perfectly normal, already-mounted cache pool still failed preflight/storage-create with "does not exist", just because its own .../system/podman subdirectory had never been created. Fixed by walking up to the nearest existing ancestor and checking whether it's on a different device than / (real mount vs. nothing mounted at all) — see podman-common.sh's new podman_path_has_real_mount_ancestor(), used by both podman-preflight.sh and podman-storage.sh. Settings gets a "Podman Service" card (status chip + Start/Restart, backed by new ajax/settings.php service_status/start/restart actions that just shell out to rc.podman) so a fresh install that failed to start can be diagnosed and retried without SSH/terminal access at all — exactly what was missing when this was first needed live. Also adds "Format a Disk for Podman Storage" (new ajax/disks.php) for a single-disk system with no cache pool at all. Only ever lists disks with literally no existing partition/filesystem/RAID-or-ZFS-membership signature and that aren't Unraid's boot flash — found live, twice, during development: the boot USB (FAT, labeled "UNRAID") passed the initial mounted-only check because this host's /boot is backed by a ZFS dataset rather than a direct partition mount, and active RAID-member cache disks passed a data-vs-blank *warning* rather than a hard exclusion. Both are now excluded outright, not just flagged — see disks.php's device_or_children_labeled_unraid() and the hasData exclusion in list_candidate_disks(). A disk formatted this way is remounted by UUID on every boot via a new plugin/sbin/podman-mount-managed-disk.sh, called from plugin/event/disks_mounted before rc.podman start. Unrelated fix bundled in: scripts/lib/slackbuild-common.sh now sets SOURCE_DATE_EPOCH (derived from the repo's last commit) before calling makepkg, so two separate builds of the same commit produce byte-identical .txz files — makepkg already supports this (`--clamp-mtime` when $SOURCE_DATE_EPOCH is set, confirmed by reading a real host's /sbin/makepkg) but nothing was setting the variable, so release.yml's "rebuild in CI and verify it matches the committed checksums" step was guaranteed to fail on the first package it checked alphabetically (observed live: aardvark-dns). Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
186 lines
7.4 KiB
Bash
Executable File
186 lines
7.4 KiB
Bash
Executable File
#!/bin/bash
|
|
# =============================================================================
|
|
# plugin/sbin/podman-storage.sh
|
|
#
|
|
# "Container Storage erstellen" — creates, mounts, and unmounts the
|
|
# podman.img loopback filesystem that backs Podman's overlay storage graph
|
|
# (images, containers, named volumes). See docs/ARCHITECTURE.md section 4.3
|
|
# for why this needs to be a real mounted filesystem (XFS with ftype=1, on a
|
|
# cache pool or dedicated disk) rather than a directory under /mnt/user —
|
|
# the FUSE (shfs) layer behind /mnt/user does not reliably support the
|
|
# overlay storage driver's filesystem requirements (d_type, etc).
|
|
#
|
|
# Usage:
|
|
# podman-storage.sh create # create podman.img if it doesn't exist yet
|
|
# podman-storage.sh mount # mount it at $PODMAN_GRAPHROOT (idempotent)
|
|
# podman-storage.sh unmount # cleanly unmount
|
|
# podman-storage.sh status # report existence/mount/usage
|
|
# =============================================================================
|
|
|
|
set -eu
|
|
|
|
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
|
|
# shellcheck source=./podman-common.sh
|
|
. "$SCRIPT_DIR/podman-common.sh"
|
|
|
|
podman_load_cfg
|
|
|
|
# -----------------------------------------------------------------------------
|
|
# cmd_create
|
|
#
|
|
# Creates podman.img at the configured size if it doesn't already exist,
|
|
# and formats it XFS with ftype=1 (required for the overlay storage driver
|
|
# to see correct directory entry types — without it, podman's overlay
|
|
# backend silently falls back to a slower/less-capable mode or fails
|
|
# outright, depending on version). Does nothing (and exits 0) if the image
|
|
# already exists — this script is meant to be safe to call on every boot.
|
|
# -----------------------------------------------------------------------------
|
|
cmd_create() {
|
|
if ! podman_storage_path_is_safe "$STORAGE_PATH"; then
|
|
return 1
|
|
fi
|
|
|
|
if [ -f "$PODMAN_STORAGE_IMAGE" ]; then
|
|
podman_log "storage: $PODMAN_STORAGE_IMAGE already exists, not recreating"
|
|
return 0
|
|
fi
|
|
|
|
if [ ! -d "$STORAGE_PATH" ]; then
|
|
if ! podman_path_has_real_mount_ancestor "$STORAGE_PATH"; then
|
|
podman_log_error "storage: $STORAGE_PATH does not exist or is not mounted."
|
|
podman_log_error "storage: check that the configured cache pool/disk is present before starting podman."
|
|
return 1
|
|
fi
|
|
podman_log "storage: $STORAGE_PATH doesn't exist yet under an already-mounted filesystem — creating it"
|
|
mkdir -p "$STORAGE_PATH"
|
|
fi
|
|
|
|
# Free space check: refuse to create an image bigger than what's actually
|
|
# available, with a small safety margin, rather than letting truncate
|
|
# silently create a sparse file that will fail unpredictably later once
|
|
# it's actually written to (see docs/ARCHITECTURE.md section 16.2).
|
|
local available_kb required_kb
|
|
available_kb=$(df --output=avail -k "$STORAGE_PATH" | tail -n1 | tr -d '[:space:]')
|
|
required_kb=$((STORAGE_IMAGE_SIZE_GB * 1024 * 1024))
|
|
if [ "$available_kb" -lt "$required_kb" ]; then
|
|
podman_log_error "storage: not enough free space on $STORAGE_PATH (need ${STORAGE_IMAGE_SIZE_GB}G, have $((available_kb / 1024 / 1024))G)"
|
|
podman_notify "Podman storage creation failed" \
|
|
"Not enough free space on $STORAGE_PATH for a ${STORAGE_IMAGE_SIZE_GB}G podman.img." \
|
|
"alert"
|
|
return 1
|
|
fi
|
|
|
|
podman_log "storage: creating ${STORAGE_IMAGE_SIZE_GB}G image at $PODMAN_STORAGE_IMAGE"
|
|
mkdir -p "$STORAGE_PATH"
|
|
|
|
# Sparse file: only actually consumes disk space as data is written,
|
|
# matching Docker-for-Unraid's docker.img behavior that users already
|
|
# understand.
|
|
truncate -s "${STORAGE_IMAGE_SIZE_GB}G" "$PODMAN_STORAGE_IMAGE"
|
|
|
|
podman_require_command mkfs.xfs
|
|
# -n ftype=1 is the whole reason this has to be a purpose-made image
|
|
# rather than a plain directory — see file header comment.
|
|
mkfs.xfs -n ftype=1 -q "$PODMAN_STORAGE_IMAGE"
|
|
|
|
podman_log "storage: created and formatted $PODMAN_STORAGE_IMAGE"
|
|
}
|
|
|
|
# -----------------------------------------------------------------------------
|
|
# cmd_mount
|
|
#
|
|
# Idempotent: does nothing if $PODMAN_GRAPHROOT is already mounted from
|
|
# podman.img. Runs a read-only integrity check (xfs_repair -n) before
|
|
# mounting and refuses to proceed if it reports corruption — per
|
|
# docs/ARCHITECTURE.md section 16.1, this project does not auto-repair
|
|
# storage without the user's explicit action, to avoid silent data loss.
|
|
# -----------------------------------------------------------------------------
|
|
cmd_mount() {
|
|
if mountpoint -q "$PODMAN_GRAPHROOT" 2> /dev/null; then
|
|
podman_log "storage: $PODMAN_GRAPHROOT already mounted"
|
|
return 0
|
|
fi
|
|
|
|
if [ ! -f "$PODMAN_STORAGE_IMAGE" ]; then
|
|
podman_log_error "storage: $PODMAN_STORAGE_IMAGE does not exist — run 'podman-storage.sh create' first"
|
|
return 1
|
|
fi
|
|
|
|
podman_require_command xfs_repair
|
|
podman_log "storage: checking filesystem integrity of $PODMAN_STORAGE_IMAGE"
|
|
if ! xfs_repair -n "$PODMAN_STORAGE_IMAGE" > /tmp/podman-xfs-repair.log 2>&1; then
|
|
podman_log_error "storage: filesystem check failed for $PODMAN_STORAGE_IMAGE — refusing to mount."
|
|
podman_log_error "storage: see /tmp/podman-xfs-repair.log. Run 'xfs_repair $PODMAN_STORAGE_IMAGE' manually to attempt repair, or restore from backup."
|
|
podman_notify "Podman storage corruption detected" \
|
|
"$PODMAN_STORAGE_IMAGE failed an integrity check and was not mounted. Manual recovery required — see plugin.log." \
|
|
"alert"
|
|
return 1
|
|
fi
|
|
|
|
mkdir -p "$PODMAN_GRAPHROOT"
|
|
podman_log "storage: mounting $PODMAN_STORAGE_IMAGE at $PODMAN_GRAPHROOT"
|
|
mount -o loop "$PODMAN_STORAGE_IMAGE" "$PODMAN_GRAPHROOT"
|
|
|
|
# Runtime state (runroot) is tmpfs-backed and fine to live on RAM-root —
|
|
# only the persistent graphroot needs the loopback filesystem.
|
|
mkdir -p /var/run/containers/storage
|
|
|
|
podman_log "storage: mounted"
|
|
}
|
|
|
|
# -----------------------------------------------------------------------------
|
|
# cmd_unmount
|
|
#
|
|
# Unmounts cleanly. Retries briefly if the mount is momentarily busy (a
|
|
# container process exiting can hold a reference for a few hundred ms),
|
|
# rather than immediately failing rc.podman's stop sequence.
|
|
# -----------------------------------------------------------------------------
|
|
cmd_unmount() {
|
|
if ! mountpoint -q "$PODMAN_GRAPHROOT" 2> /dev/null; then
|
|
podman_log "storage: $PODMAN_GRAPHROOT not mounted, nothing to do"
|
|
return 0
|
|
fi
|
|
|
|
podman_log "storage: unmounting $PODMAN_GRAPHROOT"
|
|
local attempt
|
|
for attempt in 1 2 3 4 5; do
|
|
if umount "$PODMAN_GRAPHROOT" 2> /dev/null; then
|
|
podman_log "storage: unmounted"
|
|
return 0
|
|
fi
|
|
podman_log "storage: unmount busy, retrying (attempt $attempt/5)"
|
|
sleep 1
|
|
done
|
|
|
|
podman_log_error "storage: failed to unmount $PODMAN_GRAPHROOT after 5 attempts (still busy?)"
|
|
return 1
|
|
}
|
|
|
|
cmd_status() {
|
|
echo "storage image: $PODMAN_STORAGE_IMAGE"
|
|
if [ -f "$PODMAN_STORAGE_IMAGE" ]; then
|
|
echo " exists: yes ($(du -h "$PODMAN_STORAGE_IMAGE" | cut -f1) allocated / ${STORAGE_IMAGE_SIZE_GB}G nominal)"
|
|
else
|
|
echo " exists: no"
|
|
fi
|
|
|
|
echo "graphroot: $PODMAN_GRAPHROOT"
|
|
if mountpoint -q "$PODMAN_GRAPHROOT" 2> /dev/null; then
|
|
echo " mounted: yes"
|
|
df -h "$PODMAN_GRAPHROOT" | tail -n1 | awk '{print " usage: " $3 " used / " $2 " total (" $5 " full)"}'
|
|
else
|
|
echo " mounted: no"
|
|
fi
|
|
}
|
|
|
|
case "${1:-}" in
|
|
create) cmd_create ;;
|
|
mount) cmd_mount ;;
|
|
unmount) cmd_unmount ;;
|
|
status) cmd_status ;;
|
|
*)
|
|
echo "usage: $0 {create|mount|unmount|status}" >&2
|
|
exit 1
|
|
;;
|
|
esac
|