|
- #!/bin/bash
-
- # These can be overridden when running this script
- HOSTNAME=${HOSTNAME:-$(hostname)}
- 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}"
-
- BORG_BIN="${BORG_DIR}/bin/borg.$(uname -m)"
-
- # Use stable host ID in case MAC address changes
- HOSTID="${HOSTNAME}@$(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
-
- # Create pip environment
- setup_venv()
- {
- if ! which pipenv >/dev/null 2>&1 ; then
- echo "pipenv not found, try: sudo apt install pipenv"
- exit 1
- 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_REPO}"
- BORG=${BORG_DIR}/borg.sh
- SSH=$BORG_DIR/ssh
-
- cat >"$VARS" <<EOF
- export BACKUP_USER=${BACKUP_USER}
- export BACKUP_HOST=${BACKUP_HOST}
- export BACKUP_REPO=${BACKUP_REPO}
- export HOSTNAME=${HOSTNAME}
- export BORG_REPO=${BORG_REPO}
- export BORG_HOST_ID=${HOSTID}
- export BORG_PASSCOMMAND="cat ${BORG_DIR}/passphrase"
- export BORG_DIR=${BORG_DIR}
- export SSH=${SSH}
- export BORG=${BORG}
- export BORG_BIN=${BORG_BIN}
- EOF
- if ! "$BORG" -h >/dev/null ; then
- error "Can't run the borg wrapper; does borg work?"
- fi
- }
-
- # Update paths in README and backup.py
- update_paths()
- {
- sed -i \
- -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_REPO}!${BACKUP_REPO}!g" \
- README.md
-
- sed -i\
- -e "1c#!${BORG_DIR}/.venv/bin/python" \
- backup.py
- }
-
- # See if we're just supposed to update an existing install
- if [ "$1" == "--update-paths" ] || [ "$1" == "--update" ] ; then
- if [ -e "vars.sh" ]; then
- echo "Updating paths and variables"
- update_paths
- setup_venv
- create_borg_vars
- exit 0
- else
- echo "Can't update, not set up yet"
- exit 1
- fi
- fi
-
- if [ -e "vars.sh" ]; then
- echo "Error: BORG_DIR $BORG_DIR already looks 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 ; }
-
- print_random_key()
- {
- dd if=/dev/urandom | tr -dc 'a-zA-Z0-9' | head -c 16
- }
-
- generate_keys()
- {
- 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 "" -t ecdsa \
- -C "backup-notify@$HOSTID" -f "$SSH/id_ecdsa_notify"
-
- # 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: ssh ${BACKUP_HOST} / ${BACKUP_USER}"
- 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"
- REMOTE_BORG="borg/borg"
- cmd="$REMOTE_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_notify.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}" "$REMOTE_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"
-
- # Choose a time between 1am and 6am based on this hostname
- HASH=$(echo hash of "$HOSTNAME" | sha1sum)
- HOUR=$((0x${HASH:0:8} % 5 + 1))
- MINUTE=$((0x${HASH:8:8} % 6 * 10))
- TIME=$(printf %02d:%02d:00 $HOUR $MINUTE)
-
- log "Backup time is $TIME"
-
- cat > "$TIMER_UNIT" <<EOF
- [Unit]
- Description=Borg backup to ${BACKUP_HOST}
-
- [Timer]
- OnCalendar=*-*-* $TIME
- 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
- Restart=on-failure
- RestartSec=600
- 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 --no-pager ${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
- }
-
- 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 vars.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_vars
- generate_keys
- configure_ssh
- create_repo
- export_keys
- configure_systemd
- update_paths
- git_setup
-
- echo
- 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"
-
- echo
-
- echo "All done"
|