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.
 
 
 

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