Compare commits
	
		
			15 Commits
		
	
	
		
			d2b24e3896
			...
			f70bffed37
		
	
	| Author | SHA1 | Date | |
|---|---|---|---|
| f70bffed37 | |||
| 979dfd892f | |||
| ab6dce0c2c | |||
| aff447c1b6 | |||
| f7e9c3e232 | |||
| d168c5bf54 | |||
| 31d88f9345 | |||
| ccf54b98d7 | |||
| 59ad2b5b4d | |||
| 0c74f1676c | |||
| 5e06ebd822 | |||
| 929a323cf0 | |||
| 86bb72f201 | |||
| 54437456ae | |||
| c7a6d08665 | 
							
								
								
									
										1
									
								
								.gitignore
									
									
									
									
										vendored
									
									
								
							
							
						
						
									
										1
									
								
								.gitignore
									
									
									
									
										vendored
									
									
								
							| @@ -1,3 +1,4 @@ | |||||||
|  | .venv | ||||||
| *.html | *.html | ||||||
| cache/ | cache/ | ||||||
| config/ | config/ | ||||||
|   | |||||||
| @@ -123,6 +123,8 @@ Notes | |||||||
|     pip install -e .[llfuse] |     pip install -e .[llfuse] | ||||||
|     pyinstaller --clean --noconfirm scripts/borg.exe.spec |     pyinstaller --clean --noconfirm scripts/borg.exe.spec | ||||||
|  |  | ||||||
|  | Then see `dist/borg.exe`.  Confirm the version with `dist/borg.exe --version`. | ||||||
|  |  | ||||||
| *Note:* This uses the deprecated `llfuse` instead of the newer `pyfuse3`. | *Note:* This uses the deprecated `llfuse` instead of the newer `pyfuse3`. | ||||||
| `pyfuse3` doesn't work because, at minimum, it pulls in `trio` which | `pyfuse3` doesn't work because, at minimum, it pulls in `trio` which | ||||||
| requires `ssl` which is explicitly excluded by | requires `ssl` which is explicitly excluded by | ||||||
|   | |||||||
							
								
								
									
										122
									
								
								backup.py
									
									
									
									
									
								
							
							
						
						
									
										122
									
								
								backup.py
									
									
									
									
									
								
							| @@ -10,7 +10,9 @@ import re | |||||||
| import sys | import sys | ||||||
| import stat | import stat | ||||||
| import time | import time | ||||||
|  | import select | ||||||
| import pathlib | import pathlib | ||||||
|  | import threading | ||||||
| import subprocess | import subprocess | ||||||
|  |  | ||||||
| import typing | import typing | ||||||
| @@ -105,19 +107,17 @@ class Backup: | |||||||
|         self.dry_run = dry_run |         self.dry_run = dry_run | ||||||
|         self.root_seen: dict[bytes, bool] = {} |         self.root_seen: dict[bytes, bool] = {} | ||||||
|  |  | ||||||
|         # All logged messages, with severity |         # Saved log messages | ||||||
|         self.logs: list[tuple[str, str]] = [] |         self.logs: list[tuple[str, str]] = [] | ||||||
|  |  | ||||||
|     def out(self, path: bytes): |     def out(self, path: bytes): | ||||||
|         self.outfile.write(path + (b'\n' if self.dry_run else b'\0')) |         self.outfile.write(path + (b'\n' if self.dry_run else b'\0')) | ||||||
|  |  | ||||||
|     def log(self, letter: str, msg: str): |     def log(self, letter: str, msg: str, bold: bool=False): | ||||||
|         colors = { 'E': 31, 'W': 33, 'I': 36 }; |         colors = { 'E': 31, 'W': 33, 'I': 36 }; | ||||||
|         if letter in colors: |         c = colors[letter] if letter in colors else 0 | ||||||
|             c = colors[letter] |         b = "" if bold else "\033[22m" | ||||||
|         else: |         sys.stderr.write(f"\033[1;{c}m{letter}:{b} {msg}\033[0m\n") | ||||||
|             c = 0 |  | ||||||
|         sys.stderr.write(f"\033[1;{c}m{letter}:\033[22m {msg}\033[0m\n") |  | ||||||
|         self.logs.append((letter, msg)) |         self.logs.append((letter, msg)) | ||||||
|  |  | ||||||
|     def run(self, outfile: typing.IO[bytes]): |     def run(self, outfile: typing.IO[bytes]): | ||||||
| @@ -132,10 +132,10 @@ class Backup: | |||||||
|                 if not stat.S_ISDIR(st.st_mode): |                 if not stat.S_ISDIR(st.st_mode): | ||||||
|                     raise NotADirectoryError |                     raise NotADirectoryError | ||||||
|             except FileNotFoundError: |             except FileNotFoundError: | ||||||
|                 self.log('W', f"ignoring root, does not exist: {pstr(root)}") |                 self.log('E', f"root does not exist: {pstr(root)}") | ||||||
|                 continue |                 continue | ||||||
|             except NotADirectoryError: |             except NotADirectoryError: | ||||||
|                 self.log('W', f"ignoring root, not a directory: {pstr(root)}") |                 self.log('E', f"root is not a directory: {pstr(root)}") | ||||||
|                 continue |                 continue | ||||||
|  |  | ||||||
|             self.log('I', f"processing root {pstr(root)}") |             self.log('I', f"processing root {pstr(root)}") | ||||||
| @@ -219,8 +219,11 @@ class Backup: | |||||||
|                     for entry in it: |                     for entry in it: | ||||||
|                         self.scan(path=entry.path, parent_st=st) |                         self.scan(path=entry.path, parent_st=st) | ||||||
|  |  | ||||||
|         except PermissionError as e: |         except (FileNotFoundError, | ||||||
|             self.log('E', f"can't read {pstr(path)}") |                 IsADirectoryError, | ||||||
|  |                 NotADirectoryError, | ||||||
|  |                 PermissionError) as e: | ||||||
|  |             self.log('E', f"can't read {pstr(path)}: {str(e)}") | ||||||
|             return |             return | ||||||
|  |  | ||||||
| def main(argv: list[str]): | def main(argv: list[str]): | ||||||
| @@ -229,6 +232,7 @@ def main(argv: list[str]): | |||||||
|     def humansize(string): |     def humansize(string): | ||||||
|         return humanfriendly.parse_size(string) |         return humanfriendly.parse_size(string) | ||||||
|  |  | ||||||
|  |     # Parse args | ||||||
|     parser = argparse.ArgumentParser( |     parser = argparse.ArgumentParser( | ||||||
|         prog=argv[0], |         prog=argv[0], | ||||||
|         description="Back up the local system using borg", |         description="Back up the local system using borg", | ||||||
| @@ -239,6 +243,8 @@ def main(argv: list[str]): | |||||||
|                         help="Config file", default=str(base / "config.yaml")) |                         help="Config file", default=str(base / "config.yaml")) | ||||||
|     parser.add_argument('-b', '--borg', |     parser.add_argument('-b', '--borg', | ||||||
|                         help="Borg command", default=str(base / "borg.sh")) |                         help="Borg command", default=str(base / "borg.sh")) | ||||||
|  |     parser.add_argument('-N', '--notify', | ||||||
|  |                         help="Notify command", default=str(base / "notify.sh")) | ||||||
|     parser.add_argument('-n', '--dry-run', action="store_true", |     parser.add_argument('-n', '--dry-run', action="store_true", | ||||||
|                         help="Just print log output, don't run borg") |                         help="Just print log output, don't run borg") | ||||||
|     parser.add_argument('-d', '--debug', action="store_true", |     parser.add_argument('-d', '--debug', action="store_true", | ||||||
| @@ -247,13 +253,17 @@ def main(argv: list[str]): | |||||||
|     args = parser.parse_args() |     args = parser.parse_args() | ||||||
|     config = Config(args.config) |     config = Config(args.config) | ||||||
|  |  | ||||||
|  |     # Run backup | ||||||
|     backup = Backup(config, args.dry_run) |     backup = Backup(config, args.dry_run) | ||||||
|  |     captured_output: list[bytes] = [] | ||||||
|  |  | ||||||
|     if args.dry_run: |     if args.dry_run: | ||||||
|         if args.debug: |         if args.debug: | ||||||
|             backup.run(sys.stdout.buffer) |             backup.run(sys.stdout.buffer) | ||||||
|         else: |         else: | ||||||
|             with open(os.devnull, "wb") as out: |             with open(os.devnull, "wb") as out: | ||||||
|                 backup.run(out) |                 backup.run(out) | ||||||
|  |         sys.stdout.flush() | ||||||
|     else: |     else: | ||||||
|         borg = subprocess.Popen([args.borg, |         borg = subprocess.Popen([args.borg, | ||||||
|                                  "create", |                                  "create", | ||||||
| @@ -265,13 +275,34 @@ def main(argv: list[str]): | |||||||
|                                  "--compression", "zstd,3", |                                  "--compression", "zstd,3", | ||||||
|                                  "--paths-from-stdin", |                                  "--paths-from-stdin", | ||||||
|                                  "--paths-delimiter", "\\0", |                                  "--paths-delimiter", "\\0", | ||||||
|                                  "::'{hostname}-{now:%Y%m%d-%H%M%S}'"], |                                  "::{hostname}-{now:%Y%m%d-%H%M%S}"], | ||||||
|                                 stdin=subprocess.PIPE) |                                 stdin=subprocess.PIPE, | ||||||
|  |                                 stdout=subprocess.PIPE, | ||||||
|  |                                 stderr=subprocess.STDOUT) | ||||||
|         if borg.stdin is None: |         if borg.stdin is None: | ||||||
|             raise Exception("no pipe") |             raise Exception("no pipe") | ||||||
|  |  | ||||||
|  |         # Use a thread to capture output | ||||||
|  |         def reader_thread(fh): | ||||||
|  |             os.set_blocking(fh.fileno(), False) | ||||||
|  |             while True: | ||||||
|  |                 ready = select.select([fh.fileno()], [], []) | ||||||
|  |                 if not len(ready[0]): | ||||||
|  |                     break | ||||||
|  |                 data = fh.read(8192) | ||||||
|  |                 if not len(data): | ||||||
|  |                     break | ||||||
|  |                 sys.stdout.buffer.write(data) | ||||||
|  |                 sys.stdout.flush() | ||||||
|  |                 captured_output.append(data) | ||||||
|  |             fh.close() | ||||||
|  |         reader = threading.Thread(target=reader_thread, args=(borg.stdout,)) | ||||||
|  |         reader.daemon = True | ||||||
|  |         reader.start() | ||||||
|  |  | ||||||
|         try: |         try: | ||||||
|             # Give borg some time to start, just to clean up stdout |             # Give borg some time to start, just to clean up stdout | ||||||
|             time.sleep(2) |             time.sleep(1) | ||||||
|             backup.run(borg.stdin) |             backup.run(borg.stdin) | ||||||
|         except BrokenPipeError: |         except BrokenPipeError: | ||||||
|             sys.stderr.write(f"broken pipe\n") |             sys.stderr.write(f"broken pipe\n") | ||||||
| @@ -281,14 +312,69 @@ def main(argv: list[str]): | |||||||
|             except BrokenPipeError: |             except BrokenPipeError: | ||||||
|                 pass |                 pass | ||||||
|             borg.wait() |             borg.wait() | ||||||
|  |         reader.join() | ||||||
|         ret = borg.returncode |         ret = borg.returncode | ||||||
|         if ret < 0: |         if ret < 0: | ||||||
|             sys.stderr.write(f"error: process exited with signal {-ret}\n") |             backup.log('E', f"borg exited with signal {-ret}") | ||||||
|             return 1 |  | ||||||
|         elif ret != 0: |         elif ret != 0: | ||||||
|             sys.stderr.write(f"error: process exited with return code {ret}\n") |             backup.log('E', f"borg exited with return code {ret}") | ||||||
|             return ret |  | ||||||
|  |  | ||||||
|  |     # See if we had any errors | ||||||
|  |     warnings = sum(1 for (letter, msg) in backup.logs if letter == 'W') | ||||||
|  |     errors = sum(1 for (letter, msg) in backup.logs if letter == 'E') | ||||||
|  |  | ||||||
|  |     def plural(num: int, word: str) -> str: | ||||||
|  |         suffix = "" if num == 1 else "s" | ||||||
|  |         return f"{num} {word}{suffix}" | ||||||
|  |  | ||||||
|  |     warnmsg = plural(warnings, "warning") if warnings else None | ||||||
|  |     errmsg = plural(errors, "error") if errors else None | ||||||
|  |  | ||||||
|  |     if not warnings and not errors: | ||||||
|  |         backup.log('I', f"backup successful", bold=True) | ||||||
|  |  | ||||||
|  |     else: | ||||||
|  |         if warnmsg: | ||||||
|  |             backup.log('W', f"reported {warnmsg}", bold=True) | ||||||
|  |         if errors: | ||||||
|  |             backup.log('E', f"reported {errmsg}", bold=True) | ||||||
|  |  | ||||||
|  |         # Send a notification of errors | ||||||
|  |         email = backup.config.notify_email | ||||||
|  |         if email and not args.dry_run: | ||||||
|  |             backup.log('I', f"sending error notification to {email}") | ||||||
|  |  | ||||||
|  |             # Show all of our warnings and errors.  Use a ">" prefix | ||||||
|  |             # so warnings and errors get highlighted by the mail reader. | ||||||
|  |             body = [ "Logs from backup.py:" ] | ||||||
|  |             for (letter, msg) in backup.logs: | ||||||
|  |                 if letter == "E" or letter == "W": | ||||||
|  |                     prefix = ">" | ||||||
|  |                 else: | ||||||
|  |                     prefix = " " | ||||||
|  |                 body.append(f"{prefix}{letter}: {msg}") | ||||||
|  |             body_text = "\n".join(body).encode() | ||||||
|  |  | ||||||
|  |             # Followed by borg output | ||||||
|  |             body_text += b"\n\nBorg output:\n" + b"".join(captured_output) | ||||||
|  |  | ||||||
|  |             # Subject summary | ||||||
|  |             if errmsg and warnmsg: | ||||||
|  |                 summary = f"{errmsg}, {warnmsg}" | ||||||
|  |             elif errors: | ||||||
|  |                 summary = errmsg | ||||||
|  |             else: | ||||||
|  |                 summary = warnmsg | ||||||
|  |  | ||||||
|  |             # Call notify.sh | ||||||
|  |             res = subprocess.run([args.notify, summary, email], input=body_text) | ||||||
|  |             if res.returncode != 0: | ||||||
|  |                 backup.log('E', f"failed to send notification") | ||||||
|  |                 errors += 1 | ||||||
|  |  | ||||||
|  |     # Exit with an error code if we had any errors | ||||||
|  |     if errors: | ||||||
|  |         return 1 | ||||||
|     return 0 |     return 0 | ||||||
|  |  | ||||||
| if __name__ == "__main__": | if __name__ == "__main__": | ||||||
|   | |||||||
							
								
								
									
										21
									
								
								borg.sh
									
									
									
									
									
										Executable file
									
								
							
							
						
						
									
										21
									
								
								borg.sh
									
									
									
									
									
										Executable file
									
								
							| @@ -0,0 +1,21 @@ | |||||||
|  | #!/bin/bash | ||||||
|  |  | ||||||
|  | set -e | ||||||
|  | . "$(dirname "$0")"/vars.sh | ||||||
|  |  | ||||||
|  | export BORG_PASSCOMMAND="cat ${BORG_DIR}/passphrase" | ||||||
|  | export BORG_BASE_DIR=${BORG_DIR} | ||||||
|  | export BORG_CACHE_DIR=${BORG_DIR}/cache | ||||||
|  | export BORG_CONFIG_DIR=${BORG_DIR}/config | ||||||
|  | if [ "$1" = "--rw" ] ; then | ||||||
|  |     if [ "$BORG_RW_KEY_ADDED" != "1" ] ; then | ||||||
|  |         echo "=== Need SSH key passphrase.  Check Bitwarden for:" | ||||||
|  |         echo "===   borg $HOSTNAME / read-write SSH key" | ||||||
|  |     fi | ||||||
|  |     export BORG_RSH="ssh -F $SSH/config -o BatchMode=no -o PreferredAuthentication=publickey -i $SSH/id_ecdsa" | ||||||
|  |     shift | ||||||
|  | else | ||||||
|  |     export BORG_RSH="ssh -F $SSH/config -i $SSH/id_ecdsa_appendonly" | ||||||
|  | fi | ||||||
|  |  | ||||||
|  | exec "${BORG_BIN}" "$@" | ||||||
| @@ -72,38 +72,32 @@ setup_venv() | |||||||
|     pipenv install |     pipenv install | ||||||
| } | } | ||||||
|  |  | ||||||
| # Create wrapper to execute borg | # Create shell script with environment variables | ||||||
| create_borg_wrapper() | create_borg_vars() | ||||||
| { | { | ||||||
|     BORG=${BORG_DIR}/borg.sh |     VARS=${BORG_DIR}/vars.sh | ||||||
|  |  | ||||||
|  |     # These variables are used elsewhere in this script | ||||||
|     BORG_REPO="ssh://${BACKUP_USER}@${BACKUP_HOST}/./${BACKUP_REPO}" |     BORG_REPO="ssh://${BACKUP_USER}@${BACKUP_HOST}/./${BACKUP_REPO}" | ||||||
|  |     BORG=${BORG_DIR}/borg.sh | ||||||
|     SSH=$BORG_DIR/ssh |     SSH=$BORG_DIR/ssh | ||||||
|  |  | ||||||
|     cat >"$BORG" <<EOF |     cat >"$VARS" <<EOF | ||||||
| #!/bin/sh | export BACKUP_USER=${BACKUP_USER} | ||||||
|  | export BACKUP_HOST=${BACKUP_HOST} | ||||||
|  | export BACKUP_REPO=${BACKUP_REPO} | ||||||
|  | export HOSTNAME=$(hostname) | ||||||
| export BORG_REPO=${BORG_REPO} | export BORG_REPO=${BORG_REPO} | ||||||
|  | export BORG_HOST_ID=${HOSTID} | ||||||
| export BORG_PASSCOMMAND="cat ${BORG_DIR}/passphrase" | export BORG_PASSCOMMAND="cat ${BORG_DIR}/passphrase" | ||||||
| export BORG_HOST_ID=${HOSTID} | export BORG_HOST_ID=${HOSTID} | ||||||
| export BORG_BASE_DIR=${BORG_DIR} | export BORG_DIR=${BORG_DIR} | ||||||
| export BORG_CACHE_DIR=${BORG_DIR}/cache | export SSH=${SSH} | ||||||
| export BORG_CONFIG_DIR=${BORG_DIR}/config | export BORG=${BORG} | ||||||
| if [ "\$1" = "--rw" ] ; then | export BORG_BIN=${BORG_BIN} | ||||||
|     if [ "$BORG_RW_KEY_ADDED" != "1" ] ; then |  | ||||||
|         echo "=== Need SSH key passphrase.  Check Bitwarden for:" |  | ||||||
|         echo "===   borg $(hostname) / read-write SSH key" |  | ||||||
|     fi |  | ||||||
|     export BORG_RSH="ssh -F $SSH/config -o BatchMode=no -i $SSH/id_ecdsa" |  | ||||||
|     shift |  | ||||||
| else |  | ||||||
|     export BORG_RSH="ssh -F $SSH/config -i $SSH/id_ecdsa_appendonly" |  | ||||||
| fi |  | ||||||
|  |  | ||||||
| exec "${BORG_BIN}" "\$@" |  | ||||||
| EOF | EOF | ||||||
|     chmod +x "$BORG" |  | ||||||
|     if ! "$BORG" -h >/dev/null ; then |     if ! "$BORG" -h >/dev/null ; then | ||||||
|         error "Can't run the new borg wrapper; does borg work?" |         error "Can't run the borg wrapper; does borg work?" | ||||||
|     fi |     fi | ||||||
|  |  | ||||||
| } | } | ||||||
| @@ -136,6 +130,8 @@ configure_ssh() | |||||||
|     log "Creating SSH keys" |     log "Creating SSH keys" | ||||||
|     ssh-keygen -N "" -t ecdsa \ |     ssh-keygen -N "" -t ecdsa \ | ||||||
|                -C "backup-appendonly@$HOSTID" -f "$SSH/id_ecdsa_appendonly" |                -C "backup-appendonly@$HOSTID" -f "$SSH/id_ecdsa_appendonly" | ||||||
|  |     ssh-keygen -N "" -t ecdsa \ | ||||||
|  |                -C "backup-notify@$HOSTID" -f "$SSH/id_ecdsa_notify" | ||||||
|     ssh-keygen -N "$PASS_SSH" -t ecdsa \ |     ssh-keygen -N "$PASS_SSH" -t ecdsa \ | ||||||
|                -C "backup@$HOSTID" -f "$SSH/id_ecdsa" |                -C "backup@$HOSTID" -f "$SSH/id_ecdsa" | ||||||
|  |  | ||||||
| @@ -173,7 +169,8 @@ EOF | |||||||
|     # Copy SSH keys to the server's authorized_keys file, removing any |     # Copy SSH keys to the server's authorized_keys file, removing any | ||||||
|     # existing keys with this HOSTID. |     # existing keys with this HOSTID. | ||||||
|     log "Setting up SSH keys on remote host" |     log "Setting up SSH keys on remote host" | ||||||
|     cmd="borg/borg serve --restrict-to-repository ~/$BACKUP_REPO" |     REMOTE_BORG="borg/borg" | ||||||
|  |     cmd="$REMOTE_BORG serve --restrict-to-repository ~/$BACKUP_REPO" | ||||||
|  |  | ||||||
|     keys=".ssh/authorized_keys" |     keys=".ssh/authorized_keys" | ||||||
|     backup="${keys}.old-$(date +%Y%m%d-%H%M%S)" |     backup="${keys}.old-$(date +%Y%m%d-%H%M%S)" | ||||||
| @@ -182,14 +179,14 @@ EOF | |||||||
|     run_ssh_command "if cmp -s $backup $keys; then rm $backup ; fi" |     run_ssh_command "if cmp -s $backup $keys; then rm $backup ; fi" | ||||||
|     run_ssh_command "cat >> .ssh/authorized_keys" <<EOF |     run_ssh_command "cat >> .ssh/authorized_keys" <<EOF | ||||||
| command="$cmd --append-only",restrict $(cat "$SSH/id_ecdsa_appendonly.pub") | command="$cmd --append-only",restrict $(cat "$SSH/id_ecdsa_appendonly.pub") | ||||||
| command="borg/notify.sh",restrict $(cat "$SSH/id_ecdsa_appendonly.pub") | command="borg/notify.sh",restrict $(cat "$SSH/id_ecdsa_notify.pub") | ||||||
| command="$cmd",restrict $(cat "$SSH/id_ecdsa.pub") | command="$cmd",restrict $(cat "$SSH/id_ecdsa.pub") | ||||||
| EOF | EOF | ||||||
|  |  | ||||||
|     # Test that everything worked |     # Test that everything worked | ||||||
|     log "Testing SSH login with new key" |     log "Testing SSH login with new key" | ||||||
|     if ! ssh -F "$SSH/config" -i "$SSH/id_ecdsa_appendonly" -T \ |     if ! ssh -F "$SSH/config" -i "$SSH/id_ecdsa_appendonly" -T \ | ||||||
|          "${BACKUP_USER}@${BACKUP_HOST}" borg --version </dev/null ; then |          "${BACKUP_USER}@${BACKUP_HOST}" "$REMOTE_BORG" --version </dev/null ; then | ||||||
|         error "Logging in with a key failed -- is server set up correctly?" |         error "Logging in with a key failed -- is server set up correctly?" | ||||||
|     fi |     fi | ||||||
|     log "Remote connection OK!" |     log "Remote connection OK!" | ||||||
| @@ -297,7 +294,7 @@ git_setup() | |||||||
|     fi |     fi | ||||||
|  |  | ||||||
|     log "Committing local changes to git" |     log "Committing local changes to git" | ||||||
|     git add README.md borg-backup.service borg-backup.timer borg.sh |     git add README.md borg-backup.service borg-backup.timer vars.sh | ||||||
|     git commit -a -m "autocommit after initial setup on $(hostname)" |     git commit -a -m "autocommit after initial setup on $(hostname)" | ||||||
| } | } | ||||||
|  |  | ||||||
| @@ -307,7 +304,7 @@ log "  Backup server user: ${BACKUP_USER}" | |||||||
| log "     Repository path: ${BACKUP_REPO}" | log "     Repository path: ${BACKUP_REPO}" | ||||||
|  |  | ||||||
| setup_venv | setup_venv | ||||||
| create_borg_wrapper | create_borg_vars | ||||||
| generate_keys | generate_keys | ||||||
| configure_ssh | configure_ssh | ||||||
| create_repo | create_repo | ||||||
|   | |||||||
							
								
								
									
										26
									
								
								notify.sh
									
									
									
									
									
										Executable file
									
								
							
							
						
						
									
										26
									
								
								notify.sh
									
									
									
									
									
										Executable file
									
								
							| @@ -0,0 +1,26 @@ | |||||||
|  | #!/bin/bash | ||||||
|  |  | ||||||
|  | set -e | ||||||
|  | . "$(dirname "$0")"/vars.sh | ||||||
|  |  | ||||||
|  | # Send notification email using a script on the backup host | ||||||
|  | # First argument is our hostname, second argument is destination; | ||||||
|  | # mail body is provided on stdin. | ||||||
|  |  | ||||||
|  | if tty -s ; then | ||||||
|  |     echo 'Refusing to read mail body from terminal' | ||||||
|  |     exit 1 | ||||||
|  | fi | ||||||
|  |  | ||||||
|  | SUMMARY="$1" | ||||||
|  | EMAIL="$2" | ||||||
|  |  | ||||||
|  | # Remote notify.sh wants subject as first line, not as an argument, | ||||||
|  | # since it's a bit messy to pass complex strings through ssh command | ||||||
|  | # lines. | ||||||
|  | ( echo "backup $HOSTNAME: $SUMMARY" ; cat ) | \ | ||||||
|  |     ssh \ | ||||||
|  | 	-F "$SSH/config" \ | ||||||
|  | 	-i "$SSH/id_ecdsa_notify" \ | ||||||
|  | 	"$BACKUP_USER@$BACKUP_HOST" \ | ||||||
|  | 	borg/notify.sh "$EMAIL" | ||||||
							
								
								
									
										8
									
								
								prune.sh
									
									
									
									
									
								
							
							
						
						
									
										8
									
								
								prune.sh
									
									
									
									
									
								
							| @@ -1,7 +1,7 @@ | |||||||
| #!/bin/bash | #!/bin/bash | ||||||
|  |  | ||||||
| BORG="$(dirname "$0")/borg.sh --rw" |  | ||||||
| set -e | set -e | ||||||
|  | . "$(dirname "$0")"/vars.sh | ||||||
|  |  | ||||||
| if [ "$BORG_RW_KEY_ADDED" != "1" ] ; then | if [ "$BORG_RW_KEY_ADDED" != "1" ] ; then | ||||||
|     echo "Re-executing under a new ssh agent" |     echo "Re-executing under a new ssh agent" | ||||||
| @@ -9,10 +9,10 @@ if [ "$BORG_RW_KEY_ADDED" != "1" ] ; then | |||||||
| fi | fi | ||||||
|  |  | ||||||
| echo "=== Please enter SSH key passphrase.  Check Bitwarden for:" | echo "=== Please enter SSH key passphrase.  Check Bitwarden for:" | ||||||
| echo "===   borg basis / read-write SSH key" | echo "===   borg $HOSTNAME / read-write SSH key" | ||||||
| ssh-add -v "$(realpath "$(dirname "$0")")/ssh/id_ecdsa" | ssh-add -v "$(realpath "$(dirname "$0")")/ssh/id_ecdsa" | ||||||
|  |  | ||||||
| $BORG prune \ | $BORG --rw prune \ | ||||||
|         --verbose \ |         --verbose \ | ||||||
|         --progress \ |         --progress \ | ||||||
|         --stats \ |         --stats \ | ||||||
| @@ -21,6 +21,6 @@ $BORG prune \ | |||||||
|         --keep-weekly=8 \ |         --keep-weekly=8 \ | ||||||
|         --keep-monthly=-1 |         --keep-monthly=-1 | ||||||
|  |  | ||||||
| $BORG compact \ | $BORG --rw compact \ | ||||||
|         --verbose \ |         --verbose \ | ||||||
|         --progress |         --progress | ||||||
|   | |||||||
		Reference in New Issue
	
	Block a user