Compare commits

..

14 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
Martin Sekera
f58353fe87 Add --rank2 to btv snapshot 2020-09-03 23:30:49 +02:00
Martin Sekera
aa96fa3cf5 fix gc running too soon 2020-09-01 21:02:33 +02:00
Martin Sekera
cd2458b830 fix gc exceeding limits 2020-09-01 12:22:57 +02:00
Martin Sekera
ad19226d02 add gc greedy 2020-08-29 02:42:54 +02:00
3 changed files with 152 additions and 46 deletions

166
btv
View file

@ -8,7 +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 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"
@ -195,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)
@ -243,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
@ -277,30 +297,37 @@ def do_create(args):
## determine the rank of the new snapshot ## determine the rank of the new snapshot
## ##
snaps_since_rank_1 = 1 if "--rank2" in args:
snaps_since_rank_2 = 1 snapshot.rank = 2
else:
for snap in list_snapshots(): snaps_since_rank_1 = 1
if snap.rank == 1: snaps_since_rank_2 = 1
snaps_since_rank_1 = 1
else:
snaps_since_rank_1 += 1
if snap.rank == 2: for snap in list_snapshots():
snaps_since_rank_1 = 1 if snap.rank == 1:
snaps_since_rank_2 = 1 snaps_since_rank_1 = 1
else: else:
snaps_since_rank_2 += 1 snaps_since_rank_1 += 1
## promote the snapshot if snap.rank == 2:
if snaps_since_rank_2 >= cfg.getint("snap", "rank_2_interval"): snaps_since_rank_1 = 1
if "--process" in args: snaps_since_rank_2 = 1
snapshot.rank = 2 else:
else: snaps_since_rank_2 += 1
print("!!! Rank 2 snapshot is due, please enable --process")
## promote the snapshot
if snaps_since_rank_2 >= cfg.getint("snap", "rank_2_interval"):
if "--process" in args:
snapshot.rank = 2
else:
print("!!! Rank 2 snapshot is due, please enable --process")
snapshot.rank = 1
elif snaps_since_rank_1 >= cfg.getint("snap", "rank_1_interval"):
snapshot.rank = 1 snapshot.rank = 1
elif snaps_since_rank_1 >= cfg.getint("snap", "rank_1_interval"):
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
## ##
@ -323,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
@ -347,14 +373,18 @@ def do_create(args):
else: else:
print("!!! no previous Rank 2 snapshot, please create one using btv stream") print("!!! no previous Rank 2 snapshot, please create one using btv stream")
snapshot.rank = 1 snapshot.rank = 1
## garbage collection
do_gc()
## save all snapshot metadata ## save all snapshot metadata
snapshot.dump() snapshot.dump()
print(">>> Snapshot created: rank %d %s" %(snapshot.rank, snapshot.name)) print(">>> Snapshot created: rank %d %s" %(snapshot.rank, snapshot.name))
## garbage collection
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):
""" """
@ -487,22 +517,60 @@ def do_stream(args):
def do_gc(args=None): def do_gc(args=None):
""" """
Drops old snapshots. Drops old snapshots.
If the only arg is "greedy", drops ALL snapshots except the youngest
Rank 2. If it's a number, drops that many oldest snapshots.
""" """
counts = [0, 0, 0] # Rank 0, 1, 2 if args:
limits = [ if args[0] == "greedy":
cfg.getint("gc", "rank_0_count"), newest = list_snapshots(2)[-1]
cfg.getint("gc", "rank_1_count"),
cfg.getint("gc", "rank_2_count")
]
for snap in reversed(list_snapshots()):
counts[snap.rank] += 1
if counts[snap.rank] > limits[snap.rank]:
print(" >> delete Rank %d %s" %(snap.rank, snap.name))
snap.drop() if newest:
print(">>> Dropping all snapshots except the newest Rank 2 in 5 s...")
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:
try:
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:
counts = [0, 0, 0] # Rank 0, 1, 2
limits = [
cfg.getint("gc", "rank_0_count"),
cfg.getint("gc", "rank_1_count"),
cfg.getint("gc", "rank_2_count")
]
for snap in reversed(list_snapshots()):
counts[snap.rank] += 1
if counts[snap.rank] > limits[snap.rank]:
print(" >> delete Rank %d %s" %(snap.rank, snap.name))
snap.drop()
verb_router = { verb_router = {
"snapshot": do_create, "snapshot": do_create,
@ -520,8 +588,10 @@ def print_help():
{cmd} VERB [args] {cmd} VERB [args]
Verbs: Verbs:
snapshot [--process] snapshot [--process] [--rank2]
Create a snapshot. If --process is passed, do all optional work. Create a snapshot. If --process is passed, do all optional work. If
--rank2 is passed, the new snapshot is automatically promoted to Rank 2
and --process is implied.
stream full SNAPSHOT OUTPUT_DIR stream full SNAPSHOT OUTPUT_DIR
Streams the SNAPSHOT into OUTPUT_DIR as a full (milestone) bin. Streams the SNAPSHOT into OUTPUT_DIR as a full (milestone) bin.
@ -537,6 +607,12 @@ Verbs:
gc gc
Drops old local snapshots based on garbage collector settings. Drops old local snapshots based on garbage collector settings.
gc greedy
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

@ -30,9 +30,15 @@ dir = /mnt/pool/subvol/_backup
[gc] [gc]
# How many snapshots of each Rank to keep around. # How many snapshots of each Rank to keep around.
# Note: pruning is only performed when a new Rank 2 snapshot is attempted.
# Default values are 25, 36, and 42, i.e. 5 hours for Rank 0, 2 days for Rank 1, # Default values are 25, 36, and 42, i.e. 5 hours for Rank 0, 2 days for Rank 1,
# and a week for Rank 2. # and a week for Rank 2.
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