bundlewrap/bundles/backup-server/files/rotate-single-backup-client

112 lines
3.3 KiB
Python

#!/usr/bin/env python3
from json import load
from subprocess import check_call, check_output
from sys import argv
from time import time
NODE = argv[1]
NOW = int(time())
DAY_SECONDS = 60 * 60 * 24
INTERVALS = {
'daily': DAY_SECONDS,
'weekly': 7 * DAY_SECONDS,
'monthly': 30 * DAY_SECONDS,
}
buckets = {}
def syslog(msg):
check_output(['logger', '-t', f'backup-{NODE}', msg])
with open(f'/etc/backup-server/config.json', 'r') as f:
server_settings = load(f)
with open(f'/etc/backup-server/clients/{NODE}', 'r') as f:
client_settings = load(f)
# get all existing snapshots for NODE
for line in check_output('LC_ALL=C zfs list -H -t snapshot -o name', shell=True).splitlines():
line = line.decode('UTF-8')
if line.startswith('{}/{}@'.format(server_settings['zfs-base'], NODE)):
_, snapname = line.split('@', 1)
if 'zfs-auto-snap' in snapname:
# migration from auto-snapshots, ignore
continue
ts, bucket = snapname.split('-', 1)
buckets.setdefault(bucket, set()).add(int(ts))
syslog(f'classified {line} as {bucket} from {ts}')
# determine if we need to create a new snapshot
for bucket in INTERVALS.keys():
snapshots = sorted(buckets.get(bucket, set()))
if snapshots:
last_snap = snapshots[-1]
delta = NOW - last_snap
fresh_age = INTERVALS[bucket] - DAY_SECONDS
if delta > fresh_age:
# last snapshot is older than what we want. create a new one.
check_call(
'zfs snapshot {}/{}@{}-{}'.format(
server_settings['zfs-base'],
NODE,
NOW,
bucket,
),
shell=True,
)
buckets.setdefault(bucket, set()).add(NOW)
syslog(f'created new snapshot {NOW}-{bucket}')
else:
syslog(f'existing snapshot {last_snap}-{bucket} is fresh enough')
else:
check_call(
'zfs snapshot {}/{}@{}-{}'.format(
server_settings['zfs-base'],
NODE,
NOW,
bucket,
),
shell=True,
)
buckets.setdefault(bucket, set()).add(NOW)
syslog(f'created initial snapshot {NOW}-{bucket}')
# finally, see if we can delete any snapshots, because they are old enough
for bucket in INTERVALS.keys():
snapshots = sorted(buckets.get(bucket, set()))
if not snapshots:
syslog(f'something is wrong, there are no snapshots for {bucket}')
continue
keep_age = INTERVALS[bucket] * client_settings[bucket]
# oldest snapshots come first
for ts in snapshots[:-int(client_settings[bucket])]:
delta = NOW - ts
if delta >= keep_age:
check_call(
'zfs destroy {}/{}@{}-{}'.format(
server_settings['zfs-base'],
NODE,
ts,
bucket,
),
shell=True,
)
syslog(f'removing snapshot {ts}-{bucket}, age {delta}, keep_age {keep_age}')
else:
syslog(f'keeping snapshot {ts}-{bucket}, age not reached')
for ts in snapshots[int(client_settings[bucket]):]:
syslog(f'keeping snapshot {ts}-{bucket}, count')