Compare commits

..

10 commits

Author SHA1 Message Date
Martin Sekera
83bac5876d simplify shapshot naming 2023-01-15 18:53:52 +01:00
Martin Sekera
bbd568260a fix unpack.sh chmod 2021-08-01 10:14:40 +02:00
Martin Sekera
63906d328f fix unpack.sh directory 2021-08-01 09:56:32 +02:00
Martin Sekera
831d9ae9a2 add unpack script to every snapshot 2021-08-01 09:36:19 +02:00
Martin Sekera
8345eb3217 chmod dirs rwx instead of r-x so they can be moved 2021-05-31 12:08:53 +02:00
Martin Sekera
71886bee68 chmod rank2 dir as well 2021-05-30 23:53:47 +02:00
Martin Sekera
7a51d286f2 fix a stupid mistake from the previous commit 2021-05-30 23:46:13 +02:00
Martin Sekera
b96c163c8f chmod rank2 files read-only for owner and no-access for everyone else 2021-05-30 23:40:51 +02:00
Martin Sekera
00e3baf117 add telemetry 2021-02-18 01:41:21 +01:00
Martin Sekera
5c57e26652 Add btv gc NUMBER command 2020-11-24 21:20:28 +01:00
3 changed files with 96 additions and 17 deletions

82
btv
View file

@ -8,8 +8,11 @@ import datetime
import json import json
import os import os
import shlex import shlex
import shutil
import sys import sys
import time import time
import socket
import urllib.request
CONFIG = "/etc/btv/config.ini" CONFIG = "/etc/btv/config.ini"
LOCKFILE = "/run/lock/btv/serialization.lock" LOCKFILE = "/run/lock/btv/serialization.lock"
@ -196,9 +199,9 @@ def serialize(snap, outdir, key, snap_from=None):
## prepare directories ## prepare directories
## ##
if snap_from: if snap_from:
name = "%s diff from %s" %(snap.name, snap_from.name) name = "%s to %s" %(snap_from.name, snap.name)
else: else:
name = "%s full" %(snap.name) name = snap.name
directory = os.path.join(outdir, name) directory = os.path.join(outdir, name)
os.makedirs(directory) os.makedirs(directory)
@ -244,21 +247,37 @@ def serialize(snap, outdir, key, snap_from=None):
## final touches ## final touches
## ##
## add a self-check executable ## add self-check and unpack executables
with open(os.path.join(directory, "check-integrity.sh"), "w") as f: with open(os.path.join(directory, "check-integrity.sh"), "w") as f:
f.write("#! /bin/sh\n\nsha512sum --check manifest.sha512\n") f.write("#! /bin/sh\n\nsha512sum --check manifest.sha512\n")
os.chmod(f.name, 0o555) os.chmod(f.name, 0o500)
unpack_path = os.path.join(directory, "unpack.sh")
shutil.copy("/usr/share/btv/unpack.sh", unpack_path)
os.chmod(unpack_path, 0o500)
## fix permissions and ownership of created objects ## fix permissions and ownership of created objects
outdir_stat = os.stat(outdir) outdir_stat = os.stat(outdir)
os.chown(directory, outdir_stat.st_uid, outdir_stat.st_gid) os.chown(directory, outdir_stat.st_uid, outdir_stat.st_gid)
os.chmod(directory, 0o700)
for file in os.listdir(directory): for file in os.listdir(directory):
path = os.path.join(directory, file) path = os.path.join(directory, file)
os.chown(path, outdir_stat.st_uid, outdir_stat.st_gid) os.chown(path, outdir_stat.st_uid, outdir_stat.st_gid)
if path.endswith(".aes") or path.endswith(".sha512"):
os.chmod(path, 0o400)
return 0 return 0
def ping(url):
try:
urllib.request.urlopen(url, timeout=10)
except socket.error as e:
print("Ping failed: %s" %(e))
# ------------------------------------------------------------------------------ # ------------------------------------------------------------------------------
# Verbs # Verbs
@ -306,6 +325,10 @@ def do_create(args):
elif snaps_since_rank_1 >= cfg.getint("snap", "rank_1_interval"): elif snaps_since_rank_1 >= cfg.getint("snap", "rank_1_interval"):
snapshot.rank = 1 snapshot.rank = 1
ping_url = cfg.get("monitoring", "rank2_start_url")
if snapshot.rank == 2 and ping_url:
ping(ping_url)
## create the snapshot itself ## create the snapshot itself
## ##
os.makedirs(snapshot.path) os.makedirs(snapshot.path)
@ -327,7 +350,6 @@ def do_create(args):
## do optional processing ## do optional processing
## ##
if snapshot.rank == 2: if snapshot.rank == 2:
## snapshot serialization ## snapshot serialization
# if there's a previous Rank 2 snapshot, compute a diff against it # if there's a previous Rank 2 snapshot, compute a diff against it
# if not or if the process fails, demote this snap to Rank 1 # if not or if the process fails, demote this snap to Rank 1
@ -359,6 +381,10 @@ def do_create(args):
## garbage collection ## garbage collection
do_gc() do_gc()
ping_url = cfg.get("monitoring", "rank2_end_url")
if snapshot.rank == 2 and ping_url:
ping(ping_url)
def do_list(args): def do_list(args):
""" """
@ -493,23 +519,42 @@ def do_gc(args=None):
Drops old snapshots. Drops old snapshots.
If the only arg is "greedy", drops ALL snapshots except the youngest If the only arg is "greedy", drops ALL snapshots except the youngest
Rank 2. Rank 2. If it's a number, drops that many oldest snapshots.
""" """
if args and "greedy" in args: if args:
newest = list_snapshots(2)[-1] if args[0] == "greedy":
newest = list_snapshots(2)[-1]
if newest:
print(">>> Dropping all snapshots except the newest Rank 2 in 5 s...")
time.sleep(5)
for snap in list_snapshots(): if newest:
if snap.name != newest.name: print(">>> Dropping all snapshots except the newest Rank 2 in 5 s...")
snap.drop() time.sleep(5)
for snap in list_snapshots():
if snap.name != newest.name:
snap.drop()
else:
print("!!! no Rank 2 snapshot exists")
sys.exit(1)
else: else:
print("!!! no Rank 2 snapshot exists") try:
sys.exit(1) count = int(args[0])
except ValueError:
print("!!! %s is not a count of snapshots to delete" %(args[0]))
sys.exit(1)
snaps = list_snapshots()[:count]
for snap in snaps:
print(" %d %s %s" %(snap.rank, snap.name, ", ".join(snap.notes)))
print(">>> These snapshots will be dropped in 5 s...")
time.sleep(5)
for snap in snaps:
snap.drop()
else: else:
counts = [0, 0, 0] # Rank 0, 1, 2 counts = [0, 0, 0] # Rank 0, 1, 2
@ -565,6 +610,9 @@ Verbs:
gc greedy gc greedy
Drop ALL snapshots except the newest Rank 2. Drop ALL snapshots except the newest Rank 2.
gc COUNT
Drop COUNT oldest snapshots.
""".format(cmd=sys.argv[0])) """.format(cmd=sys.argv[0]))
if __name__ == "__main__": if __name__ == "__main__":

View file

@ -35,3 +35,10 @@ dir = /mnt/pool/subvol/_backup
rank_0_count=25 rank_0_count=25
rank_1_count=36 rank_1_count=36
rank_2_count=42 rank_2_count=42
[monitoring]
# GETs this URL before starting a rank2 snapdhot
rank2_start_url =
# GETs this URL after successfully completing a rank2 snapshot
rank2_end_url =

24
unpack.sh Normal file
View file

@ -0,0 +1,24 @@
#! /bin/zsh
TIMESTAMP=($(basename "$(pwd)"))
OUTDIR="$1"
KEYFILE="$2"
function die {
>&2 echo "$2"
exit $1
}
[[ "$0" != "./unpack.sh" ]] && die 1 "This can only be executed from the snapshot directory itself."
[[ ! -d "$OUTDIR" ]] && die 1 "The first argument must be a directory to unpack subvolumes into."
[[ ! -f "$KEYFILE" ]] && die 1 "The second argument must be a readable keyfile."
./check-integrity.sh || die 2 "This snapshot failed integrity checks."
### end of checks
for ARCHIVE in *btrfs.zst.aes
do
openssl enc -d -aes-256-cbc -pbkdf2 -salt -pass "file:$KEYFILE" < "$ARCHIVE" | zstd -d | btrfs receive "$OUTDIR" || die 3 "Failed to unpack subvolume."
SUBVOL_NAME=${ARCHIVE%%.btrfs.zst.aes}
mv "${OUTDIR}/${SUBVOL_NAME}" "${OUTDIR}/${SUBVOL_NAME}.${TIMESTAMP[1]}" || die 4 "Failed to rename subvolume."
done