csback2cron.py 18.6 KB
Newer Older
1
2
#!/usr/bin/env python
## @file csback2cron.py
3
# @brief  Generate a crontab file for the csback software suite.
4
5
6
7
8
9
10
# 
# -----------------------------------------------------------------------------
# 
# $Id$
# @author Daniel Armbruster
# \date 11/09/2011
# 
11
# Purpose: Generate a crontab file for the csback toolkit using a configuration
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
# 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 <http://www.gnu.org/licenses/>.
# ----
# 
31
# Copyright (c) 2011-2012 by Daniel Armbruster
32
33
# 
# REVISIONS and CHANGES 
34
35
36
# 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
37
38
39
# 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
40
#
41
# =============================================================================
Daniel Armbruster's avatar
Daniel Armbruster committed
42
43
44
"""
Convert a csback configuration file to a usual crontab file.
"""
45
46
47
48

import sys
import getopt
import os
Daniel Armbruster's avatar
Daniel Armbruster committed
49
from datetime import datetime
50
if sys.version_info >= (2,) and sys.version_info < (3,):
51
52
53
54
  from ConfigParser import ConfigParser
  from ConfigParser import ParsingError
  from ConfigParser import NoSectionError
  from ConfigParser import NoOptionError
55
elif sys.version_info >= (3,):
56
57
58
59
  from configparser import ConfigParser
  from configparser import ParsingError
  from configparser import NoSectionError
  from configparser import NoOptionError
60
61
else:
  sys.stderr.write("csback2cron: Incompatible python version.\n")
62

63
__version__ = "V0.9.1"
64
65
66
67
__subversion__ = "$Id$"
__license__ = "GPLv2"
__author__ = "Daniel Armbruster"
__copyright__ = "Copyright (c) 2012 by Daniel Armbruster"
68

69
70
DEFAULTPATH = os.path.expanduser("~/.csback/csbackrc")
# -----------------------------------------------------------------------------
71
72
73
74
class Error(Exception):
  def __init__(self, msg):
    self.msg = msg
  def display(self):
75
    sys.stderr.write("csback2cron (ERROR): " + self.msg + "\n")
76
77

class Usage(Error):
78
  def display(self):
79
80
81
82
83
84
85
    usage_text = "Version: "+__version__+"\nLicense: "+__license__+ \
      "\n"+__subversion__+"\nAuthor: "+__author__+ """
 Usage: csback2cron [-v|--verbose] [-o|--overwrite] [-i|--infile ARG]
                    <CRONTABFILENAME>
    or: csback2cron -h|--help
  Note: If [-i|--infile CONFIGFILE] isn't passed as a argument, csback2cron
        uses ~/.csback/csbackrc as default.\n"""
Daniel Armbruster's avatar
   
Daniel Armbruster committed
86
87
    sys.stderr.write("csback2cron: " + self.msg + "\n")
    sys.stderr.write(usage_text)
88
89
90

# -----------------------------------------------------------------------------
def help():
91
92
93
94
95
96
97
98
  """
  Printing helptext to stdout.
  """
  help_text = "Version: "+__version__+"\nLicense: "+__license__+"\n"+ \
    __subversion__+"\nAuthor: "+__author__+"""
 Usage: csback2cron [-v|--verbose] [-o|--overwrite] [-i|--infile CONFIGFILE]
                    <CRONTABFILENAME>
    or: csback2cron -h|--help
99
-------------------------------------------------------------------------------
100
101
102
103
 -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.
104
                    If this argument wasn't passed csback2cron assumes
105
106
                    ~/.csback/csbackrc as default path to the configuration
                    file.
107
 -o|--overwrite     Overwrite already existing crontab.
108
109
110
 <CRONTABFILENAME>  Outputfilename of the generated crontab.
-------------------------------------------------------------------------------
Notice that csback2cron does not check any logical values e.g. pathes and/or
111
cron expressions.\n"""
112
  sys.stdout.write(help_text)
113
114

# -----------------------------------------------------------------------------
115
class Converter():
116
117
118
119
120
121
  """
  Abstract converter base class for section dictionaries.
  """
  def __init__(self, sectionDict):
    self.sectionDict = sectionDict
    self.line = ''
122
    self.addCronExpr = True
123

124
  def convert(self):
125
126
127
    """
    Abstract function
    """
128
    self.convertDict()
129
130
131
132

  def convertDict(self):
    raise NotImplementedError("function must be defined!")

133
  def __str__(self):
134
135
136
137
138
139
140
141
142
143
144
    """
    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):
145
146
    self.line = self.sectionDict['cronexpr']+' csbackmail'
    if self.sectionDict['logging']:
147
      self.line += ' -l'
148
149
150
151
152
    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
153
154
  

155
class TestConverter(Converter):
156
  """
157
  Class which implements a test section dictionary converter
158
159
  """
  def convertDict(self):
160
161
    if self.addCronExpr:
      self.line = self.sectionDict['cronexpr']+' '
162
    self.line += 'csbackchk -L'
163
    if self.sectionDict['logging']:
164
      self.line += ' -l'
165
166
167
168
169
170
171
    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']:
172
      self.line += ' -e "'+regex.strip()+'"'
173
    self.line += ' '+self.sectionDict['srcdir']+' '+self.sectionDict['dir']
174
175


176
class BackupConverter(Converter):
177
  """
178
  Class which implements a backup section dictionary converter
179
180
  """
  def convertDict(self):
181
182
183
184
185
186
187
188
189
190
191
192
    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)+";"
193
    self.line += ' csbackgen -L'
194
    if self.sectionDict['logging']:
195
      self.line += ' -l'
196
197
198
199
    if not self.sectionDict['recursive']:
      self.line += ' -R'
    if self.sectionDict['followlinks']:
      self.line += ' -f'
200
    for regex in self.sectionDict['exclude']:
201
      self.line += ' -e "'+regex.strip()+'"'
202
203
    self.line += ' -t '+self.sectionDict['targetdir']+' '+ \
      self.sectionDict['srcdir']
204
    if self.sectionDict['test']:
205
      testConv = TestConverter({'cronexpr': self.sectionDict['cronexpr'], \
206
207
208
209
210
211
212
213
214
        '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()
215
      self.line += '; '+str(testConv)
216

217
218
219
220
221
222
223
224
225
226
227
228
229
230
231

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'] 

232
233
# -----------------------------------------------------------------------------
class Processor():
234
235
236
237
  """
  Processing class which is responsible for the processing of the csbackrc
  configuration file conversion.
  """
238
  def __init__(self, configfile, crontabfile, verbose=False):
239
240
241
    self.configfile = configfile
    self.crontabfile = crontabfile
    self.config = ConfigParser(Processor.DEFAULTS)
242
    self.__verbose = verbose
243
    self.mail = {}
244
    self.copies = []
245
246
    self.backups = []
    self.tests = []
247
    self.result = []
Daniel Armbruster's avatar
Daniel Armbruster committed
248

249
  def read(self):
250
251
252
    """
    Read the csback configuration file.
    """
253
    if not os.path.isfile(self.configfile):
254
      raise Error("Given CONFIGFILE is not a regular file.")
255
    if 0 == os.stat(self.configfile).st_size:
256
257
      raise Error("Given CONFIGFILE is an empty file.")
    if self.__verbose:
258
      sys.stdout.write("csback2cron: Reading CONFIGFILE ... \n")
259
    try:  
260
      self.config.read(self.configfile)
261
262
263
264
    except ParsingError as err:
      raise Error("{0}".format(err.message))
    # fetch mail section
    if self.config.has_section('mail'):
265
      try:
266
        self.mail['cronexpr'] = self.config.get('mail', 'cronexpr').strip()
267
268
269
270
        self.mail['receivers'] = \
          self.config.get('mail', 'receivers').split(',')
        self.mail['receivers'] = [add.strip() \
          for add in self.mail['receivers']]
271
272
        self.mail['sender'] = self.config.get('mail', 'sender').strip()
        self.mail['host'] = self.config.get('mail', 'host').strip()
273
        self.mail['port'] = self.config.get('mail', 'port').strip()
274
275
        self.mail['username'] = self.config.get('mail', 'username').strip()
        self.mail['password'] = self.config.get('mail', 'password').strip()
276
277
278
279
280
281
282
283
284
285
        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 = {}
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
    # 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 = []
318
319
    # fetch backups section
    try:
320
321
322
323
324
325
326
327
328
329
330
331
      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()
332
          backup['exclude'] = []
333
334
          if self.config.has_option(key, 'exclude'):
            backup['exclude'] = self.config.get(key, 'exclude',raw=1).split(", ")
335
336
337
          backup['recursive'] = self.config.getboolean(key, 'recursive')
          backup['logging'] = self.config.getboolean(key, 'logging')
          backup['followlinks'] = self.config.getboolean(key, 'followlinks')
338
339
340
341
342
343
344
345
346
347
          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()) 
348
349
350
351
352
353
354
355
356
357
          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))
358
    except NoSectionError:
359
      self.backups = []
360
361
    # fetch tests section
    try:
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
      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'):
378
            test['exclude'] = self.config.get(key, 'exclude', raw=1).split(", ")
379
          else:
380
381
382
383
384
385
386
387
388
389
390
391
392
            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))
393
    except NoSectionError:
394
      self.tests = []
395
    if self.__verbose:
396
      sys.stdout.write("csback2cron: Finished reading CONFIGFILE.\n")
397
398

  def convert(self):
399
400
401
402
    """
    Convert the csback configuration file to a crontab file.
    """
    if self.__verbose:
403
      sys.stdout.write("csback2cron: Conversion ... \n")
404
405
406
407
408
409
410
    #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")
411
412
    # convert backup sections
    if len(self.backups) and self.__verbose:
413
      sys.stdout.write("csback2cron: Converting backup sections ...\n")
414
415
416
417
418
419
    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:
420
      sys.stdout.write("csback2cron: Converting test sections ...\n")
421
422
423
424
425
426
    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:
427
      sys.stdout.write("csback2cron: Converting mail section ...\n")
428
429
430
431
    if len(self.mail):
      mailConv = MailConverter(self.mail)
      mailConv.convert()
      self.result.append(str(mailConv)+"\n")
Daniel Armbruster's avatar
Daniel Armbruster committed
432
    
433
  def write(self):
434
435
436
    """
    Write a csback crontab file.
    """
Daniel Armbruster's avatar
Daniel Armbruster committed
437
    # header lines
438
    output = ['# This is <' + self.crontabfile + '>\n',
439
440
      '# Generated with csback2cron '+__version__+'.\n',
      '# '+__subversion__+'\n'
441
      '# '+datetime.now().strftime("%Y-%m-%d %H:%M:%S")+'\n',
442
      '# -------------------------------------------------------------\n\n']
443
    output.extend(self.result)
444
    if self.__verbose:
445
446
447
      sys.stdout.write("csback2cron: Writing " + self.crontabfile + \
        " ...\n")
    crontabfile = open(self.crontabfile, 'w')
Daniel Armbruster's avatar
Daniel Armbruster committed
448
449
    crontabfile.writelines(output)
    crontabfile.close()
450
451
    if self.__verbose:
      sys.stdout.write("csback2cron: " + self.crontabfile + " written.\n")
Daniel Armbruster's avatar
Daniel Armbruster committed
452

453
454
  DEFAULTS = {'logging': 'yes', 'recursive': 'yes', 'followlinks': 'no', \
              'tolerant': 'no', 'test': 'yes', 'copy': 'no'}
455
456
457
458
459
460
461

# -----------------------------------------------------------------------------
def main(argv=None):
  if argv is None:
    argv = sys.argv
  try:
    try:
462
      opts, args = getopt.getopt(argv[1:], "hovi:", ["help", "overwrite",\
463
        "verbose", "infile="])
464
465
    except getopt.GetoptError as err:
      raise Usage(err.msg)
466
467
    # fetch arguments
    inputfile = DEFAULTPATH
468
    verbose = False
469
    overwrite = False
470
    for opt, arg in opts:
471
      if opt in ("-v", "--verbose"):
472
        verbose = True
473
474
      elif opt in ("-o", "--overwrite"):
        overwrite = True
475
476
477
478
      elif opt in ("-h", "--help"):
        help()
        sys.exit()
      elif opt in ("-i", "--input"):
479
        inputfile = arg
480
481
      else:
        raise Usage("Unhandled option chosen.")
482
483
484
485

    if len(args) == 1:
      outputfile = args[0]
    else:
486
      raise Usage("Invalid arguments.")
487
    # checks
488
489
490
    if not overwrite and os.path.isfile(outputfile):
      raise Usage(outputfile +\
      " already exists. Enable [-o] to overwrite file.") 
491
492
493
    processor = Processor(inputfile, outputfile, verbose=verbose)
    processor.read()
    processor.convert()
494
495
496
497
    processor.write()
  except Error as err:
    err.display()
    return 2
498
499
500
501
502
503
504
505
506
  except Usage as err:
    err.display()
    return 2

# -----------------------------------------------------------------------------
if __name__ == "__main__":
  sys.exit(main())

# ----- END OF csback2cron.py -----