# Test-only helper for app/test/disksVmIntegration.test.js (never shipped as an app operation).
# Creates and removes dedicated loop-backed disk images under /var/tmp/chj-disks-test and prepares LVM/LUKS layers
# on them. Every operation first proves that the device belongs to an image created by this helper; otherwise it
# stops. Started by the test through PrivilegedExecutor as "sh -c SCRIPT chj OP ARGS...".
set -u
LC_ALL=C; export LC_ALL
PATH=/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin; export PATH
DIR=/var/tmp/chj-disks-test
printf '\036CHJ_READY\n'
op=${1:-}; [ $# -gt 0 ] && shift
die() { printf '\036CHJ_ERR=%s\n' "$1"; printf '%s\n' "${2:-}" >&2; exit 3; }
valid_id() { case $1 in ''|*[!a-z0-9]*) return 1 ;; esac; [ ${#1} -le 24 ]; }
loop_of() { case $1 in /dev/loop[0-9]*) printf '%s\n' "$1" | sed 's#^/dev/\(loop[0-9]*\).*#\1#' ;; *) return 1 ;; esac; }
# The device (loop disk or one of its partitions) must be backed by $DIR/chjtest-<id>.img.
test_device() {
  d=$(loop_of "$1") || die NOT_TEST_DEVICE "$1 is not a loop device"
  b=$(cat "/sys/block/$d/loop/backing_file" 2>/dev/null)
  case $b in "$DIR"/chjtest-[a-z0-9]*.img) ;; *) die NOT_TEST_DEVICE "$1 is not backed by a CH-J test image" ;; esac
  [ -b "$1" ] || die NOT_TEST_DEVICE "$1 is not a block device"
}
test_mount() { case $1 in /mnt/chj-disks-test-[a-z0-9]*) ;; *) die NOT_TEST_MOUNT "$1 is not a test mount point" ;; esac; case $1 in *..*|*[!A-Za-z0-9/_-]*) die NOT_TEST_MOUNT bad ;; esac; }
case $op in
  create) # ID MIB
    valid_id "$1" || die ARGS id; case $2 in ''|*[!0-9]*) die ARGS size ;; esac
    mkdir -p "$DIR" && chmod 700 "$DIR"
    img=$DIR/chjtest-$1.img; [ -e "$img" ] && die EXISTS "$img exists"
    truncate -s "${2}M" "$img" || die FAILED truncate
    # util-linux has --show; BusyBox losetup (Alpine) needs the free device first.
    dev=$(losetup -f --show -P "$img" 2>/dev/null) || { dev=$(losetup -f) && losetup -P "$dev" "$img"; } || { rm -f "$img"; die FAILED losetup; }
    printf 'dev=%s\nimg=%s\n' "$dev" "$img" ;;
  grow) # DEV MIB
    test_device "$1"; case $2 in ''|*[!0-9]*) die ARGS size ;; esac
    truncate -s "${2}M" "$(cat "/sys/block/$(loop_of "$1")/loop/backing_file")" && losetup -c "$1" || die FAILED grow
    printf 'size=%s\n' "$(blockdev --getsize64 "$1")" ;;
  write) # MOUNTPOINT: random data + checksums
    test_mount "$1"
    dd if=/dev/urandom of="$1/data.bin" bs=1024 count=16384 2>/dev/null && cp /etc/os-release "$1/os-release" && sync || die FAILED write
    (cd "$1" && sha256sum data.bin os-release) ;;
  sums) test_mount "$1"; (cd "$1" && sha256sum data.bin os-release) ;;
  tamper) # DEV: change the partition table behind a reviewed plan (adds a 1 MiB partition at the end)
    test_device "$1"
    printf ',1M\n' | sfdisk --append --no-reread "$1" >/dev/null 2>&1 || die FAILED tamper
    partx -u "$1" 2>/dev/null; echo ok ;;
  lvm) # PARTITION VG: PV + VG + 200 MiB LV with ext4
    test_device "$1"; case $2 in chjtest[a-z0-9]*) ;; *) die ARGS vg ;; esac
    pvcreate -ff -y "$1" >/dev/null && vgcreate "$2" "$1" >/dev/null && lvcreate -y -L 200M -n data "$2" >/dev/null && mkfs.ext4 -q "/dev/$2/data" || die FAILED lvm
    echo "lv=/dev/$2/data" ;;
  luks) # PARTITION NAME keyring|dm-crypt: LUKS2 with a throw-away random key file (never printed, deleted after open)
    test_device "$1"; case $2 in chjtest[a-z0-9]*) ;; *) die ARGS name ;; esac
    key=/run/$2.key; umask 077; head -c 64 /dev/urandom > "$key"
    cryptsetup luksFormat --batch-mode --type luks2 --pbkdf pbkdf2 --pbkdf-force-iterations 1000 --key-file "$key" "$1" >/dev/null 2>&1 || { rm -f "$key"; die FAILED luksFormat; }
    if [ "$3" = dm-crypt ]; then cryptsetup open --disable-keyring --key-file "$key" "$1" "$2"; else cryptsetup open --key-file "$key" "$1" "$2"; fi; rc=$?
    rm -f "$key"; [ $rc = 0 ] || die FAILED open
    mkfs.ext4 -q "/dev/mapper/$2" || die FAILED mkfs
    printf 'map=/dev/mapper/%s\nkeylocation=%s\n' "$2" "$(cryptsetup status "$2" | awk -F': *' '/key location/ { print $2 }')" ;;
  hold-lock) # ID: simulate a live disk job holding the server-wide lock (a sleeping process with a runner command line)
    valid_id "$1" || die ARGS id; [ -e /run/chj-disks.lock ] && die BUSY "lock already held"
    jobs=/var/lib/chj-server-manager/disks/jobs; job=chjtest-$1-holder; mkdir -p "$jobs/$job"
    # Two commands keep bash from exec-ing sleep, so the process keeps the runner-like command line.
    setsid sh -c 'sleep 120; :' job-run "$job" </dev/null >/dev/null 2>&1 & pid=$!
    printf '%s\n' "$pid" > "$jobs/$job/pid"; echo running > "$jobs/$job/state"; echo 1 > "$jobs/$job/total"
    mkdir /run/chj-disks.lock && printf '%s\n' "$job" > /run/chj-disks.lock/job
    echo "pid=$pid" ;;
  release-lock) # ID
    valid_id "$1" || die ARGS id; job=chjtest-$1-holder; jobs=/var/lib/chj-server-manager/disks/jobs
    [ -r "$jobs/$job/pid" ] && kill "$(cat "$jobs/$job/pid")" 2>/dev/null
    [ "$(cat /run/chj-disks.lock/job 2>/dev/null)" = "$job" ] && rm -rf /run/chj-disks.lock
    rm -rf -- "${jobs:?}/$job"; echo released ;;
  cleanup) # ID: only resources of this test run
    valid_id "$1" || die ARGS id
    for m in $(awk '$2 ~ /^\/mnt\/chj-disks-test-/ { print $2 }' /proc/mounts | sort -r); do umount "$m" 2>/dev/null; rmdir "$m" 2>/dev/null; done
    for map in /dev/mapper/chjtest*; do [ -e "$map" ] || continue; case ${map##*/} in *-*) continue ;; esac; cryptsetup close "${map##*/}" 2>/dev/null; done
    for img in "$DIR"/chjtest-"$1"*.img; do
      [ -e "$img" ] || continue
      # sysfs instead of "losetup -j" (not available in BusyBox).
      for sys in /sys/block/loop*; do
        [ "$(cat "$sys/loop/backing_file" 2>/dev/null)" = "$img" ] || continue
        dev=/dev/${sys##*/}
        for vg in $(pvs --noheadings -o vg_name "$dev"p* 2>/dev/null | tr -d ' ' | sort -u); do case $vg in chjtest*) vgremove -ff -y "$vg" >/dev/null 2>&1 ;; esac; done
        for p in "$dev"p*; do [ -b "$p" ] && pvremove -ff -y "$p" >/dev/null 2>&1; done
        for map in /dev/mapper/chjtest*; do [ -e "$map" ] || continue; cryptsetup close "${map##*/}" 2>/dev/null; done
        losetup -d "$dev" 2>/dev/null
      done
      rm -f -- "$img"
    done
    for m in /mnt/chj-disks-test-*; do [ -d "$m" ] && rmdir "$m" 2>/dev/null; done
    [ "$(cat /run/chj-disks.lock/job 2>/dev/null)" = "chjtest-$1-holder" ] && rm -rf /run/chj-disks.lock
    j=/var/lib/chj-server-manager/disks/jobs/chjtest-$1-holder; [ -r "$j/pid" ] && kill "$(cat "$j/pid")" 2>/dev/null; rm -rf -- "$j"
    rmdir "$DIR" 2>/dev/null
    echo "left=$(ls "$DIR" 2>/dev/null | wc -l) loops=$(cat /sys/block/loop*/loop/backing_file 2>/dev/null | grep -c "/chjtest-")" ;;
  cleanup-stale) # detach loops still bound to deleted CH-J test images of earlier runs (nothing else)
    n=0
    for sys in /sys/block/loop*; do
      b=$(cat "$sys/loop/backing_file" 2>/dev/null)
      case $b in "$DIR"/chjtest-[a-z0-9]*.img\ \(deleted\)) ;; *) continue ;; esac
      for m in $(awk -v d="/dev/${sys##*/}" 'index($1, d)==1 && $2 ~ /^\/mnt\/chj-disks-test-/ { print $2 }' /proc/mounts); do umount "$m"; done
      losetup -d "/dev/${sys##*/}" && n=$((n + 1))
    done
    echo "detached=$n loops=$(cat /sys/block/loop*/loop/backing_file 2>/dev/null | grep -c "/chjtest-")" ;;
  *) die ARGS op ;;
esac
