podman-verify-packages.sh/podman-update-packages.sh reported every package as "not installed" right after a genuinely successful install. Root cause: /var/log/packages is itself a symlink on Unraid (-> ../lib/pkgtools/packages), and GNU find's default -P mode doesn't descend into a symlinked starting path at all without -L — confirmed live by running the exact same find both with and without -L against a host where the packages had just installed successfully. Fixing that alone surfaced a second, previously-masked bug: "podman"'s own glob also matches podman-compose's file (a literal prefix collision), and find's unsorted output let podman-compose's record silently win podman's own check. Both fixed once via a new shared podman_find_installed_package_record() helper in podman-common.sh, rather than separately in each caller. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
260 lines
11 KiB
Bash
Executable File
260 lines
11 KiB
Bash
Executable File
#!/bin/bash
|
|
# =============================================================================
|
|
# plugin/sbin/podman-common.sh
|
|
#
|
|
# Shared constants and helper functions sourced by every other script under
|
|
# plugin/sbin/ and by plugin/rc.d/rc.podman. Centralizing these here means:
|
|
# - every script agrees on the same paths (no risk of one script writing
|
|
# config to a path another script reads from a slightly different one),
|
|
# - logging/notification behavior is consistent everywhere,
|
|
# - each individual script stays focused on its one job instead of
|
|
# re-implementing "how do I log this" or "where is podman.cfg".
|
|
#
|
|
# This file is NOT meant to be executed directly — it only defines
|
|
# functions/variables for other scripts to source:
|
|
# . "$(dirname "${BASH_SOURCE[0]}")/podman-common.sh"
|
|
#
|
|
# See docs/ARCHITECTURE.md sections 4 (Verzeichnislayout), 7 (Persistenz),
|
|
# 15 (Logging), 16 (Fehlerbehandlung) for the design this implements.
|
|
# =============================================================================
|
|
|
|
# We deliberately do NOT `set -e` in this file: it is sourced by scripts
|
|
# that set their own error-handling mode, and a library changing its
|
|
# caller's shell options would be a surprising action at a distance.
|
|
|
|
# shellcheck disable=SC2034
|
|
# This whole file is a "library" of constants and functions for OTHER
|
|
# scripts to `source` — see the header above. ShellCheck analyzes each file
|
|
# in isolation and has no way to see that e.g. $PODMAN_AUTOSTART_FILE is
|
|
# used in podman-autostart.sh, not here, so it flags every constant below
|
|
# as "appears unused". That is a false positive for this file's actual
|
|
# role; disabling SC2034 file-wide (this directive, placed before any code,
|
|
# applies to the whole file per ShellCheck's own rules) is more honest than
|
|
# scattering 15+ identical per-line suppressions.
|
|
|
|
# -----------------------------------------------------------------------------
|
|
# Path constants.
|
|
#
|
|
# PODMAN_BOOT_DIR is the persistent, source-of-truth configuration directory
|
|
# on the Unraid flash device (survives reboots — see ARCHITECTURE.md 4.1).
|
|
# Everything under /etc, /var, /usr is RAM-root and rebuilt from here (or
|
|
# from the array/cache-backed storage dir) on every boot.
|
|
# -----------------------------------------------------------------------------
|
|
PODMAN_BOOT_DIR="/boot/config/plugins/podman"
|
|
PODMAN_CFG_FILE="$PODMAN_BOOT_DIR/podman.cfg"
|
|
PODMAN_AUTOSTART_FILE="$PODMAN_BOOT_DIR/autostart"
|
|
PODMAN_AUTOSTART_DELAY_FILE="$PODMAN_BOOT_DIR/autostart-delay"
|
|
PODMAN_NETWORKS_BOOT_DIR="$PODMAN_BOOT_DIR/networks"
|
|
PODMAN_BACKUP_DIR="$PODMAN_BOOT_DIR/backup"
|
|
PODMAN_PLUGIN_LOG="$PODMAN_BOOT_DIR/plugin.log"
|
|
|
|
# Runtime (RAM-root) locations rebuilt/synced on every rc.podman start.
|
|
PODMAN_ETC_DIR="/etc/containers"
|
|
PODMAN_RUN_DIR="/var/run/podman"
|
|
PODMAN_SOCKET="$PODMAN_RUN_DIR/podman.sock"
|
|
PODMAN_STATUS_FILE="$PODMAN_RUN_DIR/rc.podman.status"
|
|
PODMAN_SERVICE_PID_FILE="$PODMAN_RUN_DIR/podman-service.pid"
|
|
|
|
# -----------------------------------------------------------------------------
|
|
# podman_load_cfg
|
|
#
|
|
# Sources /boot/config/plugins/podman/podman.cfg (the user-editable settings
|
|
# file, see config/podman.cfg.example) and applies safe defaults for any
|
|
# setting that file doesn't define — so every other script can simply
|
|
# reference $STORAGE_PATH, $PODMAN_ENABLED, etc. after calling this, without
|
|
# each script having its own copy of the defaults.
|
|
# -----------------------------------------------------------------------------
|
|
podman_load_cfg() {
|
|
# Defaults, applied BEFORE sourcing podman.cfg so the file only needs to
|
|
# override what the user actually wants to change.
|
|
STORAGE_PATH="/mnt/cache/system/podman"
|
|
STORAGE_IMAGE_SIZE_GB="20"
|
|
PODMAN_ENABLED="yes"
|
|
STOP_TIMEOUT="10"
|
|
CONFIG_SCHEMA_VERSION="1"
|
|
|
|
if [ -f "$PODMAN_CFG_FILE" ]; then
|
|
# shellcheck source=/dev/null
|
|
. "$PODMAN_CFG_FILE"
|
|
fi
|
|
|
|
# Values derived from STORAGE_PATH, computed after sourcing so a custom
|
|
# STORAGE_PATH is honored.
|
|
PODMAN_STORAGE_IMAGE="$STORAGE_PATH/podman.img"
|
|
PODMAN_LOG_DIR="$STORAGE_PATH/logs"
|
|
PODMAN_GRAPHROOT="/var/lib/containers/storage"
|
|
}
|
|
|
|
# -----------------------------------------------------------------------------
|
|
# podman_log <message>
|
|
#
|
|
# Writes a timestamped line to both stdout (so it shows up in `rc.podman`
|
|
# invocations and CI/manual runs) and to the persistent plugin log on flash
|
|
# (so it survives a reboot — see ARCHITECTURE.md section 15, Logging). Kept
|
|
# deliberately terse (no log levels/rotation logic here) — this is for
|
|
# install/lifecycle events, not container output, which lives under
|
|
# $PODMAN_LOG_DIR instead.
|
|
# -----------------------------------------------------------------------------
|
|
podman_log() {
|
|
local msg="$1"
|
|
local line
|
|
line="$(date -u +'%Y-%m-%dT%H:%M:%SZ') [podman] $msg"
|
|
echo "$line"
|
|
# Best-effort: the boot directory should always exist post-install, but
|
|
# never let logging itself fail a caller that has `set -e`.
|
|
mkdir -p "$PODMAN_BOOT_DIR" 2> /dev/null || true
|
|
echo "$line" >> "$PODMAN_PLUGIN_LOG" 2> /dev/null || true
|
|
}
|
|
|
|
# -----------------------------------------------------------------------------
|
|
# podman_log_error <message>
|
|
#
|
|
# Like podman_log, but also mirrors the message to the system log via
|
|
# `logger`, so it is visible in Unraid's Tools -> System Log GUI without the
|
|
# user having to know where the plugin's own log lives. Reserved for
|
|
# conditions the user should actually notice (see ARCHITECTURE.md 16.2).
|
|
# -----------------------------------------------------------------------------
|
|
podman_log_error() {
|
|
local msg="$1"
|
|
podman_log "ERROR: $msg"
|
|
if command -v logger > /dev/null 2>&1; then
|
|
logger -t podman-plugin "$msg"
|
|
fi
|
|
}
|
|
|
|
# -----------------------------------------------------------------------------
|
|
# podman_notify <subject> <description> [importance]
|
|
#
|
|
# Surfaces a message via Unraid's own GUI notification system
|
|
# (/usr/local/emhttp/webGui/scripts/notify) instead of inventing a parallel
|
|
# notification mechanism — see ARCHITECTURE.md section 16.2. Silently
|
|
# no-ops if that script isn't present (e.g. when running outside a real
|
|
# Unraid system, such as in CI/lint contexts).
|
|
#
|
|
# importance: "normal" (default), "warning", or "alert".
|
|
# -----------------------------------------------------------------------------
|
|
podman_notify() {
|
|
local subject="$1"
|
|
local description="$2"
|
|
local importance="${3:-normal}"
|
|
local notify_bin="/usr/local/emhttp/webGui/scripts/notify"
|
|
|
|
podman_log "notify [$importance] $subject: $description"
|
|
|
|
if [ -x "$notify_bin" ]; then
|
|
"$notify_bin" -e "unraid-podman" -s "$subject" -d "$description" -i "$importance" \
|
|
> /dev/null 2>&1 || true
|
|
fi
|
|
}
|
|
|
|
# -----------------------------------------------------------------------------
|
|
# podman_storage_path_is_safe
|
|
#
|
|
# Refuses storage paths under /mnt/user (FUSE/shfs) — see
|
|
# ARCHITECTURE.md section 4.3 for why the overlay storage driver must live
|
|
# on a real mounted filesystem (cache pool or a specific disk), not shfs.
|
|
# Returns 0 (safe) or 1 (unsafe) and prints a reason on failure.
|
|
# -----------------------------------------------------------------------------
|
|
podman_storage_path_is_safe() {
|
|
local path="$1"
|
|
case "$path" in
|
|
/mnt/user/*|/mnt/user)
|
|
echo "STORAGE_PATH ($path) is under /mnt/user (FUSE/shfs)." >&2
|
|
echo "The overlay storage driver needs a real mounted filesystem —" >&2
|
|
echo "use a cache pool or a specific disk path instead, e.g. /mnt/cache/system/podman." >&2
|
|
return 1
|
|
;;
|
|
esac
|
|
return 0
|
|
}
|
|
|
|
# -----------------------------------------------------------------------------
|
|
# podman_path_has_real_mount_ancestor <path>
|
|
#
|
|
# True if <path> itself, or its nearest EXISTING ancestor directory, lives
|
|
# on a different filesystem than / (root) — i.e. something is genuinely
|
|
# mounted along this path (a cache pool, a dedicated disk, ...), even if
|
|
# the exact leaf directory doesn't exist yet. False only when nothing real
|
|
# is mounted anywhere along the path (root/RAM all the way up), which is
|
|
# the one case that's actually unsafe to silently `mkdir -p` into.
|
|
#
|
|
# This exists because this project never auto-created $STORAGE_PATH
|
|
# itself (only podman.img inside it) — found live: a perfectly normal,
|
|
# already-mounted cache pool still failed preflight/storage-create with
|
|
# "does not exist", because the pool's own .../system/podman subdirectory
|
|
# had simply never been created. "Does the exact leaf directory exist" was
|
|
# always the wrong question; "is a real filesystem mounted somewhere along
|
|
# this path" is the one that actually matters.
|
|
# -----------------------------------------------------------------------------
|
|
podman_path_has_real_mount_ancestor() {
|
|
local path="$1"
|
|
local parent="$path"
|
|
while [ ! -d "$parent" ] && [ "$parent" != "/" ]; do
|
|
parent="$(dirname "$parent")"
|
|
done
|
|
[ "$parent" != "/" ] && [ "$(stat -c %d "$parent")" != "$(stat -c %d /)" ]
|
|
}
|
|
|
|
# -----------------------------------------------------------------------------
|
|
# podman_find_installed_package_record <name> <all-package-names...>
|
|
#
|
|
# Prints the /var/log/packages/<name>-... record for <name> (empty/failure
|
|
# if not installed). Two real bugs, both found live on an actual host,
|
|
# fixed here once instead of separately in every script that needs this:
|
|
#
|
|
# -L: /var/log/packages is itself a symlink on Unraid (->
|
|
# ../lib/pkgtools/packages) — without it, GNU find's default -P mode
|
|
# doesn't descend into it AT ALL (it returns just the symlink itself and
|
|
# nothing below it), so every package was reported "not installed"
|
|
# regardless of what had actually just been installed.
|
|
#
|
|
# Prefix collision: a plain `-name "$name-*"` glob for "podman" also
|
|
# matches podman-compose's file ("podman" is a literal prefix of
|
|
# "podman-compose"), and find's output order isn't sorted, so whichever
|
|
# one happened to come back first silently won — on a real host, that
|
|
# was podman-compose's record, reported as if it were podman's own.
|
|
# Explicitly skip any match that actually belongs to a different, more
|
|
# specific name also in the given package list.
|
|
# -----------------------------------------------------------------------------
|
|
podman_find_installed_package_record() {
|
|
local name="$1"
|
|
shift
|
|
local all_names=("$@")
|
|
local candidate other belongs_to_other base
|
|
while IFS= read -r candidate; do
|
|
[ -n "$candidate" ] || continue
|
|
base=$(basename "$candidate")
|
|
belongs_to_other=0
|
|
for other in "${all_names[@]}"; do
|
|
# Only a LONGER (more specific) other name can steal a match —
|
|
# "podman" is itself a prefix of "podman-compose", so without the
|
|
# length check, searching for "podman-compose" would wrongly
|
|
# exclude its own, genuinely correct record too (found live: this
|
|
# exact over-correction on the very first fix attempt).
|
|
if [ "$other" != "$name" ] && [ "${#other}" -gt "${#name}" ] && [ "${base#"$other"-}" != "$base" ]; then
|
|
belongs_to_other=1
|
|
break
|
|
fi
|
|
done
|
|
if [ "$belongs_to_other" -eq 0 ]; then
|
|
echo "$candidate"
|
|
return 0
|
|
fi
|
|
done < <(find -L /var/log/packages -maxdepth 1 -name "${name}-*" -print 2> /dev/null)
|
|
return 1
|
|
}
|
|
|
|
# -----------------------------------------------------------------------------
|
|
# podman_require_command <binary>
|
|
#
|
|
# Fails loudly (rather than letting a script continue and fail confusingly
|
|
# three steps later) if a required binary isn't on PATH.
|
|
# -----------------------------------------------------------------------------
|
|
podman_require_command() {
|
|
local bin="$1"
|
|
if ! command -v "$bin" > /dev/null 2>&1; then
|
|
podman_log_error "required command not found: $bin"
|
|
return 1
|
|
fi
|
|
}
|