autopostgresqlbackup 25 KB

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