166 lines
5.7 KiB
Python
166 lines
5.7 KiB
Python
#!/usr/bin/env python3
|
|
|
|
import os
|
|
import tempfile
|
|
import shutil
|
|
import errno
|
|
import subprocess
|
|
import hashlib
|
|
import datetime
|
|
import dateutil.tz
|
|
import time
|
|
import re
|
|
import io
|
|
|
|
from .. import backend_types
|
|
from .. import package_logger
|
|
|
|
TARSNAP_PATH = "/usr/local/bin/tarsnap"
|
|
|
|
def backup_instance_regex(identifier, name):
|
|
return re.compile(
|
|
r"^(?P<identifier>{})-(?P<timestamp>\d+(.\d+)?)-(?P<name>{})$"
|
|
.format(re.escape(identifier), re.escape(name)))
|
|
|
|
class TarsnapArchive(backend_types.Archive):
|
|
@property
|
|
def logger(self):
|
|
return package_logger().getChild("tarsnap_archive")
|
|
|
|
def __init__(self, backend, timestamp, fullname, backup_name):
|
|
self.fullname = fullname
|
|
self.backend = backend
|
|
self.timestamp = timestamp
|
|
self.backup_name = backup_name
|
|
|
|
def _invoke_tarsnap(self, argv):
|
|
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.decode('utf-8').strip())
|
|
code = proc.wait()
|
|
if code != 0:
|
|
self.logger.error("Tarsnap invocation failed with exit code {}".format(code))
|
|
return False
|
|
return True
|
|
|
|
def restore(self, destination):
|
|
argv = [TARSNAP_PATH, "-C", destination, "-x", "-f", self.fullname]
|
|
return self._invoke_tarsnap(argv)
|
|
|
|
def destroy(self):
|
|
self.logger.info("destroying {}".format(self))
|
|
argv = [TARSNAP_PATH, "-d", "-f", self.fullname]
|
|
return self._invoke_tarsnap(argv)
|
|
|
|
class _TarsnapPrimedListToken(object):
|
|
def __init__(self, tarsnap_output):
|
|
self.tarsnap_output_text = tarsnap_output.decode('utf-8')
|
|
|
|
def iterlines(self):
|
|
return io.StringIO(self.tarsnap_output_text)
|
|
|
|
|
|
class TarsnapBackend(backend_types.BackupBackend):
|
|
NAMES = {"tarsnap"}
|
|
|
|
@property
|
|
def logger(self):
|
|
return package_logger().getChild("tarsnap_backend")
|
|
|
|
def __init__(self, config):
|
|
super(TarsnapBackend, self).__init__(config)
|
|
self.keyfile = config.pop("keyfile", None)
|
|
self.host = config.pop("host", None)
|
|
|
|
def __str__(self):
|
|
addendum = " ({} with {})".format(self.host, self.keyfile)
|
|
return super(TarsnapBackend, self).__str__() + addendum
|
|
|
|
def create_backup_identifier(self, backup_name):
|
|
ctx = hashlib.sha1()
|
|
ctx.update(self.name.encode("utf-8"))
|
|
ctx.update(backup_name.encode("utf-8"))
|
|
return ctx.hexdigest()
|
|
|
|
def create_backup_instance_name(self, backup_name, timestamp):
|
|
unixtime = time.mktime(timestamp.timetuple())
|
|
return "{}-{}-{}".format(self.create_backup_identifier(backup_name),
|
|
unixtime, backup_name)
|
|
|
|
def perform(self, paths, backup_name, now_timestamp):
|
|
backup_instance_name = self.create_backup_instance_name(backup_name,
|
|
now_timestamp)
|
|
self.logger.info("Creating backup \"{}\": {}"
|
|
.format(backup_instance_name, ", ".join(paths)))
|
|
tmpdir = tempfile.mkdtemp()
|
|
try:
|
|
for path, name in paths.items():
|
|
os.symlink(path, os.path.join(tmpdir, name))
|
|
argv = [TARSNAP_PATH, "-C", tmpdir, "-H", "-cf", backup_instance_name]
|
|
if self.keyfile is not None:
|
|
argv += ["--keyfile", self.keyfile]
|
|
argv += list(paths.values())
|
|
self.logger.info("Invoking tarsnap: {}".format(argv))
|
|
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.decode('utf-8').strip())
|
|
code = proc.wait()
|
|
if code != 0:
|
|
self.logger.error("Tarsnap invocation failed with exit code {}".format(code))
|
|
return False
|
|
else:
|
|
return True
|
|
finally:
|
|
for path, name in paths.items():
|
|
path = os.path.join(tmpdir, name)
|
|
try:
|
|
os.unlink(path)
|
|
except OSError as e:
|
|
if e.errno == errno.ENOENT:
|
|
pass
|
|
os.rmdir(tmpdir)
|
|
|
|
def existing_archives_for_name(self, backup_name, primed_list_token=None):
|
|
argv = [TARSNAP_PATH, "--list-archives"]
|
|
if self.keyfile is not None:
|
|
argv += ["--keyfile", self.keyfile]
|
|
|
|
proc = None
|
|
f = None
|
|
if primed_list_token is not None:
|
|
f = primed_list_token.iterlines()
|
|
else:
|
|
proc = subprocess.Popen(argv, stdout=subprocess.PIPE)
|
|
f = io.TextIOWrapper(proc.stdout, 'utf-8')
|
|
|
|
identifier = self.create_backup_identifier(backup_name)
|
|
regex = backup_instance_regex(identifier, backup_name)
|
|
|
|
results = []
|
|
for line in f:
|
|
m = regex.match(line)
|
|
if m:
|
|
ts = float(m.groupdict()["timestamp"])
|
|
results.append(TarsnapArchive(self, ts, m.group(), backup_name))
|
|
|
|
if proc is not None and proc.wait() != 0:
|
|
self.logger.error("Tarsnap invocation failed with exit code {}".format(proc.returncode))
|
|
|
|
return results
|
|
|
|
def get_primed_list_token(self):
|
|
argv = [TARSNAP_PATH, "--list-archives"]
|
|
if self.keyfile is not None:
|
|
argv += ["--keyfile", self.keyfile]
|
|
|
|
proc = subprocess.Popen(argv, stdout=subprocess.PIPE)
|
|
|
|
token = _TarsnapPrimedListToken(proc.stdout.read())
|
|
|
|
if proc.wait() != 0:
|
|
self.logger.error("Tarsnap invocation failed with exit code {}".format(proc.returncode))
|
|
|
|
return token
|