...
 
Commits (8)
#!/usr/bin/env python3.5
from datetime import datetime
from collections import defaultdict
def inRange(low, high):
return (lambda x: low <= int(x) < high)
def partition(seq, key):
d = defaultdict(list)
for x in seq:
d[key(x)].append(x)
return d
def all_candidates(d, n):
if not d:
return [], []
keep = []
remove = []
for key, val in d.items():
k,r = candidates(val,n)
keep.extend(k)
remove.extend(r)
return keep, remove
def candidates(l, n):
if n <= 0:
# Keep none
return [], l
elif n >= len(l):
# keep all
return l, []
else:
indices = list(map(lambda x: int(x/len(l)), range(n)))
# I'm sure there's a more pythonic way to do this
keep = []
discard = []
for idx, val in enumerate(l):
if idx in indices:
keep.append(val)
else:
discard.append(val)
return keep, discard
class Calendar:
def __init__(self,snapshots):
times = sorted(snapshots.keys())
now = datetime.now().timestamp()
h24 = now - 60*60*24
d7 = now - 60*60*60*7
d30 = now - 60*60*60*30
d365 = now - 60*60*60*365
self.last_24h = list(filter(inRange(h24,now),times))
self.last_7d = list(filter(inRange(d7,h24),times))
self.last_30d = list(filter(inRange(d30,d7),times))
self.last_365d = list(filter(inRange(d365,d30),times))
self.hour_target = 1
self.day_target = 4
self.week_target = 14
self.month_target = 30
self.year_target = 4
times = list(map(lambda x: datetime.utcfromtimestamp(float(x)), sorted(snapshots.keys())))
now = datetime.utcnow()
self.today_by_hour = partition([x for x in times if x.date() == now.date()], lambda x: x.hour)
self.this_month_by_day = partition([x for x in times if x.year == now.year and x.month == now.month and x.day < now.day], lambda x: x.day)
self.this_year_by_month = partition([x for x in times if x.year == now.year and x.month < now.month], lambda x: x.month)
self.previous_years_by_year = partition([x for x in times if x.year < now.year], lambda x: x.year)
def today(self):
return sum(map(lambda x: len(x),self.today_by_hour.values()))
def this_month(self):
return sum(map(lambda x: len(x),self.this_month_by_day.values()))
def this_year(self):
return sum(map(lambda x: len(x),self.this_year_by_month.values()))
def previous_years(self):
return sum(map(lambda x: len(x),self.previous_years_by_year.values()))
def prune(self):
keep = []
remove = []
k,r = all_candidates(self.today_by_hour, self.hour_target)
keep.extend(k)
remove.extend(r)
k,r = all_candidates(self.this_month_by_day, self.day_target)
keep.extend(k)
remove.extend(r)
k,r = all_candidates(self.this_year_by_month, self.month_target)
keep.extend(k)
remove.extend(r)
k,r = all_candidates(self.previous_years_by_year, self.year_target)
keep.extend(k)
remove.extend(r)
return keep, remove
......@@ -83,7 +83,7 @@ def summary():
cal = calendar.Calendar(snapshots[name])
count = len(snapshots[name])
recent = sorted(snapshots[name].keys(),key = lambda i: int(i)).pop()
print("{:<12s} {:>5d} snapshots {:<20s} {:>3d} {:>3d} {:>4d} {:>4d}".format(name,count,util.humanizeTime(recent),len(cal.last_24h),len(cal.last_7d),len(cal.last_30d),len(cal.last_365d)))
print("{:<12s} {:>5d} snapshots {:<20s} {:>3d} {:>3d} {:>4d} {:>4d}".format(name,count,util.humanizeTime(recent),cal.today(),cal.this_month(),cal.this_year(),cal.previous_years()))
newCommand("summary", None, "Give a summary of snapshots", summary)
......@@ -154,3 +154,13 @@ def nuke():
zfs.deleteSnap(snap)
newCommand("nuke", [Command.ARG_ID ], "Remove snapsot", nuke)
def prune():
util.loadSnapshots()
for name, place in sorted(config.places.items()):
cal = calendar.Calendar(snapshots[name])
count = len(snapshots[name])
keep, remove = cal.prune()
print ("%s: Total: %d Keep %d Remove %d" % (name,count,len(keep),len(remove)))
newCommand("prune", None, "Suggest which snapshots to prune", prune)
......@@ -23,6 +23,7 @@ class Configuration:
# Default paths, can be overriden
ZFS_BIN = "/sbin/zfs"
TARSNAP_BIN = "/usr/local/bin/tarsnap"
CONFIG_FILE = GLOBALFILE
def __init__(self):
self.zfs_bin = Configuration.ZFS_BIN
......
......@@ -35,7 +35,7 @@ class Snapshot:
HASH = hashlib.sha256
HASH_LEN = 6
HASH_LEN = 8
def isSnappyZFS(snap):
return re.match(r"snappy-(\d+)",snap)
......
......@@ -9,6 +9,7 @@ import snappylib.snapshot
import snappylib.zfs as zfs
import snappylib.tarsnap as tarsnap
import snappylib.configuration as configuration
from snappylib.configuration import config
def getPlace():
if len(sys.argv) < 1:
......@@ -92,16 +93,16 @@ def check():
startCheck("whether configuration file exists")
# XXX Specify alternate config file
if os.path.isfile("snappy.ini"):
if os.path.isfile(config.CONFIG_FILE):
passCheck()
else:
failCheck("No configuration file (snappy.ini) found")
failCheck("No configuration file (%s) found" % config.CONFIG_FILE)
startCheck("whether configuration file parses")
try:
configuration.loadINI("snappy.ini")
configuration.loadINI(config.CONFIG_FILE)
except configparser.Error as e:
failCheck("Error parsing configuration file (snappy.ini)", str(e))
failCheck("Error parsing configuration file (%s)" % config.CONFIG_FILE, str(e))
else:
passCheck()
......