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.
 
 
 

336 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. # 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. echo "=== Need SSH key passphrase. Check Bitwarden for:"
  80. echo "=== borg $(hostname) / read-write SSH key"
  81. export BORG_RSH="ssh -F $SSH/config -o BatchMode=no -i $SSH/id_ecdsa"
  82. shift
  83. else
  84. export BORG_RSH="ssh -F $SSH/config -i $SSH/id_ecdsa_appendonly"
  85. fi
  86. exec "${BORG_BIN}" "\$@"
  87. EOF
  88. chmod +x "$BORG"
  89. if ! "$BORG" -h >/dev/null ; then
  90. error "Can't run the new borg wrapper; does borg work?"
  91. fi
  92. }
  93. print_random_key()
  94. {
  95. dd if=/dev/urandom | tr -dc 'a-zA-Z0-9' | head -c 16
  96. }
  97. generate_keys()
  98. {
  99. PASS_SSH=$(print_random_key)
  100. PASS_REPOKEY=$(print_random_key)
  101. echo "$PASS_REPOKEY" > passphrase
  102. chmod 600 passphrase
  103. }
  104. # Run a command on the remote host over an existing SSH tunnel
  105. run_ssh_command()
  106. {
  107. ssh -o ControlPath="$TMP"/ssh-control use-existing-control-tunnel "$@"
  108. }
  109. # Configure SSH key-based login
  110. configure_ssh()
  111. {
  112. mkdir "$SSH"
  113. # Create keys
  114. log "Creating SSH keys"
  115. ssh-keygen -N "" -t ecdsa \
  116. -C "backup-appendonly@$HOSTID" -f "$SSH/id_ecdsa_appendonly"
  117. ssh-keygen -N "$PASS_SSH" -t ecdsa \
  118. -C "backup@$HOSTID" -f "$SSH/id_ecdsa"
  119. # Create config snippets
  120. log "Creating SSH config and wrapper script"
  121. cat >> "$SSH/config" <<EOF
  122. User $BACKUP_USER
  123. ControlPath none
  124. ServerAliveInterval 120
  125. Compression no
  126. UserKnownHostsFile $SSH/known_hosts
  127. ForwardX11 no
  128. ForwardAgent no
  129. BatchMode yes
  130. IdentitiesOnly yes
  131. EOF
  132. # Connect to backup host, using persistent control socket
  133. log "Connecting to server"
  134. log "Please enter password; look in Bitwarden for: ${BACKUP_USER}@${BACKUP_HOST}"
  135. ssh -F "$SSH/config" -o BatchMode=no -o PubkeyAuthentication=no \
  136. -o ControlMaster=yes -o ControlPath="$TMP/ssh-control" \
  137. -o StrictHostKeyChecking=accept-new \
  138. -f "${BACKUP_USER}@${BACKUP_HOST}" sleep 600
  139. if ! run_ssh_command true >/dev/null 2>&1 </dev/null ; then
  140. error "SSH failed"
  141. fi
  142. log "Connected to ${BACKUP_USER}@${BACKUP_HOST}"
  143. # Since we now have an SSH connection, check that the repo doesn't exist
  144. if run_ssh_command "test -e $BACKUP_REPO" ; then
  145. error "$BACKUP_REPO already exists on the server, bailing out"
  146. fi
  147. # Copy SSH keys to the server's authorized_keys file, removing any
  148. # existing keys with this HOSTID.
  149. log "Setting up SSH keys on remote host"
  150. cmd="borg/borg serve --restrict-to-repository ~/$BACKUP_REPO"
  151. keys=".ssh/authorized_keys"
  152. backup="${keys}.old-$(date +%Y%m%d-%H%M%S)"
  153. run_ssh_command "mkdir -p .ssh; chmod 700 .ssh; touch $keys"
  154. run_ssh_command "mv $keys $backup; sed '/@$HOSTID\$/d' < $backup > $keys"
  155. run_ssh_command "if cmp -s $backup $keys; then rm $backup ; fi"
  156. run_ssh_command "cat >> .ssh/authorized_keys" <<EOF
  157. command="$cmd --append-only",restrict $(cat "$SSH/id_ecdsa_appendonly.pub")
  158. command="borg/notify.sh",restrict $(cat "$SSH/id_ecdsa_appendonly.pub")
  159. command="$cmd",restrict $(cat "$SSH/id_ecdsa.pub")
  160. EOF
  161. # Test that everything worked
  162. log "Testing SSH login with new key"
  163. if ! ssh -F "$SSH/config" -i "$SSH/id_ecdsa_appendonly" -T \
  164. "${BACKUP_USER}@${BACKUP_HOST}" borg --version </dev/null ; then
  165. error "Logging in with a key failed -- is server set up correctly?"
  166. fi
  167. log "Remote connection OK!"
  168. }
  169. # Create the repository on the server
  170. create_repo()
  171. {
  172. log "Creating repo $BACKUP_REPO"
  173. # Create repo
  174. $BORG init --make-parent-dirs --encryption repokey
  175. }
  176. # Export keys as HTML page
  177. export_keys()
  178. {
  179. log "Exporting keys"
  180. $BORG key export --paper '' key.txt
  181. chmod 600 key.txt
  182. cat >>key.txt <<EOF
  183. Repository: ${BORG_REPO}
  184. Passphrase: ${PASS_REPOKEY}
  185. EOF
  186. }
  187. configure_systemd()
  188. {
  189. TIMER=borg-backup.timer
  190. SERVICE=borg-backup.service
  191. TIMER_UNIT=${BORG_DIR}/${TIMER}
  192. SERVICE_UNIT=${BORG_DIR}/${SERVICE}
  193. log "Creating systemd files"
  194. cat > "$TIMER_UNIT" <<EOF
  195. [Unit]
  196. Description=Borg backup to ${BACKUP_HOST}
  197. [Timer]
  198. OnCalendar=*-*-* 01:00:00
  199. RandomizedDelaySec=1800
  200. FixedRandomDelay=true
  201. Persistent=true
  202. [Install]
  203. WantedBy=timers.target
  204. EOF
  205. cat >> "$SERVICE_UNIT" <<EOF
  206. [Unit]
  207. Description=Borg backup to ${BACKUP_HOST}
  208. [Service]
  209. Type=simple
  210. ExecStart=${BORG_DIR}/backup.py
  211. Nice=10
  212. IOSchedulingClass=best-effort
  213. IOSchedulingPriority=6
  214. EOF
  215. log "Setting up systemd"
  216. if (
  217. ln -sfv "${TIMER_UNIT}" /etc/systemd/system &&
  218. ln -sfv "${SERVICE_UNIT}" /etc/systemd/system &&
  219. systemctl --no-ask-password daemon-reload &&
  220. systemctl --no-ask-password enable ${TIMER} &&
  221. systemctl --no-ask-password start ${TIMER}
  222. ); then
  223. log "Backup timer installed:"
  224. systemctl list-timers ${TIMER}
  225. else
  226. warn ""
  227. warn "Systemd setup failed"
  228. warn "Do something like this to configure automatic backups:"
  229. echo " sudo ln -sfv \"${TIMER_UNIT}\" /etc/systemd/system &&"
  230. echo " sudo ln -sfv \"${SERVICE_UNIT}\" /etc/systemd/system &&"
  231. echo " sudo systemctl daemon-reload &&"
  232. echo " sudo systemctl enable ${TIMER} &&"
  233. echo " sudo systemctl start ${TIMER}"
  234. warn ""
  235. fi
  236. }
  237. update_paths()
  238. {
  239. sed -i \
  240. -e "s!\${HOSTNAME}!$(hostname)!g" \
  241. -e "s!\${BORG_DIR}!${BORG_DIR}!g" \
  242. -e "s!\${BACKUP_USER}!${BACKUP_USER}!g" \
  243. -e "s!\${BACKUP_HOST}!${BACKUP_HOST}!g" \
  244. -e "s!\${BACKUP_REPO}!${BACKUP_REPO}!g" \
  245. README.md
  246. sed -i\
  247. -e "1c#!${BORG_DIR}/.venv/bin/python" \
  248. backup.py
  249. }
  250. git_setup()
  251. {
  252. if ! git checkout -b "setup-$(hostname)" ; then
  253. warn "Git setup failed; ignoring"
  254. return
  255. fi
  256. log "Committing local changes to git"
  257. git add README.md borg-backup.service borg-backup.timer borg.sh
  258. git commit -a -m "autocommit after initial setup on $(hostname)"
  259. }
  260. log "Configuration:"
  261. log " Backup server host: ${BACKUP_HOST}"
  262. log " Backup server user: ${BACKUP_USER}"
  263. log " Repository path: ${BACKUP_REPO}"
  264. setup_venv
  265. create_borg_wrapper
  266. generate_keys
  267. configure_ssh
  268. create_repo
  269. export_keys
  270. configure_systemd
  271. update_paths
  272. git_setup
  273. echo
  274. notice "Add these two passwords to Bitwarden:"
  275. notice ""
  276. notice " Name: borg $(hostname)"
  277. notice " Username: read-write ssh key"
  278. notice " Password: $PASS_SSH"
  279. notice ""
  280. notice " Name: borg $(hostname)"
  281. notice " Username: repo key"
  282. notice " Password: $PASS_REPOKEY"
  283. notice ""
  284. notice "Test the backup file list with"
  285. notice " sudo ${BORG_DIR}/backup.py --dry-run"
  286. notice "and make any necessary adjustments to:"
  287. notice " ${BORG_DIR}/config.yaml"
  288. echo
  289. echo "All done"