Created
September 12, 2025 10:48
-
-
Save someodd/a15a17b100682d53aa2a9385f41f53f7 to your computer and use it in GitHub Desktop.
My LTO6 archive script.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| #!/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