Refactor Backup and BackupSet into their own module
This commit is contained in:
parent
ef7da0ea00
commit
9e838fb2e7
|
@ -0,0 +1,139 @@
|
|||
#!/usr/bin/env python2.7
|
||||
|
||||
import datetime
|
||||
import itertools
|
||||
import time
|
||||
|
||||
import dateutil.tz, dateutil.relativedelta
|
||||
|
||||
from . import package_logger
|
||||
|
||||
WEEKDAYS = [object() for _ in xrange(7)]
|
||||
MONDAY, TUESDAY, WEDNESDAY, THURSDAY, FRIDAY, SATURDAY, SUNDAY = WEEKDAYS
|
||||
MONTHLY = object()
|
||||
WEEKLY = object()
|
||||
|
||||
WEEKDAY_NUMBERS = dict(zip(WEEKDAYS, itertools.count()))
|
||||
|
||||
WEEKDAY_RELATIVE_DAY_MAP = [
|
||||
dateutil.relativedelta.MO,
|
||||
dateutil.relativedelta.TU,
|
||||
dateutil.relativedelta.WE,
|
||||
dateutil.relativedelta.TH,
|
||||
dateutil.relativedelta.FR,
|
||||
dateutil.relativedelta.SA,
|
||||
dateutil.relativedelta.SU
|
||||
]
|
||||
|
||||
LOCAL_TZ = dateutil.tz.tzlocal()
|
||||
|
||||
def module_logger():
|
||||
return package_logger().getChild("backup")
|
||||
|
||||
def next_due_run(timespec, since):
|
||||
def next_due_run_part(part):
|
||||
tgt = None
|
||||
if part is MONTHLY:
|
||||
rel = (dateutil.relativedelta
|
||||
.relativedelta(months=1, day=1, hour=0, minute=0, second=0,
|
||||
microsecond=0))
|
||||
# Go to midnight on the first of the next month
|
||||
tgt = since + rel
|
||||
if part is WEEKLY:
|
||||
part = MONDAY
|
||||
if part in WEEKDAYS:
|
||||
# if since is the same weekday as we have selected, "1st <day>"
|
||||
# would just be that day, so we need to also advance one day before
|
||||
# asking for the "1st <day>".
|
||||
relative_cls = WEEKDAY_RELATIVE_DAY_MAP[WEEKDAY_NUMBERS[part]]
|
||||
day = relative_cls(1)
|
||||
rel = (dateutil.relativedelta
|
||||
.relativedelta(weekday=day, days=+1, hour=0, minute=0,
|
||||
second=0, microsecond=0))
|
||||
tgt = since + rel
|
||||
|
||||
assert tgt is not None
|
||||
return tgt
|
||||
return min((next_due_run_part(part) for part in timespec))
|
||||
|
||||
|
||||
class Backup(object):
|
||||
@property
|
||||
def logger(self):
|
||||
return module_logger().getChild("Backup")
|
||||
|
||||
def __init__(self, name, paths, backup_name, timespec, backends):
|
||||
self.name = name
|
||||
self.paths = paths
|
||||
self.backup_name = backup_name
|
||||
self.timespec = timespec
|
||||
self.backends = backends
|
||||
|
||||
def should_run(self, last_run, now):
|
||||
due = next_due_run(self.timespec, last_run)
|
||||
return due < now
|
||||
|
||||
def perform(self):
|
||||
success = True
|
||||
for backend in self.backends:
|
||||
success = success and backend.perform(self.paths, self.name)
|
||||
return success
|
||||
|
||||
def get_all_archives(self, backends=None):
|
||||
if backends is None:
|
||||
backends = self.backends
|
||||
|
||||
for backend in backends:
|
||||
if backend not in self.backends:
|
||||
raise Exception("Passed a backend we don't own!?")
|
||||
|
||||
pairs = []
|
||||
|
||||
for backend in backends:
|
||||
pairs.append(
|
||||
[backend, backend.existing_archives_for_name(self.name)])
|
||||
|
||||
return pairs
|
||||
|
||||
def get_backends(self):
|
||||
return list(self.backends)
|
||||
|
||||
|
||||
class BackupSet(object):
|
||||
@property
|
||||
def logger(self):
|
||||
return package_logger().getChild("BackupSet")
|
||||
|
||||
def __init__(self, state, configured_backups, config_mtime, state_mtime,
|
||||
now):
|
||||
self.configured_backups = configured_backups
|
||||
self.config_mtime = config_mtime
|
||||
self.state_mtime = state_mtime
|
||||
self.state = state
|
||||
self.now = now
|
||||
|
||||
def state_after_backups(self, backups):
|
||||
new_state = self.state.copy()
|
||||
for backup in backups:
|
||||
new_state[backup.name] = time.mktime(self.now.timetuple())
|
||||
return new_state
|
||||
|
||||
def last_run_of_backup(self, backup):
|
||||
stamp = self.state.get(backup.name, 0)
|
||||
return datetime.datetime.fromtimestamp(stamp).replace(tzinfo=LOCAL_TZ)
|
||||
|
||||
def backups_due(self):
|
||||
backups_to_run = []
|
||||
|
||||
if self.config_mtime > self.state_mtime:
|
||||
self.logger.info("Configuration changed. Should run all backups.")
|
||||
return self.configured_backups
|
||||
|
||||
for backup in self.configured_backups:
|
||||
if backup.should_run(self.last_run_of_backup(backup), self.now):
|
||||
backups_to_run.append(backup)
|
||||
return backups_to_run
|
||||
|
||||
def all_backups(self):
|
||||
return self.configured_backups
|
||||
|
|
@ -12,17 +12,17 @@ import itertools
|
|||
import socket
|
||||
import argparse
|
||||
|
||||
import dateutil.parser, dateutil.tz, dateutil.relativedelta
|
||||
import dateutil.parser, dateutil.tz
|
||||
|
||||
from . import package_logger
|
||||
from . import error
|
||||
from . import backend_types
|
||||
from . import backup
|
||||
|
||||
from .backup import (MONDAY, TUESDAY, WEDNESDAY, THURSDAY, FRIDAY, SATURDAY,
|
||||
SUNDAY, WEEKLY, MONTHLY)
|
||||
|
||||
LOCAL_TZ = dateutil.tz.tzlocal()
|
||||
UTC_TZ = dateutil.tz.tzutc()
|
||||
|
||||
LOCAL_EPOCH = datetime.datetime.fromtimestamp(0).replace(tzinfo=LOCAL_TZ)
|
||||
UTC_EPOCH = datetime.datetime.utcfromtimestamp(0).replace(tzinfo=UTC_TZ)
|
||||
|
||||
if sys.platform.startswith("darwin"):
|
||||
DEFAULT_STATEFILE = "/var/db/backupmgr.state"
|
||||
|
@ -31,36 +31,9 @@ else:
|
|||
|
||||
CONFIG_LOCATION = "/etc/backupmgr.conf"
|
||||
|
||||
WEEKDAYS = [object() for _ in xrange(7)]
|
||||
MONDAY, TUESDAY, WEDNESDAY, THURSDAY, FRIDAY, SATURDAY, SUNDAY = WEEKDAYS
|
||||
MONTHLY = object()
|
||||
WEEKLY = object()
|
||||
|
||||
WEEKDAY_NUMBERS = dict(zip(WEEKDAYS, itertools.count()))
|
||||
|
||||
WEEKDAY_RELATIVE_DAY_MAP = [
|
||||
dateutil.relativedelta.MO,
|
||||
dateutil.relativedelta.TU,
|
||||
dateutil.relativedelta.WE,
|
||||
dateutil.relativedelta.TH,
|
||||
dateutil.relativedelta.FR,
|
||||
dateutil.relativedelta.SA,
|
||||
dateutil.relativedelta.SU
|
||||
]
|
||||
|
||||
def module_logger():
|
||||
return package_logger().getChild("configuration")
|
||||
|
||||
class NoConfigError(error.Error):
|
||||
def __init__(self):
|
||||
super(NoConfigError, self).__init__("No config exists.")
|
||||
|
||||
|
||||
class InvalidConfigError(error.Error):
|
||||
def __init__(self, msg):
|
||||
super(InvalidConfigError, self).__init__("Invalid config: {}".format(msg))
|
||||
|
||||
|
||||
def prefix_match(s1, s2, required_length):
|
||||
return s1[:required_length] == s2[:required_length]
|
||||
|
||||
|
@ -117,114 +90,6 @@ def validate_paths(paths):
|
|||
|
||||
return paths
|
||||
|
||||
def next_due_run(timespec, since):
|
||||
def next_due_run_part(part):
|
||||
tgt = None
|
||||
if part is MONTHLY:
|
||||
rel = (dateutil.relativedelta
|
||||
.relativedelta(months=1, day=1, hour=0, minute=0, second=0,
|
||||
microsecond=0))
|
||||
# Go to midnight on the first of the next month
|
||||
tgt = since + rel
|
||||
if part is WEEKLY:
|
||||
part = MONDAY
|
||||
if part in WEEKDAYS:
|
||||
# if since is the same weekday as we have selected, "1st <day>"
|
||||
# would just be that day, so we need to also advance one day before
|
||||
# asking for the "1st <day>".
|
||||
relative_cls = WEEKDAY_RELATIVE_DAY_MAP[WEEKDAY_NUMBERS[part]]
|
||||
day = relative_cls(1)
|
||||
rel = (dateutil.relativedelta
|
||||
.relativedelta(weekday=day, days=+1, hour=0, minute=0,
|
||||
second=0, microsecond=0))
|
||||
tgt = since + rel
|
||||
|
||||
assert tgt is not None
|
||||
return tgt
|
||||
return min((next_due_run_part(part) for part in timespec))
|
||||
|
||||
|
||||
class ConfiguredBackup(object):
|
||||
@property
|
||||
def logger(self):
|
||||
return module_logger().getChild("ConfiguredBackup")
|
||||
|
||||
def __init__(self, name, paths, backup_name, timespec, backends):
|
||||
self.name = name
|
||||
self.paths = paths
|
||||
self.backup_name = backup_name
|
||||
self.timespec = timespec
|
||||
self.backends = backends
|
||||
|
||||
def should_run(self, last_run, now):
|
||||
due = next_due_run(self.timespec, last_run)
|
||||
return due < now
|
||||
|
||||
def perform(self):
|
||||
success = True
|
||||
for backend in self.backends:
|
||||
success = success and backend.perform(self.paths, self.name)
|
||||
return success
|
||||
|
||||
def get_all_archives(self, backends=None):
|
||||
if backends is None:
|
||||
backends = self.backends
|
||||
|
||||
for backend in backends:
|
||||
if backend not in self.backends:
|
||||
raise Exception("Passed a backend we don't own!?")
|
||||
|
||||
pairs = []
|
||||
|
||||
for backend in backends:
|
||||
pairs.append(
|
||||
[backend, backend.existing_archives_for_name(self.name)])
|
||||
|
||||
return pairs
|
||||
|
||||
def get_backends(self):
|
||||
return list(self.backends)
|
||||
|
||||
|
||||
class ConfiguredBackupSet(object):
|
||||
@property
|
||||
def logger(self):
|
||||
return package_logger().getChild("ConfiguredBackupSet")
|
||||
|
||||
def __init__(self, state, configured_backups, config_mtime, state_mtime,
|
||||
now):
|
||||
self.configured_backups = configured_backups
|
||||
self.config_mtime = config_mtime
|
||||
self.state_mtime = state_mtime
|
||||
self.state = state
|
||||
self.now = now
|
||||
|
||||
def state_after_backups(self, backups):
|
||||
new_state = self.state.copy()
|
||||
for backup in backups:
|
||||
new_state[backup.name] = time.mktime(self.now.timetuple())
|
||||
return new_state
|
||||
|
||||
def last_run_of_backup(self, backup):
|
||||
stamp = self.state.get(backup.name, 0)
|
||||
return datetime.datetime.fromtimestamp(stamp).replace(tzinfo=LOCAL_TZ)
|
||||
|
||||
def backups_due(self):
|
||||
backups_to_run = []
|
||||
|
||||
if self.config_mtime > self.state_mtime:
|
||||
self.logger.info("Configuration changed. Should run all backups.")
|
||||
return self.configured_backups
|
||||
|
||||
for backup in self.configured_backups:
|
||||
if backup.should_run(self.last_run_of_backup(backup), self.now):
|
||||
backups_to_run.append(backup)
|
||||
return backups_to_run
|
||||
|
||||
def all_backups(self):
|
||||
return self.configured_backups
|
||||
|
||||
|
||||
def parse_simple_date(datestr):
|
||||
try:
|
||||
timestamp = float(datestr)
|
||||
|
@ -240,6 +105,17 @@ def parse_simple_date(datestr):
|
|||
|
||||
return date
|
||||
|
||||
|
||||
class NoConfigError(error.Error):
|
||||
def __init__(self):
|
||||
super(NoConfigError, self).__init__("No config exists.")
|
||||
|
||||
|
||||
class InvalidConfigError(error.Error):
|
||||
def __init__(self, msg):
|
||||
super(InvalidConfigError, self).__init__("Invalid config: {}".format(msg))
|
||||
|
||||
|
||||
class Config(object):
|
||||
@property
|
||||
def logger(self):
|
||||
|
@ -368,7 +244,7 @@ class Config(object):
|
|||
return backend
|
||||
backends = [find_backend(backend_name) for backend_name in backends]
|
||||
|
||||
return ConfiguredBackup(name, paths, backup_name, timespec, backends)
|
||||
return backup.Backup(name, paths, backup_name, timespec, backends)
|
||||
|
||||
if not isinstance(config_dict.get("backups", None), list):
|
||||
raise InvalidConfigError("Expected a list of backups")
|
||||
|
@ -376,7 +252,7 @@ class Config(object):
|
|||
parse_backup(backup_dict) for backup_dict in config_dict["backups"]
|
||||
]
|
||||
|
||||
for name, count in collections.Counter([backup.name for backup in configured_backups]).items():
|
||||
for name, count in collections.Counter([configured_backup.name for configured_backup in configured_backups]).items():
|
||||
if count > 1:
|
||||
raise InvalidConfigError("Duplicate backup \"{}\"".format(name))
|
||||
|
||||
|
@ -390,6 +266,6 @@ class Config(object):
|
|||
|
||||
state = self.load_state()
|
||||
|
||||
self.configured_backups = ConfiguredBackupSet(
|
||||
self.configured_backups = backup.BackupSet(
|
||||
state, configured_backups, os.stat(self.configfile).st_mtime,
|
||||
state_mtime, datetime.datetime.now().replace(tzinfo=LOCAL_TZ))
|
||||
|
|
Loading…
Reference in New Issue