Compare commits

..

200 commits

Author SHA1 Message Date
Jérémy Lecour 201b22c145
Client - Release 24.05.1
All checks were successful
gitea/evobackup/pipeline/head This commit looks good
gitea/evobackup/pipeline/tag This commit looks good
2024-05-14 08:19:37 +02:00
Jérémy Lecour 0e98c6d4cd
client: fix shell syntax error 2024-05-14 08:18:02 +02:00
Jérémy Lecour f3cfda48f7
Client - Release 24.05
All checks were successful
gitea/evobackup/pipeline/head This commit looks good
gitea/evobackup/pipeline/tag This commit looks good
2024-05-02 10:22:39 +02:00
Jérémy Lecour dc003ecbca
Add previous release dates in CHANGELOG 2024-05-02 10:20:53 +02:00
Jérémy Lecour 0d32cb2cde
evobackupctl: update LIBDIR when copying the template 2024-05-02 10:18:25 +02:00
Jérémy Lecour 96aa0cf7a2
mention the Ansible role in the client README 2024-05-02 10:17:46 +02:00
Jérémy Lecour be6ebdf415
evobackupctl: simplify the program path retrieval 2024-05-02 10:17:22 +02:00
Jérémy Lecour ebad3bb8d9
README formatting (syntax highlighting)
All checks were successful
gitea/evobackup/pipeline/head This commit looks good
gitea/evobackup/pipeline/tag This commit looks good
2024-04-30 10:02:35 +02:00
Jérémy Lecour 3bcc39088b
README formatting
All checks were successful
gitea/evobackup/pipeline/head This commit looks good
2024-04-30 09:59:37 +02:00
Jérémy Lecour ef8c04fe4e
client: release 24.04.1
All checks were successful
gitea/evobackup/pipeline/head This commit looks good
2024-04-30 09:57:53 +02:00
Jérémy Lecour 981420ed23
Add a general README and a client README 2024-04-30 09:56:28 +02:00
Jérémy Lecour 8a6db11ca7
zzz_evobackup: improve the documentation 2024-04-30 09:56:27 +02:00
Jérémy Lecour 939ced46c2
evobackupctl: fix args check 2024-04-30 09:56:25 +02:00
Jérémy Lecour 288d61d6b9
client: release 24.04
All checks were successful
gitea/evobackup/pipeline/head This commit looks good
2024-04-29 17:59:57 +02:00
Jérémy Lecour 1c44943143
Merge branch 'client-functions' 2024-04-29 17:58:51 +02:00
Jérémy Lecour 2cf8264127
evobackupctl: change option name
All checks were successful
gitea/evobackup/pipeline/head This commit looks good
2024-04-29 17:52:35 +02:00
Jérémy Lecour d9674acff0
evobackupctl: add "--init-jail-commands" 2024-04-29 16:29:44 +02:00
Jérémy Lecour 73bd007f6e
evobackupctl: use $ARGS 2024-04-29 16:27:22 +02:00
Jérémy Lecour 00e5384cd1
Use placeholder values for script template 2024-04-29 14:49:41 +02:00
Jérémy Lecour ee111b8c5c
client: update VERSION 2024-04-29 10:59:30 +02:00
Jérémy Lecour 0f1d9148fd
evobackupctl: If no argument is provided, print help and exit 2024-04-29 10:59:09 +02:00
Jérémy Lecour fd856b7423
evobackupctl: update verison command output 2024-04-29 10:48:46 +02:00
Jérémy Lecour 9c8dca7b70
update CHANGELOG 2024-04-08 13:15:22 +02:00
Jérémy Lecour 699ed5a781
use $TMPDIR if available 2024-03-22 11:32:18 +01:00
Jérémy Lecour 859bb800d4 Merge pull request 'fix-tempfiles' (#72) from fix-tempfiles into master
All checks were successful
gitea/evobackup/pipeline/head This commit looks good
Reviewed-on: evolix/evobackup#72
2024-03-22 11:27:34 +01:00
Brice Waegeneire 21533c6fb9 client: Write temporary files in /tmp
All checks were successful
gitea/evobackup/pipeline/head This commit looks good
By default "mktemp" creates a temporary file in the current working directory,
which in the context of Debian's cron is /. To avoid littering /, we add the
option "--tmpdir" that use $TMPDIR or /tmp instead.
2024-03-22 10:05:10 +01:00
Brice Waegeneire 9510546d48 client: Correctly clean up temporary files
The function "build_rsync_main_cmd" is called in a subshell, so it can't
effectively modify it's parent variable "temp_files". To correctly cleanup
those temporary files, we do it when this specific function exits.
2024-03-22 09:56:03 +01:00
Mathieu Trossevin f1d4e6ed9d
fix(includes): Avoid breaking is_btrfs if path doesn't exists
Some checks failed
gitea/evobackup/pipeline/head There was a failure building this commit
This should return with a err code of 1, not result in the entire
command breaking
2024-02-06 15:03:21 +01:00
Jérémy Lecour da1d4356dd
don't exit the whole program if a sync task can't be done 2024-01-26 16:04:00 +01:00
Jérémy Lecour 86ee6406a5
manually source dump libraries 2024-01-15 07:35:27 +01:00
Jérémy Lecour 230ad8ae79
update comments for the new function name 2024-01-11 08:03:09 +01:00
Jérémy Lecour 282bb6f1bc
dynamically include dump files 2024-01-10 15:06:17 +01:00
Jérémy Lecour 45d0205cc1
add pigz compression method 2024-01-09 18:02:22 +01:00
Jérémy Lecour f71dd862bb
Add options to dump_mysql_tabs 2024-01-09 13:30:55 +01:00
Jérémy Lecour 8d9c60b543
Add command logging 2024-01-09 11:24:03 +01:00
Jérémy Lecour 2b4333bded
path_to_str: simplify code 2024-01-09 11:22:18 +01:00
Jérémy Lecour f3e25a7beb
Use env-based shebang for shell scripts 2024-01-09 10:11:31 +01:00
Jérémy Lecour 7a9663781d
add automatic trace mode if TRACE=1 2024-01-09 08:47:18 +01:00
Jérémy Lecour a5ff9199b3
long option name 2024-01-09 08:46:58 +01:00
Jérémy Lecour 257679364d
rewrite (again) mysql functions 2024-01-09 08:45:24 +01:00
Jérémy Lecour 01fb231437
Vagrant definition for manual tests 2024-01-08 23:01:43 +01:00
Jérémy Lecour 0f0b328f27
include main.sh in evobackupctl 2024-01-02 12:11:55 +01:00
Jérémy Lecour 28a0f7a17a
CHANGELOG update (no semver) 2023-12-29 18:26:07 +01:00
Jérémy Lecour ebc20cf4e8
cleanup + changelog 2023-12-29 18:24:27 +01:00
Jérémy Lecour 98bfc5d840
Introducing evobackupctl 2023-12-29 18:15:39 +01:00
Jérémy Lecour ceb12254be
add simple bin script to show version… 2023-12-29 15:17:39 +01:00
Jérémy Lecour 4b71218ae1
simplify cron script 2023-12-29 15:17:08 +01:00
Jérémy Lecour 8c7a963266
more comments 2023-12-29 14:06:28 +01:00
Jérémy Lecour a0f5c31175
whitespaces and comments 2023-12-29 14:05:01 +01:00
Jérémy Lecour b7ce6e1cff
Rewrite mysql dump functions and reorganize code 2023-12-29 13:50:51 +01:00
Jérémy Lecour 5840e8efee
whitespaces 2023-12-29 13:49:08 +01:00
Jérémy Lecour 430e4f52ac
Remove empty error directories at exit 2023-12-29 13:48:02 +01:00
Jérémy Lecour 491c839014
Add note for includes/excludes brace expansion
All checks were successful
gitea/evobackup/pipeline/head This commit looks good
2023-12-28 15:29:39 +01:00
Jérémy Lecour 3268ff0858
Add note for includes/excludes brace expansion 2023-12-28 15:23:52 +01:00
William Hirigoyen 4298da250b Add WARNING and CRITICAL values to default conf
All checks were successful
gitea/evobackup/pipeline/head This commit looks good
2023-12-19 10:26:30 +01:00
William Hirigoyen d359883700 server: fix install.md paths
All checks were successful
gitea/evobackup/pipeline/head This commit looks good
2023-12-19 09:45:25 +01:00
Jérémy Lecour d4ad3b735f
split mkdir and chmod 2023-11-23 09:03:25 +01:00
Jérémy Lecour 32d41a4564
dump_redis: fix error_dir 2023-11-23 09:03:05 +01:00
Bruno TATU 4cd1554780 Correction url dans README.md
All checks were successful
gitea/evobackup/pipeline/head This commit looks good
2023-10-27 11:30:53 +02:00
Jérémy Lecour 68daa1ebf2
deploy-evobackup-beta : fix various issues
* bad permissions
* hard-coded path for cron job
* Ansible syntax for booleans
2023-07-06 10:26:01 +02:00
Jérémy Lecour c3f238b3c6
VERSION="23.07-pre" 2023-07-06 10:25:15 +02:00
Jérémy Lecour f0f177bae9
wording 2023-07-06 09:47:39 +02:00
Jérémy Lecour 03961156a8
split dump_mysql_grants and dump_mysql_variables 2023-07-06 09:42:38 +02:00
Jérémy Lecour c8f0781e75
whitespace 2023-07-06 09:12:33 +02:00
Jérémy Lecour 54a52d7b8f
Exclude amavis virusmails directory from backup
Cf. #981f5118cec40f560c1a743be8d9ef0509305c94
2023-07-06 09:10:30 +02:00
Jérémy Lecour b4ab681061
#71538 : fix Redis dump list in case directory is a symlink
Cf. #fd9bb57f8bd0d2537d82e7e3cf4362e3da0538ea
2023-07-06 09:08:23 +02:00
Jérémy Lecour d71fd5343f
Compress Redis dump 2023-07-06 09:06:21 +02:00
William Hirigoyen 981f5118ce Ajout exclusion /var/lib/amavis/virusmails
All checks were successful
gitea/evobackup/pipeline/head This commit looks good
2023-06-15 11:18:49 +02:00
Jérémy Lecour 5dac827bb5 invert dry-run logic
All checks were successful
gitea/evobackup/pipeline/head This commit looks good
2023-05-03 22:08:17 +02:00
William Hirigoyen 4807dfbc99 Fix missing dump.rdb in Redis dump compression, which caused compression fail.
All checks were successful
gitea/evobackup/pipeline/head This commit looks good
2023-04-19 17:46:34 +02:00
William Hirigoyen fd9bb57f8b #71538 : fix Redis dump list in case directory is a symlink
All checks were successful
gitea/evobackup/pipeline/head This commit looks good
2023-04-17 17:13:23 +02:00
Alexis Ben Miloud--Josselin d62455cd1a Compress Redis dump
All checks were successful
gitea/evobackup/pipeline/head This commit looks good
2023-04-04 15:05:30 +02:00
Jérémy Lecour 7379427051 fix PG dump per base : dump everything and not only the schema 2023-03-31 17:27:20 +02:00
Bruno Tatu 72f5900cf3 On backup les données dans les bases
All checks were successful
gitea/evobackup/pipeline/head This commit looks good
2023-03-30 16:37:36 +02:00
Jérémy Lecour a5b553ab99 dump: global/per-base schema along with data 2023-03-23 08:47:37 +01:00
Jérémy Lecour 81177d63f5 remov dump_mysql_meta 2023-03-23 07:52:00 +01:00
Jérémy Lecour 0a485b3054 dump.sh reorganize backup directories, again 2023-03-23 07:48:41 +01:00
Jérémy Lecour 6f755b94ba dump.sh reorganize backup directories 2023-03-22 22:06:24 +01:00
Jérémy Lecour 158f135c4c Merge branch 'split-lib' into client-functions 2023-03-22 14:46:33 +01:00
Jérémy Lecour 5ac990473e remove monolithic script
All checks were successful
gitea/evobackup/pipeline/head This commit looks good
2023-03-22 14:19:29 +01:00
Jérémy Lecour a6573c6db3 changelog
All checks were successful
gitea/evobackup/pipeline/head This commit looks good
2023-03-22 14:17:42 +01:00
Jérémy Lecour ea054f314c Add some comments 2023-03-22 14:17:10 +01:00
Jérémy Lecour 70e541dd6d zzz_evobackup.sh: LIBDIR="/usr/local/lib/evobackup"
All checks were successful
gitea/evobackup/pipeline/head This commit looks good
2023-03-22 14:11:03 +01:00
Jérémy Lecour 5aeba28d5c utilities.sh: fix line count 2023-03-22 14:10:27 +01:00
Jérémy Lecour 4475ee9af8 dump.sh: improve options handling
* default values,
* reset variable each time
* option masterdata seulement si présente
2023-03-22 14:10:11 +01:00
Jérémy Lecour feafe01692 Delete error directories recursively
All checks were successful
gitea/evobackup/pipeline/head This commit looks good
2023-03-08 09:22:28 +01:00
Jérémy Lecour 1fa1eb7793 Delete README containing dead links 2023-02-27 14:56:45 +01:00
Jérémy Lecour 50f81f2716 Add options for dump functions
All checks were successful
gitea/evobackup/pipeline/head This commit looks good
2023-02-08 22:53:28 +01:00
Jérémy Lecour 2e9eb4a946 variable for script path 2023-02-08 22:51:38 +01:00
Jérémy Lecour 149b5d0e8d comments
All checks were successful
gitea/evobackup/pipeline/head This commit looks good
2023-01-28 21:14:22 +01:00
Jérémy Lecour d532ac83da client: declare variable earlier
All checks were successful
gitea/evobackup/pipeline/head This commit looks good
2023-01-28 16:20:51 +01:00
Jérémy Lecour 767d509390 deploy evobackup beta with configured MAIL and LIBDIR 2023-01-28 16:20:36 +01:00
Jérémy Lecour 70fbab9bb0 Test presence of old config file before trying to delete it
All checks were successful
gitea/evobackup/pipeline/head This commit looks good
2023-01-28 16:07:39 +01:00
Jérémy Lecour c5d82eda68 deployment playbook
All checks were successful
gitea/evobackup/pipeline/head This commit looks good
2023-01-16 14:26:15 +01:00
Jérémy Lecour 0491598c1f hook functions 2023-01-16 14:26:04 +01:00
Jérémy Lecour 2bf4d0dd0f mtree includes must be directories 2023-01-16 14:25:31 +01:00
Jérémy Lecour ed7f9e79ae default value 2023-01-16 13:16:19 +01:00
Jérémy Lecour 7784ba5548 load libraries just before calling main
All checks were successful
gitea/evobackup/pipeline/head This commit looks good
2023-01-16 09:58:17 +01:00
Jérémy Lecour 2ea9614e3c WIP: separate lib and custom code
All checks were successful
gitea/evobackup/pipeline/head This commit looks good
2023-01-15 22:56:03 +01:00
Jérémy Lecour f9aa722ac9 log errors as they happen 2023-01-14 18:51:37 +01:00
Jérémy Lecour 86f0046797 send rsync full log file if it exists 2023-01-13 18:17:54 +01:00
Jérémy Lecour 518fa9d1e7 Store errors in dedicated and persistent directories 2023-01-13 17:17:56 +01:00
Jérémy Lecour 27568820bf revert "declare -a" on array variables 2023-01-13 17:15:33 +01:00
Jérémy Lecour 22814bc5d7 Ldap dump file name 2023-01-13 17:13:06 +01:00
Jérémy Lecour 9665a4ef00 commands arguments (long options and whitespaces 2023-01-13 16:58:24 +01:00
Jérémy Lecour 46c012f5fc skip mtree if disabled or missing 2023-01-13 13:30:57 +01:00
Jérémy Lecour e9cf39ad40 remove PING_BEFORE_SSH 2023-01-13 11:26:41 +01:00
Jérémy Lecour 22ba5ed823 declare bash arrays 2023-01-13 11:26:19 +01:00
Jérémy Lecour 7f4cb78826 shellcheck 2023-01-13 11:17:20 +01:00
Jérémy Lecour 7199ffc64f Add PING_BEFORE_SSH (enabled by default) 2023-01-09 11:45:39 +01:00
Jérémy Lecour 4ff1bc5976 better comments 2023-01-06 16:59:12 +01:00
Jérémy Lecour aeebb815c8 Use bash array for temp_files 2023-01-06 14:45:02 +01:00
Jérémy Lecour c2d08ed80e create and sync mtree files 2023-01-06 14:34:51 +01:00
Jérémy Lecour c3c98b64f2 Use bash array for list of paths to include 2023-01-06 14:33:20 +01:00
Jérémy Lecour 053c339e8f better comments 2023-01-05 13:45:17 +01:00
Jérémy Lecour d75d75cd4c Use an array to build the rsync commands, instead of eval 2023-01-04 23:32:12 +01:00
Jérémy Lecour 58f41963a7 store temp_files in TMPDIR instead of current directory 2023-01-04 14:51:10 +01:00
Jérémy Lecour f6c8d966d7 shellcheck 2023-01-04 14:20:12 +01:00
Jérémy Lecour 82df2b38e9 move variables around to simplify common usage 2023-01-04 14:19:48 +01:00
Jérémy Lecour f5660b1e46 doc 2023-01-04 12:34:17 +01:00
Jérémy Lecour 8d4105cf31 sync only the Rsync stats alongside the canary file 2023-01-04 11:34:42 +01:00
Jérémy Lecour a957498b6f push rsync log file with the canary file 2023-01-04 09:40:26 +01:00
Jérémy Lecour 17c2868fee shellcheck fixes 2023-01-04 09:20:41 +01:00
Jérémy Lecour c3f65a1722 extract variables 2023-01-04 09:19:47 +01:00
Jérémy Lecour 9ee784509d add whitespace to align log outputs with start/stop 2023-01-04 09:16:00 +01:00
Jérémy Lecour b6d50cc921 remove trailing slash in dump_dir 2023-01-04 09:15:26 +01:00
Jérémy Lecour 0235906546 fix dump_file 2023-01-04 09:13:46 +01:00
Jérémy Lecour b1c5b693ee Output error file if size is not null 2023-01-04 09:13:22 +01:00
Jérémy Lecour 65ba8695ad Add documentation comments 2023-01-04 07:45:47 +01:00
Jérémy Lecour c6a89cbc32 Reorder functions 2023-01-04 07:35:26 +01:00
Jérémy Lecour c368c9b11a typo 2023-01-03 23:50:34 +01:00
Jérémy Lecour 910a7398fb error codes 2023-01-03 23:50:23 +01:00
Jérémy Lecour e3c7da32a9 Add logs and error control 2023-01-03 23:30:50 +01:00
Jérémy Lecour 4496ea883a explicit canary file 2023-01-03 09:59:13 +01:00
Jérémy Lecour cb5c842979 Extract functions for each local task
All checks were successful
gitea/evobackup/pipeline/head This commit looks good
2023-01-01 23:04:44 +01:00
Jérémy Lecour 05a62e17b5 client: Release 22.12
All checks were successful
gitea/evobackup/pipeline/head This commit looks good
2022-12-27 13:37:48 +01:00
Jérémy Lecour 8babc64e0d client: log line with more details
All checks were successful
gitea/evobackup/pipeline/head This commit looks good
2022-12-27 13:36:24 +01:00
Jérémy Lecour aa7366ce2e client: separate Rsync for the canary file if the main Rsync has finished without errors
All checks were successful
gitea/evobackup/pipeline/head This commit looks good
2022-12-27 11:56:07 +01:00
Jérémy Lecour ef744f77cf client: No more fallback if dump-server-state is missing
All checks were successful
gitea/evobackup/pipeline/head This commit looks good
2022-12-27 11:11:02 +01:00
Jérémy Lecour 80426c9ba9 whitespaces 2022-12-27 11:09:37 +01:00
Jérémy Lecour b34ee2c3dc client: use long options for readability 2022-12-27 11:09:11 +01:00
Jérémy Lecour 94b470770f client: use sub shells instead of moving around
All checks were successful
gitea/evobackup/pipeline/head This commit looks good
2022-12-27 10:46:53 +01:00
Jérémy Lecour ed0645c9d2 remodel how we build the rsync command (#63)
All checks were successful
gitea/evobackup/pipeline/head This commit looks good
* use a log file for rsync
* build the command argument by argument, without backslashes
* move excludes into a file

Co-authored-by: Jeremy Lecour <jlecour@evolix.fr>
Reviewed-on: evolix/evobackup#63
2022-12-27 10:43:39 +01:00
Jérémy Lecour 140a498e28 client: Only one loop for all redis instances
All checks were successful
gitea/evobackup/pipeline/head This commit looks good
2022-12-27 10:38:55 +01:00
David Prevot bdd3ef7350 CI: Push *.buildinfo too
All checks were successful
gitea/evobackup/pipeline/head This commit looks good
2022-12-22 11:39:42 +01:00
Alexis Ben Miloud--Josselin 6e0ab85c6b client: ignore errors when listing instances
All checks were successful
gitea/evobackup/pipeline/head This commit looks good
2022-12-13 09:53:10 +01:00
David Prevot ff681275e3 CI: Don’t use Drone CI anymore
All checks were successful
gitea/evobackup/pipeline/head This commit looks good
2022-12-12 17:27:36 +01:00
David Prevot 1a892ba002 CI: Actually limit upload to .deb and .changes
All checks were successful
continuous-integration/drone/push Build is passing
gitea/evobackup/pipeline/head This commit looks good
2022-12-12 17:13:28 +01:00
Alexis Ben Miloud--Josselin e3a969f3e2 client: replace rm ** by find -delete
All checks were successful
continuous-integration/drone/push Build is passing
gitea/evobackup/pipeline/head This commit looks good
** has no particular meaning in bash unless shopt globstar is
set (it isn't by default).
2022-12-12 16:06:58 +01:00
David Prevot ca4fe4ffb5 CI: Limit upload to .deb and .changes
All checks were successful
continuous-integration/drone/push Build is passing
gitea/evobackup/pipeline/head This commit looks good
2022-12-12 11:42:19 +01:00
David Prevot 2338c92fe3 CI: Upload to pub2
All checks were successful
continuous-integration/drone/push Build is passing
gitea/evobackup/pipeline/head This commit looks good
2022-12-12 10:56:13 +01:00
David Prevot d2a76dce9c CI: use build-area instead of outside of scope subdirectory 2022-12-12 10:26:06 +01:00
David Prevot 4476802182 Use sbuild during CI
All checks were successful
continuous-integration/drone/push Build is passing
gitea/evobackup/pipeline/head This commit looks good
2022-12-09 13:42:44 +01:00
Mathieu Trossevin 1e35aaa4db
Corrige messages d'erreurs intempestif
All checks were successful
continuous-integration/drone/push Build is passing
gitea/evobackup/pipeline/head This commit looks good
2022-12-08 14:09:37 +01:00
Jérémy Lecour 7b10b56e35 add gobal .gitignore
All checks were successful
continuous-integration/drone/push Build is passing
gitea/evobackup/pipeline/head This commit looks good
continuous-integration/drone/tag Build is passing
gitea/evobackup/pipeline/tag This commit looks good
2022-11-28 15:27:26 +01:00
Jérémy Lecour c8cfbe18aa server: release 22.11
All checks were successful
continuous-integration/drone/push Build is passing
gitea/evobackup/pipeline/head This commit looks good
continuous-integration/drone/tag Build is passing
2022-11-28 15:06:53 +01:00
Jérémy Lecour 777b469485 bkctld check-canary: add tests 2022-11-28 15:06:53 +01:00
Jérémy Lecour d3c75ab94e bkctld-stats: filter active jails and columnize the output 2022-11-28 15:06:53 +01:00
Jérémy Lecour 860b982556 better error message 2022-11-28 15:06:53 +01:00
Jérémy Lecour 8ee2aa3b51 bkctld-check-canary: new subcommand to check canary files and content 2022-11-28 15:06:53 +01:00
Alexis Ben Miloud--Josselin 2b83cd71bc postgresql: change wd before dump
All checks were successful
continuous-integration/drone/push Build is passing
gitea/evobackup/pipeline/head This commit looks good
2022-11-14 11:29:43 +01:00
Jérémy Lecour 88a7907fd3 typo
All checks were successful
continuous-integration/drone/push Build is passing
gitea/evobackup/pipeline/head This commit looks good
continuous-integration/drone/tag Build is passing
2022-10-28 15:48:59 +02:00
Jérémy Lecour 18e0563377 quotes
All checks were successful
continuous-integration/drone/push Build is passing
gitea/evobackup/pipeline/head This commit looks good
2022-10-28 14:02:26 +02:00
Jérémy Lecour e921c92cae More explicit warning for rsync comments/spaces 2022-10-27 18:48:35 +02:00
William Hirigoyen aff5dbba95 Add --no-header option for status command.
All checks were successful
continuous-integration/drone/push Build is passing
gitea/evobackup/pipeline/head This commit looks good
2022-10-27 17:10:14 +02:00
Jérémy Lecour f683691853 client: tolerate absence of mtr or traceroute
All checks were successful
continuous-integration/drone/push Build is passing
gitea/evobackup/pipeline/head This commit looks good
2022-10-13 09:09:01 +02:00
Eric Morino c769a6e823 Update link to the installation documentation
All checks were successful
continuous-integration/drone/push Build is passing
gitea/evobackup/pipeline/head This commit looks good
2022-09-05 14:42:06 +02:00
William Hirigoyen 5739b8afe2 Revert "Fix variable reading in case VAR is specified multiple times in file."
All checks were successful
continuous-integration/drone/push Build is passing
gitea/evobackup/pipeline/head This commit looks good
This reverts commit 89580f2929.
2022-08-26 18:05:14 +02:00
William Hirigoyen 89580f2929 Fix variable reading in case VAR is specified multiple times in file.
All checks were successful
continuous-integration/drone/push Build is passing
gitea/evobackup/pipeline/head This commit looks good
2022-08-26 10:58:48 +02:00
Alexis Ben Miloud--Josselin c80881debf Exclure datadir MongoDB
All checks were successful
continuous-integration/drone/push Build is passing
gitea/evobackup/pipeline/head This commit looks good
2022-08-24 14:40:30 +02:00
Jérémy Lecour e7b7f50d9d server: release 22.07
Some checks are pending
continuous-integration/drone/push Build is passing
gitea.evolix.org/evobackup/pipeline/head Build started...
gitea/evobackup/pipeline/head This commit looks good
gitea/evobackup/pipeline/tag This commit looks good
continuous-integration/drone/tag Build is passing
2022-07-20 14:31:22 +02:00
Jérémy Lecour a3ca2f0f68 check-setup: check minifirewall version only if minifirewall is present
All checks were successful
continuous-integration/drone/push Build is passing
2022-07-20 14:24:12 +02:00
Jérémy Lecour 1891c98f57 check-setup: use findmnt with mountpoint instead of target 2022-07-20 14:23:13 +02:00
Jérémy Lecour aef2637c1e check-setup: get minifirewall version from internal variable
All checks were successful
continuous-integration/drone/push Build is passing
there is no other backward compatible way :
* really old version don't have a know version
* some versions used to display the version on each command, but it is removed.
* the VERSION variable seems to be the most forward-compatible way
2022-07-20 13:43:33 +02:00
David Prevot f0581fee47 CI: Drop .git directory that was not present during first build
All checks were successful
continuous-integration/drone/push Build is passing
gitea.evolix.org on plain agent00/evobackup/pipeline/head This commit looks good
2022-07-11 14:40:38 +02:00
David Prevot ba4629bee7 server/README.md: tfix
All checks were successful
continuous-integration/drone/push Build is passing
gitea.evolix.org on plain agent00/evobackup/pipeline/head This commit looks good
2022-06-30 16:13:29 +02:00
David Prevot 7901fd1950 Jenkins CI: Improve clean up
All checks were successful
continuous-integration/drone/push Build is passing
2022-06-29 14:43:42 +02:00
David Prevot bdb84e809f CI: Don’t rely on /tmp
/tmp is mounted noexec, so can’t be used to run scripts.
2022-06-29 14:03:24 +02:00
David Prevot 3150d48ba5 server: release 22.06
All checks were successful
continuous-integration/drone/push Build is passing
continuous-integration/drone/tag Build is passing
gitea.evolix.org on plain agent00/evobackup/pipeline/head This commit looks good
2022-06-28 16:30:15 +02:00
David Prevot 6c6b64ed34 Update client changelog 2022-06-28 16:26:32 +02:00
David Prevot 604317d4a7 client: tfix (comment) 2022-06-28 16:16:04 +02:00
David Prevot 92ae8a1c7a Jenkins CI: Unconditional clean up 2022-06-28 16:03:14 +02:00
David Prevot 14ca808658 Jenkins CI: Provide .Jenkinsfile as .drone.yml
All checks were successful
continuous-integration/drone/push Build is passing
TODO: upload package when it works.
2022-06-28 15:35:17 +02:00
David Prevot 0bea9f24ab CI: Build Bullseye package under Bullseye 2022-06-28 15:34:11 +02:00
David Prevot f13c6404fd Drone CI: fetch tags 2022-06-28 15:32:29 +02:00
David Prevot e14ab10562 Drone CI: don’t sign packages (yet?) 2022-06-28 15:31:28 +02:00
Brice Waegeneire 1bc62d81cd client: fix pt-show-grants error path
All checks were successful
continuous-integration/drone/push Build is passing
2022-06-20 17:52:26 +02:00
Jérémy Lecour ec638ecb3b move from sh to bash
All checks were successful
continuous-integration/drone/push Build is passing
2022-06-15 14:19:50 +02:00
Jérémy Dubois 0d48a8eec3 update-evobackup-canary : do not use GNU date, for it to be compatible with OpenBSD
All checks were successful
continuous-integration/drone/push Build is passing
2022-06-09 16:16:47 +02:00
Alexis Ben Miloud--Josselin 328763380f [elasticsearch] Écrire la sortie de curl dans log normal
All checks were successful
continuous-integration/drone/push Build is passing
2022-06-07 10:38:04 +02:00
Mathieu Trossevin 36f3cccc92
Ajoute support pour findmnt de Debian 8
All checks were successful
continuous-integration/drone/push Build is passing
2022-06-06 15:21:16 +02:00
Jérémy Lecour 417876cbba zzz_evobackup: replace rsync option --verbose by --itemize-changes
All checks were successful
continuous-integration/drone/push Build is passing
2022-06-02 15:53:00 +02:00
Jérémy Lecour 6c2ba1bc79 zzz_evobackup: do not use rsync compression
All checks were successful
continuous-integration/drone/push Build is passing
2022-06-02 14:56:57 +02:00
Jérémy Lecour 400be16e8a Ajout de liens vers les docs de la partie "client"
All checks were successful
continuous-integration/drone/push Build is passing
2022-06-01 10:14:48 +02:00
Jérémy Lecour 7faedbeab1 remove RSYNC_LOGFILE
All checks were successful
continuous-integration/drone/push Build is passing
2022-06-01 09:48:17 +02:00
Jérémy Lecour 86c01a1075 WIP: extract code into functions 2022-06-01 09:48:17 +02:00
Jérémy Dubois c621324845 Use --dump-dir instead of --backup-dir for OpenBSD too
All checks were successful
continuous-integration/drone/push Build is passing
2022-05-18 09:51:34 +02:00
Jérémy Lecour 89b0636cf6 bkctld-init: create "incs/\<jail\>" directory for jails
All checks were successful
continuous-integration/drone/push Build is passing
2022-05-05 11:43:02 +02:00
Jérémy Lecour f9a295daae fix broken test 2022-05-05 11:39:32 +02:00
Jérémy Lecour ed183fb854 Add canary to zzz_evobackup 2022-05-05 11:24:45 +02:00
32 changed files with 4469 additions and 619 deletions

33
.Jenkinsfile Normal file
View file

@ -0,0 +1,33 @@
pipeline {
agent { label 'sbuild' }
stages {
stage('Build Debian package') {
when {
branch 'debian'
}
steps {
script {
sh 'gbp buildpackage'
}
archiveArtifacts allowEmptyArchive: true, artifacts: 'build-area/*.gz,build-area/*.bz2,build-area/*.xz,build-area/*.deb,build-area/*.dsc,build-area/*.changes,build-area/*.buildinfo,build-area/*.build,build-area/lintian.txt'
}
}
stage('Upload Debian package') {
when {
branch 'debian'
}
steps {
script {
sh 'rsync -avP build-area/bkctld*.deb build-area/bkctld*.changes build-area/bkctld*.buildinfo pub.evolix.org:/srv/upload/'
}
}
}
}
post {
// Clean after build
always {
cleanWs()
}
}
}

View file

@ -1,41 +0,0 @@
kind: pipeline
name: default
steps:
- name: build debian package
image: evolix/gbp:latest
branches:
- debian
commands:
- mk-build-deps --install --remove debian/control
- git clean --force
- gbp buildpackage
volumes:
- name: tmp
path: /tmp
when:
branch:
- debian
- name: upload debian package
image: drillster/drone-rsync
settings:
hosts: ["pub.evolix.net"]
port: 22
user: droneci
key:
from_secret: drone_private_key
target: /home/droneci/bkctld/
source: /tmp/bkctld/
delete: true
volumes:
- name: tmp
path: /tmp
when:
branch:
- debian
volumes:
- name: tmp
host:
path: /tmp

2
.gitignore vendored Normal file
View file

@ -0,0 +1,2 @@
*.swp
.vagrant

20
README.md Normal file
View file

@ -0,0 +1,20 @@
EvoBackup
=========
EvoBackup is a combination of tools to manage backups on Evolix servers.
## The client side
_What you install on the servers you want to backup._
There is a backup script (usually executed by cron or similar), a utility script and some libraries.
More information in the [client README](/evolix/evobackup/src/branch/master/client/README.md).
## The server side
_What you install on the servers that store the backups._
This is also known as `bkctld` : a program to manage SSH servers in chroots to isolate backup destinations, daily copies and data retention.
More information in the [server README](/evolix/evobackup/src/branch/master/server/README.md).

View file

@ -1,8 +1,13 @@
# Changelog
All notable changes to this project will be documented in this file.
The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/),
and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
The format is based on [Keep a Changelog](http://keepachangelog.com/en/1.0.0/).
This project does not follow semantic versioning.
The **major** part of the version is the year
The **minor** part changes is the month
The **patch** part changes is incremented if multiple releases happen the same month
## [Unreleased]
@ -10,18 +15,84 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
### Changed
* Use --dump-dir instead of --backup-dir to supress dump-server-state warning
### Deprecated
### Removed
### Fixed
* Make start_time and stop_time compatible with OpenBSD
### Security
## [22.03]
## [24.05.1] - 2022-05-14
### Fixed
* client: fix shell syntax error
## [24.05] - 2022-05-02
### Added
* evobackupctl: update LIBDIR when copying the template
### Changed
* evobackupctl: simplify the program path retrieval
## [24.04.1] - 2022-04-30
### Fixed
* evobackupctl: quote ARGS variable for options parsing.
## [24.04] - 2022-04-29
### Added
* Vagrant definition for manual tests
### Changed
* split functions into libraries
* add evobackupctl script
* change the "zzz_evobackup" script to a template, easy to copy with evobackupctl
* use env-based shebang for shell scripts
* use $TMPDIR if available
### Removed
* update-evobackup-canary is managed by ansible-roles.git
* deployment by Ansible is managed elsewhere (now in evolix-private.git, later in ansible-roles.git)
### Fixed
* don't exit the whole program if a sync task can't be done
## [22.12] - 2022-12-27
### Changed
* Use --dump-dir instead of --backup-dir to suppress dump-server-state warning
* Do not use rsync compression
* Replace rsync option --verbose by --itemize-changes
* Add canary to zzz_evobackup
* update-evobackup-canary: do not use GNU date, for it to be compatible with OpenBSD
* Add AGPL License and README
* Script now depends on Bash
* tolerate absence of mtr or traceroute
* Only one loop for all Redis instances
* remodel how we build the rsync command
* use sub shells instead of moving around
* Separate Rsync for the canary file if the main Rsync has finished without errors
### Removed
* No more fallback if dump-server-state is missing
### Fixed
* Make start_time and stop_time compatible with OpenBSD
## [22.03] - 2022-04-03
Split client and server parts of the project

166
client/README.md Normal file
View file

@ -0,0 +1,166 @@
EvoBackup — the client side
===========================
_What you install on the servers you want to backup._
## Design
### backup script
The tip of the iceberg is a script (often called `zzz_evobackup` because it is executed by cron at the very end of the _daily_ tasks list).
This is where you setup **what**, **how** and **where** to backup on remote server(s).
There are 2 main phases in the backup :
1. **local tasks**: everything you want to do locally (save state information, dump databases…).
2. **sync tasks**: which data (including what has been prepared in the local phase) you want to send to remote servers.
### libraries
The vast majority of the logic is in libraries, to help maintaining them without having to modify the backup script.
They contain mostly _dump_ functions that are called from the backup script.
Those functions contain a lot of code for logging, options parsing, error management, and some code specific to the dump task.
### utility script
A scripts named `evobackupctl` helps initializing a backup jail on remote servers or install the backup script where you want.
## Install and update
To install, copy these files :
* `lib/*``/usr/local/lib/evobackup`
* `bin/evobackupctl``/usr/local/bin/evobackupctl`
To update, simply overwrite them, since their content should (must?) not be customized locally.
There is also an [evobackup-client](https://gitea.evolix.org/evolix/ansible-roles/src/branch/unstable/evobackup-client) Ansible role to do this (and some other stuff) automatically.
## Usage
### backup script
#### minimal configuration
##### mail notifications
The absolute minimum you must do is set the `MAIL` variable to the email address you want to be notified at.
##### sync tasks
If you want to sync files to a remote server, you have to set the `SERVERS` variable with at least one host and port.
Beware that the default `evolix-system` _sync_ doesn't sync `/home`, `/srv`
If you want to sync files to multiple groups of servers, you can add as many _sync_ sections as you want.
A _sync_ section must contain something like this :
~~~bash
# The name of the "sync" (visible in logs)
SYNC_NAME="evolix-system"
# List of servers
SERVERS=(
host1:port1
host2:port2
)
# List of paths to include in the sync
RSYNC_INCLUDES=(
"${rsync_default_includes[@]}"
/etc
/root
/var
)
# List of paths to exclude from the sync
RSYNC_EXCLUDES=(
"${rsync_default_excludes[@]}"
)
# Actual sync command
sync "${SYNC_NAME}" "SERVERS[@]" "RSYNC_INCLUDES[@]" "RSYNC_EXCLUDES[@]"
~~~
##### local tasks
By default, the `local_tasks()` function only:
* executes [dump-server-state](https://gitea.evolix.org/evolix/dump-server-state) to put have a saved copy of a lot of information about the server
* saves a traceroute to some key network endpoints (using the `dump_traceroute()` function)
You can enable (by uncommenting) as many _dump_ functions as you want.
### advanced customization
Since this is a shell script, you can add any bash-compatible code you want.
If you do so you should read the libraries code to make sure that you don't overwrite existing functions.
##### sync tasks
If you don't want to sync files to any remote servers, you can simply replace the content of the `sync_tasks()` function by a no-op command (`:`).
`RSYNC_INCLUDES` and `RSYNC_EXCLUDES` refer to `${rsync_default_includes[@]}` and `${rsync_default_excludes[@]}` (defined in the `main.sh` library) to simplify the configuration. If you want to precisely customize the lists you can remove them and add you own.
##### local tasks
Existing _dump_ functions (as defined in libraries) are usable as-is, but you can also create your own local custom functions.
You have to define them in the backup script (or in a file that you source from the backup script).
You should prefix their name with `dump_` base your customization on the `dump_custom()` (documented in the backup script) to keep the boilerplate code (for logging, error management…).
You can customize some values inside the `setup_custom()`, like the server's hostname, the notification mail subject…
If you want to source libraries from a different path, you can change the `LIBDIR` variable at the end of the backup script.
### utility tool
The command is `evobackupctl`.
~~~
# evobackupctl --help
evobackupctl helps managing evobackup scripts
Options
-h, --help print this message and exit
-V, --version print version and exit
--jail-init-commands print jail init commands
--copy-template=PATH copy the backup template to PATH
# evobackupctl --version
evobackupctl version 24.04
Copyright 2024 Evolix <info@evolix.fr>,
Jérémy Lecour <jlecour@evolix.fr>.
evobackupctl comes with ABSOLUTELY NO WARRANTY. This is free software,
and you are welcome to redistribute it under certain conditions.
See the GNU General Public License v3.0 for details.
~~~
#### jail init commands
It prints a list of commands you can execute on remote backup servers to configure a backup "jail".
~~~
# evobackupctl --jail-init-commands
Copy-paste those lines on backup server(s) :
----------
SERVER_NAME=example-hostname
SERVER_IP=203.0.113.1
echo 'ssh-ed25519 xxxxxx root@example-hostname' > /root/${SERVER_NAME}.pub
bkctld init ${SERVER_NAME}
bkctld key ${SERVER_NAME} /root/${SERVER_NAME}.pub
bkctld ip ${SERVER_NAME} ${SERVER_IP}
bkctld start ${SERVER_NAME}
bkctld status ${SERVER_NAME}
grep --quiet --extended-regexp "^\s?NODE=" /etc/default/bkctld && bkctld sync ${SERVER_NAME}
----------
~~~
#### copy-template
It copies the backups script template to the path of your choice, nothing more.
~~~
# evobackupctl --copy-template /etc/cron.daily/zzz_evobackup
New evobackup script has been saved to '/etc/cron.daily/zzz_evobackup'.
Remember to customize it (mail notifications, backup servers…).
~~~

49
client/Vagrantfile vendored Normal file
View file

@ -0,0 +1,49 @@
# -*- mode: ruby -*-
# vi: set ft=ruby :
# Load ~/.VagrantFile if exist, permit local config provider
vagrantfile = File.join(Dir.home, ".VagrantFile")
load File.expand_path(vagrantfile) if File.exist?(vagrantfile)
Vagrant.configure("2") do |config|
# Run "vagrant rsync-auto" to sync after each change
config.vm.synced_folder ".", "/vagrant", type: "rsync", disabled: true
config.vm.synced_folder "bin", "/usr/local/bin", type: "rsync"
config.vm.synced_folder "lib", "/usr/local/lib/evobackup", type: "rsync"
config.ssh.shell = "/bin/sh"
config.vm.provider :libvirt do |libvirt|
# libvirt.storage :file, :size => '10G', :device => 'vdb'
libvirt.memory = 1024
libvirt.cpus = 1
end
config_script = <<~SCRIPT
set -e
sed -i -e 's/# en_US.UTF-8 UTF-8/en_US.UTF-8 UTF-8/; s/# fr_FR.UTF-8 UTF-8/fr_FR.UTF-8 UTF-8/' /etc/locale.gen && \
echo 'LANG="fr_FR.UTF-8"'>/etc/default/locale && \
dpkg-reconfigure --frontend=noninteractive locales && \
update-locale LANG=fr_FR.UTF-8
exit 0
SCRIPT
[
{version: "buster"},
{version: "bullseye"},
{version: "bookworm"}
].each do |i|
config.vm.define(i[:version].to_s) do |node|
node.vm.hostname = "evobackup-#{i[:version]}"
node.vm.box = "debian/#{i[:version]}64"
node.vm.provision "config", type: "shell", inline: config_script
node.vm.provision :ansible do |ansible|
ansible.playbook = "vagrant.yml"
end
end
end
end

156
client/bin/evobackupctl Normal file
View file

@ -0,0 +1,156 @@
#!/usr/bin/env bash
# shellcheck disable=SC2155
readonly PROGPATH=$(readlink -m "${0}")
readonly PROGNAME=$(basename "${PROGPATH}")
# shellcheck disable=SC2124
readonly ARGS=$@
# Change this to wherever you install the libraries
readonly LIBDIR="/usr/local/lib/evobackup"
source "${LIBDIR}/main.sh"
show_version() {
cat <<END
${PROGNAME} version ${VERSION}
Copyright 2024 Evolix <info@evolix.fr>,
Jérémy Lecour <jlecour@evolix.fr>.
${PROGNAME} comes with ABSOLUTELY NO WARRANTY. This is free software,
and you are welcome to redistribute it under certain conditions.
See the GNU General Public License v3.0 for details.
END
}
show_help() {
cat <<END
${PROGNAME} helps managing evobackup scripts
Options
-h, --help print this message and exit
-V, --version print version and exit
--jail-init-commands print jail init commands
--copy-template=PATH copy the backup template to PATH
END
}
jail_init_commands() {
if [ ! -f /root/.ssh/id_ed25519.pub ]; then
ssh-keygen -t ed25519 -f /root/.ssh/id_ed25519 -N ''
echo ""
fi
SSH_KEY=$(cat /root/.ssh/id_ed25519.pub)
SERVER_NAME=$(hostname -s)
if [ "$(uname -s)" = "OpenBSD" ]; then
SERVER_IP=$(ifconfig egress | grep "inet " | head -1 | awk '{ print $2}')
else
SERVER_IP=$(curl -4 https://ifconfig.me 2> /dev/null || hostname -I | awk '{ print $1}')
fi
echo "Copy-paste those lines on backup server(s) :"
echo "----------"
echo "SERVER_NAME=${SERVER_NAME}"
echo "SERVER_IP=${SERVER_IP}"
echo "echo '${SSH_KEY}' > /root/\${SERVER_NAME}.pub"
echo "bkctld init \${SERVER_NAME}"
echo "bkctld key \${SERVER_NAME} /root/\${SERVER_NAME}.pub"
echo "bkctld ip \${SERVER_NAME} \${SERVER_IP}"
echo "bkctld start \${SERVER_NAME}"
echo "bkctld status \${SERVER_NAME}"
echo "grep --quiet --extended-regexp \"^\\s?NODE=\" /etc/default/bkctld && bkctld sync \${SERVER_NAME}"
echo "----------"
}
copy_template() {
dest_path=${1}
dest_dir="$(dirname "${dest_path}")"
if [ -e "${dest_path}" ]; then
printf "Path for new evobackup script '%s' already exists.\n" "${dest_path}" >&2
exit 1
elif [ ! -e "${dest_dir}" ]; then
printf "Parent directory '%s' doesn't exist. Create it first.\n" "${dest_dir}" >&2
exit 1
else
if cp "${LIBDIR}/zzz_evobackup.sh" "${dest_path}"; then
chmod 750 "${dest_path}"
# Insert metadata about the template
sed -i "s|@COMMAND@|${PROGPATH} ${ARGS}|" "${dest_path}"
sed -i "s|@DATE@|$(date --iso-8601=seconds)|" "${dest_path}"
sed -i "s|@VERSION@|${VERSION}|" "${dest_path}"
# Make sure that the library directory is correct
sed -i "s|^LIBDIR=.\+|LIBDIR=\"${LIBDIR}\"|" "${dest_path}"
printf "New evobackup script has been saved to '%s'.\n" "${dest_path}"
printf "Remember to customize it (mail notifications, backup servers…).\n"
exit 0
fi
fi
}
main() {
# If no argument is provided, print help and exit
# shellcheck disable=SC2086
if [ -z "${ARGS}" ]; then
show_help
exit 0
fi
# Parse options, based on https://gist.github.com/deshion/10d3cb5f88a21671e17a
while :; do
case ${1:-''} in
-V|--version)
show_version
exit 0
;;
-h|--help)
show_help
exit 0
;;
--jail-init-commands)
jail_init_commands
exit 0
;;
--copy-template)
# copy-template option, with value separated by space
if [ -n "$2" ]; then
copy_template "${2}"
shift
else
printf "'%s' requires a non-empty option argument.\n" "--copy-template" >&2
exit 1
fi
;;
--copy-template=?*)
# copy-template option, with value separated by =
copy_template "${1#*=}"
;;
--copy-template=)
# copy-template option, without value
printf "'%s' requires a non-empty option argument.\n" "--copy-template" >&2
exit 1
;;
--)
# End of all options.
shift
break
;;
-?*|[[:alnum:]]*)
# ignore unknown options
printf "unknown option '%s'.\n" "${1}" >&2
exit 1
;;
*)
# Default case: If no more options then break out of the loop.
break
;;
esac
shift
done
}
main ${ARGS}

View file

@ -0,0 +1,301 @@
#!/usr/bin/env bash
# shellcheck disable=SC2034,SC2317,SC2155
#######################################################################
# Snapshot Elasticsearch data
#
# Arguments:
# --protocol=<http|https> (default: http)
# --cacert=[String] (default: <none>)
# path to the CA certificate to use when using https
# --host=[String] (default: localhost)
# --port=[Integer] (default: 9200)
# --user=[String] (default: <none>)
# --password=[String] (default: <none>)
# --repository=[String] (default: snaprepo)
# --snapshot=[String] (default: snapshot.daily)
#######################################################################
dump_elasticsearch() {
local option_protocol="http"
local option_cacert=""
local option_host="localhost"
local option_port="9200"
local option_user=""
local option_password=""
local option_repository="snaprepo"
local option_snapshot="snapshot.daily"
local option_others=""
# Parse options, based on https://gist.github.com/deshion/10d3cb5f88a21671e17a
while :; do
case ${1:-''} in
--protocol)
# protocol options, with value separated by space
if [ -n "$2" ]; then
option_protocol="${2}"
shift
else
log_error "LOCAL_TASKS - ${FUNCNAME[0]}: '--protocol' requires a non-empty option argument."
exit 1
fi
;;
--protocol=?*)
# protocol options, with value separated by =
option_protocol="${1#*=}"
;;
--protocol=)
# protocol options, without value
log_error "LOCAL_TASKS - ${FUNCNAME[0]}: '--protocol' requires a non-empty option argument."
exit 1
;;
--cacert)
# cacert options, with value separated by space
if [ -n "$2" ]; then
option_cacert="${2}"
shift
else
log_error "LOCAL_TASKS - ${FUNCNAME[0]}: '--cacert' requires a non-empty option argument."
exit 1
fi
;;
--cacert=?*)
# cacert options, with value separated by =
option_cacert="${1#*=}"
;;
--cacert=)
# cacert options, without value
log_error "LOCAL_TASKS - ${FUNCNAME[0]}: '--cacert' requires a non-empty option argument."
exit 1
;;
--host)
# host options, with value separated by space
if [ -n "$2" ]; then
option_host="${2}"
shift
else
log_error "LOCAL_TASKS - ${FUNCNAME[0]}: '--host' requires a non-empty option argument."
exit 1
fi
;;
--host=?*)
# host options, with value separated by =
option_host="${1#*=}"
;;
--host=)
# host options, without value
log_error "LOCAL_TASKS - ${FUNCNAME[0]}: '--host' requires a non-empty option argument."
exit 1
;;
--port)
# port options, with value separated by space
if [ -n "$2" ]; then
option_port="${2}"
shift
else
log_error "LOCAL_TASKS - ${FUNCNAME[0]}: '--port' requires a non-empty option argument."
exit 1
fi
;;
--port=?*)
# port options, with value separated by =
option_port="${1#*=}"
;;
--port=)
# port options, without value
log_error "LOCAL_TASKS - ${FUNCNAME[0]}: '--port' requires a non-empty option argument."
exit 1
;;
--user)
# user options, with value separated by space
if [ -n "$2" ]; then
option_user="${2}"
shift
else
log_error "LOCAL_TASKS - ${FUNCNAME[0]}: '--user' requires a non-empty option argument."
exit 1
fi
;;
--user=?*)
# user options, with value separated by =
option_user="${1#*=}"
;;
--user=)
# user options, without value
log_error "LOCAL_TASKS - ${FUNCNAME[0]}: '--user' requires a non-empty option argument."
exit 1
;;
--password)
# password options, with value separated by space
if [ -n "$2" ]; then
option_password="${2}"
shift
else
log_error "LOCAL_TASKS - ${FUNCNAME[0]}: '--password' requires a non-empty option argument."
exit 1
fi
;;
--password=?*)
# password options, with value separated by =
option_password="${1#*=}"
;;
--password=)
# password options, without value
log_error "LOCAL_TASKS - ${FUNCNAME[0]}: '--password' requires a non-empty option argument."
exit 1
;;
--repository)
# repository options, with value separated by space
if [ -n "$2" ]; then
option_repository="${2}"
shift
else
log_error "LOCAL_TASKS - ${FUNCNAME[0]}: '--repository' requires a non-empty option argument."
exit 1
fi
;;
--repository=?*)
# repository options, with value separated by =
option_repository="${1#*=}"
;;
--repository=)
# repository options, without value
log_error "LOCAL_TASKS - ${FUNCNAME[0]}: '--repository' requires a non-empty option argument."
exit 1
;;
--snapshot)
# snapshot options, with value separated by space
if [ -n "$2" ]; then
option_snapshot="${2}"
shift
else
log_error "LOCAL_TASKS - ${FUNCNAME[0]}: '--snapshot' requires a non-empty option argument."
exit 1
fi
;;
--snapshot=?*)
# snapshot options, with value separated by =
option_snapshot="${1#*=}"
;;
--snapshot=)
# snapshot options, without value
log_error "LOCAL_TASKS - ${FUNCNAME[0]}: '--snapshot' requires a non-empty option argument."
exit 1
;;
--)
# End of all options.
shift
option_others=${*}
break
;;
-?*|[[:alnum:]]*)
# ignore unknown options
log_error "LOCAL_TASKS - ${FUNCNAME[0]}: unknown option '${1}' (ignored)"
;;
*)
# Default case: If no more options then break out of the loop.
break
;;
esac
shift
done
# Use the default Elasticsearch CA certificate when using HTTPS, if not specified directly
local default_cacert="/etc/elasticsearch/certs/http_ca.crt"
if [ "${option_protocol}" = "https" ] && [ -z "${option_cacert}" ] && [ -f "${default_cacert}" ]; then
option_cacert="${default_cacert}"
fi
local errors_dir="${ERRORS_DIR}/elasticsearch-${option_repository}-${option_snapshot}"
rm -rf "${errors_dir}"
mkdir -p "${errors_dir}"
# No need to change recursively, the top directory is enough
chmod 700 "${errors_dir}"
log "LOCAL_TASKS - ${FUNCNAME[0]}: start ${option_snapshot}"
## Take a snapshot as a backup.
## Warning: You need to have a path.repo configured.
## See: https://wiki.evolix.org/HowtoElasticsearch#snapshots-et-sauvegardes
local base_url="${option_protocol}://${option_host}:${option_port}"
local repository_url="${base_url}/_snapshot/${option_repository}"
local snapshot_url="${repository_url}/${option_snapshot}"
# Verify snapshot repository
local error_file="${errors_dir}/verify.err"
declare -a connect_options
connect_options=()
if [ -n "${option_cacert}" ]; then
connect_options+=(--cacert "${option_cacert}")
fi
if [ -n "${option_user}" ] || [ -n "${option_password}" ]; then
local connect_options+=("--user ${option_user}:${option_password}")
fi
if [ -n "${option_others}" ]; then
# word splitting is deliberate here
# shellcheck disable=SC2206
connect_options+=(${option_others})
fi
# Add the http return code at the end of the output
connect_options+=(--write-out '%{http_code}\n')
connect_options+=(--silent)
declare -a dump_options
dump_options=()
dump_options+=(--request POST)
dump_cmd="curl ${connect_options[*]} ${dump_options[*]} ${repository_url}/_verify?pretty"
log "LOCAL_TASKS - ${FUNCNAME[0]}: ${dump_cmd}"
${dump_cmd} > "${error_file}"
# test if the last line of the log file is "200"
tail -n 1 "${error_file}" | grep --quiet "^200$" "${error_file}"
local last_rc=$?
# shellcheck disable=SC2086
if [ ${last_rc} -ne 0 ]; then
log_error "LOCAL_TASKS - ${FUNCNAME[0]}: repository verification returned an error ${last_rc}" "${error_file}"
GLOBAL_RC=${E_DUMPFAILED}
else
rm -f "${error_file}"
# Delete snapshot
declare -a dump_options
dump_options=()
dump_options+=(--request DELETE)
dump_cmd="curl ${connect_options[*]} ${dump_options[*]} ${snapshot_url}"
log "LOCAL_TASKS - ${FUNCNAME[0]}: ${dump_cmd}"
${dump_cmd} > /dev/null
# Create snapshot
local error_file="${errors_dir}/create.err"
declare -a dump_options
dump_options=()
dump_options+=(--request PUT)
dump_cmd="curl ${connect_options[*]} ${dump_options[*]} ${snapshot_url}?wait_for_completion=true"
log "LOCAL_TASKS - ${FUNCNAME[0]}: ${dump_cmd}"
${dump_cmd} > "${error_file}"
# test if the last line of the log file is "200"
tail -n 1 "${error_file}" | grep --quiet "^200$" "${error_file}"
local last_rc=$?
# shellcheck disable=SC2086
if [ ${last_rc} -ne 0 ]; then
log_error "LOCAL_TASKS - ${FUNCNAME[0]}: curl returned an error ${last_rc}" "${error_file}"
GLOBAL_RC=${E_DUMPFAILED}
else
rm -f "${error_file}"
fi
fi
log "LOCAL_TASKS - ${FUNCNAME[0]}: stop ${option_snapshot}"
}

559
client/lib/dump/misc.sh Normal file
View file

@ -0,0 +1,559 @@
#!/usr/bin/env bash
# shellcheck disable=SC2034,SC2317,SC2155
#######################################################################
# Dump LDAP files (config, data, all)
#
# Arguments: <none>
#######################################################################
dump_ldap() {
## OpenLDAP : example with slapcat
local dump_dir="${LOCAL_BACKUP_DIR}/ldap"
rm -rf "${dump_dir}"
mkdir -p "${dump_dir}"
chmod 700 "${dump_dir}"
log "LOCAL_TASKS - ${FUNCNAME[0]}: start ${FUNCNAME[0]} to ${dump_dir}"
dump_cmd="slapcat -n 0 -l ${dump_dir}/config.bak"
log "LOCAL_TASKS - ${FUNCNAME[0]}: ${dump_cmd}"
${dump_cmd}
dump_cmd="slapcat -n 1 -l ${dump_dir}/data.bak"
log "LOCAL_TASKS - ${FUNCNAME[0]}: ${dump_cmd}"
${dump_cmd}
dump_cmd="slapcat -l ${dump_dir}/all.bak"
log "LOCAL_TASKS - ${FUNCNAME[0]}: ${dump_cmd}"
${dump_cmd}
log "LOCAL_TASKS - ${FUNCNAME[0]}: stop ${FUNCNAME[0]}"
}
#######################################################################
# Copy dump file of Redis instances
#
# Arguments:
# --instances=[Integer] (default: all)
#######################################################################
dump_redis() {
all_instances=$(find /var/lib/ -mindepth 1 -maxdepth 1 '(' -type d -o -type l ')' -name 'redis*')
local option_instances=""
# Parse options, based on https://gist.github.com/deshion/10d3cb5f88a21671e17a
while :; do
case ${1:-''} in
--instances)
# instances options, with key and value separated by space
if [ -n "$2" ]; then
if [ "${2}" == "all" ]; then
read -a option_instances <<< "${all_instances}"
else
IFS="," read -a option_instances <<< "${2}"
fi
shift
else
log_error "LOCAL_TASKS - ${FUNCNAME[0]}: '--instances' requires a non-empty option argument."
exit 1
fi
;;
--instances=?*)
# instances options, with key and value separated by =
if [ "${1#*=}" == "all" ]; then
read -a option_instances <<< "${all_instances}"
else
IFS="," read -a option_instances <<< "${1#*=}"
fi
;;
--instances=)
# instances options, without value
log_error "LOCAL_TASKS - ${FUNCNAME[0]}: '--instances' requires a non-empty option argument."
exit 1
;;
--)
# End of all options.
shift
break
;;
-?*|[[:alnum:]]*)
# ignore unknown options
log_error "LOCAL_TASKS - ${FUNCNAME[0]}: unknown option '${1}' (ignored)"
;;
*)
# Default case: If no more options then break out of the loop.
break
;;
esac
shift
done
for instance in "${option_instances[@]}"; do
name=$(basename "${instance}")
local dump_dir="${LOCAL_BACKUP_DIR}/${name}"
local errors_dir=$(errors_dir_from_dump_dir "${dump_dir}")
rm -rf "${dump_dir}" "${errors_dir}"
mkdir -p "${dump_dir}" "${errors_dir}"
# No need to change recursively, the top directory is enough
chmod 700 "${dump_dir}" "${errors_dir}"
if [ -f "${instance}/dump.rdb" ]; then
local error_file="${errors_dir}/${name}.err"
log "LOCAL_TASKS - ${FUNCNAME[0]}: start ${dump_dir}"
# Copy the Redis database
dump_cmd="cp -a ${instance}/dump.rdb ${dump_dir}/dump.rdb"
log "LOCAL_TASKS - ${FUNCNAME[0]}: ${dump_cmd}"
${dump_cmd} 2> "${error_file}"
local last_rc=$?
# shellcheck disable=SC2086
if [ ${last_rc} -ne 0 ]; then
log_error "LOCAL_TASKS - ${FUNCNAME[0]}: cp ${instance}/dump.rdb to ${dump_dir} returned an error ${last_rc}" "${error_file}"
GLOBAL_RC=${E_DUMPFAILED}
else
rm -f "${error_file}"
fi
# Compress the Redis database
dump_cmd="gzip ${dump_dir}/dump.rdb"
log "LOCAL_TASKS - ${FUNCNAME[0]}: ${dump_cmd}"
${dump_cmd}
local last_rc=$?
# shellcheck disable=SC2086
if [ ${last_rc} -ne 0 ]; then
log_error "LOCAL_TASKS - ${FUNCNAME[0]}: gzip ${dump_dir}/dump.rdb returned an error ${last_rc}" "${error_file}"
GLOBAL_RC=${E_DUMPFAILED}
else
rm -f "${error_file}"
fi
log "LOCAL_TASKS - ${FUNCNAME[0]}: stop ${dump_dir}"
else
log_error "LOCAL_TASKS - ${FUNCNAME[0]}: '${instance}/dump.rdb' not found."
fi
done
}
#######################################################################
# Dump all collections of a MongoDB database
# using a custom authentication, instead of /etc/mysql/debian.cnf
#
# Arguments:
# --port=[String] (default: <blank>)
# --user=[String] (default: <blank>)
# --password=[String] (default: <blank>)
# --dump-label=[String] (default: "default")
# used as suffix of the dump dir to differenciate multiple instances
# Other options after -- are passed as-is to mongodump
#
# don't forget to create use with read-only access
# > use admin
# > db.createUser( { user: "mongobackup", pwd: "PASS", roles: [ "backup", ] } )
#######################################################################
dump_mongodb() {
local option_port=""
local option_user=""
local option_password=""
local option_dump_label=""
local option_others=""
# Parse options, based on https://gist.github.com/deshion/10d3cb5f88a21671e17a
while :; do
case ${1:-''} in
--port)
# port options, with value separated by space
if [ -n "$2" ]; then
option_port="${2}"
shift
else
log_error "LOCAL_TASKS - ${FUNCNAME[0]}: '--port' requires a non-empty option argument."
exit 1
fi
;;
--port=?*)
# port options, with value separated by =
option_port="${1#*=}"
;;
--port=)
# port options, without value
log_error "LOCAL_TASKS - ${FUNCNAME[0]}: '--port' requires a non-empty option argument."
exit 1
;;
--user)
# user options, with value separated by space
if [ -n "$2" ]; then
option_user="${2}"
shift
else
log_error "LOCAL_TASKS - ${FUNCNAME[0]}: '--user' requires a non-empty option argument."
exit 1
fi
;;
--user=?*)
# user options, with value separated by =
option_user="${1#*=}"
;;
--user=)
# user options, without value
log_error "LOCAL_TASKS - ${FUNCNAME[0]}: '--user' requires a non-empty option argument."
exit 1
;;
--password)
# password options, with value separated by space
if [ -n "$2" ]; then
option_password="${2}"
shift
else
log_error "LOCAL_TASKS - ${FUNCNAME[0]}: '--password' requires a non-empty option argument."
exit 1
fi
;;
--password=?*)
# password options, with value separated by =
option_password="${1#*=}"
;;
--password=)
# password options, without value
log_error "LOCAL_TASKS - ${FUNCNAME[0]}: '--password' requires a non-empty option argument."
exit 1
;;
--dump-label)
# dump-label options, with value separated by space
if [ -n "$2" ]; then
option_dump_label="${2}"
shift
else
log_error "LOCAL_TASKS - ${FUNCNAME[0]}: '--dump-label' requires a non-empty option argument."
exit 1
fi
;;
--dump-label=?*)
# dump-label options, with value separated by =
option_dump_label="${1#*=}"
;;
--dump-label=)
# dump-label options, without value
log_error "LOCAL_TASKS - ${FUNCNAME[0]}: '--dump-label' requires a non-empty option argument."
exit 1
;;
--)
# End of all options.
shift
option_others=${*}
break
;;
-?*|[[:alnum:]]*)
# ignore unknown options
log_error "LOCAL_TASKS - ${FUNCNAME[0]}: unknown option '${1}' (ignored)"
;;
*)
# Default case: If no more options then break out of the loop.
break
;;
esac
shift
done
if [ -z "${option_dump_label}" ]; then
if [ -n "${option_port}" ]; then
option_dump_label="${option_port}"
else
option_dump_label="default"
fi
fi
local dump_dir="${LOCAL_BACKUP_DIR}/mongodb-${option_dump_label}"
local errors_dir=$(errors_dir_from_dump_dir "${dump_dir}")
rm -rf "${dump_dir}" "${errors_dir}"
mkdir -p "${dump_dir}" "${errors_dir}"
# No need to change recursively, the top directory is enough
chmod 700 "${dump_dir}" "${errors_dir}"
local error_file="${errors_dir}.err"
log "LOCAL_TASKS - ${FUNCNAME[0]}: start ${dump_dir}"
declare -a dump_options
dump_options=()
if [ -n "${option_port}" ]; then
dump_options+=(--port="${option_port}")
fi
if [ -n "${option_user}" ]; then
dump_options+=(--username="${option_user}")
fi
if [ -n "${option_password}" ]; then
dump_options+=(--password="${option_password}")
fi
dump_options+=(--out="${dump_dir}/")
if [ -n "${option_others}" ]; then
# word splitting is deliberate here
# shellcheck disable=SC2206
dump_options+=(${option_others})
fi
dump_cmd="mongodump ${dump_options[*]}"
log "LOCAL_TASKS - ${FUNCNAME[0]}: ${dump_cmd} > /dev/null"
${dump_cmd} 2> "${error_file}" > /dev/null
local last_rc=$?
# shellcheck disable=SC2086
if [ ${last_rc} -ne 0 ]; then
log_error "LOCAL_TASKS - ${FUNCNAME[0]}: mongodump to ${dump_dir} returned an error ${last_rc}" "${error_file}"
GLOBAL_RC=${E_DUMPFAILED}
else
rm -f "${error_file}"
fi
log "LOCAL_TASKS - stop ${FUNCNAME[0]}: ${dump_dir}"
}
#######################################################################
# Dump RAID configuration
#
# Arguments: <none>
#######################################################################
dump_raid_config() {
local dump_dir="${LOCAL_BACKUP_DIR}/raid"
local errors_dir=$(errors_dir_from_dump_dir "${dump_dir}")
rm -rf "${dump_dir}" "${errors_dir}"
mkdir -p "${dump_dir}" "${errors_dir}"
# No need to change recursively, the top directory is enough
chmod 700 "${dump_dir}" "${errors_dir}"
if command -v megacli > /dev/null; then
local error_file="${errors_dir}/megacli.cfg"
local dump_file="${dump_dir}/megacli.err"
log "LOCAL_TASKS - ${FUNCNAME[0]}: start ${dump_file}"
dump_cmd="megacli -CfgSave -f ${dump_file} -a0"
log "LOCAL_TASKS - ${FUNCNAME[0]}: ${dump_cmd}"
${dump_cmd} 2> "${error_file}"
local last_rc=$?
# shellcheck disable=SC2086
if [ ${last_rc} -ne 0 ]; then
log_error "LOCAL_TASKS - ${FUNCNAME[0]}: megacli to ${dump_file} returned an error ${last_rc}" "${error_file}"
GLOBAL_RC=${E_DUMPFAILED}
else
rm -f "${error_file}"
fi
log "LOCAL_TASKS - ${FUNCNAME[0]}: stop ${dump_file}"
elif command -v perccli > /dev/null; then
local error_file="${errors_dir}/perccli.cfg"
local dump_file="${dump_dir}/perccli.err"
# log "LOCAL_TASKS - ${FUNCNAME[0]}: start ${dump_file}"
# TODO: find out what the correct command is
# dump_cmd="perccli XXXX"
# log "LOCAL_TASKS - ${FUNCNAME[0]}: ${dump_cmd}"
# ${dump_cmd} 2> ${error_file}
# local last_rc=$?
# # shellcheck disable=SC2086
# if [ ${last_rc} -ne 0 ]; then
# log_error "LOCAL_TASKS - ${FUNCNAME[0]}: perccli to ${dump_file} returned an error ${last_rc}" "${error_file}"
# GLOBAL_RC=${E_DUMPFAILED}
# else
# rm -f "${error_file}"
# fi
# log "LOCAL_TASKS - ${FUNCNAME[0]}: stop ${dump_file}"
else
log "LOCAL_TASKS - ${FUNCNAME[0]}: 'megacli' and 'perccli' not found, unable to dump RAID configuration"
fi
}
#######################################################################
# Save some traceroute/mtr results
#
# Arguments:
# --targets=[IP,HOST] (default: <none>)
#######################################################################
dump_traceroute() {
local option_targets=""
# Parse options, based on https://gist.github.com/deshion/10d3cb5f88a21671e17a
while :; do
case ${1:-''} in
--targets)
# targets options, with key and value separated by space
if [ -n "$2" ]; then
IFS="," read -a option_targets <<< "${2}"
shift
else
log_error "LOCAL_TASKS - ${FUNCNAME[0]}: '--targets' requires a non-empty option argument."
exit 1
fi
;;
--targets=?*)
# targets options, with key and value separated by =
IFS="," read -a option_targets <<< "${1#*=}"
;;
--targets=)
# targets options, without value
log_error "LOCAL_TASKS - ${FUNCNAME[0]}: '--targets' requires a non-empty option argument."
exit 1
;;
--)
# End of all options.
shift
break
;;
-?*|[[:alnum:]]*)
# ignore unknown options
log_error "LOCAL_TASKS - ${FUNCNAME[0]}: unknown option '${1}' (ignored)"
;;
*)
# Default case: If no more options then break out of the loop.
break
;;
esac
shift
done
local dump_dir="${LOCAL_BACKUP_DIR}/traceroute"
local errors_dir=$(errors_dir_from_dump_dir "${dump_dir}")
rm -rf "${dump_dir}" "${errors_dir}"
mkdir -p "${dump_dir}" "${errors_dir}"
# No need to change recursively, the top directory is enough
chmod 700 "${dump_dir}" "${errors_dir}"
mtr_bin=$(command -v mtr)
if [ -n "${mtr_bin}" ]; then
for target in "${option_targets[@]}"; do
local dump_file="${dump_dir}/mtr-${target}"
log "LOCAL_TASKS - ${FUNCNAME[0]}: start ${dump_file}"
${mtr_bin} -r "${target}" > "${dump_file}"
log "LOCAL_TASKS - ${FUNCNAME[0]}: stop ${dump_file}"
done
fi
traceroute_bin=$(command -v traceroute)
if [ -n "${traceroute_bin}" ]; then
for target in "${option_targets[@]}"; do
local dump_file="${dump_dir}/traceroute-${target}"
log "LOCAL_TASKS - ${FUNCNAME[0]}: start ${dump_file}"
${traceroute_bin} -n "${target}" > "${dump_file}" 2>&1
log "LOCAL_TASKS - ${FUNCNAME[0]}: stop ${dump_file}"
done
fi
}
#######################################################################
# Save many system information, using dump_server_state
#
# Arguments:
# any option for dump-server-state (except --dump-dir) is usable
# (default: --all)
#######################################################################
dump_server_state() {
local dump_dir="${LOCAL_BACKUP_DIR}/server-state"
rm -rf "${dump_dir}"
# Do not create the directory
# mkdir -p -m 700 "${dump_dir}"
log "LOCAL_TASKS - ${FUNCNAME[0]}: start ${dump_dir}"
# pass all options
read -a options <<< "${@}"
# if no option is given, use "--all" as fallback
if [ ${#options[@]} -le 0 ]; then
options=(--all)
fi
# add "--dump-dir" in case it is missing (as it should)
options+=(--dump-dir "${dump_dir}")
dump_server_state_bin=$(command -v dump-server-state)
if [ -z "${dump_server_state_bin}" ]; then
log_error "LOCAL_TASKS - ${FUNCNAME[0]}: dump-server-state is missing"
rc=1
else
dump_cmd="${dump_server_state_bin} ${options[*]}"
log "LOCAL_TASKS - ${FUNCNAME[0]}: ${dump_cmd}"
${dump_cmd}
local last_rc=$?
# shellcheck disable=SC2086
if [ ${last_rc} -ne 0 ]; then
log_error "LOCAL_TASKS - ${FUNCNAME[0]}: dump-server-state returned an error ${last_rc}, check ${dump_dir}"
GLOBAL_RC=${E_DUMPFAILED}
fi
fi
log "LOCAL_TASKS - ${FUNCNAME[0]}: stop ${dump_dir}"
}
#######################################################################
# Save RabbitMQ data
#
# Arguments: <none>
#
# Warning: This has been poorly tested
#######################################################################
dump_rabbitmq() {
local dump_dir="${LOCAL_BACKUP_DIR}/rabbitmq"
local errors_dir=$(errors_dir_from_dump_dir "${dump_dir}")
rm -rf "${dump_dir}" "${errors_dir}"
mkdir -p "${dump_dir}" "${errors_dir}"
# No need to change recursively, the top directory is enough
chmod 700 "${dump_dir}" "${errors_dir}"
local error_file="${errors_dir}.err"
local dump_file="${dump_dir}/config"
log "LOCAL_TASKS - ${FUNCNAME[0]}: start ${dump_file}"
dump_cmd="rabbitmqadmin export ${dump_file}"
log "LOCAL_TASKS - ${FUNCNAME[0]}: ${dump_cmd}"
${dump_cmd} 2> "${error_file}"
local last_rc=$?
# shellcheck disable=SC2086
if [ ${last_rc} -ne 0 ]; then
log_error "LOCAL_TASKS - ${FUNCNAME[0]}: pg_dump to ${dump_file} returned an error ${last_rc}" "${error_file}"
GLOBAL_RC=${E_DUMPFAILED}
else
rm -f "${error_file}"
fi
log "LOCAL_TASKS - ${FUNCNAME[0]}: stop ${dump_file}"
}
#######################################################################
# Save Files ACL on various partitions.
#
# Arguments: <none>
#######################################################################
dump_facl() {
local dump_dir="${LOCAL_BACKUP_DIR}/facl"
local errors_dir=$(errors_dir_from_dump_dir "${dump_dir}")
rm -rf "${dump_dir}" "${errors_dir}"
mkdir -p "${dump_dir}" "${errors_dir}"
# No need to change recursively, the top directory is enough
chmod 700 "${dump_dir}" "${errors_dir}"
log "LOCAL_TASKS - ${FUNCNAME[0]}: start ${dump_dir}"
dump_cmd="getfacl -R /etc > ${dump_dir}/etc.txt"
log "LOCAL_TASKS - ${FUNCNAME[0]}: ${dump_cmd}"
${dump_cmd}
dump_cmd="getfacl -R /home > ${dump_dir}/home.txt"
log "LOCAL_TASKS - ${FUNCNAME[0]}: ${dump_cmd}"
${dump_cmd}
dump_cmd="getfacl -R /usr > ${dump_dir}/usr.txt"
log "LOCAL_TASKS - ${FUNCNAME[0]}: ${dump_cmd}"
${dump_cmd}
dump_cmd="getfacl -R /var > ${dump_dir}/var.txt"
log "LOCAL_TASKS - ${FUNCNAME[0]}: ${dump_cmd}"
${dump_cmd}
log "LOCAL_TASKS - ${FUNCNAME[0]}: stop ${dump_dir}"
}

1551
client/lib/dump/mysql.sh Normal file
View file

@ -0,0 +1,1551 @@
#!/usr/bin/env bash
# shellcheck disable=SC2034,SC2317,SC2155
#######################################################################
# Dump complete summary of an instance (using pt-mysql-summary)
#
# Arguments:
# --port=[Integer] (default: <blank>)
# --socket=[String] (default: <blank>)
# --user=[String] (default: <blank>)
# --password=[String] (default: <blank>)
# --defaults-file=[String] (default: <blank>)
# --defaults-extra-file=[String] (default: <blank>)
# --defaults-group-suffix=[String] (default: <blank>)
# --dump-label=[String] (default: "default")
# used as suffix of the dump dir to differenciate multiple instances
#######################################################################
dump_mysql_summary() {
local option_port=""
local option_socket=""
local option_defaults_file=""
local option_defaults_extra_file=""
local option_defaults_group_suffix=""
local option_user=""
local option_password=""
local option_dump_label=""
# Parse options, based on https://gist.github.com/deshion/10d3cb5f88a21671e17a
while :; do
case ${1:-''} in
--defaults-file)
# defaults-file options, with value separated by space
if [ -n "$2" ]; then
option_defaults_file="${2}"
shift
else
log_error "LOCAL_TASKS - ${FUNCNAME[0]}: '--defaults-file' requires a non-empty option argument."
exit 1
fi
;;
--defaults-file=?*)
# defaults-file options, with value separated by =
option_defaults_file="${1#*=}"
;;
--defaults-file=)
# defaults-file options, without value
log_error "LOCAL_TASKS - ${FUNCNAME[0]}: '--defaults-file' requires a non-empty option argument."
exit 1
;;
--defaults-extra-file)
# defaults-file options, with value separated by space
if [ -n "$2" ]; then
option_defaults_extra_file="${2}"
shift
else
log_error "LOCAL_TASKS - ${FUNCNAME[0]}: '--defaults-file' requires a non-empty option argument."
exit 1
fi
;;
--defaults-extra-file=?*)
# defaults-extra-file options, with value separated by =
option_defaults_extra_file="${1#*=}"
;;
--defaults-extra-file=)
# defaults-extra-file options, without value
log_error "LOCAL_TASKS - ${FUNCNAME[0]}: '--defaults-extra-file' requires a non-empty option argument."
exit 1
;;
--defaults-group-suffix)
# defaults-group-suffix options, with value separated by space
if [ -n "$2" ]; then
option_defaults_group_suffix="${2}"
shift
else
log_error "LOCAL_TASKS - ${FUNCNAME[0]}: '--defaults-group-suffix' requires a non-empty option argument."
exit 1
fi
;;
--defaults-group-suffix=?*)
# defaults-group-suffix options, with value separated by =
option_defaults_group_suffix="${1#*=}"
;;
--defaults-group-suffix=)
# defaults-group-suffix options, without value
log_error "LOCAL_TASKS - ${FUNCNAME[0]}: '--defaults-group-suffix' requires a non-empty option argument."
exit 1
;;
--port)
# port options, with value separated by space
if [ -n "$2" ]; then
option_port="${2}"
shift
else
log_error "LOCAL_TASKS - ${FUNCNAME[0]}: '--port' requires a non-empty option argument."
exit 1
fi
;;
--port=?*)
# port options, with value separated by =
option_port="${1#*=}"
;;
--port=)
# port options, without value
log_error "LOCAL_TASKS - ${FUNCNAME[0]}: '--port' requires a non-empty option argument."
exit 1
;;
--socket)
# socket options, with value separated by space
if [ -n "$2" ]; then
option_socket="${2}"
shift
else
log_error "LOCAL_TASKS - ${FUNCNAME[0]}: '--socket' requires a non-empty option argument."
exit 1
fi
;;
--socket=?*)
# socket options, with value separated by =
option_socket="${1#*=}"
;;
--socket=)
# socket options, without value
log_error "LOCAL_TASKS - ${FUNCNAME[0]}: '--socket' requires a non-empty option argument."
exit 1
;;
--user)
# user options, with value separated by space
if [ -n "$2" ]; then
option_user="${2}"
shift
else
log_error "LOCAL_TASKS - ${FUNCNAME[0]}: '--user' requires a non-empty option argument."
exit 1
fi
;;
--user=?*)
# user options, with value separated by =
option_user="${1#*=}"
;;
--user=)
# user options, without value
log_error "LOCAL_TASKS - ${FUNCNAME[0]}: '--user' requires a non-empty option argument."
exit 1
;;
--password)
# password options, with value separated by space
if [ -n "$2" ]; then
option_password="${2}"
shift
else
log_error "LOCAL_TASKS - ${FUNCNAME[0]}: '--password' requires a non-empty option argument."
exit 1
fi
;;
--password=?*)
# password options, with value separated by =
option_password="${1#*=}"
;;
--password=)
# password options, without value
log_error "LOCAL_TASKS - ${FUNCNAME[0]}: '--password' requires a non-empty option argument."
exit 1
;;
--dump-label)
# dump-label options, with value separated by space
if [ -n "$2" ]; then
option_dump_label="${2}"
shift
else
log_error "LOCAL_TASKS - ${FUNCNAME[0]}: '--dump-label' requires a non-empty option argument."
exit 1
fi
;;
--dump-label=?*)
# dump-label options, with value separated by =
option_dump_label="${1#*=}"
;;
--dump-label=)
# dump-label options, without value
log_error "LOCAL_TASKS - ${FUNCNAME[0]}: '--dump-label' requires a non-empty option argument."
exit 1
;;
--)
# End of all options.
shift
break
;;
-?*|[[:alnum:]]*)
# ignore unknown options
log_error "LOCAL_TASKS - ${FUNCNAME[0]}: unkwnown option (ignored): '${1}'"
;;
*)
# Default case: If no more options then break out of the loop.
break
;;
esac
shift
done
if [ -z "${option_dump_label}" ]; then
if [ -n "${option_defaults_group_suffix}" ]; then
option_dump_label="${option_defaults_group_suffix}"
elif [ -n "${option_port}" ]; then
option_dump_label="${option_port}"
elif [ -n "${option_socket}" ]; then
option_dump_label=$(path_to_str "${option_socket}")
else
option_dump_label="default"
fi
fi
local dump_dir="${LOCAL_BACKUP_DIR}/mysql-${option_dump_label}-summary"
local errors_dir=$(errors_dir_from_dump_dir "${dump_dir}")
rm -rf "${dump_dir}" "${errors_dir}"
mkdir -p "${dump_dir}" "${errors_dir}"
# No need to change recursively, the top directory is enough
chmod 700 "${dump_dir}" "${errors_dir}"
## Dump all grants (requires 'percona-toolkit' package)
if command -v pt-mysql-summary > /dev/null; then
local error_file="${errors_dir}/mysql-summary.err"
local dump_file="${dump_dir}/mysql-summary.out"
log "LOCAL_TASKS - ${FUNCNAME[0]}: start ${dump_file}"
## Connection options
declare -a connect_options
connect_options=()
if [ -n "${option_defaults_file}" ]; then
connect_options+=(--defaults-file="${option_defaults_file}")
fi
if [ -n "${option_defaults_extra_file}" ]; then
connect_options+=(--defaults-extra-file="${option_defaults_extra_file}")
fi
if [ -n "${option_defaults_group_suffix}" ]; then
connect_options+=(--defaults-group-suffix="${option_defaults_group_suffix}")
fi
if [ -n "${option_port}" ]; then
connect_options+=(--protocol=tcp)
connect_options+=(--port="${option_port}")
fi
if [ -n "${option_socket}" ]; then
connect_options+=(--protocol=socket)
connect_options+=(--socket="${option_socket}")
fi
if [ -n "${option_user}" ]; then
connect_options+=(--user="${option_user}")
fi
if [ -n "${option_password}" ]; then
connect_options+=(--password="${option_password}")
fi
declare -a options
options=()
options+=(--sleep=0)
dump_cmd="pt-mysql-summary ${options[*]} -- ${connect_options[*]}"
log "LOCAL_TASKS - ${FUNCNAME[0]}: ${dump_cmd}"
${dump_cmd} 2> "${error_file}" > "${dump_file}"
local last_rc=$?
# shellcheck disable=SC2086
if [ ${last_rc} -ne 0 ]; then
log_error "LOCAL_TASKS - ${FUNCNAME[0]}: pt-mysql-summary to ${dump_file} returned an error ${last_rc}" "${error_file}"
GLOBAL_RC=${E_DUMPFAILED}
else
rm -f "${error_file}"
fi
log "LOCAL_TASKS - ${FUNCNAME[0]}: stop ${dump_file}"
else
log "LOCAL_TASKS - ${FUNCNAME[0]}: 'pt-mysql-summary' not found, unable to dump summary"
fi
}
#######################################################################
# Dump grants of an instance
#
# Arguments:
# --port=[Integer] (default: <blank>)
# --socket=[String] (default: <blank>)
# --user=[String] (default: <blank>)
# --password=[String] (default: <blank>)
# --defaults-file=[String] (default: <blank>)
# --dump-label=[String] (default: "default")
# used as suffix of the dump dir to differenciate multiple instances
#######################################################################
dump_mysql_grants() {
local option_port=""
local option_socket=""
local option_defaults_file=""
local option_user=""
local option_password=""
local option_dump_label=""
# Parse options, based on https://gist.github.com/deshion/10d3cb5f88a21671e17a
while :; do
case ${1:-''} in
--defaults-file)
# defaults-file options, with value separated by space
if [ -n "$2" ]; then
option_defaults_file="${2}"
shift
else
log_error "LOCAL_TASKS - ${FUNCNAME[0]}: '--defaults-file' requires a non-empty option argument."
exit 1
fi
;;
--defaults-file=?*)
# defaults-file options, with value separated by =
option_defaults_file="${1#*=}"
;;
--defaults-file=)
# defaults-file options, without value
log_error "LOCAL_TASKS - ${FUNCNAME[0]}: '--defaults-file' requires a non-empty option argument."
exit 1
;;
--port)
# port options, with value separated by space
if [ -n "$2" ]; then
option_port="${2}"
shift
else
log_error "LOCAL_TASKS - ${FUNCNAME[0]}: '--port' requires a non-empty option argument."
exit 1
fi
;;
--port=?*)
# port options, with value separated by =
option_port="${1#*=}"
;;
--port=)
# port options, without value
log_error "LOCAL_TASKS - ${FUNCNAME[0]}: '--port' requires a non-empty option argument."
exit 1
;;
--socket)
# socket options, with value separated by space
if [ -n "$2" ]; then
option_socket="${2}"
shift
else
log_error "LOCAL_TASKS - ${FUNCNAME[0]}: '--socket' requires a non-empty option argument."
exit 1
fi
;;
--socket=?*)
# socket options, with value separated by =
option_socket="${1#*=}"
;;
--socket=)
# socket options, without value
log_error "LOCAL_TASKS - ${FUNCNAME[0]}: '--socket' requires a non-empty option argument."
exit 1
;;
--user)
# user options, with value separated by space
if [ -n "$2" ]; then
option_user="${2}"
shift
else
log_error "LOCAL_TASKS - ${FUNCNAME[0]}: '--user' requires a non-empty option argument."
exit 1
fi
;;
--user=?*)
# user options, with value separated by =
option_user="${1#*=}"
;;
--user=)
# user options, without value
log_error "LOCAL_TASKS - ${FUNCNAME[0]}: '--user' requires a non-empty option argument."
exit 1
;;
--password)
# password options, with value separated by space
if [ -n "$2" ]; then
option_password="${2}"
shift
else
log_error "LOCAL_TASKS - ${FUNCNAME[0]}: '--password' requires a non-empty option argument."
exit 1
fi
;;
--password=?*)
# password options, with value separated by =
option_password="${1#*=}"
;;
--password=)
# password options, without value
log_error "LOCAL_TASKS - ${FUNCNAME[0]}: '--password' requires a non-empty option argument."
exit 1
;;
--dump-label)
# dump-label options, with value separated by space
if [ -n "$2" ]; then
option_dump_label="${2}"
shift
else
log_error "LOCAL_TASKS - ${FUNCNAME[0]}: '--dump-label' requires a non-empty option argument."
exit 1
fi
;;
--dump-label=?*)
# dump-label options, with value separated by =
option_dump_label="${1#*=}"
;;
--dump-label=)
# dump-label options, without value
log_error "LOCAL_TASKS - ${FUNCNAME[0]}: '--dump-label' requires a non-empty option argument."
exit 1
;;
--)
# End of all options.
shift
break
;;
-?*|[[:alnum:]]*)
# ignore unknown options
log_error "LOCAL_TASKS - ${FUNCNAME[0]}: unknown option '${1}' (ignored)"
;;
*)
# Default case: If no more options then break out of the loop.
break
;;
esac
shift
done
if [ -z "${option_dump_label}" ]; then
if [ -n "${option_port}" ]; then
option_dump_label="${option_port}"
elif [ -n "${option_socket}" ]; then
option_dump_label=$(path_to_str "${option_socket}")
else
option_dump_label="default"
fi
fi
local dump_dir="${LOCAL_BACKUP_DIR}/mysql-${option_dump_label}-grants"
local errors_dir=$(errors_dir_from_dump_dir "${dump_dir}")
rm -rf "${dump_dir}" "${errors_dir}"
mkdir -p "${dump_dir}" "${errors_dir}"
# No need to change recursively, the top directory is enough
chmod 700 "${dump_dir}" "${errors_dir}"
## Dump all grants (requires 'percona-toolkit' package)
if command -v pt-show-grants > /dev/null; then
local error_file="${errors_dir}/all_grants.err"
local dump_file="${dump_dir}/all_grants.sql"
log "LOCAL_TASKS - ${FUNCNAME[0]}: start ${dump_file}"
declare -a options
options=()
if [ -n "${option_defaults_file}" ]; then
options+=(--defaults-file="${option_defaults_file}")
fi
if [ -n "${option_port}" ]; then
options+=(--port="${option_port}")
fi
if [ -n "${option_socket}" ]; then
options+=(--socket="${option_socket}")
fi
if [ -n "${option_user}" ]; then
options+=(--user="${option_user}")
fi
if [ -n "${option_password}" ]; then
options+=(--password="${option_password}")
fi
options+=(--flush)
options+=(--no-header)
dump_cmd="pt-show-grants ${options[*]}"
log "LOCAL_TASKS - ${FUNCNAME[0]}: ${dump_cmd}"
${dump_cmd} 2> "${error_file}" > "${dump_file}"
local last_rc=$?
# shellcheck disable=SC2086
if [ ${last_rc} -ne 0 ]; then
log_error "LOCAL_TASKS - ${FUNCNAME[0]}: pt-show-grants to ${dump_file} returned an error ${last_rc}" "${error_file}"
GLOBAL_RC=${E_DUMPFAILED}
else
rm -f "${error_file}"
fi
log "LOCAL_TASKS - ${FUNCNAME[0]}: stop ${dump_file}"
else
log "LOCAL_TASKS - ${FUNCNAME[0]}: 'pt-show-grants' not found, unable to dump grants"
fi
}
#######################################################################
# Dump a single compressed file of all databases of an instance
# and a file containing only the schema.
#
# Arguments:
# --masterdata (default: <absent>)
# --port=[Integer] (default: <blank>)
# --socket=[String] (default: <blank>)
# --user=[String] (default: <blank>)
# --password=[String] (default: <blank>)
# --defaults-file=[String] (default: <blank>)
# --defaults-extra-file=[String] (default: <blank>)
# --defaults-group-suffix=[String] (default: <blank>)
# --dump-label=[String] (default: "default")
# used as suffix of the dump dir to differenciate multiple instances
# --compress=<gzip|pigz|bzip2|xz|none> (default: "gzip")
# Other options after -- are passed as-is to mysqldump
#######################################################################
dump_mysql_global() {
local option_masterdata=""
local option_port=""
local option_socket=""
local option_defaults_file=""
local option_defaults_extra_file=""
local option_defaults_group_suffix=""
local option_user=""
local option_password=""
local option_dump_label=""
local option_compress=""
local option_others=""
# Parse options, based on https://gist.github.com/deshion/10d3cb5f88a21671e17a
while :; do
case ${1:-''} in
--masterdata)
option_masterdata="--masterdata"
;;
--defaults-file)
# defaults-file options, with value separated by space
if [ -n "$2" ]; then
option_defaults_file="${2}"
shift
else
log_error "LOCAL_TASKS - ${FUNCNAME[0]}: '--defaults-file' requires a non-empty option argument."
exit 1
fi
;;
--defaults-file=?*)
# defaults-file options, with value separated by =
option_defaults_file="${1#*=}"
;;
--defaults-file=)
# defaults-file options, without value
log_error "LOCAL_TASKS - ${FUNCNAME[0]}: '--defaults-file' requires a non-empty option argument."
exit 1
;;
--defaults-extra-file)
# defaults-file options, with value separated by space
if [ -n "$2" ]; then
option_defaults_extra_file="${2}"
shift
else
log_error "LOCAL_TASKS - ${FUNCNAME[0]}: '--defaults-file' requires a non-empty option argument."
exit 1
fi
;;
--defaults-extra-file=?*)
# defaults-extra-file options, with value separated by =
option_defaults_extra_file="${1#*=}"
;;
--defaults-extra-file=)
# defaults-extra-file options, without value
log_error "LOCAL_TASKS - ${FUNCNAME[0]}: '--defaults-extra-file' requires a non-empty option argument."
exit 1
;;
--defaults-group-suffix)
# defaults-group-suffix options, with value separated by space
if [ -n "$2" ]; then
option_defaults_group_suffix="${2}"
shift
else
log_error "LOCAL_TASKS - ${FUNCNAME[0]}: '--defaults-group-suffix' requires a non-empty option argument."
exit 1
fi
;;
--defaults-group-suffix=?*)
# defaults-group-suffix options, with value separated by =
option_defaults_group_suffix="${1#*=}"
;;
--defaults-group-suffix=)
# defaults-group-suffix options, without value
log_error "LOCAL_TASKS - ${FUNCNAME[0]}: '--defaults-group-suffix' requires a non-empty option argument."
exit 1
;;
--port)
# port options, with value separated by space
if [ -n "$2" ]; then
option_port="${2}"
shift
else
log_error "LOCAL_TASKS - ${FUNCNAME[0]}: '--port' requires a non-empty option argument."
exit 1
fi
;;
--port=?*)
# port options, with value separated by =
option_port="${1#*=}"
;;
--port=)
# port options, without value
log_error "LOCAL_TASKS - ${FUNCNAME[0]}: '--port' requires a non-empty option argument."
exit 1
;;
--socket)
# socket options, with value separated by space
if [ -n "$2" ]; then
option_socket="${2}"
shift
else
log_error "LOCAL_TASKS - ${FUNCNAME[0]}: '--socket' requires a non-empty option argument."
exit 1
fi
;;
--socket=?*)
# socket options, with value separated by =
option_socket="${1#*=}"
;;
--socket=)
# socket options, without value
log_error "LOCAL_TASKS - ${FUNCNAME[0]}: '--socket' requires a non-empty option argument."
exit 1
;;
--user)
# user options, with value separated by space
if [ -n "$2" ]; then
option_user="${2}"
shift
else
log_error "LOCAL_TASKS - ${FUNCNAME[0]}: '--user' requires a non-empty option argument."
exit 1
fi
;;
--user=?*)
# user options, with value separated by =
option_user="${1#*=}"
;;
--user=)
# user options, without value
log_error "LOCAL_TASKS - ${FUNCNAME[0]}: '--user' requires a non-empty option argument."
exit 1
;;
--password)
# password options, with value separated by space
if [ -n "$2" ]; then
option_password="${2}"
shift
else
log_error "LOCAL_TASKS - ${FUNCNAME[0]}: '--password' requires a non-empty option argument."
exit 1
fi
;;
--password=?*)
# password options, with value separated by =
option_password="${1#*=}"
;;
--password=)
# password options, without value
log_error "LOCAL_TASKS - ${FUNCNAME[0]}: '--password' requires a non-empty option argument."
exit 1
;;
--dump-label)
# dump-label options, with value separated by space
if [ -n "$2" ]; then
option_dump_label="${2}"
shift
else
log_error "LOCAL_TASKS - ${FUNCNAME[0]}: '--dump-label' requires a non-empty option argument."
exit 1
fi
;;
--dump-label=?*)
# dump-label options, with value separated by =
option_dump_label="${1#*=}"
;;
--dump-label=)
# dump-label options, without value
log_error "LOCAL_TASKS - ${FUNCNAME[0]}: '--dump-label' requires a non-empty option argument."
exit 1
;;
--compress)
# compress options, with value separated by space
if [ -n "$2" ]; then
option_compress="${2}"
shift
else
log_error "LOCAL_TASKS - ${FUNCNAME[0]}: '--compress' requires a non-empty option argument."
exit 1
fi
;;
--compress=?*)
# compress options, with value separated by =
option_compress="${1#*=}"
;;
--compress=)
# compress options, without value
log_error "LOCAL_TASKS - ${FUNCNAME[0]}: '--compress' requires a non-empty option argument."
exit 1
;;
--)
# End of all options.
shift
option_others=${*}
break
;;
-?*|[[:alnum:]]*)
# ignore unknown options
log_error "LOCAL_TASKS - ${FUNCNAME[0]}: unknown option '${1}' (ignored)"
;;
*)
# Default case: If no more options then break out of the loop.
break
;;
esac
shift
done
case "${option_compress}" in
none)
compress_cmd="cat"
dump_ext=""
;;
bzip2|bz|bz2)
compress_cmd="bzip2 --best"
dump_ext=".bz"
;;
xz)
compress_cmd="xz --best"
dump_ext=".xz"
;;
pigz)
compress_cmd="pigz --best"
dump_ext=".gz"
;;
gz|gzip|*)
compress_cmd="gzip --best"
dump_ext=".gz"
;;
esac
if [ -z "${option_dump_label}" ]; then
if [ -n "${option_defaults_group_suffix}" ]; then
option_dump_label="${option_defaults_group_suffix}"
elif [ -n "${option_port}" ]; then
option_dump_label="${option_port}"
elif [ -n "${option_socket}" ]; then
option_dump_label=$(path_to_str "${option_socket}")
else
option_dump_label="default"
fi
fi
## Connection options
declare -a connect_options
connect_options=()
if [ -n "${option_defaults_file}" ]; then
connect_options+=(--defaults-file="${option_defaults_file}")
fi
if [ -n "${option_defaults_extra_file}" ]; then
connect_options+=(--defaults-extra-file="${option_defaults_extra_file}")
fi
if [ -n "${option_defaults_group_suffix}" ]; then
connect_options+=(--defaults-group-suffix="${option_defaults_group_suffix}")
fi
if [ -n "${option_port}" ]; then
connect_options+=(--protocol=tcp)
connect_options+=(--port="${option_port}")
fi
if [ -n "${option_socket}" ]; then
connect_options+=(--protocol=socket)
connect_options+=(--socket="${option_socket}")
fi
if [ -n "${option_user}" ]; then
connect_options+=(--user="${option_user}")
fi
if [ -n "${option_password}" ]; then
connect_options+=(--password="${option_password}")
fi
## Global all databases in one file
local dump_dir="${LOCAL_BACKUP_DIR}/mysql-${option_dump_label}"
local errors_dir=$(errors_dir_from_dump_dir "${dump_dir}")
rm -rf "${dump_dir}" "${errors_dir}"
mkdir -p "${dump_dir}" "${errors_dir}"
# No need to change recursively, the top directory is enough
chmod 700 "${dump_dir}" "${errors_dir}"
local error_file="${errors_dir}/mysqldump.err"
local dump_file="${dump_dir}/mysqldump.sql${dump_ext}"
log "LOCAL_TASKS - ${FUNCNAME[0]}: start ${dump_file}"
declare -a dump_options
dump_options=()
dump_options+=(--opt)
dump_options+=(--force)
dump_options+=(--events)
dump_options+=(--hex-blob)
dump_options+=(--all-databases)
if [ -n "${option_masterdata}" ]; then
dump_options+=("${option_masterdata}")
fi
if [ -n "${option_others}" ]; then
# word splitting is deliberate here
# shellcheck disable=SC2206
dump_options+=(${option_others})
fi
## WARNING : logging and executing the command must be separate
## because otherwise Bash would interpret | and > as strings and not syntax.
dump_cmd="mysqldump ${connect_options[*]} ${dump_options[*]} 2> ${error_file} | ${compress_cmd} > ${dump_file}"
log "LOCAL_TASKS - ${FUNCNAME[0]}: ${dump_cmd}"
mysqldump "${connect_options[@]}" "${dump_options[@]}" 2> "${error_file}" | ${compress_cmd} > "${dump_file}"
local last_rc=$?
# shellcheck disable=SC2086
if [ ${last_rc} -ne 0 ]; then
log_error "LOCAL_TASKS - ${FUNCNAME[0]}: mysqldump returned an error ${last_rc}" "${error_file}"
GLOBAL_RC=${E_DUMPFAILED}
else
rm -f "${error_file}"
fi
log "LOCAL_TASKS - ${FUNCNAME[0]}: stop ${dump_file}"
## Schema only (no data) for each databases
local error_file="${errors_dir}/mysqldump.schema.err"
local dump_file="${dump_dir}/mysqldump.schema.sql"
log "LOCAL_TASKS - ${FUNCNAME[0]}: start ${dump_file}"
declare -a dump_options
dump_options=()
dump_options+=(--force)
dump_options+=(--no-data)
dump_options+=(--all-databases)
if [ -n "${option_others}" ]; then
# word splitting is deliberate here
# shellcheck disable=SC2206
dump_options+=(${option_others})
fi
dump_cmd="mysqldump ${connect_options[*]} ${dump_options[*]}"
log "LOCAL_TASKS - ${FUNCNAME[0]}: ${dump_cmd}"
${dump_cmd} 2> "${error_file}" > "${dump_file}"
local last_rc=$?
# shellcheck disable=SC2086
if [ ${last_rc} -ne 0 ]; then
log_error "LOCAL_TASKS - ${FUNCNAME[0]}: mysqldump returned an error ${last_rc}" "${error_file}"
GLOBAL_RC=${E_DUMPFAILED}
else
rm -f "${error_file}"
fi
log "LOCAL_TASKS - ${FUNCNAME[0]}: stop ${dump_file}"
}
#######################################################################
# Dump a file of each databases of an instance
# and a file containing only the schema.
#
# Arguments:
# --port=[Integer] (default: <blank>)
# --socket=[String] (default: <blank>)
# --user=[String] (default: <blank>)
# --password=[String] (default: <blank>)
# --defaults-file=[String] (default: <blank>)
# --defaults-extra-file=[String] (default: <blank>)
# --defaults-group-suffix=[String] (default: <blank>)
# --dump-label=[String] (default: "default")
# used as suffix of the dump dir to differenciate multiple instances
# --compress=<gzip|pigz|bzip2|xz|none> (default: "gzip")
# Other options after -- are passed as-is to mysqldump
#######################################################################
dump_mysql_per_base() {
local option_port=""
local option_socket=""
local option_defaults_file=""
local option_defaults_extra_file=""
local option_defaults_group_suffix=""
local option_user=""
local option_password=""
local option_dump_label=""
local option_compress=""
local option_others=""
# Parse options, based on https://gist.github.com/deshion/10d3cb5f88a21671e17a
while :; do
case ${1:-''} in
--defaults-file)
# defaults-file options, with value separated by space
if [ -n "$2" ]; then
option_defaults_file="${2}"
shift
else
log_error "LOCAL_TASKS - ${FUNCNAME[0]}: '--defaults-file' requires a non-empty option argument."
exit 1
fi
;;
--defaults-file=?*)
# defaults-file options, with value separated by =
option_defaults_file="${1#*=}"
;;
--defaults-file=)
# defaults-file options, without value
log_error "LOCAL_TASKS - ${FUNCNAME[0]}: '--defaults-file' requires a non-empty option argument."
exit 1
;;
--defaults-extra-file)
# defaults-file options, with value separated by space
if [ -n "$2" ]; then
option_defaults_extra_file="${2}"
shift
else
log_error "LOCAL_TASKS - ${FUNCNAME[0]}: '--defaults-file' requires a non-empty option argument."
exit 1
fi
;;
--defaults-extra-file=?*)
# defaults-extra-file options, with value separated by =
option_defaults_extra_file="${1#*=}"
;;
--defaults-extra-file=)
# defaults-extra-file options, without value
log_error "LOCAL_TASKS - ${FUNCNAME[0]}: '--defaults-extra-file' requires a non-empty option argument."
exit 1
;;
--defaults-group-suffix)
# defaults-group-suffix options, with value separated by space
if [ -n "$2" ]; then
option_defaults_group_suffix="${2}"
shift
else
log_error "LOCAL_TASKS - ${FUNCNAME[0]}: '--defaults-group-suffix' requires a non-empty option argument."
exit 1
fi
;;
--defaults-group-suffix=?*)
# defaults-group-suffix options, with value separated by =
option_defaults_group_suffix="${1#*=}"
;;
--defaults-group-suffix=)
# defaults-group-suffix options, without value
log_error "LOCAL_TASKS - ${FUNCNAME[0]}: '--defaults-group-suffix' requires a non-empty option argument."
exit 1
;;
--port)
# port options, with value separated by space
if [ -n "$2" ]; then
option_port="${2}"
shift
else
log_error "LOCAL_TASKS - ${FUNCNAME[0]}: '--port' requires a non-empty option argument."
exit 1
fi
;;
--port=?*)
# port options, with value separated by =
option_port="${1#*=}"
;;
--port=)
# port options, without value
log_error "LOCAL_TASKS - ${FUNCNAME[0]}: '--port' requires a non-empty option argument."
exit 1
;;
--socket)
# socket options, with value separated by space
if [ -n "$2" ]; then
option_socket="${2}"
shift
else
log_error "LOCAL_TASKS - ${FUNCNAME[0]}: '--socket' requires a non-empty option argument."
exit 1
fi
;;
--socket=?*)
# socket options, with value separated by =
option_socket="${1#*=}"
;;
--socket=)
# socket options, without value
log_error "LOCAL_TASKS - ${FUNCNAME[0]}: '--socket' requires a non-empty option argument."
exit 1
;;
--user)
# user options, with value separated by space
if [ -n "$2" ]; then
option_user="${2}"
shift
else
log_error "LOCAL_TASKS - ${FUNCNAME[0]}: '--user' requires a non-empty option argument."
exit 1
fi
;;
--user=?*)
# user options, with value separated by =
option_user="${1#*=}"
;;
--user=)
# user options, without value
log_error "LOCAL_TASKS - ${FUNCNAME[0]}: '--user' requires a non-empty option argument."
exit 1
;;
--password)
# password options, with value separated by space
if [ -n "$2" ]; then
option_password="${2}"
shift
else
log_error "LOCAL_TASKS - ${FUNCNAME[0]}: '--password' requires a non-empty option argument."
exit 1
fi
;;
--password=?*)
# password options, with value separated by =
option_password="${1#*=}"
;;
--password=)
# password options, without value
log_error "LOCAL_TASKS - ${FUNCNAME[0]}: '--password' requires a non-empty option argument."
exit 1
;;
--dump-label)
# dump-label options, with value separated by space
if [ -n "$2" ]; then
option_dump_label="${2}"
shift
else
log_error "LOCAL_TASKS - ${FUNCNAME[0]}: '--dump-label' requires a non-empty option argument."
exit 1
fi
;;
--dump-label=?*)
# dump-label options, with value separated by =
option_dump_label="${1#*=}"
;;
--dump-label=)
# dump-label options, without value
log_error "LOCAL_TASKS - ${FUNCNAME[0]}: '--dump-label' requires a non-empty option argument."
exit 1
;;
--compress)
# compress options, with value separated by space
if [ -n "$2" ]; then
option_compress="${2}"
shift
else
log_error "LOCAL_TASKS - ${FUNCNAME[0]}: '--compress' requires a non-empty option argument."
exit 1
fi
;;
--compress=?*)
# compress options, with value separated by =
option_compress="${1#*=}"
;;
--compress=)
# compress options, without value
log_error "LOCAL_TASKS - ${FUNCNAME[0]}: '--compress' requires a non-empty option argument."
exit 1
;;
--)
# End of all options.
shift
option_others=${*}
break
;;
-?*|[[:alnum:]]*)
# ignore unknown options
log_error "LOCAL_TASKS - ${FUNCNAME[0]}: unknown option '${1}' (ignored)"
;;
*)
# Default case: If no more options then break out of the loop.
break
;;
esac
shift
done
case "${option_compress}" in
none)
compress_cmd="cat"
dump_ext=""
;;
bzip2|bz|bz2)
compress_cmd="bzip2 --best"
dump_ext=".bz"
;;
xz)
compress_cmd="xz --best"
dump_ext=".xz"
;;
pigz)
compress_cmd="pigz --best"
dump_ext=".gz"
;;
gz|gzip|*)
compress_cmd="gzip --best"
dump_ext=".gz"
;;
esac
if [ -z "${option_dump_label}" ]; then
if [ -n "${option_defaults_group_suffix}" ]; then
option_dump_label="${option_defaults_group_suffix}"
elif [ -n "${option_port}" ]; then
option_dump_label="${option_port}"
elif [ -n "${option_socket}" ]; then
option_dump_label=$(path_to_str "${option_socket}")
else
option_dump_label="default"
fi
fi
## Connection options
declare -a connect_options
connect_options=()
if [ -n "${option_defaults_file}" ]; then
connect_options+=(--defaults-file="${option_defaults_file}")
fi
if [ -n "${option_defaults_extra_file}" ]; then
connect_options+=(--defaults-extra-file="${option_defaults_extra_file}")
fi
if [ -n "${option_defaults_group_suffix}" ]; then
connect_options+=(--defaults-group-suffix="${option_defaults_group_suffix}")
fi
if [ -n "${option_port}" ]; then
connect_options+=(--protocol=tcp)
connect_options+=(--port="${option_port}")
fi
if [ -n "${option_socket}" ]; then
connect_options+=(--protocol=socket)
connect_options+=(--socket="${option_socket}")
fi
if [ -n "${option_user}" ]; then
connect_options+=(--user="${option_user}")
fi
if [ -n "${option_password}" ]; then
connect_options+=(--password="${option_password}")
fi
local dump_dir="${LOCAL_BACKUP_DIR}/mysql-${option_dump_label}-per-base"
local errors_dir=$(errors_dir_from_dump_dir "${dump_dir}")
rm -rf "${dump_dir}" "${errors_dir}"
mkdir -p "${dump_dir}" "${errors_dir}"
# No need to change recursively, the top directory is enough
chmod 700 "${dump_dir}" "${errors_dir}"
databases=$(mysql "${connect_options[@]}" --execute="show databases" --silent --skip-column-names \
| grep --extended-regexp --invert-match "^(Database|information_schema|performance_schema|sys)")
for database in ${databases}; do
local error_file="${errors_dir}/${database}.err"
local dump_file="${dump_dir}/${database}.sql${dump_ext}"
log "LOCAL_TASKS - ${FUNCNAME[0]}: start ${dump_file}"
declare -a dump_options
dump_options=()
dump_options+=(--opt)
dump_options+=(--force)
dump_options+=(--events)
dump_options+=(--hex-blob)
dump_options+=(--databases "${database}")
if [ -n "${option_others}" ]; then
# word splitting is deliberate here
# shellcheck disable=SC2206
dump_options+=(${option_others})
fi
## WARNING : logging and executing the command must be separate
## because otherwise Bash would interpret | and > as strings and not syntax.
log "LOCAL_TASKS - ${FUNCNAME[0]}: mysqldump ${connect_options[*]} ${dump_options[*]} | ${compress_cmd} > ${dump_file}"
mysqldump "${connect_options[@]}" "${dump_options[@]}" 2> "${error_file}" | ${compress_cmd} > "${dump_file}"
local last_rc=$?
# shellcheck disable=SC2086
if [ ${last_rc} -ne 0 ]; then
log_error "LOCAL_TASKS - ${FUNCNAME[0]}: mysqldump returned an error ${last_rc}" "${error_file}"
GLOBAL_RC=${E_DUMPFAILED}
else
rm -f "${error_file}"
fi
log "LOCAL_TASKS - ${FUNCNAME[0]}: stop ${dump_file}"
## Schema only (no data) for each databases
local error_file="${errors_dir}/${database}.schema.err"
local dump_file="${dump_dir}/${database}.schema.sql"
log "LOCAL_TASKS - ${FUNCNAME[0]}: start ${dump_file}"
declare -a dump_options
dump_options=()
dump_options+=(--force)
dump_options+=(--no-data)
dump_options+=(--databases "${database}")
if [ -n "${option_others}" ]; then
# word splitting is deliberate here
# shellcheck disable=SC2206
dump_options+=(${option_others})
fi
dump_cmd="mysqldump ${connect_options[*]} ${dump_options[*]}"
log "LOCAL_TASKS - ${FUNCNAME[0]}: ${dump_cmd}"
${dump_cmd} 2> "${error_file}" > "${dump_file}"
local last_rc=$?
# shellcheck disable=SC2086
if [ ${last_rc} -ne 0 ]; then
log_error "LOCAL_TASKS - ${FUNCNAME[0]}: mysqldump returned an error ${last_rc}" "${error_file}"
GLOBAL_RC=${E_DUMPFAILED}
else
rm -f "${error_file}"
fi
log "LOCAL_TASKS - ${FUNCNAME[0]}: stop ${dump_file}"
done
}
#######################################################################
# Dump "tabs style" separate schema/data for each database of an instance
#
# Arguments:
# --port=[Integer] (default: <blank>)
# --socket=[String] (default: <blank>)
# --user=[String] (default: <blank>)
# --password=[String] (default: <blank>)
# --defaults-file=[String] (default: <blank>)
# --defaults-extra-file=[String] (default: <blank>)
# --defaults-group-suffix=[String] (default: <blank>)
# --dump-label=[String] (default: "default")
# used as suffix of the dump dir to differenciate multiple instances
# --compress=<gzip|pigz|bzip2|xz|none> (default: "gzip")
# Other options after -- are passed as-is to mysqldump
#######################################################################
dump_mysql_tabs() {
local option_port=""
local option_socket=""
local option_defaults_file=""
local option_defaults_extra_file=""
local option_defaults_group_suffix=""
local option_user=""
local option_password=""
local option_dump_label=""
local option_compress=""
local option_others=""
# Parse options, based on https://gist.github.com/deshion/10d3cb5f88a21671e17a
while :; do
case ${1:-''} in
--defaults-file)
# defaults-file options, with value separated by space
if [ -n "$2" ]; then
option_defaults_file="${2}"
shift
else
log_error "LOCAL_TASKS - ${FUNCNAME[0]}: '--defaults-file' requires a non-empty option argument."
exit 1
fi
;;
--defaults-file=?*)
# defaults-file options, with value separated by =
option_defaults_file="${1#*=}"
;;
--defaults-file=)
# defaults-file options, without value
log_error "LOCAL_TASKS - ${FUNCNAME[0]}: '--defaults-file' requires a non-empty option argument."
exit 1
;;
--defaults-extra-file)
# defaults-file options, with value separated by space
if [ -n "$2" ]; then
option_defaults_extra_file="${2}"
shift
else
log_error "LOCAL_TASKS - ${FUNCNAME[0]}: '--defaults-file' requires a non-empty option argument."
exit 1
fi
;;
--defaults-extra-file=?*)
# defaults-extra-file options, with value separated by =
option_defaults_extra_file="${1#*=}"
;;
--defaults-extra-file=)
# defaults-extra-file options, without value
log_error "LOCAL_TASKS - ${FUNCNAME[0]}: '--defaults-extra-file' requires a non-empty option argument."
exit 1
;;
--defaults-group-suffix)
# defaults-group-suffix options, with value separated by space
if [ -n "$2" ]; then
option_defaults_group_suffix="${2}"
shift
else
log_error "LOCAL_TASKS - ${FUNCNAME[0]}: '--defaults-group-suffix' requires a non-empty option argument."
exit 1
fi
;;
--defaults-group-suffix=?*)
# defaults-group-suffix options, with value separated by =
option_defaults_group_suffix="${1#*=}"
;;
--defaults-group-suffix=)
# defaults-group-suffix options, without value
log_error "LOCAL_TASKS - ${FUNCNAME[0]}: '--defaults-group-suffix' requires a non-empty option argument."
exit 1
;;
--port)
# port options, with value separated by space
if [ -n "$2" ]; then
option_port="${2}"
shift
else
log_error "LOCAL_TASKS - ${FUNCNAME[0]}: '--port' requires a non-empty option argument."
exit 1
fi
;;
--port=?*)
# port options, with value separated by =
option_port="${1#*=}"
;;
--port=)
# port options, without value
log_error "LOCAL_TASKS - ${FUNCNAME[0]}: '--port' requires a non-empty option argument."
exit 1
;;
--socket)
# socket options, with value separated by space
if [ -n "$2" ]; then
option_socket="${2}"
shift
else
log_error "LOCAL_TASKS - ${FUNCNAME[0]}: '--socket' requires a non-empty option argument."
exit 1
fi
;;
--socket=?*)
# socket options, with value separated by =
option_socket="${1#*=}"
;;
--socket=)
# socket options, without value
log_error "LOCAL_TASKS - ${FUNCNAME[0]}: '--socket' requires a non-empty option argument."
exit 1
;;
--user)
# user options, with value separated by space
if [ -n "$2" ]; then
option_user="${2}"
shift
else
log_error "LOCAL_TASKS - ${FUNCNAME[0]}: '--user' requires a non-empty option argument."
exit 1
fi
;;
--user=?*)
# user options, with value separated by =
option_user="${1#*=}"
;;
--user=)
# user options, without value
log_error "LOCAL_TASKS - ${FUNCNAME[0]}: '--user' requires a non-empty option argument."
exit 1
;;
--password)
# password options, with value separated by space
if [ -n "$2" ]; then
option_password="${2}"
shift
else
log_error "LOCAL_TASKS - ${FUNCNAME[0]}: '--password' requires a non-empty option argument."
exit 1
fi
;;
--password=?*)
# password options, with value separated by =
option_password="${1#*=}"
;;
--password=)
# password options, without value
log_error "LOCAL_TASKS - ${FUNCNAME[0]}: '--password' requires a non-empty option argument."
exit 1
;;
--dump-label)
# dump-label options, with value separated by space
if [ -n "$2" ]; then
option_dump_label="${2}"
shift
else
log_error "LOCAL_TASKS - ${FUNCNAME[0]}: '--dump-label' requires a non-empty option argument."
exit 1
fi
;;
--dump-label=?*)
# dump-label options, with value separated by =
option_dump_label="${1#*=}"
;;
--dump-label=)
# dump-label options, without value
log_error "LOCAL_TASKS - ${FUNCNAME[0]}: '--dump-label' requires a non-empty option argument."
exit 1
;;
--compress)
# compress options, with value separated by space
if [ -n "$2" ]; then
option_compress="${2}"
shift
else
log_error "LOCAL_TASKS - ${FUNCNAME[0]}: '--compress' requires a non-empty option argument."
exit 1
fi
;;
--compress=?*)
# compress options, with value separated by =
option_compress="${1#*=}"
;;
--compress=)
# compress options, without value
log_error "LOCAL_TASKS - ${FUNCNAME[0]}: '--compress' requires a non-empty option argument."
exit 1
;;
--)
# End of all options.
shift
option_others=${*}
break
;;
-?*|[[:alnum:]]*)
# ignore unknown options
log_error "LOCAL_TASKS - ${FUNCNAME[0]}: unknown option '${1}' (ignored)"
;;
*)
# Default case: If no more options then break out of the loop.
break
;;
esac
shift
done
case "${option_compress}" in
none)
compress_cmd="cat"
dump_ext=""
;;
bzip2|bz|bz2)
compress_cmd="bzip2 --best"
dump_ext=".bz"
;;
xz)
compress_cmd="xz --best"
dump_ext=".xz"
;;
pigz)
compress_cmd="pigz --best"
dump_ext=".gz"
;;
gz|gzip|*)
compress_cmd="gzip --best"
dump_ext=".gz"
;;
esac
if [ -z "${option_dump_label}" ]; then
if [ -n "${option_defaults_group_suffix}" ]; then
option_dump_label="${option_defaults_group_suffix}"
elif [ -n "${option_port}" ]; then
option_dump_label="${option_port}"
elif [ -n "${option_socket}" ]; then
option_dump_label=$(path_to_str "${option_socket}")
else
option_dump_label="default"
fi
fi
## Connection options
declare -a connect_options
connect_options=()
if [ -n "${option_defaults_file}" ]; then
connect_options+=(--defaults-file="${option_defaults_file}")
fi
if [ -n "${option_defaults_extra_file}" ]; then
connect_options+=(--defaults-extra-file="${option_defaults_extra_file}")
fi
if [ -n "${option_defaults_group_suffix}" ]; then
connect_options+=(--defaults-group-suffix="${option_defaults_group_suffix}")
fi
if [ -n "${option_port}" ]; then
connect_options+=(--protocol=tcp)
connect_options+=(--port="${option_port}")
fi
if [ -n "${option_socket}" ]; then
connect_options+=(--protocol=socket)
connect_options+=(--socket="${option_socket}")
fi
if [ -n "${option_user}" ]; then
connect_options+=(--user="${option_user}")
fi
if [ -n "${option_password}" ]; then
connect_options+=(--password="${option_password}")
fi
databases=$(mysql "${connect_options[@]}" --execute="show databases" --silent --skip-column-names \
| grep --extended-regexp --invert-match "^(Database|information_schema|performance_schema|sys)")
for database in ${databases}; do
local dump_dir="${LOCAL_BACKUP_DIR}/mysql-${option_dump_label}-tabs/${database}"
local errors_dir=$(errors_dir_from_dump_dir "${dump_dir}")
rm -rf "${dump_dir}" "${errors_dir}"
mkdir -p "${dump_dir}" "${errors_dir}"
# No need to change recursively, the top directory is enough
chmod 700 "${dump_dir}" "${errors_dir}"
chown -RL mysql "${dump_dir}"
local error_file="${errors_dir}.err"
log "LOCAL_TASKS - ${FUNCNAME[0]}: start ${dump_dir}"
declare -a dump_options
dump_options=()
dump_options+=(--force)
dump_options+=(--quote-names)
dump_options+=(--opt)
dump_options+=(--events)
dump_options+=(--hex-blob)
dump_options+=(--skip-comments)
dump_options+=(--fields-enclosed-by='\"')
dump_options+=(--fields-terminated-by=',')
dump_options+=(--tab="${dump_dir}")
if [ -n "${option_others}" ]; then
# word splitting is deliberate here
# shellcheck disable=SC2206
dump_options+=(${option_others})
fi
dump_options+=("${database}")
dump_cmd="mysqldump ${connect_options[*]} ${dump_options[*]}"
log "LOCAL_TASKS - ${FUNCNAME[0]}: ${dump_cmd}"
${dump_cmd} 2> "${error_file}"
local last_rc=$?
# shellcheck disable=SC2086
if [ ${last_rc} -ne 0 ]; then
log_error "LOCAL_TASKS - ${FUNCNAME[0]}: mysqldump to ${dump_dir} returned an error ${last_rc}" "${error_file}"
GLOBAL_RC=${E_DUMPFAILED}
else
rm -f "${error_file}"
fi
log "LOCAL_TASKS - ${FUNCNAME[0]}: stop ${dump_dir}"
done
}

View file

@ -0,0 +1,343 @@
#!/usr/bin/env bash
# shellcheck disable=SC2034,SC2317,SC2155
#######################################################################
# Dump a single file of all PostgreSQL databases
#
# Arguments:
# --dump-label=[String] (default: "default")
# used as suffix of the dump dir to differenciate multiple instances
# --compress=<gzip|pigz|bzip2|xz|none> (default: "gzip")
# Other options after -- are passed as-is to pg_dump
#######################################################################
dump_postgresql_global() {
local option_dump_label=""
local option_compress=""
local option_others=""
# Parse options, based on https://gist.github.com/deshion/10d3cb5f88a21671e17a
while :; do
case ${1:-''} in
--dump-label)
# dump-label options, with value separated by space
if [ -n "$2" ]; then
option_dump_label="${2}"
shift
else
log_error "LOCAL_TASKS - ${FUNCNAME[0]}: '--dump-label' requires a non-empty option argument."
exit 1
fi
;;
--dump-label=?*)
# dump-label options, with value separated by =
option_dump_label="${1#*=}"
;;
--dump-label=)
# dump-label options, without value
log_error "LOCAL_TASKS - ${FUNCNAME[0]}: '--dump-label' requires a non-empty option argument."
exit 1
;;
--compress)
# compress options, with value separated by space
if [ -n "$2" ]; then
option_compress="${2}"
shift
else
log_error "LOCAL_TASKS - ${FUNCNAME[0]}: '--compress' requires a non-empty option argument."
exit 1
fi
;;
--compress=?*)
# compress options, with value separated by =
option_compress="${1#*=}"
;;
--compress=)
# compress options, without value
log_error "LOCAL_TASKS - ${FUNCNAME[0]}: '--compress' requires a non-empty option argument."
exit 1
;;
--)
# End of all options.
shift
option_others=${*}
break
;;
-?*|[[:alnum:]]*)
# ignore unknown options
log_error "LOCAL_TASKS - ${FUNCNAME[0]}: unknown option '${1}' (ignored)"
;;
*)
# Default case: If no more options then break out of the loop.
break
;;
esac
shift
done
case "${option_compress}" in
none)
compress_cmd="cat"
dump_ext=""
;;
bzip2|bz|bz2)
compress_cmd="bzip2 --best"
dump_ext=".bz"
;;
xz)
compress_cmd="xz --best"
dump_ext=".xz"
;;
pigz)
compress_cmd="pigz --best"
dump_ext=".gz"
;;
gz|gzip|*)
compress_cmd="gzip --best"
dump_ext=".gz"
;;
esac
if [ -z "${option_dump_label}" ]; then
if [ -n "${option_defaults_group_suffix}" ]; then
option_dump_label="${option_defaults_group_suffix}"
elif [ -n "${option_port}" ]; then
option_dump_label="${option_port}"
elif [ -n "${option_socket}" ]; then
option_dump_label=$(path_to_str "${option_socket}")
else
option_dump_label="default"
fi
fi
local dump_dir="${LOCAL_BACKUP_DIR}/postgresql-${option_dump_label}-global"
local errors_dir=$(errors_dir_from_dump_dir "${dump_dir}")
rm -rf "${dump_dir}" "${errors_dir}"
mkdir -p "${dump_dir}" "${errors_dir}"
# No need to change recursively, the top directory is enough
chmod 700 "${dump_dir}" "${errors_dir}"
## example with pg_dumpall and with compression
local error_file="${errors_dir}/pg_dumpall.err"
local dump_file="${dump_dir}/pg_dumpall.sql${dump_ext}"
log "LOCAL_TASKS - ${FUNCNAME[0]}: start ${dump_file}"
declare -a dump_options
dump_options=()
if [ -n "${option_others}" ]; then
# word splitting is deliberate here
# shellcheck disable=SC2206
dump_options+=(${option_others})
fi
dump_cmd="(sudo -u postgres pg_dumpall ${dump_options[*]}) 2> ${error_file} | ${compress_cmd} > ${dump_file}"
log "LOCAL_TASKS - ${FUNCNAME[0]}: ${dump_cmd}"
${dump_cmd}
local last_rc=$?
# shellcheck disable=SC2086
if [ ${last_rc} -ne 0 ]; then
log_error "LOCAL_TASKS - ${FUNCNAME[0]}: pg_dumpall to ${dump_file} returned an error ${last_rc}" "${error_file}"
GLOBAL_RC=${E_DUMPFAILED}
else
rm -f "${error_file}"
fi
log "LOCAL_TASKS - ${FUNCNAME[0]}: stop ${dump_file}"
## example with pg_dumpall and without compression
## WARNING: you need space in ~postgres
# local error_file="${errors_dir}/pg_dumpall.err"
# local dump_file="${dump_dir}/pg_dumpall.sql"
# log "LOCAL_TASKS - ${FUNCNAME[0]}: start ${dump_file}"
#
# (su - postgres -c "pg_dumpall > ~/pg.dump.bak") 2> "${error_file}"
# mv ~postgres/pg.dump.bak "${dump_file}"
#
# log "LOCAL_TASKS - ${FUNCNAME[0]}: stop ${dump_file}"
}
#######################################################################
# Dump a compressed file per database
#
# Arguments: <none>
#######################################################################
dump_postgresql_per_base() {
local option_dump_label=""
local option_compress=""
local option_others=""
# Parse options, based on https://gist.github.com/deshion/10d3cb5f88a21671e17a
while :; do
case ${1:-''} in
--dump-label)
# dump-label options, with value separated by space
if [ -n "$2" ]; then
option_dump_label="${2}"
shift
else
log_error "LOCAL_TASKS - ${FUNCNAME[0]}: '--dump-label' requires a non-empty option argument."
exit 1
fi
;;
--dump-label=?*)
# dump-label options, with value separated by =
option_dump_label="${1#*=}"
;;
--dump-label=)
# dump-label options, without value
log_error "LOCAL_TASKS - ${FUNCNAME[0]}: '--dump-label' requires a non-empty option argument."
exit 1
;;
--compress)
# compress options, with value separated by space
if [ -n "$2" ]; then
option_compress="${2}"
shift
else
log_error "LOCAL_TASKS - ${FUNCNAME[0]}: '--compress' requires a non-empty option argument."
exit 1
fi
;;
--compress=?*)
# compress options, with value separated by =
option_compress="${1#*=}"
;;
--compress=)
# compress options, without value
log_error "LOCAL_TASKS - ${FUNCNAME[0]}: '--compress' requires a non-empty option argument."
exit 1
;;
--)
# End of all options.
shift
option_others=${*}
break
;;
-?*|[[:alnum:]]*)
# ignore unknown options
log_error "LOCAL_TASKS - ${FUNCNAME[0]}: unknown option '${1}' (ignored)"
;;
*)
# Default case: If no more options then break out of the loop.
break
;;
esac
shift
done
case "${option_compress}" in
none)
compress_cmd="cat"
dump_ext=""
;;
bzip2|bz|bz2)
compress_cmd="bzip2 --best"
dump_ext=".bz"
;;
xz)
compress_cmd="xz --best"
dump_ext=".xz"
;;
pigz)
compress_cmd="pigz --best"
dump_ext=".gz"
;;
gz|gzip|*)
compress_cmd="gzip --best"
dump_ext=".gz"
;;
esac
if [ -z "${option_dump_label}" ]; then
if [ -n "${option_defaults_group_suffix}" ]; then
option_dump_label="${option_defaults_group_suffix}"
elif [ -n "${option_port}" ]; then
option_dump_label="${option_port}"
elif [ -n "${option_socket}" ]; then
option_dump_label=$(path_to_str "${option_socket}")
else
option_dump_label="default"
fi
fi
local dump_dir="${LOCAL_BACKUP_DIR}/postgresql-${option_dump_label}-per-base"
local errors_dir=$(errors_dir_from_dump_dir "${dump_dir}")
rm -rf "${dump_dir}" "${errors_dir}"
mkdir -p "${dump_dir}" "${errors_dir}"
# No need to change recursively, the top directory is enough
chmod 700 "${dump_dir}" "${errors_dir}"
(
# shellcheck disable=SC2164
cd /var/lib/postgresql
databases=$(sudo -u postgres psql -U postgres -lt | awk -F \| '{print $1}' | grep -v "template.*")
for database in ${databases} ; do
local error_file="${errors_dir}/${database}.err"
local dump_file="${dump_dir}/${database}.sql${dump_ext}"
log "LOCAL_TASKS - ${FUNCNAME[0]}: start ${dump_file}"
declare -a dump_options
dump_options=()
dump_options+=(--create)
dump_options+=(-U postgres)
dump_options+=(-d "${database}")
if [ -n "${option_others}" ]; then
# word splitting is deliberate here
# shellcheck disable=SC2206
dump_options+=(${option_others})
fi
dump_cmd="(sudo -u postgres /usr/bin/pg_dump ${dump_options[*]}) 2> ${error_file} | ${compress_cmd} > ${dump_file}"
log "LOCAL_TASKS - ${FUNCNAME[0]}: ${dump_cmd}"
${dump_cmd}
local last_rc=$?
# shellcheck disable=SC2086
if [ ${last_rc} -ne 0 ]; then
log_error "LOCAL_TASKS - ${FUNCNAME[0]}: pg_dump to ${dump_file} returned an error ${last_rc}" "${error_file}"
GLOBAL_RC=${E_DUMPFAILED}
else
rm -f "${error_file}"
fi
log "LOCAL_TASKS - ${FUNCNAME[0]}: stop ${dump_file}"
done
)
}
#######################################################################
# Dump a compressed file per database
#
# Arguments: <none>
#
# TODO: add arguments to include/exclude tables
#######################################################################
dump_postgresql_filtered() {
local dump_dir="${LOCAL_BACKUP_DIR}/postgresql-filtered"
local errors_dir=$(errors_dir_from_dump_dir "${dump_dir}")
rm -rf "${dump_dir}" "${errors_dir}"
mkdir -p "${dump_dir}" "${errors_dir}"
# No need to change recursively, the top directory is enough
chmod 700 "${dump_dir}" "${errors_dir}"
local error_file="${errors_dir}/pg-backup.err"
local dump_file="${dump_dir}/pg-backup.tar"
log "LOCAL_TASKS - ${FUNCNAME[0]}: start ${dump_file}"
## example with all tables from MYBASE excepts TABLE1 and TABLE2
# pg_dump -p 5432 -h 127.0.0.1 -U USER --clean -F t --inserts -f "${dump_file}" -t 'TABLE1' -t 'TABLE2' MYBASE 2> "${error_file}"
## example with only TABLE1 and TABLE2 from MYBASE
# pg_dump -p 5432 -h 127.0.0.1 -U USER --clean -F t --inserts -f "${dump_file}" -T 'TABLE1' -T 'TABLE2' MYBASE 2> "${error_file}"
local last_rc=$?
# shellcheck disable=SC2086
if [ ${last_rc} -ne 0 ]; then
log_error "LOCAL_TASKS - ${FUNCNAME[0]}: pg_dump to ${dump_file} returned an error ${last_rc}" "${error_file}"
GLOBAL_RC=${E_DUMPFAILED}
else
rm -f "${error_file}"
fi
log "LOCAL_TASKS - ${FUNCNAME[0]}: stop ${dump_file}"
}

466
client/lib/main.sh Normal file
View file

@ -0,0 +1,466 @@
#!/usr/bin/env bash
# shellcheck disable=SC2034,SC2317
readonly VERSION="24.05.1"
# set all programs to C language (english)
export LC_ALL=C
# If expansion is attempted on an unset variable or parameter, the shell prints an
# error message, and, if not interactive, exits with a non-zero status.
set -o nounset
# The pipeline's return status is the value of the last (rightmost) command
# to exit with a non-zero status, or zero if all commands exit successfully.
set -o pipefail
# Enable trace mode if called with environment variable TRACE=1
if [[ "${TRACE-0}" == "1" ]]; then
set -o xtrace
fi
source "${LIBDIR}/utilities.sh"
source "${LIBDIR}/dump/elasticsearch.sh"
source "${LIBDIR}/dump/mysql.sh"
source "${LIBDIR}/dump/postgresql.sh"
source "${LIBDIR}/dump/misc.sh"
# Called from main, it is wrapping the local_tasks function defined in the real script
local_tasks_wrapper() {
log "START LOCAL_TASKS"
# Remove old log directories (recursively)
find "${LOCAL_BACKUP_DIR}/" -type d -name "${PROGNAME}.errors-*" -ctime +30 -exec rm -rf \;
local_tasks_type="$(type -t local_tasks)"
if [ "${local_tasks_type}" = "function" ]; then
local_tasks
else
log_error "There is no 'local_tasks' function to execute"
fi
# TODO: check if this is still needed
# print_error_files_content
log "STOP LOCAL_TASKS"
}
# Called from main, it is wrapping the sync_tasks function defined in the real script
sync_tasks_wrapper() {
declare -a SERVERS # Indexed array for server/port values
declare -a RSYNC_INCLUDES # Indexed array for includes
declare -a RSYNC_EXCLUDES # Indexed array for excludes
case "${SYSTEM}" in
linux)
# NOTE: remember to single-quote paths if they contain globs (*)
# and you want to defer expansion
declare -a rsync_default_includes=(
/bin
/boot
/lib
/opt
/sbin
/usr
)
;;
*bsd)
# NOTE: remember to single-quote paths if they contain globs (*)
# and you want to defer expansion
declare -a rsync_default_includes=(
/bin
/bsd
/sbin
/usr
)
;;
*)
echo "Unknown system '${SYSTEM}'" >&2
exit 1
;;
esac
if [ -f "${CANARY_FILE}" ]; then
rsync_default_includes+=("${CANARY_FILE}")
fi
readonly rsync_default_includes
# NOTE: remember to single-quote paths if they contain globs (*)
# and you want to defer expansion
declare -a rsync_default_excludes=(
/dev
/proc
/run
/sys
/tmp
/usr/doc
/usr/obj
/usr/share/doc
/usr/src
/var/apt
/var/cache
'/var/db/munin/*.tmp'
/var/lib/amavis/amavisd.sock
/var/lib/amavis/tmp
/var/lib/amavis/virusmails
'/var/lib/clamav/*.tmp'
/var/lib/elasticsearch
/var/lib/metche
/var/lib/mongodb
'/var/lib/munin/*tmp*'
/var/lib/mysql
/var/lib/php/sessions
/var/lib/php5
/var/lib/postgres
/var/lib/postgresql
/var/lib/sympa
/var/lock
/var/run
/var/spool/postfix
/var/spool/smtpd
/var/spool/squid
/var/state
/var/tmp
lost+found
'.nfs.*'
'lxc/*/rootfs/tmp'
'lxc/*/rootfs/usr/doc'
'lxc/*/rootfs/usr/obj'
'lxc/*/rootfs/usr/share/doc'
'lxc/*/rootfs/usr/src'
'lxc/*/rootfs/var/apt'
'lxc/*/rootfs/var/cache'
'lxc/*/rootfs/var/lib/php5'
'lxc/*/rootfs/var/lib/php/sessions'
'lxc/*/rootfs/var/lock'
'lxc/*/rootfs/var/run'
'lxc/*/rootfs/var/state'
'lxc/*/rootfs/var/tmp'
/home/mysqltmp
)
readonly rsync_default_excludes
sync_tasks_type="$(type -t sync_tasks)"
if [ "${sync_tasks_type}" = "function" ]; then
sync_tasks
else
log_error "There is no 'sync_tasks' function to execute"
fi
}
sync() {
local sync_name=${1}
local -a rsync_servers=("${!2}")
local -a rsync_includes=("${!3}")
local -a rsync_excludes=("${!4}")
## Initialize variable to store SSH connection errors
declare -a SSH_ERRORS=()
log "START SYNC_TASKS - sync=${sync_name}"
# echo "### sync ###"
# for server in "${rsync_servers[@]}"; do
# echo "server: ${server}"
# done
# for include in "${rsync_includes[@]}"; do
# echo "include: ${include}"
# done
# for exclude in "${rsync_excludes[@]}"; do
# echo "exclude: ${exclude}"
# done
local -i n=0
local server=""
if [ "${SERVERS_FALLBACK}" = "1" ]; then
# We try to find a suitable server
while :; do
server=$(pick_server ${n} "${sync_name}")
rc=$?
if [ ${rc} != 0 ]; then
GLOBAL_RC=${E_NOSRVAVAIL}
log "STOP SYNC_TASKS - sync=${sync_name}'"
return
fi
if test_server "${server}"; then
break
else
server=""
n=$(( n + 1 ))
fi
done
else
# we force the server
server=$(pick_server "${n}" "${sync_name}")
fi
rsync_server=$(echo "${server}" | cut -d':' -f1)
rsync_port=$(echo "${server}" | cut -d':' -f2)
log "SYNC_TASKS - sync=${sync_name}: use ${server}"
# Rsync complete log file for the current run
RSYNC_LOGFILE="/var/log/${PROGNAME}.${sync_name}.rsync.log"
# Rsync stats for the current run
RSYNC_STATSFILE="/var/log/${PROGNAME}.${sync_name}.rsync-stats.log"
# reset Rsync log file
if [ -n "$(command -v truncate)" ]; then
truncate -s 0 "${RSYNC_LOGFILE}"
truncate -s 0 "${RSYNC_STATSFILE}"
else
printf "" > "${RSYNC_LOGFILE}"
printf "" > "${RSYNC_STATSFILE}"
fi
# Initialize variable here, we need it later
local -a mtree_files=()
if [ "${MTREE_ENABLED}" = "1" ]; then
mtree_bin=$(command -v mtree)
if [ -n "${mtree_bin}" ]; then
# Dump filesystem stats with mtree
log "SYNC_TASKS - sync=${sync_name}: start mtree"
# Loop over Rsync includes
for i in "${!rsync_includes[@]}"; do
include="${rsync_includes[i]}"
if [ -d "${include}" ]; then
# … but exclude for mtree what will be excluded by Rsync
mtree_excludes_file="$(mktemp --tmpdir "${PROGNAME}.${sync_name}.mtree-excludes.XXXXXX")"
add_to_temp_files "${mtree_excludes_file}"
for j in "${!rsync_excludes[@]}"; do
echo "${rsync_excludes[j]}" | grep -E "^([^/]|${include})" | sed -e "s|^${include}|.|" >> "${mtree_excludes_file}"
done
mtree_file="/var/log/evobackup.$(basename "${include}").mtree"
add_to_temp_files "${mtree_file}"
${mtree_bin} -x -c -p "${include}" -X "${mtree_excludes_file}" > "${mtree_file}"
mtree_files+=("${mtree_file}")
fi
done
if [ "${#mtree_files[@]}" -le 0 ]; then
log_error "SYNC_TASKS - ${sync_name}: ERROR: mtree didn't produce any file"
fi
log "SYNC_TASKS - sync=${sync_name}: stop mtree (files: ${mtree_files[*]})"
else
log "SYNC_TASKS - sync=${sync_name}: skip mtree (missing)"
fi
else
log "SYNC_TASKS - sync=${sync_name}: skip mtree (disabled)"
fi
rsync_bin=$(command -v rsync)
# Build the final Rsync command
# Rsync main options
rsync_main_args=()
rsync_main_args+=(--archive)
rsync_main_args+=(--itemize-changes)
rsync_main_args+=(--quiet)
rsync_main_args+=(--stats)
rsync_main_args+=(--human-readable)
rsync_main_args+=(--relative)
rsync_main_args+=(--partial)
rsync_main_args+=(--delete)
rsync_main_args+=(--delete-excluded)
rsync_main_args+=(--force)
rsync_main_args+=(--ignore-errors)
rsync_main_args+=(--log-file "${RSYNC_LOGFILE}")
rsync_main_args+=(--rsh "ssh -p ${rsync_port} -o 'ConnectTimeout ${SSH_CONNECT_TIMEOUT}'")
# Rsync excludes
for i in "${!rsync_excludes[@]}"; do
rsync_main_args+=(--exclude "${rsync_excludes[i]}")
done
# Rsync local sources
rsync_main_args+=("${rsync_includes[@]}")
# Rsync remote destination
rsync_main_args+=("root@${rsync_server}:${REMOTE_BACKUP_DIR}/")
# … log it
log "SYNC_TASKS - sync=${sync_name}: Rsync main command : ${rsync_bin} ${rsync_main_args[*]}"
# … execute it
${rsync_bin} "${rsync_main_args[@]}"
rsync_main_rc=$?
# Copy last lines of rsync log to the main log
tail -n 30 "${RSYNC_LOGFILE}" >> "${LOGFILE}"
# Copy Rsync stats to special file
tail -n 30 "${RSYNC_LOGFILE}" | grep --invert-match --extended-regexp " [\<\>ch\.\*]\S{10} " > "${RSYNC_STATSFILE}"
# We ignore rc=24 (vanished files)
if [ ${rsync_main_rc} -ne 0 ] && [ ${rsync_main_rc} -ne 24 ]; then
log_error "SYNC_TASKS - sync=${sync_name}: Rsync main command returned an error ${rsync_main_rc}" "${LOGFILE}"
GLOBAL_RC=${E_SYNCFAILED}
else
# Build the report Rsync command
local -a rsync_report_args
rsync_report_args=()
# Rsync options
rsync_report_args+=(--rsh "ssh -p ${rsync_port} -o 'ConnectTimeout ${SSH_CONNECT_TIMEOUT}'")
# Rsync local sources
if [ "${#mtree_files[@]}" -gt 0 ]; then
# send mtree files if there is any
rsync_report_args+=("${mtree_files[@]}")
fi
if [ -f "${RSYNC_LOGFILE}" ]; then
# send rsync full log file if it exists
rsync_report_args+=("${RSYNC_LOGFILE}")
fi
if [ -f "${RSYNC_STATSFILE}" ]; then
# send rsync stats log file if it exists
rsync_report_args+=("${RSYNC_STATSFILE}")
fi
# Rsync remote destination
rsync_report_args+=("root@${rsync_server}:${REMOTE_LOG_DIR}/")
# … log it
log "SYNC_TASKS - sync=${sync_name}: Rsync report command : ${rsync_bin} ${rsync_report_args[*]}"
# … execute it
${rsync_bin} "${rsync_report_args[@]}"
fi
log "STOP SYNC_TASKS - sync=${sync_name}"
}
setup() {
# Default return-code (0 == succes)
GLOBAL_RC=0
# Possible error codes
readonly E_NOSRVAVAIL=21 # No server is available
readonly E_SYNCFAILED=20 # Failed sync task
readonly E_DUMPFAILED=10 # Failed dump task
# explicit PATH
PATH=/sbin:/usr/sbin:/bin:/usr/bin:/usr/local/sbin:/usr/local/bin
# System name (linux, openbsd…)
: "${SYSTEM:=$(uname | tr '[:upper:]' '[:lower:]')}"
# Hostname (for logs and notifications)
: "${HOSTNAME:=$(hostname)}"
# Store pid in a file named after this program's name
: "${PROGNAME:=$(basename "$0")}"
: "${PIDFILE:="/var/run/${PROGNAME}.pid"}"
# Customize the log path if you want multiple scripts to have separate log files
: "${LOGFILE:="/var/log/evobackup.log"}"
# Canary file to update before executing tasks
: "${CANARY_FILE:="/zzz_evobackup_canary"}"
# Date format for log messages
: "${DATE_FORMAT:="%Y-%m-%d %H:%M:%S"}"
# Should we fallback on other servers when the first one is unreachable?
: "${SERVERS_FALLBACK:=1}"
# timeout (in seconds) for SSH connections
: "${SSH_CONNECT_TIMEOUT:=90}"
: "${LOCAL_BACKUP_DIR:="/home/backup"}"
# shellcheck disable=SC2174
mkdir -p -m 700 "${LOCAL_BACKUP_DIR}"
: "${ERRORS_DIR:="${LOCAL_BACKUP_DIR}/${PROGNAME}.errors-${START_TIME}"}"
# shellcheck disable=SC2174
mkdir -p -m 700 "${ERRORS_DIR}"
# Backup directory on remote server
: "${REMOTE_BACKUP_DIR:="/var/backup"}"
# Log directory in remote server
: "${REMOTE_LOG_DIR:="/var/log"}"
# Email address for notifications
: "${MAIL:="root"}"
# Email subject for notifications
: "${MAIL_SUBJECT:="[info] EvoBackup - Client ${HOSTNAME}"}"
# Enable/disable local tasks (default: enabled)
: "${LOCAL_TASKS:=1}"
# Enable/disable sync tasks (default: enabled)
: "${SYNC_TASKS:=1}"
# Enable/disable mtree (default: enabled)
: "${MTREE_ENABLED:=1}"
# If "setup_custom" exists and is a function, let's call it
setup_custom_type="$(type -t setup_custom)"
if [ "${setup_custom_type}" = "function" ]; then
setup_custom
fi
## Force umask
umask 077
# Initialize a list of temporary files
declare -a TEMP_FILES=()
# Any file in this list will be deleted when the program exits
trap "cleanup" EXIT
}
run_evobackup() {
# Start timer
START_EPOCH=$(/bin/date +%s)
START_TIME=$(/bin/date +"%Y%m%d%H%M%S")
# Configure variables and environment
setup
log "START GLOBAL - VERSION=${VERSION} LOCAL_TASKS=${LOCAL_TASKS} SYNC_TASKS=${SYNC_TASKS}"
# /!\ Only one backup processus can run at the sametime /!\
# Based on PID file, kill any running process before continuing
enforce_single_process "${PIDFILE}"
# Update canary to keep track of each run
update-evobackup-canary --who "${PROGNAME}" --file "${CANARY_FILE}"
if [ "${LOCAL_TASKS}" = "1" ]; then
local_tasks_wrapper
fi
if [ "${SYNC_TASKS}" = "1" ]; then
sync_tasks_wrapper
fi
STOP_EPOCH=$(/bin/date +%s)
case "${SYSTEM}" in
*bsd)
start_time=$(/bin/date -f "%s" -j "${START_EPOCH}" +"${DATE_FORMAT}")
stop_time=$(/bin/date -f "%s" -j "${STOP_EPOCH}" +"${DATE_FORMAT}")
;;
*)
start_time=$(/bin/date --date="@${START_EPOCH}" +"${DATE_FORMAT}")
stop_time=$(/bin/date --date="@${STOP_EPOCH}" +"${DATE_FORMAT}")
;;
esac
duration=$(( STOP_EPOCH - START_EPOCH ))
log "STOP GLOBAL - start='${start_time}' stop='${stop_time}' duration=${duration}s"
send_mail
exit ${GLOBAL_RC}
}

143
client/lib/utilities.sh Normal file
View file

@ -0,0 +1,143 @@
#!/usr/bin/env bash
# Output a message to the log file
log() {
local msg="${1:-$(cat /dev/stdin)}"
local pid=$$
printf "[%s] %s[%s]: %s\\n" \
"$(/bin/date +"${DATE_FORMAT}")" "${PROGNAME}" "${pid}" "${msg}" \
>> "${LOGFILE}"
}
log_error() {
local error_msg=${1}
local error_file=${2:-""}
if [ -n "${error_file}" ] && [ -f "${error_file}" ]; then
printf "\n### %s\n" "${error_msg}" >&2
# shellcheck disable=SC2046
if [ $(wc -l "${error_file}" | cut -d " " -f 1) -gt 30 ]; then
printf "~~~{%s (tail -30)}\n" "${error_file}" >&2
tail -n 30 "${error_file}" >&2
else
printf "~~~{%s}\n" "${error_file}" >&2
cat "${error_file}" >&2
fi
printf "~~~\n" >&2
log "${error_msg}, check ${error_file}"
else
printf "\n### %s\n" "${error_msg}" >&2
log "${error_msg}"
fi
}
add_to_temp_files() {
TEMP_FILES+=("${1}")
}
# Remove all temporary file created during the execution
cleanup() {
# shellcheck disable=SC2086
rm -f "${TEMP_FILES[@]}"
find "${ERRORS_DIR}" -type d -empty -delete
}
enforce_single_process() {
local pidfile=$1
if [ -e "${pidfile}" ]; then
pid=$(cat "${pidfile}")
# Does process still exist?
if kill -0 "${pid}" 2> /dev/null; then
# Killing the childs of evobackup.
for ppid in $(pgrep -P "${pid}"); do
kill -9 "${ppid}";
done
# Then kill the main PID.
kill -9 "${pid}"
printf "%s is still running (PID %s). Process has been killed" "$0" "${pid}\\n" >&2
else
rm -f "${pidfile}"
fi
fi
add_to_temp_files "${pidfile}"
echo "$$" > "${pidfile}"
}
# Build the error directory (inside ERRORS_DIR) based on the dump directory path
errors_dir_from_dump_dir() {
local dump_dir=$1
local relative_path=$(realpath --relative-to="${LOCAL_BACKUP_DIR}" "${dump_dir}")
# return absolute path
realpath --canonicalize-missing "${ERRORS_DIR}/${relative_path}"
}
# Call test_server with "HOST:PORT" string
# It will return with 0 if the server is reachable.
# It will return with 1 and a message on stderr if not.
test_server() {
local item=$1
# split HOST and PORT from the input string
local host=$(echo "${item}" | cut -d':' -f1)
local port=$(echo "${item}" | cut -d':' -f2)
local new_error
# Test if the server is accepting connections
ssh -q -o "ConnectTimeout ${SSH_CONNECT_TIMEOUT}" "${host}" -p "${port}" -t "exit"
# shellcheck disable=SC2181
if [ $? = 0 ]; then
# SSH connection is OK
return 0
else
# SSH connection failed
new_error=$(printf "Failed to connect to \`%s' within %s seconds" "${item}" "${SSH_CONNECT_TIMEOUT}")
log "${new_error}"
SSH_ERRORS+=("${new_error}")
return 1
fi
}
# Call pick_server with an optional positive integer to get the nth server in the list.
pick_server() {
local -i increment=${1:-0}
local -i list_length=${#SERVERS[@]}
local sync_name=${2:-""}
if (( increment >= list_length )); then
# We've reached the end of the list
new_error="No more server available"
new_error="${new_error} for sync '${sync_name}'"
log "${new_error}"
SSH_ERRORS+=("${new_error}")
# Log errors to stderr
for i in "${!SSH_ERRORS[@]}"; do
printf "%s\n" "${SSH_ERRORS[i]}" >&2
done
return 1
fi
# Extract the day of month, without leading 0 (which would give an octal based number)
today=$(/bin/date +%e)
# A salt is useful to randomize the starting point in the list
# but stay identical each time it's called for a server (based on hostname).
salt=$(hostname | cksum | cut -d' ' -f1)
# Pick an integer between 0 and the length of the SERVERS list
# It changes each day
n=$(( (today + salt + increment) % list_length ))
echo "${SERVERS[n]}"
}
send_mail() {
tail -20 "${LOGFILE}" | mail -s "${MAIL_SUBJECT}" "${MAIL}"
}
path_to_str() {
echo "${1}" | sed -e 's|^/||; s|/$||; s|/|:|g'
}

326
client/lib/zzz_evobackup.sh Normal file
View file

@ -0,0 +1,326 @@
#!/usr/bin/env bash
#
# Evobackup client
# See https://gitea.evolix.org/evolix/evobackup
#
# This is a generated backup script made by:
# command: @COMMAND@
# version: @VERSION@
# date: @DATE@
#######################################################################
#
# You must configure the MAIL variable to receive notifications.
#
# There is some optional configuration that you can do
# at the end of this script.
#
#######################################################################
# Email adress for notifications
MAIL=__NOTIFICATION_MAIL__
#######################################################################
#
# The "sync_tasks" function will be called by the "run_evobackup" function.
#
# You can customize the variables:
# * "SYNC_NAME" (String)
# * "SERVERS" (Array of HOST:PORT)
# * "RSYNC_INCLUDES" (Array of paths to include)
# * "RSYNC_EXCLUDES" (Array of paths to exclude)
#
# WARNING: remember to single-quote paths if they contain globs (*)
# and you want to pass them as-is to Rsync.
#
# The "sync" function can be called multiple times
# with a different set of variables.
# That way you can to sync to various destinations.
#
# Default includes/excludes are defined in the "main" library,
# referenced at this end of this file.
#
#######################################################################
# shellcheck disable=SC2034
sync_tasks() {
########## System-only backup (to Evolix servers) #################
SYNC_NAME="evolix-system"
SERVERS=(
__SRV0_HOST__:__SRV0_PORT__
__SRV1_HOST__:__SRV1_PORT__
)
RSYNC_INCLUDES=(
"${rsync_default_includes[@]}"
/etc
/root
/var
)
RSYNC_EXCLUDES=(
"${rsync_default_excludes[@]}"
)
sync "${SYNC_NAME}" "SERVERS[@]" "RSYNC_INCLUDES[@]" "RSYNC_EXCLUDES[@]"
########## Full backup (to client servers) ########################
### SYNC_NAME="client-full"
### SERVERS=(
### client-backup00.evolix.net:2221
### client-backup01.evolix.net:2221
### )
### RSYNC_INCLUDES=(
### "${rsync_default_includes[@]}"
### /etc
### /root
### /var
### /home
### /srv
### )
### RSYNC_EXCLUDES=(
### "${rsync_default_excludes[@]}"
### )
### sync "${SYNC_NAME}" "SERVERS[@]" "RSYNC_INCLUDES[@]" "RSYNC_EXCLUDES[@]"
}
#######################################################################
#
# The "local_tasks" function will be called by the "run_evobackup" function.
#
# You can call any available "dump_xxx" function
# (usually installed at /usr/local/lib/evobackup/dump-*.sh)
#
# You can also write some custom functions and call them.
# A "dump_custom" example is available further down.
#
#######################################################################
local_tasks() {
########## Server state ###########
# Run dump-server-state to extract system information
#
# Options : any dump-server-state supported option
# (except --dump-dir that will be overwritten)
# See 'dump-server-state -h' for details.
#
dump_server_state
########## MySQL ##################
# Very common strategy for a single instance server with default configuration :
#
### dump_mysql_global; dump_mysql_grants; dump_mysql_summary
#
# See below for details regarding dump functions for MySQL/MariaDB
# Dump all databases in a single compressed file
#
# Options :
# --masterdata (default: <absent>)
# --port=[Integer] (default: <blank>)
# --socket=[String] (default: <blank>)
# --user=[String] (default: <blank>)
# --password=[String] (default: <blank>)
# --defaults-file=[String] (default: <blank>)
# --defaults-extra-file=[String] (default: <blank>)
# --defaults-group-suffix=[String] (default: <blank>)
# --dump-label=[String] (default: "default")
# used as suffix of the dump dir to differenciate multiple instances
#
### dump_mysql_global
# Dump each database separately, in a compressed file
#
# Options :
# --port=[Integer] (default: <blank>)
# --socket=[String] (default: <blank>)
# --user=[String] (default: <blank>)
# --password=[String] (default: <blank>)
# --defaults-file=[String] (default: <blank>)
# --defaults-extra-file=[String] (default: <blank>)
# --defaults-group-suffix=[String] (default: <blank>)
# --dump-label=[String] (default: "default")
# used as suffix of the dump dir to differenciate multiple instances
#
### dump_mysql_per_base
# Dump permissions of an instance (using pt-show-grants)
#
# Options :
# --port=[Integer] (default: <blank>)
# --socket=[String] (default: <blank>)
# --user=[String] (default: <blank>)
# --password=[String] (default: <blank>)
# --defaults-file=[String] (default: <blank>)
# --dump-label=[String] (default: "default")
# used as suffix of the dump dir to differenciate multiple instances
#
# WARNING - unsupported options :
# --defaults-extra-file
# --defaults-group-suffix
# You have to provide credentials manually
#
### dump_mysql_grants
# Dump complete summary of an instance (using pt-mysql-summary)
#
# Options :
# --port=[Integer] (default: <blank>)
# --socket=[String] (default: <blank>)
# --user=[String] (default: <blank>)
# --password=[String] (default: <blank>)
# --defaults-file=[String] (default: <blank>)
# --defaults-extra-file=[String] (default: <blank>)
# --defaults-group-suffix=[String] (default: <blank>)
# --dump-label=[String] (default: "default")
# used as suffix of the dump dir to differenciate multiple instances
#
### dump_mysql_summary
# Dump each table in separate schema/data files
#
# Options :
# --port=[Integer] (default: <blank>)
# --socket=[String] (default: <blank>)
# --user=[String] (default: <blank>)
# --password=[String] (default: <blank>)
# --defaults-file=[String] (default: <blank>)
# --defaults-extra-file=[String] (default: <blank>)
# --defaults-group-suffix=[String] (default: <blank>)
# --dump-label=[String] (default: "default")
# used as suffix of the dump dir to differenciate multiple instances
#
### dump_mysql_tabs
########## PostgreSQL #############
# Dump all databases in a single file (compressed or not)
#
### dump_postgresql_global
# Dump a specific databse with only some tables, or all but some tables (must be configured)
#
### dump_postgresql_filtered
# Dump each database separately, in a compressed file
#
### dump_postgresql_per_base
########## MongoDB ################
### dump_mongodb [--user=foo] [--password=123456789]
########## Redis ##################
# Copy data file for all instances
#
### dump_redis [--instances=<all|instance1|instance2>]
########## Elasticsearch ##########
# Snapshot data for a single-node cluster
#
### dump_elasticsearch_snapshot_singlenode [--protocol=http] [--host=localhost] [--port=9200] [--user=foo] [--password=123456789] [--repository=snaprepo] [--snapshot=snapshot.daily]
# Snapshot data for a multi-node cluster
#
### dump_elasticsearch_snapshot_multinode [--protocol=http] [--host=localhost] [--port=9200] [--user=foo] [--password=123456789] [--repository=snaprepo] [--snapshot=snapshot.daily] [--nfs-server=192.168.2.1]
########## RabbitMQ ###############
### dump_rabbitmq
########## MegaCli ################
# Copy RAID config
#
### dump_megacli_config
# Dump file access control lists
#
### dump_facl
########## OpenLDAP ###############
### dump_ldap
########## Network ################
# Dump network routes with mtr and traceroute (warning: could be long with aggressive firewalls)
#
### dump_traceroute --targets=host_or_ip[,host_or_ip]
dump_traceroute --targets=8.8.8.8,www.evolix.fr,travaux.evolix.net
# No-op, in case nothing is enabled
:
}
# This is an example for a custom dump function
# Uncomment, customize and call it from the "local_tasks" function
### dump_custom() {
### # Set dump and errors directories and files
### local dump_dir="${LOCAL_BACKUP_DIR}/custom"
### local dump_file="${dump_dir}/dump.gz"
### local errors_dir=$(errors_dir_from_dump_dir "${dump_dir}")
### local error_file="${errors_dir}/dump.err"
###
### # Reset dump and errors directories
### rm -rf "${dump_dir}" "${errors_dir}"
### # shellcheck disable=SC2174
### mkdir -p -m 700 "${dump_dir}" "${errors_dir}"
###
### # Log the start of the function
### log "LOCAL_TASKS - ${FUNCNAME[0]}: start ${dump_file}"
###
### # Prepare the dump command (errors go to the error file and the data to the dump file)
### dump_cmd="my-dump-command 2> ${error_file} > ${dump_file}"
### log "LOCAL_TASKS - ${FUNCNAME[0]}: ${dump_cmd}"
###
### # Execute the dump command
### ${dump_cmd}
###
### # Check result and deal with potential errors
### local last_rc=$?
### # shellcheck disable=SC2086
### if [ ${last_rc} -ne 0 ]; then
### log_error "LOCAL_TASKS - ${FUNCNAME[0]}: my-dump-command to ${dump_file} returned an error ${last_rc}" "${error_file}"
### GLOBAL_RC=${E_DUMPFAILED}
### else
### rm -f "${error_file}"
### fi
###
### # Log the end of the function
### log "LOCAL_TASKS - ${FUNCNAME[0]}: stop ${dump_file}"
### }
########## Optional configuration #####################################
setup_custom() {
# System name ("linux" and "openbsd" currently supported)
### SYSTEM="$(uname)"
# Host name for logs and notifications
### HOSTNAME="$(hostname)"
# Email subject for notifications
### MAIL_SUBJECT="[info] EvoBackup - Client ${HOSTNAME}"
# No-op in case nothing is executed
:
}
########## Libraries ##################################################
# Change this to wherever you install the libraries
LIBDIR="/usr/local/lib/evobackup"
source "${LIBDIR}/main.sh"
########## Let's go! ##################################################
run_evobackup

76
client/vagrant.yml Normal file
View file

@ -0,0 +1,76 @@
# To be used through "vagrant up" or "vagrant provision".
---
- hosts: bookworm,bullseye,buster
gather_facts: yes
become: yes
vars_files:
- '~/GIT/evolix-private/vars/evolinux-secrets.yml'
vars:
evolinux_hostname: "localhost"
evolinux_domain: "localdomain.tld"
evomaintenance_alert_email: "evomaintenance-{{ evolinux_hostname }}@evolix.fr"
evomaintenance_install_vendor: True
client_number: "XXX"
monitoring_mode: "everytime"
evocheck_force_install: "local"
evoadmin_host: "evoadmin.{{ evolinux_hostname }}.evolix.eu"
evoadmin_contact_email: root@localhost
postfix_slow_transport_include: True
evolinux_ssh_allow_current_user: True
minifirewall_additional_trusted_ips: ["192.168.0.0/16", "10.0.0.0/8"]
minifirewall_http_sites: ["0.0.0.0/0"]
packweb_enable_evoadmin_vhost: True
packweb_phpmyadmin_suffix: "uE34swx9"
evolinux_apt_include: True
evolinux_etcgit_include: True
evolinux_hostname_include: True
evolinux_kernel_include: True
evolinux_fstab_include: True
evolinux_packages_include: True
evolinux_system_include: True
evolinux_evomaintenance_include: True
evolinux_ssh_include: True
evolinux_users_include: False
evolinux_root_include: True
evolinux_postfix_include: True
evolinux_logs_include: True
evolinux_default_www_include: True
evolinux_hardware_include: True
evolinux_provider_online_include: False
evolinux_provider_orange_fce_include: False
evolinux_log2mail_include: True
evolinux_minifirewall_include: True
evolinux_munin_include: True
evolinux_nagios_nrpe_include: True
evolinux_fail2ban_include: False
mysql_custom_datadir: '/home/mysql'
mysql_custom_tmpdir: '/home/tmpmysql'
mysql_custom_logdir: '/home/mysql-logs'
# evolinux_apt_public_sources: False
apt_upgrade: True
# TODO Try to to make it work without the following line
# packweb_multiphp_versions:
# - php74
# - php82
# autosysadmin_config:
# repair_http: "on"
# repair_mysql: off
# repair_all: 'off'
roles:
- mysql
# - evolinux-base
# # - evolinux-users
# - ./ansible/roles/autosysadmin-agent
# - packweb-apache
# # - redis
# - { role: redis, redis_instance_name: foo, redis_port: 6380 }
# - { role: redis, redis_instance_name: bar, redis_port: 6381 }

View file

@ -1,520 +0,0 @@
#!/bin/sh
#
# Script Evobackup client
# See https://gitea.evolix.org/evolix/evobackup
#
# Author: Gregory Colpart <reg@evolix.fr>
# Contributors:
# Romain Dessort <rdessort@evolix.fr>
# Benoît Série <bserie@evolix.fr>
# Tristan Pilat <tpilat@evolix.fr>
# Victor Laborie <vlaborie@evolix.fr>
# Jérémy Lecour <jlecour@evolix.fr>
#
# Licence: AGPLv3
#
# /!\ DON'T FORGET TO SET "MAIL" and "SERVERS" VARIABLES
# Fail on unassigned variables
set -u
##### Configuration ###################################################
VERSION="22.03"
# email adress for notifications
MAIL=jdoe@example.com
# list of hosts (hostname or IP) and SSH port for Rsync
SERVERS="node0.backup.example.com:2XXX node1.backup.example.com:2XXX"
# Should we fallback on servers when the first is unreachable ?
SERVERS_FALLBACK=${SERVERS_FALLBACK:-1}
# timeout (in seconds) for SSH connections
SSH_CONNECT_TIMEOUT=${SSH_CONNECT_TIMEOUT:-90}
# We use /home/backup : feel free to use your own dir
LOCAL_BACKUP_DIR="/home/backup"
# You can set "linux" or "bsd" manually or let it choose automatically
SYSTEM=$(uname | tr '[:upper:]' '[:lower:]')
# Store pid in a file named after this program's name
PROGNAME=$(basename "$0")
PIDFILE="/var/run/${PROGNAME}.pid"
# Customize the log path if you have multiple scripts and with separate logs
LOGFILE="/var/log/evobackup.log"
# Enable/Disable tasks
LOCAL_TASKS=${LOCAL_TASKS:-1}
SYNC_TASKS=${SYNC_TASKS:-1}
HOSTNAME=$(hostname)
##### SETUP AND FUNCTIONS #############################################
START_EPOCH=$(/bin/date +%s)
DATE_FORMAT="%Y-%m-%d %H:%M:%S"
# shellcheck disable=SC2174
mkdir -p -m 700 ${LOCAL_BACKUP_DIR}
PATH=/sbin:/usr/sbin:/bin:/usr/bin:/usr/local/sbin:/usr/local/bin
## lang = C for english outputs
export LANGUAGE=C
export LANG=C
## Force umask
umask 077
## Initialize variable to store SSH connection errors
SERVERS_SSH_ERRORS=""
# Call test_server with "HOST:PORT" string
# It will return with 0 if the server is reachable.
# It will return with 1 and a message on stderr if not.
test_server() {
item=$1
# split HOST and PORT from the input string
host=$(echo "${item}" | cut -d':' -f1)
port=$(echo "${item}" | cut -d':' -f2)
# Test if the server is accepting connections
ssh -q -o "ConnectTimeout ${SSH_CONNECT_TIMEOUT}" "${host}" -p "${port}" -t "exit"
# shellcheck disable=SC2181
if [ $? = 0 ]; then
# SSH connection is OK
return 0
else
# SSH connection failed
new_error=$(printf "Failed to connect to \`%s' within %s seconds" "${item}" "${SSH_CONNECT_TIMEOUT}")
log "${new_error}"
SERVERS_SSH_ERRORS=$(printf "%s\\n%s" "${SERVERS_SSH_ERRORS}" "${new_error}" | sed -e '/^$/d')
return 1
fi
}
# Call pick_server with an optional positive integer to get the nth server in the list.
pick_server() {
increment=${1:-0}
list_length=$(echo "${SERVERS}" | wc -w)
if [ "${increment}" -ge "${list_length}" ]; then
# We've reached the end of the list
new_error="No more server available"
log "${new_error}"
SERVERS_SSH_ERRORS=$(printf "%s\\n%s" "${SERVERS_SSH_ERRORS}" "${new_error}" | sed -e '/^$/d')
# Log errors to stderr
printf "%s\\n" "${SERVERS_SSH_ERRORS}" >&2
return 1
fi
# Extract the day of month, without leading 0 (which would give an octal based number)
today=$(/bin/date +%e)
# A salt is useful to randomize the starting point in the list
# but stay identical each time it's called for a server (based on hostname).
salt=$(hostname | cksum | cut -d' ' -f1)
# Pick an integer between 0 and the length of the SERVERS list
# It changes each day
item=$(( (today + salt + increment) % list_length ))
# cut starts counting fields at 1, not 0.
field=$(( item + 1 ))
echo "${SERVERS}" | cut -d' ' -f${field}
}
log() {
msg="${1:-$(cat /dev/stdin)}"
pid=$$
printf "[%s] %s[%s]: %s\\n" \
"$(/bin/date +"${DATE_FORMAT}")" "${PROGNAME}" "${pid}" "${msg}" \
>> "${LOGFILE}"
}
log "START GLOBAL - VERSION=${VERSION} LOCAL_TASKS=${LOCAL_TASKS} SYNC_TASKS=${SYNC_TASKS}"
## Verify other evobackup process and kill if needed
if [ -e "${PIDFILE}" ]; then
pid=$(cat "${PIDFILE}")
# Does process still exist ?
if kill -0 "${pid}" 2> /dev/null; then
# Killing the childs of evobackup.
for ppid in $(pgrep -P "${pid}"); do
kill -9 "${ppid}";
done
# Then kill the main PID.
kill -9 "${pid}"
printf "%s is still running (PID %s). Process has been killed" "$0" "${pid}\\n" >&2
else
rm -f "${PIDFILE}"
fi
fi
echo "$$" > "${PIDFILE}"
# shellcheck disable=SC2064
trap "rm -f ${PIDFILE}" EXIT
##### LOCAL BACKUP ####################################################
if [ "${LOCAL_TASKS}" = "1" ]; then
log "START LOCAL_TASKS"
# You can comment or uncomment sections below to customize the backup
## OpenLDAP : example with slapcat
# slapcat -n 0 -l ${LOCAL_BACKUP_DIR}/config.ldap.bak
# slapcat -n 1 -l ${LOCAL_BACKUP_DIR}/data.ldap.bak
# slapcat -l ${LOCAL_BACKUP_DIR}/ldap.bak
## MySQL
## Purge previous dumps
# rm -f ${LOCAL_BACKUP_DIR}/mysql.*.gz
# rm -rf ${LOCAL_BACKUP_DIR}/mysql
# rm -rf ${LOCAL_BACKUP_DIR}/mysqlhotcopy
# rm -rf /home/mysqldump
## example with global and compressed mysqldump
# mysqldump --defaults-extra-file=/etc/mysql/debian.cnf -P 3306 \
# --opt --all-databases --force --events --hex-blob | gzip --best > ${LOCAL_BACKUP_DIR}/mysql.bak.gz
## example with compressed SQL dump (with data) for each databases
# mkdir -p -m 700 ${LOCAL_BACKUP_DIR}/mysql/
# for i in $(mysql --defaults-extra-file=/etc/mysql/debian.cnf -P 3306 -e 'show databases' -s --skip-column-names \
# | egrep -v "^(Database|information_schema|performance_schema|sys)"); do
# mysqldump --defaults-extra-file=/etc/mysql/debian.cnf --force -P 3306 --events --hex-blob $i | gzip --best > ${LOCAL_BACKUP_DIR}/mysql/${i}.sql.gz
# done
## Dump all grants (requires 'percona-toolkit' package)
# mkdir -p -m 700 ${LOCAL_BACKUP_DIR}/mysql/
# pt-show-grants --flush --no-header > ${LOCAL_BACKUP_DIR}/mysql/all_grants.sql
# Dump all variables
# mysql -A -e"SHOW GLOBAL VARIABLES;" > ${LOCAL_BACKUP_DIR}/MySQLCurrentSettings.txt
## example with SQL dump (schema only, no data) for each databases
# mkdir -p -m 700 ${LOCAL_BACKUP_DIR}/mysql/
# for i in $(mysql --defaults-extra-file=/etc/mysql/debian.cnf -P 3306 -e 'show databases' -s --skip-column-names \
# | egrep -v "^(Database|information_schema|performance_schema|sys)"); do
# mysqldump --defaults-extra-file=/etc/mysql/debian.cnf --force -P 3306 --no-data --databases $i > ${LOCAL_BACKUP_DIR}/mysql/${i}.schema.sql
# done
## example with *one* uncompressed SQL dump for *one* database (MYBASE)
# mkdir -p -m 700 ${LOCAL_BACKUP_DIR}/mysql/MYBASE
# chown -RL mysql ${LOCAL_BACKUP_DIR}/mysql/
# mysqldump --defaults-extra-file=/etc/mysql/debian.cnf --force -Q \
# --opt --events --hex-blob --skip-comments -T ${LOCAL_BACKUP_DIR}/mysql/MYBASE MYBASE
## example with two dumps for each table (.sql/.txt) for all databases
# for i in $(echo SHOW DATABASES | mysql --defaults-extra-file=/etc/mysql/debian.cnf -P 3306 \
# | egrep -v "^(Database|information_schema|performance_schema|sys)" ); \
# do mkdir -p -m 700 /home/mysqldump/$i ; chown -RL mysql /home/mysqldump ; \
# mysqldump --defaults-extra-file=/etc/mysql/debian.cnf --force -P 3306 -Q --opt --events --hex-blob --skip-comments \
# --fields-enclosed-by='\"' --fields-terminated-by=',' -T /home/mysqldump/$i $i; done
## example with mysqlhotcopy
# mkdir -p -m 700 ${LOCAL_BACKUP_DIR}/mysqlhotcopy/
# mysqlhotcopy MYBASE ${LOCAL_BACKUP_DIR}/mysqlhotcopy/
## example for multiples MySQL instances
# mysqladminpasswd=$(grep -m1 'password = .*' /root/.my.cnf|cut -d" " -f3)
# grep -E "^port\s*=\s*\d*" /etc/mysql/my.cnf |while read instance; do
# instance=$(echo "$instance"|awk '{ print $3 }')
# if [ "$instance" != "3306" ]
# then
# mysqldump -P $instance --opt --all-databases --hex-blob -u mysqladmin -p$mysqladminpasswd | gzip --best > ${LOCAL_BACKUP_DIR}/mysql.$instance.bak.gz
# fi
# done
## PostgreSQL
## Purge previous dumps
# rm -rf ${LOCAL_BACKUP_DIR}/pg.*.gz
# rm -rf ${LOCAL_BACKUP_DIR}/pg-backup.tar
# rm -rf ${LOCAL_BACKUP_DIR}/postgresql/*
## example with pg_dumpall (warning: you need space in ~postgres)
# su - postgres -c "pg_dumpall > ~/pg.dump.bak"
# mv ~postgres/pg.dump.bak ${LOCAL_BACKUP_DIR}/
## another method with gzip directly piped
# cd /var/lib/postgresql
# sudo -u postgres pg_dumpall | gzip > ${LOCAL_BACKUP_DIR}/pg.dump.bak.gz
# cd - > /dev/null
## example with all tables from MYBASE excepts TABLE1 and TABLE2
# pg_dump -p 5432 -h 127.0.0.1 -U USER --clean -F t --inserts -f ${LOCAL_BACKUP_DIR}/pg-backup.tar -t 'TABLE1' -t 'TABLE2' MYBASE
## example with only TABLE1 and TABLE2 from MYBASE
# pg_dump -p 5432 -h 127.0.0.1 -U USER --clean -F t --inserts -f ${LOCAL_BACKUP_DIR}/pg-backup.tar -T 'TABLE1' -T 'TABLE2' MYBASE
## example with compressed PostgreSQL dump for each databases
# mkdir -p -m 700 ${LOCAL_BACKUP_DIR}/postgresql
# chown postgres:postgres ${LOCAL_BACKUP_DIR}/postgresql
# dbs=$(sudo -u postgres psql -U postgres -lt | awk -F\| '{print $1}' |grep -v template*)
#
# for databases in $dbs ; do sudo -u postgres /usr/bin/pg_dump --create -s -U postgres -d $databases | gzip --best -c > ${LOCAL_BACKUP_DIR}/postgresql/$databases.sql.gz ; done
## MongoDB
## don't forget to create use with read-only access
## > use admin
## > db.createUser( { user: "mongobackup", pwd: "PASS", roles: [ "backup", ] } )
## Purge previous dumps
# rm -rf ${LOCAL_BACKUP_DIR}/mongodump/
# mkdir -p -m 700 ${LOCAL_BACKUP_DIR}/mongodump/
# mongodump --quiet -u mongobackup -pPASS -o ${LOCAL_BACKUP_DIR}/mongodump/
# if [ $? -ne 0 ]; then
# echo "Error with mongodump!"
# fi
## Redis
## Purge previous dumps
# rm -rf ${LOCAL_BACKUP_DIR}/redis/
# rm -rf ${LOCAL_BACKUP_DIR}/redis-*
## example with copy .rdb file
## for the default instance :
# mkdir -p -m 700 ${LOCAL_BACKUP_DIR}/redis/
# cp /var/lib/redis/dump.rdb ${LOCAL_BACKUP_DIR}/redis/
## for multiple instances :
# for instance in $(ls -d /var/lib/redis-*); do
# name=$(basename $instance)
# mkdir -p ${LOCAL_BACKUP_DIR}/${name}
# cp -a ${instance}/dump.rdb ${LOCAL_BACKUP_DIR}/${name}
# done
## ElasticSearch
## Take a snapshot as a backup.
## Warning: You need to have a path.repo configured.
## See: https://wiki.evolix.org/HowtoElasticsearch#snapshots-et-sauvegardes
# curl -s -XDELETE "localhost:9200/_snapshot/snaprepo/snapshot.daily" -o /tmp/es_delete_snapshot.daily.log
# curl -s -XPUT "localhost:9200/_snapshot/snaprepo/snapshot.daily?wait_for_completion=true" -o /tmp/es_snapshot.daily.log
## Clustered version here
## It basically the same thing except that you need to check that NFS is mounted
# if ss | grep ':nfs' | grep -q 'ip\.add\.res\.s1' && ss | grep ':nfs' | grep -q 'ip\.add\.res\.s2'
# then
# curl -s -XDELETE "localhost:9200/_snapshot/snaprepo/snapshot.daily" -o /tmp/es_delete_snapshot.daily.log
# curl -s -XPUT "localhost:9200/_snapshot/snaprepo/snapshot.daily?wait_for_completion=true" -o /tmp/es_snapshot.daily.log
# else
# echo 'Cannot make a snapshot of elasticsearch, at least one node is not mounting the repository.'
# fi
## If you need to keep older snapshot, for example the last 10 daily snapshots, replace the XDELETE and XPUT lines by :
# for snapshot in $(curl -s -XGET "localhost:9200/_snapshot/snaprepo/_all?pretty=true" | grep -Eo 'snapshot_[0-9]{4}-[0-9]{2}-[0-9]{2}' | head -n -10); do
# curl -s -XDELETE "localhost:9200/_snapshot/snaprepo/${snapshot}" | grep -v -Fx '{"acknowledged":true}'
# done
# date=$(/bin/date +%F)
# curl -s -XPUT "localhost:9200/_snapshot/snaprepo/snapshot_${date}?wait_for_completion=true" -o /tmp/es_snapshot_${date}.log
## RabbitMQ
## export config
#rabbitmqadmin export ${LOCAL_BACKUP_DIR}/rabbitmq.config >> $LOGFILE
## MegaCli config
#megacli -CfgSave -f ${LOCAL_BACKUP_DIR}/megacli_conf.dump -a0 >/dev/null
## Dump network routes with mtr and traceroute (warning: could be long with aggressive firewalls)
for addr in 8.8.8.8 www.evolix.fr travaux.evolix.net; do
mtr -r ${addr} > ${LOCAL_BACKUP_DIR}/mtr-${addr}
traceroute -n ${addr} > ${LOCAL_BACKUP_DIR}/traceroute-${addr} 2>&1
done
server_state_dir="${LOCAL_BACKUP_DIR}/server-state"
dump_server_state_bin=$(command -v dump-server-state)
if [ "${SYSTEM}" = "linux" ]; then
if [ -n "${dump_server_state_bin}" ]; then
${dump_server_state_bin} --all --force --dump-dir "${server_state_dir}"
else
mkdir -p "${server_state_dir}"
## Dump system and kernel versions
uname -a > ${server_state_dir}/uname.txt
## Dump process with ps
ps auwwx > ${server_state_dir}/ps.txt
## Dump network connections with ss
ss -taupen > ${server_state_dir}/netstat.txt
## List Debian packages
dpkg -l > ${server_state_dir}/packages
dpkg --get-selections > ${server_state_dir}/packages.getselections
apt-cache dumpavail > ${server_state_dir}/packages.available
## Dump iptables
if [ -x /sbin/iptables ]; then
{ /sbin/iptables -L -n -v; /sbin/iptables -t filter -L -n -v; } > ${server_state_dir}/iptables.txt
fi
## Dump findmnt(8) output
FINDMNT_BIN=$(command -v findmnt)
if [ -x "${FINDMNT_BIN}" ]; then
${FINDMNT_BIN} > ${server_state_dir}/findmnt.txt
fi
## Dump MBR / table partitions
disks=$(lsblk -l | grep disk | grep -v -E '(drbd|fd[0-9]+)' | awk '{print $1}')
for disk in ${disks}; do
dd if="/dev/${disk}" of="${server_state_dir}/MBR-${disk}" bs=512 count=1 2>&1 | grep -Ev "(records in|records out|512 bytes)"
fdisk -l "/dev/${disk}" > "${server_state_dir}/partitions-${disk}" 2>&1
done
cat ${server_state_dir}/partitions-* > ${server_state_dir}/partitions
fi
else
if [ -n "${dump_server_state_bin}" ]; then
${dump_server_state_bin} --all --force --backup-dir "${server_state_dir}"
else
mkdir -p "${server_state_dir}"
## Dump system and kernel versions
uname -a > ${server_state_dir}/uname
## Dump process with ps
ps auwwx > ${server_state_dir}/ps.out
## Dump network connections with fstat
fstat | head -1 > ${server_state_dir}/netstat.out
fstat | grep internet >> ${server_state_dir}/netstat.out
## List OpenBSD packages
pkg_info -m > ${server_state_dir}/packages
## Dump MBR / table partitions
disklabel sd0 > ${server_state_dir}/partitions
## Dump pf infos
pfctl -sa > ${server_state_dir}/pfctl-sa.txt
fi
fi
## Dump rights
#getfacl -R /var > ${server_state_dir}/rights-var.txt
#getfacl -R /etc > ${server_state_dir}/rights-etc.txt
#getfacl -R /usr > ${server_state_dir}/rights-usr.txt
#getfacl -R /home > ${server_state_dir}/rights-home.txt
log "STOP LOCAL_TASKS"
fi
##### REMOTE BACKUP ###################################################
if [ "${SYNC_TASKS}" = "1" ]; then
n=0
server=""
if [ "${SERVERS_FALLBACK}" = "1" ]; then
# We try to find a suitable server
while :; do
server=$(pick_server "${n}")
test $? = 0 || exit 2
if test_server "${server}"; then
break
else
server=""
n=$(( n + 1 ))
fi
done
else
# we force the server
server=$(pick_server "${n}")
fi
SSH_SERVER=$(echo "${server}" | cut -d':' -f1)
SSH_PORT=$(echo "${server}" | cut -d':' -f2)
if [ "${SYSTEM}" = "linux" ]; then
rep="/bin /boot /lib /opt /sbin /usr"
else
rep="/bsd /bin /sbin /usr"
fi
log "START SYNC_TASKS - server=${server}"
# /!\ DO NOT USE COMMENTS in the rsync command /!\
# It breaks the command and destroys data, simply remove (or add) lines.
# Remote shell command
RSH_COMMAND="ssh -p ${SSH_PORT} -o 'ConnectTimeout ${SSH_CONNECT_TIMEOUT}'"
# ignore check because we want it to split the different arguments to $rep
# shellcheck disable=SC2086
rsync -avzh --relative --stats --delete --delete-excluded --force --ignore-errors --partial \
--exclude "dev" \
--exclude "lost+found" \
--exclude ".nfs.*" \
--exclude "/usr/doc" \
--exclude "/usr/obj" \
--exclude "/usr/share/doc" \
--exclude "/usr/src" \
--exclude "/var/apt" \
--exclude "/var/cache" \
--exclude "/var/lib/amavis/amavisd.sock" \
--exclude "/var/lib/amavis/tmp" \
--exclude "/var/lib/clamav/*.tmp" \
--exclude "/var/lib/elasticsearch" \
--exclude "/var/lib/metche" \
--exclude "/var/lib/munin/*tmp*" \
--exclude "/var/db/munin/*.tmp" \
--exclude "/var/lib/mysql" \
--exclude "/var/lib/php5" \
--exclude "/var/lib/php/sessions" \
--exclude "/var/lib/postgres" \
--exclude "/var/lib/postgresql" \
--exclude "/var/lib/sympa" \
--exclude "/var/lock" \
--exclude "/var/run" \
--exclude "/var/spool/postfix" \
--exclude "/var/spool/smtpd" \
--exclude "/var/spool/squid" \
--exclude "/var/state" \
--exclude "/var/tmp" \
--exclude "lxc/*/rootfs/tmp" \
--exclude "lxc/*/rootfs/usr/doc" \
--exclude "lxc/*/rootfs/usr/obj" \
--exclude "lxc/*/rootfs/usr/share/doc" \
--exclude "lxc/*/rootfs/usr/src" \
--exclude "lxc/*/rootfs/var/apt" \
--exclude "lxc/*/rootfs/var/cache" \
--exclude "lxc/*/rootfs/var/lib/php5" \
--exclude "lxc/*/rootfs/var/lib/php/sessions" \
--exclude "lxc/*/rootfs/var/lock" \
--exclude "lxc/*/rootfs/var/log" \
--exclude "lxc/*/rootfs/var/run" \
--exclude "lxc/*/rootfs/var/state" \
--exclude "lxc/*/rootfs/var/tmp" \
--exclude "/home/mysqltmp" \
${rep} \
/etc \
/root \
/var \
/home \
-e "${RSH_COMMAND}" \
"root@${SSH_SERVER}:/var/backup/" \
| tail -30 >> $LOGFILE
log "STOP SYNC_TASKS - server=${server}"
fi
##### REPORTING #######################################################
STOP_EPOCH=$(/bin/date +%s)
if [ "${SYSTEM}" = "openbsd" ]; then
start_time=$(/bin/date -f "%s" -j "${START_EPOCH}" +"${DATE_FORMAT}")
stop_time=$(/bin/date -f "%s" -j "${STOP_EPOCH}" +"${DATE_FORMAT}")
else
start_time=$(/bin/date --date="@${START_EPOCH}" +"${DATE_FORMAT}")
stop_time=$(/bin/date --date="@${STOP_EPOCH}" +"${DATE_FORMAT}")
fi
duration=$(( STOP_EPOCH - START_EPOCH ))
log "STOP GLOBAL - start='${start_time}' stop='${stop_time}' duration=${duration}s"
tail -20 "${LOGFILE}" \
| mail -s "[info] EvoBackup - Client ${HOSTNAME}" ${MAIL}

View file

@ -1,19 +1,59 @@
# Changelog
All notable changes to this project will be documented in this file.
The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/),
and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
The format is based on [Keep a Changelog](http://keepachangelog.com/en/1.0.0/).
This project does not follow semantic versioning.
The **major** part of the version is the year
The **minor** part changes is the month
The **patch** part changes is incremented if multiple releases happen the same month
## [Unreleased]
### Added
### Changed
### Deprecated
### Removed
### Fixed
* Test presence of old config file before trying to delete it
### Security
## [22.11] - 2022-11-28
### Added
* check-canary: new subcommand to check canary files and content
### Changed
* stats: filter active jails and columnize the output
## [22.07] - 2022-07-20
### Changed
* check-setup: check minifirewall version only if minifirewall is present
* check-setup: get minifirewall version from internal variable (there is no other backward compatible way)
* check-setup: use findmnt with mountpoint instead of target
## [22.06] - 2022-06-28
### Added
* bkctld-init: create "incs/\<jail\>" directory for jails
### Fixed
* shell syntax error when ${btrfs_bin} variable is empty
* read_variable + read_numerical_variable: keep the last found value only
* Debian 8 findmnt(8) support
### Security

View file

@ -4,7 +4,7 @@ Bkctld (aka server-side evobackup)
bkctld helps you manage the receiving side of a backup infrastructure.
It is licensed under the AGPLv3.
With bkctld you create and manage "jails". They contain a chrooted and dedicated SSH server, with it's own TCP port and optionnaly it's own set of iptables rules.
With bkctld you create and manage "jails". They contain a chrooted and dedicated SSH server, with its own TCP port and optionally its own set of iptables rules.
With bkctld you can have hundreds of jails, one for each client to push its data (using Rsync/SFTP). Each client can only see its own data.
@ -30,9 +30,7 @@ This volume can also be encrypted with **LUKS**.
## Security considerations
The client obviously has access to its uploaded data (in the chroot), but the timestamped copies are outside the chroot, to reduce the risk or complete backup erasure from a compromised client.
Since the client connects to the backup server with root, it can mess with the jail and destroy the data. But the timestamped copies are out of reach because outside of the chroot.
The client obviously has access to its uploaded data (in the chroot), but the timestamped copies are outside the chroot, to reduce the risk of complete backup erasure from a compromised client.
It means that **if the client server is compromised**, an attacker can destroy the latest copy of the backed up data, but not the timestamped copies.
And **if the backup server is compromised** an attacker has complete access to all the backup data (inside and outside the jails), but they don't have any access to the client.
@ -41,7 +39,7 @@ This architecture is as secure as SSH, Rsync, chroot and iptables are.
## Install
See the [installation guide](docs/install.md) for instructions.
See the [installation guide](https://intra.evolix.net/OutilsInternes/bkctld) for instructions.
## Testing
@ -51,6 +49,12 @@ You can deploy test environments with Vagrant :
vagrant up
~~~
To destroy Vagrant VMs :
~~~
vagrant destroy
~~~
### Deployment
Run `vagrant rsync-auto` in a terminal for automatic synchronization of
@ -77,6 +81,8 @@ vagrant@buster-btrfs $ sudo -i
root@buster-btrfs # bats /vagrant/test/*.bats
~~~
[comment]: <> (* pour vim)
You should shellcheck your bats files, but with shellcheck > 0.4.6, because the 0.4.0 version doesn't support bats syntax.
## Usage
@ -99,7 +105,7 @@ pandoc -f markdown \
#### Client configuration
You can backup various systems in the evobackup jails : Linux, BSD,
Windows, macOS. The only need Rsync or an SFTP client.
Windows, macOS. The only need is Rsync or an SFTP client.
~~~
rsync -av -e "ssh -p SSH_PORT" /home/ root@SERVER_NAME:/var/backup/home/

View file

@ -53,6 +53,9 @@ while :; do
-f|--force)
export FORCE=1
;;
--no-header)
export HEADER=0
;;
*)
# Default case: If no more options then break out of the loop.
break
@ -64,7 +67,7 @@ done
subcommand="${1:-}"
case "${subcommand}" in
"inc" | "rm" | "check-jails" | "check-setup" | "stats" | "list")
"inc" | "rm" | "check-jails" | "check-setup" | "check-canary" | "stats" | "list")
"${LIBDIR}/bkctld-${subcommand}"
;;
"check")
@ -116,7 +119,9 @@ case "${subcommand}" in
;;
"status")
jail_name="${2:-}"
printf '%-30s %-10s %-10s %-25s %-20s\n' 'JAIL NAME' 'STATUS' 'PORT' 'RETENTION (DAY/MONTH)' 'IP'
if [ "${HEADER}" = "1" ]; then
printf '%-30s %-10s %-10s %-25s %-20s\n' 'JAIL NAME' 'STATUS' 'PORT' 'RETENTION (DAY/MONTH)' 'IP'
fi
if [ "${jail_name}" = "all" ] || [ -z "${jail_name}" ]; then
for jail in $("${LIBDIR}/bkctld-list"); do
"${LIBDIR}/bkctld-${subcommand}" "${jail}"

View file

@ -15,3 +15,5 @@
#LOGLEVEL=6
#NODE=''
#ARCHIVESDIR='/backup/archives'
#WARNING=48
#CRITICAL=72

View file

@ -2,15 +2,7 @@
## Install from package
A Debian package is available in the Evolix repository
~~~
echo "deb http://pub.evolix.net/ stretch" >> /etc/apt/sources.list
apt update
apt install bkctld
~~~
Then edit `/etc/default/bkctld`
The install documentation is [here](https://intra.evolix.net/OutilsInternes/bkctld)
## Instal from sources
@ -19,17 +11,17 @@ Warning: `cp`-ing the files without `-n` or `-i` will replace existing files !
~~~
# git clone https://gitea.evolix.org/evolix/evobackup.git
# cd evobackup
# cp bkctld /usr/local/sbin/
# cp server/bkctld /usr/local/sbin/
# mkdir -p /usr/local/lib/bkctld
# cp lib/* /usr/local/lib/bkctld/
# cp server/lib/* /usr/local/lib/bkctld/
# mkdir -p /usr/local/share/bkctld
# cp tpl/* /usr/local/share/bkctld/
# cp bkctld.service /lib/systemd/system/
# cp server/tpl/* /usr/local/share/bkctld/
# cp server/bkctld.service /lib/systemd/system/
# mkdir -p /usr/local/share/doc/bkctld
# cp zzz_evobackup /usr/local/share/doc/bkctld/
# cp client/zzz_evobackup /usr/local/share/doc/bkctld/
# mkdir -p /usr/local/share/bash_completion/
# cp bash_completion /usr/local/share/bash_completion/bkctld
# cp bkctld.conf /etc/default/bkctld
# cp server/bash_completion /usr/local/share/bash_completion/bkctld
# cp server/bkctld.conf /etc/default/bkctld
~~~
## Chroot dependencies

59
server/lib/bkctld-check-canary Executable file
View file

@ -0,0 +1,59 @@
#!/bin/sh
#
# Description: check canary file
# Usage: check-canary [<jailname>|all]
#
# shellcheck source=./includes
LIBDIR="$(dirname $0)" && . "${LIBDIR}/includes"
return=0
nb_crit=0
nb_warn=0
nb_ok=0
nb_unkn=0
output=""
date=$(date +"%Y-%m-%d")
# Check each jail status
check_jail() {
jail_name=$1
jail_path=$(jail_path "${jail_name}")
canary_absolute_file="${jail_path}/var/backup/${CANARY_RELATIVE_FILE}"
if [ -f "${canary_absolute_file}" ]; then
if grep --quiet --fixed-string "${date}" "${canary_absolute_file}"; then
nb_ok=$((nb_ok + 1))
output="${output}OK - ${jail_name} - entries found for ${date} in ${CANARY_RELATIVE_FILE} file\n"
else
nb_crit=$((nb_crit + 1))
output="${output}CRITICAL - ${jail_name} - No entry for ${date} in ${CANARY_RELATIVE_FILE} file\n"
[ "${return}" -le 2 ] && return=2
fi
else
nb_crit=$((nb_crit + 1))
output="${output}CRITICAL - ${jail_name} - missing ${CANARY_RELATIVE_FILE} file\n"
[ "${return}" -le 2 ] && return=2
fi
}
for jail_name in $(jails_list); do
check_jail "${jail_name}"
done
[ "${return}" -ge 0 ] && header="OK"
[ "${return}" -ge 1 ] && header="WARNING"
[ "${return}" -ge 2 ] && header="CRITICAL"
[ "${return}" -ge 3 ] && header="UNKNOWN"
printf "%s - %s UNK / %s CRIT / %s WARN / %s OK\n\n" "${header}" "${nb_unkn}" "${nb_crit}" "${nb_warn}" "${nb_ok}"
printf "${output}" | grep -E "^UNKNOWN"
printf "${output}" | grep -E "^CRITICAL"
printf "${output}" | grep -E "^WARNING"
printf "${output}" | grep -E "^OK"
exit "${return}"

View file

@ -16,7 +16,7 @@ output=""
# Verify backup partition is mounted and writable
findmnt --mountpoint "${BACKUP_PARTITION}" -O rw > /dev/null
findmnt -O rw --mountpoint "${BACKUP_PARTITION}" > /dev/null
if [ "$?" -ne 0 ]; then
nb_crit=$((nb_crit + 1))
output="${output}CRITICAL - Backup disk \`/backup' is not mounted (or read-only) !\n"
@ -29,11 +29,12 @@ fi
# Check if the firewall file is sourced
minifirewall_config=/etc/default/minifirewall
minifirewall_version=$(/etc/init.d/minifirewall status | head -1 | cut -d ' ' -f 3)
if [ -n "${FIREWALL_RULES}" ] \
&& [ -r "${FIREWALL_RULES}" ] \
&& [ -f "${minifirewall_config}" ]; then
minifirewall_version=$(grep -E -o "^VERSION=(\S+)" /etc/init.d/minifirewall | head -1 | cut -d '=' -f 2 | tr -d "'" | tr -d '"')
if [ -n "${minifirewall_version}" ] && dpkg --compare-versions "${minifirewall_version}" ge "22.03"; then
# Minifirewall 22.03+ includes files automatically
nb_ok=$((nb_ok + 1))

View file

@ -12,11 +12,12 @@ if [ -z "${jail_name}" ]; then
show_help && exit 1
fi
jail_path=$(jail_path "${jail_name}")
incs_path=$(incs_path "${jail_name}")
test -d "${jail_path}" && error "Skip jail \`${jail_name}' : it already exists"
# Create config and jails directory
mkdir --parents "${CONFDIR}" "${JAILDIR}"
# Create config, jails and incs directories
mkdir --parents "${CONFDIR}" "${JAILDIR}" "${INCDIR}"
if is_btrfs "$(dirname "${JAILDIR}")" || is_btrfs "${JAILDIR}"; then
btrfs_bin=$(command -v btrfs)
@ -28,6 +29,8 @@ else
mkdir --parents "${jail_path}"
fi
mkdir --parents "${incs_path}"
setup_jail_chroot "${jail_name}"
setup_jail_config "${jail_name}"

View file

@ -49,7 +49,7 @@ fi
"${LIBDIR}/bkctld-is-on" "${jail_name}" && "${LIBDIR}/bkctld-stop" "${jail_name}"
rm -f "${CONFDIR}/${jail_name}"
test -f "${CONFDIR}/${jail_name}" && rm -f "${CONFDIR}/${jail_name}"
rm -rf "$(jail_config_dir "${jail_name}")"
btrfs_bin=$(command -v btrfs)

View file

@ -51,15 +51,15 @@ if dry_run; then
else
mv "${jail_path}" "${new_jail_path}"
fi
if dry_run; then
if [ -d "${incs_path}" ]; then
if [ -d "${incs_path}" ]; then
if dry_run; then
echo "[dry-run] rename ${incs_path} to ${new_incs_path}"
fi
else
if [ -d "${incs_path}" ]; then
else
mv "${incs_path}" "${new_incs_path}"
fi
fi
if [ -d "${jail_config_dir}" ]; then
if dry_run; then
echo "[dry-run] rename ${jail_config_dir} to ${new_jail_config_dir}"

View file

@ -15,27 +15,29 @@ ionice -c3 "${DUC}" index -d "${IDX_FILE}" "${JAILDIR}"
touch "${INDEX_DIR}/.lastrun.duc"
EOF
[ ! -f "${INDEX_DIR}/.lastrun.duc" ] && notice "First run of DUC always in progress ..." && exit 0
[ ! -f "${INDEX_DIR}/.lastrun.duc" ] && notice "First run of DUC still in progress ..." && exit 0
[ ! -f ${IDX_FILE} ] && error "Index file doesn't exits !"
printf "Last update of index file : "
stat --format=%Y "${INDEX_DIR}/.lastrun.duc" | xargs -i -n1 date -R -d "@{}"
echo "<jail> <size> <incs> <lastconn>" | awk '{ printf("%- 30s %- 10s %- 10s %- 15s\n", $1, $2, $3, $4); }'
duc_output=$(mktemp)
stat_output=$(mktemp)
incs_output=$(mktemp)
jail_patterns_list=$(mktemp)
# shellcheck disable=SC2064
trap "rm ${duc_output} ${incs_output} ${stat_output}" 0
trap "rm ${duc_output} ${incs_output} ${stat_output} ${jail_patterns_list}" 0
"${DUC}" ls -d "${IDX_FILE}" "${JAILDIR}" > "${duc_output}"
"${DUC}" ls --database "${IDX_FILE}" "${JAILDIR}" > "${duc_output}"
awk '{ print $2 }' "${duc_output}" | while read jail_name; do
jails_list | sed -e "s/^\(.*\)$/\\\\b\1\\\\b/" > "${jail_patterns_list}"
grep -f "${jail_patterns_list}" "${duc_output}" | awk '{ print $2 }' | while read jail_name; do
jail_path=$(jail_path "${jail_name}")
stat --format=%Y "${jail_path}/var/log/lastlog" | xargs -i -n1 date -d "@{}" "+%d-%m-%Y" >> "${stat_output}"
incs_policy_file=$(current_jail_incs_policy_file ${jail_name})
incs_policy_file=$(current_jail_incs_policy_file "${jail_name}")
incs_policy="0"
if [ -r "${incs_policy_file}" ]; then
days=$(grep "^\+" "${incs_policy_file}" | grep --count "day")
@ -45,4 +47,7 @@ awk '{ print $2 }' "${duc_output}" | while read jail_name; do
echo "${incs_policy}" >> "${incs_output}"
done
paste "${duc_output}" "${incs_output}" "${stat_output}" | awk '{ printf("%- 30s %- 10s %- 10s %- 15s\n", $2, $1, $3, $4); }'
(
echo "<jail> <size> <incs> <lastconn>"
paste "${duc_output}" "${incs_output}" "${stat_output}" | awk '{ printf("%s %s %s %s\n", $2, $1, $3, $4); }'
) | column -t

View file

@ -1,7 +1,7 @@
#!/bin/sh
#
# Description: Display status of SSH server
# Usage: status [<jailname>|all]
# Usage: [--no-header] status [<jailname>|all]
#
# shellcheck source=./includes

View file

@ -6,7 +6,7 @@
[ -f /etc/default/bkctld ] && . /etc/default/bkctld
VERSION="22.04"
VERSION="22.11"
LIBDIR=${LIBDIR:-/usr/lib/bkctld}
CONFDIR="${CONFDIR:-/etc/evobackup}"
@ -20,6 +20,7 @@ LOCKDIR="${LOCKDIR:-/run/lock/bkctld}"
ARCHIVESDIR="${ARCHIVESDIR:-${BACKUP_PARTITION}/archives}"
INDEX_DIR="${INDEX_DIR:-${BACKUP_PARTITION}/index}"
IDX_FILE="${IDX_FILE:-${INDEX_DIR}/bkctld-jails.idx}"
CANARY_RELATIVE_FILE="${CANARY_RELATIVE_FILE:-/zzz_evobackup_canary}"
SSHD_PID="${SSHD_PID:-/run/sshd.pid}"
SSHD_CONFIG="${SSHD_CONFIG:-/etc/ssh/sshd_config}"
AUTHORIZED_KEYS="${AUTHORIZED_KEYS:-/root/.ssh/authorized_keys}"
@ -29,6 +30,7 @@ CRITICAL="${CRITICAL:-48}"
WARNING="${WARNING:-24}"
DUC=$(command -v duc-nox || command -v duc)
FORCE="${FORCE:-0}"
HEADER="${HEADER:-1}"
show_version() {
cat <<END
@ -62,6 +64,13 @@ EOF
printf "\n"
}
is_quiet() {
test ${QUIET} -eq 1
}
is_verbose() {
test ${VERBOSE} -eq 1
}
log_date() {
echo "[$(date +"%Y-%m-%d %H:%M:%S")]"
}
@ -127,7 +136,7 @@ is_btrfs() {
inode=$(stat --format=%i "${path}")
test $inode -eq 256
test "$inode" -eq 256
}
# Returns the list of jails found in the "jails" directory (default)
@ -243,15 +252,15 @@ relative_date() {
new_tmp_file() {
name=${1:-}
mktemp --tmpdir=/tmp "bkctld.${$}.${name}.XXXXX"
mktemp --tmpdir "bkctld.${$}.${name}.XXXXX"
}
new_tmp_dir() {
name=${1:-}
mktemp --directory --tmpdir=/tmp "bkctld.${$}.${name}.XXXXX"
mktemp --directory --tmpdir "bkctld.${$}.${name}.XXXXX"
}
cleanup_tmp() {
find /tmp -name "bkctld.${$}.*" -delete
find "${TMPDIR:-/tmp}" -name "bkctld.${$}.*" -delete
}
new_lock_file() {
lock_file=${1:-}

View file

@ -144,7 +144,7 @@ OUT
@test "Check setup WARNING if firewall rules are not sourced" {
/usr/lib/bkctld/bkctld-start ${JAILNAME}
mkdir /etc/minifirewall.d/
mkdir --parents /etc/minifirewall.d/
firewall_rules_file="/etc/minifirewall.d/bkctld"
set_variable "/etc/default/bkctld" "FIREWALL_RULES" "${firewall_rules_file}"
echo "" > "${firewall_rules_file}"
@ -159,7 +159,7 @@ OUT
@test "Check setup OK if firewall rules are sourced" {
/usr/lib/bkctld/bkctld-start ${JAILNAME}
mkdir /etc/minifirewall.d/
mkdir --parents /etc/minifirewall.d/
firewall_rules_file="/etc/minifirewall.d/bkctld"
set_variable "/etc/default/bkctld" "FIREWALL_RULES" "${firewall_rules_file}"
echo "" > "${firewall_rules_file}"
@ -252,3 +252,25 @@ OUT
assert_failure
}
# TODO: write many more tests for bkctld-check-incs
@test "Check-canary fails if a canary file doesn't exist" {
run /usr/lib/bkctld/bkctld-check-canary "${JAILNAME}"
assert_equal "$status" "2"
assert_line "CRITICAL - ${JAILNAME} - missing /zzz_evobackup_canary file"
}
@test "Check-canary fails if a canary is missing today's entries" {
today="$(date +%Y-%m-%d)"
touch "${JAILPATH}/var/backup/zzz_evobackup_canary"
run /usr/lib/bkctld/bkctld-check-canary "${JAILNAME}"
assert_equal "$status" "2"
assert_line "CRITICAL - ${JAILNAME} - No entry for ${today} in /zzz_evobackup_canary file"
}
@test "Check-canary succeeds if a canary has today's entries" {
echo "$(date "+%FT%T%z") bats-test" >> "${JAILPATH}/var/backup/zzz_evobackup_canary"
run /usr/lib/bkctld/bkctld-check-canary "${JAILNAME}"
assert_success
}

View file

@ -9,6 +9,11 @@ load test_helper
run test -e "${CONFDIR}/${JAILNAME}.d/incs_policy"
[ "${status}" -eq 0 ]
}
@test "Inc directory after jail init" {
# An incs_policy file should exist
run test -d "${INCDIR}/${JAILNAME}"
[ "${status}" -eq 0 ]
}
@test "Normal inc creation" {
/usr/lib/bkctld/bkctld-inc