Refactor Backup and BackupSet into their own module

This commit is contained in:
Conor Hughes 2014-11-13 18:40:55 -08:00
parent ef7da0ea00
commit 9e838fb2e7
2 changed files with 158 additions and 143 deletions

139
backupmgr/backup.py Normal file
View File

@ -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

View File

@ -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))