mirror of
https://codeberg.org/shufflecake/shufflecake-c.git
synced 2025-12-27 06:04:57 -05:00
745 lines
26 KiB
Bash
Executable file
745 lines
26 KiB
Bash
Executable file
#!/usr/bin/env bash
|
|
|
|
# Copyright The Shufflecake Project Authors (2022)
|
|
# Copyright The Shufflecake Project Contributors (2022)
|
|
# Copyright Contributors to the The Shufflecake Project.
|
|
|
|
# See the AUTHORS file at the top-level directory of this distribution and at
|
|
# <https://www.shufflecake.net/permalinks/shufflecake-c/AUTHORS>
|
|
|
|
# This file is part of the program shufflecake-c, which is part of the Shufflecake
|
|
# Project. Shufflecake is a plausible deniability (hidden storage) layer for
|
|
# Linux. See <https://www.shufflecake.net>.
|
|
|
|
# This program is free software: you can redistribute it and/or modify it
|
|
# under the terms of the GNU General Public License as published by the Free
|
|
# Software Foundation, either version 2 of the License, or (at your option)
|
|
# any later version. This program is distributed in the hope that it will be
|
|
# useful, but WITHOUT ANY WARRANTY; without even the implied warranty of
|
|
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General
|
|
# Public License for more details. You should have received a copy of the
|
|
# GNU General Public License along with this program.
|
|
# If not, see <https://www.gnu.org/licenses/>.
|
|
|
|
# Consistency test script for Shufflecake
|
|
# NOTE: there is an undocumented --auto option that skips all user interaction and uses loop devices for the tests
|
|
|
|
# Variables
|
|
SCRIPTNAME=$(basename "$0")
|
|
SCRIPT_DIR="$(dirname "$(realpath "$0")")"
|
|
LOOP_FILENAME="${SCRIPT_DIR}/sflc-test-loop-file.img"
|
|
HASHFILE1="${SCRIPT_DIR}/testfile1.sha256"
|
|
HASHFILE2="${SCRIPT_DIR}/testfile2.sha256"
|
|
LOOP_DEVICE=""
|
|
BLOCK_DEVICE=""
|
|
SFLCVOLUME1=""
|
|
MNTPOINT1=""
|
|
SFLCVOLUME2=""
|
|
MNTPOINT2=""
|
|
TIMEFORMAT='%3R'
|
|
SFLCPATH=""
|
|
SFLCNAME=""
|
|
DMSFLC_INSTALLED=false
|
|
AUTO_MODE=false
|
|
|
|
# Colors
|
|
BLUE='\033[0;34m'
|
|
GREEN='\033[0;32m'
|
|
RED='\033[0;31m'
|
|
NC='\033[0m' # No color
|
|
|
|
# Help
|
|
print_help() {
|
|
# xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx 79 chars
|
|
echo -e "${BLUE}Usage: ${SCRIPTNAME} [OPTION]... [BLOCKDEVICE]${NC}"
|
|
echo " "
|
|
echo "This script is used to test consistency of Shufflecake on this machine."
|
|
echo "This script is part of the Shufflecake test suite."
|
|
echo "Shufflecake is a plausible deniability (hidden storage) layer for Linux."
|
|
echo -e "Visit ${BLUE}https://www.shufflecake.net${NC} for more info and documentation."
|
|
echo " "
|
|
echo "This script requires root because it operates on block devices, please run it "
|
|
echo -e "with ${BLUE}sudo${NC}. It does the following:"
|
|
echo "1) Creates a Shufflecake (Lite) device with two volumes."
|
|
echo "2) Opens both volumes, formats them, and mounts them."
|
|
echo "3) Writes two large random files into each volume, saves their checksums."
|
|
echo "4) Unmounts and closes the used volume."
|
|
echo "5) Re-opens both devices, reads the files, and checks they didn't change."
|
|
echo "6) Deletes the files, then close the volumes."
|
|
echo "7) Re-re-opens both devices, and checks that free slices have been recovered."
|
|
echo " "
|
|
# xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx 79 chars
|
|
echo "You must have already compiled and installed Shufflecake in order to run this."
|
|
echo "The script will search for Shufflecake either in the default installation "
|
|
echo "directory, or in the current directory, or in the parent directory (one level "
|
|
echo -e "above this). If the module ${BLUE}dm-sflc${NC} is not loaded, the script "
|
|
echo "will load it, execute, and then unload it."
|
|
echo " "
|
|
# xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx 79 chars
|
|
echo "You can pass the path to a block device as an optional argument, otherwise the "
|
|
echo "script will ask for one. If no path is provided, the script will create a 2 GiB"
|
|
echo "local file and use it to back loop devices as a virtual block devices to be "
|
|
echo "formatted with the appropriate tools. The file will be removed at the end."
|
|
echo " "
|
|
# xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx 79 chars
|
|
echo -e "${BLUE}WARNING: ALL CONTENT OF THE PROVIDED BLOCK DEVICE WILL BE ERASED!${NC}"
|
|
echo " "
|
|
exit 0
|
|
}
|
|
|
|
# Function for debugging
|
|
bpx() {
|
|
if [ -z "$1" ]; then
|
|
echo "BPX: Paused. Press any key to continue..." >&2
|
|
else
|
|
echo "BPX: $1. Press any key to continue..." >&2
|
|
fi
|
|
read -n1 -s
|
|
}
|
|
|
|
# Show usage
|
|
usage() {
|
|
echo -e "Use ${BLUE}${SCRIPTNAME} --help${NC} for usage and help."
|
|
}
|
|
|
|
# Clean up function, called by trap on exit
|
|
cleanup() {
|
|
echo "Exiting and cleaning..."
|
|
|
|
# Unmount filesystem 1 if mounted
|
|
if [ -n "$MNTPOINT1" ] && grep -qs "$MNTPOINT1" /proc/mounts; then
|
|
echo "Unmounting \"$MNTPOINT1\" ..."
|
|
umount "$MNTPOINT1"
|
|
fi
|
|
# Unmount filesystem 2 if mounted
|
|
if [ -n "$MNTPOINT2" ] && grep -qs "$MNTPOINT2" /proc/mounts; then
|
|
echo "Unmounting \"$MNTPOINT2\" ..."
|
|
umount "$MNTPOINT2"
|
|
fi
|
|
|
|
# Remove mount point directory 1 if it exists
|
|
if [ -n "$MNTPOINT1" ] && [ -d "$MNTPOINT1" ]; then
|
|
rmdir "$MNTPOINT1"
|
|
fi
|
|
# Remove mount point directory 2 if it exists
|
|
if [ -n "$MNTPOINT2" ] && [ -d "$MNTPOINT2" ]; then
|
|
rmdir "$MNTPOINT2"
|
|
fi
|
|
|
|
# Close shufflecake volume if open. Check if *either* SFLCVOLUME variable
|
|
# was set and if *either* corresponding device path still exists.
|
|
local sflc_open=false
|
|
if [ -n "$SFLCVOLUME1" ] && [ -b "$SFLCVOLUME1" ]; then sflc_open=true; fi
|
|
if [ -n "$SFLCVOLUME2" ] && [ -b "$SFLCVOLUME2" ]; then sflc_open=true; fi
|
|
|
|
if [ "$sflc_open" = true ]; then
|
|
echo "Closing Shufflecake device on \"$BLOCK_DEVICE\" ..."
|
|
"$SFLCNAME" close "$BLOCK_DEVICE" &> /dev/null
|
|
fi
|
|
|
|
# Unload kernel module if we loaded it
|
|
unload_dmsflc
|
|
|
|
# Detach loop device if created
|
|
if [[ -n "$LOOP_DEVICE" ]]; then
|
|
echo "Detaching \"$LOOP_DEVICE\" ..."
|
|
losetup -d "$LOOP_DEVICE"
|
|
echo "Deleting \"$LOOP_FILENAME\" ..."
|
|
rm -f "$LOOP_FILENAME"
|
|
echo "Loop device detached and backing file deleted."
|
|
fi
|
|
|
|
# Clean up hash files
|
|
rm -f "$HASHFILE1" "$HASHFILE2" 2>/dev/null
|
|
}
|
|
|
|
# Set trap to run cleanup function on exit
|
|
trap cleanup EXIT SIGHUP SIGINT SIGTERM
|
|
|
|
# Check that this script is run as root
|
|
check_sudo() {
|
|
if [[ $EUID -ne 0 ]]; then
|
|
echo -e "${RED}Error: This script must be run as root.${NC}"
|
|
usage
|
|
exit 1
|
|
fi
|
|
}
|
|
|
|
# Find the path of Shufflecake executable
|
|
find_sflc_path() {
|
|
local cmd="shufflecake"
|
|
|
|
# Check if the command exists in the current directory
|
|
if [[ -x "./${cmd}" ]]; then
|
|
SFLCPATH=$(realpath ./)
|
|
SFLCNAME=$(realpath "./${cmd}")
|
|
return
|
|
fi
|
|
|
|
# Check if the command exists in the parent directory
|
|
if [[ -x "../${cmd}" ]]; then
|
|
SFLCPATH=$(realpath ../)
|
|
SFLCNAME=$(realpath "../${cmd}")
|
|
return
|
|
fi
|
|
|
|
# Check if the command exists in the directories listed in PATH
|
|
IFS=':' read -ra dirs <<< "$PATH"
|
|
for dir in "${dirs[@]}"; do
|
|
if [[ -x "${dir}/${cmd}" ]]; then
|
|
SFLCPATH=$(realpath "$dir")
|
|
SFLCNAME=$(realpath "${dir}/${cmd}")
|
|
return
|
|
fi
|
|
done
|
|
|
|
# If the command was not found, print an error message
|
|
echo -e "${RED}ERROR: Command '${cmd}' not found${NC}." >&2
|
|
exit 1
|
|
}
|
|
|
|
# Find and load module dm-sflc
|
|
load_dmsflc() {
|
|
local mod="dm-sflc"
|
|
# Kernel uses underscores, but module files may use hyphens.
|
|
local mod_name_loaded="${mod//-/_}"
|
|
|
|
# First, make sure that dm-mod is loaded
|
|
modprobe dm_mod
|
|
|
|
# Check if the module is already loaded
|
|
if lsmod | grep -q "^${mod_name_loaded} "; then
|
|
DMSFLC_INSTALLED=true
|
|
echo "Module '${mod}' is already loaded."
|
|
return
|
|
fi
|
|
|
|
# Try loading from system modules first
|
|
if modprobe "$mod" &> /dev/null; then
|
|
echo "Module '${mod}' loaded from system modules."
|
|
return
|
|
fi
|
|
|
|
# If not, look for the module file and try to load it
|
|
local mod_file="${mod}.ko"
|
|
|
|
# Check if the module file exists in the current directory
|
|
if [[ -f "./${mod_file}" ]]; then
|
|
insmod "$(realpath "./${mod_file}")" || exit 1
|
|
echo "Module '${mod}' loaded from current directory."
|
|
return
|
|
fi
|
|
|
|
# Check if the module file exists in the parent directory
|
|
if [[ -f "../${mod_file}" ]]; then
|
|
insmod "$(realpath "../${mod_file}")" || exit 1
|
|
echo "Module '${mod}' loaded from parent directory."
|
|
return
|
|
fi
|
|
|
|
# If the module file was not found, print an error message
|
|
echo -e "${RED} ERROR: Module file '${mod_file}' not found.${NC}." >&2
|
|
exit 1
|
|
}
|
|
|
|
# Unload dm-sflc if it was loaded by this script
|
|
unload_dmsflc() {
|
|
local mod="dm-sflc"
|
|
# Kernel uses underscores, but module files may use hyphens.
|
|
local mod_name_loaded="${mod//-/_}"
|
|
|
|
if [ "$DMSFLC_INSTALLED" = true ]; then
|
|
echo "Module '${mod}' will not be unloaded because it was already present when the script started."
|
|
return
|
|
fi
|
|
|
|
if lsmod | grep -q "^${mod_name_loaded} "; then
|
|
echo "Unloading module '${mod}'..."
|
|
rmmod "${mod_name_loaded}" || echo -e "${RED}Warning: Failed to unload module '${mod}'.${NC}" >&2
|
|
fi
|
|
}
|
|
|
|
# Function to check if argument is a block device
|
|
check_block_device() {
|
|
if [ -b "$1" ]; then
|
|
echo "OK, block device path \"$1\" is valid." >&2
|
|
else
|
|
echo -e "${RED}Error: \"$1\" is not a valid block device, aborting.${NC}"
|
|
usage
|
|
exit 1
|
|
fi
|
|
}
|
|
|
|
# Function to create loop device
|
|
create_loop_device() {
|
|
# Check for 2 GiB of free disk space ***
|
|
echo "Checking for 2 GiB of free disk space..." >&2
|
|
local required_kb=2097152 # 2 GiB = 2048 * 1024 KiB
|
|
local free_kb
|
|
free_kb=$(df -k "$SCRIPT_DIR" | awk 'NR==2 {print $4}')
|
|
|
|
if [ "$free_kb" -lt "$required_kb" ]; then
|
|
echo -e "${RED}Error: Not enough free disk space to create 2 GiB loop file.${NC}" >&2
|
|
echo "Required: $required_kb KiB, Available: $free_kb KiB in $SCRIPT_DIR" >&2
|
|
exit 1
|
|
fi
|
|
echo "OK, sufficient disk space available." >&2
|
|
|
|
echo "I will now try to create a file \"$LOOP_FILENAME\" ..." >&2
|
|
if [ -e "$LOOP_FILENAME" ]; then
|
|
echo -e "${RED}Error: Impossible to generate file, \"$LOOP_FILENAME\" already exists.${NC}"
|
|
exit 1
|
|
fi
|
|
dd if=/dev/zero of="$LOOP_FILENAME" bs=1M count=2048 &> /dev/null
|
|
echo "Writing of empty file complete. I will now try to attach it to a new loop device..." >&2
|
|
|
|
# Create the loop device
|
|
LOOP_DEVICE=$(losetup -f --show "$LOOP_FILENAME")
|
|
if [ $? -ne 0 ] || [ -z "$LOOP_DEVICE" ]; then
|
|
echo -e "${RED}Error: Failed to create loop device.${NC}"
|
|
exit 1
|
|
fi
|
|
|
|
echo "Successfully created loop device \"$LOOP_DEVICE\" ." >&2
|
|
echo "$LOOP_DEVICE"
|
|
}
|
|
|
|
# Function for user confirmation
|
|
confirm() {
|
|
while true; do
|
|
echo -e "${BLUE}Are you sure you want to proceed? All data on disk \"$BLOCK_DEVICE\" will be erased. (y/n)${NC}"
|
|
read -r response
|
|
case "$response" in
|
|
[yY] | [yY][eE][sS]) # Responded Yes
|
|
return 0 # Return 0 for Yes (success, convention for bash scripting)
|
|
;;
|
|
[nN] | [nN][oO]) # Responded No
|
|
return 1 # Return 1 for No (error, convention for bash scripting)
|
|
;;
|
|
*) # Responded something else
|
|
echo "Please press only (y)es or (n)o."
|
|
;;
|
|
esac
|
|
done
|
|
}
|
|
|
|
# Tests
|
|
do_test() {
|
|
local TESTFILE1="testfile.100M"
|
|
local TESTFILE2="testfile.1400M"
|
|
# Use global HASHFILE1 and HASHFILE2
|
|
MNTPOINT1=$(realpath "./sflc_mnt_1") # Use global
|
|
MNTPOINT2=$(realpath "./sflc_mnt_2") # Use global
|
|
|
|
echo "Starting consistency test for Shufflecake Lite..."
|
|
echo "Initializing block device \"$BLOCK_DEVICE\" with two Shufflecake Lite volumes (--skip-randfill)..."
|
|
etime=$( (time echo -e "passwd1\npasswd2" | "$SFLCNAME" --skip-randfill -n 2 init "$BLOCK_DEVICE" &> /dev/null) 2>&1 )
|
|
if [ $? -ne 0 ]; then echo -e "${RED}ERROR: Shufflecake init failed.${NC}" >&2; exit 1; fi
|
|
echo -e "${GREEN}Action init took $etime seconds.${NC}"
|
|
|
|
# --- STAGE 1: OPEN, CREATE, HASH ---
|
|
echo -e "${BLUE}--- STAGE 1: Opening volumes, creating files, and calculating hashes ---${NC}"
|
|
|
|
echo "Opening all volumes..."
|
|
local BEFORE_OPEN
|
|
BEFORE_OPEN=$(ls /dev/mapper/sflc_* 2>/dev/null | sort -V)
|
|
|
|
etime=$( (time echo -e "passwd2" | "$SFLCNAME" open "$BLOCK_DEVICE" &> /dev/null) 2>&1 )
|
|
if [ $? -ne 0 ]; then echo -e "${RED}ERROR: Shufflecake open failed.${NC}" >&2; exit 1; fi
|
|
|
|
local AFTER_OPEN
|
|
AFTER_OPEN=$(ls /dev/mapper/sflc_* 2>/dev/null | sort -V)
|
|
local NEW_VOLUMES
|
|
NEW_VOLUMES=$(comm -13 <(echo "$BEFORE_OPEN") <(echo "$AFTER_OPEN"))
|
|
|
|
if [ $(echo "$NEW_VOLUMES" | wc -l) -ne 2 ]; then
|
|
echo -e "${RED}ERROR: Expected 2 new volumes to be created, but found $(echo "$NEW_VOLUMES" | wc -l).${NC}" >&2
|
|
exit 1
|
|
fi
|
|
|
|
# Assign to global vars for cleanup. `sort -V` and `sed` ensure sflc_..._1 is vol1.
|
|
SFLCVOLUME1=$(echo "$NEW_VOLUMES" | sed -n '1p')
|
|
SFLCVOLUME2=$(echo "$NEW_VOLUMES" | sed -n '2p')
|
|
|
|
echo -e "${GREEN}Action open took $etime seconds.${NC}"
|
|
echo "Found volume 1: $SFLCVOLUME1"
|
|
echo "Found volume 2: $SFLCVOLUME2"
|
|
|
|
# --- Volume 1 ---
|
|
echo "Formatting $SFLCVOLUME1 with ext4..."
|
|
mkfs.ext4 -F "$SFLCVOLUME1" &> /dev/null # -F to force, non-interactive
|
|
if [ $? -ne 0 ]; then echo -e "${RED}ERROR: mkfs.ext4 failed on \"$SFLCVOLUME1\".${NC}" >&2; exit 1; fi
|
|
udevadm settle
|
|
|
|
mkdir -p "$MNTPOINT1"
|
|
mount "$SFLCVOLUME1" "$MNTPOINT1"
|
|
if [ $? -ne 0 ]; then echo -e "${RED}ERROR: Mount failed for \"$SFLCVOLUME1\" on \"$MNTPOINT1\".${NC}" >&2; exit 1; fi
|
|
echo "Volume 1 mounted at $MNTPOINT1."
|
|
|
|
echo "Creating 100MB test file on volume 1..."
|
|
dd if=/dev/urandom of="${MNTPOINT1}/${TESTFILE1}" bs=1M count=100 &> /dev/null
|
|
if [ $? -ne 0 ]; then echo -e "${RED}ERROR: dd failed for volume 1.${NC}" >&2; exit 1; fi
|
|
|
|
echo "Calculating SHA256 hash for 100MB file..."
|
|
sha256sum "${MNTPOINT1}/${TESTFILE1}" | cut -d' ' -f1 > "$HASHFILE1"
|
|
if [ $? -ne 0 ]; then echo -e "${RED}ERROR: sha256sum failed for volume 1.${NC}" >&2; exit 1; fi
|
|
echo "Hash for volume 1 saved to $HASHFILE1."
|
|
|
|
# --- Volume 2 ---
|
|
echo "Formatting $SFLCVOLUME2 with ext4..."
|
|
mkfs.ext4 -F "$SFLCVOLUME2" &> /dev/null # -F to force, non-interactive
|
|
if [ $? -ne 0 ]; then echo -e "${RED}ERROR: mkfs.ext4 failed on \"$SFLCVOLUME2\".${NC}" >&2; exit 1; fi
|
|
udevadm settle
|
|
|
|
mkdir -p "$MNTPOINT2"
|
|
mount "$SFLCVOLUME2" "$MNTPOINT2"
|
|
if [ $? -ne 0 ]; then echo -e "${RED}ERROR: Mount failed for \"$SFLCVOLUME2\" on \"$MNTPOINT2\".${NC}" >&2; exit 1; fi
|
|
echo "Volume 2 mounted at $MNTPOINT2."
|
|
|
|
echo "Creating 1400MB test file on volume 2..."
|
|
dd if=/dev/urandom of="${MNTPOINT2}/${TESTFILE2}" bs=1M count=1400 &> /dev/null
|
|
if [ $? -ne 0 ]; then echo -e "${RED}ERROR: dd failed for volume 2.${NC}" >&2; exit 1; fi
|
|
|
|
echo "Calculating SHA256 hash for 1400MB file..."
|
|
sha256sum "${MNTPOINT2}/${TESTFILE2}" | cut -d' ' -f1 > "$HASHFILE2"
|
|
if [ $? -ne 0 ]; then echo -e "${RED}ERROR: sha256sum failed for volume 2.${NC}" >&2; exit 1; fi
|
|
echo "Hash for volume 2 saved to $HASHFILE2."
|
|
|
|
# --- STAGE 2: UNMOUNT, CLOSE ---
|
|
echo -e "${BLUE}--- STAGE 2: Unmounting and closing volumes ---${NC}"
|
|
|
|
umount "$MNTPOINT1"
|
|
if [ $? -ne 0 ]; then echo -e "${RED}ERROR: Unmount failed for \"$MNTPOINT1\".${NC}" >&2; exit 1; fi
|
|
rmdir "$MNTPOINT1"
|
|
|
|
umount "$MNTPOINT2"
|
|
if [ $? -ne 0 ]; then echo -e "${RED}ERROR: Unmount failed for \"$MNTPOINT2\".${NC}" >&2; exit 1; fi
|
|
rmdir "$MNTPOINT2"
|
|
|
|
echo "Closing Shufflecake device on \"$BLOCK_DEVICE\" ..."
|
|
"$SFLCNAME" close "$BLOCK_DEVICE" &> /dev/null
|
|
if [ $? -ne 0 ]; then echo -e "${RED}ERROR: Shufflecake close failed.${NC}" >&2; exit 1; fi
|
|
|
|
# Clear the globals so the cleanup trap doesn't try to re-close/re-umount
|
|
SFLCVOLUME1=""
|
|
SFLCVOLUME2=""
|
|
MNTPOINT1=""
|
|
MNTPOINT2=""
|
|
echo "Volumes unmounted and closed."
|
|
|
|
# --- STAGE 3: RE-OPEN, RE-MOUNT, CHECK ---
|
|
echo -e "${BLUE}--- STAGE 3: Re-opening volumes and checking file integrity ---${NC}"
|
|
|
|
echo "Re-opening all volumes..."
|
|
BEFORE_OPEN=$(ls /dev/mapper/sflc_* 2>/dev/null | sort -V)
|
|
|
|
etime=$( (time echo -e "passwd2" | "$SFLCNAME" open "$BLOCK_DEVICE" &> /dev/null) 2>&1 )
|
|
if [ $? -ne 0 ]; then echo -e "${RED}ERROR: Shufflecake (re-)open failed.${NC}" >&2; exit 1; fi
|
|
|
|
AFTER_OPEN=$(ls /dev/mapper/sflc_* 2>/dev/null | sort -V)
|
|
NEW_VOLUMES=$(comm -13 <(echo "$BEFORE_OPEN") <(echo "$AFTER_OPEN"))
|
|
|
|
if [ $(echo "$NEW_VOLUMES" | wc -l) -ne 2 ]; then
|
|
echo -e "${RED}ERROR: Expected 2 new volumes to be re-opened, but found $(echo "$NEW_VOLUMES" | wc -l).${NC}" >&2
|
|
exit 1
|
|
fi
|
|
|
|
# Re-assign to global vars for cleanup
|
|
SFLCVOLUME1=$(echo "$NEW_VOLUMES" | sed -n '1p')
|
|
SFLCVOLUME2=$(echo "$NEW_VOLUMES" | sed -n '2p')
|
|
|
|
echo -e "${GREEN}Action (re-)open took $etime seconds.${NC}"
|
|
echo "Found volume 1: $SFLCVOLUME1"
|
|
echo "Found volume 2: $SFLCVOLUME2"
|
|
|
|
# --- Volume 1 Check ---
|
|
MNTPOINT1=$(realpath "./sflc_mnt_1") # Re-set global for cleanup
|
|
mkdir -p "$MNTPOINT1"
|
|
mount "$SFLCVOLUME1" "$MNTPOINT1"
|
|
if [ $? -ne 0 ]; then echo -e "${RED}ERROR: Re-mount failed for \"$SFLCVOLUME1\" on \"$MNTPOINT1\".${NC}" >&2; exit 1; fi
|
|
echo "Volume 1 re-mounted at $MNTPOINT1."
|
|
|
|
echo "Checking hash for 100MB file..."
|
|
local ORIGINAL_HASH1
|
|
ORIGINAL_HASH1=$(cat "$HASHFILE1")
|
|
local NEW_HASH1
|
|
NEW_HASH1=$(sha256sum "${MNTPOINT1}/${TESTFILE1}" | cut -d' ' -f1)
|
|
|
|
if [ "$ORIGINAL_HASH1" == "$NEW_HASH1" ]; then
|
|
echo -e "${GREEN}SUCCESS: Volume 1 file hash matches!${NC}"
|
|
else
|
|
echo -e "${RED}FAILURE: Volume 1 file hash MISMATCH!${NC}" >&2
|
|
echo "Original: $ORIGINAL_HASH1" >&2
|
|
echo "New: $NEW_HASH1" >&2
|
|
exit 1
|
|
fi
|
|
|
|
# --- Volume 2 Check ---
|
|
MNTPOINT2=$(realpath "./sflc_mnt_2") # Re-set global for cleanup
|
|
mkdir -p "$MNTPOINT2"
|
|
mount "$SFLCVOLUME2" "$MNTPOINT2"
|
|
if [ $? -ne 0 ]; then echo -e "${RED}ERROR: Re-mount failed for \"$SFLCVOLUME2\" on \"$MNTPOINT2\".${NC}" >&2; exit 1; fi
|
|
echo "Volume 2 re-mounted at $MNTPOINT2."
|
|
|
|
echo "Checking hash for 1400MB file..."
|
|
local ORIGINAL_HASH2
|
|
ORIGINAL_HASH2=$(cat "$HASHFILE2")
|
|
local NEW_HASH2
|
|
NEW_HASH2=$(sha256sum "${MNTPOINT2}/${TESTFILE2}" | cut -d' ' -f1)
|
|
|
|
if [ "$ORIGINAL_HASH2" == "$NEW_HASH2" ]; then
|
|
echo -e "${GREEN}SUCCESS: Volume 2 file hash matches!${NC}"
|
|
else
|
|
echo -e "${RED}FAILURE: Volume 2 file hash MISMATCH!${NC}" >&2
|
|
echo "Original: $ORIGINAL_HASH2" >&2
|
|
echo "New: $NEW_HASH2" >&2
|
|
exit 1
|
|
fi
|
|
|
|
# --- STAGE 4: CHECK SLICE RECOVERY (GARBAGE COLLECTION) ---
|
|
echo -e "${BLUE}--- STAGE 4: Checking slice recovery (garbage collection) ---${NC}"
|
|
|
|
# 1. Get the BDEV_ID (Major:Minor) for the underlying block device
|
|
local MAJOR MINOR BDEV_ID
|
|
MAJOR=$(ls -l "$BLOCK_DEVICE" | awk '{print $5}' | tr -d ',')
|
|
MINOR=$(ls -l "$BLOCK_DEVICE" | awk '{print $6}')
|
|
BDEV_ID="${MAJOR}:${MINOR}"
|
|
|
|
local SLICE_FILE="/sys/module/dm_sflc/bdevs/${BDEV_ID}/free_slices"
|
|
|
|
if [ ! -f "$SLICE_FILE" ]; then
|
|
echo -e "${RED}FAILURE: Cannot find slice file at $SLICE_FILE!${NC}" >&2
|
|
echo "Device ID was ${BDEV_ID}. Is the dm-sflc module loaded correctly?" >&2
|
|
exit 1
|
|
fi
|
|
|
|
# 2. Get the number of free slices *before* deleting files
|
|
local SLICES_BEFORE
|
|
SLICES_BEFORE=$(cat "$SLICE_FILE")
|
|
if ! [[ "$SLICES_BEFORE" =~ ^[0-9]+$ ]]; then
|
|
echo -e "${RED}FAILURE: Invalid content in $SLICE_FILE: $(cat "$SLICE_FILE")${NC}" >&2
|
|
exit 1
|
|
fi
|
|
echo "Free slices before delete: $SLICES_BEFORE"
|
|
|
|
# 3. Delete the files
|
|
echo "Deleting test files..."
|
|
rm -f "${MNTPOINT1}/${TESTFILE1}"
|
|
rm -f "${MNTPOINT2}/${TESTFILE2}"
|
|
sleep 1
|
|
sync # Ensure all I/O is flushed
|
|
sleep 1
|
|
|
|
|
|
# 4. Run fstrim to notify Shufflecake of the freed blocks
|
|
echo "Running fstrim on $MNTPOINT1 (ext4)..."
|
|
fstrim "$MNTPOINT1"
|
|
if [ $? -ne 0 ]; then echo -e "${RED}WARNING: fstrim failed on \"$MNTPOINT1\". Slice recovery test may fail.${NC}" >&2; fi
|
|
|
|
echo "Running fstrim on $MNTPOINT2 (ext4)..."
|
|
fstrim "$MNTPOINT2"
|
|
if [ $? -ne 0 ]; then echo -e "${RED}WARNING: fstrim failed on \"$MNTPOINT2\". Slice recovery test may fail.${NC}" >&2; fi
|
|
|
|
sleep 1
|
|
sync # Ensure all I/O is flushed
|
|
sleep 1
|
|
|
|
# 5. Unmount and close the device
|
|
echo "Unmounting volumes..."
|
|
umount "$MNTPOINT1"
|
|
if [ $? -ne 0 ]; then echo -e "${RED}ERROR: Unmount failed for \"$MNTPOINT1\".${NC}" >&2; exit 1; fi
|
|
rmdir "$MNTPOINT1"
|
|
|
|
umount "$MNTPOINT2"
|
|
if [ $? -ne 0 ]; then echo -e "${RED}ERROR: Unmount failed for \"$MNTPOINT2\".${NC}" >&2; exit 1; fi
|
|
rmdir "$MNTPOINT2"
|
|
|
|
echo "Closing Shufflecake device on \"$BLOCK_DEVICE\" ..."
|
|
"$SFLCNAME" close "$BLOCK_DEVICE" &> /dev/null
|
|
if [ $? -ne 0 ]; then echo -e "${RED}ERROR: Shufflecake close failed.${NC}" >&2; exit 1; fi
|
|
sleep 1
|
|
|
|
# Clear globals so cleanup trap doesn't complain
|
|
SFLCVOLUME1=""
|
|
SFLCVOLUME2=""
|
|
MNTPOINT1=""
|
|
MNTPOINT2=""
|
|
echo "Volumes unmounted and closed."
|
|
|
|
# 6. Re-open the device to trigger garbage collection
|
|
echo "Re-opening device to trigger slice recovery..."
|
|
etime=$( (time echo -e "passwd2" | "$SFLCNAME" open "$BLOCK_DEVICE" &> /dev/null) 2>&1 )
|
|
if [ $? -ne 0 ]; then echo -e "${RED}ERROR: Shufflecake (re-re-)open failed.${NC}" >&2; exit 1; fi
|
|
echo -e "${GREEN}Action (re-re-)open took $etime seconds.${NC}"
|
|
sleep 1
|
|
|
|
# Re-populate globals for the final cleanup trap
|
|
local ALL_OPEN_VOLS
|
|
ALL_OPEN_VOLS=$(ls /dev/mapper/sflc_* 2>/dev/null | sort -V)
|
|
if [ $(echo "$ALL_OPEN_VOLS" | wc -l) -ne 2 ]; then
|
|
echo -e "${RED}ERROR: Expected 2 volumes to be re-re-opened, but found $(echo "$ALL_OPEN_VOLS" | wc -l).${NC}" >&2
|
|
exit 1
|
|
fi
|
|
SFLCVOLUME1=$(echo "$ALL_OPEN_VOLS" | sed -n '1p')
|
|
SFLCVOLUME2=$(echo "$ALL_OPEN_VOLS" | sed -n '2p')
|
|
echo "Found volume 1: $SFLCVOLUME1"
|
|
echo "Found volume 2: $SFLCVOLUME2"
|
|
|
|
# Proactively mount volumes to prevent OS auto-mounter from grabbing them
|
|
echo "Proactively mounting volumes to prevent OS auto-mount..."
|
|
MNTPOINT1=$(realpath "./sflc_mnt_1") # Re-set global for cleanup
|
|
mkdir -p "$MNTPOINT1"
|
|
mount "$SFLCVOLUME1" "$MNTPOINT1"
|
|
if [ $? -ne 0 ]; then echo -e "${RED}ERROR: Proactive re-mount failed for \"$SFLCVOLUME1\" on \"$MNTPOINT1\".${NC}" >&2; exit 1; fi
|
|
|
|
MNTPOINT2=$(realpath "./sflc_mnt_2") # Re-set global for cleanup
|
|
mkdir -p "$MNTPOINT2"
|
|
mount "$SFLCVOLUME2" "$MNTPOINT2"
|
|
if [ $? -ne 0 ]; then echo -e "${RED}ERROR: Proactive re-mount failed for \"$SFLCVOLUME2\" on \"$MNTPOINT2\".${NC}" >&2; exit 1; fi
|
|
echo "Volumes re-mounted."
|
|
|
|
# 7. Check the slice file again
|
|
if [ ! -f "$SLICE_FILE" ]; then
|
|
echo -e "${RED}FAILURE: Cannot find slice file at $SLICE_FILE after re-open!${NC}" >&2
|
|
exit 1
|
|
fi
|
|
|
|
local SLICES_AFTER
|
|
SLICES_AFTER=$(cat "$SLICE_FILE")
|
|
if ! [[ "$SLICES_AFTER" =~ ^[0-9]+$ ]]; then
|
|
echo -e "${RED}FAILURE: Invalid content in $SLICE_FILE after re-open: $(cat "$SLICE_FILE")${NC}" >&2
|
|
exit 1
|
|
fi
|
|
echo "Free slices after recovery: $SLICES_AFTER"
|
|
|
|
# 8. Do the math and check
|
|
local SLICES_RECOVERED
|
|
SLICES_RECOVERED=$((SLICES_AFTER - SLICES_BEFORE))
|
|
local TOTAL_DELETED=1500 # 100MiB + 1400MiB = 1500 slices
|
|
local EXPECTED_RECOVERY=1200 # 80% of 1500
|
|
|
|
echo "Total slices deleted (approx): $TOTAL_DELETED"
|
|
echo "Slices recovered: $SLICES_RECOVERED"
|
|
echo "Expected recovery (>= 80%): $EXPECTED_RECOVERY"
|
|
|
|
if [ "$SLICES_RECOVERED" -ge "$EXPECTED_RECOVERY" ]; then
|
|
echo -e "${GREEN}SUCCESS: Slice recovery test passed! Recovered $SLICES_RECOVERED slices (>= $EXPECTED_RECOVERY).${NC}"
|
|
else
|
|
echo -e "${RED}FAILURE: Slice recovery test FAILED! Recovered only $SLICES_RECOVERED slices (< $EXPECTED_RECOVERY).${NC}" >&2
|
|
# Umount (failure case)
|
|
echo "Unmounting volumes after test failure..."
|
|
umount "$MNTPOINT1" &> /dev/null
|
|
rmdir "$MNTPOINT1" &> /dev/null
|
|
umount "$MNTPOINT2" &> /dev/null
|
|
rmdir "$MNTPOINT2" &> /dev/null
|
|
MNTPOINT1=""
|
|
MNTPOINT2=""
|
|
|
|
exit 1
|
|
fi
|
|
|
|
# Umount (success case)
|
|
echo "Unmounting volumes..."
|
|
umount "$MNTPOINT1"
|
|
if [ $? -ne 0 ]; then echo -e "${RED}ERROR: Unmount failed for \"$MNTPOINT1\".${NC}" >&2; exit 1; fi
|
|
rmdir "$MNTPOINT1"
|
|
|
|
umount "$MNTPOINT2"
|
|
if [ $? -ne 0 ]; then echo -e "${RED}ERROR: Unmount failed for \"$MNTPOINT2\".${NC}" >&2; exit 1; fi
|
|
rmdir "$MNTPOINT2"
|
|
|
|
# Clear mount globals so the final trap doesn't try to unmount them again
|
|
MNTPOINT1=""
|
|
MNTPOINT2=""
|
|
|
|
echo -e "${GREEN}--- ALL CONSISTENCY TESTS PASSED ---${NC}"
|
|
# The cleanup trap will handle closing the sflc device.
|
|
}
|
|
|
|
|
|
#####################################################################
|
|
|
|
# MAIN SCRIPT BODY STARTS HERE
|
|
|
|
#####################################################################
|
|
|
|
# BANNER
|
|
# xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx 79 chars
|
|
echo -e "${BLUE}===============================================================================${NC}"
|
|
echo -e "${BLUE} Consistency Test for Shufflecake Lite${NC}"
|
|
echo -e "${BLUE}===============================================================================${NC}"
|
|
|
|
while [ "$#" -gt 0 ]; do
|
|
case "$1" in
|
|
--help|-?)
|
|
print_help
|
|
;;
|
|
--auto)
|
|
AUTO_MODE=true
|
|
;;
|
|
*)
|
|
if [ -n "$BLOCK_DEVICE" ]; then
|
|
echo -e "${RED}Error: You can only specify one block device.${NC}" >&2
|
|
usage
|
|
exit 1
|
|
fi
|
|
BLOCK_DEVICE="$1"
|
|
;;
|
|
esac
|
|
shift
|
|
done
|
|
|
|
# Check for conflicting options
|
|
if [ "$AUTO_MODE" = true ] && [ -n "$BLOCK_DEVICE" ]; then
|
|
echo -e "${RED}Error: --auto cannot be used when a BLOCKDEVICE is specified.${NC}" >&2
|
|
usage
|
|
exit 1
|
|
fi
|
|
|
|
# PRELIMINARY CHECKS
|
|
check_sudo
|
|
|
|
echo -e "${BLUE}Initializing Shufflecake...${NC}"
|
|
echo "Searching Shufflecake executable..."
|
|
find_sflc_path
|
|
echo "Shufflecake executable found at \"$SFLCNAME\"."
|
|
echo "Searching and loading dm-sflc module..."
|
|
load_dmsflc
|
|
echo "Module dm-sflc status determined. DMSFLC_INSTALLED flag is: $DMSFLC_INSTALLED."
|
|
echo " "
|
|
|
|
|
|
# DETERMINE BLOCK DEVICE
|
|
if [ "$AUTO_MODE" = true ]; then
|
|
echo "Auto-mode enabled. Creating a local file and loop device..."
|
|
LOOP_DEVICE=$(create_loop_device)
|
|
BLOCK_DEVICE="$LOOP_DEVICE"
|
|
elif [ -z "$BLOCK_DEVICE" ]; then
|
|
echo "Now you will be asked to enter the path for a block device to be used for the "
|
|
echo "benchmarks (all content will be erased). If no path is provided (default"
|
|
echo "choice), then the script will create a 1 GiB file in the current directory and "
|
|
echo "use it to back a loop device instead, then the file will be removed at the end."
|
|
echo " "
|
|
echo -n "Please enter the path for a block device (default: none): "
|
|
read -r BLOCK_DEVICE
|
|
if [ -z "$BLOCK_DEVICE" ]; then
|
|
echo "No path provided, creating a local file and loop device..."
|
|
LOOP_DEVICE=$(create_loop_device)
|
|
BLOCK_DEVICE="$LOOP_DEVICE"
|
|
fi
|
|
fi
|
|
|
|
check_block_device "$BLOCK_DEVICE"
|
|
|
|
# MAIN PROGRAM
|
|
if [ "$AUTO_MODE" = true ]; then
|
|
echo "Auto-mode: Skipping confirmation and running test."
|
|
do_test
|
|
elif confirm; then
|
|
do_test
|
|
else
|
|
echo "Aborting..."
|
|
fi
|
|
|
|
# The cleanup trap will automatically run on exit.
|
|
# END SCRIPT
|