add restore verb
This commit is contained in:
parent
c75034ff37
commit
c647995056
|
@ -1,4 +1,5 @@
|
|||
#!/usr/bin/env python2.7
|
||||
# -*- coding: utf-8 -*-
|
||||
|
||||
import logging
|
||||
import os
|
||||
|
@ -6,12 +7,19 @@ import sys
|
|||
import traceback
|
||||
import datetime
|
||||
import dateutil
|
||||
import dateutil.tz
|
||||
|
||||
from . import configuration
|
||||
from . import package_logger
|
||||
from . import error
|
||||
from . import backend_types
|
||||
from . import logging_handlers
|
||||
from . import archive_specifiers
|
||||
|
||||
def pretty_archive(archive):
|
||||
local_time = archive.datetime.astimezone(dateutil.tz.tzlocal())
|
||||
human_time = local_time.strftime("%Y-%m-%d %H:%M:%S")
|
||||
return "{} ({})".format(human_time, archive.timestamp)
|
||||
|
||||
class Application(object):
|
||||
@property
|
||||
|
@ -50,9 +58,23 @@ class Application(object):
|
|||
def get_all_backups(self):
|
||||
return self.config.all_configured_backups()
|
||||
|
||||
def get_backup_by_name(self, name):
|
||||
for backup in self.get_all_backups():
|
||||
if backup.name == name:
|
||||
return backup
|
||||
else:
|
||||
raise error.Error("Couldn't find backup with name {}".format(name))
|
||||
|
||||
def get_all_backends(self):
|
||||
return self.config.all_configured_backends()
|
||||
|
||||
def get_backend_by_name(self, name):
|
||||
for backend in self.get_all_backends():
|
||||
if backend.name == name:
|
||||
return backend
|
||||
else:
|
||||
raise error.Error("Couldn't find backend with name {}".format(name))
|
||||
|
||||
def note_successful_backups(self, backups):
|
||||
self.config.save_state_given_new_backups(backups)
|
||||
|
||||
|
@ -62,8 +84,7 @@ class Application(object):
|
|||
def within_timespec(self, archive):
|
||||
before = self.config.config_options.before
|
||||
after = self.config.config_options.after
|
||||
archive_date = datetime.datetime.utcfromtimestamp(archive.time)
|
||||
archive_date = archive_date.replace(tzinfo=dateutil.tz.tzutc())
|
||||
archive_date = archive.datetime
|
||||
if before is not None and archive_date >= before:
|
||||
return False
|
||||
if after is not None and archive_date <= after:
|
||||
|
@ -83,16 +104,14 @@ class Application(object):
|
|||
self.note_successful_backups(backup_successes)
|
||||
self.logger.info("Successfully completed {}/{} backups.".format(len(backup_successes), len(backups)))
|
||||
|
||||
def list_backups(self):
|
||||
def list_archives(self):
|
||||
for backup in self.get_all_backups():
|
||||
sys.stdout.write("{}:\n".format(backup.name))
|
||||
for backend, archives in backup.get_all_archives():
|
||||
archives = (archive for archive in archives if self.within_timespec(archive))
|
||||
sys.stdout.write("\t{}:\n".format(backend.name))
|
||||
for archive in sorted(archives, cmp=lambda x,y: cmp(x.time, y.time)):
|
||||
time = datetime.datetime.fromtimestamp(archive.time)
|
||||
human_time = time.strftime("%Y-%m-%d %H:%M:%S")
|
||||
sys.stdout.write("\t\t{} ({})\n".format(human_time, archive.time))
|
||||
for archive in sorted(archives, cmp=lambda x,y: cmp(x.datetime, y.datetime)):
|
||||
sys.stdout.write("\t\t{}\n".format(pretty_archive(archive)))
|
||||
|
||||
def list_configured_backups(self):
|
||||
for backup in self.get_all_backups():
|
||||
|
@ -104,21 +123,50 @@ class Application(object):
|
|||
for backend in self.get_all_backends():
|
||||
sys.stdout.write("{}\n".format(backend))
|
||||
|
||||
def restore_backup(self):
|
||||
backup_name = self.config.config_options.backup
|
||||
backup = self.get_backup_by_name(backup_name)
|
||||
backend_name = self.config.config_options.backend
|
||||
backend = self.get_backend_by_name(backend_name)
|
||||
if backend not in backup.backends:
|
||||
raise error.Error(
|
||||
"backend {} not configured for backup {}".format(backend_name,
|
||||
backup_name))
|
||||
spec_str = self.config.config_options.archive_spec
|
||||
spec = archive_specifiers.ArchiveSpecifier(spec_str)
|
||||
matches = []
|
||||
for _, archives in backup.get_all_archives(backends=[backend]):
|
||||
for i, archive in enumerate(sorted(archives, cmp=lambda x,y: cmp(x.datetime, y.datetime))):
|
||||
if spec.evaluate(archive, i):
|
||||
matches.append(archive)
|
||||
|
||||
if len(matches) > 1:
|
||||
msg = "Spec {} matched more than one archive!".format(spec_str)
|
||||
for match in matches:
|
||||
msg += "\n\t{}".format(pretty_archive(match))
|
||||
raise error.Error(msg)
|
||||
if len(matches) == 0:
|
||||
raise error.Error("Spec {} matched no archives!".format(spec_str))
|
||||
|
||||
archive = matches[0]
|
||||
return archive.restore(self.config.config_options.destination)
|
||||
|
||||
def unknown_verb(self):
|
||||
raise Exception("Unknown verb")
|
||||
|
||||
def run(self):
|
||||
verbs = {
|
||||
"backup": self.perform_backups,
|
||||
"list": self.list_backups,
|
||||
"list": self.list_archives,
|
||||
"list-configured-backups": self.list_configured_backups,
|
||||
"list-backends": self.list_backends
|
||||
"list-backends": self.list_backends,
|
||||
"restore": self.restore_backup
|
||||
}
|
||||
try:
|
||||
self.bootstrap()
|
||||
self.load_config()
|
||||
verbs.get(self.config.config_options.verb, self.unknown_verb)()
|
||||
sys.exit(0)
|
||||
ok = verbs.get(self.config.config_options.verb, self.unknown_verb)()
|
||||
sys.exit(0 if ok or ok is None else 1)
|
||||
except error.Error as e:
|
||||
self.logger.fatal(e.message)
|
||||
sys.exit(1)
|
||||
|
|
|
@ -0,0 +1,59 @@
|
|||
#!/usr/bin/env python2.7
|
||||
# -*- coding: utf-8 -*-
|
||||
|
||||
CONCRETE_SPECIFIERS = set()
|
||||
|
||||
class ArchiveSpecifierMeta(type):
|
||||
def __init__(cls, *args, **kwargs):
|
||||
super(ArchiveSpecifierMeta, cls).__init__(*args, **kwargs)
|
||||
if cls.concrete:
|
||||
CONCRETE_SPECIFIERS.add(cls)
|
||||
|
||||
def __call__(cls, specifier_str, *args, **kwargs):
|
||||
if cls in CONCRETE_SPECIFIERS:
|
||||
return super(ArchiveSpecifierMeta, cls).__call__(specifier_str, *args, **kwargs)
|
||||
|
||||
for specifier_type in CONCRETE_SPECIFIERS:
|
||||
if specifier_type.acceptable_specifier(specifier_str):
|
||||
return specifier_type(specifier_str)
|
||||
|
||||
raise ValueError("No specifier matched {}".format(specifier_str))
|
||||
|
||||
@property
|
||||
def concrete(self):
|
||||
return not self.__dict__.get("ABSTRACT", False)
|
||||
|
||||
class ArchiveSpecifier(object):
|
||||
__metaclass__ = ArchiveSpecifierMeta
|
||||
ABSTRACT = True
|
||||
|
||||
class OrdinalArchiveSpecifier(ArchiveSpecifier):
|
||||
@classmethod
|
||||
def acceptable_specifier(cls, specifier_str):
|
||||
try:
|
||||
value = int(specifier_str)
|
||||
except ValueError:
|
||||
return False
|
||||
return value < 1000000000
|
||||
|
||||
def __init__(self, specifier_str):
|
||||
self.ordinal = int(specifier_str)
|
||||
|
||||
def evaluate(self, archive, ordinal):
|
||||
return ordinal == self.ordinal
|
||||
|
||||
class TimestampArchiveSpecifier(ArchiveSpecifier):
|
||||
@classmethod
|
||||
def acceptable_specifier(cls, specifier_str):
|
||||
try:
|
||||
value = float(specifier_str)
|
||||
except ValueError:
|
||||
return False
|
||||
has_dot = "." in specifier_str
|
||||
return value > 1000000000 or has_dot
|
||||
|
||||
def __init__(self, specifier_str):
|
||||
self.timestamp = float(specifier_str)
|
||||
|
||||
def evaluate(self, archive, ordinal):
|
||||
return archive.timestamp == self.timestamp
|
|
@ -2,6 +2,8 @@
|
|||
|
||||
import pkgutil
|
||||
import inspect
|
||||
import datetime
|
||||
import dateutil.tz
|
||||
|
||||
from . import error
|
||||
|
||||
|
@ -39,5 +41,12 @@ class BackupBackend(object):
|
|||
def __str__(self):
|
||||
return "{}: {}".format(self.__class__.__name__, self.name)
|
||||
|
||||
class Archive(object):
|
||||
@property
|
||||
def datetime(self):
|
||||
dt = datetime.datetime.fromtimestamp(self.timestamp)
|
||||
dt = dt.replace(tzinfo=dateutil.tz.tzlocal())
|
||||
return dt
|
||||
|
||||
def load_backend_types():
|
||||
from . import backup_backends
|
||||
|
|
|
@ -6,6 +6,8 @@ import shutil
|
|||
import errno
|
||||
import subprocess
|
||||
import hashlib
|
||||
import datetime
|
||||
import dateutil.tz
|
||||
import time
|
||||
import re
|
||||
|
||||
|
@ -19,10 +21,28 @@ def backup_instance_regex(identifier, name):
|
|||
r"^(?P<identifier>{})-(?P<timestamp>\d+(.\d+)?)-(?P<name>{})$"
|
||||
.format(re.escape(identifier), re.escape(name)))
|
||||
|
||||
class TarsnapArchive(object):
|
||||
def __init__(self, backend, time):
|
||||
class TarsnapArchive(backend_types.Archive):
|
||||
@property
|
||||
def logger(self):
|
||||
return package_logger().getChild("tarsnap_archive")
|
||||
|
||||
def __init__(self, backend, timestamp, fullname):
|
||||
self.fullname = fullname
|
||||
self.backend = backend
|
||||
self.time = time
|
||||
self.timestamp = timestamp
|
||||
|
||||
def restore(self, destination):
|
||||
argv = [TARSNAP_PATH, "-C", destination, "-x", "-f", self.fullname]
|
||||
proc = subprocess.Popen(argv, stderr=subprocess.STDOUT, stdout=subprocess.PIPE)
|
||||
proc_logger = self.logger.getChild("tarsnap_output")
|
||||
for line in proc.stdout:
|
||||
proc_logger.info(line.strip())
|
||||
code = proc.wait()
|
||||
if code != 0:
|
||||
self.logger.error("Tarsnap invocation failed with exit code {}".format(code))
|
||||
return False
|
||||
return True
|
||||
|
||||
|
||||
class TarsnapBackend(backend_types.BackupBackend):
|
||||
NAMES = {"tarsnap"}
|
||||
|
@ -98,7 +118,7 @@ class TarsnapBackend(backend_types.BackupBackend):
|
|||
m = regex.match(line)
|
||||
if m:
|
||||
ts = float(m.groupdict()["timestamp"])
|
||||
results.append(TarsnapArchive(self, ts))
|
||||
results.append(TarsnapArchive(self, ts, m.group()))
|
||||
|
||||
if proc.wait() != 0:
|
||||
self.logger.error("Tarsnap invocation failed with exit code {}".format(proc.returncode))
|
||||
|
|
|
@ -139,10 +139,17 @@ class ConfiguredBackup(object):
|
|||
success = success and backend.perform(self.paths, self.name)
|
||||
return success
|
||||
|
||||
def get_all_archives(self):
|
||||
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 self.backends:
|
||||
for backend in backends:
|
||||
pairs.append(
|
||||
[backend, backend.existing_archives_for_name(self.name)])
|
||||
|
||||
|
@ -216,18 +223,30 @@ class Config(object):
|
|||
parser.add_argument("-q", "--quiet", action="store_true",
|
||||
help="Be quiet on logging to stdout/stderr")
|
||||
subparsers = parser.add_subparsers()
|
||||
|
||||
parser_backup = subparsers.add_parser("backup")
|
||||
parser_backup.set_defaults(verb="backup")
|
||||
|
||||
parser_list = subparsers.add_parser("list")
|
||||
parser_list.set_defaults(verb="list")
|
||||
parser_list.add_argument("--before", dest="before", default=None,
|
||||
type=parse_simple_date)
|
||||
parser_list.add_argument("--after", dest="after", default=None,
|
||||
type=parse_simple_date)
|
||||
|
||||
parser_restore = subparsers.add_parser("restore")
|
||||
parser_restore.set_defaults(verb="restore")
|
||||
parser_restore.add_argument("backup", metavar="BACKUPNAME", type=str)
|
||||
parser_restore.add_argument("backend", metavar="BACKENDNAME", type=str)
|
||||
parser_restore.add_argument("archive_spec", metavar="SPEC", type=str)
|
||||
parser_restore.add_argument("destination", metavar="DEST", type=str)
|
||||
|
||||
parser_list_backups = subparsers.add_parser("list-configured-backups")
|
||||
parser_list_backups.set_defaults(verb="list-configured-backups")
|
||||
|
||||
parser_list_backends = subparsers.add_parser("list-backends")
|
||||
parser_list_backends.set_defaults(verb="list-backends")
|
||||
|
||||
return parser.parse_args(self.argv)
|
||||
|
||||
def default_state(self):
|
||||
|
|
Loading…
Reference in New Issue