autopostgresqlbackup 24 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757758759760761762763764765766767768769770771772773774775776777778779780781782783784785786787788789790791792793794795796797798799800801802803804805806807808809810811812813814815816817818819820821822823824825826827828829830831832833834835836837838839840841842843844845846847848849850851852853854855856857858859860861862863864865866867868869870871872873874875876877878879880881882883884885886887888889890891892893894895896897898899900901902903904905906907908909910911912913914915916917918919920921922923924925926927928929930931932933934935936937938939940941942943944945946947948949950951952953954
  1. #!/usr/bin/env bash
  2. # {{{ License and Copyright
  3. # https://github.com/k0lter/autopostgresqlbackup
  4. # Copyright (c) 2005 Aaron Axelsen <axelseaa@amadmax.com>
  5. # 2005 Friedrich Lobenstock <fl@fl.priv.at>
  6. # 2013-2023 Emmanuel Bouthenot <kolter@openics.org>
  7. #
  8. # This program is free software; you can redistribute it and/or modify
  9. # it under the terms of the GNU General Public License as published by
  10. # the Free Software Foundation; either version 2 of the License, or
  11. # (at your option) any later version.
  12. #
  13. # This program is distributed in the hope that it will be useful,
  14. # but WITHOUT ANY WARRANTY; without even the implied warranty of
  15. # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
  16. # GNU General Public License for more details.
  17. #
  18. # You should have received a copy of the GNU General Public License
  19. # along with this program; if not, write to the Free Software
  20. # Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA
  21. # }}}
  22. # {{{ Constants
  23. PATH=/usr/local/bin:/usr/bin:/bin:/usr/local/postgres/bin:/usr/local/pgsql/bin
  24. HOMEPAGE="https://github.com/k0lter/autopostgresqlbackup"
  25. NAME="AutoPostgreSQLBackup" # Script name
  26. VERSION="2.2" # Version Number
  27. DATE="$(date '+%Y-%m-%d_%Hh%Mm')" # Datestamp e.g 2002-09-21
  28. DNOW="$(date '+%u')" # Day number of the week 1 to 7 where 1 represents Monday
  29. DNOM="$(date '+%d')" # Date of the Month e.g. 27
  30. # }}}
  31. # {{{ Variables
  32. # Configuration file or directory
  33. CONFIG="/etc/autodbbackup.d"
  34. # Legacy configuration file path (for backward compatibility)
  35. CONFIG_COMPAT="/etc/default/autopostgresqlbackup"
  36. # Email Address to send errors to. If empty errors are displayed on stdout.
  37. MAILADDR="root"
  38. # Database engines supported: postgresql, mysql
  39. DBENGINE="postgresql"
  40. # Only while using PostgreSQL DB Engine
  41. SU_USERNAME=""
  42. # Username to access the Database server
  43. USERNAME=""
  44. # Password to access then Database server
  45. PASSWORD=""
  46. # Host name (or IP address) of the Database server.
  47. DBHOST="localhost"
  48. # Port of Database server.
  49. DBPORT=""
  50. # List of database(s) names(s) to backup.
  51. DBNAMES="all"
  52. # List of databases to exclude
  53. DBEXCLUDE=""
  54. # Virtual database name used to dump global objects (users, roles, tablespaces)
  55. GLOBALS_OBJECTS="postgres_globals"
  56. # Backup directory
  57. BACKUPDIR="/var/backups"
  58. # Include CREATE DATABASE statement
  59. CREATE_DATABASE="yes"
  60. # Which day do you want weekly backups?
  61. DOWEEKLY=7
  62. # Which day do you want monthly backups?
  63. DOMONTHLY=1
  64. # Backup retention count for daily backups.
  65. BRDAILY=14
  66. # Backup retention count for weekly backups.
  67. BRWEEKLY=5
  68. # Backup retention count for monthly backups.
  69. BRMONTHLY=12
  70. # Compression tool.
  71. COMP="gzip"
  72. # Compression tools options.
  73. COMP_OPTS=
  74. # pg_dump path (pg_dump will be used if empty)
  75. PGDUMP=
  76. # pg_dumpall path (pg_dumpall will be used if empty)
  77. PGDUMPALL=
  78. # Options string for use with all_dump (see pg_dump manual page).
  79. PGDUMP_OPTS=
  80. # Options string for use with pg_dumpall (see pg_dumpall manual page).
  81. PGDUMPALL_OPTS=
  82. # mysql path (mysql will be used if empty)
  83. MY=
  84. # mysqldump path (mysqldump will be used if empty)
  85. MYDUMP=
  86. # Options string for use with mysqldump (see myqldump manual page).
  87. MYDUMP_OPTS=
  88. # Backup files extension
  89. EXT="sql"
  90. # Backup files permission
  91. PERM=600
  92. # Minimum size (in bytes) for a dump/file (compressed or not).
  93. MIN_DUMP_SIZE=256
  94. # Enable encryption (asymmetric) with GnuPG.
  95. ENCRYPTION="no"
  96. # Encryption public key (path to the key)
  97. ENCRYPTION_PUBLIC_KEY=
  98. # Suffix for encyrpted files
  99. ENCRYPTION_SUFFIX=".enc"
  100. # Command or script to execute before backups
  101. PREBACKUP=
  102. # Command or script to execute after backups
  103. POSTBACKUP=
  104. # Debug mode
  105. DEBUG="no"
  106. # Encryption prerequisites
  107. GPG_HOMEDIR=
  108. # Database connection arguments
  109. CONN_ARGS=()
  110. # Hostname
  111. HOSTNAME="$(uname -n)"
  112. if [[ "${HOSTNAME}" != *.* ]]; then
  113. HOSTNAME="$(hostname --fqdn)"
  114. fi
  115. # Return Code
  116. RC=0
  117. # }}}
  118. # {{{ log{,ger,_info,_debug,_warn,_error}()
  119. logger() {
  120. local fd line severity reset color
  121. fd="${1}"
  122. severity="${2}"
  123. reset=
  124. color=
  125. if [ -n "${TERM}" ]; then
  126. reset="\e[0m"
  127. case "${severity}" in
  128. error)
  129. color="\e[0;91m"
  130. ;;
  131. warn)
  132. color="\e[0;93m"
  133. ;;
  134. debug)
  135. color="\e[0;96m"
  136. ;;
  137. *)
  138. color="\e[0;94m"
  139. ;;
  140. esac
  141. fi
  142. while IFS= read -r line ; do
  143. printf "%s|%s|%s\n" "${fd}" "${severity}" "${line}" >> "${LOG_FILE}"
  144. if [ "${DEBUG}" = "yes" ]; then
  145. if [ "${fd}" = "out" ]; then
  146. printf "${color}%6s${reset}|%s\n" "${severity}" "${line}" >&6
  147. elif [ "${fd}" = "err" ]; then
  148. printf "${color}%6s${reset}|%s\n" "${severity}" "${line}" >&7
  149. fi
  150. fi
  151. done
  152. }
  153. log() {
  154. echo "$@" | logger "out" ""
  155. }
  156. log_debug() {
  157. echo "$@" | logger "out" "debug"
  158. }
  159. log_info() {
  160. echo "$@" | logger "out" "info"
  161. }
  162. log_error() {
  163. echo "$@" | logger "err" "error"
  164. }
  165. log_warn() {
  166. echo "$@" | logger "err" "warn"
  167. }
  168. # }}}
  169. # {{{ arg_encode()
  170. arg_encode() {
  171. while read -r arg ; do
  172. echo "${arg}" | sed \
  173. -e 's/%/%25/g' \
  174. -e 's/ /%20/g' \
  175. -e 's/\$/%24/g' \
  176. -e 's/`/%60/g' \
  177. -e 's/"/%22/g' \
  178. -e "s/'/%27/g" \
  179. -e "s/#/%23/g" \
  180. -e 's/=/%3D/g' \
  181. -e 's/\[/%5B/g' \
  182. -e 's/\]/%5D/g' \
  183. -e 's/!/%21/g' \
  184. -e 's/>/%3E/g' \
  185. -e 's/</%3C/g' \
  186. -e 's/|/%7C/g' \
  187. -e 's/;/%3B/g' \
  188. -e 's/{/%7B/g' \
  189. -e 's/}/%7D/g' \
  190. -e 's/(/%28/g' \
  191. -e 's/)/%29/g' \
  192. -e 's/\*/%2A/g' \
  193. -e 's/:/%3A/g' \
  194. -e 's/\?/%3F/g' \
  195. -e 's/&/%26/g' \
  196. -e 's/\//%2F/g'
  197. done
  198. }
  199. # }}}
  200. # {{{ arg_decode()
  201. arg_decode() {
  202. while read -r arg ; do
  203. echo -e "${arg//%/\\x}"
  204. done
  205. }
  206. # }}}
  207. # {{{ gpg_setup()
  208. gpg_setup() {
  209. GPG_HOMEDIR="$(mktemp --quiet --directory -t "${NAME}.XXXXXX")"
  210. chmod 700 "${GPG_HOMEDIR}"
  211. log_debug "With encryption enabled creating a temporary GnuPG home in ${GPG_HOMEDIR}"
  212. gpg --quiet --homedir "${GPG_HOMEDIR}" --quick-gen-key --batch --passphrase-file /dev/null "root@${HOSTNAME}"
  213. }
  214. # }}}
  215. # {{{ encryption()
  216. encryption() {
  217. log_debug "Encrypting using public key ${ENCRYPTION_PUBLIC_KEY}"
  218. gpg --homedir "${GPG_HOMEDIR}" --encrypt --passphrase-file /dev/null --recipient-file "${ENCRYPTION_PUBLIC_KEY}" 2> >(logger "err" "error")
  219. }
  220. # }}}
  221. # {{{ compression()
  222. compression () {
  223. if [ -n "${COMP_OPTS}" ]; then
  224. IFS=" " read -r -a comp_args <<< "${COMP_OPTS}"
  225. log_debug "Compressing using '${COMP} ${comp_args[*]}'"
  226. "${COMP}" "${comp_args[@]}" 2> >(logger "err" "error")
  227. else
  228. log_debug "Compressing using '${COMP}'"
  229. "${COMP}" 2> >(logger "err" "error")
  230. fi
  231. }
  232. # }}}
  233. # {{{ postgresqldb_init()
  234. postgresqldb_init () {
  235. if [ -z "${DBPORT}" ]; then
  236. DBPORT="5432"
  237. fi
  238. CONN_ARGS=(--port "${DBPORT}")
  239. if [ "${DBHOST}" != "localhost" ]; then
  240. CONN_ARGS+=(--host "${DBHOST}")
  241. fi
  242. if [ -z "${USERNAME}" ]; then
  243. USERNAME="postgres"
  244. fi
  245. CONN_ARGS+=(--username "${USERNAME}")
  246. if [ -z "${PGDUMP}" ]; then
  247. PGDUMP="pg_dump"
  248. fi
  249. if [ -z "${PGDUMPALL}" ]; then
  250. PGDUMPALL="pg_dumpall"
  251. fi
  252. }
  253. # }}}
  254. # {{{ postgresqldb_list()
  255. postgresqldb_list () {
  256. local cmd_prog cmd_args raw_dblist dblist dbexcl databases
  257. cmd_prog="psql"
  258. cmd_args=(-t -l -A -F:)
  259. if [ "${#CONN_ARGS[@]}" -gt 0 ]; then
  260. cmd_args+=("${CONN_ARGS[@]}")
  261. fi
  262. log_debug "Running command: ${cmd_prog} ${cmd_args[*]}"
  263. raw_dblist=$(
  264. if [ -n "${SU_USERNAME}" ]; then
  265. if ! su - "${SU_USERNAME}" -c "${cmd_prog} ${cmd_args[*]}" 2> >(logger "err" "error"); then
  266. log_error "Running (as user '${SU_USERNAME}' command '${cmd_prog} ${cmd_args[*]}' has failed"
  267. fi
  268. elif ! "${cmd_prog}" "${cmd_args[@]}" 2> >(logger "err" "error"); then
  269. log_error "Running command '${cmd_prog} ${cmd_args[*]}' has failed"
  270. fi
  271. )
  272. read -r -a dblist <<< "$(
  273. printf "%s\n" "${raw_dblist}" | \
  274. sed -E -n 's/^([^:]+):.+$/\1/p' | \
  275. arg_encode | \
  276. tr '\n' ' '
  277. )"
  278. log_debug "Automatically found databases: ${dblist[*]}"
  279. if [ -n "${DBEXCLUDE}" ]; then
  280. IFS=" " read -r -a dbexcl <<< "${DBEXCLUDE}"
  281. else
  282. dbexcl=()
  283. fi
  284. dbexcl+=(template0)
  285. log_debug "Excluded databases: ${dbexcl[*]}"
  286. mapfile -t databases < <(
  287. comm -23 \
  288. <(IFS=$'\n'; echo "${dblist[*]}" | sort) \
  289. <(IFS=$'\n'; echo "${dbexcl[*]}" | sort) \
  290. )
  291. databases+=("${GLOBALS_OBJECTS}")
  292. log_debug "Database(s) to be backuped: ${databases[*]}"
  293. printf "%s " "${databases[@]}"
  294. }
  295. # }}}
  296. # {{{ postgresqldb_dump()
  297. postgresqldb_dump () {
  298. local db_name cmd_prog cmd_args pg_args
  299. db_name="${1}"
  300. if [ -n "${PGDUMP_OPTS}" ]; then
  301. IFS=" " read -r -a PGDUMP_ARGS <<< "${PGDUMP_OPTS}"
  302. else
  303. PGDUMP_ARGS=()
  304. fi
  305. # pg_dumpall options
  306. if [ -n "${PGDUMPALL_OPTS}" ]; then
  307. IFS=" " read -r -a PGDUMPALL_ARGS <<< "${PGDUMPALL_OPTS}"
  308. else
  309. PGDUMPALL_ARGS=()
  310. fi
  311. if [ "${db_name}" = "${GLOBALS_OBJECTS}" ]; then
  312. cmd_prog="${PGDUMPALL}"
  313. cmd_args=(--globals-only)
  314. pg_args=("${PGDUMPALL_ARGS[@]}")
  315. else
  316. cmd_prog="${PGDUMP}"
  317. if [ -n "${SU_USERNAME}" ]; then
  318. cmd_args=("'${db_name}'")
  319. else
  320. cmd_args=("${db_name}")
  321. fi
  322. pg_args=("${PGDUMP_ARGS[@]}")
  323. if [ "${CREATE_DATABASE}" = "yes" ]; then
  324. pg_args+=(--create)
  325. fi
  326. fi
  327. if [ "${#CONN_ARGS[@]}" -gt 0 ]; then
  328. cmd_args+=("${CONN_ARGS[@]}")
  329. fi
  330. if [ "${#pg_args[@]}" -gt 0 ]; then
  331. cmd_args+=("${pg_args[@]}")
  332. fi
  333. log_debug "Running command: ${cmd_prog} ${cmd_args[*]}"
  334. if [ -n "${SU_USERNAME}" ]; then
  335. if ! su - "${SU_USERNAME}" -c "${cmd_prog} ${cmd_args[*]}" 2> >(logger "err" "error"); then
  336. log_error "Running (as user '${SU_USERNAME}' command '${cmd_prog} ${cmd_args[*]}' has failed"
  337. fi
  338. elif ! "${cmd_prog}" "${cmd_args[@]}" 2> >(logger "err" "error"); then
  339. log_error "Running command '${cmd_prog} ${cmd_args[*]}' has failed"
  340. fi
  341. }
  342. # }}}
  343. # {{{ mysqldb_init()
  344. mysqldb_init () {
  345. CONN_ARGS=()
  346. if [ -z "${DBPORT}" ]; then
  347. DBPORT="3306"
  348. fi
  349. if [ "${DBHOST}" != "localhost" ]; then
  350. CONN_ARGS+=(--host "${DBHOST}")
  351. fi
  352. if [ "${DBPORT}" != "3306" ]; then
  353. CONN_ARGS+=(--port "${DBPORT}")
  354. fi
  355. if [ -z "${USERNAME}" ]; then
  356. USERNAME="root"
  357. fi
  358. CONN_ARGS+=(--user "${USERNAME}")
  359. if [ -n "${PASSWORD}" ]; then
  360. CONN_ARGS+=(--password "${PASSWORD}")
  361. fi
  362. if [ -z "${MY}" ]; then
  363. MY="mysql"
  364. fi
  365. if [ -z "${MYDUMP}" ]; then
  366. MYDUMP="mysqldump"
  367. fi
  368. }
  369. # }}}
  370. # {{{ mysqldb_list()
  371. mysqldb_list () {
  372. local cmd_prog cmd_args raw_dblist dblist dbexcl databases
  373. cmd_prog="${MY}"
  374. cmd_args=(--batch --skip-column-names --execute 'SHOW DATABASES;')
  375. if [ "${#CONN_ARGS[@]}" -gt 0 ]; then
  376. cmd_args+=("${CONN_ARGS[@]}")
  377. fi
  378. log_debug "Running command: ${cmd_prog} ${cmd_args[*]}"
  379. raw_dblist=$(
  380. if ! "${cmd_prog}" "${cmd_args[@]}" 2> >(logger "err" "error"); then
  381. log_error "Running command '${cmd_prog} ${cmd_args[*]}' has failed"
  382. fi
  383. )
  384. read -r -a dblist <<< "$(
  385. printf "%s\n" "${raw_dblist}" | \
  386. arg_encode | \
  387. tr '\n' ' '
  388. )"
  389. log_debug "Automatically found databases: ${dblist[*]}"
  390. if [ -n "${DBEXCLUDE}" ]; then
  391. IFS=" " read -r -a dbexcl <<< "${DBEXCLUDE}"
  392. else
  393. dbexcl=()
  394. fi
  395. dbexcl+=(information_schema performance_schema mysql)
  396. log_debug "Excluded databases: ${dbexcl[*]}"
  397. mapfile -t databases < <(
  398. comm -23 \
  399. <(IFS=$'\n'; echo "${dblist[*]}" | sort) \
  400. <(IFS=$'\n'; echo "${dbexcl[*]}" | sort) \
  401. )
  402. log_debug "Database(s) to be backuped: ${databases[*]}"
  403. printf "%s " "${databases[@]}"
  404. }
  405. # }}}
  406. # {{{ mysqldb_dump()
  407. mysqldb_dump () {
  408. local db_name cmd_prog cmd_args my_args
  409. db_name="${1}"
  410. if [ -n "${MYDUMP_OPTS}" ]; then
  411. IFS=" " read -r -a MYDUMP_ARGS <<< "${MYDUMP_OPTS}"
  412. else
  413. MYDUMP_ARGS=()
  414. fi
  415. cmd_prog="${MYDUMP}"
  416. cmd_args=("${db_name}")
  417. my_args=("${MYDUMP_ARGS[@]}")
  418. my_args+=(--quote-names --events --routines)
  419. if [ "${CREATE_DATABASE}" = "no" ]; then
  420. my_args+=(--databases)
  421. else
  422. my_args+=(--no-create-db)
  423. fi
  424. if [ "${#CONN_ARGS[@]}" -gt 0 ]; then
  425. cmd_args+=("${CONN_ARGS[@]}")
  426. fi
  427. if [ "${#my_args[@]}" -gt 0 ]; then
  428. cmd_args+=("${my_args[@]}")
  429. fi
  430. log_debug "Running command: ${cmd_prog} ${cmd_args[*]}"
  431. if ! "${cmd_prog}" "${cmd_args[@]}" 2> >(logger "err" "error"); then
  432. log_error "Running command '${cmd_prog} ${cmd_args[*]}' has failed"
  433. fi
  434. }
  435. # }}}
  436. # {{{ db_init()
  437. db_init () {
  438. case ${DBENGINE} in
  439. postgresql)
  440. postgresqldb_init
  441. ;;
  442. mysql)
  443. mysqldb_init
  444. ;;
  445. *)
  446. log_error "Unsupported database engine ${DBENGINE}, check DBENGINE configuration parameter"
  447. return 1
  448. ;;
  449. esac
  450. }
  451. # }}}
  452. # {{{ db_list()
  453. db_list () {
  454. case ${DBENGINE} in
  455. postgresql)
  456. postgresqldb_list
  457. ;;
  458. mysql)
  459. mysqldb_list
  460. ;;
  461. *)
  462. log_error "Unsupported database engine ${DBENGINE}, check DBENGINE configuration parameter"
  463. return 1
  464. ;;
  465. esac
  466. }
  467. # }}}
  468. # {{{ db_dump()
  469. db_dump () {
  470. case ${DBENGINE} in
  471. postgresql|mysql)
  472. ${DBENGINE}db_dump "${1}"
  473. ;;
  474. *)
  475. log_error "Unsupported database engine ${DBENGINE}, check DBENGINE configuration parameter"
  476. return 1
  477. ;;
  478. esac
  479. }
  480. # }}}
  481. # {{{ db_purge()
  482. db_purge() {
  483. local dumpdir db when count line
  484. dumpdir="${1}"
  485. db="${2}"
  486. when="${3}"
  487. count="${4}"
  488. # Since version >= 2.0 the dump filename no longer contains the week number
  489. # or the abbreviated month name so in order to be sure to remove the older
  490. # dumps we need to sort the filename on the datetime part (YYYY-MM-DD_HHhMMm)
  491. log_info "Rotating ${count} ${when} backups..."
  492. log_debug "Looking for '${db}_*' in '${dumpdir}/${when}/${db}'"
  493. find "${dumpdir}/${when}/${db}/" -name "${db}_*" | \
  494. sed -E 's/(^.+([0-9]{4}-[0-9]{2}-[0-9]{2}_[0-9]{2}h[0-9]{2}m).*$)/\2 \1/' | \
  495. sort -r | \
  496. sed -E -n 's/\S+ //p' | \
  497. tail -n "+${count}" | \
  498. xargs -L1 rm -fv | \
  499. while IFS= read -r line ; do
  500. log_info "${line}"
  501. done
  502. }
  503. # }}}
  504. # {{{ dump()
  505. dump() {
  506. local db_name dump_file comp_ext
  507. db_name="${1}"
  508. dump_file="${2}"
  509. if [ -n "${COMP}" ]; then
  510. comp_ext=".comp"
  511. case "${COMP}" in
  512. gzip|pigz)
  513. comp_ext=".gz"
  514. ;;
  515. bzip2)
  516. comp_ext=".bz2"
  517. ;;
  518. xz)
  519. comp_ext=".xz"
  520. ;;
  521. zstd)
  522. comp_ext=".zstd"
  523. ;;
  524. esac
  525. dump_file="${dump_file}${comp_ext}"
  526. fi
  527. if [ "${ENCRYPTION}" = "yes" ]; then
  528. dump_file="${dump_file}${ENCRYPTION_SUFFIX}"
  529. fi
  530. if [ -n "${COMP}" ] && [ "${ENCRYPTION}" = "yes" ]; then
  531. log_debug "Dumping (${db_name}) +compress +encrypt to '${dump_file}'"
  532. db_dump "${db_name}" | compression | encryption > "${dump_file}"
  533. elif [ -n "${COMP}" ]; then
  534. log_debug "Dumping (${db_name}) +compress to '${dump_file}'"
  535. db_dump "${db_name}" | compression > "${dump_file}"
  536. elif [ "${ENCRYPTION}" = "yes" ]; then
  537. log_debug "Dumping (${db_name}) +encrypt to '${dump_file}'"
  538. db_dump "${db_name}" | encryption > "${dump_file}"
  539. else
  540. log_debug "Dumping (${db_name}) to '${dump_file}'"
  541. db_dump "${db_name}" > "${dump_file}"
  542. fi
  543. if [ -f "${dump_file}" ]; then
  544. log_debug "Fixing permissions (${PERM}) on '${dump_file}'"
  545. chmod "${PERM}" "${dump_file}"
  546. fsize=$(stat -c '%s' "${dump_file}")
  547. if [ ! -s "${dump_file}" ]; then
  548. log_error "Something went wrong '${dump_file}' is empty"
  549. elif [ "${fsize}" -lt "${MIN_DUMP_SIZE}" ]; then
  550. log_warn "'${dump_file}' (${fsize} bytes) is below the minimum required size (${MIN_DUMP_SIZE} bytes)"
  551. fi
  552. else
  553. log_error "Something went wrong '${dump_file}' does not exists (error during dump?)"
  554. fi
  555. }
  556. # }}}
  557. # {{{ setup()
  558. setup() {
  559. # Using a shared memory filesystem (if available) to avoid
  560. # issues when there is no left space on backup storage
  561. if [ -w "/dev/shm" ]; then
  562. LOG_DIR="/dev/shm"
  563. fi
  564. LOG_PREFIX="${LOG_DIR}/${NAME}_${DBHOST//\//_}-$(date '+%Y-%m-%d_%Hh%Mm')"
  565. LOG_FILE="${LOG_PREFIX}.log"
  566. LOG_REPORT="${LOG_PREFIX}.report"
  567. # Create required directories
  568. if [ ! -e "${BACKUPDIR}" ]; then # Check Backup Directory exists.
  569. mkdir -p "${BACKUPDIR}"
  570. fi
  571. if [ ! -e "${BACKUPDIR}/daily" ]; then # Check Daily Directory exists.
  572. mkdir -p "${BACKUPDIR}/daily"
  573. fi
  574. if [ ! -e "${BACKUPDIR}/weekly" ]; then # Check Weekly Directory exists.
  575. mkdir -p "${BACKUPDIR}/weekly"
  576. fi
  577. if [ ! -e "${BACKUPDIR}/monthly" ]; then # Check Monthly Directory exists.
  578. mkdir -p "${BACKUPDIR}/monthly"
  579. fi
  580. HOST="${DBHOST}:${DBPORT}"
  581. if [ "${DBHOST}" = "localhost" ]; then
  582. HOST="${HOSTNAME}:${DBPORT} (socket)"
  583. fi
  584. }
  585. # }}}
  586. # {{{ cleanup()
  587. cleanup() {
  588. # Cleanup GnuPG home dir
  589. if [ -d "${GPG_HOMEDIR}" ]; then
  590. rm -rf "${GPG_HOMEDIR}"
  591. fi
  592. # Clean up log files
  593. rm -f "${LOG_FILE}" "${LOG_REPORT}"
  594. }
  595. # }}}
  596. # {{{ setup_io()
  597. setup_io() {
  598. exec 6>&1 # Link file descriptor #6 with stdout.
  599. # Saves stdout.
  600. exec 7>&2 # Link file descriptor #7 with stderr.
  601. # Saves stderr.
  602. exec > >( logger "out")
  603. exec 2> >( logger "err")
  604. }
  605. # }}}
  606. # {{{ cleanup_io()
  607. cleanup_io() {
  608. exec 1>&6 6>&- # Restore stdout and close file descriptor #6.
  609. exec 2>&7 7>&- # Restore stdout and close file descriptor #7.
  610. }
  611. # }}}
  612. # {{{ usage()
  613. usage() {
  614. cat <<EOH
  615. USAGE: $(basename "$0") [OPTIONS]
  616. ${NAME} ${VERSION}
  617. A fully automated tool to make periodic backups of PostgreSQL databases.
  618. Options:
  619. -h Shows this help
  620. -d Run in debug mode (no mail sent)
  621. -c Configuration file or directory (default: ${CONFIG})
  622. Note: if ${CONFIG} file or directory does not exists
  623. but ${CONFIG_COMPAT} exists, it will be used
  624. for backward compatibility.
  625. EOH
  626. }
  627. # }}}
  628. # {{{ Process command line arguments
  629. while getopts "hdc:" OPTION ; do
  630. case "${OPTION}" in
  631. h)
  632. usage
  633. exit 0
  634. ;;
  635. d)
  636. DEBUG="yes"
  637. ;;
  638. c)
  639. CONFIG="${OPTARG}"
  640. CONFIG_COMPAT=
  641. ;;
  642. *)
  643. printf "Try \`%s -h\` to check the command line arguments\n" "${NAME}" >&2
  644. exit 1
  645. esac
  646. done
  647. # }}}
  648. # {{{ I/O redirection(s) for logging
  649. setup_io
  650. # }}}
  651. # {{{ Setup runtime settings
  652. setup
  653. # }}}
  654. # {{{ Config file loading
  655. CONFIG_N=0
  656. if [ -d "${CONFIG}" ]; then
  657. CONFIG_N=$(find "${CONFIG}" -type f -iname '*.conf' | wc -l)
  658. fi
  659. if [ -f "${CONFIG_COMPAT}" ]; then
  660. log_debug "Loading config '${CONFIG}' (for backward compatibility)"
  661. # shellcheck source=/dev/null
  662. . "${CONFIG_COMPAT}"
  663. setup
  664. elif [ "${CONFIG_N}" -gt 0 ]; then
  665. CMD="$(readlink -f "${0}")"
  666. CMD_ARGS=()
  667. if [ "${DEBUG}" = "yes" ]; then
  668. CMD_ARGS+=(-d)
  669. fi
  670. cleanup_io
  671. cleanup
  672. find "${CONFIG}" -type f -iname '*.conf' -print0 | \
  673. xargs -0 -L1 "${CMD}" "${CMD_ARGS[@]}" -c
  674. exit $?
  675. elif [ -f "${CONFIG}" ]; then
  676. log_debug "Loading config '${CONFIG}'"
  677. # shellcheck source=/dev/null
  678. . "${CONFIG}"
  679. setup
  680. else
  681. log_error "${NAME}: config file or directory '${CONFIG}' does not exists or directory '${CONFIG}' does not contains any configuration files."
  682. cleanup_io
  683. cleanup
  684. exit 1
  685. fi
  686. # }}}
  687. # {{{ PreBackup
  688. # Run command before we begin
  689. if [ -n "${PREBACKUP}" ]; then
  690. log_info "Prebackup command output:"
  691. ${PREBACKUP} | \
  692. while IFS= read -r line ; do
  693. log " ${line}"
  694. done
  695. fi
  696. # }}}
  697. # {{{ main()
  698. log_info "${NAME} version ${VERSION}"
  699. log_info "Homepage: ${HOMEPAGE}"
  700. log_info "Backup of Database Server (${DBENGINE}) - ${HOST}"
  701. if [ -n "${COMP}" ]; then
  702. if ! command -v "${COMP}" >/dev/null ; then
  703. log_warn "Disabling compression, '${COMP}' command not found"
  704. unset COMP
  705. fi
  706. fi
  707. if [ "${ENCRYPTION}" = "yes" ]; then
  708. if [ ! -s "${ENCRYPTION_PUBLIC_KEY}" ]; then
  709. log_warn "Disabling encryption, '${ENCRYPTION_PUBLIC_KEY}' is empty or does not exists"
  710. ENCRYPTION="no"
  711. elif ! command -v "gpg" >/dev/null ; then
  712. log_warn "Disabling encryption, 'gpg' command not found"
  713. ENCRYPTION="no"
  714. else
  715. gpg_setup
  716. if ! keyinfo="$(gpg --quiet --homedir "${GPG_HOMEDIR}" "${ENCRYPTION_PUBLIC_KEY}" 2>/dev/null)"; then
  717. log_warn "Disabling encryption, key in '${ENCRYPTION_PUBLIC_KEY}' does not seems to be a valid public key"
  718. ENCRYPTION="no"
  719. if command -v "openssl" >/dev/null && openssl x509 -noout -in "${ENCRYPTION_PUBLIC_KEY}" >/dev/null 2>&1; then
  720. log_warn "public key in '${ENCRYPTION_PUBLIC_KEY}' seems to be in PEM format"
  721. log_warn "Encryption using openssl is no longer supported: see ${HOMEPAGE}#openssl-encryption"
  722. fi
  723. else
  724. keyfp="$(echo "${keyinfo}" | sed -E -n 's/^\s*([a-z0-9]+)\s*$/\1/pi')"
  725. keyuid="$(echo "${keyinfo}" | sed -E -n 's/^\s*uid\s+(\S.*)$/\1/pi' | head -n1)"
  726. log_info "Encryption public key is: 0x${keyfp} (${keyuid})"
  727. fi
  728. fi
  729. fi
  730. log_info "Backup Start: $(date)"
  731. if [ "${DNOM}" = "${DOMONTHLY}" ]; then
  732. period="monthly"
  733. rotate="${BRMONTHLY}"
  734. elif [ "${DNOW}" = "${DOWEEKLY}" ]; then
  735. period="weekly"
  736. rotate="${BRWEEKLY}"
  737. else
  738. period="daily"
  739. rotate="${BRDAILY}"
  740. fi
  741. db_init
  742. # If backing up all DBs on the server
  743. if [ "${DBNAMES}" = "all" ]; then
  744. DBNAMES="$(db_list)"
  745. fi
  746. for db_enc in ${DBNAMES} ; do
  747. db="$(echo "${db_enc}" | arg_decode)"
  748. log_info "Backup of Database (${period}) '${db}'"
  749. backupdbdir="${BACKUPDIR}/${period}/${db_enc}"
  750. if [ ! -e "${backupdbdir}" ]; then
  751. log_debug "Creating Backup DB directory '${backupdbdir}'"
  752. mkdir -p "${backupdbdir}"
  753. fi
  754. db_purge "${BACKUPDIR}" "${db_enc}" "${period}" "${rotate}"
  755. backupfile="${backupdbdir}/${db_enc}_${DATE}.${EXT}"
  756. dump "${db}" "${backupfile}"
  757. done
  758. log_info "Backup End: $(date)"
  759. log_info "Total disk space used for ${BACKUPDIR}: $(du -hs "${BACKUPDIR}" | cut -f1)"
  760. # }}}
  761. # {{{ PostBackup
  762. # Run command when we're done
  763. if [ -n "${POSTBACKUP}" ]; then
  764. log_info "Postbackup command output:"
  765. ${POSTBACKUP} | \
  766. while IFS= read -r line ; do
  767. log " ${line}"
  768. done
  769. fi
  770. # }}}
  771. # {{{ cleanup I/O redirections
  772. cleanup_io
  773. # }}}
  774. # {{{ Reporting
  775. if grep -q '^err|' "${LOG_FILE}"; then
  776. RC=1
  777. fi
  778. if [ "${DEBUG}" = "no" ] && [ ${RC} = 1 ]; then
  779. (
  780. printf "*Errors/Warnings* (below) reported during backup on *%s*:\n\n" "${HOST}"
  781. grep '^err|' "${LOG_FILE}" | cut -d '|' -f 3- | \
  782. while IFS= read -r line ; do
  783. printf " | %s\n" "${line}"
  784. done
  785. printf "\n\nFull backup log follows:\n\n"
  786. grep -v '^...|debug|' "${LOG_FILE}" | \
  787. while IFS="|" read -r fd level line ; do
  788. if [ -n "${level}" ]; then
  789. printf "%8s| %s\n" "*${level}*" "${line}"
  790. else
  791. printf "%8s| %s\n" "" "${line}"
  792. fi
  793. done
  794. printf "\nFor more information, try to run %s in debug mode, see \`%s -h\`\n" "${NAME}" "$(basename "$0")"
  795. ) > "${LOG_REPORT}"
  796. if [ -n "${MAILADDR}" ]; then
  797. mail -s "${NAME} issues on ${HOSTNAME}" "${MAILADDR}" < "${LOG_REPORT}"
  798. else
  799. cat "${LOG_REPORT}"
  800. fi
  801. fi
  802. # }}}
  803. # {{{ Cleanup and exit()
  804. cleanup
  805. exit ${RC}
  806. # }}}
  807. # vim: foldmethod=marker foldlevel=0 foldenable