Initial working implementation, missing command line parsing
This commit is contained in:
@@ -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__":
|
||||||
|
|||||||
Reference in New Issue
Block a user