#!/usr/bin/python3
"""Stefan Huber's simplistic backup solution."""
+__version__ = "0.1"
+__author__ = "Stefan Huber"
+
import datetime
import os, shutil, sys
import configparser
import hashlib
import subprocess
import random, re
+import logging
Mode = ["full", "incr", "diff"]
class BackupManager:
"""List and create backups"""
- def __init__(self, conffn, alwaysyes):
+ def __init__(self, conffn):
self.conf = Config()
- self.alwaysyes = alwaysyes
self.conf.read(conffn)
- def backupFileSet(self, fileset, targetdir, log, since=None):
+ def backupFileSet(self, fileset, targetdir, since=None):
"""Create an archive for given fileset at given target directory."""
- print("Running file set: " + fileset.name)
+ logger = logging.getLogger('backup')
+
+ logger.info("Running file set: " + fileset.name)
tarpath = "/bin/tar"
fsfn = os.path.join(targetdir, fileset.name) + "." + self.conf.format
taropts += ["--exclude", pat]
tarargs = [tarpath] + taropts + ["-f", fsfn] + fileset.dirs
- #print("tarargs: ", tarargs)
- print("tar call: " + " ".join(tarargs), file=log)
- tarp = subprocess.Popen( tarargs, stderr=subprocess.PIPE )
+ logger.debug("tar call: " + " ".join(tarargs))
+ tarp = subprocess.Popen( tarargs, bufsize=-1, \
+ stdout=subprocess.PIPE, stderr=subprocess.PIPE )
+
+ # Output stdout of tar
+ while tarp.poll() == None:
+ l = tarp.stdout.readline()
+ if l != "":
+ logging.debug(l.decode().rstrip())
+
+ # Output remaining output of tar
+ for l in tarp.stdout.readlines():
+ logging.debug(l.decode().rstrip())
rett = tarp.wait()
if rett != 0:
- sys.stderr.write( tarp.stderr.read() )
- msg = tarpath + " returned with exit status " + str(rett) + "."
- print(msg)
- print(msg, log)
+ for l in tarp.stderr.readlines():
+ logger.error( l.decode().strip().rstrip() )
+ sys.stderr.write( tarp.stderr.read().decode() )
+ logger.error(tarpath + " returned with exit status " + str(rett) + ".")
def backup(self, epoch=None, mode=None):
if epoch == None:
epoch = self.getDesiredEpoch(oldbackups, now)
if epoch == None:
- print("No backup planned.")
+ logging.info("No backup planned.")
return
# Get mode of backup
if mode == None:
mode = self.conf.epochmodes[epoch]
- print("Making a backup. Epoch: " + epoch + ", mode: " + mode)
+ logging.info("Making a backup. Epoch: " + epoch + ", mode: " + mode)
oldfullbackups = [ b for b in oldbackups if b.mode == "full" ]
# No old full backups existing
if mode != "full" and len(oldfullbackups)==0:
- print("No full backups existing. Making a full backup.")
+ logging.info("No full backups existing. Making a full backup.")
# Checksum changed -> self.config file changed
- if self.conf.checksum != self.conf.lastchecksum:
- print("Config file changed since last time.")
- if mode != "full":
- print("** Warning: full backup recommended!")
+ if self.conf.checksum != self.conf.lastchecksum and mode != "full":
+ logging.warning("Full backup recommended as config file has changed.")
# If we have a full backup, we backup everything
since = sorted(oldbackups, key=lambda b: b.date)[-1].date
if since != None:
- print("Making backup relative to ", since.ctime())
+ logging.debug("Making backup relative to " + since.ctime())
yesno = self.ask_user_yesno("Proceed? [Y, n] ")
if yesno == "n":
os.mkdir( targetdir )
- log = open(os.path.join(targetdir, "log.log"), 'w')
- print("Started: " + now.ctime(), file=log)
+ logger = logging.getLogger('backup')
+ ch = logging.FileHandler( os.path.join(targetdir, "log") )
+ ch.setLevel(logging.INFO)
+ logger.addHandler(ch)
+ logger.info("Started: " + now.ctime())
# Backup all file sets
for s in self.conf.sets:
- self.backupFileSet(s, targetdir, log, since)
+ self.backupFileSet(s, targetdir, since)
- print("Stopped: " + datetime.datetime.now().ctime(), file=log)
- log.close()
+ logger.info("Stopped: " + datetime.datetime.now().ctime())
# Rename backup directory to final name
os.rename( targetdir, os.path.join(basedir, dirname) )
def prune(self):
"""Prune old backup files"""
- allDirs = self.listAllDirs()
+ allDirs = sorted(self.listAllDirs())
# Collect all directories not matching backup name
removeDirs = [ d for d in allDirs if not Backup.isBackupDir(d) ]
removeDirs += [ Backup.getDirName(b.date, b.epoch, b.mode) for b in old]
- print("List of stale/outdated entries:")
+ logging.info("List of stale/outdated entries:")
for d in allDirs:
+ msg = ""
if d in removeDirs:
- print("[*] ", end="")
+ msg = "[*] "
else:
- print("[ ] ", end="")
+ msg = "[ ] "
if Backup.isBackupDir(d):
- print( Backup.fromDirName(d).colAlignedString())
+ msg += Backup.fromDirName(d).colAlignedString()
else:
- print(d)
+ msg += d
+
+ logging.info(msg)
# Check that dirs to be removed is in list of all dirs
for d in removeDirs:
assert( d in allDirs )
if len(removeDirs) == 0:
- print("No stale/outdated entries to remove.")
+ logging.info("No stale/outdated entries to remove.")
return
basedir = self.conf.directory
shutil.rmtree(os.path.join(basedir, d))
def ask_user_yesno(self, question):
- if self.alwaysyes:
- print(question + " y")
- return "y"
- else:
+ if logging.getLogger().isEnabledFor(logging.INFO):
return input(question)
+ else:
+ return "y"
def printUsage():
print(" -e, --epoch <epoch> force to create backup for given epoch:")
print(" year, month, week, day, hour, sporadic")
print(" -m, --mode <mode> override mode: full, diff, or incr")
- print(" -y, --yes always assume 'yes' when user is asked")
+ print(" -v, --verbose be more verbose and interact with user")
+ print(" --verbosity LEVEL set verbosity to LEVEL, which can be")
+ print(" warning, info, debug")
+ print(" -V, --version print version info")
if __name__ == "__main__":
+ logging.basicConfig(format='%(message)s')
conffn = "/etc/shbackup.conf"
cmd = "list"
mode = None
epoch = None
- yes = False
i = 0
while i < len(sys.argv)-1:
i += 1
conffn = sys.argv[i]
- elif opt in ["-y", "--yes"]:
- yes = True
+ elif opt in ["-V", "--version"]:
+ print("shbackup " + __version__)
+ exit(0)
+
+ elif opt in ["-v", "--verbose"]:
+ logging.getLogger().setLevel(logging.INFO)
+
+ elif opt in ["--verbosity"]:
+ i += 1
+ level = sys.argv[i]
+ numlevel = getattr(logging, level.upper(), None)
+ if not isinstance(numlevel, int):
+ raise ValueError('Invalid verbosity level: %s' % level)
+ logging.getLogger().setLevel(numlevel)
elif opt in ["-m", "--mode"]:
i += 1
mode = sys.argv[i]
if not mode in Mode:
- print("Unknown mode '" + mode + "'.")
+ logging.error("Unknown mode '" + mode + "'.")
exit(1)
elif opt in ["-e", "--epoch"]:
i += 1
epoch = sys.argv[i]
if not epoch in Epoch:
- print("Unknown epoch '" + epoch + "'.")
+ logging.error("Unknown epoch '" + epoch + "'.")
exit(1)
cmd = opt
else:
- print("Unknown option: " + opt)
+ logging.error("Unknown option: " + opt)
exit(1)
try:
- man = BackupManager(conffn, yes)
+ man = BackupManager(conffn)
if cmd == "backup":
man.backup(epoch, mode)
man.prune()
except (Config.ReadError, configparser.DuplicateOptionError) as e:
- print("Error reading config file: " + e.message)
+ logging.error("Error reading config file: " + e.message)