Compare commits
3 Commits
552e929247
...
6978cfc012
Author | SHA1 | Date | |
---|---|---|---|
6978cfc012 | |||
883f984aef | |||
2dd60aaf28 |
3
.gitignore
vendored
Normal file
3
.gitignore
vendored
Normal file
|
@ -0,0 +1,3 @@
|
|||
*.html
|
||||
|
||||
|
42
Makefile
42
Makefile
|
@ -1,17 +1,35 @@
|
|||
.PHONY: all
|
||||
all: check
|
||||
@echo "Use 'make deploy' to copy to https://psy.jim.sh/borg-setup.sh"
|
||||
all:
|
||||
@echo
|
||||
@echo "For initial setup, run"
|
||||
@echo " sudo ./initial-setup.sh"
|
||||
@echo
|
||||
@echo "Or run borg commands with e.g.:"
|
||||
@echo " ./borg.sh info"
|
||||
@echo " ./borg.sh list"
|
||||
@echo
|
||||
|
||||
.PHONY: check
|
||||
check:
|
||||
shellcheck -f gcc borg-setup.sh
|
||||
.PHONY: ctrl
|
||||
ctrl: test-setup
|
||||
|
||||
.PHONY: test
|
||||
test:
|
||||
.venv:
|
||||
mkdir .venv
|
||||
pipenv install --dev
|
||||
|
||||
.PHONY: test-backup
|
||||
test-backup: .venv
|
||||
.venv/bin/mypy backup.py
|
||||
./backup.py --max-size 1GiB --one-file-system /tmp | grep -a 'bigf'
|
||||
|
||||
.PHONY: test-setup
|
||||
test-setup:
|
||||
shellcheck -f gcc initial-setup.sh
|
||||
rm -rf /tmp/test-borg
|
||||
BORG_DIR=/tmp/test-borg ./borg-setup.sh
|
||||
ls -al /tmp/test-borg
|
||||
mkdir /tmp/test-borg
|
||||
: "normally this would be a git clone, but we want the working tree..."
|
||||
git ls-files -z | tar --null -T - -cf - | tar -C /tmp/test-borg -xvf -
|
||||
/tmp/test-borg/initial-setup.sh
|
||||
|
||||
.PHONY: deploy
|
||||
deploy:
|
||||
scp borg-setup.sh psy:/www/psy
|
||||
.PHONY: clean
|
||||
clean:
|
||||
rm -f README.html
|
||||
|
|
13
Pipfile
Normal file
13
Pipfile
Normal file
|
@ -0,0 +1,13 @@
|
|||
[[source]]
|
||||
url = "https://pypi.python.org/simple"
|
||||
verify_ssl = true
|
||||
name = "pypi"
|
||||
|
||||
[packages]
|
||||
humanfriendly = "*"
|
||||
|
||||
[dev-packages]
|
||||
mypy = "*"
|
||||
|
||||
[requires]
|
||||
python_version = "3"
|
81
Pipfile.lock
generated
Normal file
81
Pipfile.lock
generated
Normal file
|
@ -0,0 +1,81 @@
|
|||
{
|
||||
"_meta": {
|
||||
"hash": {
|
||||
"sha256": "4f504c785e3ed5b203a82a5f40516507f80a01b8d1d0ad5a905f139cafc41a51"
|
||||
},
|
||||
"pipfile-spec": 6,
|
||||
"requires": {
|
||||
"python_version": "3"
|
||||
},
|
||||
"sources": [
|
||||
{
|
||||
"name": "pypi",
|
||||
"url": "https://pypi.python.org/simple",
|
||||
"verify_ssl": true
|
||||
}
|
||||
]
|
||||
},
|
||||
"default": {
|
||||
"humanfriendly": {
|
||||
"hashes": [
|
||||
"sha256:1697e1a8a8f550fd43c2865cd84542fc175a61dcb779b6fee18cf6b6ccba1477",
|
||||
"sha256:6b0b831ce8f15f7300721aa49829fc4e83921a9a301cc7f606be6686a2288ddc"
|
||||
],
|
||||
"index": "pypi",
|
||||
"version": "==10.0"
|
||||
}
|
||||
},
|
||||
"develop": {
|
||||
"mypy": {
|
||||
"hashes": [
|
||||
"sha256:088cd9c7904b4ad80bec811053272986611b84221835e079be5bcad029e79dd9",
|
||||
"sha256:0aadfb2d3935988ec3815952e44058a3100499f5be5b28c34ac9d79f002a4a9a",
|
||||
"sha256:119bed3832d961f3a880787bf621634ba042cb8dc850a7429f643508eeac97b9",
|
||||
"sha256:1a85e280d4d217150ce8cb1a6dddffd14e753a4e0c3cf90baabb32cefa41b59e",
|
||||
"sha256:3c4b8ca36877fc75339253721f69603a9c7fdb5d4d5a95a1a1b899d8b86a4de2",
|
||||
"sha256:3e382b29f8e0ccf19a2df2b29a167591245df90c0b5a2542249873b5c1d78212",
|
||||
"sha256:42c266ced41b65ed40a282c575705325fa7991af370036d3f134518336636f5b",
|
||||
"sha256:53fd2eb27a8ee2892614370896956af2ff61254c275aaee4c230ae771cadd885",
|
||||
"sha256:704098302473cb31a218f1775a873b376b30b4c18229421e9e9dc8916fd16150",
|
||||
"sha256:7df1ead20c81371ccd6091fa3e2878559b5c4d4caadaf1a484cf88d93ca06703",
|
||||
"sha256:866c41f28cee548475f146aa4d39a51cf3b6a84246969f3759cb3e9c742fc072",
|
||||
"sha256:a155d80ea6cee511a3694b108c4494a39f42de11ee4e61e72bc424c490e46457",
|
||||
"sha256:adaeee09bfde366d2c13fe6093a7df5df83c9a2ba98638c7d76b010694db760e",
|
||||
"sha256:b6fb13123aeef4a3abbcfd7e71773ff3ff1526a7d3dc538f3929a49b42be03f0",
|
||||
"sha256:b94e4b785e304a04ea0828759172a15add27088520dc7e49ceade7834275bedb",
|
||||
"sha256:c0df2d30ed496a08de5daed2a9ea807d07c21ae0ab23acf541ab88c24b26ab97",
|
||||
"sha256:c6c2602dffb74867498f86e6129fd52a2770c48b7cd3ece77ada4fa38f94eba8",
|
||||
"sha256:ceb6e0a6e27fb364fb3853389607cf7eb3a126ad335790fa1e14ed02fba50811",
|
||||
"sha256:d9dd839eb0dc1bbe866a288ba3c1afc33a202015d2ad83b31e875b5905a079b6",
|
||||
"sha256:e4dab234478e3bd3ce83bac4193b2ecd9cf94e720ddd95ce69840273bf44f6de",
|
||||
"sha256:ec4e0cd079db280b6bdabdc807047ff3e199f334050db5cbb91ba3e959a67504",
|
||||
"sha256:ecd2c3fe726758037234c93df7e98deb257fd15c24c9180dacf1ef829da5f921",
|
||||
"sha256:ef565033fa5a958e62796867b1df10c40263ea9ded87164d67572834e57a174d"
|
||||
],
|
||||
"index": "pypi",
|
||||
"version": "==0.910"
|
||||
},
|
||||
"mypy-extensions": {
|
||||
"hashes": [
|
||||
"sha256:090fedd75945a69ae91ce1303b5824f428daf5a028d2f6ab8a299250a846f15d",
|
||||
"sha256:2d82818f5bb3e369420cb3c4060a7970edba416647068eb4c5343488a6c604a8"
|
||||
],
|
||||
"version": "==0.4.3"
|
||||
},
|
||||
"toml": {
|
||||
"hashes": [
|
||||
"sha256:806143ae5bfb6a3c6e736a764057db0e6a0e05e338b5630894a5f779cabb4f9b",
|
||||
"sha256:b3bda1d108d5dd99f4a20d24d9c348e91c4db7ab1b749200bded2f839ccbe68f"
|
||||
],
|
||||
"version": "==0.10.2"
|
||||
},
|
||||
"typing-extensions": {
|
||||
"hashes": [
|
||||
"sha256:49f75d16ff11f1cd258e1b988ccff82a3ca5570217d7ad8c5f48205dd99a677e",
|
||||
"sha256:d8226d10bc02a29bcc81df19a26e56a9647f8b0a6d4a83924139f4a8b01f17b7",
|
||||
"sha256:f1d25edafde516b146ecd0613dabcc61409817af4766fbbcfb8d1ad4ec441a34"
|
||||
],
|
||||
"version": "==3.10.0.2"
|
||||
}
|
||||
}
|
||||
}
|
119
README.md
119
README.md
|
@ -1,22 +1,103 @@
|
|||
# Design
|
||||
|
||||
- On bucket, we have a separate user account "jim-backups". Password
|
||||
for this account is in bitwarden.
|
||||
|
||||
- Repository keys are repokeys, with passphrases saved on clients
|
||||
and in bitwarden.
|
||||
|
||||
- Each client has two SSH keys: one for append-only operation (no
|
||||
pass) and one for read-write (password in bitwarden)
|
||||
|
||||
- Pruning requires the password and is a manual operation (run `sudo
|
||||
/opt/borg/prune.sh`)
|
||||
|
||||
- Systemd timers start daily backups
|
||||
|
||||
# Usage
|
||||
Initial setup
|
||||
=============
|
||||
|
||||
Run on client:
|
||||
|
||||
wget https://psy.jim.sh/borg-setup.sh
|
||||
sudo ./borg-setup.sh
|
||||
sudo git clone https://git.jim.sh/jim/borg-setup.git /opt/borg
|
||||
sudo /opt/borg/initial-setup.sh
|
||||
|
||||
Customize `/opt/borg/backup.yaml` as desired.
|
||||
|
||||
|
||||
|
||||
Cheat sheet
|
||||
===========
|
||||
|
||||
*The copy of this file left on the client will have the variables
|
||||
in this section filled in automatically*
|
||||
|
||||
### Configuration
|
||||
|
||||
Hostname: ${HOSTNAME}
|
||||
Base directory: ${BORG_DIR}
|
||||
Destination: ${BACKUP_USER}@${BACKUP_HOST}
|
||||
Repository: ${BACKUP_REPO}
|
||||
|
||||
### Commands
|
||||
|
||||
See when next backup is scheduled:
|
||||
|
||||
systemctl list-timers borg-backup.timer
|
||||
|
||||
See progress of most recent backup:
|
||||
|
||||
systemctl status -l -n 99999 borg-backup
|
||||
|
||||
Start backup now:
|
||||
|
||||
sudo systemctl start borg-backup
|
||||
|
||||
Interrupt backup in progress:
|
||||
|
||||
sudo systemctl stop borg-backup
|
||||
|
||||
Show backups and related info:
|
||||
|
||||
sudo ${BORG_DIR}/borg.sh info
|
||||
sudo ${BORG_DIR}/borg.sh list
|
||||
|
||||
Run Borg using the read-write SSH key:
|
||||
|
||||
sudo ${BORG_DIR}/borg.sh --rw list
|
||||
|
||||
Mount and look at files:
|
||||
|
||||
mkdir mnt
|
||||
sudo ${BORG_DIR}/borg.sh mount :: mnt
|
||||
sudo -s # to explore as root
|
||||
sudo umount mnt
|
||||
|
||||
Prune old backups. Only run if sure local system was never compromised,
|
||||
as object deletion could have been queued during append-only operations.
|
||||
Requires SSH key password from bitwarden.
|
||||
|
||||
sudo ${BORG_DIR}/prune.sh
|
||||
|
||||
|
||||
|
||||
Design
|
||||
======
|
||||
|
||||
- On server, we have a separate user account "jim-backups". Password
|
||||
for this account is in bitwarden in the "Backups" folder, under `ssh
|
||||
backup.jim.sh`.
|
||||
|
||||
- Repository keys are repokeys, which get stored on the server, inside
|
||||
the repo. Passphrases are stored:
|
||||
- on clients (in `/opt/borg/passphrase`, for making backups)
|
||||
- in bitwarden (under `borg <hostname>`, user `repo key`)
|
||||
|
||||
- Each client has two SSH keys for connecting to the server:
|
||||
- `/opt/borg/ssh/id_ecdsa_appendonly`
|
||||
- configured on server for append-only operation
|
||||
- used for making backups
|
||||
- no password
|
||||
- `/opt/borg/ssh/id_ecdsa`
|
||||
- configured on server for read-write operation
|
||||
- used for manual recovery, management, pruning
|
||||
- password in bitwarden (under `borg [hostname]`, user `read-write ssh key`)
|
||||
|
||||
- Pruning requires the password and is a manual operation, and should only
|
||||
be run when the client has not been compromised.
|
||||
|
||||
sudo /opt/borg/prune.sh
|
||||
|
||||
- Systemd timers start daily backups:
|
||||
|
||||
/etc/systemd/system/borg-backup.service -> /opt/borg/borg-backup.service
|
||||
/etc/systemd/system/borg-backup.timer -> /opt/borg/borg-backup.timer
|
||||
|
||||
- Backup script `/opt/borg/backup.py` uses configuration in
|
||||
`/opt/borg/backup.yaml` to generate our own list of files, excluding
|
||||
anything that's too large by default. This requires borg 1.2.0b1
|
||||
or newer, which is why the setup scripts download a specific version.
|
||||
|
|
180
backup.py
Executable file
180
backup.py
Executable file
|
@ -0,0 +1,180 @@
|
|||
#!.venv/bin/python
|
||||
|
||||
import os
|
||||
import sys
|
||||
import stat
|
||||
from typing import Optional, Tuple
|
||||
import humanfriendly # type: ignore
|
||||
import wcmatch.glob # type: ignore
|
||||
import re
|
||||
import dataclasses
|
||||
import enum
|
||||
|
||||
class MatchResult(enum.Enum):
|
||||
INCLUDE_IF_SIZE_OK = 0
|
||||
INCLUDE_ALWAYS = 1
|
||||
EXCLUDE_ALWAYS = 2
|
||||
|
||||
@dataclasses.dataclass
|
||||
class PatternRule:
|
||||
re_inc: list[re.Pattern]
|
||||
re_exc: list[re.Pattern]
|
||||
|
||||
def match(self, path: str) -> Tuple[bool, bool]:
|
||||
if "big" in path:
|
||||
print(self, file=sys.stderr)
|
||||
|
||||
for inc in self.re_inc:
|
||||
if inc.match(path):
|
||||
break
|
||||
else:
|
||||
return
|
||||
|
||||
for exc in self.re_exc:
|
||||
if exc.match(path):
|
||||
return False
|
||||
return True
|
||||
|
||||
class Lister:
|
||||
def __init__(self, one_file_system: bool, max_size: bool):
|
||||
self.one_file_system = one_file_system
|
||||
self.max_size = max_size
|
||||
if max_size is None:
|
||||
max_size = float('inf')
|
||||
self.stdout = os.fdopen(sys.stdout.fileno(), "wb", closefd=False)
|
||||
|
||||
# Remember files we've skipped because they were too big, so that
|
||||
# we can warn again at the end.
|
||||
self.skipped_size: set[bytes] = set()
|
||||
|
||||
# Remember errors
|
||||
self.skipped_error: set[bytes] = set()
|
||||
|
||||
def __del__(self):
|
||||
self.stdout.close()
|
||||
|
||||
def out(self, path: bytes):
|
||||
# Use '\0\n' as a separator, so that we can both separate it
|
||||
# cleanly in Borg, and also view it on stdout.
|
||||
self.stdout.write(path + b'\0\n')
|
||||
|
||||
def log(self, letter: str, msg: str):
|
||||
colors = { 'E': 31, 'W': 33, 'I': 36 };
|
||||
if letter in colors:
|
||||
c = colors[letter]
|
||||
else:
|
||||
c = 0
|
||||
sys.stderr.write(f"\033[1;{c}m{letter}:\033[22m {msg}\033[0m\n")
|
||||
|
||||
def scan(self, path: bytes,
|
||||
parent_st: os.stat_result=None,
|
||||
rules: list[PatternRule]=[]):
|
||||
"""If the given path should be backed up, print it. If it's
|
||||
a directory and its contents should be included, recurse."""
|
||||
|
||||
# Copy the path in string form, for logging and pathspec
|
||||
# parsing. Otherwise, we use bytes directly.
|
||||
pathstr = path.decode(errors='backslashreplace')
|
||||
|
||||
try:
|
||||
# See if we match any rules
|
||||
for r in rules:
|
||||
if r.match(pathstr):
|
||||
self.log('I', f"ignore {pathstr}")
|
||||
return
|
||||
|
||||
# Stat the path
|
||||
st = os.lstat(path)
|
||||
is_dir = stat.S_ISDIR(st.st_mode)
|
||||
|
||||
if is_dir:
|
||||
# Skip if it crosses a mount point
|
||||
if self.one_file_system:
|
||||
if parent_st is not None and st.st_dev != parent_st.st_dev:
|
||||
self.log('I', f"skipping {pathstr}: "
|
||||
"on different filesystem")
|
||||
return
|
||||
|
||||
# Add contents of any .nobackup file to our
|
||||
# parser rules
|
||||
child_rules = rules
|
||||
|
||||
try:
|
||||
def prepend_base(regex):
|
||||
if regex[0] != '^':
|
||||
raise Exception(f'bad regex: {regex}')
|
||||
return '^' + os.path.join(pathstr, '') + regex[1:]
|
||||
with open(os.path.join(path, b".nobackup")) as f:
|
||||
rule = PatternRule([], [])
|
||||
for line in f:
|
||||
if line[0] == '#':
|
||||
continue
|
||||
(inc, exc) = wcmatch.glob.translate(
|
||||
[ line.rstrip('\r\n') ],
|
||||
flags=(wcmatch.glob.NEGATE |
|
||||
wcmatch.glob.GLOBSTAR |
|
||||
wcmatch.glob.DOTGLOB |
|
||||
wcmatch.glob.EXTGLOB |
|
||||
wcmatch.glob.BRACE))
|
||||
for x in inc:
|
||||
rule.re_inc.append(re.compile(prepend_base(x)))
|
||||
for x in exc:
|
||||
rule.re_exc.append(re.compile(prepend_base(x)))
|
||||
child_rules.append(rule)
|
||||
except FileNotFoundError:
|
||||
pass
|
||||
|
||||
# Recurse and process each entry
|
||||
with os.scandir(path) as it:
|
||||
for entry in it:
|
||||
self.scan(entry.path, st, child_rules)
|
||||
|
||||
else:
|
||||
# For regular files, ensure they're not too big
|
||||
if stat.S_ISREG(st.st_mode) and st.st_size > self.max_size:
|
||||
def format_size(n):
|
||||
return humanfriendly.format_size(
|
||||
n, keep_width=True, binary=True)
|
||||
a = format_size(st.st_size)
|
||||
b = format_size(self.max_size)
|
||||
self.log('W', f"skipping {pathstr}: "
|
||||
+ f"file size {a} exceeds limit {b}")
|
||||
self.skipped_size.add(path)
|
||||
return
|
||||
|
||||
# Every other filename gets printed; devices, symlinks, etc
|
||||
# will get handled by Borg
|
||||
self.out(path)
|
||||
|
||||
except PermissionError as e:
|
||||
self.log('E', f"can't read {pathstr}")
|
||||
self.skipped_error.add(path)
|
||||
return
|
||||
|
||||
def main(argv):
|
||||
import argparse
|
||||
|
||||
def humansize(string):
|
||||
return humanfriendly.parse_size(string)
|
||||
|
||||
parser = argparse.ArgumentParser(
|
||||
prog=argv[0],
|
||||
description="Build up a directory and file list for backups")
|
||||
|
||||
parser.add_argument('-s', '--max-size', type=humansize,
|
||||
help="Ignore files bigger than this, by default")
|
||||
parser.add_argument('-x', '--one-file-system', action='store_true',
|
||||
help="Don't cross mount points when recursing")
|
||||
parser.add_argument('dirs', metavar='DIR', nargs='+',
|
||||
help="Root directories to scan recursively")
|
||||
|
||||
args = parser.parse_args()
|
||||
|
||||
lister = Lister(one_file_system=args.one_file_system,
|
||||
max_size=args.max_size)
|
||||
for p in args.dirs:
|
||||
lister.scan(os.fsencode(p))
|
||||
|
||||
if __name__ == "__main__":
|
||||
import sys
|
||||
main(sys.argv)
|
|
@ -1,10 +1,21 @@
|
|||
#!/bin/bash
|
||||
|
||||
BORG_DIR=${BORG_DIR:-/opt/borg}
|
||||
# These can be overridden when running this script
|
||||
BACKUP_HOST=${BACKUP_HOST:-backup.jim.sh}
|
||||
BACKUP_USER=${BACKUP_USER:-jim-backups}
|
||||
BACKUP_REPO=${BACKUP_REPO:-borg/$(hostname)}
|
||||
|
||||
# Borg binary and hash
|
||||
BORG_URL="https://github.com/borgbackup/borg/releases/download/1.2.0b3/borg-linux64"
|
||||
BORG_SHA256=8dd6c2769d9bf3ca7a65ebf6781302029fc3b15105aff63d33195c007f897360
|
||||
|
||||
# Main dir is where this repo was checked out
|
||||
BORG_DIR="$(realpath "$(dirname "$0")")"
|
||||
|
||||
# This is named with uppercase so that it doesn't tab-complete for
|
||||
# "./b<tab>", which should give us "./borg.sh"
|
||||
BORG_BIN="${BORG_DIR}/Borg.bin"
|
||||
|
||||
# Use stable host ID in case MAC address changes
|
||||
HOSTID="$(hostname -f)@$(python -c 'import uuid;print(uuid.getnode())')"
|
||||
|
||||
|
@ -18,13 +29,13 @@ trap 'error_handler ${BASH_SOURCE} ${LINENO} $?' ERR
|
|||
set -o errexit
|
||||
set -o errtrace
|
||||
|
||||
if [ -e "$BORG_DIR" ]; then
|
||||
echo "Error: BORG_DIR $BORG_DIR already exists; giving up"
|
||||
exit 1
|
||||
if [ -e "$BORG_DIR/.setup-complete" ]; then
|
||||
echo "Error: BORG_DIR $BORG_DIR was already set up; giving up."
|
||||
echo "Use \"git clean\" to return it to original state if desired"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
# Make a temp dir to work in
|
||||
mkdir "$BORG_DIR"
|
||||
TMP=$(mktemp -d --tmpdir="$BORG_DIR")
|
||||
|
||||
# Install some cleanup handlers
|
||||
|
@ -57,18 +68,20 @@ notice() { msg 32 "$@" ; }
|
|||
warn() { msg 31 "$@" ; }
|
||||
error() { msg 31 "Error:" "$@" ; exit 1 ; }
|
||||
|
||||
# Install required packages
|
||||
install_dependencies()
|
||||
# Create pip environment
|
||||
setup_venv()
|
||||
{
|
||||
NEED=
|
||||
check() {
|
||||
command -v "$1" >/dev/null || NEED+=" $2"
|
||||
}
|
||||
check borg borgbackup
|
||||
if [ -n "${NEED:+x}" ]; then
|
||||
log "Need to install packages: $NEED"
|
||||
apt install --no-upgrade $NEED
|
||||
( cd "${BORG_DIR}" && mkdir .venv && pipenv install )
|
||||
}
|
||||
|
||||
# Install borg
|
||||
install_borg()
|
||||
{
|
||||
curl -L --progress-bar -o "${BORG_BIN}" "${BORG_URL}"
|
||||
if ! echo "${BORG_SHA256} ${BORG_BIN}" | sha256sum -c ; then
|
||||
error "hash error"
|
||||
fi
|
||||
chmod +x "${BORG_BIN}"
|
||||
}
|
||||
|
||||
# Create wrapper to execute borg
|
||||
|
@ -87,8 +100,16 @@ export BORG_HOST_ID=${HOSTID}
|
|||
export BORG_BASE_DIR=${BORG_DIR}
|
||||
export BORG_CACHE_DIR=${BORG_DIR}/cache
|
||||
export BORG_CONFIG_DIR=${BORG_DIR}/config
|
||||
export BORG_RSH="ssh -F $SSH/config -i $SSH/id_ecdsa_appendonly"
|
||||
exec borg "\$@"
|
||||
if [ "\$1" = "--rw" ] ; then
|
||||
echo "=== Need SSH key passphrase. Check Bitwarden for:"
|
||||
echo "=== borg $(hostname) / read-write SSH key"
|
||||
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
|
||||
chmod +x "$BORG"
|
||||
if ! "$BORG" -h >/dev/null ; then
|
||||
|
@ -162,7 +183,7 @@ EOF
|
|||
# Copy SSH keys to the server's authorized_keys file, removing any
|
||||
# existing keys with this HOSTID.
|
||||
log "Setting up SSH keys on remote host"
|
||||
cmd="borg serve --restrict-to-repository ~/$BACKUP_REPO"
|
||||
cmd="borg/borg serve --restrict-to-repository ~/$BACKUP_REPO"
|
||||
|
||||
keys=".ssh/authorized_keys"
|
||||
backup="${keys}.old-$(date +%Y%m%d-%H%M%S)"
|
||||
|
@ -204,68 +225,6 @@ Passphrase: ${PASS_REPOKEY}
|
|||
EOF
|
||||
}
|
||||
|
||||
# Create helper scripts to backup, prune, and mount
|
||||
create_scripts()
|
||||
{
|
||||
cat > "${BORG_DIR}/backup.sh" <<EOF
|
||||
#!/bin/bash
|
||||
|
||||
BORG=$BORG_DIR/borg.sh
|
||||
set -e
|
||||
|
||||
# Explicitly list a bunch of directories to back up, in case they come
|
||||
# from different filesystems. If not, duplicates have no effect.
|
||||
DIRS="/"
|
||||
for DIR in /usr /var /home /boot /efi ; do
|
||||
if [ -e "\$DIR" ] ; then
|
||||
DIRS="\$DIRS \$DIR"
|
||||
fi
|
||||
done
|
||||
|
||||
# Allow dirs to be overridden
|
||||
BORG_BACKUP_DIRS=\${BORG_BACKUP_DIRS:-\$DIRS}
|
||||
|
||||
echo "Backing up: \$BORG_BACKUP_DIRS"
|
||||
|
||||
\$BORG create \\
|
||||
--verbose \\
|
||||
--list \\
|
||||
--filter E \\
|
||||
--stats \\
|
||||
--exclude-caches \\
|
||||
--one-file-system \\
|
||||
--checkpoint-interval 900 \\
|
||||
--compression zstd,3 \\
|
||||
::'{hostname}-{now:%Y%m%d-%H%M%S}' \\
|
||||
\$BORG_BACKUP_DIRS
|
||||
|
||||
\$BORG check \\
|
||||
--verbose \\
|
||||
--last 10
|
||||
EOF
|
||||
|
||||
cat > "${BORG_DIR}/prune.sh" <<EOF
|
||||
#!/bin/bash
|
||||
|
||||
BORG=$BORG_DIR/borg.sh
|
||||
set -e
|
||||
|
||||
echo "=== Need SSH key passphrase. Check Bitwarden for:"
|
||||
echo "=== borg $(hostname) / read-write SSH key"
|
||||
\$BORG prune \\
|
||||
--rsh="ssh -F $SSH/config -o BatchMode=no -i $SSH/id_ecdsa" \\
|
||||
--verbose \\
|
||||
--stats \\
|
||||
--keep-within=7d \\
|
||||
--keep-daily=14 \\
|
||||
--keep-weekly=8 \\
|
||||
--keep-monthly=-1
|
||||
EOF
|
||||
|
||||
chmod 755 "${BORG_DIR}/backup.sh"
|
||||
chmod 755 "${BORG_DIR}/prune.sh"
|
||||
}
|
||||
|
||||
configure_systemd()
|
||||
{
|
||||
TIMER=borg-backup.timer
|
||||
|
@ -295,7 +254,7 @@ Description=Borg backup to ${BACKUP_HOST}
|
|||
|
||||
[Service]
|
||||
Type=simple
|
||||
ExecStart=${BORG_DIR}/backup.sh
|
||||
ExecStart=${BORG_DIR}/backup.py
|
||||
Nice=10
|
||||
IOSchedulingClass=best-effort
|
||||
IOSchedulingPriority=6
|
||||
|
@ -324,52 +283,15 @@ EOF
|
|||
fi
|
||||
}
|
||||
|
||||
make_readme()
|
||||
update_readme()
|
||||
{
|
||||
cat > "${BORG_DIR}/README" <<EOF
|
||||
Backup Configuration
|
||||
--------------------
|
||||
|
||||
Hostname: $(hostname)
|
||||
Destination: ${BACKUP_USER}@${BACKUP_HOST}
|
||||
Repository: ${BACKUP_REPO}
|
||||
|
||||
Cheat sheet
|
||||
-----------
|
||||
|
||||
See when next backup is scheduled:
|
||||
|
||||
systemctl list-timers borg-backup.timer
|
||||
|
||||
See progress of most recent backup:
|
||||
|
||||
systemctl status -l -n 99999 borg-backup
|
||||
|
||||
Start backup now:
|
||||
|
||||
sudo systemctl start borg-backup
|
||||
|
||||
Interrupt backup in progress:
|
||||
|
||||
sudo systemctl stop borg-backup
|
||||
|
||||
Show backups and related info:
|
||||
|
||||
sudo ${BORG_DIR}/borg.sh info
|
||||
sudo ${BORG_DIR}/borg.sh list
|
||||
|
||||
Mount and look at files:
|
||||
|
||||
mkdir mnt
|
||||
sudo ${BORG_DIR}/borg.sh mount :: mnt
|
||||
sudo -s # to explore as root
|
||||
sudo umount mnt
|
||||
|
||||
Prune old backups. Only run if sure local system was never compromised,
|
||||
as object deletion could have been queued during append-only operations.
|
||||
Requires SSH key password from bitwarden.
|
||||
sudo ${BORG_DIR}/prune.sh
|
||||
EOF
|
||||
sed -i \
|
||||
-e "s!\${HOSTNAME}!$(hostname)!g" \
|
||||
-e "s!\${BORG_DIR}!${BORG_DIR}!g" \
|
||||
-e "s!\${BACKUP_USER}!${BACKUP_USER}!g" \
|
||||
-e "s!\${BACKUP_HOST}!${BACKUP_HOST}!g" \
|
||||
-e "s!\${BACKUP_REPO}!${BACKUP_REPO}!g" \
|
||||
"${BORG_DIR}/README.md"
|
||||
}
|
||||
|
||||
log "Configuration:"
|
||||
|
@ -377,28 +299,31 @@ log " Backup server host: ${BACKUP_HOST}"
|
|||
log " Backup server user: ${BACKUP_USER}"
|
||||
log " Repository path: ${BACKUP_REPO}"
|
||||
|
||||
install_dependencies
|
||||
setup_venv
|
||||
install_borg
|
||||
create_borg_wrapper
|
||||
generate_keys
|
||||
configure_ssh
|
||||
create_repo
|
||||
export_keys
|
||||
create_scripts
|
||||
configure_systemd
|
||||
make_readme
|
||||
update_readme
|
||||
|
||||
echo
|
||||
notice "Add these two passwords to Bitwarden:"
|
||||
notice ""
|
||||
notice " Name: borg $(hostname)"
|
||||
notice " Username: repo key"
|
||||
notice " Password: $PASS_REPOKEY"
|
||||
notice ""
|
||||
notice " Name: borg $(hostname)"
|
||||
notice " Username: read-write ssh key"
|
||||
notice " Password: $PASS_SSH"
|
||||
notice ""
|
||||
notice "You should also print out the full repo key: ${BORG_DIR}/key.txt"
|
||||
notice " Name: borg $(hostname)"
|
||||
notice " Username: repo key"
|
||||
notice " Password: $PASS_REPOKEY"
|
||||
notice " Notes: (paste the following key)"
|
||||
sed -ne '/BORG/,/^$/{/./p}' "${BORG_DIR}/key.txt"
|
||||
notice ""
|
||||
notice ""
|
||||
|
||||
echo
|
||||
|
||||
echo "All done"
|
Loading…
Reference in New Issue
Block a user