141 lines
5.2 KiB
Bash
Executable File
141 lines
5.2 KiB
Bash
Executable File
#!/usr/bin/env sh
|
|
|
|
# Copyright (C) 2026 Fluxer Contributors
|
|
#
|
|
# This file is part of Fluxer.
|
|
#
|
|
# Fluxer is free software: you can redistribute it and/or modify
|
|
# it under the terms of the GNU Affero General Public License as published by
|
|
# the Free Software Foundation, either version 3 of the License, or
|
|
# (at your option) any later version.
|
|
#
|
|
# Fluxer 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 Affero General Public License for more details.
|
|
#
|
|
# You should have received a copy of the GNU Affero General Public License
|
|
# along with Fluxer. If not, see <https://www.gnu.org/licenses/>.
|
|
|
|
set -eu
|
|
|
|
TIMESTAMP=$(date +%Y%m%d-%H%M%S)
|
|
BACKUP_NAME="cassandra-backup-${TIMESTAMP}"
|
|
SNAPSHOT_TAG="backup-${TIMESTAMP}"
|
|
DATA_DIR="/var/lib/cassandra/data"
|
|
TEMP_DIR="/tmp/${BACKUP_NAME}"
|
|
AGE_PUBLIC_KEY_FILE="${AGE_PUBLIC_KEY_FILE:-/tmp/age_public_key.txt}"
|
|
ENCRYPTED_BACKUP="${BACKUP_NAME}.tar.age"
|
|
MAX_BACKUP_COUNT=168 # 7 days of hourly backups
|
|
CASSANDRA_HOST="${CASSANDRA_HOST:-cassandra}"
|
|
|
|
# AWS CLI configuration for B2
|
|
export AWS_ACCESS_KEY_ID="${B2_KEY_ID}"
|
|
export AWS_SECRET_ACCESS_KEY="${B2_APPLICATION_KEY}"
|
|
export AWS_DEFAULT_REGION="${B2_REGION}"
|
|
B2_ENDPOINT_URL="https://${B2_ENDPOINT}"
|
|
|
|
echo "[$(date)] Starting Cassandra backup: ${BACKUP_NAME}"
|
|
|
|
# Step 1: Create snapshot
|
|
echo "[$(date)] Creating Cassandra snapshot: ${SNAPSHOT_TAG}"
|
|
if ! nodetool -h "${CASSANDRA_HOST}" snapshot -t "${SNAPSHOT_TAG}"; then
|
|
echo "[$(date)] Error: Failed to create snapshot"
|
|
exit 1
|
|
fi
|
|
echo "[$(date)] Snapshot created successfully"
|
|
|
|
# Step 2: Collect snapshot files
|
|
echo "[$(date)] Collecting snapshot files"
|
|
mkdir -p "${TEMP_DIR}"
|
|
|
|
# Find all snapshot directories and copy to temp location
|
|
find "${DATA_DIR}" -type d -name "${SNAPSHOT_TAG}" | while IFS= read -r snapshot_dir; do
|
|
# Get relative path from data dir
|
|
rel_path=$(dirname "${snapshot_dir#$DATA_DIR/}")
|
|
target_dir="${TEMP_DIR}/${rel_path}"
|
|
mkdir -p "${target_dir}"
|
|
cp -r "${snapshot_dir}" "${target_dir}/"
|
|
done
|
|
|
|
# Copy schema
|
|
echo "[$(date)] Saving schema"
|
|
if [ -n "${CASSANDRA_PASSWORD:-}" ]; then
|
|
if ! cqlsh -u cassandra -p "${CASSANDRA_PASSWORD}" "${CASSANDRA_HOST}" -e "DESC SCHEMA;" 2>/dev/null | sed '/^WARNING:/d' > "${TEMP_DIR}/schema.cql"; then
|
|
echo "Warning: Could not export schema"
|
|
fi
|
|
else
|
|
if ! cqlsh "${CASSANDRA_HOST}" -e "DESC SCHEMA;" 2>/dev/null | sed '/^WARNING:/d' > "${TEMP_DIR}/schema.cql"; then
|
|
echo "Warning: Could not export schema (no password set)"
|
|
fi
|
|
fi
|
|
|
|
# Save cluster topology info
|
|
nodetool -h "${CASSANDRA_HOST}" describecluster > "${TEMP_DIR}/cluster_topology.txt" 2>/dev/null || true
|
|
nodetool -h "${CASSANDRA_HOST}" status > "${TEMP_DIR}/cluster_status.txt" 2>/dev/null || true
|
|
|
|
echo "[$(date)] Snapshot collection completed"
|
|
|
|
# Step 3: Check if encryption is enabled
|
|
if [ ! -f "${AGE_PUBLIC_KEY_FILE}" ]; then
|
|
echo "[$(date)] Warning: Age public key not found - skipping encryption and upload"
|
|
echo "[$(date)] Backup stored locally at: ${TEMP_DIR}"
|
|
|
|
# Clear snapshot from Cassandra
|
|
nodetool -h "${CASSANDRA_HOST}" clearsnapshot -t "${SNAPSHOT_TAG}"
|
|
exit 0
|
|
fi
|
|
|
|
# Step 4: Create tar archive and encrypt with age (streaming)
|
|
echo "[$(date)] Encrypting backup with age..."
|
|
if ! tar -C /tmp -cf - "${BACKUP_NAME}" | \
|
|
age -r "$(cat "${AGE_PUBLIC_KEY_FILE}")" -o "/tmp/${ENCRYPTED_BACKUP}"; then
|
|
echo "[$(date)] Error: Encryption failed"
|
|
rm -rf "${TEMP_DIR}"
|
|
nodetool -h "${CASSANDRA_HOST}" clearsnapshot -t "${SNAPSHOT_TAG}"
|
|
exit 1
|
|
fi
|
|
echo "[$(date)] Encryption completed: ${ENCRYPTED_BACKUP}"
|
|
|
|
# Get file size
|
|
BACKUP_SIZE=$(du -h "/tmp/${ENCRYPTED_BACKUP}" | cut -f1)
|
|
echo "[$(date)] Encrypted backup size: ${BACKUP_SIZE}"
|
|
|
|
# Step 5: Upload encrypted backup to B2
|
|
echo "[$(date)] Uploading encrypted backup to B2..."
|
|
if ! aws s3 cp "/tmp/${ENCRYPTED_BACKUP}" \
|
|
"s3://${B2_BUCKET_NAME}/${ENCRYPTED_BACKUP}" \
|
|
--endpoint-url="${B2_ENDPOINT_URL}"; then
|
|
echo "[$(date)] Error: Upload to B2 failed"
|
|
rm -f "/tmp/${ENCRYPTED_BACKUP}"
|
|
rm -rf "${TEMP_DIR}"
|
|
nodetool -h "${CASSANDRA_HOST}" clearsnapshot -t "${SNAPSHOT_TAG}"
|
|
exit 1
|
|
fi
|
|
echo "[$(date)] Upload completed successfully"
|
|
|
|
# Step 6: Cleanup
|
|
echo "[$(date)] Cleaning up temporary files..."
|
|
rm -f "/tmp/${ENCRYPTED_BACKUP}"
|
|
rm -rf "${TEMP_DIR}"
|
|
|
|
# Clear snapshot from Cassandra
|
|
echo "[$(date)] Clearing snapshot from Cassandra"
|
|
nodetool -h "${CASSANDRA_HOST}" clearsnapshot -t "${SNAPSHOT_TAG}"
|
|
|
|
# Step 7: Purge old backups from B2
|
|
echo "[$(date)] Purging old backups from B2 (keeping last ${MAX_BACKUP_COUNT})..."
|
|
aws s3 ls "s3://${B2_BUCKET_NAME}/" --endpoint-url="${B2_ENDPOINT_URL}" | \
|
|
grep "cassandra-backup-.*\.tar\.age$" | \
|
|
awk '{print $4}' | \
|
|
sort -r | \
|
|
tail -n +$((MAX_BACKUP_COUNT + 1)) | \
|
|
while IFS= read -r old_backup; do
|
|
echo "[$(date)] Deleting old backup: ${old_backup}"
|
|
aws s3 rm "s3://${B2_BUCKET_NAME}/${old_backup}" --endpoint-url="${B2_ENDPOINT_URL}" || true
|
|
done
|
|
|
|
echo "[$(date)] Backup process completed successfully"
|
|
echo "[$(date)] Backup name: ${ENCRYPTED_BACKUP}"
|
|
echo "[$(date)] Backup size: ${BACKUP_SIZE}"
|