Skip to content

Instantly share code, notes, and snippets.

@someodd
Created September 12, 2025 10:48
Show Gist options
  • Select an option

  • Save someodd/a15a17b100682d53aa2a9385f41f53f7 to your computer and use it in GitHub Desktop.

Select an option

Save someodd/a15a17b100682d53aa2a9385f41f53f7 to your computer and use it in GitHub Desktop.
My LTO6 archive script.
#!/usr/bin/env bash
# lto_backup.sh — tiny, cautious LTO-6 backup helper
# - Assumes LTO-6 native capacity (~2.5 TB, decimal bytes).
# - Prompts before each step; requires exact "OK".
# - Starts writing from BOT; proceeding will overwrite existing data.
# - Uses /dev/nst0 (no-rewind); rewinds explicitly only when needed.
set -Eeuo pipefail
# -------- run-in-screen reminder (non-blocking) --------
cat <<'EOM'
=============================================================
NOTE: Long tape backups can take hours.
It's strongly recommended you run this inside a screen
session so it won't abort if your terminal disconnects.
Example:
screen -S lto
./lto_backup.sh
To reconnect later:
screen -r lto
=============================================================
EOM
# -------- tiny helpers --------
SUDO=""
[[ ${EUID:-$(id -u)} -ne 0 ]] && SUDO="sudo"
confirm() {
echo
echo "TYPE OK to proceed, or anything else to cancel."
read -r ans
if [[ "${ans:-}" != "OK" ]]; then
echo "Cancelled."
exit 1
fi
}
failtrap() {
local exit_code=$?
echo
echo "ERROR: Script failed (exit ${exit_code}) at line ${BASH_LINENO[0]:-?} running command: ${BASH_COMMAND:-?}"
echo "Stopping. No further steps will run."
exit "${exit_code}"
}
trap failtrap ERR
need_bin() { command -v "$1" >/dev/null 2>&1 || { echo "Missing: $1 (please install)"; exit 1; }; }
# -------- deps --------
for bin in mt tar zstd stenc head mktemp grep awk; do need_bin "$bin"; done
# -------- constants & defaults --------
TAPE_DEV="/dev/nst0"
DEFAULT_KEY="/etc/2024-11-lto5.key"
DEFAULT_SRC="/media/root/BackupRAID"
# LTO-6 native capacity (vendor decimal bytes ~2.5 TB)
LTO6_NATIVE_BYTES=2500000000000
THRESH=$(( (LTO6_NATIVE_BYTES * 90) / 100 ))
# -------- 1) check tape drive status --------
echo
echo "Step 1: Check tape drive status on ${TAPE_DEV}."
echo "We will run: ${SUDO} mt -f ${TAPE_DEV} status"
echo "If no tape is present, you'll be prompted to insert one and retry."
confirm
while true; do
if ${SUDO} mt -f "${TAPE_DEV}" status; then
echo "Tape drive is online."
break
else
echo
echo "Could not read tape status. Insert a tape and press Enter to retry,"
echo "or type 'q' then Enter to quit."
read -r retry
[[ "${retry:-}" == "q" ]] && { echo "User quit."; exit 1; }
fi
done
# -------- 2) probe for existing content (non-destructive) --------
echo
echo "Step 2: Probe for an existing zstd+tar archive (non-destructive)."
echo "We will rewind, try a listing, then leave tape positioned at BOT for safety."
echo "Commands:"
echo " ${SUDO} mt -f ${TAPE_DEV} rewind"
echo " ${SUDO} tar -tvf ${TAPE_DEV} --use-compress-program=zstd (may fail if not zstd+tar)"
echo " ${SUDO} mt -f ${TAPE_DEV} rewind"
confirm
TMP_LIST="$(mktemp)"
TMP_ERR="$(mktemp)"
${SUDO} mt -f "${TAPE_DEV}" rewind
if ${SUDO} tar -tvf "${TAPE_DEV}" --use-compress-program=zstd >"${TMP_LIST}" 2>"${TMP_ERR}"; then
echo "NOTE: Detected a zstd+tar archive on tape (first few entries):"
head -n 10 "${TMP_LIST}" || true
echo "(Full listing suppressed.)"
else
echo "No readable zstd+tar listing. The tape may be empty, a different format, or encrypted."
fi
${SUDO} mt -f "${TAPE_DEV}" rewind
rm -f "${TMP_LIST}" "${TMP_ERR}"
# -------- overwrite warning & LTO-6 assumption --------
echo
echo "IMPORTANT:"
echo "- Assuming LTO-6 (native capacity ~2.5 TB)."
echo "- Proceeding will start writing from BEGINNING OF TAPE (BOT) and will OVERWRITE existing data as it writes."
echo "- If you need a full erase, use: mt -f ${TAPE_DEV} erase (can take a long time)."
confirm
# -------- 3) encryption: query with --detail, summarize, then KEEP / REPLACE / ABORT --------
echo
echo "Step 3: Tape encryption (drive-managed)."
echo "We will first QUERY the current state, then you choose to KEEP, REPLACE, or ABORT."
echo "Query command:"
echo " ${SUDO} stenc -a 1 -f ${TAPE_DEV} --detail"
confirm
STENC_DETAIL_OUT="$(mktemp)"
if ${SUDO} stenc -a 1 -f "${TAPE_DEV}" --detail | tee "${STENC_DETAIL_OUT}"; then
:
else
echo "WARNING: Could not query encryption state with '--detail'."
echo "You can still continue, but it's safer to resolve this first."
fi
DEFAULT_ACTION="KEEP"
if [[ -s "${STENC_DETAIL_OUT}" ]]; then
DRIVE_ENC=$(grep -E '^Drive Encryption:' "${STENC_DETAIL_OUT}" | awk '{print $NF}')
DRIVE_INP=$(grep -E '^Drive Input:' "${STENC_DETAIL_OUT}" | awk '{print $NF}')
DRIVE_OUT=$(grep -E '^Drive Output:' "${STENC_DETAIL_OUT}" | awk '{print $NF}')
RAW_PROTECT=$(grep -A1 -E '^Drive Input:' "${STENC_DETAIL_OUT}" | tail -n +2 | grep -qi 'Protecting from raw read' && echo "yes" || echo "no")
NO_PLAINTEXT=$(grep -A1 -E '^Drive Output:' "${STENC_DETAIL_OUT}" | tail -n +2 | grep -qi 'Unencrypted data not outputted' && echo "yes" || echo "no")
VOL_ENC=$(grep -E '^Volume Encryption:' "${STENC_DETAIL_OUT}" | awk '{print $NF}')
echo
echo "Encryption summary:"
echo " Drive Encryption: ${DRIVE_ENC:-unknown}"
echo " Drive Input: ${DRIVE_INP:-unknown} (future writes)"
echo " Drive Output: ${DRIVE_OUT:-unknown} (reads)"
echo " Raw-read protect: ${RAW_PROTECT}"
echo " No-plaintext: ${NO_PLAINTEXT}"
echo " Volume state: ${VOL_ENC:-unknown}"
if [[ "${DRIVE_ENC,,}" == "on" && "${DRIVE_INP,,}" == "encrypting" ]]; then
DEFAULT_ACTION="KEEP"
echo "Suggestion: Drive is already set to encrypt input; KEEP is a safe default."
else
DEFAULT_ACTION="REPLACE"
echo "Suggestion: Drive not encrypting input; consider REPLACE to enable with your key."
fi
fi
rm -f "${STENC_DETAIL_OUT}"
read -r -p "Encryption action? [KEEP/REPLACE/ABORT] (default ${DEFAULT_ACTION}): " ENC_ACTION
ENC_ACTION="${ENC_ACTION:-$DEFAULT_ACTION}"
case "$ENC_ACTION" in
KEEP|keep)
echo "Keeping current encryption state/key. No changes will be made."
;;
REPLACE|replace)
read -r -p "Enter NEW encryption key path [${DEFAULT_KEY}]: " KEY_PATH
KEY_PATH="${KEY_PATH:-$DEFAULT_KEY}"
echo
echo "We will switch to the new key by turning encryption OFF, then ON with --protect."
echo "Commands:"
echo " ${SUDO} stenc -a 1 -f ${TAPE_DEV} -e off"
echo " ${SUDO} stenc -a 1 -f ${TAPE_DEV} -e on -k ${KEY_PATH} --protect"
echo "NOTES:"
echo "- This only affects FUTURE writes; already-written encrypted data stays encrypted with its original key."
echo "- Preserve the old key to read previously written data."
confirm
${SUDO} stenc -a 1 -f "${TAPE_DEV}" -e off
${SUDO} stenc -a 1 -f "${TAPE_DEV}" -e on -k "${KEY_PATH}" --protect
echo "Re-querying encryption state (detail):"
${SUDO} stenc -a 1 -f "${TAPE_DEV}" --detail || true
;;
ABORT|abort)
echo "Aborting at user request."
exit 1
;;
*)
echo "Unrecognized choice: $ENC_ACTION"
exit 1
;;
esac
# -------- 4) choose source directory --------
echo
echo "Step 4: Choose directory to archive."
read -r -p "Directory to archive [${DEFAULT_SRC}]: " SRC_DIR
SRC_DIR="${SRC_DIR:-$DEFAULT_SRC}"
if [[ ! -d "$SRC_DIR" ]]; then
echo "Source directory does not exist: $SRC_DIR"
exit 1
fi
# Guard against archiving '/'
if [[ "$SRC_DIR" == "/" ]]; then
echo "WARNING: Archiving '/' is risky (device/proc/sys/run/tmp can cause issues)."
echo "Type OK to *still* proceed (not recommended), or anything else to cancel."
confirm
TAR_EXCLUDES=(--exclude=/proc --exclude=/sys --exclude=/dev --exclude=/run --exclude=/tmp)
else
TAR_EXCLUDES=()
fi
# -------- 5) capacity check (LTO-6, conservative, tolerate permission errors) --------
echo
echo "Step 5: Capacity check (assumes LTO-6 native ~2.5 TB; ignores compression)."
echo "Calculating apparent byte size of ${SRC_DIR} with elevated permissions (du -sb --apparent-size)…"
TMP_DU_ERR="$(mktemp)"
set +e
DU_OUT=$(${SUDO} du -sb --apparent-size -- "$SRC_DIR" 2>"$TMP_DU_ERR")
DU_STATUS=$?
set -e
if (( DU_STATUS != 0 )); then
echo "du reported errors. Showing the first few lines so you can decide:"
head -n 10 "$TMP_DU_ERR" || true
echo
echo "The size estimate may be UNDERCOUNTED because some paths were unreadable."
echo "Type OK to proceed anyway, or anything else to cancel."
confirm
fi
rm -f "$TMP_DU_ERR"
# Parse bytes even if there were warnings (may be empty on severe failures)
SRC_BYTES=$(awk '{print $1}' <<<"${DU_OUT:-0}")
if [[ -z "${SRC_BYTES}" || "${SRC_BYTES}" == "0" ]]; then
echo "WARNING: Could not determine source size reliably."
echo "Type OK to proceed without a capacity check, or anything else to cancel."
confirm
else
echo "Source size (apparent): ${SRC_BYTES} bytes"
echo "LTO-6 native capacity: ${LTO6_NATIVE_BYTES} bytes"
echo "Conservative threshold (90%): ${THRESH} bytes"
if (( SRC_BYTES > THRESH )); then
echo
echo "WARNING: Source appears larger than 90% of LTO-6 native capacity."
echo "Compression *may* help, but to stay safe we will stop unless you explicitly accept the risk."
echo "Type OK to proceed anyway (risk of incomplete write), or anything else to cancel."
confirm
fi
fi
# -------- 6) notify compression --------
echo
echo "Step 6: We will use zstd compression via tar's --use-compress-program."
echo "No fancy flags—kept simple for reliability."
confirm
# -------- 7) write to tape --------
echo
echo "Step 7: Write archive to tape from '${SRC_DIR}'."
echo "We will run:"
cat <<EOF
${SUDO} tar \\
--totals --checkpoint=10000 --checkpoint-action=dot \\
--use-compress-program="zstd" -cvf ${TAPE_DEV} "${TAR_EXCLUDES[@]}" "${SRC_DIR}"
EOF
echo
echo "This streams files to tape with visible progress (dots) and totals."
confirm
${SUDO} tar \
--totals --checkpoint=10000 --checkpoint-action=dot \
--use-compress-program="zstd" -cvf "${TAPE_DEV}" "${TAR_EXCLUDES[@]}" "${SRC_DIR}"
# -------- 8) verify listing --------
echo
echo "Step 8: Rewind and list contents to verify (this may take a long time)."
echo "We will run:"
echo " ${SUDO} mt -f ${TAPE_DEV} rewind"
echo " ${SUDO} tar -tvf ${TAPE_DEV} --use-compress-program=zstd"
confirm
${SUDO} mt -f "${TAPE_DEV}" rewind
${SUDO} tar -tvf "${TAPE_DEV}" --use-compress-program=zstd
echo
echo "All done."
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment