-
Hahn Axel (hahn) authoredHahn Axel (hahn) authored
localdump.sh 24.33 KiB
#!/bin/bash
# ======================================================================
#
# BACKUP SCRIPTS - CREATE LOCAL DUMPS
#
# ----------------------------------------------------------------------
# ah - Axel Hahn <axel.hahn@iml.unibe.ch>
# ds - Daniel Schueler <daniel.schueler@iml.unibe.ch>
#
# 2016-11-04 ah,ds created
# 2016-11-10 ..... v1.0
# 2017-03-27 ..... added first param for mode backup|restore
# 2018-02-09 ..... fix: restore-selection of target uses default on return only
# 2021-05-18 ..... move supported backup types to plugins/localdump/[service].sh
# 2021-07-13 ..... remove leading ./ in localdump.sh restore
# 2022-02-18 ..... WIP: use class like functions
# 2022-03-17 ..... WIP: add lines with prefix __DB__
# 2022-11-04 ah rename hooks
# 2024-03-14 ah v2.0: use profiles for local and remote databases
# 2024-03-18 ah fix for db detection from file and cli restore
# 2024-10-02 ah reset $rc before calling db plugin
# 2024-12-13 ah set gzip compression from 9 to 4
# ======================================================================
# --- variables:
# ARCHIVE_BASEDIR {string} base directory for db archive (couchdb2 only)
# BACKUP_BASEDIR {string} base directory for db dumps
# BACKUP_DATE {string} string with current timestamp; will be part of filename for backups
# BACKUP_KEEP_DAYS {int} count of days how long to keep db dumps below $BACKUP_BASEDIR
# BACKUP_PLUGINDIR {string} scripts for supported databases; [APP]/plugins/localdump
# BACKUP_SCRIPT {string} script name of db service
# BACKUP_TARGETDIR {string} target directory db dumps of current service
# SERVICENAME {string} name of db service (one of mysql|pgsql|...)
# ----------------------------------------------------------------------
# CONFIG VARS
# ----------------------------------------------------------------------
. $(dirname $0)/vendor/ini.class.sh || exit 1
. $(dirname $0)/vendor/color.class.sh || exit 1
. $(dirname $0)/includes/jobhelper.sh || exit 1
. $(dirname $0)/includes/inc_bash.sh || exit 1
. $(dirname $0)/includes/dbdetect.class.sh || exit 1
if [ ! -r "${JOBFILE}" ]; then
color.echo error "ERROR: missing config file ${JOBFILE}."
exit 1
fi
LOCALDUMP_LOADED=1
ARCHIVE_BASEDIR=
BACKUP_BASEDIR=
BACKUP_PLUGINDIR=
DBD_DEBUG=0
# Cleanup local dumps older N days
typeset -i BACKUP_KEEP_DAYS=0
BACKUP_DATE=
LASTINPUT=
# ----------------------------------------------------------------------
# FUNCTIONS 4 DB-WRAPPER
# ----------------------------------------------------------------------
# helpfer function for SERVICENAME.backup
# it is called after the service specific dump was done.
# param {string} filename of created dump file
function db._compressDumpfile(){
local _outfile=$1
# $myrc is last returncode - set in fetchrc
if [ $myrc -eq 0 ]; then
echo -n "gzip $_outfile ... "
gzip -4 -f "${_outfile}"
fetchrc
else
color.echo error "ERROR occured while dumping - no gzip of $_outfile"
fi
# echo -n "__DB__$SERVICENAME INFO: backup to "
# ls -l "$_outfile"* 2>&1
# echo
}
# ----------------------------------------------------------------------
# FUNCTIONS 4 BACKUP
# ----------------------------------------------------------------------
# ------------------------------------------------------------
# cleanup a backup dir: remove old files and delete empty dirs
function cleanup_backup_target(){
if [ -d "${BACKUP_TARGETDIR}" ]; then
h3 "CLEANUP ${BACKUP_TARGETDIR} older $BACKUP_KEEP_DAYS days ..."
echo find "${BACKUP_TARGETDIR}" -mtime +$BACKUP_KEEP_DAYS -delete -print
color.preset cmd
find "${BACKUP_TARGETDIR}" -mtime +$BACKUP_KEEP_DAYS -delete -print
color.reset
if [ $(find "${BACKUP_TARGETDIR}" -type f | wc -l) -eq 0 ]; then
echo "INFO: the directory is empty - deleting it"
rm -rf "${BACKUP_TARGETDIR}"
fi
fi
}
# ------------------------------------------------------------
# compress a file
# shared function in localdump_*
# param string filename of uncompressed output file
function compress_file(){
echo -n compressing $1 ...
gzip -9 -f "${1}"
fetchrc
}
# ------------------------------------------------------------
# create a backup directory with name of service
# shared function in localdump_*
function create_targetdir(){
mkdir -p "${BACKUP_TARGETDIR}" 2>/dev/null
if [ ! -d "${BACKUP_TARGETDIR}" ]; then
color.echo "error" "FATAL ERROR: directory ${BACKUP_TARGETDIR} was not created"
exit 1
fi
}
# ------------------------------------------------------------
# generate a base filename for backup dump based on on db name
# ... and added timestamp
# param string name of database schema
# --> see listBackupedDBs() and guessDB() - these function must be able to split this
function get_outfile(){
echo $*__${BACKUP_DATE}
}
# ------------------------------------------------------------
# get name of a service script
# param string name of a service
function get_service_script(){
local _service=$1
local _type; _type=$( dbdetect.getType "$_service" )
ls -1 ${BACKUP_PLUGINDIR}/${_type}.sh 2>/dev/null
}
# ------------------------------------------------------------
# get a list of existing database profiles
function get_database_profiles(){
for config in $(dbdetect.getConfigs); do
if dbdetect.exists "$config"; then
echo "$( dbdetect.getProfile $config )"
fi
done
}
# ------------------------------------------------------------
# show directory infos with count of files and used space
# show used space and count of files and dirs
function show_info_backup_target(){
if [ -d "${BACKUP_TARGETDIR}" ]; then
h3 "INFO about backup target ${BACKUP_TARGETDIR}"
echo -n "used space: "
du -hs "${BACKUP_TARGETDIR}"
echo -n "subdirs : "
find "${BACKUP_TARGETDIR}" -type d | wc -l
echo -n "files : "
find "${BACKUP_TARGETDIR}" -type f | wc -l
echo -n "free space: "
df -h "${BACKUP_TARGETDIR}" | tail -1 | awk '{ print $4 }'
echo
fi
}
# ----------------------------------------------------------------------
# FUNCTIONS 4 RESTORE
# ----------------------------------------------------------------------
# ------------------------------------------------------------
# restore: show profiles from that exist backups
# global string BACKUP_BASEDIR base directory of all backups
function listBackupedServices(){
(
test -n "${BACKUP_BASEDIR}" && test -d "${BACKUP_BASEDIR}" \
&& find "${BACKUP_BASEDIR}" -mindepth 1 -maxdepth 1 -type d -exec basename {} \;
test -n "${ARCHIVE_BASEDIR}" && test -d "${ARCHIVE_BASEDIR}" \
&& find "${ARCHIVE_BASEDIR}" -mindepth 1 -maxdepth 1 -type d -exec basename {} \;
) | sort -u
}
# ------------------------------------------------------------
# restore: show databases or dumps of a given database that can be restored
# global string BACKUP_BASEDIR base directory of all backups of selected dbprofile
# param string optional: DB-Name for file filter to select from existing dumps;
function listBackupedDBs(){
local _filter="$1"
if [ -d "${BACKUP_TARGETDIR}" ]; then
if [ -z "$_filter" ]; then
# list all databases
find "${BACKUP_TARGETDIR}" -mindepth 1 -maxdepth 1 -type f -exec basename {} \; \
| sed -e "s#__[0-9][0-9][0-9][0-9][0-9][0-9][0-9][0-9]-[0-9][0-9].*##g" \
-e "s#\.couchdb\.*##g" \
-e "s#\.gz.meta\$##g" \
-e "s#\.gz\$##g" \
| sort -ud \
| sed "s#^\./##g"
else
# list dumps of a database
ls -ltr ${BACKUP_TARGETDIR}/${_filter}*gz | sed "s,${BACKUP_TARGETDIR}/,,g"
fi
else
color.echo error "ERROR: ${BACKUP_TARGETDIR} does not exist - here are no backups to restore."
echo
echo "You can try to restore dumps:"
echo "1) Restore dump files from a backup set"
echo " $(dirname $0)/restore.sh $BACKUP_BASEDIR"
echo "2) Copy restored dumps into $BACKUP_TARGETDIR"
echo "3) Start database restore again"
echo " $(dirname $0)/localdump.sh restore [profile]"
echo
exit 1
fi
}
# ------------------------------------------------------------
# guess name of the database file
# param string filename of db dump; can be full path or not
function guessDB(){
dumpfile=$1
# the metafile is written in sqlite backup to store full path
metafile=${BACKUP_TARGETDIR}/${dumpfile}.meta
if [ -f $metafile ]; then
grep "^/" "$metafile" || grep "^ File: " "$metafile" | cut -c 9-
else
sBasename=$(basename $1)
sDb=$(echo ${sBasename} | sed "s#__[0-9][0-9][0-9][0-9][0-9][0-9][0-9][0-9]-[0-9][0-9].*##g" | sed "s#\.couchdbdump\.gz##g" )
# ^ ^
# timestamp in backup file __/ for couchdb2 restore from archive __/
if [ -z $sDb ]; then
color.echo error "ERROR: db name was not detected from file $1"
exit 1
fi
echo $sDb
fi
}
# ------------------------------------------------------------
# show a selection + a prompt and read the input
# - If the selection is just 1 line it will be returned
# - If the user presses just return the script will exit
# param string selection of items to select from
# param string prompt to show
function showSelectAndInput(){
local _selection="$1"
local _prompt="$2"
local _lines
typeset -i _lines; _lines=$( grep -c "." <<< "$_selection" )
case $_lines in
0)
color.echo error "ERROR: No data found for a selection. Aborting."
echo
exit 1
;;
1)
echo "INFO: No interaction on a single choice. Using"
color.echo "cmd" " $_selection"
LASTINPUT="$_selection"
return 0
;;
*)
echo "$_selection"
color.print input "${_prompt} >"
read -r LASTINPUT
if [ -z "$LASTINPUT" ]; then
echo "No input given. Aborting."
exit 1
fi
;;
esac
}
# ------------------------------------------------------------
# read .meta file (that contains output of stats) and restore last owner and file permissions
# param string filename of db dump
# param string restored database file
function restorePermissions(){
local sMyMeta="${1}.meta"
local sTargetfile="$2"
if [ -f "${sMyMeta}" ]; then
# Access: (0674/-rw-rwxr--) Uid: ( 1000/ axel) Gid: ( 1000/ axel)
# ^ ^ ^
# _sPerm _sUser _sGroup
local _sPerm=$( grep "^Access: (" "${sMyMeta}" | cut -f 2 -d '(' | cut -f 1 -d '/')
if [ -n "$_sPerm" ]; then
local _sUser=$( grep "^Access: (" "${sMyMeta}" | cut -f 3 -d '(' | cut -f 1 -d '/' | tr -d ' ')
local _sGroup=$( grep "^Access: (" "${sMyMeta}" | cut -f 4 -d '(' | cut -f 1 -d '/' | tr -d ' ')
echo -n "Restoring file owner $_sUser:$_sGroup and permissions $_sPerm ... "
chown "$_sUser:$_sGroup" "${sTargetfile}" && chmod "$_sPerm" "${sTargetfile}"
fetchrc
fi
fi
}
# ------------------------------------------------------------
# show help
# ------------------------------------------------------------
function showhelp(){
local _self
_self=$( basename "$0" )
cat <<EOH
LOCALDUMP detects existing local databases and dumps them locally.
It is included in the backup.sh to dump all before a file backup will store
them. It can be started seperately for manual database backups or for restore.
SYNTAX:
$_self [OPTIONS] <operation> <profile [more_profiles]>
OPTIONS:
-h|--help show this help
PARAMETERS:"
operation - one of check|backup|restore; optional parameter
backup dump all databases/ schemes of a given service
check show info only if the service is available
restore import a dump into same or new database
Without a filename it starts an interactive mode
profile - name of database profiles
You get a list of all available services without parameter
Use ALL for bulk command
file - filename of db dump to restore to origin database scheme
EXAMPLES:
$_self backup
$_self backup ALL
Backup all databases of all found services
$_self backup mysql
Backup all Mysql databases.
$_self restore
Start interactive restore of a database of any service.
$_self restore sqlite
Start interactive restore of an sqlite database.
$_self restore <file-to-restore> [<database-name>]
Restore a given dump file to the origin database scheme or
to a new/ other database with the given name.
EOH
}
# ----------------------------------------------------------------------
# INIT
# ----------------------------------------------------------------------
while [[ "$#" -gt 0 ]]; do case $1 in
-h|--help) showhelp; exit 0;;
*) if grep "^-" <<< "$1" >/dev/null ; then
echo; color.echo error "ERROR: Unknown parameter: $1"; echo; showhelp; exit 2
fi
break;
;;
esac; done
mode=""
case "$1" in
backup|check|restore|shell)
mode=$1
shift 1
;;
esac
if [ -z "$mode" ]; then
color.echo error "ERROR: missing parameter for operation."
echo
showhelp
echo
echo "Hint: On this machine working profiles:"
get_database_profiles | nl
echo
exit 1
fi
# ----- init vars
BACKUP_BASEDIR=$(_j_getvar "${JOBFILE}" "dir-localdumps")
# check
if [ -z "$BACKUP_BASEDIR" ]; then
color.echo error "ERROR: missing config for backup target."
echo There must be an entry dir-localdumps in ${JOBFILE}
exit 1
fi
ARCHIVE_BASEDIR=$(_j_getvar "${JOBFILE}" dir-dbarchive)
BACKUP_PLUGINDIR=$(dirname $0)/plugins/localdump
DBD_BASEDIR=$BACKUP_PLUGINDIR/profiles
BACKUP_KEEP_DAYS=$(_j_getvar ${JOBFILE} "keep-days")
if [ $BACKUP_KEEP_DAYS -eq 0 ]; then
BACKUP_KEEP_DAYS=7
fi
BACKUP_DATE=$(/bin/date +%Y%m%d-%H%M)
# ----- checks
# . /usr/local/bin/inc_cronfunctions.sh
j_requireUser "root"
h1 $(date) IML BACKUP :: LOCALDUMP
export SERVICENAME=$1
# BACKUP_TARGETDIR=${BACKUP_BASEDIR}/${SERVICENAME}
# BACKUP_SCRIPT=$( get_service_script ${SERVICENAME} )
case "$mode" in
# ------------------------------------------------------------
check)
DBD_DEBUG=1
for PROFILENAME in $(dbdetect.getConfigs)
do
echo "----- $PROFILENAME"
dbdetect.exists "${PROFILENAME}"
echo
done
# . $BACKUP_SCRIPT $mode
;;
# ------------------------------------------------------------
backup)
if [ "$1" = "ALL" ] || [ -z "$1" ]; then
profiles2run=$(get_database_profiles)
echo "INFO: Calling local backup scripts for all active profiles"
echo "$profiles2run" | nl
else
profiles2run=$*
echo "INFO: I try to dump the profiles you gave as parameter: $profiles2run"
echo
fi
iProfilesFound=$( grep -c . <<< "$profiles2run" )
test "$iProfilesFound" -eq "0" && echo "INFO: No match - no database dumps needed."
typeset -i iProfileCounter=0
# ----- GO
# PROFILENAME mysql_localhost_13306
# SERVICENAME mysql
#
for PROFILENAME in $profiles2run
do
iProfileCounter+=1
if dbdetect.setProfile "${PROFILENAME}"; then
h2 "START PROFILE $iProfileCounter of $iProfilesFound [${PROFILENAME}]"
SERVICENAME=$( dbdetect.getType "$PROFILENAME" )
BACKUP_PARAMS=$( dbdetect.getParams )
BACKUP_TARGETDIR=${BACKUP_BASEDIR}/${PROFILENAME}
ARCHIVE_DIR=${ARCHIVE_BASEDIR}/${PROFILENAME}
BACKUP_SCRIPT=$( get_service_script ${SERVICENAME} )
# ------ set env
# echo "BACKUP_PARAMS = $BACKUP_PARAMS"
# dbdetect.setenv
eval $( dbdetect.setenv )
_j_runHooks "200-before-db-service"
h3 "BACKUP [${PROFILENAME}] -> ${SERVICENAME}"
rcbak=$rc
rc=0
. $BACKUP_SCRIPT $mode
test $rc -gt 0 && j_notify "db ${SERVICENAME}" "$BACKUP_SCRIPT $mode was finished with rc=$rc" $rc
_j_runHooks "230-after-db-service" "$rc"
# ------ unset env
eval $( dbdetect.unssetenv )
# ----- post jobs: cleanup
cleanup_backup_target
show_info_backup_target
rc=$rc+$rcbak
else
echo "SKIP: profile $iProfileCounter of $iProfilesFound '$PROFILENAME' "
# see why it is not active
DBD_DEBUG=1; dbdetect.setProfile "${PROFILENAME}"; echo; DBD_DEBUG=0
fi
# just to have it in the output
dbdetect.validate
done
;;
# ------------------------------------------------------------
restore)
h1 "RESTORE DATABASE"
if ! listBackupedServices | grep -q . ; then
color.echo error "ERROR: No database dump was found in [${BACKUP_BASEDIR}] nor [${ARCHIVE_BASEDIR}]."
exit 1
fi
if [ -z $1 ] || [ ! -f "$1" ]; then
parService="$1"
# ----- interactive selections
h2 "Select profile that has a dump"
if [ -z "${parService}" ]; then
showSelectAndInput "$( listBackupedServices )" "Restore for profile name"
parService="$LASTINPUT"
else
echo "Taken from command line: $parService"
fi
# ----- check if profile exists
if ! dbdetect.setProfile "${parService}"; then
color.echo error "ERROR: profile [${parService}] is not known here (or database service is stopped)."
echo
exit 1
fi
# ----- check if dump exists in archive and in backup
if [ -d "${BACKUP_BASEDIR}/${parService}" ] && [ -d "${ARCHIVE_BASEDIR}/${parService}" ]; then
echo
showSelectAndInput "$(echo "${BACKUP_BASEDIR}"; echo "${ARCHIVE_BASEDIR}")" "Select a source directory"
BACKUP_BASEDIR="$LASTINPUT"
else
# just one test needed because BACKUP_BASEDIR is BACKUP_BASEDIR
test -d "${ARCHIVE_BASEDIR}/${parService}" && BACKUP_BASEDIR="${ARCHIVE_BASEDIR}"
fi
# ----- check if target dir with profile exists
if [ ! -d "${BACKUP_BASEDIR}/${parService}" ]; then
color.echo error "ERROR: Directory does not exist '${BACKUP_BASEDIR}/${parService}'."
exit 1
fi
BACKUP_TARGETDIR="${BACKUP_BASEDIR}/${parService}"
h2 "Select a database schema"
showSelectAndInput "$(listBackupedDBs)" "Name of database to restore"
fileprefix="$LASTINPUT"
echo
h2 "Select a specific dump for that database"
showSelectAndInput "$(listBackupedDBs $fileprefix)" "Backupset to import"
dbfile="$LASTINPUT"
# if there is a single dump in backup folder:
# '-rw-r--r-- 1 root root 481 Mar 13 12:27 ahcrawler__20240313-1227.sql.gz'
# --> take the last part behind the last space to get a filename
echo "$LASTINPUT" | grep -qE "^[rwxsSt\-]{10}" \
&& dbfile="$(rev <<< \'"$LASTINPUT"\' | cut -d ' ' -f 1 | rev | sed "s#'\$##" )"
echo
sTargetDb=$(guessDB ${dbfile})
color.print input "New database name [$sTargetDb] >"
read -r sTargetDb
if [ -z "$sTargetDb" ]; then
sTargetDb=$(guessDB ${dbfile})
fi
echo
sDumpfile="${BACKUP_TARGETDIR}/${dbfile}"
else
sDumpfile=$1
sTargetDb=$2
fi
shift 2
# ----- start restore
if [ ! -f "${sDumpfile}" ]; then
color.echo error "ERROR: [${sDumpfile}] is not a file"
rc=$rc+1
else
# We expect a structure: /somedir/whatever/<PROFILENAME>/<database>.gz
dumpDirname=$( dirname "${sDumpfile}" )
PROFILENAME=$( basename "${dumpDirname}" )
echo "INFO: detected profile: $PROFILENAME"
echo ""
if dbdetect.setProfile "${PROFILENAME}"; then
SERVICENAME=$( dbdetect.getType "$PROFILENAME" )
BACKUP_TARGETDIR=${BACKUP_BASEDIR}/${PROFILENAME}
BACKUP_SCRIPT=$( get_service_script ${SERVICENAME} )
ARCHIVE_DIR=${ARCHIVE_BASEDIR}/${PROFILENAME}
BACKUP_PARAMS=$( dbdetect.getParams )
eval $( dbdetect.setenv )
. $BACKUP_SCRIPT $mode "${sDumpfile}" "${sTargetDb}"
if [ $? -ne 0 -o $rc -ne 0 ]; then
color.echo error "ERROR: $mode failed. See ouput above. :-/"
else
color.echo ok "OK, $mode was successful."
fi
# ------ unset env
eval $( dbdetect.unssetenv )
else
color.echo error "ERROR: Profile $PROFILENAME was detected but its database service is not available."
fi
fi
;;
# ------------------------------------------------------------
# shell)
# export BACKUP_TARGETDIR
# . $BACKUP_SCRIPT
# (
# mycmd=
# echo
# echo "Starting interactive shell..."
# echo
# echo "STATUS: STILL ALPHA as long existing db plugins are not rewritten."
# echo
# echo "INFO: Try ${SERVICENAME}.help to see database specific commands."
# echo "INFO: Type exit and return to leave the shell."
# echo
# while [ ! "$mycmd" = "exit" ]; do
# echo -n "[${SERVICENAME}]"
# color.print input " $( pwd )"
# echo -n " % "
# read -r mycmd
# if [ ! "$mycmd" = "exit" ];then
# color.preset cmd
# eval $mycmd
# color.reset
# fi
# done
# )
# ;;
# ----- start restore
*)
color.echo error "ERROR: unknown command [$mode]"
;;
esac
echo _______________________________________________________________________________
echo STATUS $0 exit with final returncode rc=$rc
exit $rc
# ----------------------------------------------------------------------