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.
 
 
 

354 lines
9.0 KiB

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