#! /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 # Kovács Zoltán # 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! :)