|
- #!/bin/bash
-
- # These can be overridden when running this script
- BACKUP_HOST=${BACKUP_HOST:-backup.jim.sh}
- BACKUP_USER=${BACKUP_USER:-jim-backups}
- BACKUP_REPO=${BACKUP_REPO:-borg/$(hostname)}
-
- # Main dir is where this repo was checked out
- BORG_DIR="$(realpath "$(dirname "$0")")"
- cd "${BORG_DIR}"
-
- # This is named with uppercase so that it doesn't tab-complete for
- # "./b<tab>", which should give us "./borg.sh"
- BORG_BIN="${BORG_DIR}/Borg.bin"
-
- # Use stable host ID in case MAC address changes
- HOSTID="$(hostname -f)@$(python -c 'import uuid;print(uuid.getnode())')"
-
- 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
-
- if [ -e ".setup-complete" ]; then
- echo "Error: BORG_DIR $BORG_DIR was already set up; giving up."
- echo "Use \"git clean\" to return it to original state if desired"
- exit 1
- 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 ; }
-
- # Create pip environment
- setup_venv()
- {
- mkdir .venv
- pipenv install
- }
-
- # Create wrapper to execute borg
- create_borg_wrapper()
- {
- BORG=${BORG_DIR}/borg.sh
- BORG_REPO="ssh://${BACKUP_USER}@${BACKUP_HOST}/./${BACKUP_REPO}"
- SSH=$BORG_DIR/ssh
-
- cat >"$BORG" <<EOF
- #!/bin/sh
-
- export BORG_REPO=${BORG_REPO}
- export BORG_PASSCOMMAND="cat ${BORG_DIR}/passphrase"
- export BORG_HOST_ID=${HOSTID}
- export BORG_BASE_DIR=${BORG_DIR}
- export BORG_CACHE_DIR=${BORG_DIR}/cache
- export BORG_CONFIG_DIR=${BORG_DIR}/config
- if [ "\$1" = "--rw" ] ; then
- if [ "$BORG_RW_KEY_ADDED" != "1" ] ; then
- echo "=== Need SSH key passphrase. Check Bitwarden for:"
- echo "=== borg $(hostname) / read-write SSH key"
- fi
- export BORG_RSH="ssh -F $SSH/config -o BatchMode=no -i $SSH/id_ecdsa"
- shift
- else
- export BORG_RSH="ssh -F $SSH/config -i $SSH/id_ecdsa_appendonly"
- fi
-
- exec "${BORG_BIN}" "\$@"
- EOF
- chmod +x "$BORG"
- if ! "$BORG" -h >/dev/null ; then
- error "Can't run the new borg wrapper; does borg work?"
- fi
-
- }
-
- print_random_key()
- {
- dd if=/dev/urandom | tr -dc 'a-zA-Z0-9' | head -c 16
- }
-
- generate_keys()
- {
- PASS_SSH=$(print_random_key)
- PASS_REPOKEY=$(print_random_key)
- 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 "$PASS_SSH" -t ecdsa \
- -C "backup@$HOSTID" -f "$SSH/id_ecdsa"
-
- # Create config snippets
- log "Creating SSH config and wrapper script"
- cat >> "$SSH/config" <<EOF
- User $BACKUP_USER
- ControlPath none
- ServerAliveInterval 120
- Compression no
- UserKnownHostsFile $SSH/known_hosts
- ForwardX11 no
- ForwardAgent no
- BatchMode yes
- IdentitiesOnly yes
- EOF
-
- # Connect to backup host, using persistent control socket
- log "Connecting to server"
- log "Please enter password; look in Bitwarden for: ${BACKUP_USER}@${BACKUP_HOST}"
- ssh -F "$SSH/config" -o BatchMode=no -o PubkeyAuthentication=no \
- -o ControlMaster=yes -o ControlPath="$TMP/ssh-control" \
- -o StrictHostKeyChecking=accept-new \
- -f "${BACKUP_USER}@${BACKUP_HOST}" sleep 600
- if ! run_ssh_command true >/dev/null 2>&1 </dev/null ; then
- error "SSH failed"
- fi
- log "Connected to ${BACKUP_USER}@${BACKUP_HOST}"
-
- # Since we now have an SSH connection, check that the repo doesn't exist
- if run_ssh_command "test -e $BACKUP_REPO" ; then
- error "$BACKUP_REPO already exists on the server, bailing out"
- fi
-
- # Copy SSH keys to the server's authorized_keys file, removing any
- # existing keys with this HOSTID.
- log "Setting up SSH keys on remote host"
- cmd="borg/borg serve --restrict-to-repository ~/$BACKUP_REPO"
-
- keys=".ssh/authorized_keys"
- backup="${keys}.old-$(date +%Y%m%d-%H%M%S)"
- run_ssh_command "mkdir -p .ssh; chmod 700 .ssh; touch $keys"
- run_ssh_command "mv $keys $backup; sed '/@$HOSTID\$/d' < $backup > $keys"
- run_ssh_command "if cmp -s $backup $keys; then rm $backup ; fi"
- run_ssh_command "cat >> .ssh/authorized_keys" <<EOF
- command="$cmd --append-only",restrict $(cat "$SSH/id_ecdsa_appendonly.pub")
- command="borg/notify.sh",restrict $(cat "$SSH/id_ecdsa_appendonly.pub")
- command="$cmd",restrict $(cat "$SSH/id_ecdsa.pub")
- EOF
-
- # Test that everything worked
- log "Testing SSH login with new key"
- if ! ssh -F "$SSH/config" -i "$SSH/id_ecdsa_appendonly" -T \
- "${BACKUP_USER}@${BACKUP_HOST}" borg --version </dev/null ; then
- error "Logging in with a key failed -- is server set up correctly?"
- fi
- log "Remote connection OK!"
- }
-
- # Create the repository on the server
- create_repo()
- {
- log "Creating repo $BACKUP_REPO"
- # Create repo
- $BORG init --make-parent-dirs --encryption repokey
- }
-
- # Export keys as HTML page
- export_keys()
- {
- log "Exporting keys"
- $BORG key export --paper '' key.txt
- chmod 600 key.txt
- cat >>key.txt <<EOF
-
- Repository: ${BORG_REPO}
- Passphrase: ${PASS_REPOKEY}
- EOF
- }
-
- configure_systemd()
- {
- TIMER=borg-backup.timer
- SERVICE=borg-backup.service
- TIMER_UNIT=${BORG_DIR}/${TIMER}
- SERVICE_UNIT=${BORG_DIR}/${SERVICE}
-
- log "Creating systemd files"
-
- cat > "$TIMER_UNIT" <<EOF
- [Unit]
- Description=Borg backup to ${BACKUP_HOST}
-
- [Timer]
- OnCalendar=*-*-* 01:00:00
- RandomizedDelaySec=1800
- FixedRandomDelay=true
- Persistent=true
-
- [Install]
- WantedBy=timers.target
- EOF
-
- cat >> "$SERVICE_UNIT" <<EOF
- [Unit]
- Description=Borg backup to ${BACKUP_HOST}
-
- [Service]
- Type=simple
- ExecStart=${BORG_DIR}/backup.py
- Nice=10
- IOSchedulingClass=best-effort
- IOSchedulingPriority=6
- EOF
-
- log "Setting up systemd"
- if (
- ln -sfv "${TIMER_UNIT}" /etc/systemd/system &&
- ln -sfv "${SERVICE_UNIT}" /etc/systemd/system &&
- systemctl --no-ask-password daemon-reload &&
- systemctl --no-ask-password enable ${TIMER} &&
- systemctl --no-ask-password start ${TIMER}
- ); then
- log "Backup timer installed:"
- systemctl list-timers ${TIMER}
- else
- warn ""
- warn "Systemd setup failed"
- warn "Do something like this to configure automatic backups:"
- echo " sudo ln -sfv \"${TIMER_UNIT}\" /etc/systemd/system &&"
- echo " sudo ln -sfv \"${SERVICE_UNIT}\" /etc/systemd/system &&"
- echo " sudo systemctl daemon-reload &&"
- echo " sudo systemctl enable ${TIMER} &&"
- echo " sudo systemctl start ${TIMER}"
- warn ""
- fi
- }
-
- update_paths()
- {
- sed -i \
- -e "s!\${HOSTNAME}!$(hostname)!g" \
- -e "s!\${BORG_DIR}!${BORG_DIR}!g" \
- -e "s!\${BACKUP_USER}!${BACKUP_USER}!g" \
- -e "s!\${BACKUP_HOST}!${BACKUP_HOST}!g" \
- -e "s!\${BACKUP_REPO}!${BACKUP_REPO}!g" \
- README.md
-
- sed -i\
- -e "1c#!${BORG_DIR}/.venv/bin/python" \
- backup.py
- }
-
- git_setup()
- {
- if ! git checkout -b "setup-$(hostname)" ; then
- warn "Git setup failed; ignoring"
- return
- fi
-
- log "Committing local changes to git"
- git add README.md borg-backup.service borg-backup.timer borg.sh
- git commit -a -m "autocommit after initial setup on $(hostname)"
- }
-
- log "Configuration:"
- log " Backup server host: ${BACKUP_HOST}"
- log " Backup server user: ${BACKUP_USER}"
- log " Repository path: ${BACKUP_REPO}"
-
- setup_venv
- create_borg_wrapper
- generate_keys
- configure_ssh
- create_repo
- export_keys
- configure_systemd
- update_paths
- git_setup
-
- echo
- notice "Add these two passwords to Bitwarden:"
- notice ""
- notice " Name: borg $(hostname)"
- notice " Username: read-write ssh key"
- notice " Password: $PASS_SSH"
- 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"
-
- echo
-
- echo "All done"
|