2
0
Kovács Zoltán 005d0503e2 Ubuntu 24.04 LTS conformance mods
* docker-compose-v2 compatibility
* MySQL/MariaDB dual comaptibility
ACME client minor upgrade
2024-08-24 19:46:03 +02:00

325 lines
12 KiB
Bash
Executable File

#! /bin/bash
#
# Dumps a MySQL or MariaDB database from a native or dockerized instance running
# on this box. This is a wrapper script to the mysqldump or mariadb-dump tool.
#
# If the DB engine is dockerized you need call as a Docker manager user
# (member of the docker Linux group).
#
# Accepts few mysql/mariadb-dump options as well as the optional database password
# and the optional output pathname:
#
# $0 [-u dbuser] [-p dbpass] [-h dbhost] [-P dbport]
# [-C container] [-d database] [-f dumpfile ] [--compress] [--force]
# [database (if not in -d)] [dumpfile (if not in -f)]
#
# Author: Kovács Zoltán <kovacs.zoltan@smartfront.hu>
# Kovács Zoltán <kovacsz@marcusconsulting.hu>
# License: GNU/GPL v3+ (https://www.gnu.org/licenses/gpl-3.0.en.html)
# 2024-08-24 v1.1
# mod: MariaDB and MySQL dual compatibility (native and dockerized).
# Tested with Ubuntu 24.04 LTS.
# 2023-06-18 v1.0
# new: forked from the "SMARTERP_skeleton" repository.
# 2023-02-15 v0.3
# fix: Some updates to MySQL messed with our mysqldump settings.
# https://dev.mysql.com/doc/relnotes/mysql/5.7/en/news-5-7-31.html#mysqld-5-7-31-security
# --no-tablespaces (the recommended option) has been added to fix.
# https://dev.mysql.com/doc/refman/5.7/en/flush.html#flush-tables-with-read-lock
# --single-transaction=false has been added as a temporary workaround.
# 2020-11-12 v0.2
# fix: "$(which gzip)" instad of "$GZIP", see also:
# https://www.gnu.org/software/gzip/manual/html_node/Environment.html
# mod: Accepts a dump folder name as well, instead of a dump file name.
# 2020-09-17 v0.1 Initial release
# Accepted environment variables and their defaults.
#
MYCONTAINER=${MYCONTAINER-""} # Docker container's name
MYDATABASE=${MYDATABASE-""} # Database name to dump
MYDUMP=${MYDUMP-""} # Dump file pathname
MYHOST=${MYHOST:-"localhost"} # Connection parameter
MYOPTIONS=${MYOPTIONS-""} # Options to pass to mysqldump
MYPASSWORD=${MYPASSWORD-""} # Credential for the MySQL user
MYPORT=${MYPORT:-"3306"} # Connection parameter
MYUSER=${MYUSER:-"root"} # MySQL user for this dump
### Temporailly ignored! Need to sanitize.
MYOPTIONS=""
# Other initialisations.
#
MYDUMPFORCED="" # Dumps despite failed checks
# Our default parameters for the mysql/mariadb-dump
# Content of the MYOPTIONS will also appended during the actual dump.
dumpparameters="--comments --events --routines --triggers "
dumpparameters+="--complete-insert --dump-date --force --no-create-db "
dumpparameters+="--opt --single-transaction "
## https://dev.mysql.com/doc/relnotes/mysql/5.7/en/news-5-7-31.html#mysqld-5-7-31-security
dumpparameters+="--no-tablespaces "
## https://dev.mysql.com/doc/relnotes/mysql/5.7/en/news-5-7-41.html
## a temporary workaround only
dumpparameters+="--single-transaction=false "
# Technical databases which are never dumped.
vetodatabases="information_schema mysql performance_schema sys"
# Messages.
#
MSG_ABORTED="aborted"
MSG_BADCRED="Bad credentials for MySQL"
MSG_BADOPT="Invalid option"
MSG_DOESNOTRUN="Doesn't run the database container"
MSG_DOCKERGRPNEED="You must be a member of the docker group."
MSG_FAILSIZE="Failed to size the database"
MSG_FORCED="but forced to continue"
MSG_MISSINGDEP="Fatal: missing dependency"
MSG_MISSINGDB="Missing database"
MSG_NONWRITE="The target directory isn't writable"
MSG_NOSPACE="Not enough space to dump the database"
MSG_USAGE="Usage: $0 [options] [database [dump_pathname|-]]\n"
MSG_USAGE+="Option:\tENVVAR:\n"
MSG_USAGE+=" -C\tMYCONTAINER\tMySQL Docker container's name\n"
MSG_USAGE+=" -d\tMYDATABASE\tMySQL database to dump\n"
MSG_USAGE+=" -f\tMYDUMP\t\tDumpfile pathname\n"
MSG_USAGE+=" -h\tMYHOST\t\tHostname or IP to connect (localhost)\n"
MSG_USAGE+=" -p\tMYPORT\t\tTCP port to connect (3306)\n"
MSG_USAGE+=" -P\tMYPASSWORD\tMySQL password\n"
MSG_USAGE+=" -u\tMYUSER\t\tMySQL username (root)\n"
MSG_USAGE+="--compress\t\tCompresses with gzip\n"
MSG_USAGE+="--force\t\t\tForces the operation despite the failed checks\n"
# Getting options.
#
while getopts ":-:c:C:d:D:f:F:h:H:p:P:u:U:" option
do
case ${option} in
"-" )
if [ "$OPTARG" = "compress" ]; then compress="yes"
elif [ "$OPTARG" = "force" ]; then MYDUMPFORCED="yes"
elif [ "$OPTARG" = "help" ]; then echo -e "$MSG_USAGE" >&2; exit
else echo "$MSG_BADOPT --$OPTARG" >&2; exit 1
fi
;;
"c" | "C" )
MYCONTAINER="$OPTARG"
;;
"d" | "D" )
MYDATABASE="$OPTARG"
;;
"f" | "F" )
MYDUMP="$OPTARG"
;;
"h" | "H" )
MYHOST="$OPTARG"
;;
"P" )
MYPASSWORD="$OPTARG"
;;
"p" )
MYPORT="$OPTARG"
;;
"u" | "U" )
MYUSER="$OPTARG"
;;
\? )
echo "$MSG_BADOPT -$OPTARG" >&2; exit 1
;;
esac
done; shift $((OPTIND -1))
# All options are processed.
# Checks the dependencies.
#
# Conditional dependencies.
if [ -n "$MYCONTAINER" ]; then
# Dockerized
additem="docker"
else
# Native - MySQL or MariaDB CLI?
if [ -n "$(which mysql)" ]
then additem="mysql mysqldump"
else additem="mariadb mariadb-dump"; fi
fi
# Common dependencies.
TR=$(which tr 2>/dev/null)
if [ -z "$TR" ]; then echo "$MSG_MISSINGDEP tr."; exit 1 ; fi
for item in cat cut date df dirname grep gzip hostname id pwd sed tail tee $additem
do
if [ -n "$(which $item)" ]
then export $(echo $item | "$TR" '[:lower:]' '[:upper:]')=$(which $item)
else echo "$MSG_MISSINGDEP $item." >&2; exit 1; fi
done
# All dependencies are available via "$THECOMMAND" (upper case) call.
#
# Unifies the call of the clients in a native environment.
if [ -z "$MYCONTAINER" ]; then
if [ -z "$MYSQL" -a -n "$MARIADB" ]; then MYSQL="$MARIADB"; fi
if [ -z "$MYSQLDUMP" -a -n "$MARIADB_DUMP" ]; then MYSQLDUMP="$MARIADB_DUMP"; fi
fi
#
# An additional bugfix (use "$(which gzip)" instead of "$GZIP"):
# https://www.gnu.org/software/gzip/manual/html_node/Environment.html
GZIP=""
# Need to be root or a Docker manager user if the DB runs in a container.
#
[[ -n "$MYCONTAINER" ]] && [[ "$USER" != 'root' ]] \
&& [[ -z "$(echo "$("$ID" -Gn "$USER") " | "$GREP" ' docker ')" ]] \
&& echo "$MSG_DOCKERGRPNEED" >&2 && exit 1 #"
# If the MySQL is dockerized the container must be running.
#
[[ -n "$MYCONTAINER" ]] \
&& [[ -z "$("$DOCKER" ps -q -f name=$MYCONTAINER)" ]] \
&& echo "$MSG_DOESNOTRUN $MYCONTAINER" >&2 && exit 1
# Determines the (mandatory) database to dump.
#
# Lack of -d the 1st non-option parameter is the database's name.
if [ -z "$MYDATABASE" -a -n "$1" ]; then MYDATABASE="$1"; shift; fi
if [ -z "$MYDATABASE" ]; then echo -e "$MSG_USAGE" >&2; exit 1; fi
# A humble sanitization.
if [[ ! "$MYDATABASE" =~ ^([[:alnum:]]|[_])*$ ]]; then
echo -e "$MSG_USAGE" >&2; exit 1; fi
# Silently refuses MySQL internal databases.
for veto in $vetodatabases ""
do
[[ "$MYDATABASE" = "$veto" ]] && exit 0
done
# We've a database name to dump.
# Optional backup file pathname, defaults to ./hostname.timestamp.MYDATABASE.sql
#
if [ -z "$MYDUMP" -a -n "$1" ]; then MYDUMP="$1"; shift; fi
if [ -d "$MYDUMP" ]; then
MYDUMP+="/$MYDATABASE.$("$DATE" '+%Y%m%d_%H%M%S').$("$HOSTNAME").sql"
fi
if [ -z "$MYDUMP" ]; then
MYDUMP="$PWD/$MYDATABASE.$("$DATE" '+%Y%m%d_%H%M%S').$("$HOSTNAME").sql"
fi
if [ "$MYDUMP" = "-" ]; then
# If '-' was given as the MYDUMP, we need output to STDOUT.
MYDUMP=""
logfile="/dev/null"
else
# Adds the relevant extension to the MYDUMP and the logfile.
MYDUMP="${MYDUMP%.sql}.sql"
logfile="${MYDUMP%.sql}.log"
fi
# The folder to contain the new files must be writable.
[[ -n "$MYDUMP" ]] && [[ ! -w "$("$DIRNAME" "$MYDUMP")" ]] \
&& echo "$MSG_NONWRITE \"$("$DIRNAME" "$MYDUMP")\"" >&2 && exit 1
# Prepopulates the MySQL commands.
#
my_connect=""
[[ -n "$MYHOST" ]] && my_connect+=" --host=$MYHOST"
[[ -n "$MYPORT" ]] && my_connect+=" --port=$MYPORT"
[[ -n "$MYUSER" ]] && my_connect+=" --user=$MYUSER"
[[ -n "$MYPASSWORD" ]] && my_connect+=" --password=$MYPASSWORD"
# Checks credentials and existence of the database given.
#
if [ -z "$MYCONTAINER" ]; then
databases=$("$MYSQL" -N --batch $my_connect --execute='show databases;' 2>/dev/null )
else
# In a containerized environment we've to find out which CLIs are inside.
# Try the mysql first:
MYSQL="mysql"; MYSQLDUMP="mysqldump"
databases=$("$DOCKER" exec $MYCONTAINER sh -c "$MYSQL -N --batch $my_connect --execute='show databases;'" 2>/dev/null)
# On failure try the mariadb:
if [ -z "$databases" ]; then
MYSQL="mariadb"; MYSQLDUMP="mariadb-dump"
databases=$("$DOCKER" exec $MYCONTAINER sh -c "$MYSQL -N --batch $my_connect --execute='show databases;'" 2>/dev/null)
fi
fi
# CLI and Credentials are OK?
[[ -z "$databases" ]] \
&& echo "$MSG_BADCRED ($MYUSER@$([[ -n "$MYCONTAINER" ]] && echo "$MYCONTAINER:")$MYHOST)." >&2 && exit 1
# Existence?
[[ ! "$databases" =~ (^|[[:space:]])"$MYDATABASE"($|[[:space:]]) ]] \
&& echo "$MSG_MISSINGDB \"$MYDATABASE\"." >&2 && exit 1
# We've the database connection and existence checked.
# Do we size the database?
#
dbsize=0
# It isn't relevant when we'll dump to the STDOUT.
if [ -n "$MYDUMP" ]; then
# Calculates the size of the database (KB).
SQLVERB='SELECT table_schema, '
SQLVERB+='ROUND(SUM(data_length + index_length) / 1024, 0) '
SQLVERB+="FROM information_schema.TABLES WHERE table_schema='$MYDATABASE' "
SQLVERB+="GROUP BY table_schema;"
if [ -n "$MYCONTAINER" ]; then
# Dockerized database.
dbsize=$("$DOCKER" exec $MYCONTAINER sh -c "echo \"$SQLVERB\" | "$MYSQL" -N --batch $my_connect" 2>/dev/null | \
"$CUT" -d$'\t' -f2)
else
# Self-hosted database.
dbsize=$("$MYSQL" -N --batch $my_connect --execute="$SQLVERB" 2>/dev/null | \
"$CUT" -d$'\t' -f2)
fi
# Some sanitization
dbsize="${dbsize//[[:space:]]/}"
[[ -z "$dbsize" ]] && dbsize=0
[[ ! "$dbsize" =~ ^([[:digit:]])*$ ]] && dbsize=0
# On failure aborts here, except if it had forced.
if [ $dbsize -eq 0 ]; then echo -en "$MSG_FAILSIZE" | "$TEE" -a "$logfile"
if [ "$MYDUMPFORCED" ]; then
echo " - $MSG_FORCED" | "$TEE" -a "$logfile"
else
echo " - $MSG_ABORTED" | "$TEE" -a "$logfile"; exit 1
fi
fi
fi
# We've the database size.
# Checks the storage space available.
# Note, that we'll compare the size of the running database, not the dump!
# TODO: find a better estimate.
#
# It isn't relevant when we'll dump to the STDOUT or the database has no size.
if [ -n "$MYDUMP" -a $dbsize -gt 0 ]; then
# KB units
freespace=$("$DF" --output=avail -k "$("$DIRNAME" "$MYDUMP")" | $TAIL -n1) #"
# It is enough?
if [ $freespace -lt $dbsize ]; then echo -en "$MSG_NOSPACE" | "$TEE" -a "$logfile"
# On failure aborts here, except if it had forced.
if [ "$MYDUMPFORCED" ]; then
echo " - $MSG_FORCED" | "$TEE" -a "$logfile"
else
echo " - $MSG_ABORTED" | "$TEE" -a "$logfile"; exit 1
fi
fi
fi
# We've the space checked.
# Some cleanup.
#
[[ -n "$MYDUMP" && -f "$MYDUMP" ]] && rm "$MYDUMP" >/dev/null
[[ -n "$logfile" && -f "$logfile" ]] && rm "$logfile" >/dev/null
#
# Dumping.
#
if [ -n "$MYDUMP" ]; then
# Dumps into a file (then optionally compresses). Writes a separate log too.
# TODO: pipelined compress - doesn't work with Docker yet(?).
[[ -n "$MYCONTAINER" ]] \
&& "$DOCKER" exec $MYCONTAINER sh -c "$MYSQLDUMP $my_connect $dumpparameters $MYOPTIONS $MYDATABASE" \
>"$MYDUMP" 2>>"$logfile" \
|| "$MYSQLDUMP" $my_connect $dumpparameters $MYOPTIONS $MYDATABASE \
>"$MYDUMP" 2>>"$logfile"
# Optional compression.
[[ -n "$compress" ]] && "$(which gzip)" "$MYDUMP" 2>/dev/null
else
# Dumps to STDOUT without logging.
[[ -n "$MYCONTAINER" ]] \
&& "$DOCKER" exec $MYCONTAINER sh -c "$MYSQLDUMP $my_connect $dumpparameters $MYOPTIONS $MYDATABASE" \
2>/dev/null \
|| "$MYSQLDUMP" $my_connect $dumpparameters $MYOPTIONS $MYDATABASE \
2>>/dev/null
fi
# That's all, Folks! :)