|
1 | 1 | #!/usr/bin/env sh
|
2 | 2 |
|
| 3 | +# Initialize logging with timestamp |
| 4 | +log() { |
| 5 | + local level="$1" |
| 6 | + local message="$2" |
| 7 | + echo "[$(date '+%Y-%m-%d %H:%M:%S')] ${level}: ${message}" |
| 8 | +} |
| 9 | + |
| 10 | +# Sentry reporting with validation and backwards compatibility |
| 11 | +error_to_sentry() { |
| 12 | + local error_message="$1" |
| 13 | + local db_name="$2" |
| 14 | + local status_code="$3" |
| 15 | + |
| 16 | + # Check if SENTRY_DSN is configured - ensures restore continues |
| 17 | + if [ -z "${SENTRY_DSN:-}" ]; then |
| 18 | + log "DEBUG" "Sentry logging skipped - SENTRY_DSN not configured" |
| 19 | + return 0 |
| 20 | + fi |
| 21 | + |
| 22 | + # Validate SENTRY_DSN format |
| 23 | + echo "${SENTRY_DSN}" | grep -E "^https://[^@]+@[^/]+/[0-9]+$" >/dev/null 2>&1 |
| 24 | + if [ $? -ne 0 ]; then |
| 25 | + log "WARN" "Invalid SENTRY_DSN format - Sentry logging will be skipped" |
| 26 | + return 0 |
| 27 | + fi |
| 28 | + |
| 29 | + # Attempt to send event to Sentry |
| 30 | + if sentry-cli send-event \ |
| 31 | + --message "${error_message}" \ |
| 32 | + --level error \ |
| 33 | + --tag "database:${db_name}" \ |
| 34 | + --tag "status:${status_code}"; then |
| 35 | + log "DEBUG" "Successfully sent error to Sentry - Message: ${error_message}, Database: ${db_name}, Status: ${status_code}" |
| 36 | + else |
| 37 | + log "WARN" "Failed to send error to Sentry, but continuing restore process" |
| 38 | + fi |
| 39 | + |
| 40 | + return 0 |
| 41 | +} |
| 42 | + |
3 | 43 | MYNAME="postgresql-backup-restore"
|
4 | 44 | STATUS=0
|
5 |
| - |
6 |
| -echo "${MYNAME}: restore: Started" |
| 45 | +log "INFO" "${MYNAME}: restore: Started" |
7 | 46 |
|
8 | 47 | # Ensure the database user exists.
|
9 |
| -echo "${MYNAME}: checking for DB user ${DB_USER}" |
| 48 | +log "INFO" "${MYNAME}: checking for DB user ${DB_USER}" |
10 | 49 | result=$(psql --host=${DB_HOST} --username=${DB_ROOTUSER} --command='\du' | grep ${DB_USER})
|
11 | 50 | if [ -z "${result}" ]; then
|
12 | 51 | result=$(psql --host=${DB_HOST} --username=${DB_ROOTUSER} --command="create role ${DB_USER} with login password '${DB_USERPASSWORD}' inherit;")
|
13 | 52 | if [ "${result}" != "CREATE ROLE" ]; then
|
14 |
| - message="Create role command failed: ${result}" |
15 |
| - echo "${MYNAME}: FATAL: ${message}" |
| 53 | + error_message="Create role command failed: ${result}" |
| 54 | + log "ERROR" "${MYNAME}: FATAL: ${error_message}" |
| 55 | + error_to_sentry "${error_message}" "${DB_NAME}" "1" |
16 | 56 | exit 1
|
17 | 57 | fi
|
18 | 58 | fi
|
19 | 59 |
|
20 | 60 | # Delete database if it exists.
|
21 |
| -echo "${MYNAME}: checking for DB ${DB_NAME}" |
| 61 | +log "INFO" "${MYNAME}: checking for DB ${DB_NAME}" |
22 | 62 | result=$(psql --host=${DB_HOST} --username=${DB_ROOTUSER} --list | grep ${DB_NAME})
|
23 | 63 | if [ -z "${result}" ]; then
|
24 |
| - message="Database "${DB_NAME}" on host "${DB_HOST}" does not exist." |
25 |
| - echo "${MYNAME}: INFO: ${message}" |
| 64 | + log "INFO" "${MYNAME}: INFO: Database \"${DB_NAME}\" on host \"${DB_HOST}\" does not exist." |
26 | 65 | else
|
27 |
| - message="finding current owner of DB ${DB_NAME}" |
28 |
| - echo "${MYNAME}: ${message}" |
| 66 | + log "INFO" "${MYNAME}: finding current owner of DB ${DB_NAME}" |
29 | 67 | db_owner=$(psql --host=${DB_HOST} --username=${DB_ROOTUSER} --command='\list' | grep ${DB_NAME} | cut -d '|' -f 2 | sed -e 's/ *//g')
|
30 |
| - message="Database owner is ${db_owner}" |
31 |
| - echo "${MYNAME}: INFO: ${message}" |
| 68 | + log "INFO" "${MYNAME}: INFO: Database owner is ${db_owner}" |
32 | 69 |
|
33 |
| - echo "${MYNAME}: deleting database ${DB_NAME}" |
| 70 | + log "INFO" "${MYNAME}: deleting database ${DB_NAME}" |
34 | 71 | result=$(psql --host=${DB_HOST} --dbname=postgres --username=${db_owner} --command="DROP DATABASE ${DB_NAME};")
|
35 | 72 | if [ "${result}" != "DROP DATABASE" ]; then
|
36 |
| - message="Drop database command failed: ${result}" |
37 |
| - echo "${MYNAME}: FATAL: ${message}" |
| 73 | + error_message="Drop database command failed: ${result}" |
| 74 | + log "ERROR" "${MYNAME}: FATAL: ${error_message}" |
| 75 | + error_to_sentry "${error_message}" "${DB_NAME}" "1" |
38 | 76 | exit 1
|
39 | 77 | fi
|
40 | 78 | fi
|
41 | 79 |
|
42 |
| -echo "${MYNAME}: copying database ${DB_NAME} backup from ${S3_BUCKET}" |
| 80 | +# Download the backup and checksum files |
| 81 | +log "INFO" "${MYNAME}: copying database ${DB_NAME} backup and checksum from ${S3_BUCKET}" |
43 | 82 | start=$(date +%s)
|
| 83 | + |
| 84 | +# Download database backup |
44 | 85 | s3cmd get -f ${S3_BUCKET}/${DB_NAME}.sql.gz /tmp/${DB_NAME}.sql.gz || STATUS=$?
|
45 |
| -end=$(date +%s) |
| 86 | +if [ $STATUS -ne 0 ]; then |
| 87 | + error_message="${MYNAME}: FATAL: Copy backup of ${DB_NAME} from ${S3_BUCKET} returned non-zero status ($STATUS) in $(expr $(date +%s) - ${start}) seconds." |
| 88 | + log "ERROR" "${error_message}" |
| 89 | + error_to_sentry "${error_message}" "${DB_NAME}" "${STATUS}" |
| 90 | + exit $STATUS |
| 91 | +fi |
46 | 92 |
|
| 93 | +# Download checksum file |
| 94 | +s3cmd get -f ${S3_BUCKET}/${DB_NAME}.sql.sha256.gz /tmp/${DB_NAME}.sql.sha256.gz || STATUS=$? |
| 95 | +end=$(date +%s) |
47 | 96 | if [ $STATUS -ne 0 ]; then
|
48 |
| - echo "${MYNAME}: FATAL: Copy backup of ${DB_NAME} from ${S3_BUCKET} returned non-zero status ($STATUS) in $(expr ${end} - ${start}) seconds." |
| 97 | + error_message="${MYNAME}: FATAL: Copy checksum of ${DB_NAME} from ${S3_BUCKET} returned non-zero status ($STATUS) in $(expr ${end} - ${start}) seconds." |
| 98 | + log "ERROR" "${error_message}" |
| 99 | + error_to_sentry "${error_message}" "${DB_NAME}" "${STATUS}" |
49 | 100 | exit $STATUS
|
50 | 101 | else
|
51 |
| - echo "${MYNAME}: Copy backup of ${DB_NAME} from ${S3_BUCKET} completed in $(expr ${end} - ${start}) seconds." |
| 102 | + log "INFO" "${MYNAME}: Copy backup and checksum of ${DB_NAME} from ${S3_BUCKET} completed in $(expr ${end} - ${start}) seconds." |
52 | 103 | fi
|
53 | 104 |
|
54 |
| -echo "${MYNAME}: decompressing backup of ${DB_NAME}" |
| 105 | +# Decompress both files |
| 106 | +log "INFO" "${MYNAME}: decompressing backup and checksum of ${DB_NAME}" |
55 | 107 | start=$(date +%s)
|
| 108 | + |
| 109 | +# Decompress backup file |
56 | 110 | gunzip -f /tmp/${DB_NAME}.sql.gz || STATUS=$?
|
57 |
| -end=$(date +%s) |
| 111 | +if [ $STATUS -ne 0 ]; then |
| 112 | + error_message="${MYNAME}: FATAL: Decompressing backup of ${DB_NAME} returned non-zero status ($STATUS) in $(expr $(date +%s) - ${start}) seconds." |
| 113 | + log "ERROR" "${error_message}" |
| 114 | + error_to_sentry "${error_message}" "${DB_NAME}" "${STATUS}" |
| 115 | + exit $STATUS |
| 116 | +fi |
58 | 117 |
|
| 118 | +# Decompress checksum file |
| 119 | +gunzip -f /tmp/${DB_NAME}.sql.sha256.gz || STATUS=$? |
| 120 | +end=$(date +%s) |
59 | 121 | if [ $STATUS -ne 0 ]; then
|
60 |
| - echo "${MYNAME}: FATAL: Decompressing backup of ${DB_NAME} returned non-zero status ($STATUS) in $(expr ${end} - ${start}) seconds." |
| 122 | + error_message="${MYNAME}: FATAL: Decompressing checksum of ${DB_NAME} returned non-zero status ($STATUS) in $(expr ${end} - ${start}) seconds." |
| 123 | + log "ERROR" "${error_message}" |
| 124 | + error_to_sentry "${error_message}" "${DB_NAME}" "${STATUS}" |
61 | 125 | exit $STATUS
|
62 | 126 | else
|
63 |
| - echo "${MYNAME}: Decompressing backup of ${DB_NAME} completed in $(expr ${end} - ${start}) seconds." |
| 127 | + log "INFO" "${MYNAME}: Decompressing backup and checksum of ${DB_NAME} completed in $(expr ${end} - ${start}) seconds." |
64 | 128 | fi
|
65 | 129 |
|
66 |
| -echo "${MYNAME}: restoring ${DB_NAME}" |
| 130 | +# Validate the checksum |
| 131 | +log "INFO" "${MYNAME}: Validating backup integrity with checksum" |
| 132 | +cd /tmp || { |
| 133 | + error_message="${MYNAME}: FATAL: Failed to change directory to /tmp" |
| 134 | + log "ERROR" "${error_message}" |
| 135 | + error_to_sentry "${error_message}" "${DB_NAME}" "1" |
| 136 | + exit 1 |
| 137 | +} |
| 138 | + |
| 139 | +sha256sum -c "${DB_NAME}.sql.sha256" || { |
| 140 | + error_message="${MYNAME}: FATAL: Checksum validation failed for backup of ${DB_NAME}. The backup may be corrupted or tampered with." |
| 141 | + log "ERROR" "${error_message}" |
| 142 | + error_to_sentry "${error_message}" "${DB_NAME}" "1" |
| 143 | + exit 1 |
| 144 | +} |
| 145 | +log "INFO" "${MYNAME}: Checksum validation successful - backup integrity confirmed" |
| 146 | + |
| 147 | +# Restore the database |
| 148 | +log "INFO" "${MYNAME}: restoring ${DB_NAME}" |
67 | 149 | start=$(date +%s)
|
68 |
| -psql --host=${DB_HOST} --username=${DB_ROOTUSER} --dbname=postgres ${DB_OPTIONS} < /tmp/${DB_NAME}.sql || STATUS=$? |
| 150 | +psql --host=${DB_HOST} --username=${DB_ROOTUSER} --dbname=postgres ${DB_OPTIONS} < /tmp/${DB_NAME}.sql || STATUS=$? |
69 | 151 | end=$(date +%s)
|
70 | 152 |
|
71 | 153 | if [ $STATUS -ne 0 ]; then
|
72 |
| - echo "${MYNAME}: FATAL: Restore of ${DB_NAME} returned non-zero status ($STATUS) in $(expr ${end} - ${start}) seconds." |
| 154 | + error_message="${MYNAME}: FATAL: Restore of ${DB_NAME} returned non-zero status ($STATUS) in $(expr ${end} - ${start}) seconds." |
| 155 | + log "ERROR" "${error_message}" |
| 156 | + error_to_sentry "${error_message}" "${DB_NAME}" "${STATUS}" |
73 | 157 | exit $STATUS
|
74 | 158 | else
|
75 |
| - echo "${MYNAME}: Restore of ${DB_NAME} completed in $(expr ${end} - ${start}) seconds." |
| 159 | + log "INFO" "${MYNAME}: Restore of ${DB_NAME} completed in $(expr ${end} - ${start}) seconds." |
76 | 160 | fi
|
77 | 161 |
|
78 |
| -echo "${MYNAME}: restore: Completed" |
| 162 | +# Verify database restore success |
| 163 | +log "INFO" "${MYNAME}: Verifying database restore success" |
| 164 | +result=$(psql --host=${DB_HOST} --username=${DB_ROOTUSER} --list | grep ${DB_NAME}) |
| 165 | +if [ -z "${result}" ]; then |
| 166 | + error_message="${MYNAME}: FATAL: Database ${DB_NAME} not found after restore attempt." |
| 167 | + log "ERROR" "${error_message}" |
| 168 | + error_to_sentry "${error_message}" "${DB_NAME}" "1" |
| 169 | + exit 1 |
| 170 | +else |
| 171 | + log "INFO" "${MYNAME}: Database ${DB_NAME} successfully restored and verified." |
| 172 | +fi |
| 173 | + |
| 174 | +# Clean up temporary files |
| 175 | +rm -f "/tmp/${DB_NAME}.sql" "/tmp/${DB_NAME}.sql.sha256" |
| 176 | +log "INFO" "${MYNAME}: Temporary files cleaned up" |
| 177 | + |
| 178 | +log "INFO" "${MYNAME}: restore: Completed" |
79 | 179 | exit $STATUS
|
0 commit comments