111 lines
3.3 KiB
Python
111 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')
|