#!/bin/bash # These can be overridden when running this script set_default_variables() { HOSTNAME=${HOSTNAME:-$(hostname)} BACKUP_HOST=${BACKUP_HOST:-backup.jim.sh} BACKUP_PORT=${BACKUP_PORT:-222} BACKUP_USER=${BACKUP_USER:-jim-backups} BACKUP_REPO=${BACKUP_REPO:-borg/${HOSTNAME}} SYSTEMD_UNIT=${SYSTEMD_UNIT:-borg-backup} # Use stable host ID in case MAC address changes. # Note that this host ID is only used to manage locks, so it's # not crucial that it remains stable. UUID=$(python3 -c 'import uuid;print(uuid.getnode())') HOSTID=${BORG_HOST_ID:-"${HOSTNAME}@${UUID}"} log "Configuration:" log " HOSTNAME Local hostname: \033[1m${HOSTNAME}" log " HOSTID Local host ID: \033[1m${HOSTID}" log " BACKUP_USER Backup server user: \033[1m${BACKUP_USER}" log " BACKUP_HOST Backup server host: \033[1m${BACKUP_HOST}" log " BACKUP_PORT Backup server port: \033[1m${BACKUP_PORT}" log " BACKUP_REPO Server repository: \033[1m${BACKUP_REPO}" log " SYSTEMD_UNIT Systemd unit name: \033[1m${SYSTEMD_UNIT}" for i in $(seq 15 -1 1); do printf "\rPress ENTER or wait $i seconds to continue... \b" if read -t 1 ; then break; fi done } # Main dir is where this repo was checked out BORG_DIR="$(realpath "$(dirname "$0")")" cd "${BORG_DIR}" BORG_BIN="${BORG_DIR}/bin/borg.$(uname -m)" function error_handler() { echo "Error at $1 line $2:" echo -n '>>> ' ; tail -n +"$2" < "$1" | head -1 echo "... exited with code $3" exit "$3" } trap 'error_handler ${BASH_SOURCE} ${LINENO} $?' ERR set -o errexit set -o errtrace # Create pip environment setup_venv() { if ! which pipenv >/dev/null 2>&1 ; then error "pipenv not found, try: sudo apt install pipenv" fi mkdir -p .venv pipenv install } # Create shell script with environment variables create_borg_vars() { VARS=${BORG_DIR}/vars.sh # These variables are used elsewhere in this script BORG_REPO="ssh://${BACKUP_USER}@${BACKUP_HOST}:${BACKUP_PORT}/./${BACKUP_REPO}" BORG=${BORG_DIR}/borg.sh SSH=$BORG_DIR/ssh cat >"$VARS" </dev/null ; then error "Can't run the borg wrapper; does borg work?" fi } # Copy templated files, filling in templates as needed install_templated_files() { DOCS="README.md" SCRIPTS="notify.sh logs.sh" for i in ${DOCS} ${SCRIPTS}; do sed -e "s!\${HOSTNAME}!${HOSTNAME}!g" \ -e "s!\${BORG_DIR}!${BORG_DIR}!g" \ -e "s!\${BORG_BIN}!${BORG_BIN}!g" \ -e "s!\${BACKUP_USER}!${BACKUP_USER}!g" \ -e "s!\${BACKUP_HOST}!${BACKUP_HOST}!g" \ -e "s!\${BACKUP_PORT}!${BACKUP_PORT}!g" \ -e "s!\${BACKUP_REPO}!${BACKUP_REPO}!g" \ -e "s!\${SYSTEMD_UNIT}!${SYSTEMD_UNIT}!g" \ templates/$i > $i done chmod +x ${SCRIPTS} } # Update local paths in scripts update_paths() { sed -i\ -e "1c#!${BORG_DIR}/.venv/bin/python" \ backup.py } # See if we're just supposed to update an existing install, or recovering parse_args() { RECOVER=0 UPDATE=0 if [ "$1" == "--recover" ] ; then if [ -e "vars.sh" ]; then error "It looks like this borg was already set up, can only recover from fresh start" fi RECOVER=1 elif [ "$1" == "--update-paths" ] || [ "$1" == "--update" ] ; then if [ ! -e "vars.sh" ]; then error "Can't update, not set up yet" fi UPDATE=1 elif [ -n "$1" ] ; then error "Unknown arg $1" elif [ -e "vars.sh" ]; then warn "Error: BORG_DIR $BORG_DIR already looks set up." warn "Use \"git clean\" to return it to original state if desired". warn "Or specify --update to refresh things from latest git." error "Giving up" fi } # Make a temp dir to work in TMP=$(mktemp -d) # Install some cleanup handlers cleanup() { set +o errexit set +o errtrace trap - ERR ssh -o ControlPath="$TMP"/ssh-control -O exit x >/dev/null 2>&1 rm -rf -- "$TMP" } cleanup_int() { echo cleanup exit 1 } trap cleanup 0 trap cleanup_int 1 2 15 msg() { color="$1" shift echo -ne "\033[1;${color}m===\033[0;${color}m" "$@" echo -e "\033[0m" } log(){ msg 33 "$@" ; } notice() { msg 32 "$@" ; } warn() { msg 31 "$@" ; } error() { msg 31 "Error:" "$@" ; exit 1 ; } print_random_key() { dd if=/dev/urandom | tr -dc 'a-zA-Z0-9' | head -c 16 } generate_keys() { if [ $RECOVER -eq 1 ] ; then notice "Recovering configuration in order to use an existing backup" read -s -p "Repo key for \"borg ${HOSTNAME}\": " PASS_REPOKEY echo read -s -p "Again: " PASS_REPOKEY2 echo if [ -z "$PASS_REPOKEY" ] || [ $PASS_REPOKEY != $PASS_REPOKEY2 ] ; then error "Bad repo key" fi else PASS_REPOKEY=$(print_random_key) fi echo "$PASS_REPOKEY" > passphrase chmod 600 passphrase } # Run a command on the remote host over an existing SSH tunnel run_ssh_command() { ssh -o ControlPath="$TMP"/ssh-control use-existing-control-tunnel "$@" } # Configure SSH key-based login configure_ssh() { mkdir "$SSH" # Create keys log "Creating SSH keys" ssh-keygen -N "" -t ecdsa \ -C "backup-appendonly@$HOSTID" -f "$SSH/id_ecdsa_appendonly" ssh-keygen -N "" -t ecdsa \ -C "backup-notify@$HOSTID" -f "$SSH/id_ecdsa_notify" # Create config snippets log "Creating SSH config and wrapper script" cat >> "$SSH/config" </dev/null 2>&1 $keys" run_ssh_command "if cmp -s $backup $keys; then rm $backup ; fi" run_ssh_command "cat >> .ssh/authorized_keys" <>key.txt < "$TIMER_UNIT" <> "$SERVICE_UNIT" </dev/null notice "Testing borg: if host location changed, say 'y' here" ${BORG_DIR}/borg.sh info notice "Done -- check 'git diff' and verify changes." exit 0 fi set_default_variables setup_venv create_borg_vars generate_keys configure_ssh [ $RECOVER -eq 0 ] && create_repo export_keys configure_systemd install_templated_files update_paths git_setup echo if [ $RECOVER -eq 1 ] ; then notice "You should be set up with borg pointing to the existing repo now." notice "Use commands like these to look at the backup:" notice " sudo ${BORG_DIR}/borg.sh info" notice " sudo ${BORG_DIR}/borg.sh list" notice "You'll want to now restore files like ${BORG_DIR}/config.yaml before enabling systemd timers" else notice "Add this password to Bitwarden:" notice "" notice " Name: borg ${HOSTNAME}" notice " Username: repo key" notice " Password: $PASS_REPOKEY" notice "" notice "Test the backup file list with" notice " sudo ${BORG_DIR}/backup.py --dry-run" notice "and make any necessary adjustments to:" notice " ${BORG_DIR}/config.yaml" fi echo echo "All done" } parse_args "$@" main