#!/usr/bin/env python ## @file csback2cron.py # @brief Generate a crontab file for the csback software suite. # # ----------------------------------------------------------------------------- # # $Id$ # @author Daniel Armbruster # \date 11/09/2011 # # Purpose: Generate a crontab file for the csback toolkit using a configuration # file. # # ---- # This file is part of csback. # # csback is free software: you can redistribute it and/or modify # it under the terms of the GNU General Public License as published by # the Free Software Foundation, either version 3 of the License, or # (at your option) any later version. # # csback is distributed in the hope that it will be useful, # but WITHOUT ANY WARRANTY; without even the implied warranty of # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the # GNU General Public License for more details. # # You should have received a copy of the GNU General Public License # along with csback. If not, see . # ---- # # Copyright (c) 2011-2012 by Daniel Armbruster # # REVISIONS and CHANGES # 11/09/2011 V0.1 Daniel Armbruster # 14/09/2011 V0.8 Now multiple exclude flags in CONFIGFILE are supported # 15/09/2011 V0.8.1 Added several improvements # 08/01/2012 V0.9 New configuration file syntax. Complete rework. Make use # of python's configparser module. # 10/01/2012 V0.9.1 added copy section for single rsync command settings # # ============================================================================= """ Convert a csback configuration file to a usual crontab file. """ import sys import getopt import os from datetime import datetime if sys.version_info >= (2,) and sys.version_info < (3,): from ConfigParser import ConfigParser from ConfigParser import ParsingError from ConfigParser import NoSectionError from ConfigParser import NoOptionError elif sys.version_info >= (3,): from configparser import ConfigParser from configparser import ParsingError from configparser import NoSectionError from configparser import NoOptionError else: sys.stderr.write("csback2cron: Incompatible python version.\n") __version__ = "V0.9.1" __subversion__ = "$Id$" __license__ = "GPLv2" __author__ = "Daniel Armbruster" __copyright__ = "Copyright (c) 2012 by Daniel Armbruster" DEFAULTPATH = os.path.expanduser("~/.csback/csbackrc") # ----------------------------------------------------------------------------- class Error(Exception): def __init__(self, msg): self.msg = msg def display(self): sys.stderr.write("csback2cron (ERROR): " + self.msg + "\n") class Usage(Error): def display(self): usage_text = "Version: "+__version__+"\nLicense: "+__license__+ \ "\n"+__subversion__+"\nAuthor: "+__author__+ """ Usage: csback2cron [-v|--verbose] [-o|--overwrite] [-i|--infile ARG] or: csback2cron -h|--help Note: If [-i|--infile CONFIGFILE] isn't passed as a argument, csback2cron uses ~/.csback/csbackrc as default.\n""" sys.stderr.write("csback2cron: " + self.msg + "\n") sys.stderr.write(usage_text) # ----------------------------------------------------------------------------- def help(): """ Printing helptext to stdout. """ help_text = "Version: "+__version__+"\nLicense: "+__license__+"\n"+ \ __subversion__+"\nAuthor: "+__author__+""" Usage: csback2cron [-v|--verbose] [-o|--overwrite] [-i|--infile CONFIGFILE] or: csback2cron -h|--help ------------------------------------------------------------------------------- -v|--verbose Be verbose. -h|--help Display this help. -i|--infile ARG If ARG is the path to csbackrc - the configuration file for the csback crontab generation. If this argument wasn't passed csback2cron assumes ~/.csback/csbackrc as default path to the configuration file. -o|--overwrite Overwrite already existing crontab. Outputfilename of the generated crontab. ------------------------------------------------------------------------------- Notice that csback2cron does not check any logical values e.g. pathes and/or cron expressions.\n""" sys.stdout.write(help_text) # ----------------------------------------------------------------------------- class Converter(): """ Abstract converter base class for section dictionaries. """ def __init__(self, sectionDict): self.sectionDict = sectionDict self.line = '' self.addCronExpr = True def convert(self): """ Abstract function """ self.convertDict() def convertDict(self): raise NotImplementedError("function must be defined!") def __str__(self): """ String representation of converted section dictionary. """ return "{0}".format(self.line) class MailConverter(Converter): """ Class which implements a mail section dictionary converter """ def convertDict(self): self.line = self.sectionDict['cronexpr']+' csbackmail' if self.sectionDict['logging']: self.line += ' -l' self.line += ' -H '+self.sectionDict['host']+' -P '+ \ self.sectionDict['port']+' -u '+self.sectionDict['username']+' -p ' \ + self.sectionDict['password']+' -s '+self.sectionDict['sender'] for receiver in self.sectionDict['receivers']: self.line += ' -r ' + receiver class TestConverter(Converter): """ Class which implements a test section dictionary converter """ def convertDict(self): if self.addCronExpr: self.line = self.sectionDict['cronexpr']+' ' self.line += 'csbackchk -L' if self.sectionDict['logging']: self.line += ' -l' if not self.sectionDict['recursive']: self.line += ' -R' if self.sectionDict['followlinks']: self.line += ' -f' if self.sectionDict['tolerant']: self.line += ' -t' for regex in self.sectionDict['exclude']: self.line += ' -e "'+regex.strip()+'"' self.line += ' '+self.sectionDict['srcdir']+' '+self.sectionDict['dir'] class BackupConverter(Converter): """ Class which implements a backup section dictionary converter """ def convertDict(self): self.line = self.sectionDict['cronexpr'] if self.sectionDict['copy']: if self.sectionDict['recursive']: self.sectionDict['copy-special'].append('--recursive') copyConv = CopyConverter({'cronexpr': self.sectionDict['cronexpr'], \ 'srcdir': self.sectionDict['srcdir'], \ 'targetdir': self.sectionDict['targetdir'], \ 'special': self.sectionDict['copy-special'], \ 'exclude': self.sectionDict['copy-exclude']}) copyConv.addCronExpr = False copyConv.convert() self.line += str(copyConv)+";" self.line += ' csbackgen -L' if self.sectionDict['logging']: self.line += ' -l' if not self.sectionDict['recursive']: self.line += ' -R' if self.sectionDict['followlinks']: self.line += ' -f' for regex in self.sectionDict['exclude']: self.line += ' -e "'+regex.strip()+'"' self.line += ' -t '+self.sectionDict['targetdir']+' '+ \ self.sectionDict['srcdir'] if self.sectionDict['test']: testConv = TestConverter({'cronexpr': self.sectionDict['cronexpr'], \ 'srcdir': self.sectionDict['targetdir'], \ 'dir': self.sectionDict['targetdir'], \ 'exclude': self.sectionDict['exclude'], \ 'recursive': self.sectionDict['recursive'], \ 'logging': self.sectionDict['logging'], \ 'followlinks': self.sectionDict['followlinks'], \ 'tolerant': self.sectionDict['tolerant']}) testConv.addCronExpr = False testConv.convert() self.line += '; '+str(testConv) class CopyConverter(Converter): """ Class which implements a copy section dictionary converter """ def convertDict(self): if self.addCronExpr: self.line = self.sectionDict['cronexpr']+' ' self.line +='rsync -q' for special in self.sectionDict['special']: self.line += ' '+special.strip() for regex in self.sectionDict['exclude']: self.line += " --exclude='"+regex.strip()+"'" self.line += ' '+self.sectionDict['srcdir']+' '+self.sectionDict['targetdir'] # ----------------------------------------------------------------------------- class Processor(): """ Processing class which is responsible for the processing of the csbackrc configuration file conversion. """ def __init__(self, configfile, crontabfile, verbose=False): self.configfile = configfile self.crontabfile = crontabfile self.config = ConfigParser(Processor.DEFAULTS) self.__verbose = verbose self.mail = {} self.copies = [] self.backups = [] self.tests = [] self.result = [] def read(self): """ Read the csback configuration file. """ if not os.path.isfile(self.configfile): raise Error("Given CONFIGFILE is not a regular file.") if 0 == os.stat(self.configfile).st_size: raise Error("Given CONFIGFILE is an empty file.") if self.__verbose: sys.stdout.write("csback2cron: Reading CONFIGFILE ... \n") try: self.config.read(self.configfile) except ParsingError as err: raise Error("{0}".format(err.message)) # fetch mail section if self.config.has_section('mail'): try: self.mail['cronexpr'] = self.config.get('mail', 'cronexpr').strip() self.mail['receivers'] = \ self.config.get('mail', 'receivers').split(',') self.mail['receivers'] = [add.strip() \ for add in self.mail['receivers']] self.mail['sender'] = self.config.get('mail', 'sender').strip() self.mail['host'] = self.config.get('mail', 'host').strip() self.mail['port'] = self.config.get('mail', 'port').strip() self.mail['username'] = self.config.get('mail', 'username').strip() self.mail['password'] = self.config.get('mail', 'password').strip() if self.config.has_option('mail', 'logging'): self.mail['logging'] = self.config.getboolean('mail', 'logging') else: self.mail['logging'] = False except NoOptionError as err: raise Error("{0}", err.message) except ValueError: raise Error("Argument error in [mail] section.") else: self.mail = {} # fetch copies section try: copyKeys = self.config.get('copies', 'keys') copyKeys = set(copyKeys.split(",")) copyKeys = ['copy_'+key.strip() for key in copyKeys] # fetch every copy section for key in copyKeys: if not self.config.has_section(key): raise Error("Section {0} in configfile not defined.".format(key)) copy = {'id': key} try: copy['cronexpr'] = self.config.get(key, 'cronexpr').strip() copy['srcdir'] = self.config.get(key, 'srcdir').strip() copy['targetdir'] = self.config.get(key, 'targetdir').strip() copy['exclude'] = [] if self.config.has_option(key, 'exclude'): copy['exclude'].extend(self.config.get( \ key, 'exclude', raw=1).split(", ")) copy['special'] = [] if self.config.has_option(key, 'specialcommands'): copy['special'].extend(self.config.get( \ key, 'specialcommands', raw=1).split(", ")) except NoOptionError as err: raise Error("{0}".format(err.message)) except ValueError: raise Error("Argument error in [{0}] section.".format(key)) else: self.copies.append(copy) except NoOptionError as err: raise Error("{0}".format(err.message)) except NoSectionError: self.copies = [] # fetch backups section try: backupKeys = self.config.get('backups', 'keys') backupKeys = set(backupKeys.split(",")) backupKeys = ['backup_'+key.strip() for key in backupKeys] # fetch every backup section for key in backupKeys: if not self.config.has_section(key): raise Error("Section {0} in configfile not defined.".format(key)) backup = {'id': key} try: backup['cronexpr'] = self.config.get(key, 'cronexpr').strip() backup['srcdir'] = self.config.get(key, 'srcdir').strip() backup['targetdir'] = self.config.get(key, 'targetdir').strip() backup['exclude'] = [] if self.config.has_option(key, 'exclude'): backup['exclude'] = self.config.get(key, 'exclude',raw=1).split(", ") backup['recursive'] = self.config.getboolean(key, 'recursive') backup['logging'] = self.config.getboolean(key, 'logging') backup['followlinks'] = self.config.getboolean(key, 'followlinks') backup['copy'] = self.config.getboolean(key, 'copy') backup['copy-exclude'] = [] backup['copy-special'] = [] if backup['copy']: if self.config.has_option(key, 'copy-exclude'): backup['copy-exclude'].extend(self.config.get( \ key, 'copy-exclude',raw=1).split(", ")) if self.config.has_option(key, 'copy-special'): backup['copy-special'].append(self.config.get( \ key, 'copy-special').strip()) backup['test'] = self.config.getboolean(key, 'test') backup['tolerant'] = self.config.getboolean(key, 'tolerant') except NoOptionError as err: raise Error("{0}".format(err.message)) except ValueError: raise Error("Argument error in [{0}] section.".format(key)) else: self.backups.append(backup) except NoOptionError as err: raise Error("{0}".format(err.message)) except NoSectionError: self.backups = [] # fetch tests section try: testKeys = self.config.get('tests', 'keys') testKeys = set(testKeys.split(",")) testKeys = ['test_'+key.strip() for key in testKeys] # fetch every test section for key in testKeys: if not self.config.has_section(key): raise Error("Section {0} in configfile not defined.".format(key)) test = {'id': key} try: test['cronexpr'] = self.config.get(key, 'cronexpr').strip() test['dir'] = self.config.get(key, 'dir').strip() if self.config.has_option(key, 'srcdir'): test['srcdir'] = self.config.get(key, 'srcdir').strip() else: test['srcdir'] = test['dir'] if self.config.has_option(key, 'exclude'): test['exclude'] = self.config.get(key, 'exclude', raw=1).split(", ") else: test['exclude'] = [] test['recursive'] = self.config.getboolean(key, 'recursive') test['logging'] = self.config.getboolean(key, 'logging') test['followlinks'] = self.config.getboolean(key, 'followlinks') test['tolerant'] = self.config.getboolean(key, 'tolerant') except NoOptionError as err: raise Error("{0}".format(err.message)) except ValueError: raise Error("Argument error in [{0}] section.".format(key)) else: self.tests.append(test) except NoOptionError as err: raise Error("{0}".format(err.message)) except NoSectionError: self.tests = [] if self.__verbose: sys.stdout.write("csback2cron: Finished reading CONFIGFILE.\n") def convert(self): """ Convert the csback configuration file to a crontab file. """ if self.__verbose: sys.stdout.write("csback2cron: Conversion ... \n") #convert copy sections if len(self.copies) and self.__verbose: sys.stdout.write("csback2cron: Converting copy sections ...\n") for copy in self.copies: copyConv = CopyConverter(copy) copyConv.convert() self.result.append(str(copyConv)+"\n") # convert backup sections if len(self.backups) and self.__verbose: sys.stdout.write("csback2cron: Converting backup sections ...\n") for backup in self.backups: backupConv = BackupConverter(backup) backupConv.convert() self.result.append(str(backupConv)+"\n") # convert test sections if len(self.tests) and self.__verbose: sys.stdout.write("csback2cron: Converting test sections ...\n") for test in self.tests: testConv = TestConverter(test) testConv.convert() self.result.append(str(testConv)+"\n") # convert mail section if len(self.mail) and self.__verbose: sys.stdout.write("csback2cron: Converting mail section ...\n") if len(self.mail): mailConv = MailConverter(self.mail) mailConv.convert() self.result.append(str(mailConv)+"\n") def write(self): """ Write a csback crontab file. """ # header lines output = ['# This is <' + self.crontabfile + '>\n', '# Generated with csback2cron '+__version__+'.\n', '# '+__subversion__+'\n' '# '+datetime.now().strftime("%Y-%m-%d %H:%M:%S")+'\n', '# -------------------------------------------------------------\n\n'] output.extend(self.result) if self.__verbose: sys.stdout.write("csback2cron: Writing " + self.crontabfile + \ " ...\n") crontabfile = open(self.crontabfile, 'w') crontabfile.writelines(output) crontabfile.close() if self.__verbose: sys.stdout.write("csback2cron: " + self.crontabfile + " written.\n") DEFAULTS = {'logging': 'yes', 'recursive': 'yes', 'followlinks': 'no', \ 'tolerant': 'no', 'test': 'yes', 'copy': 'no'} # ----------------------------------------------------------------------------- def main(argv=None): if argv is None: argv = sys.argv try: try: opts, args = getopt.getopt(argv[1:], "hovi:", ["help", "overwrite",\ "verbose", "infile="]) except getopt.GetoptError as err: raise Usage(err.msg) # fetch arguments inputfile = DEFAULTPATH verbose = False overwrite = False for opt, arg in opts: if opt in ("-v", "--verbose"): verbose = True elif opt in ("-o", "--overwrite"): overwrite = True elif opt in ("-h", "--help"): help() sys.exit() elif opt in ("-i", "--input"): inputfile = arg else: raise Usage("Unhandled option chosen.") if len(args) == 1: outputfile = args[0] else: raise Usage("Invalid arguments.") # checks if not overwrite and os.path.isfile(outputfile): raise Usage(outputfile +\ " already exists. Enable [-o] to overwrite file.") processor = Processor(inputfile, outputfile, verbose=verbose) processor.read() processor.convert() processor.write() except Error as err: err.display() return 2 except Usage as err: err.display() return 2 # ----------------------------------------------------------------------------- if __name__ == "__main__": sys.exit(main()) # ----- END OF csback2cron.py -----