From b5860daf11ac353049cb1654b9414a129e5cfb96 Mon Sep 17 00:00:00 2001 From: Jan Tuomi Date: Wed, 13 May 2026 00:13:57 +0300 Subject: Rework --- templates/usr_local_bin_backup.sh.j2 | 365 ----------------------------------- 1 file changed, 365 deletions(-) delete mode 100644 templates/usr_local_bin_backup.sh.j2 (limited to 'templates/usr_local_bin_backup.sh.j2') diff --git a/templates/usr_local_bin_backup.sh.j2 b/templates/usr_local_bin_backup.sh.j2 deleted file mode 100644 index f6a0e2c..0000000 --- a/templates/usr_local_bin_backup.sh.j2 +++ /dev/null @@ -1,365 +0,0 @@ -{% raw %}#!/usr/bin/env bash -set -euo pipefail - -# ===== CONFIG (filled by Ansible) ===== -KEEP_LOCAL=30 -KEEP_REMOTE=10 -KEYFILE="/root/.ssh/backup" -HOST_DIR="backup" -DATASET="{% endraw %}{{ backup_zfs_dataset }}{% raw %}" -BACKUP_EXCLUDE_PROP="{% endraw %}{{ backup_zfs_exclude_property | default('com.pursotin:backup') }}{% raw %}" -USER="{% endraw %}{{ backup_ssh_user }}{% raw %}" -HOST="{% endraw %}{{ backup_ssh_host }}{% raw %}" -EMAIL_TO=root -# ===================================== - -# ----- Globals for notification ----- -STARTED_AT="$(date '+%Y-%m-%dT%H:%M:%S%z')" -FINISHED_AT="" -MESSAGE_LOG="" -BACKUP_NAME="" # e.g. zroot@2025-09-30-12-00-00-0300.enc -BACKUP_SIZE_BYTES="" # numeric bytes - -# ----- Helpers ----- -die() { echo "Error: $*" >&2; exit 1; } - -require_cmds() { - local cmds=("$@") - for c in "${cmds[@]}"; do command -v "$c" >/dev/null 2>&1 || die "Missing command: $c"; done -} - -log_note() { - # Echo to console and append to message buffer - local msg="$1" - echo "$msg" - MESSAGE_LOG+="$msg"$'\n' -} - -timestamp() { - # Replace '+' with '-' so timezone is filename-safe and lexicographically sortable within TZ. - date +%Y-%m-%d-%H-%M-%S%z | tr '+' '-' -} - -humanize_bytes() { - local bytes="$1" - if [[ "${bytes}" =~ ^[0-9]+$ ]]; then - awk -v b="${bytes}" ' - BEGIN { - unit_count = split("B KiB MiB GiB TiB PiB EiB", units, " ") - i = 1 - while (b >= 1024 && i < unit_count) { - b = b / 1024 - i++ - } - if (i == 1) { - printf "%.0f %s\n", b, units[i] - } else { - printf "%.2f %s\n", b, units[i] - } - } - ' - else - echo "unknown" - fi -} - -latest_snapshot_for_dataset() { - # Latest snapshot on the TOP dataset only (newest first). Returns e.g. zroot@2025-09-30-... - zfs list -t snapshot -o name -S creation "$DATASET" 2>/dev/null \ - | awk 'NR==2{print; exit}' -} - -list_top_snapshots_newest_first() { - # Only list snapshots on the top dataset (not children), newest first. - zfs list -t snapshot -o name -S creation "$DATASET" 2>/dev/null \ - | awk 'NR>1{print $1}' -} - -list_excluded_datasets() { - # List datasets (including inherited values) where backup property is explicitly false. - zfs get -r -H -o name,value "${BACKUP_EXCLUDE_PROP}" "${DATASET}" 2>/dev/null \ - | awk 'tolower($2)=="false"{print $1}' -} - -list_remote_backups_sorted() { - # We store files under ${HOST_DIR}/@.enc - echo "ls ${HOST_DIR}" \ - | sftp -q -i "${KEYFILE}" "${USER}@${HOST}" 2>/dev/null \ - | tail -n +2 | sort -} - -# Upload a local file to the storage box path "${HOST_DIR}/" -sftp_put() { - local local_file="$1" - local remote_name="$2" # just the filename, no directory - sftp -i "${KEYFILE}" "${USER}@${HOST}" </dev/null 2>&1; then - echo "Remote directory already exists: ${HOST_DIR}" - log_note "init-remote: verified remote directory '${HOST_DIR}'" - return 0 - fi - - echo "Creating remote directory: ${HOST_DIR}" - set +e - sftp -i "${KEYFILE}" "${USER}@${HOST}" </dev/null 2>&1; then - echo "Remote directory created: ${HOST_DIR}" - log_note "init-remote: created remote directory '${HOST_DIR}'" - else - die "Remote directory ${HOST_DIR} not found after creation" - fi -} - -cmd_snapshot() { - echo "" - echo "[snapshot]" - require_cmds zfs date tr - local ts snap - ts="$(timestamp)" - snap="${DATASET}@${ts}" - echo "Taking recursive snapshot \"${snap}\"" - (set -x; zfs snapshot -r "${snap}") - echo "Recursive snapshot \"${snap}\" created" - log_note "snapshot: created recursive snapshot '${snap}'" -} - -cmd_send_to_remote() { - echo "" - echo "[send-to-remote]" - require_cmds zfs age sftp mktemp stat awk sort - - local snap base target tmp size_bytes ds parent skip - local -a send_args excluded_raw excluded - snap="$(latest_snapshot_for_dataset)" - [[ -n "${snap}" ]] || die "No snapshot found to send. Run 'snapshot' first or ensure the dataset has snapshots." - - base="$(basename "${snap}")" # e.g., zroot@2025-09-30-12-00-00-0300 - target="${base}.enc" - tmp="$(mktemp -t backup_send.XXXXXX)" - trap 'rm -f "${tmp}"' EXIT - - send_args=(-Rvc) - mapfile -t excluded_raw < <(list_excluded_datasets | sort) - excluded=() - for ds in "${excluded_raw[@]}"; do - skip=0 - [[ -n "${ds}" ]] || continue - for parent in "${excluded[@]}"; do - if [[ "${ds}" == "${parent}" || "${ds}" == "${parent}/"* ]]; then - skip=1 - break - fi - done - [[ "${skip}" -eq 1 ]] || excluded+=("${ds}") - done - - if [[ "${#excluded[@]}" -gt 0 ]]; then - echo "Excluding datasets where ${BACKUP_EXCLUDE_PROP}=false:" - for ds in "${excluded[@]}"; do - if [[ "${ds}" == "${DATASET}" ]]; then - die "Top dataset ${DATASET} is excluded via ${BACKUP_EXCLUDE_PROP}=false; refusing to create an empty backup stream." - fi - echo " - ${ds}" - send_args+=(-X "${ds}") - done - log_note "send-to-remote: excluded ${#excluded[@]} dataset(s) using ${BACKUP_EXCLUDE_PROP}=false" - fi - - echo "Creating encrypted replication stream to temp file: ${tmp}" - # -R: recursive hierarchy, -v: progress to stderr, -c: send compressed (keeps on-disk compression) - (set -x; zfs send "${send_args[@]}" "${snap}" | age -e -i "${KEYFILE}" > "${tmp}") - - size_bytes="$(stat -f %z "${tmp}" 2>/dev/null || stat -c %s "${tmp}" 2>/dev/null || echo "unknown")" - echo "Local stream size: ${size_bytes} bytes" - - echo "Uploading via SFTP to ${HOST}:${HOST_DIR}/${target}" - if sftp_put "${tmp}" "${target}"; then - echo "Upload complete" - else - die "SFTP upload failed (does the remote directory '${HOST_DIR}' exist? Run 'init-remote')" - fi - - # Set globals for notify() - BACKUP_NAME="${target}" - BACKUP_SIZE_BYTES="${size_bytes}" - - log_note "send-to-remote: uploaded '${snap}' as '${target}' (${size_bytes} bytes) to ${HOST}:${HOST_DIR}" - rm -f "${tmp}" - trap - EXIT -} - -cmd_prune_remote() { - echo "" - echo "[prune-remote]" - require_cmds sftp awk sort wc - - echo "Fetching remote backup listing from sftp://${HOST}/${HOST_DIR}" - BACKUPS="$(list_remote_backups_sorted || true)" - mapfile -t BACKUP_ARR < <(printf "%s\n" "${BACKUPS}") - local count="${#BACKUP_ARR[@]}" - - if [[ "${count}" -le "${KEEP_REMOTE}" ]]; then - echo "Remote backups (${count}) <= KEEP_REMOTE (${KEEP_REMOTE}); nothing to prune." - log_note "prune-remote: kept ${count} (<= ${KEEP_REMOTE}); no deletions" - return 0 - fi - - echo "Pruning remote backups, keeping latest ${KEEP_REMOTE} (will delete $(("${count}" - "${KEEP_REMOTE}")))" - local to_delete_count=$((count - KEEP_REMOTE)) - local deleted=0 - for ((i=0; i