#!/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 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 # 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 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