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.
 
 
 

330 lines
8.5 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="$cmd",restrict $(cat "$SSH/id_ecdsa.pub")
  169. EOF
  170. # Test that everything worked
  171. log "Testing SSH login with new key"
  172. if ! ssh -F "$SSH/config" -i "$SSH/id_ecdsa_appendonly" -T \
  173. "${BACKUP_USER}@${BACKUP_HOST}" borg --version </dev/null ; then
  174. error "Logging in with a key failed -- is server set up correctly?"
  175. fi
  176. log "Remote connection OK!"
  177. }
  178. # Create the repository on the server
  179. create_repo()
  180. {
  181. log "Creating repo $BACKUP_REPO"
  182. # Create repo
  183. $BORG init --make-parent-dirs --encryption repokey
  184. }
  185. # Export keys as HTML page
  186. export_keys()
  187. {
  188. log "Exporting keys"
  189. $BORG key export --paper '' "${BORG_DIR}/key.txt"
  190. chmod 600 "${BORG_DIR}/key.txt"
  191. cat >>"${BORG_DIR}/key.txt" <<EOF
  192. Repository: ${BORG_REPO}
  193. Passphrase: ${PASS_REPOKEY}
  194. EOF
  195. }
  196. configure_systemd()
  197. {
  198. TIMER=borg-backup.timer
  199. SERVICE=borg-backup.service
  200. TIMER_UNIT=${BORG_DIR}/${TIMER}
  201. SERVICE_UNIT=${BORG_DIR}/${SERVICE}
  202. log "Creating systemd files"
  203. cat > "$TIMER_UNIT" <<EOF
  204. [Unit]
  205. Description=Borg backup to ${BACKUP_HOST}
  206. [Timer]
  207. OnCalendar=*-*-* 01:00:00
  208. RandomizedDelaySec=1800
  209. FixedRandomDelay=true
  210. Persistent=true
  211. [Install]
  212. WantedBy=timers.target
  213. EOF
  214. cat >> "$SERVICE_UNIT" <<EOF
  215. [Unit]
  216. Description=Borg backup to ${BACKUP_HOST}
  217. [Service]
  218. Type=simple
  219. ExecStart=${BORG_DIR}/backup.py
  220. Nice=10
  221. IOSchedulingClass=best-effort
  222. IOSchedulingPriority=6
  223. EOF
  224. log "Setting up systemd"
  225. if (
  226. ln -sfv "${TIMER_UNIT}" /etc/systemd/system &&
  227. ln -sfv "${SERVICE_UNIT}" /etc/systemd/system &&
  228. systemctl --no-ask-password daemon-reload &&
  229. systemctl --no-ask-password enable ${TIMER} &&
  230. systemctl --no-ask-password start ${TIMER}
  231. ); then
  232. log "Backup timer installed:"
  233. systemctl list-timers ${TIMER}
  234. else
  235. warn ""
  236. warn "Systemd setup failed"
  237. warn "Do something like this to configure automatic backups:"
  238. echo " sudo ln -sfv \"${TIMER_UNIT}\" /etc/systemd/system &&"
  239. echo " sudo ln -sfv \"${SERVICE_UNIT}\" /etc/systemd/system &&"
  240. echo " sudo systemctl daemon-reload &&"
  241. echo " sudo systemctl enable ${TIMER} &&"
  242. echo " sudo systemctl start ${TIMER}"
  243. warn ""
  244. fi
  245. }
  246. update_readme()
  247. {
  248. sed -i \
  249. -e "s!\${HOSTNAME}!$(hostname)!g" \
  250. -e "s!\${BORG_DIR}!${BORG_DIR}!g" \
  251. -e "s!\${BACKUP_USER}!${BACKUP_USER}!g" \
  252. -e "s!\${BACKUP_HOST}!${BACKUP_HOST}!g" \
  253. -e "s!\${BACKUP_REPO}!${BACKUP_REPO}!g" \
  254. "${BORG_DIR}/README.md"
  255. }
  256. log "Configuration:"
  257. log " Backup server host: ${BACKUP_HOST}"
  258. log " Backup server user: ${BACKUP_USER}"
  259. log " Repository path: ${BACKUP_REPO}"
  260. setup_venv
  261. install_borg
  262. create_borg_wrapper
  263. generate_keys
  264. configure_ssh
  265. create_repo
  266. export_keys
  267. configure_systemd
  268. update_readme
  269. echo
  270. notice "Add these two passwords to Bitwarden:"
  271. notice ""
  272. notice " Name: borg $(hostname)"
  273. notice " Username: read-write ssh key"
  274. notice " Password: $PASS_SSH"
  275. notice ""
  276. notice " Name: borg $(hostname)"
  277. notice " Username: repo key"
  278. notice " Password: $PASS_REPOKEY"
  279. notice " Notes: (paste the following key)"
  280. sed -ne '/BORG/,/^$/{/./p}' "${BORG_DIR}/key.txt"
  281. notice ""
  282. notice ""
  283. echo
  284. echo "All done"