Simplify things by placing the server wrap code into it's own function
in the main script.
Move to predominantly lowercase and local variables.

Introduce usage of separate config files for each server.
master
mj-saunders 2022-11-04 16:55:16 +04:00
parent f648dc627f
commit 41580f9a0d
5 changed files with 226 additions and 171 deletions

2
.gitignore vendored 100644
View File

@ -0,0 +1,2 @@
server_config/*
!server_config/README*

15
README.md 100644
View File

@ -0,0 +1,15 @@
# BorgBackup Reverse
If you find yourself in a situation where your only backup solution is remote
to local, this may provide a semi-sane method to achieve this without having to
store any credentials on your remote machines.
## Description
TODO
## Usage
TODO

287
backup.sh
View File

@ -4,124 +4,179 @@
set -e
[ -n "$DEBUG" ] && set -x
display_usage() {
echo -e "Usage: $0 create|extract repo-name\n"
# NOTE:
# The repo variable is used in a few different ways - it should be the name of:
# 1. an existing borg repository
# 2. a config file in $borg_dir/server_config/
# 3. an entry in your ssh config
main()
{
local borg_old=0
while getopts "cp:h" option; do
case "$option" in
c) borg_old=1
;;
p) BORG_REPO_PATH=$OPTARG
;;
h)
display_usage 0
;;
*)
echo "Invalid option"
display_usage 1
;;
esac
done
shift "$((OPTIND - 1))"
if [ $# -ne 2 ]; then
display_usage 1
fi
if [ `id -u` -eq 0 ]; then
echo "This script must not be run as root!"
display_usage 1
fi
if [ -z ${BORG_REPO_PATH} ]; then
echo "[ERROR] BORG_REPO_PATH is empty or unset."
display_usage 1
fi
local mode=$1
local repo=$2
local borg_dir="${HOME}/.borg"
if [ $mode != "create" ] && [ $mode != "extract" ]; then
display_usage 1
fi
# Server config
# The config file is expected to provide several arrays:
# include, exclude and volatile
# TODO: Move config to more standard location
#local config_file="${borg_dir}/server_config/${repo}"
local config_file="server_config/${repo}"
if [[ ! -f ${config_file} ]]; then
echo "[ERROR] Config file missing; expected ${config_file}"
exit 1
fi
source "${config_file}"
# Make sure the config defined the required arrays
for var in include exclude; do
if [[ -z ${!var} ]]; then
echo "[ERROR] ${repo}: The config requires an '${var}' array"
exit 1
fi
done
# borg-backup requires --exclude to prepend each excluded item
exclude=( "${exclude[@]/#/--exclude }" )
## Logging
local log_dir="${borg_dir}/log"
local log_month_dir=${log_dir}/$(date --utc "+%Y-%m")
local log_file=${log_month_dir}/$(date --utc "+%Y-%m-%d_%H.%M.%SZ")_${repo}.log
if [[ ! -d "${log_month_dir}" ]]; then
echo "== Creating log directory"
mkdir --parent "${log_month_dir}" \
|| { echo "[ERROR] Failed to create log directory. Aborting."; exit 1; }
fi
touch ${log_file} \
|| { echo "[ERROR] Failed to create log file. Aborting"; exit 1; }
echo "== NOTE: To monitor progress, run this from another terminal:"
echo " $ tail -f ${log_file}"
# Temporarilly disallow glob
set -o noglob
local borg_options=(--show-rc --list --stats --one-file-system --exclude-caches)
if [[ -n $DEBUG ]]; then
borg_options+=(--verbose)
fi
if [ -z $BORG_OLD ]; then
borg_options+=(--show-version --exclude-nodump --keep-exclude-tags)
fi
local socket_local="/tmp/borg-local.sock"
local socket_remote="/tmp/borg-remote.sock"
if [ $mode == 'create' ]; then
echo "== CREATING"
# NOTE: "backup-server" is arbitrary and can be anything
# the socat-wrapper will ignore it
server_wrap $socket_local $socket_remote $mode $repo \
borg create \
"${borg_options[@]}" \
"${exclude[@]}" "${volatile[@]}" \
ssh://backup-server/$BORG_REPO_PATH/$repo::{utcnow:%Y-%m-%d} \
"${include[@]}" \
2>&1 | tee ${log_file}
# TODO: Test and fix up extract procedure
else
echo "== EXTRACTING"
local archive=$(borg info $BORG_REPO_PATH/$repo --last 1 | grep 'Archive name' | awk '{print $3}')
server_wrap $socket_local $socket_remote $mode $repo \
borg extract \
--dry-run \
ssh://backup-server/$BORG_REPO_PATH/$repo::$archive \
2>&1 | tee $log_file
fi
# Re-allow glob
set +o noglob
# Tidy up
# NOTE: socket_remote is dealt with in 'server_wrap.sh'
if [ -f $socket_local ]; then
rm $socket_local
fi
}
exit_abnormal () {
display_usage
exit 1
server_wrap()
{
if [ $# -lt 5 ]; then
echo "[ERROR] Missing argument(s)"
exit 1
fi
set -o noglob
local socket_local="$1"
local socket_remote="$2"
local mode="$3"
local repo="$4"
local borg_command="${@:5}; rm $socket_remote"
local remote_socat_command="'bash -c \"exec socat STDIO UNIX-CONNECT:$socket_remote\"'"
# Make command more robust against premature expansion
borg_command=`echo $borg_command | sed "s/--exclude\s\(\S\+\)/--exclude \'\1\'/g"`
# TODO: Handle extract
#if [ $mode == "extract" ]; then
# SH_CMD="cd /mnt"
#fi
exec socat UNIX-LISTEN:"$socket_local" \
"EXEC:borg serve --append-only --restrict-to-path $BORG_REPO_PATH --umask 077" &
ssh -t -R "$socket_remote":"$socket_local" $repo \
sudo BORG_RSH="$remote_socat_command" "$borg_command"
set +o noglob
}
if [ $# -ne 2 ]; then
exit_abnormal
fi
if [ `id -u` -eq 0 ]; then
echo "This script must not be run as root!"
exit_abnormal
fi
display_usage()
{
echo "Usage: $(basename $0) [-ch] [-p path] create|extract repo-name"
echo " -c Use flags compatible with borg < (version tbd)"
echo " -p Repository path. Overrides BORG_REPO_PATH"
exit $1
}
#user_name=borg
#if [ "$(id --user --name)" != "$bbbs_user_name" ]; then
# echo "$0 must be run as $bbbs_user_name"
# exit
#fi
while getopts "h?" option; do
case "$option" in
h|\?)
display_usage
exit 0
;;
esac
done
MODE=$1
REPO=$2
if [ $MODE != "create" ] && [ $MODE != "extract" ]; then
echo "Mode must be either 'create' or 'extract'"
exit_abnormal
fi
log_base_dir="${HOME}/.borg/logs"
month_dir=$(date --utc "+%Y-%m")
log_dir=${log_base_dir}/${month_dir}
log_file=${log_dir}/$(date --utc "+%Y-%m-%d_%H.%M.%SZ")_${REPO}.log
mkdir --parent "${log_dir}"
echo "To monitor progress, run this in another terminal:"
echo "$ tail -f ${log_file}"
# Temporarilly disallow glob
set -o noglob
BORG_OPTIONS="--show-rc --list --stats --one-file-system --exclude-caches"
if [ -z $BORG_OLD ]; then
BORG_OPTIONS+=" --show-version --exclude-nodump --keep-exclude-tags"
fi
[ -n $DEBUG ] && BORG_OPTIONS+=" --verbose"
COMMON_TARGET="/ /boot /etc /root /home /opt /srv /var /var/log /usr"
COMMON_EXCLUDE="--exclude /sys/ --exclude /proc/ --exclude /dev/ --exclude /run/ --exclude /var/run/ --exclude /var/lock --exclude /mnt/"
VOLATILE_EXCLUDE="--exclude /tmp/ --exclude /var/tmp/ --exclude /lost+found --exclude /var/cache/ --exclude /root/.cache --exclude /home/*/.cache"
LXD_BASE="/var/lib/lxd/*/rootfs"
LXD_EXCLUDE="--exclude $LXD_BASE/lost+found --exclude $LXD_BASE/media/* --exclude $LXD_BASE/mnt/* \
--exclude $LXD_BASE/proc/* --exclude $LXD_BASE/run/* --exclude $LXD_BASE/sys/* \
--exclude $LXD_BASE/tmp/*"
LXC_BASE="/home/*/.local/share/lxc/*/rootfs"
LXC_EXCLUDE="--exclude $LXC_BASE/lost+found --exclude $LXC_BASE/media/* --exclude $LXC_BASE/mnt/* \
--exclude $LXC_BASE/proc/* --exclude $LXC_BASE/run/* --exclude $LXC_BASE/sys/* \
--exclude $LXC_BASE/tmp/*"
# Especially when not using --one-file-system
# --exclude /var/run # -> /run
SOCK_LOCAL="/tmp/borg-local.sock"
SOCK_REMOTE="/tmp/borg-remote.sock"
if [ $MODE == 'create' ]; then
echo "=== CREATING"
. ./server-wrap.sh -s $SOCK_LOCAL $SOCK_REMOTE $MODE $REPO \
borg create \
"$BORG_OPTIONS" \
"$COMMON_EXCLUDE" "$VOLATILE_EXCLUDE" \
"$LXD_EXCLUDE" "$LXC_EXCLUDE" \
ssh://backup-server/$BORG_REPO_PATH/$REPO::{utcnow:%Y-%m-%d} \
"$COMMON_TARGET" \
2>&1 | tee ${log_file}
# Tidy up Client .sock - dealt with in 'server-wrap.sh'
# . Chance are, on error, the .sock file will not be cleaned up properly
# Current setup requires no specific files on remote machine
# Could perhaps require one small script that just handles any necessary cleanup
# --exclude /var/lib/lxd/ \
# --exclude /var/lib/vz/images/ \
# TODO: Test and fix up extract procedure
else
echo "=== EXTRACTING"
ARCHIVE=$(borg info $BORG_REPO_PATH/$REPO --last 1 | grep 'Archive name' | awk '{print $3}')
. ./server-wrap.sh -s $SOCK_LOCAL $SOCK_REMOTE $MODE $REPO \
borg extract \
--dry-run \
ssh://backup-server/$BORG_REPO_PATH/$REPO::$ARCHIVE \
2>&1 | tee $log_file
fi
# Re-allow glob
set +o noglob
# Tidy up - if necessary
if [ -f $SOCK_LOCAL ]; then
rm $SOCK_LOCAL
fi
main "$@"

View File

@ -1,55 +0,0 @@
#!/bin/bash
if [ -z ${BORG_REPO_PATH} ]; then
echo "BORG_REPO_PATH is empty or unset. Please set and try again."
exit 1
fi
# Temporarilly disallow glob
set -o noglob
SOCK_SRV="$2"
SOCK_CLI="$3"
CLIENT_SOCAT="'bash -c \"exec socat STDIO UNIX-CONNECT:$SOCK_CLI\"'"
MODE="$4"
REPO="$5"
BORG_CMD="${@:6}"
# Add some cleanup to the command
BORG_CMD+=" && rm $SOCK_CLI"
# Make command more robust from premature expansion
BORG_CMD=`echo $BORG_CMD | sed "s/--exclude\s\(\S\+\)/--exclude \'\1\'/g"`
echo $BORG_CMD
if [ $MODE == "extract" ]; then
SH_CMD="cd /mnt"
fi
#user_name="borg"
#if [ "$(id --user --name)" != "$user_name" -o $# -lt 6 ]; then
if [ $# -lt 6 ]; then
echo "$0 must be run as $user_name"
echo "usage: sudo -u $user_name [env vars] $0 [-s|--socket] path-to/local-listening.sock path-to/remote-connecting.sock path-to/socat-wrapper user@sourcehost <client borg command>"
echo "usage: sudo -u $user_name [env vars] $0 [-t|--tcp] local-listening-port remote-connecting-port path-to/socat-wrapper user@sourcehost <client borg command>"
echo
echo "example: sudo -u $user_name BORGW_RESTRICT_PATH=/path/to/repos $0 -s /tmp/local.sock /tmp/remote.sock /opt/borg/client-wrap"\
"\"backuped-server -p 22\" sudo borg create ssh://backup-server/./my-repo::{hostname}_{utcnow} paths to backup"
echo "example: sudo -u $user_name SSH_ARGS=\"-o ProxyCommand=ssh -W %h:%p gateway-server -p 22\" BORGW_RESTRICT_REPOSITORY=/path/to/repos/repo"\
"$0 -t 12345 12345 /opt/borg/client-wrap backuped-server sudo borg"\
"create ssh://backup-server/./::{hostname}_{utcnow} paths to backup"
echo
echo "Note: \"backup-server\" is arbitrary and can be anything - the socat-wrapper will ignore it"
else
exec socat UNIX-LISTEN:"$SOCK_SRV" \
"EXEC:borg serve --append-only --restrict-to-path $BORG_REPO_PATH --umask 077" &
ssh -t -R "$SOCK_CLI":"$SOCK_SRV" $REPO sudo BORG_RSH="$CLIENT_SOCAT" "$BORG_CMD"
fi
# Re-allow glob
set +o noglob

View File

@ -0,0 +1,38 @@
# Server Configuration
Create a file for each of your remote servers to be backed up.
The contents is used to determine what and what not to back up.
## File Names
Each filename must match an entry in your `~/.ssh/config`.
It will be used to connect to the remote machine.
It must also match the name of the target borg-backup repository.
A minimal example:
```
~/.ssh/config
Host repo-name
HostName <ip-address>
User foo
```
## File Content
We use bash arrays to list `include` and `exclude` paths.
These are then sourced by the backup script and prepared for use with `borg`.
See the `template` file for a more complete example.
```
include=( path/to/include ... )
exclude=(
# Comments should be safe to use
path/to/exclude
...
)
```