Initial working implementation, missing command line parsing

This commit is contained in:
2021-09-30 21:46:35 +02:00
parent 0e1d8aeb8b
commit 9d92d78c9e

View File

@@ -1,8 +1,12 @@
from datetime import datetime, timezone from datetime import datetime, timezone
from dateutil.relativedelta import relativedelta from dateutil.relativedelta import relativedelta
from pprint import pprint
import subprocess import subprocess
import sys import sys
from pprint import pprint
# TODO: Argument parsing
verbose = True
dry_run = False
def keep_time(now, snapshots, convert, delta, count): def keep_time(now, snapshots, convert, delta, count):
@@ -11,9 +15,10 @@ def keep_time(now, snapshots, convert, delta, count):
return keep return keep
start = convert(now) - delta*(count-1) start = convert(now) - delta*(count-1)
for snapshot in snapshots: for snapshot in snapshots:
if snapshot >= start: snapdate = datetime.strptime(snapshot, 'zfs-smart-snap-%Y-%m-%d-%H%M').replace(tzinfo=timezone.utc)
if snapdate >= start:
keep.add(snapshot) keep.add(snapshot)
start = convert(snapshot) + delta start = convert(snapdate) + delta
if start > now: if start > now:
break break
return keep return keep
@@ -49,7 +54,7 @@ def snapshots_to_keep(now, snapshots, yearly=0, monthly=0, daily=0, hourly=0, fr
keep.update(keep_day(now, snapshots, daily)) keep.update(keep_day(now, snapshots, daily))
keep.update(keep_hour(now, snapshots, hourly)) keep.update(keep_hour(now, snapshots, hourly))
keep.update(keep_frequent(now, snapshots, frequently)) keep.update(keep_frequent(now, snapshots, frequently))
return sorted(list(keep)) return keep
def parse_int(fields, name, fieldname): def parse_int(fields, name, fieldname):
@@ -59,14 +64,13 @@ def parse_int(fields, name, fieldname):
return int(fields[fieldname]) return int(fields[fieldname])
except ValueError: except ValueError:
print(f"Warning: Could not parse value {fields[fieldname]} for fs/zvol {name} prop {fieldname}", print(f"Warning: Could not parse value {fields[fieldname]} for fs/zvol {name} prop {fieldname}",
output=sys.stderr) file=sys.stderr)
return -1 return -1
def zfs_list(fieldnames, args): def zfs_list(fieldnames, args):
proc = subprocess.run(['/usr/sbin/zfs', 'list', '-H', '-o', ','.join(['name'] + fieldnames)] + args, proc = subprocess.run(['/usr/sbin/zfs', 'list', '-H', '-o', ','.join(['name'] + fieldnames)] + args,
capture_output=True, check=True, encoding='UTF-8') capture_output=True, check=True, encoding='UTF-8')
pprint(proc.args)
if not fieldnames: if not fieldnames:
result = list() result = list()
else: else:
@@ -96,7 +100,6 @@ def read_snapshots(fs_to_snap):
result = dict() result = dict()
for name in raw_list: for name in raw_list:
fs, snap = name.split('@') fs, snap = name.split('@')
print(f'{fs}, {snap}')
if snap.startswith('zfs-smart-snap'): if snap.startswith('zfs-smart-snap'):
result[fs] = result.get(fs, []) + [snap] result[fs] = result.get(fs, []) + [snap]
return result return result
@@ -113,25 +116,53 @@ def find_latest_snapshots(existing_snaps):
def written_since_last(fs, snap): def written_since_last(fs, snap):
proc = subprocess.run(['/usr/sbin/zfs', 'list', '-Hp', '-o', f'written@{snap}', fs], proc = subprocess.run(['/usr/sbin/zfs', 'list', '-Hp', '-o', f'written@{snap}', fs],
check=True, capture_output=True, encoding="UTF-8") check=True, capture_output=True, encoding="UTF-8")
return int(proc.stdout) > 0 res = int(proc.stdout)
if verbose and res == 0:
print(f"{fs} not written since {snap}, skipping")
return res > 0
def main(): def main():
now = datetime.now(timezone.utc).replace(second=0, microsecond=0) now = datetime.now(timezone.utc).replace(second=0, microsecond=0)
if verbose:
print(f"zfs_smart_snapshot running at {now.strftime('%Y-%m-%d-%H%M')}")
props = read_snapshot_props() props = read_snapshot_props()
pprint(props)
fs_to_snap = [k for k, v in props.items() if v['snapshot']] fs_to_snap = [k for k, v in props.items() if v['snapshot']]
if fs_to_snap: if fs_to_snap:
existing_snaps = read_snapshots(fs_to_snap) existing_snaps = read_snapshots(fs_to_snap)
pprint(existing_snaps)
latest_snaps = find_latest_snapshots(existing_snaps) latest_snaps = find_latest_snapshots(existing_snaps)
fs_to_really_snap = [fs for fs in fs_to_snap if fs not in latest_snaps or fs_to_really_snap = [fs for fs in fs_to_snap if fs not in latest_snaps or
written_since_last(fs, latest_snaps[fs])] written_since_last(fs, latest_snaps[fs])]
if fs_to_really_snap: if fs_to_really_snap:
pools = {fs.split('/')[0] for fs in fs_to_really_snap}
snapname = 'zfs-smart-snap-' + now.strftime('%Y-%m-%d-%H%M') snapname = 'zfs-smart-snap-' + now.strftime('%Y-%m-%d-%H%M')
subprocess.run(['/usr/sbin/zfs', 'snapshot'] + [f'{fs}@{snapname}' for fs in fs_to_really_snap], check=True) snaps_to_create = [f'{fs}@{snapname}' for fs in fs_to_really_snap]
if dry_run or verbose:
phrase = "Would" if dry_run else "Will"
print(f"{phrase} create snapshots " + ", ".join(snaps_to_create))
if not dry_run:
for pool in pools:
subprocess.run(['/usr/sbin/zfs', 'snapshot'] + [snap for snap in snaps_to_create if snap.startswith(pool)], check=True)
for fs in fs_to_really_snap:
existing_snaps[fs] = existing_snaps.get(fs, []) + [snapname]
# TODO: Cleanup expired snapshots to_delete = []
for fs in (fs for fs in fs_to_snap if fs in existing_snaps):
to_keep = snapshots_to_keep(now, existing_snaps[fs], props[fs]['yearly'], props[fs]['monthly'], props[fs]['daily'], props[fs]['hourly'], props[fs]['frequently'])
# Always keep latest snapshot. If it would be deleted (e.g. due to a incoherent snapshot history configuration) this could confuse the
# 'written since last snapshot' logic and cause a cycle of creating/deleteing/creating the snapshot on an idle file system
if existing_snaps[fs][-1] not in to_keep:
print(f"Warning: Latest snap {fs}@{existing_snaps[fs][-1]} not preserved by retention policy, keeping anyways", file=sys.stderr)
to_keep.add(existing_snaps[fs][-1])
to_delete += (f'{fs}@{snap}' for snap in set(existing_snaps[fs]).difference(to_keep))
if not dry_run:
if verbose and len(to_delete) > 0:
print("Will destroy snapshots " + ", ".join(sorted(to_delete)))
for delete_snap in to_delete:
subprocess.run(['/usr/sbin/zfs', 'destroy', delete_snap], check=True)
elif len(to_delete) > 0:
print("Would destroy snapshots " + ", ".join(sorted(to_delete)))
if __name__ == "__main__": if __name__ == "__main__":