My backup scripts and tools
You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
 
 
 

344 lines
8.9 KiB

  1. #!/bin/bash
  2. # These can be overridden when running this script
  3. BACKUP_HOST=${BACKUP_HOST:-backup.jim.sh}
  4. BACKUP_USER=${BACKUP_USER:-jim-backups}
  5. BACKUP_REPO=${BACKUP_REPO:-borg/$(hostname)}
  6. # Borg binary and hash
  7. BORG_URL="https://github.com/borgbackup/borg/releases/download/1.2.0b3/borg-linux64"
  8. BORG_SHA256=8dd6c2769d9bf3ca7a65ebf6781302029fc3b15105aff63d33195c007f897360
  9. # Main dir is where this repo was checked out
  10. BORG_DIR="$(realpath "$(dirname "$0")")"
  11. # This is named with uppercase so that it doesn't tab-complete for
  12. # "./b<tab>", which should give us "./borg.sh"
  13. BORG_BIN="${BORG_DIR}/Borg.bin"
  14. # Use stable host ID in case MAC address changes
  15. HOSTID="$(hostname -f)@$(python -c 'import uuid;print(uuid.getnode())')"
  16. function error_handler() {
  17. echo "Error at $1 line $2:"
  18. echo -n '>>> ' ; tail -n +"$2" < "$1" | head -1
  19. echo "... exited with code $3"
  20. exit "$3"
  21. }
  22. trap 'error_handler ${BASH_SOURCE} ${LINENO} $?' ERR
  23. set -o errexit
  24. set -o errtrace
  25. if [ -e "$BORG_DIR/.setup-complete" ]; then
  26. echo "Error: BORG_DIR $BORG_DIR was already set up; giving up."
  27. echo "Use \"git clean\" to return it to original state if desired"
  28. exit 1
  29. fi
  30. # Make a temp dir to work in
  31. TMP=$(mktemp -d --tmpdir="$BORG_DIR")
  32. # Install some cleanup handlers
  33. cleanup()
  34. {
  35. set +o errexit
  36. set +o errtrace
  37. trap - ERR
  38. ssh -o ControlPath="$TMP"/ssh-control -O exit x >/dev/null 2>&1
  39. rm -rf -- "$TMP"
  40. }
  41. cleanup_int()
  42. {
  43. echo
  44. cleanup
  45. exit 1
  46. }
  47. trap cleanup 0
  48. trap cleanup_int 1 2 15
  49. msg()
  50. {
  51. color="$1"
  52. shift
  53. echo -ne "\033[1;${color}m===\033[0;${color}m" "$@"
  54. echo -e "\033[0m"
  55. }
  56. log(){ msg 33 "$@" ; }
  57. notice() { msg 32 "$@" ; }
  58. warn() { msg 31 "$@" ; }
  59. error() { msg 31 "Error:" "$@" ; exit 1 ; }
  60. # Create pip environment
  61. setup_venv()
  62. {
  63. ( cd "${BORG_DIR}" && mkdir .venv && pipenv install )
  64. }
  65. # Install borg
  66. install_borg()
  67. {
  68. curl -L --progress-bar -o "${BORG_BIN}" "${BORG_URL}"
  69. if ! echo "${BORG_SHA256} ${BORG_BIN}" | sha256sum -c ; then
  70. error "hash error"
  71. fi
  72. chmod +x "${BORG_BIN}"
  73. }
  74. # Create wrapper to execute borg
  75. create_borg_wrapper()
  76. {
  77. BORG=${BORG_DIR}/borg.sh
  78. BORG_REPO="ssh://${BACKUP_USER}@${BACKUP_HOST}/./${BACKUP_REPO}"
  79. SSH=$BORG_DIR/ssh
  80. cat >"$BORG" <<EOF
  81. #!/bin/sh
  82. export BORG_REPO=${BORG_REPO}
  83. export BORG_PASSCOMMAND="cat ${BORG_DIR}/passphrase"
  84. export BORG_HOST_ID=${HOSTID}
  85. export BORG_BASE_DIR=${BORG_DIR}
  86. export BORG_CACHE_DIR=${BORG_DIR}/cache
  87. export BORG_CONFIG_DIR=${BORG_DIR}/config
  88. if [ "\$1" = "--rw" ] ; then
  89. echo "=== Need SSH key passphrase. Check Bitwarden for:"
  90. echo "=== borg $(hostname) / read-write SSH key"
  91. export BORG_RSH="ssh -F $SSH/config -o BatchMode=no -i $SSH/id_ecdsa"
  92. shift
  93. else
  94. export BORG_RSH="ssh -F $SSH/config -i $SSH/id_ecdsa_appendonly"
  95. fi
  96. exec "${BORG_BIN}" "\$@"
  97. EOF
  98. chmod +x "$BORG"
  99. if ! "$BORG" -h >/dev/null ; then
  100. error "Can't run the new borg wrapper; does borg work?"
  101. fi
  102. }
  103. print_random_key()
  104. {
  105. dd if=/dev/urandom | tr -dc 'a-zA-Z0-9' | head -c 16
  106. }
  107. generate_keys()
  108. {
  109. PASS_SSH=$(print_random_key)
  110. PASS_REPOKEY=$(print_random_key)
  111. echo "$PASS_REPOKEY" > "${BORG_DIR}/passphrase"
  112. chmod 600 "${BORG_DIR}/passphrase"
  113. }
  114. # Run a command on the remote host over an existing SSH tunnel
  115. run_ssh_command()
  116. {
  117. ssh -o ControlPath="$TMP"/ssh-control use-existing-control-tunnel "$@"
  118. }
  119. # Configure SSH key-based login
  120. configure_ssh()
  121. {
  122. mkdir "$SSH"
  123. # Create keys
  124. log "Creating SSH keys"
  125. ssh-keygen -N "" -t ecdsa \
  126. -C "backup-appendonly@$HOSTID" -f "$SSH/id_ecdsa_appendonly"
  127. ssh-keygen -N "$PASS_SSH" -t ecdsa \
  128. -C "backup@$HOSTID" -f "$SSH/id_ecdsa"
  129. # Create config snippets
  130. log "Creating SSH config and wrapper script"
  131. cat >> "$SSH/config" <<EOF
  132. User $BACKUP_USER
  133. ControlPath none
  134. ServerAliveInterval 120
  135. Compression no
  136. UserKnownHostsFile $SSH/known_hosts
  137. ForwardX11 no
  138. ForwardAgent no
  139. BatchMode yes
  140. IdentitiesOnly yes
  141. EOF
  142. # Connect to backup host, using persistent control socket
  143. log "Connecting to server"
  144. log "Please enter password; look in Bitwarden for: ${BACKUP_USER}@${BACKUP_HOST}"
  145. ssh -F "$SSH/config" -o BatchMode=no -o PubkeyAuthentication=no \
  146. -o ControlMaster=yes -o ControlPath="$TMP/ssh-control" \
  147. -o StrictHostKeyChecking=accept-new \
  148. -f "${BACKUP_USER}@${BACKUP_HOST}" sleep 600
  149. if ! run_ssh_command true >/dev/null 2>&1 </dev/null ; then
  150. error "SSH failed"
  151. fi
  152. log "Connected to ${BACKUP_USER}@${BACKUP_HOST}"
  153. # Since we now have an SSH connection, check that the repo doesn't exist
  154. if run_ssh_command "test -e $BACKUP_REPO" ; then
  155. error "$BACKUP_REPO already exists on the server, bailing out"
  156. fi
  157. # Copy SSH keys to the server's authorized_keys file, removing any
  158. # existing keys with this HOSTID.
  159. log "Setting up SSH keys on remote host"
  160. cmd="borg/borg serve --restrict-to-repository ~/$BACKUP_REPO"
  161. keys=".ssh/authorized_keys"
  162. backup="${keys}.old-$(date +%Y%m%d-%H%M%S)"
  163. run_ssh_command "mkdir -p .ssh; chmod 700 .ssh; touch $keys"
  164. run_ssh_command "mv $keys $backup; sed '/@$HOSTID\$/d' < $backup > $keys"
  165. run_ssh_command "if cmp -s $backup $keys; then rm $backup ; fi"
  166. run_ssh_command "cat >> .ssh/authorized_keys" <<EOF
  167. command="$cmd --append-only",restrict $(cat "$SSH/id_ecdsa_appendonly.pub")
  168. command="borg/notify.sh",restrict $(cat "$SSH/id_ecdsa_appendonly.pub")
  169. command="$cmd",restrict $(cat "$SSH/id_ecdsa.pub")
  170. EOF
  171. # Test that everything worked
  172. log "Testing SSH login with new key"
  173. if ! ssh -F "$SSH/config" -i "$SSH/id_ecdsa_appendonly" -T \
  174. "${BACKUP_USER}@${BACKUP_HOST}" borg --version </dev/null ; then
  175. error "Logging in with a key failed -- is server set up correctly?"
  176. fi
  177. log "Remote connection OK!"
  178. }
  179. # Create the repository on the server
  180. create_repo()
  181. {
  182. log "Creating repo $BACKUP_REPO"
  183. # Create repo
  184. $BORG init --make-parent-dirs --encryption repokey
  185. }
  186. # Export keys as HTML page
  187. export_keys()
  188. {
  189. log "Exporting keys"
  190. $BORG key export --paper '' "${BORG_DIR}/key.txt"
  191. chmod 600 "${BORG_DIR}/key.txt"
  192. cat >>"${BORG_DIR}/key.txt" <<EOF
  193. Repository: ${BORG_REPO}
  194. Passphrase: ${PASS_REPOKEY}
  195. EOF
  196. }
  197. configure_systemd()
  198. {
  199. TIMER=borg-backup.timer
  200. SERVICE=borg-backup.service
  201. TIMER_UNIT=${BORG_DIR}/${TIMER}
  202. SERVICE_UNIT=${BORG_DIR}/${SERVICE}
  203. log "Creating systemd files"
  204. cat > "$TIMER_UNIT" <<EOF
  205. [Unit]
  206. Description=Borg backup to ${BACKUP_HOST}
  207. [Timer]
  208. OnCalendar=*-*-* 01:00:00
  209. RandomizedDelaySec=1800
  210. FixedRandomDelay=true
  211. Persistent=true
  212. [Install]
  213. WantedBy=timers.target
  214. EOF
  215. cat >> "$SERVICE_UNIT" <<EOF
  216. [Unit]
  217. Description=Borg backup to ${BACKUP_HOST}
  218. [Service]
  219. Type=simple
  220. ExecStart=${BORG_DIR}/backup.py
  221. Nice=10
  222. IOSchedulingClass=best-effort
  223. IOSchedulingPriority=6
  224. EOF
  225. log "Setting up systemd"
  226. if (
  227. ln -sfv "${TIMER_UNIT}" /etc/systemd/system &&
  228. ln -sfv "${SERVICE_UNIT}" /etc/systemd/system &&
  229. systemctl --no-ask-password daemon-reload &&
  230. systemctl --no-ask-password enable ${TIMER} &&
  231. systemctl --no-ask-password start ${TIMER}
  232. ); then
  233. log "Backup timer installed:"
  234. systemctl list-timers ${TIMER}
  235. else
  236. warn ""
  237. warn "Systemd setup failed"
  238. warn "Do something like this to configure automatic backups:"
  239. echo " sudo ln -sfv \"${TIMER_UNIT}\" /etc/systemd/system &&"
  240. echo " sudo ln -sfv \"${SERVICE_UNIT}\" /etc/systemd/system &&"
  241. echo " sudo systemctl daemon-reload &&"
  242. echo " sudo systemctl enable ${TIMER} &&"
  243. echo " sudo systemctl start ${TIMER}"
  244. warn ""
  245. fi
  246. }
  247. update_readme()
  248. {
  249. sed -i \
  250. -e "s!\${HOSTNAME}!$(hostname)!g" \
  251. -e "s!\${BORG_DIR}!${BORG_DIR}!g" \
  252. -e "s!\${BACKUP_USER}!${BACKUP_USER}!g" \
  253. -e "s!\${BACKUP_HOST}!${BACKUP_HOST}!g" \
  254. -e "s!\${BACKUP_REPO}!${BACKUP_REPO}!g" \
  255. "${BORG_DIR}/README.md"
  256. }
  257. git_setup()
  258. {
  259. if ! git checkout -b "setup-$(hostname)" ; then
  260. warn "Git setup failed; ignoring"
  261. return
  262. fi
  263. log "Committing local changes to git"
  264. git status
  265. }
  266. log "Configuration:"
  267. log " Backup server host: ${BACKUP_HOST}"
  268. log " Backup server user: ${BACKUP_USER}"
  269. log " Repository path: ${BACKUP_REPO}"
  270. setup_venv
  271. install_borg
  272. create_borg_wrapper
  273. generate_keys
  274. configure_ssh
  275. create_repo
  276. export_keys
  277. configure_systemd
  278. update_readme
  279. git_setup
  280. echo
  281. notice "Add these two passwords to Bitwarden:"
  282. notice ""
  283. notice " Name: borg $(hostname)"
  284. notice " Username: read-write ssh key"
  285. notice " Password: $PASS_SSH"
  286. notice ""
  287. notice " Name: borg $(hostname)"
  288. notice " Username: repo key"
  289. notice " Password: $PASS_REPOKEY"
  290. notice ""
  291. notice "Test the backup file list with"
  292. notice " sudo ${BORG_DIR}/backup.py --dry-run"
  293. notice "and make any necessary adjustments to:"
  294. notice " ${BORG_DIR}/config.yaml"
  295. echo
  296. echo "All done"