A path with an apostrophe broke the remote shell quoting and a big file outran the connect timeout, so intact backups were reported corrupt and absent
This commit is contained in:
@@ -1656,6 +1656,10 @@
|
|||||||
# HOST1_BACKUP_VERIFY_SHARES / HOST2_BACKUP_VERIFY_SHARES
|
# HOST1_BACKUP_VERIFY_SHARES / HOST2_BACKUP_VERIFY_SHARES
|
||||||
BACKUP_VERIFY_SAMPLE=10 # random files to check per share
|
BACKUP_VERIFY_SAMPLE=10 # random files to check per share
|
||||||
BACKUP_VERIFY_MIN_SIZE=1M # minimum file size to include in sample
|
BACKUP_VERIFY_MIN_SIZE=1M # minimum file size to include in sample
|
||||||
|
BACKUP_VERIFY_MD5_TIMEOUT_MAX=600 # ceiling for one remote checksum. The per-file budget
|
||||||
|
# scales with size (~50MB/s); this caps it. A fixed
|
||||||
|
# connect-sized timeout killed multi-GB checksums and
|
||||||
|
# the empty result was then reported as MISSING.
|
||||||
|
|
||||||
# ━━━ SMART Health ━━━
|
# ━━━ SMART Health ━━━
|
||||||
# Monitors drive SMART attributes — discovers all drives via /dev/sd* and /dev/nvme*.
|
# Monitors drive SMART attributes — discovers all drives via /dev/sd* and /dev/nvme*.
|
||||||
|
|||||||
Executable → Regular
+46
-10
@@ -122,6 +122,7 @@ source "$SCRIPT_DIR/../load_config.sh"
|
|||||||
parse_args "$@"
|
parse_args "$@"
|
||||||
|
|
||||||
SSH_TIMEOUT=15
|
SSH_TIMEOUT=15
|
||||||
|
BACKUP_VERIFY_MD5_TIMEOUT_MAX="${BACKUP_VERIFY_MD5_TIMEOUT_MAX:-600}"
|
||||||
|
|
||||||
# ==============================================================================================
|
# ==============================================================================================
|
||||||
# ━━━ Setup ━━━
|
# ━━━ Setup ━━━
|
||||||
@@ -228,6 +229,7 @@ TOTAL_CHECKED=0
|
|||||||
TOTAL_MATCH=0
|
TOTAL_MATCH=0
|
||||||
TOTAL_MISMATCH=0
|
TOTAL_MISMATCH=0
|
||||||
TOTAL_MISSING=0
|
TOTAL_MISSING=0
|
||||||
|
TOTAL_UNVERIFIED=0
|
||||||
SHARES_WITH_ISSUES=()
|
SHARES_WITH_ISSUES=()
|
||||||
|
|
||||||
for share in "${VERIFY_SHARES[@]}"; do
|
for share in "${VERIFY_SHARES[@]}"; do
|
||||||
@@ -265,6 +267,7 @@ for share in "${VERIFY_SHARES[@]}"; do
|
|||||||
SHARE_MATCH=0
|
SHARE_MATCH=0
|
||||||
SHARE_MISMATCH=0
|
SHARE_MISMATCH=0
|
||||||
SHARE_MISSING=0
|
SHARE_MISSING=0
|
||||||
|
SHARE_UNVERIFIED=0
|
||||||
|
|
||||||
for local_file in "${SAMPLE_FILES[@]}"; do
|
for local_file in "${SAMPLE_FILES[@]}"; do
|
||||||
[[ -z "$local_file" ]] && continue
|
[[ -z "$local_file" ]] && continue
|
||||||
@@ -276,19 +279,49 @@ for share in "${VERIFY_SHARES[@]}"; do
|
|||||||
continue
|
continue
|
||||||
fi
|
fi
|
||||||
|
|
||||||
# Remote checksum via SSH — timeout protected
|
# The path is interpolated into a remote shell command, so it must be escaped for
|
||||||
remote_md5=$(timeout "$SSH_TIMEOUT" ssh -i "$SSH_KEY" \
|
# reuse as one word. A bare '$local_file' inside single quotes breaks on the first
|
||||||
|
# apostrophe — "Frieren - Beyond Journey's End" ended the quote early, md5sum fell
|
||||||
|
# back to reading stdin, and the empty-input hash d41d8cd9... was reported as a
|
||||||
|
# MISMATCH against a file that is byte-identical on the remote.
|
||||||
|
printf -v remote_q '%q' "$local_file"
|
||||||
|
|
||||||
|
# Existence and content are separate questions. Asking them together means a slow
|
||||||
|
# checksum is indistinguishable from an absent file.
|
||||||
|
remote_exists=$(timeout "$SSH_TIMEOUT" ssh -i "$SSH_KEY" \
|
||||||
-o ConnectTimeout="$SSH_TIMEOUT" \
|
-o ConnectTimeout="$SSH_TIMEOUT" \
|
||||||
-o StrictHostKeyChecking=no \
|
-o StrictHostKeyChecking=no \
|
||||||
root@"$REMOTE_SERVER" \
|
root@"$REMOTE_SERVER" \
|
||||||
"md5sum '$local_file' 2>/dev/null | awk '{print \$1}'" 2>/dev/null)
|
"test -f $remote_q && echo yes" 2>/dev/null </dev/null)
|
||||||
|
|
||||||
(( TOTAL_CHECKED++ ))
|
(( TOTAL_CHECKED++ ))
|
||||||
|
|
||||||
if [[ -z "$remote_md5" ]]; then
|
if [[ "$remote_exists" != "yes" ]]; then
|
||||||
warn "$ICON_ERROR MISSING: $(basename "$local_file")"
|
warn "$ICON_ERROR MISSING: $(basename "$local_file")"
|
||||||
(( SHARE_MISSING++ ))
|
(( SHARE_MISSING++ ))
|
||||||
(( TOTAL_MISSING++ ))
|
(( TOTAL_MISSING++ ))
|
||||||
|
continue
|
||||||
|
fi
|
||||||
|
|
||||||
|
# md5sum of a multi-GB file cannot finish inside a connect-sized timeout. Budget by
|
||||||
|
# size — a 5.9GB file needs ~30s and was being killed at 15s, then counted MISSING
|
||||||
|
# even though it was present and correct.
|
||||||
|
local_size=$(stat -c%s "$local_file" 2>/dev/null || echo 0)
|
||||||
|
md5_timeout=$(( local_size / 52428800 + SSH_TIMEOUT ))
|
||||||
|
(( md5_timeout > BACKUP_VERIFY_MD5_TIMEOUT_MAX )) && md5_timeout=$BACKUP_VERIFY_MD5_TIMEOUT_MAX
|
||||||
|
|
||||||
|
remote_md5=$(timeout "$md5_timeout" ssh -i "$SSH_KEY" \
|
||||||
|
-o ConnectTimeout="$SSH_TIMEOUT" \
|
||||||
|
-o StrictHostKeyChecking=no \
|
||||||
|
root@"$REMOTE_SERVER" \
|
||||||
|
"md5sum $remote_q 2>/dev/null | awk '{print \$1}'" 2>/dev/null </dev/null)
|
||||||
|
|
||||||
|
if [[ -z "$remote_md5" ]]; then
|
||||||
|
# Present but unreadable within budget. Reporting this as a mismatch or a miss
|
||||||
|
# would be a claim the run did not earn.
|
||||||
|
warn "$ICON_WARN UNVERIFIED (checksum timed out after ${md5_timeout}s): $(basename "$local_file")"
|
||||||
|
(( SHARE_UNVERIFIED++ ))
|
||||||
|
(( TOTAL_UNVERIFIED++ ))
|
||||||
elif [[ "$local_md5" == "$remote_md5" ]]; then
|
elif [[ "$local_md5" == "$remote_md5" ]]; then
|
||||||
log "MATCH: $(basename "$local_file")"
|
log "MATCH: $(basename "$local_file")"
|
||||||
(( SHARE_MATCH++ ))
|
(( SHARE_MATCH++ ))
|
||||||
@@ -303,8 +336,8 @@ for share in "${VERIFY_SHARES[@]}"; do
|
|||||||
done
|
done
|
||||||
|
|
||||||
# Per-share result — only visible if issues found
|
# Per-share result — only visible if issues found
|
||||||
if [[ "$SHARE_MISMATCH" -gt 0 || "$SHARE_MISSING" -gt 0 ]]; then
|
if [[ "$SHARE_MISMATCH" -gt 0 || "$SHARE_MISSING" -gt 0 || "$SHARE_UNVERIFIED" -gt 0 ]]; then
|
||||||
warn "$SHARE_NAME — match: $SHARE_MATCH missing: $SHARE_MISSING mismatch: $SHARE_MISMATCH"
|
warn "$SHARE_NAME — match: $SHARE_MATCH missing: $SHARE_MISSING mismatch: $SHARE_MISMATCH unverified: $SHARE_UNVERIFIED"
|
||||||
SHARES_WITH_ISSUES+=("$SHARE_NAME")
|
SHARES_WITH_ISSUES+=("$SHARE_NAME")
|
||||||
else
|
else
|
||||||
echo "$SHARE_NAME — all $SHARE_MATCH files match ✅"
|
echo "$SHARE_NAME — all $SHARE_MATCH files match ✅"
|
||||||
@@ -325,10 +358,11 @@ echo "$ICON_VERIFY Checked: $TOTAL_CHECKED files"
|
|||||||
echo "$ICON_TIME Duration: $(format_duration $(( END - START )))"
|
echo "$ICON_TIME Duration: $(format_duration $(( END - START )))"
|
||||||
echo ""
|
echo ""
|
||||||
|
|
||||||
if [[ "$TOTAL_MISMATCH" -gt 0 || "$TOTAL_MISSING" -gt 0 ]]; then
|
if [[ "$TOTAL_MISMATCH" -gt 0 || "$TOTAL_MISSING" -gt 0 || "$TOTAL_UNVERIFIED" -gt 0 ]]; then
|
||||||
echo "$ICON_SUCCESS Match: $TOTAL_MATCH"
|
echo "$ICON_SUCCESS Match: $TOTAL_MATCH"
|
||||||
warn "Missing: $TOTAL_MISSING"
|
warn "Missing: $TOTAL_MISSING"
|
||||||
[[ "$TOTAL_MISMATCH" -gt 0 ]] && echo "$ICON_ERROR Mismatch: $TOTAL_MISMATCH"
|
[[ "$TOTAL_MISMATCH" -gt 0 ]] && echo "$ICON_ERROR Mismatch: $TOTAL_MISMATCH"
|
||||||
|
[[ "$TOTAL_UNVERIFIED" -gt 0 ]] && warn "Unverified: $TOTAL_UNVERIFIED (present, checksum timed out)"
|
||||||
fi
|
fi
|
||||||
|
|
||||||
if [[ "$DRY_RUN" == true ]]; then
|
if [[ "$DRY_RUN" == true ]]; then
|
||||||
@@ -337,6 +371,8 @@ elif [[ "$TOTAL_MISMATCH" -gt 0 || "$TOTAL_MISSING" -gt 0 ]]; then
|
|||||||
echo "$ICON_ERROR Status: ISSUES FOUND — ${#SHARES_WITH_ISSUES[@]} share(s) need attention: ${SHARES_WITH_ISSUES[*]}"
|
echo "$ICON_ERROR Status: ISSUES FOUND — ${#SHARES_WITH_ISSUES[@]} share(s) need attention: ${SHARES_WITH_ISSUES[*]}"
|
||||||
notify "Backup verify FAILED on $(hostname) → $REMOTE_SERVER_NAME — mismatches: $TOTAL_MISMATCH missing: $TOTAL_MISSING — shares: ${SHARES_WITH_ISSUES[*]}" \
|
notify "Backup verify FAILED on $(hostname) → $REMOTE_SERVER_NAME — mismatches: $TOTAL_MISMATCH missing: $TOTAL_MISSING — shares: ${SHARES_WITH_ISSUES[*]}" \
|
||||||
"Backup Verify" "warning"
|
"Backup Verify" "warning"
|
||||||
|
elif [[ "$TOTAL_UNVERIFIED" -gt 0 ]]; then
|
||||||
|
warn "Status: $TOTAL_MATCH verified, $TOTAL_UNVERIFIED could not be checksummed in time — NOT a clean run"
|
||||||
else
|
else
|
||||||
echo "$ICON_DONE Status: all $TOTAL_CHECKED files match across ${#VERIFY_SHARES[@]} shares ✅"
|
echo "$ICON_DONE Status: all $TOTAL_CHECKED files match across ${#VERIFY_SHARES[@]} shares ✅"
|
||||||
fi
|
fi
|
||||||
|
|||||||
Reference in New Issue
Block a user