#!/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')