#!/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 -----