X-Git-Url: https://git.sthu.org/?p=sitarba.git;a=blobdiff_plain;f=sitarba;h=c8f18e5dc58a198c3cc9844c303c7e1a5c870727;hp=e57e2508500fe22a966357413cb6d398c73a9f20;hb=6f010747e7b87c12af58b2b1254854e20217c037;hpb=207d81ff3a5012639ae13be8470856b1a1ddc7ac diff --git a/sitarba b/sitarba index e57e250..c8f18e5 100755 --- a/sitarba +++ b/sitarba @@ -15,6 +15,11 @@ import logging Modes = ["full", "incr", "diff"] + +class Options: + dryrun = False + + class Epoch: units = { @@ -45,7 +50,7 @@ class Epoch: def isRipe(self, oldest, now): - if self.unit==None: + if self.unit == None: return True delta = now-oldest @@ -115,16 +120,16 @@ class Backup: @staticmethod def fromDirName(dirname): - [strdate, strtime, epoch, mode] = dirname.split("-") + [strdate, strtime, epoch, mode] = dirname.split("-") - if not mode in Modes: - raise ValueError("Invalid mode: " + mode) + if not mode in Modes: + raise ValueError("Invalid mode: " + mode) - date = datetime.datetime(int(strdate[0:4]), - int(strdate[4:6]), int(strdate[6:8]),\ - int(strtime[0:2]), int(strtime[2:4])) + date = datetime.datetime(int(strdate[0:4]), + int(strdate[4:6]), int(strdate[6:8]),\ + int(strtime[0:2]), int(strtime[2:4])) - return Backup(date, epoch, mode) + return Backup(date, epoch, mode) def __repr__(self): return "[date: " + self.date.ctime() + \ @@ -178,7 +183,7 @@ class Config: self.sets = [] self.checksum = None self.lastchecksum = None - self.epochs = Epochs = { "sporadic" : Epoch() } + self.epochs = { "sporadic" : Epoch() } def __repr__(self): @@ -201,15 +206,15 @@ class Config: def _read_global(self, config, sec): for opt in config.options(sec): - if opt=="backupdir": + if opt == "backupdir": self.backupdir = config.get(sec, opt) if not os.path.isdir(self.backupdir): raise Config.ReadError("Backupdir '{0}' does not exist.".format(self.backupdir)) - elif opt=="format": + elif opt == "format": self.format = config.get(sec, opt) if not self.format in Config.formats: raise Config.ReadError("Invalid 'format' given.") - elif opt=="tarbin": + elif opt == "tarbin": self.tarbin = config.get(sec, opt) if not os.path.isfile(self.tarbin): raise Config.ReadError("Tar binary '{0}' does not exist.".format(self.tarbin)) @@ -224,11 +229,15 @@ class Config: e = Epoch() if name in self.epochs: raise Config.ReadError("Epoch '{0}' already defined.".format(name)) + p = re.compile(r'^\w+$') + if not p.match(name): + raise Config.ReadError("Epoch name '{0}' does not only " + \ + "comprise alphanumeric characters.".format(name)) if name in Epoch.units: e.unit = name for opt in config.options(sec): - if opt=="numkeeps": + if opt == "numkeeps": try: e.numkeeps = int(config.getint(sec, opt)) except ValueError: @@ -236,16 +245,16 @@ class Config: if e.numkeeps <= 0: raise Config.ReadError("Non-positive numkeeps '{0}' given.".format(e.numkeeps)) - elif opt=="mode": + elif opt == "mode": e.mode = config.get(sec, opt) if not e.mode in Modes: raise Config.ReadError("Invalid mode '{0}'.".format(e.mode)) - elif opt=="timespan": + elif opt == "timespan": if name in Epoch.units: raise Config.ReadError("The time delta of a standard epoch " + \ "is not supposed to be redefined. ") - td = config.get(sec,opt) + td = config.get(sec, opt) try: mult, unit = Epoch.parseTimedelta(td) e.unit = unit @@ -267,6 +276,11 @@ class Config: def _read_set(self, config, sec): name = sec[4:].strip() + p = re.compile(r'^\w+$') + if not p.match(name): + raise Config.ReadError("Set name '{0}' does not only " + \ + "comprise alphanumeric characters.".format(name)) + dirs = [] excludes = [] @@ -274,7 +288,7 @@ class Config: if opt.startswith("dir"): dirs += [config.get(sec, opt)] elif opt.startswith("exclude"): - excludes += [config.get(sec,opt)] + excludes += [config.get(sec, opt)] else: raise Config.ReadError("Unknown option '" + opt + "'.") @@ -296,7 +310,7 @@ class Config: for sec in config.sections(): - if sec=="global": + if sec == "global": self._read_global(config, sec) elif sec.startswith("epoch "): @@ -347,7 +361,7 @@ class BackupManager: return [ d for d in dirs if os.path.isdir(os.path.join(basedir, d)) ] - def listOldBackups(self): + def listExistingBackups(self): """Returns a list of old backups.""" backups = [] @@ -369,7 +383,7 @@ class BackupManager: continue # Get backups of that epoch - byepoch = list(sorted( [ b for b in backups if b.epoch==e], \ + byepoch = list(sorted( [ b for b in backups if b.epoch == e], \ key=lambda b: b.date)) # If there are any, determine the latest @@ -393,6 +407,10 @@ class BackupManager: fsfn = os.path.join(targetdir, fileset.name) + "." + self.conf.format taropts = [] + # Tar is verbose is sitarba is verbose + if LogConf.con.level <= logging.DEBUG: + taropts += ["--verbose"] + # Add the since date, if given if since != None: taropts += ["-N", since.strftime("%Y-%m-%d %H:%M:%S")] @@ -405,11 +423,12 @@ class BackupManager: for pat in fileset.excludes: taropts += ["--exclude", pat] + # Adding directories to backup taropts += ["-C", "/"] + [ "./" + d.lstrip("/") for d in fileset.dirs] # Launch the tar process - tarargs = [self.conf.tarbin] + ["-cpvaf", fsfn] + taropts + tarargs = [self.conf.tarbin] + ["-cpaf", fsfn] + taropts logfile.debug("tar call: " + " ".join(tarargs)) tarp = subprocess.Popen( tarargs, bufsize=-1, \ stdout=subprocess.PIPE, stderr=subprocess.PIPE ) @@ -423,7 +442,7 @@ class BackupManager: # Read stdout and stderr of tarp errmsg = b"" while tarp.poll() == None: - rd,wr,ex = select.select([tarp.stdout, tarp.stderr], [], [], 0.05) + rd, wr, ex = select.select([tarp.stdout, tarp.stderr], [], [], 0.05) if tarp.stdout in rd: logging.debug( tarp.stdout.readline()[:-1].decode() ) if tarp.stderr in rd: @@ -449,7 +468,7 @@ class BackupManager: then use mode for given epoch. Use given mode otherwise.""" now = datetime.datetime.now() - oldbackups = self.listOldBackups() + oldbackups = self.listExistingBackups() # Get epoch of backup if epoch == None: @@ -466,7 +485,7 @@ class BackupManager: oldfullbackups = [ b for b in oldbackups if b.mode == "full" ] # No old full backups existing - if mode != "full" and len(oldfullbackups)==0: + if mode != "full" and len(oldfullbackups) == 0: logging.info("No full backups existing. Making a full backup.") # Checksum changed -> self.config file changed @@ -484,6 +503,9 @@ class BackupManager: if since != None: logging.debug("Making backup relative to " + since.ctime()) + if Options.dryrun: + return + yesno = self.ask_user_yesno("Proceed? [Y, n] ") if yesno == "n": return @@ -493,12 +515,12 @@ class BackupManager: dirname = Backup.getDirName(now, epoch, mode) tmpdirname = dirname + ("-%x" % (random.random()*2e16) ) targetdir = os.path.join(basedir, tmpdirname) - os.mkdir( targetdir ) + os.mkdir(targetdir) # Add file logger logfile = logging.getLogger("backuplog") - fil = logging.FileHandler( os.path.join(targetdir, "log") ) + fil = logging.FileHandler(os.path.join(targetdir, "log")) fil.setLevel(logging.DEBUG) logfile.addHandler(fil) @@ -526,14 +548,16 @@ class BackupManager: """Prune old backup files""" allDirs = sorted(self.listAllDirs()) - # Collect all directories not matching backup name + # Collect all directories that are removed removeDirs = [ d for d in allDirs if not Backup.isBackupDir(d) ] - # Get all directories which are kept - backups = self.listOldBackups() - keepdirs = [] + # Get all backups + backups = self.listExistingBackups() + # Group backups by epoch and sort them by age byepoch = { e : list(sorted( [ b for b in backups if b.epoch == e ], \ - key=lambda b : b.date, reverse=True)) for e in self.conf.getRealEpochsSorted() } + key=lambda b : b.date, reverse=True)) \ + for e in self.conf.getRealEpochsSorted() } + # If we have too many backups of a specific epoch --> add them to remove list for e in byepoch: epoch = self.conf.epochs[e] old = byepoch[e][epoch.numkeeps:] @@ -563,6 +587,9 @@ class BackupManager: logging.info("No stale/outdated entries to remove.") return + if Options.dryrun: + return + basedir = self.conf.backupdir yesno = self.ask_user_yesno("Remove entries marked by '*'? [y, N] ") if yesno == "y": @@ -570,7 +597,7 @@ class BackupManager: try: shutil.rmtree(os.path.join(basedir, d)) except OSError as e: - logging.error("Error when removing '%s': %s" % (d,e.strerror) ) + logging.error("Error when removing '%s': %s" % (d, e.strerror) ) def ask_user_yesno(self, question): @@ -583,7 +610,7 @@ class BackupManager: def printUsage(): """Print --help text""" - print("shbackup - a simple backup solution.") + print("sitarba - a simple backup solution.") print("") print("Usage:") print(" " + sys.argv[0] + " {options} [cmd]") @@ -597,10 +624,11 @@ def printUsage(): print("Options:") print(" -h, --help print this usage text") print(" -c, --conf FILE use given configuration file") - print(" default: /etc/shbackup.conf") + print(" default: /etc/sitarba.conf") print(" -e, --epoch EPOCH force to create backup for given epoch, which") print(" can be 'sporadic' or one of the configured epochs") print(" -m, --mode MODE override mode: full, diff, or incr") + print(" -n, --dry-run don't do anything, just tell what would be done") print(" -v, --verbose be more verbose and interact with user") print(" --verbosity LEVEL set verbosity to LEVEL, which can be") print(" error, warning, info, debug") @@ -630,7 +658,7 @@ if __name__ == "__main__": LogConf.setup() - conffn = "/etc/shbackup.conf" + conffn = "/etc/sitarba.conf" cmd = "list" mode = None epoch = None @@ -649,7 +677,7 @@ if __name__ == "__main__": conffn = sys.argv[i] elif opt in ["-V", "--version"]: - print("shbackup " + __version__) + print("sitarba " + __version__) exit(0) elif opt in ["-v", "--verbose"]: @@ -670,6 +698,9 @@ if __name__ == "__main__": logging.error("Unknown mode '" + mode + "'.") exit(1) + elif opt in ["-n", "--dry-run"]: + Options.dryrun = True + elif opt in ["-e", "--epoch"]: i += 1 epoch = sys.argv[i] @@ -686,7 +717,7 @@ if __name__ == "__main__": logging.debug("Config: " + str(man.conf)) - if epoch!=None and not epoch in man.conf.epochs.keys(): + if epoch != None and not epoch in man.conf.epochs.keys(): logging.error("Unknown epoch '" + epoch + "'.") exit(1) @@ -694,7 +725,7 @@ if __name__ == "__main__": man.backup(epoch, mode) if cmd == "list": - for b in sorted(man.listOldBackups(), key=lambda b: b.date): + for b in sorted(man.listExistingBackups(), key=lambda b: b.date): print(b.colAlignedString()) if cmd == "prune":