autopostgresqlbackup 25 KB

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