diff --git a/client/CHANGELOG.md b/client/CHANGELOG.md index 06b5faa..ee335f4 100644 --- a/client/CHANGELOG.md +++ b/client/CHANGELOG.md @@ -1,24 +1,43 @@ # 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 +* 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 + ### Deprecated ### 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 + ### Security ## [22.12] + ### Changed * Use --dump-dir instead of --backup-dir to suppress dump-server-state warning diff --git a/client/README.md b/client/README.md deleted file mode 100644 index 6bd23f2..0000000 --- a/client/README.md +++ /dev/null @@ -1,3 +0,0 @@ -Pour l'installation de `zzz_evobackup`, voir - -Pour `update-evobackup-canary`, voir diff --git a/client/Vagrantfile b/client/Vagrantfile new file mode 100644 index 0000000..de3337a --- /dev/null +++ b/client/Vagrantfile @@ -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 diff --git a/client/bin/evobackupctl b/client/bin/evobackupctl new file mode 100644 index 0000000..286eb94 --- /dev/null +++ b/client/bin/evobackupctl @@ -0,0 +1,153 @@ +#!/usr/bin/env bash + +# shellcheck disable=SC2155 +readonly PROGNAME=$(basename "${0}") +# shellcheck disable=SC2155 +readonly PROGDIR=$(readlink -m "$(dirname "${0}")") +# 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 <, + Jérémy Lecour . + +${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 < /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}" + + sed -i "s|@COMMAND@|${PROGDIR}/${PROGNAME} ${ARGS}|" "${dest_path}" + sed -i "s|@DATE@|$(date --iso-8601=seconds)|" "${dest_path}" + sed -i "s|@VERSION@|${VERSION}|" "${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} diff --git a/client/lib/dump/elasticsearch.sh b/client/lib/dump/elasticsearch.sh new file mode 100644 index 0000000..1e5475b --- /dev/null +++ b/client/lib/dump/elasticsearch.sh @@ -0,0 +1,301 @@ +#!/usr/bin/env bash +# shellcheck disable=SC2034,SC2317,SC2155 + +####################################################################### +# Snapshot Elasticsearch data +# +# Arguments: +# --protocol= (default: http) +# --cacert=[String] (default: ) +# path to the CA certificate to use when using https +# --host=[String] (default: localhost) +# --port=[Integer] (default: 9200) +# --user=[String] (default: ) +# --password=[String] (default: ) +# --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}" +} diff --git a/client/lib/dump/misc.sh b/client/lib/dump/misc.sh new file mode 100644 index 0000000..c7ea1fa --- /dev/null +++ b/client/lib/dump/misc.sh @@ -0,0 +1,559 @@ +#!/usr/bin/env bash +# shellcheck disable=SC2034,SC2317,SC2155 + +####################################################################### +# Dump LDAP files (config, data, all) +# +# Arguments: +####################################################################### +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: ) +# --user=[String] (default: ) +# --password=[String] (default: ) +# --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: +####################################################################### +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: ) +####################################################################### +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: +# +# 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: +####################################################################### +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}" +} diff --git a/client/lib/dump/mysql.sh b/client/lib/dump/mysql.sh new file mode 100644 index 0000000..0d288b4 --- /dev/null +++ b/client/lib/dump/mysql.sh @@ -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: ) +# --socket=[String] (default: ) +# --user=[String] (default: ) +# --password=[String] (default: ) +# --defaults-file=[String] (default: ) +# --defaults-extra-file=[String] (default: ) +# --defaults-group-suffix=[String] (default: ) +# --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: ) +# --socket=[String] (default: ) +# --user=[String] (default: ) +# --password=[String] (default: ) +# --defaults-file=[String] (default: ) +# --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: ) +# --port=[Integer] (default: ) +# --socket=[String] (default: ) +# --user=[String] (default: ) +# --password=[String] (default: ) +# --defaults-file=[String] (default: ) +# --defaults-extra-file=[String] (default: ) +# --defaults-group-suffix=[String] (default: ) +# --dump-label=[String] (default: "default") +# used as suffix of the dump dir to differenciate multiple instances +# --compress= (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: ) +# --socket=[String] (default: ) +# --user=[String] (default: ) +# --password=[String] (default: ) +# --defaults-file=[String] (default: ) +# --defaults-extra-file=[String] (default: ) +# --defaults-group-suffix=[String] (default: ) +# --dump-label=[String] (default: "default") +# used as suffix of the dump dir to differenciate multiple instances +# --compress= (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: ) +# --socket=[String] (default: ) +# --user=[String] (default: ) +# --password=[String] (default: ) +# --defaults-file=[String] (default: ) +# --defaults-extra-file=[String] (default: ) +# --defaults-group-suffix=[String] (default: ) +# --dump-label=[String] (default: "default") +# used as suffix of the dump dir to differenciate multiple instances +# --compress= (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 +} diff --git a/client/lib/dump/postgresql.sh b/client/lib/dump/postgresql.sh new file mode 100644 index 0000000..302942c --- /dev/null +++ b/client/lib/dump/postgresql.sh @@ -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= (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: +####################################################################### +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: +# +# 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}" +} diff --git a/client/lib/main.sh b/client/lib/main.sh new file mode 100644 index 0000000..910101e --- /dev/null +++ b/client/lib/main.sh @@ -0,0 +1,466 @@ +#!/usr/bin/env bash +# shellcheck disable=SC2034,SC2317 + +readonly VERSION="24.04-pre1" + +# 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} +} diff --git a/client/lib/utilities.sh b/client/lib/utilities.sh new file mode 100644 index 0000000..e14a434 --- /dev/null +++ b/client/lib/utilities.sh @@ -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' +} diff --git a/client/lib/zzz_evobackup.sh b/client/lib/zzz_evobackup.sh new file mode 100644 index 0000000..c8bbe22 --- /dev/null +++ b/client/lib/zzz_evobackup.sh @@ -0,0 +1,325 @@ +#!/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: ) + # --port=[Integer] (default: ) + # --socket=[String] (default: ) + # --user=[String] (default: ) + # --password=[String] (default: ) + # --defaults-file=[String] (default: ) + # --defaults-extra-file=[String] (default: ) + # --defaults-group-suffix=[String] (default: ) + # --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: ) + # --socket=[String] (default: ) + # --user=[String] (default: ) + # --password=[String] (default: ) + # --defaults-file=[String] (default: ) + # --defaults-extra-file=[String] (default: ) + # --defaults-group-suffix=[String] (default: ) + # --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: ) + # --socket=[String] (default: ) + # --user=[String] (default: ) + # --password=[String] (default: ) + # --defaults-file=[String] (default: ) + # --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: ) + # --socket=[String] (default: ) + # --user=[String] (default: ) + # --password=[String] (default: ) + # --defaults-file=[String] (default: ) + # --defaults-extra-file=[String] (default: ) + # --defaults-group-suffix=[String] (default: ) + # --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: ) + # --socket=[String] (default: ) + # --user=[String] (default: ) + # --password=[String] (default: ) + # --defaults-file=[String] (default: ) + # --defaults-extra-file=[String] (default: ) + # --defaults-group-suffix=[String] (default: ) + # --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=] + + ########## 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 command +### log "LOCAL_TASKS - ${FUNCNAME[0]}: start ${dump_file}" +### +### # Execute your dump command +### # Send errors 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}" +### ${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 command +### 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 diff --git a/client/update-evobackup-canary b/client/update-evobackup-canary deleted file mode 100644 index 868c3be..0000000 --- a/client/update-evobackup-canary +++ /dev/null @@ -1,129 +0,0 @@ -#!/bin/sh - -PROGNAME="update-evobackup-canary" -REPOSITORY="https://gitea.evolix.org/evolix/evobackup" - -VERSION="22.06" -readonly VERSION - -# base functions - -show_version() { - cat <, - Jérémy Lecour , - and others. - -${REPOSITORY} - -${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 <> "${canary_file}" -} - -# parse options -# based on https://gist.github.com/deshion/10d3cb5f88a21671e17a -while :; do - case $1 in - -h|-\?|--help) - show_help - exit 0 - ;; - -V|--version) - show_version - exit 0 - ;; - - -w|--who) - # with value separated by space - if [ -n "$2" ]; then - who=$2 - shift - else - printf 'ERROR: "-w|--who" requires a non-empty option argument.\n' >&2 - exit 1 - fi - ;; - --who=?*) - # with value speparated by = - who=${1#*=} - ;; - --who=) - # without value - printf 'ERROR: "--who" requires a non-empty option argument.\n' >&2 - exit 1 - ;; - - -f|--file) - # with value separated by space - if [ -n "$2" ]; then - canary_file=$2 - shift - else - printf 'ERROR: "-f|--file" requires a non-empty option argument.\n' >&2 - exit 1 - fi - ;; - --file=?*) - # with value speparated by = - canary_file=${1#*=} - ;; - --file=) - # without value - printf 'ERROR: "--file" requires a non-empty option argument.\n' >&2 - exit 1 - ;; - - --) - # End of all options. - shift - break - ;; - -?*) - # ignore unknown options - printf 'WARN: Unknown option : %s\n' "$1" >&2 - exit 1 - ;; - *) - # Default case: If no more options then break out of the loop. - break - ;; - esac - - shift -done - -export LC_ALL=C - -set -u - -main diff --git a/client/vagrant.yml b/client/vagrant.yml new file mode 100644 index 0000000..39647a5 --- /dev/null +++ b/client/vagrant.yml @@ -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 } diff --git a/client/zzz_evobackup b/client/zzz_evobackup deleted file mode 100755 index a075cef..0000000 --- a/client/zzz_evobackup +++ /dev/null @@ -1,677 +0,0 @@ -#!/bin/bash -# -# Script Evobackup client -# See https://gitea.evolix.org/evolix/evobackup -# -# Authors: Evolix , -# Gregory Colpart , -# Romain Dessort , -# Benoit Série , -# Tristan Pilat , -# Victor Laborie , -# Jérémy Lecour -# and others. -# -# Licence: AGPLv3 -# -# /!\ DON'T FORGET TO SET "MAIL" and "SERVERS" VARIABLES - -##### Configuration ################################################### - -VERSION="22.12" - -# 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" - -# explicit PATH -PATH=/sbin:/usr/sbin:/bin:/usr/bin:/usr/local/sbin:/usr/local/bin - -# Should we fallback on other servers when the first one 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" - -# Full Rsync log file, reset each time -RSYNC_LOGFILE="/var/log/${PROGNAME}.rsync.log" - -HOSTNAME=$(hostname) - -DATE_FORMAT="%Y-%m-%d %H:%M:%S" - -# Enable/disable local tasks (default: enabled) -: "${LOCAL_TASKS:=1}" -# Enable/disable sync tasks (default: enabled) -: "${SYNC_TASKS:=1}" - -CANARY_FILE="/zzz_evobackup_canary" - -# Source paths can be customized -# Empty lines, and lines containing # or ; are ignored -# NOTE: remember to single-quote paths if they contain globs (*) -# and you want to defer expansion -RSYNC_INCLUDES=" -/etc -/root -/var -/home -" - -# Excluded paths can be customized -# Empty lines, and lines beginning with # or ; are ignored -# NOTE: remember to single-quote paths if they contain globs (*) -# and you want to defer expansion -RSYNC_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 -" - - -##### FUNCTIONS ####################################################### - -local_tasks() { - 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 - # find ${LOCAL_BACKUP_DIR}/ -type f -name '*.err' -delete - - ## example with global and compressed mysqldump - # mysqldump --defaults-extra-file=/etc/mysql/debian.cnf -P 3306 \ - # --opt --all-databases --force --events --hex-blob 2> ${LOCAL_BACKUP_DIR}/mysql.bak.err | gzip --best > ${LOCAL_BACKUP_DIR}/mysql.bak.gz - # last_rc=$? - # if [ ${last_rc} -ne 0 ]; then - # error "mysqldump (global compressed) returned an error ${last_rc}, check ${LOCAL_BACKUP_DIR}/mysql.bak.err" - # rc=101 - # fi - - ## 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 \ - # | grep --extended-regexp --invert-match "^(Database|information_schema|performance_schema|sys)"); do - # mysqldump --defaults-extra-file=/etc/mysql/debian.cnf --force -P 3306 --events --hex-blob $i 2> ${LOCAL_BACKUP_DIR}/${i}.err | gzip --best > ${LOCAL_BACKUP_DIR}/mysql/${i}.sql.gz - # last_rc=$? - # if [ ${last_rc} -ne 0 ]; then - # error "mysqldump (${i} compressed) returned an error ${last_rc}, check ${LOCAL_BACKUP_DIR}/${i}.err" - # rc=102 - # fi - # done - - ## Dump all grants (requires 'percona-toolkit' package) - # mkdir -p -m 700 ${LOCAL_BACKUP_DIR}/mysql/ - # pt-show-grants --flush --no-header 2> ${LOCAL_BACKUP_DIR}/mysql/all_grants.err > ${LOCAL_BACKUP_DIR}/mysql/all_grants.sql - # last_rc=$? - # if [ ${last_rc} -ne 0 ]; then - # error "pt-show-grants returned an error ${last_rc}, check ${LOCAL_BACKUP_DIR}/mysql/all_grants.err" - # rc=103 - # fi - - # Dump all variables - # mysql -A -e"SHOW GLOBAL VARIABLES;" 2> ${LOCAL_BACKUP_DIR}/MySQLCurrentSettings.err > ${LOCAL_BACKUP_DIR}/MySQLCurrentSettings.txt - # last_rc=$? - # if [ ${last_rc} -ne 0 ]; then - # error "mysql (variables) returned an error ${last_rc}, check ${LOCAL_BACKUP_DIR}/MySQLCurrentSettings.err" - # rc=104 - # fi - - ## 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 \ - # | grep --extended-regexp --invert-match "^(Database|information_schema|performance_schema|sys)"); do - # mysqldump --defaults-extra-file=/etc/mysql/debian.cnf --force -P 3306 --no-data --databases $i 2> ${LOCAL_BACKUP_DIR}/${i}.schema.err > ${LOCAL_BACKUP_DIR}/mysql/${i}.schema.sql - # last_rc=$? - # if [ ${last_rc} -ne 0 ]; then - # error "mysqldump (${i} schema) returned an error ${last_rc}, check ${LOCAL_BACKUP_DIR}/${i}.schema.err" - # rc=105 - # fi - # 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 2> ${LOCAL_BACKUP_DIR}/mysql/MYBASE.err - # last_rc=$? - # if [ ${last_rc} -ne 0 ]; then - # error "mysqldump (MYBASE) returned an error ${last_rc}, check ${LOCAL_BACKUP_DIR}/mysql/MYBASE.err" - # rc=106 - # fi - - ## 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 \ - # | grep --extended-regexp --invert-match "^(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 2> /home/mysqldump/$i.err" - # last_rc=$? - # if [ ${last_rc} -ne 0 ]; then - # error "mysqldump (${i} files) returned an error ${last_rc}, check /home/mysqldump/$i.err" - # rc=107 - # fi - # done - - ## example with mysqlhotcopy - # mkdir -p -m 700 ${LOCAL_BACKUP_DIR}/mysqlhotcopy/ - # mysqlhotcopy MYBASE ${LOCAL_BACKUP_DIR}/mysqlhotcopy/ 2> ${LOCAL_BACKUP_DIR}/mysqlhotcopy/MYBASE.err - # last_rc=$? - # if [ ${last_rc} -ne 0 ]; then - # error "mysqlhotcopy returned an error ${last_rc}, check ${LOCAL_BACKUP_DIR}/mysqlhotcopy/MYBASE.err" - # rc=108 - # fi - - ## example for multiples MySQL instances - # mysqladminpasswd=$(grep -m1 'password = .*' /root/.my.cnf|cut -d" " -f3) - # grep --extended-regexp "^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 2> ${LOCAL_BACKUP_DIR}/mysql.${instance}.err | gzip --best > ${LOCAL_BACKUP_DIR}/mysql.${instance}.bak.gz - # last_rc=$? - # if [ ${last_rc} -ne 0 ]; then - # error "mysqldump (instance ${instance}) returned an error ${last_rc}, check ${LOCAL_BACKUP_DIR}/mysql.${instance}.err" - # rc=107 - # fi - # 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 - # ) - - ## 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 - # ( - # cd /var/lib/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 -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-* - ## Copy dump.rdb file for each found instance - # for instance in $(find /var/lib/ -mindepth 1 -maxdepth 1 '(' -type d -o -type l ')' -name 'redis*'); do - # if [ -f "${instance}/dump.rdb" ]; then - # name=$(basename $instance) - # mkdir -p ${LOCAL_BACKUP_DIR}/${name} - # cp -a "${instance}/dump.rdb" "${LOCAL_BACKUP_DIR}/${name}" - # gzip "${LOCAL_BACKUP_DIR}/${name}/dump.rdb" - # fi - # 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" >> "${LOGFILE}" - # curl -s -XPUT "localhost:9200/_snapshot/snaprepo/snapshot.daily?wait_for_completion=true" >> "${LOGFILE}" - ## 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" >> "${LOGFILE}" - # curl -s -XPUT "localhost:9200/_snapshot/snaprepo/snapshot.daily?wait_for_completion=true" >> "${LOGFILE}" - # 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" >> "${LOGFILE}" - - ## 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) - network_targets="8.8.8.8 www.evolix.fr travaux.evolix.net" - mtr_bin=$(command -v mtr) - if [ -n "${mtr_bin}" ]; then - for addr in ${network_targets}; do - ${mtr_bin} -r "${addr}" > "${LOCAL_BACKUP_DIR}/mtr-${addr}" - done - fi - traceroute_bin=$(command -v traceroute) - if [ -n "${traceroute_bin}" ]; then - for addr in ${network_targets}; do - ${traceroute_bin} -n "${addr}" > "${LOCAL_BACKUP_DIR}/traceroute-${addr}" 2>&1 - done - fi - - server_state_dir="${LOCAL_BACKUP_DIR}/server-state" - - dump_server_state_bin=$(command -v dump-server-state) - if [ -z "${dump_server_state_bin}" ]; then - error "dump-server-state is missing" - rc=1 - else - if [ "${SYSTEM}" = "linux" ]; then - ${dump_server_state_bin} --all --force --dump-dir "${server_state_dir}" - last_rc=$? - if [ ${last_rc} -ne 0 ]; then - error "dump-server-state returned an error ${last_rc}, check ${server_state_dir}" - rc=1 - fi - else - ${dump_server_state_bin} --all --force --dump-dir "${server_state_dir}" - last_rc=$? - if [ ${last_rc} -ne 0 ]; then - error "dump-server-state returned an error ${last_rc}, check ${server_state_dir}" - rc=1 - fi - 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" -} -build_rsync_main_cmd() { - ################################################################### - # /!\ WARNING /!\ WARNING /!\ WARNING /!\ WARNING /!\ WARNING /!\ # - ################################################################### - # DO NOT USE COMMENTS in rsync lines # - # DO NOT ADD WHITESPACES AFTER \ in rsync lines # - # It breaks the command and destroys data # - # You should not modify this, unless you are really REALLY sure # - ################################################################### - - # Create a temp file for excludes and includes - includes_file="$(mktemp --tmpdir "${PROGNAME}.includes.XXXXXX")" - excludes_file="$(mktemp --tmpdir "${PROGNAME}.excludes.XXXXXX")" - # … and add them to the list of files to delete at exit - temp_files="${includes_file} ${excludes_file}" - trap "rm -f ${temp_files}" EXIT - - # Store includes/excludes in files - # without blank lines of comments (# or ;) - echo "${RSYNC_INCLUDES}" | sed -e 's/\s*\(#\|;\).*//; /^\s*$/d' > "${includes_file}" - echo "${RSYNC_EXCLUDES}" | sed -e 's/\s*\(#\|;\).*//; /^\s*$/d' > "${excludes_file}" - - # Rsync command - cmd="$(command -v rsync)" - - # Rsync main options - cmd="${cmd} --archive" - cmd="${cmd} --itemize-changes" - cmd="${cmd} --quiet" - cmd="${cmd} --stats" - cmd="${cmd} --human-readable" - cmd="${cmd} --relative" - cmd="${cmd} --partial" - cmd="${cmd} --delete" - cmd="${cmd} --delete-excluded" - cmd="${cmd} --force" - cmd="${cmd} --ignore-errors" - cmd="${cmd} --log-file=${RSYNC_LOGFILE}" - cmd="${cmd} --rsh='ssh -p ${SSH_PORT} -o \"ConnectTimeout ${SSH_CONNECT_TIMEOUT}\"'" - - # Rsync excludes - while read line ; do - cmd="${cmd} --exclude ${line}" - done < "${excludes_file}" - - # Rsync local sources - cmd="${cmd} ${default_includes}" - while read line ; do - cmd="${cmd} ${line}" - done < "${includes_file}" - - # Rsync remote destination - cmd="${cmd} root@${SSH_SERVER}:/var/backup/" - - # output final command - echo "${cmd}" -} -build_rsync_canary_cmd() { - # Rsync command - cmd="$(command -v rsync)" - # Rsync options - cmd="${cmd} --rsh='ssh -p ${SSH_PORT} -o \"ConnectTimeout ${SSH_CONNECT_TIMEOUT}\"'" - # Rsync local source - cmd="${cmd} ${CANARY_FILE}" - # Rsync remote destination - cmd="${cmd} root@${SSH_SERVER}:/var/backup/" - - # output final command - echo "${cmd}" -} -sync_tasks() { - 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) - - log "START SYNC_TASKS - server=${server}" - - # default paths, depending on system - if [ "${SYSTEM}" = "linux" ]; then - default_includes="/bin /boot /lib /opt /sbin /usr" - else - default_includes="/bsd /bin /sbin /usr" - fi - - # reset Rsync log file - if [ -n "$(command -v truncate)" ]; then - truncate -s 0 "${RSYNC_LOGFILE}" - else - printf "" > "${RSYNC_LOGFILE}" - fi - - # Build the final Rsync command - rsync_main_cmd=$(build_rsync_main_cmd) - - # … log it - log "SYNC_TASKS - Rsync main command : ${rsync_main_cmd}" - - # … execute it - eval "${rsync_main_cmd}" - - rsync_main_rc=$? - - # Copy last lines of rsync log to the main log - tail -n 30 "${RSYNC_LOGFILE}" >> "${LOGFILE}" - - if [ ${rsync_main_rc} -ne 0 ]; then - error "rsync returned an error ${rsync_main_rc}, check ${LOGFILE}" - rc=201 - else - # Build the canary Rsync command - rsync_canary_cmd=$(build_rsync_canary_cmd) - - # … log it - log "SYNC_TASKS - Rsync canary command : ${rsync_canary_cmd}" - - # … execute it - eval "${rsync_canary_cmd}" - fi - - log "STOP SYNC_TASKS - server=${server}" -} - -# 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}" -} -error() { - msg="${1:-$(cat /dev/stdin)}" - pid=$$ - printf "[%s] %s[%s]: %s\\n" \ - "$(/bin/date +"${DATE_FORMAT}")" "${PROGNAME}" "${pid}" "${msg}" \ - >&2 -} - -main() { - START_EPOCH=$(/bin/date +%s) - log "START GLOBAL - VERSION=${VERSION} LOCAL_TASKS=${LOCAL_TASKS} SYNC_TASKS=${SYNC_TASKS}" - - # shellcheck disable=SC2174 - mkdir -p -m 700 ${LOCAL_BACKUP_DIR} - - ## Force umask - umask 077 - - ## Initialize variable to store SSH connection errors - SERVERS_SSH_ERRORS="" - - ## 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}" - - # Initialize a list of files to delete at exit - # Any file added to the list will also be deleted at exit - temp_files="${PIDFILE}" - - # shellcheck disable=SC2064 - trap "rm -f ${temp_files}" EXIT - - # Update canary to keep track of each run - update-evobackup-canary --who "${PROGNAME}" - - if [ "${LOCAL_TASKS}" = "1" ]; then - local_tasks - fi - - if [ "${SYNC_TASKS}" = "1" ]; then - sync_tasks - fi - - 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} -} - -# set all programs to C language (english) -export LC_ALL=C - -# Error on unassigned variable -set -u - -# Default return-code (0 == succes) -rc=0 - -# execute main funciton -main - -exit ${rc} diff --git a/server/CHANGELOG.md b/server/CHANGELOG.md index 3012189..c62e455 100644 --- a/server/CHANGELOG.md +++ b/server/CHANGELOG.md @@ -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] @@ -16,6 +21,8 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ### Fixed +* Test presence of old config file before trying to delete it + ### Security ## [22.11] - 2022-11-28 diff --git a/server/lib/bkctld-remove b/server/lib/bkctld-remove index 382bac7..87a0ec0 100755 --- a/server/lib/bkctld-remove +++ b/server/lib/bkctld-remove @@ -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) diff --git a/server/lib/includes b/server/lib/includes index 1567c77..0cec858 100755 --- a/server/lib/includes +++ b/server/lib/includes @@ -252,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:-}