"Fossies" - the Fresh Open Source Software Archive

Member "safekeep-1.5.1/safekeep" (16 Nov 2020, 93250 Bytes) of package /linux/misc/safekeep-1.5.1.tar.gz:


As a special service "Fossies" has tried to format the requested source page into HTML format using (guessed) Python source code syntax highlighting (style: standard) with prefixed line numbers. Alternatively you can here view or download the uninterpreted source code file. See also the latest Fossies "Diffs" side-by-side code changes report for "safekeep": 1.5.0_vs_1.5.1.

    1 #!/usr/bin/python3
    2 
    3 # Copyright (C) 2006-2010 Lattica, Inc.
    4 #
    5 # SafeKeep is free software; you can redistribute it and/or modify
    6 # it under the terms of the GNU General Public License as published by
    7 # the Free Software Foundation, either version 2 of the License, or
    8 # (at your option) any later version.
    9 #
   10 # Safekeep is distributed in the hope that it will be useful,
   11 # but WITHOUT ANY WARRANTY; without even the implied warranty of
   12 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
   13 # GNU General Public License for more details.
   14 #
   15 # You should have received a copy of the GNU General Public License
   16 # along with Safekeep.  If not, see <http://www.gnu.org/licenses/>.
   17 
   18 import getopt, os, re, sys, fnmatch, stat
   19 import subprocess, tempfile, time, traceback, shlex
   20 import getpass, pwd, xml.dom.minidom
   21 import socket, smtplib, io
   22 import fcntl
   23 
   24 from subprocess import PIPE, STDOUT
   25 
   26 ######################################################################
   27 # Global settings
   28 ######################################################################
   29 
   30 config_file = '/etc/safekeep/safekeep.conf'
   31 config_ext = '.backup'
   32 trickle_cmd = 'trickle'
   33 logbuf = []
   34 is_client = False
   35 verbosity_level = 1
   36 verbosity_ssh = ''
   37 verbosity_trickle = ''
   38 work_user = getpass.getuser()
   39 backup_user = None
   40 backup_tempdir = None
   41 client_user = 'root'
   42 home_dir = None
   43 base_dir = None
   44 client_defaults = []
   45 current_pid = os.getpid()
   46 default_bandwidth = {}
   47 default_snapshot = '20%FREE'
   48 statistics = []
   49 error_counter = 0
   50 warning_counter = 0
   51 ssh_keygen_type = 'rsa'
   52 ssh_keygen_bits = 4096
   53 SSH_TYPES = ['dsa', 'rsa', 'ed25519', 'ecdsa']
   54 SSH_KEY_TYPES = ['ssh-dss', 'ssh-rsa', 'ssh-ed25519', 'ecdsa-sha2-nistp256', 'ecdsa-sha2-nistp384', 'ecdsa-sha2-nistp521']
   55 ssh_StrictHostKeyChecking = 'ask'
   56 SSH_STRICT_HOSTKEY_CHECK_OPTS = ['ask', 'yes', 'no' ]
   57 # Default mount options, overridden elsewhere:
   58 # Key is a file system type, or 'snapshot' for default for snapshot mount
   59 # or 'bind' for a bind mount (check mount for details)
   60 default_mountoptions = {'xfs' : 'ro,nouuid', 'snapshot' : 'ro', 'bind' : 'ro'}
   61 
   62 PROTOCOL = "1.4"
   63 VERSION = "1.5.1"
   64 VEBOSITY_BY_CLASS = {'DBG': 3, 'INFO': 2, 'WARN': 1, 'ERR': 0}
   65 
   66 ######################################################################
   67 # Miscellaneous support functions
   68 ######################################################################
   69 
   70 class ClientException(Exception):
   71     def __init__(self, value, traceback=None):
   72         self.value = value
   73         self.traceback = traceback
   74 
   75     def __str__(self):
   76         return repr(self.value)
   77 
   78 #def byte_or_str(arg):
   79 #    if isinstance(arg, (str,)): # check if its in bytes
   80 #        return arg
   81 #    else:
   82 #        return arg.decode('utf-8', 'backslashreplace')
   83 
   84 def send(msg):
   85     print(msg)
   86     sys.stdout.flush()
   87 
   88 def log(msg, cls=None):
   89     global logbuf
   90     if cls:
   91         if is_client: cls = cls.lower()
   92         msg = '%s: %s' % (cls, msg)
   93     else:
   94         for c in list(VEBOSITY_BY_CLASS.keys()):
   95             if msg.upper().startswith(c + ': '):
   96                 cls = c
   97                 break
   98         else:
   99             cls = 'UNK'
  100 
  101     cutoff = VEBOSITY_BY_CLASS.get(cls.upper())
  102     if cutoff is None: cutoff = 3
  103     if is_client or verbosity_level >= cutoff:
  104         logbuf.append(msg)
  105         if is_client:
  106             send(msg)
  107         else:
  108             print(msg, file=sys.stderr)
  109 
  110 def info_file(filename, marker=None, stats=None):
  111     info('# File: ' + filename)
  112     errs = 0
  113     fin = open(filename, 'r')
  114     try:
  115         for line in fin.readlines():
  116             if marker:
  117                 if line.startswith(marker):
  118                     marker = None
  119                 continue
  120             if line.startswith("Errors "):
  121                 errs = int(line[6:])
  122             info(line.rstrip())
  123             if stats:
  124                 vals = line.rstrip().split(None, 2)
  125                 key = vals.pop(0)
  126                 if len(vals) == 2:
  127                     stats[key] = vals[1][1:-1]
  128                 else:
  129                     stats[key] = vals[0]
  130     finally:
  131         fin.close()
  132     return errs
  133 
  134 def stacktrace():
  135     exc_file = io.StringIO()
  136     traceback.print_exc(None, exc_file)
  137     return "\n" + exc_file.getvalue()
  138 
  139 def debug(msg):
  140     log(msg, 'DBG')
  141 
  142 def info(msg):
  143     log(msg, 'INFO')
  144 
  145 def warn(msg):
  146     log(msg, 'WARN')
  147 
  148 def error(msg, ex=None):
  149     extra = ""
  150     if ex and verbosity_level > 2:
  151         extra = stacktrace()
  152     log(msg + extra, 'ERR')
  153 
  154 def do_spawn(args, stdin=None, stdout=False):
  155     if isinstance(args, (str,)):
  156         debug('Run [' + args + ']')
  157     else:
  158         debug('Run [' + ' '.join(args) + ']')
  159 
  160     _shell = isinstance(args, (str,))
  161     if stdin:
  162         _stdin = PIPE
  163     else:
  164         _stdin = None
  165     if stdout:
  166         _stderr = None
  167     else:
  168         _stderr = STDOUT
  169 
  170     proc = subprocess.Popen(args, bufsize=1, shell=_shell, stdin=_stdin, stdout=PIPE, stderr=_stderr, close_fds=True, encoding='utf-8', errors='backslashreplace')
  171     child_in = proc.stdin
  172     child_out = proc.stdout
  173 
  174     if stdin:
  175         child_in.write(stdin)
  176         child_in.close()
  177 
  178     lines = []
  179     for line in child_out:
  180         lines.append(line)
  181         if not stdout:
  182             info(line.rstrip())
  183     child_out.close()
  184 
  185     return (proc.wait(), lines)
  186 
  187 def _spawn(args, stdin=None, stdout=False):
  188     if isinstance(args, (str,)):
  189         cmd = args.split(None)[0]
  190     else:
  191         cmd = args[0]
  192 
  193     try:
  194         rc, out = do_spawn(args, stdin, stdout)
  195     except OSError as ex:
  196         ret = "OSError: %s" % (ex)
  197         error('%s failed: %s' % (cmd, ret))
  198         return ret
  199 
  200     if not rc:
  201         ret = None
  202     elif rc > 0:
  203         ret = 'exited with non zero status: %d' % rc
  204     elif rc < 0:
  205         ret = 'killed by signal: %d' % -rc
  206     else:
  207         ret = 'unknown exit status: %d' % rc
  208     if ret:
  209         error('%s failed: %s' % (cmd, ret))
  210     return (ret, out)
  211 
  212 # this just spawns an external program (optionally through a shell)
  213 # and returns True it it fails, and False if it successed
  214 def spawn(args):
  215     rc, out = _spawn(args)
  216     return rc
  217 
  218 # this spawns an external program (optionally through a shell),
  219 # feeds it any input via stdin, captures the output and returns it.
  220 # if it fails it returns None, otherwise it returns the output
  221 def call(args, stdin=None):
  222     rc, out = _spawn(args, stdin, stdout=True)
  223     if rc:
  224         return None
  225     return out
  226 
  227 def try_to_run(args):
  228     try:
  229         rc, out = do_spawn(args, None, True)
  230     except OSError as ex:
  231         return None
  232     if not rc in (0, 1):
  233         return None
  234     return out or ''
  235 
  236 #
  237 # Statistics format routines for the "server" type
  238 #
  239 def print_stats_table_server_text(stats):
  240     result = '|{0:<8}|{1:<8}'.format(stats['id'], stats['state'])
  241     if len(stats) > 2:
  242         result += '|{0:>6}|{1:<24}|{2:<24}|{3:>24}|{4:>12}|{5:>12}|{6:>13}|'.format(
  243                             stats['Errors'],
  244                             stats['StartTime'],
  245                             stats['EndTime'],
  246                             stats['ElapsedTime'],
  247                             stats['SourceFileSize'],
  248                             stats['MirrorFileSize'],
  249                             stats['TotalDestinationSizeChange'])
  250     else:
  251         result += '|{0:>6}|{0:<24}|{0:<24}|{0:>24}|{0:>12}|{0:>12}|{0:>13}|'.format('')
  252     return result
  253 
  254 def print_stats_table_server_html(stats):
  255     if stats.get('state') == 'OK':
  256         color = ' bgcolor="#81F7BE"'
  257     elif 'CLEAN' in stats.get('state'):
  258         color = ' bgcolor="#66CCFF"'
  259     elif 'WARNING' in stats.get('state'):
  260         color = ' bgcolor="#FFCC66"'
  261     else:
  262         color = ' bgcolor="#F78181"'
  263     result = '<tr' + color + '><td>' + stats['id'] + '</td><td>' + stats['state'] + '</td>'
  264     if len(stats) > 2:
  265         result += '<td align="right">' + stats['Errors'] + '</td>' + \
  266                   '<td>' + stats['StartTime'] + '</td>' + \
  267                   '<td>' + stats['EndTime']  + '</td>' + \
  268                   '<td align="right">' + stats['ElapsedTime'] + '</td>' + \
  269                   '<td align="right">' + stats['SourceFileSize'] + '</td>' + \
  270                   '<td align="right">' + stats['MirrorFileSize'] + '</td>' + \
  271                   '<td align="right">' + stats['TotalDestinationSizeChange'] + '</td>'
  272     else:
  273         result += '<td align="right"></td>' + \
  274                   '<td></td>' + \
  275                   '<td></td>' + \
  276                   '<td align="right"></td>' + \
  277                   '<td align="right"></td>' + \
  278                   '<td align="right"></td>' + \
  279                   '<td align="right"></td>'
  280     result += '</tr>'
  281     return result
  282 
  283 def stats_to_table_server_text():
  284     result = ['-' * 141 + '\r\n',
  285               '|{0:<8}|{1:<8}|{2:<6}|{3:<24}|{4:<24}|{5:<24}|{6:<12}|{7:<12}|{8:<13}|'.format(
  286                   'Name',
  287                   'State',
  288                   'Errors',
  289                   'Start time',
  290                   'End time',
  291                   'Elapsed time',
  292                   'Source size',
  293                   'Mirror size',
  294                   'Total changed') + \
  295               '\r\n',
  296               '-' * 141 + '\r\n']
  297 
  298     for stats in statistics:
  299         result.append(print_stats_table_server_text(stats) + '\r\n' + '-' * 141 + '\r\n')
  300 
  301     return result
  302 
  303 def stats_to_table_server_html():
  304     result = ['<html><body><table border="1"><tr>'
  305               '<th>Name</th>'
  306               '<th>State</th>'
  307               '<th>Errors</th>'
  308               '<th>Start time</th>'
  309               '<th>End time</th>'
  310               '<th>Elapsed time</th>'
  311               '<th>Source size</th>'
  312               '<th>Mirror size</th>'
  313               '<th>Total changed</th>'
  314               '</tr>']
  315 
  316     for stats in statistics:
  317         result.append(print_stats_table_server_html(stats) + '\r\n')
  318 
  319     result.append('</table></html>')
  320     return result
  321 
  322 #
  323 # Statistics format routines for the "list" type
  324 #
  325 def print_stats_table_list_text(stats):
  326     result = '|{0:<8}|{1:<8}'.format(stats['id'], stats['state'])
  327     if len(stats) > 2:
  328         result += '|{0:<24}|{1:<24}|{2:>10}|'.format(
  329                             stats['CurrentMirror'],
  330                             stats['OldestIncrement'],
  331                             stats['Increments'])
  332     else:
  333         result += '|{0:<24}|{0:<24}|{0:>10}|'.format('')
  334     return result
  335 
  336 def print_stats_table_list_html(stats):
  337     if stats.get('state') == 'OK':
  338         color = ' bgcolor="#81F7BE"'
  339     else:
  340         color = ' bgcolor="#F78181"'
  341     result = '<tr' + color + '><td>' + stats['id'] + '</td><td>' + stats['state'] + '</td>'
  342     if len(stats) > 2:
  343         result += '<td>' + stats['CurrentMirror'] + '</td>' + \
  344                   '<td>' + stats['OldestIncrement']  + '</td>' + \
  345                   '<td align="right">' + stats['Increments'] + '</td>'
  346     else:
  347         result += '<td></td>' + \
  348                   '<td></td>' + \
  349                   '<td align="right"></td>'
  350     result += '</tr>'
  351     return result
  352 
  353 def stats_to_table_list_text():
  354     result = ['-' * 80 + '\r\n',
  355               '|{0:<8}|{1:<8}|{2:<24}|{3:<24}|{4:<10}|'.format(
  356                   'Name',
  357                   'State',
  358                   'Current Mirror',
  359                   'Oldest Increment',
  360                   'Increments') + \
  361               '\r\n',
  362               '-' * 80 + '\r\n']
  363 
  364     for stats in statistics:
  365         result.append(print_stats_table_list_text(stats) + '\r\n' + '-' * 80 + '\r\n')
  366 
  367     return result
  368 
  369 def stats_to_table_list_html():
  370     result = ['<html><body><table border="1"><tr>'
  371               '<th>Name</th>'
  372               '<th>State</th>'
  373               '<th>Current Mirror</th>'
  374               '<th>Oldest Increment</th>'
  375               '<th>Increments</th>'
  376               '</tr>']
  377 
  378     for stats in statistics:
  379         result.append(print_stats_table_list_html(stats) + '\r\n')
  380 
  381     result.append('</table></html>')
  382     return result
  383 
  384 #
  385 # Main statistics printing functions
  386 #
  387 def stats_to_table(mode, fmt):
  388     if not mode in ('server', 'list'): return 'Mode: %s: not currently supported' % mode
  389     if not fmt in ('html', 'text'): return 'Format: %s: not currently supported' % fmt
  390     if len(statistics) == 0: return 'No statistics available'
  391 
  392     try:
  393         result = eval('stats_to_table_' + mode + '_' + fmt + '()')
  394     except NameError as ex:
  395         error('ERROR: %s' % (ex or ''), ex)
  396         result = 'Internal error: no statistics available'
  397 
  398     return result
  399 
  400 def send_notification(email, mode):
  401     global logbuf, error_counter, warning_counter
  402     if not logbuf: return
  403     email_to = email.get('to')
  404     if not email_to: return
  405     if 'smtp' in email:
  406         if 'port' in email['smtp']:
  407             portstr = ':%d' % email['smtp']['port']
  408         else:
  409             portstr = ''
  410         info('Sending email to %s via %s%s' % (','.join(email_to), email['smtp'].get('server', 'Local'), portstr))
  411     else:
  412         info('Sending email to %s via %s' % (','.join(email_to), 'Local'))
  413     hostname = socket.getfqdn()
  414     if 'from' in email:
  415         email_from = email['from']
  416     else:
  417         email_from = 'SafeKeep@' + hostname
  418 
  419     if error_counter > 0:
  420         global_status = '%s errors' % error_counter
  421     elif warning_counter > 0:
  422         global_status = '%s warnings' % warning_counter
  423     else:
  424         global_status = 'OK'
  425 
  426     msg = 'From: ' + email_from + '\r\n' + \
  427           'To: ' + ', '.join(email_to) + '\r\n' + \
  428           'Subject: SafeKeep ' + global_status + ' for ' + hostname + '\r\n' + \
  429           'Date: ' + time.strftime("%a, %d %b %Y %H:%M:%S %z") + '\r\n'
  430     if 'format' not in email:
  431         msg += '\r\n' + '\r\n'.join(logbuf) + '\r\n'
  432     else:
  433         msg += 'Content-Type: multipart/mixed;boundary=safebounder001\r\n'
  434         if 'summary' in email and mode in ('server', 'list'):
  435             msg += '\r\n--safebounder001\r\n'
  436             if email['format'] == 'text':
  437                 msg += 'Content-type: text/plain;charset=utf-8\r\n'
  438             else:
  439                 msg += 'Content-type: text/html;charset=utf-8\r\n'
  440             msg += '\r\n' + ''.join(stats_to_table(mode, email['format'])) + \
  441                    '\r\n'
  442         msg += '\r\n--safebounder001\r\n' + \
  443                'Content-type: text/plain;charset=utf-8\r\n' + \
  444                '\r\n' + '\r\n'.join(logbuf) + '\r\n' + \
  445                '\r\n--safebounder001--\r\n'
  446     if 'smtp' in email:
  447         server = smtplib.SMTP(email['smtp'].get('server'), email['smtp'].get('port'))
  448         server.sendmail(email_from, email_to, msg.encode('ascii', 'backslashreplace'))
  449         server.quit()
  450     else:
  451         cmd = ['/usr/sbin/sendmail', '-t', '-f', email_from]
  452         call(cmd, stdin=msg)
  453 
  454 def is_temp_root(directory):
  455     return directory != '/'
  456 
  457 def reroot(root, path):
  458     if root == '/': return path
  459     if root.endswith('/'): root = root[:-1]
  460     if not path: return root
  461     if path.startswith('/'): return root + path
  462     return os.path.join(root, path)
  463 
  464 def parse_prop_file(filename):
  465     props = {}
  466     fin = open(filename)
  467     lines = fin.readlines()
  468     fin.close()
  469     for line in lines:
  470         line = line.strip()
  471         if len(line) == 0 or line[0] == '#': continue
  472         if '=' in line:
  473             key, value = line.split('=', 1)
  474             props[key.strip()] = value.strip()
  475         else:
  476             props[line] = None
  477     return props
  478 
  479 ######################################################################
  480 # Configuration file parser
  481 ######################################################################
  482 
  483 class ConfigException(Exception):
  484     def __init__(self, value):
  485         self.value = value
  486     def __str__(self):
  487         return repr(self.value)
  488 
  489 def parse_true_false(el, attr, default=None):
  490     true_false = el.getAttribute(attr).lower()
  491     if true_false and not true_false in ('true', 'yes', '1', 'false', 'no', '0'):
  492         raise ConfigException('Option needs to be true or false: attr %s: value %s' % (attr, el.getAttribute(attr)))
  493     if true_false in ('true', 'yes', '1'):
  494         return 'true'
  495     elif true_false in ('false', 'no', '0'):
  496         return 'false'
  497     return default
  498 
  499 def parse_dump(dump_el):
  500     dbtype = dump_el.getAttribute('type')
  501     if not dbtype:
  502         raise ConfigException('You need to specify the database type')
  503     if dbtype not in ('postgres', 'postgresql', 'pgsql', 'mysql'):
  504         raise ConfigException('Invalid database type: %s' % dbtype)
  505     db = dump_el.getAttribute('db')
  506     user = dump_el.getAttribute('user')
  507     dbuser = dump_el.getAttribute('dbuser')
  508     dbpasswd = dump_el.getAttribute('dbpasswd')
  509     opts = (dump_el.getAttribute('options') or '').split()
  510 
  511     dbfile = dump_el.getAttribute('file')
  512     if not dbfile:
  513         raise ConfigException('You need to specify where the database should be dumped')
  514     cleanup = parse_true_false(dump_el, 'cleanup')
  515     save_db_name = parse_true_false(dump_el, 'save-db-name')
  516     if dbtype in ('postgres', 'postgresql', 'pgsql') and db and save_db_name == 'false':
  517         warn('Database dump options: pgsql, save-db-name = false and dump all databases, are incompatible: save-db-name ignored')
  518         save_db_name = None
  519     return {'type' : dbtype, 'db' : db, 'user' : user, 'dbuser' : dbuser, 'dbpasswd': dbpasswd,
  520             'opts' : opts, 'file' : dbfile, 'cleanup' : cleanup, 'save-db-name' : save_db_name}
  521 
  522 def parse_snap(snap_el):
  523     global default_snapshot
  524     device = snap_el.getAttribute('device')
  525     if not device:
  526         raise ConfigException('Please specify the device to be snapshot')
  527     if device.rfind('/') == -1 or device.endswith('/'):
  528         raise ConfigException('The device name seems incorrect: ' + device)
  529     size = snap_el.getAttribute('size') or default_snapshot
  530     if not size:
  531         raise ConfigException('Please specify the size for the snapshot')
  532     tags = []
  533     tag_el = snap_el.getAttribute('tag')
  534     if tag_el:
  535         for tag in tag_el.split(','):
  536             if tag:
  537                 if not tag.startswith('@'): tag = '@' + tag.lstrip()
  538                 tags.append(tag.strip())
  539             elif is_client:
  540                 warn('Device: %s: empty tag in taglist: %s' % (device, tag_el))
  541     mountoptions = snap_el.getAttribute('mount-options')
  542     writable = parse_true_false(snap_el, 'writable')
  543     return {'device' : device, 'size' : size, 'tags' : tags, 'mountoptions' : mountoptions, 'snap_writable' : writable}
  544 
  545 def parse_clude(clude_el):
  546     path = clude_el.getAttribute('path')
  547     path = path.replace('*', r'\*').replace('?', r'\?')
  548     path = path.replace('[', r'\[').replace(']', r'\]')
  549     glob = clude_el.getAttribute('glob')
  550     regexp = clude_el.getAttribute('regexp')
  551     if not path and not glob and not regexp:
  552         raise ConfigException('Empty ' + clude_el.tagName)
  553     return {'type' : clude_el.tagName, 'path' : path, 'glob' : glob, 'regexp' : regexp}
  554 
  555 def parse_bandwidth(bw_el):
  556     return {
  557         'overall': int(bw_el.getAttribute('overall') or 0),
  558         'download': int(bw_el.getAttribute('download') or 0),
  559         'upload': int(bw_el.getAttribute('upload') or 0)
  560     }
  561 
  562 def parse_data_attributes(data_el):
  563     return {
  564         'exclude-devices': parse_true_false(data_el, 'exclude-devices', 'false'),
  565         'exclude-sockets': parse_true_false(data_el, 'exclude-sockets', 'false'),
  566         'exclude-fifos': parse_true_false(data_el, 'exclude-fifos', 'false')
  567     }
  568 
  569 def parse_config(backup_el, dflt_id):
  570     if backup_el.tagName != 'backup':
  571         raise ConfigException('Invalid config file, the top level element must be <backup>')
  572     cfg_id = backup_el.getAttribute('id')
  573     if not cfg_id: cfg_id = dflt_id
  574 
  575     if parse_true_false(backup_el, 'enabled') == 'false':
  576         return None
  577 
  578     host_el = backup_el.getElementsByTagName('host')
  579     if host_el:
  580         host = host_el[0].getAttribute('name')
  581         port = host_el[0].getAttribute('port')
  582         user = host_el[0].getAttribute('user')
  583         nice = host_el[0].getAttribute('nice')
  584         key_ctrl = host_el[0].getAttribute('key-ctrl')
  585         key_data = host_el[0].getAttribute('key-data')
  586     else:
  587         host = port = user = nice = key_ctrl = key_data = None
  588     if host and port and not port.isdigit():
  589         raise ConfigException('Host port must be a number: "%s"' % port)
  590     if host and not user:
  591         user = client_user
  592     if host and not key_ctrl:
  593         key_ctrl = os.path.join('.ssh', 'safekeep-server-ctrl-key')
  594     if host and not key_data:
  595         key_data = os.path.join('.ssh', 'safekeep-server-data-key')
  596     if key_ctrl and not os.path.isabs(key_ctrl):
  597         key_ctrl = os.path.join(home_dir, key_ctrl)
  598     if key_data and not os.path.isabs(key_data):
  599         key_data = os.path.join(home_dir, key_data)
  600 
  601     bw = {}
  602     bw_el = backup_el.getElementsByTagName('bandwidth')
  603     if len(bw_el) == 1:
  604         bw = parse_bandwidth(bw_el[0])
  605     elif len(bw_el) > 1:
  606         raise ConfigException('Can not have more than one bandwidth element')
  607 
  608     repo_el = backup_el.getElementsByTagName('repo')
  609     repo_dir = None
  610     retention = None
  611     if len(repo_el) == 1:
  612         repo_dir = repo_el[0].getAttribute('path')
  613         retention = repo_el[0].getAttribute('retention')
  614     elif len(repo_el) > 1:
  615         raise ConfigException('Can not have more than one repo element')
  616     if not repo_dir: repo_dir = cfg_id
  617     repo_dir = os.path.join(base_dir, repo_dir)
  618 
  619     options_els = backup_el.getElementsByTagName('options')
  620     options = []
  621     if len(options_els) > 0:
  622         for options_el in options_els[0].childNodes:
  623             if options_el.nodeType != options_el.ELEMENT_NODE:
  624                 continue
  625             option = options_el.nodeName
  626             if option == 'special-files':
  627                 warn('options element special-files is deprecated, use data attributes instead')
  628             if option in ('special-files', 'rdiff-backup'):
  629                 if options_el.hasAttributes():
  630                     for key, value in list(options_el.attributes.items()):
  631                         options.append({option : {key : value}})
  632                 else:
  633                     raise ConfigException('Option "%s" has no value' % option)
  634             else:
  635                 raise ConfigException('Unknown option "%s"' % option)
  636 
  637     setup_el = backup_el.getElementsByTagName('setup')
  638     writable = None
  639     dumps = []
  640     snaps = []
  641     script = None
  642     run_on = 'client'
  643     if len(setup_el) > 0:
  644         writable = parse_true_false(setup_el[0], 'writable')
  645         dump_els = setup_el[0].getElementsByTagName('dump')
  646         for dump_el in dump_els:
  647             dumps.append(parse_dump(dump_el))
  648         snap_els = setup_el[0].getElementsByTagName('snapshot')
  649         for snap_el in snap_els:
  650             snaps.append(parse_snap(snap_el))
  651         script_el = setup_el[0].getElementsByTagName('script')
  652         if len(script_el) == 1:
  653             script = script_el[0].getAttribute('path')
  654             if not ':' in script:
  655                 if is_client and os.path.isfile(script):
  656                     warn('Assuming client based script: %s' % script)
  657                     script = 'client:' + script
  658                 else:
  659                     script = 'server:' + script
  660             run_on_conf = script_el[0].getAttribute('run-on')
  661             if run_on_conf is not None and len(run_on_conf.strip()) > 0:
  662                 # assert the values
  663                 if not run_on_conf in ['server', 'client']:
  664                     raise ConfigException('Incorrect configuration value for run-on attribute: %s. '
  665                         'Supported values are "server" and "client"' % run_on_conf)
  666                 run_on = run_on_conf
  667 
  668             if run_on_conf == 'server' and script.startswith('client:'):
  669                 raise ConfigException('Incorrect combination: run-on="server" and client-side path of a script')
  670         elif len(script_el) > 1:
  671             raise ConfigException('Can not have more than one setup script element')
  672 
  673     data_options = {}
  674     data_el = backup_el.getElementsByTagName('data')
  675 
  676     if len(data_el) == 1:
  677         data_options = parse_data_attributes(data_el[0])
  678         child_els = data_el[0].childNodes
  679         cludes = []
  680         for child_el in child_els:
  681             if child_el.nodeType != child_el.ELEMENT_NODE:
  682                 continue
  683             if child_el.tagName not in ('include', 'exclude'):
  684                 continue
  685             cludes.append(parse_clude(child_el))
  686         cludes.append({'type' : 'exclude', 'path' : '', 'glob' : '', 'regexp' : '.*'})
  687     elif len(data_el) > 1:
  688         raise ConfigException('Can not have more than one data element')
  689     else:
  690         path_xcludes = ['/dev/', '/media/', '/mnt/', '/net/', '/proc/', '/selinux/', '/sys/',
  691                         '/tmp/', '/var/cache', '/var/lock', '/var/run', '/var/tmp',
  692                         '/var/named/chroot/dev', '/var/named/chroot/proc',
  693                         '/var/named/chroot/var/run', '/var/named/chroot/var/tmp']
  694         cludes = [{'type' : 'exclude', 'path' : path, 'glob' : None, 'regexp' : None} for path in path_xcludes]
  695 
  696     return {'id': cfg_id, 'host' : host, 'port' : port, 'nice' : nice, 'user' : user, 'key_ctrl' : key_ctrl, 'key_data' : key_data,
  697             'dir' : repo_dir, 'retention' : retention, 'dumps' : dumps, 'snaps' : snaps, 'script' : script, 'mount_writable' : writable,
  698             'cludes' : cludes, 'data_options' : data_options, 'options' : options, 'bw' : bw, 'run_on': run_on}
  699 
  700 def parse_locs(cfglocs):
  701     cfgfiles = []
  702     for cfg in cfglocs:
  703         if os.path.isdir(cfg):
  704             for ent in os.listdir(cfg):
  705                 if not ent.endswith(config_ext):
  706                     warn('Ignoring file %s not ending in %s' % (os.path.join(cfg, ent), config_ext))
  707                     continue
  708                 filepath = os.path.join(cfg, ent)
  709                 if not os.path.isfile(filepath):
  710                     continue
  711                 cfgfiles.append(filepath)
  712         elif os.path.isfile(cfg):
  713             cfgfiles.append(cfg)
  714         else:
  715             warn('Inaccessible configuration, ignoring: %s' % cfg)
  716 
  717     cfgs = {}
  718     for filepath in cfgfiles:
  719         filename = os.path.splitext(os.path.basename(filepath))[0]
  720 
  721         cfg_file = open(filepath)
  722         cfg_str = cfg_file.read().strip()
  723         cfg_file.close()
  724 
  725         dom = xml.dom.minidom.parseString(cfg_str)
  726         try:
  727             cfg = parse_config(dom.documentElement, filename)
  728         finally:
  729             dom.unlink()
  730         if not cfg: continue
  731         cfg['text'] = cfg_str
  732         if cfg['id'] in cfgs:
  733             raise ConfigException('Duplicate client ID: %s' % cfg['id'])
  734         cfgs[cfg['id']] = cfg
  735 
  736     return cfgs
  737 
  738 ######################################################################
  739 # Script, DB and SNAPSHOT support
  740 #   setup methods can raise exception to signal errors
  741 #   teardown methods must succeed and cleanup the state
  742 ######################################################################
  743 
  744 def check_script_permissions(script):
  745     if not os.path.isfile(script):
  746         return '%s is not a regular file' % script
  747     if not os.access(script, os.X_OK):
  748         return '%s is not executable' % script
  749 
  750     statinfo = os.stat(script)
  751     if statinfo.st_uid and statinfo.st_uid != os.getuid():
  752         return '%s is owned by others' % script
  753 
  754     if statinfo.st_mode & (stat.S_IWGRP | stat.S_IWOTH):
  755         return '%s is writable by others' % script
  756 
  757     return None
  758 
  759 
  760 def _call_script(step, cfg, bdir, mode):
  761     debug('Do %s_side_script: step %s' % (mode, step))
  762 
  763     ret = None
  764     script = cfg['script']
  765 
  766     if script:
  767         if mode == 'server': script = cfg['script'].split(':', 1)[1]
  768         debug('%s_side_script: script = %s' % (mode, script))
  769         if os.path.exists(script):
  770             ret = check_script_permissions(script)
  771             if not ret:
  772                 ret = spawn([script, step, cfg['id'], bdir, mode])
  773         else:
  774             debug('%s_side_script: %s not found' % (mode, script))
  775 
  776     return ret
  777 
  778 
  779 def client_side_script(step, cfg, bdir):
  780     return _call_script(step, cfg, bdir, 'client')
  781 
  782 
  783 def server_side_script(step, cfg, bdir):
  784     return _call_script(step, cfg, bdir, 'server')
  785 
  786 
  787 def do_client_dbdump(cfg):
  788     debug('Doing DB dumps')
  789     ec = 0
  790     for dump in cfg['dumps']:
  791         dbtype = dump['type']
  792         opts = dump['opts']
  793         passwdfile = None
  794         if dbtype in ('postgres', 'postgresql', 'pgsql'):
  795             if dump['db']:
  796                 args = ['pg_dump']
  797                 if dump['save-db-name'] != 'false':
  798                     args.extend(['-C'])
  799             else:
  800                 args = ['pg_dumpall']
  801             if dump['dbuser']:
  802                 args.extend(['-U', dump['dbuser']])
  803             args.extend(opts)
  804             if dump['db']:
  805                 args.extend([dump['db']])
  806             if dump['dbpasswd']:
  807                 (fd, passwdfile) = tempfile.mkstemp()
  808                 f = os.fdopen(fd, 'w')
  809                 f.write('*:*:*:*:%s' % dump['dbpasswd'])
  810                 f.close()
  811 
  812         elif dbtype in ('mysql'):
  813             args = ['mysqldump']
  814             if dump['dbuser']:
  815                 args.extend(['-u', dump['dbuser']])
  816             if dump['dbpasswd']:
  817                 args.extend(['-p%s' % dump['dbpasswd']])
  818             if not dump['db']:
  819                 if dump['save-db-name'] == 'false':
  820                     args.extend(['-n', '-A'])
  821                 else:
  822                     args.extend(['-A'])
  823             args.extend(opts)
  824             if dump['db']:
  825                 if dump['save-db-name'] == 'true':
  826                     args.extend(['-B', dump['db']])
  827                 else:
  828                     args.extend([dump['db']])
  829 
  830         else:
  831             warn('Invalid database type: ' + dbtype)
  832             continue
  833 
  834         if dump['user']:
  835             cmd = ' '.join([shlex.quote(arg) for arg in args])
  836             args = ['su', '-c', cmd, '-', dump['user']]
  837         cmd = ' '.join([shlex.quote(arg) for arg in args])
  838         cmd = '%s > %s' % (cmd, shlex.quote(dump['file']))
  839 
  840         if passwdfile:
  841             os.environ['PGPASSFILE'] = passwdfile
  842         try:
  843             ec = spawn(cmd)
  844         finally:
  845             if passwdfile:
  846                 del os.environ['PGPASSFILE']
  847                 os.remove(passwdfile)
  848         if ec:
  849             warn('Can not dump the database: %s' % dump['db'])
  850     return ec
  851 
  852 def do_client_dbdump_teardown(cfg):
  853     debug('Tear down DB dumps')
  854     for dump in cfg['dumps']:
  855         if dump['cleanup'] != 'true':
  856             continue
  857         try:
  858             os.remove(dump['file'])
  859         except OSError as e:
  860             warn('Unable to remove dump file: %s for database %s because: %s' %
  861                  (dump['file'], dump['db'], e))
  862 
  863 def lvm_snap_information():
  864     lines = call(['lvs', '--separator', ':', '--noheadings']) or ''
  865     lvms = []
  866     for line in lines:
  867         if line.count(':') > 3:
  868             (volume, group, attr, blah1) = line.lstrip().split(':', 3)
  869             if fnmatch.fnmatch(volume, '*_snap_safekeep-*') and attr[0].lower() == 's':
  870                 lvms.append([volume, group])
  871     return lvms
  872 
  873 def mount_information(reverse=False):
  874     lines = call(['mount']) or ''
  875     mounts = []
  876     pattern = re.compile(r"^(\S+) on (.+) type (\S+) \((\S+)\)")
  877     if reverse:
  878         lines.reverse()
  879     for line in lines:
  880         matches = pattern.match(line)
  881         if matches is not None:
  882             mounts.append(matches.groups())
  883     return mounts
  884 
  885 def normalise_lvm_device(device):
  886     return device.replace('/mapper', '').replace('-', '/').replace('//', '-')
  887 
  888 def map_lvm_device(device):
  889     return normalise_lvm_device(device).split('/')[-2:]
  890 
  891 def check_lvm_information(device):
  892     (group, volume) = map_lvm_device(device)
  893     for (lvm_volume, lvm_group) in lvm_snap_information():
  894         if lvm_group == group and lvm_volume.startswith(volume):
  895             return True
  896     return False
  897 
  898 def do_lvremove(device):
  899     (group, volume) = device.split('/')[-2:]
  900     if group == 'mapper':
  901         lvmdev = device
  902     else:
  903         lvmdev = '/dev/mapper/%s-%s' % (group.replace('-', '--'), volume.replace('-', '--'))
  904     if os.path.exists(lvmdev):
  905         for i in range(1, 10):
  906             os.sync()
  907             ret = spawn(['lvremove', '--force', device])
  908             if ret:
  909                 ret = spawn(['dmsetup', 'remove', lvmdev])
  910                 ret = spawn(['dmsetup', 'remove', lvmdev + '-cow'])
  911                 ret = spawn(['lvremove', '--force', device])
  912             if not ret:
  913                 break
  914     else:
  915         warn('lvremove called for non-existent device: %s' % lvmdev)
  916         ret = 0     # Equivalent to lvremove succeeding
  917     return ret
  918 
  919 def generate_snap_information(device, bdir, mountpoint, mounttype):
  920     assert os.path.ismount(mountpoint)
  921     (group, volume) = map_lvm_device(device)
  922     lvmdev = os.path.join('/dev', group, volume)
  923     if bdir[-1] == '/': bdir = bdir[:-1]
  924     snapname = '%s_snap_%s' % (volume, os.path.basename(bdir))
  925     snapdev = os.path.join('/dev', group, snapname)
  926     if os.path.isabs(mountpoint[0]): mountpoint = mountpoint[1:]
  927     return (lvmdev, snapdev, os.path.join(bdir, mountpoint), mounttype)
  928 
  929 def update_mountoptions(mountoptions, writable):
  930     if writable == 'true':
  931         sub = 'rw'
  932     else:
  933         sub = 'ro'
  934     mod = False
  935     options = mountoptions.split(',')
  936     for i in range(len(options)):
  937         if options[i] in ('rw', 'ro'):
  938             options[i] = sub
  939             mod = True
  940     if not mod: options.append(sub)
  941     return ','.join(options)
  942 
  943 def do_client_snap_device(snap, bdir, mountpoint, mounttype, writable):
  944     assert is_temp_root(bdir)
  945     device = snap['device']
  946     debug('Doing FS snapshot for %s' % device)
  947     (lvmdev, snapdev, snapmnt, snaptyp) = generate_snap_information(device, bdir, mountpoint, mounttype)
  948     if not os.path.isdir(snapmnt):
  949         warn('Cannot find the mountpoint %s for %s' % (snapmnt, device))
  950         return False
  951     if 'snapdevice' in snap:
  952         warn('bind mount of snapshot not currently supported: %s' % mountpoint)
  953         return True     # Should be False, but this will still do a good backup
  954     args = ['lvcreate']
  955     if snap['tags']:
  956         for tag in snap['tags']:
  957             args.extend(['--addtag', tag.strip()])
  958     size = snap['size']
  959     if size.count('%'):
  960         if size.endswith('%'): size += 'ORIGIN'
  961         args.extend(['--snapshot', '--extents', size])
  962     else:
  963         args.extend(['--snapshot', '--size', size])
  964     args.extend(['--name', os.path.basename(snapdev), lvmdev])
  965     os.sync()   # Should not be needed except for very old kernels
  966     ec = spawn(args)
  967     if ec:
  968         warn('Can not snapshot the device: %s' % device)
  969         return False
  970 
  971     # no need to mkdir since the mountpoint already exists
  972     args = ['mount', '-t', snaptyp]
  973     if snap['mountoptions']:
  974         mountoptions = snap['mountoptions']
  975     elif snaptyp in default_mountoptions:
  976         mountoptions = default_mountoptions[snaptyp]
  977     elif 'snapshot' in default_mountoptions:
  978         mountoptions = default_mountoptions['snapshot']
  979     if snap['snap_writable']:
  980         writable = snap['snap_writable']
  981     if writable:
  982         mountoptions = update_mountoptions(mountoptions, writable)
  983     if mountoptions:
  984         args.extend(['-o', mountoptions])
  985     args.extend([snapdev, snapmnt])
  986     ec = spawn(args)
  987     if ec:
  988         warn('Can not mount the snapshot: %s' % device)
  989         ret = do_lvremove(snapdev)
  990         if ret:
  991             warn('Can not tear down snapshot: %s' % device)
  992         return False
  993     snap['snapdevice'] = snapdev
  994     snap['mountpoint'] = snapmnt
  995     return True
  996 
  997 def do_client_snap_teardown(cfg, bdir):
  998     assert is_temp_root(bdir)
  999     debug('Tear down FS snapshots dumps')
 1000     snaps = list(cfg['snaps'])
 1001     snaps.reverse()
 1002     for snap in snaps:
 1003         if 'mountpoint' in snap: del snap['mountpoint']
 1004         device = snap['device']
 1005         if not 'snapdevice' in snap:
 1006             warn('No snapdevice for device teardown: %s' % device)
 1007             continue
 1008 
 1009         snapdev = snap['snapdevice']
 1010         debug('Tear down FS snapshot dump for %s -> %s' % (snapdev, device))
 1011 
 1012         ret = do_lvremove(snapdev)
 1013         if ret:
 1014             warn('Can not tear down snapshot: %s' % device)
 1015             continue
 1016         del snap['snapdevice']
 1017 
 1018 def find_snapshot(cfg, device):
 1019     for snap in cfg['snaps']:
 1020         if normalise_lvm_device(snap['device']) == normalise_lvm_device(device):
 1021             debug('find_snapshot device matched: %s' % device)
 1022             return snap
 1023     debug('find_snapshot device no matched for %s' % device)
 1024     return None
 1025 
 1026 def mount_excluded(cfg, mountpoint):
 1027     debug("mount_excluded: %s" % mountpoint)
 1028     if not mountpoint.endswith('/'): mountpoint = mountpoint + '/'
 1029     for clude in cfg['cludes']:
 1030         if clude['type'] == 'exclude' and clude['path']:
 1031             if mountpoint.startswith(clude['path']):
 1032                 debug("mount_excluded: %s: matched %s" % (mountpoint, clude['path']))
 1033                 return True
 1034     debug("mount_excluded: %s: no matches" % mountpoint)
 1035     return False
 1036 
 1037 def do_umount_all(bdir):
 1038     assert is_temp_root(bdir)
 1039     total_ret = 0
 1040     for (device, mountpoint, mounttype, mountoptions) in mount_information(True):
 1041         if mountpoint.startswith(bdir):
 1042             debug("Removing mount %s" % mountpoint)
 1043             ret = spawn(['umount', mountpoint])
 1044             if ret:
 1045                 warn('Can not unmount snapshot: %s' % mountpoint)
 1046                 total_ret += ret
 1047     return total_ret
 1048 
 1049 def do_rbind(cfg, startpath, bdir):
 1050     for (device, mountpoint, mounttype, mountoptions) in mount_information(False):
 1051         debug("Testing %s on %s" % (mountpoint, device))
 1052         if mountpoint.startswith(startpath) and device.startswith('/'):
 1053             if not mount_excluded(cfg, mountpoint):
 1054                 if 'bind' in mountoptions.split(','):
 1055                     warn('bind mount of snapshot not currently supported: %s' % mountpoint)
 1056                     continue
 1057                 snap = find_snapshot(cfg, device)
 1058                 if snap:
 1059                     ret = not do_client_snap_device(snap, bdir, mountpoint, mounttype, cfg['mount_writable'])
 1060                 else:
 1061                     ret = spawn(['mount', '--bind', mountpoint, reroot(bdir, mountpoint)])
 1062                     if ret:
 1063                         debug("mount --bind %s: failed: unwinding" % mountpoint)
 1064                     else:
 1065                         spawn(['mount', '--make-unbindable', reroot(bdir, mountpoint)])
 1066                         if 'bind' in default_mountoptions:
 1067                             mountoptions = default_mountoptions['bind']
 1068                         if cfg['mount_writable']:
 1069                             mountoptions = update_mountoptions(mountoptions, cfg['mount_writable'])
 1070                         if mountoptions:
 1071                             spawn(['mount', '-o',
 1072                                 ('remount,%s,bind' % mountoptions),
 1073                                 mountpoint, reroot(bdir, mountpoint)])
 1074                 if ret:
 1075                     ret = spawn(['umount', reroot(bdir, startpath)])
 1076                     if ret:
 1077                         warn('Failed to unmount: %s' % reroot(bdir, startpath))
 1078                     do_client_snap_teardown(cfg, bdir)
 1079                     return 1
 1080 
 1081     return 0
 1082 
 1083 ######################################################################
 1084 # Client implementation
 1085 ######################################################################
 1086 
 1087 def do_client_config(cmd):
 1088     cfgStr = ''
 1089 
 1090     (cfg_cmd, cnt_str, dflt_id) = cmd.split(':', 2)
 1091     for i in range(int(cnt_str)):
 1092         line = sys.stdin.readline()
 1093         if not line: raise ConfigException('Unexpected end of file')
 1094         cfgStr += line
 1095 
 1096     return do_client_config_parse(cfgStr, dflt_id)
 1097 
 1098 def do_client_config_parse(cfgStr, dflt_id=None):
 1099     dom = xml.dom.minidom.parseString(cfgStr)
 1100     try:
 1101         return parse_config(dom.documentElement, dflt_id)
 1102     finally:
 1103         dom.unlink()
 1104 
 1105 def do_client_remote_script(script_file, cfg, cmd):
 1106     (cfg_cmd, server_file, cnt_str) = cmd.split(':', 2)
 1107     debug("do_client_remote_script: %s -> %s: cnt_str = %s" % (server_file, cfg['script'], cnt_str.strip()))
 1108     if int(cnt_str) > 0:
 1109         try:
 1110             for i in range(int(cnt_str)):
 1111                 line = sys.stdin.readline()
 1112                 if not line: raise Exception('Unexpected end of file')
 1113                 script_file.write(line)
 1114         finally:
 1115             script_file.close()
 1116 
 1117         os.chmod(cfg['script'], stat.S_IXUSR | stat.S_IRUSR)
 1118         return True
 1119     else:
 1120         script_file.close()
 1121         os.remove(cfg['script'])
 1122 
 1123     return False
 1124 
 1125 def do_client_setup(cfg):
 1126     debug('Do setup of %s' % cfg['host'])
 1127 
 1128     total_ret = do_client_dbdump(cfg)
 1129 
 1130     if len(cfg['snaps']) > 0:
 1131         debug('Checking FS snapshots')
 1132         for snap in cfg['snaps']:
 1133             device = snap['device']
 1134             if check_lvm_information(device) and not do_client_scrub():
 1135                 raise Exception("Previous snapshots found for %s and automatic correction failed: run 'safekeep --server --cleanup %s' to correct" % (device, cfg['host']))
 1136 
 1137         ret = spawn(['modprobe', 'dm-snapshot'])
 1138         if ret:
 1139             warn('modprobe dm-snapshot failed, continuing')
 1140             total_ret += ret
 1141         bdir = tempfile.mkdtemp(suffix="-rbind", prefix="safekeep-%d-" % current_pid, dir="/mnt")
 1142         ret = do_rbind(cfg, '/', bdir)
 1143         if ret:
 1144             warn('mount --rbind failed, snapshotting will be disabled')
 1145             total_ret += ret
 1146             try:
 1147                 os.rmdir(bdir)
 1148             except OSError as e:
 1149                 warn('Failed to remove: %s: %s' % (bdir, e))
 1150                 total_ret += 1
 1151             bdir = '/'
 1152     else:
 1153         bdir = '/'
 1154     debug('Working root is %s' % bdir)
 1155 
 1156     return (bdir, total_ret)
 1157 
 1158 def do_client_cleanup(cfg, bdir):
 1159     debug('Do cleanup of %s in %s' % (cfg['host'], bdir))
 1160     if is_temp_root(bdir):
 1161         ret = do_umount_all(bdir)
 1162         if ret:
 1163             warn('Failed to unmount tree: %s' % bdir)
 1164         else:
 1165             try:
 1166                 os.rmdir(bdir)
 1167             except OSError as e:
 1168                 warn('Unable to remove: %s: %s' % (bdir, e))
 1169 
 1170         do_client_snap_teardown(cfg, bdir)
 1171 
 1172     do_client_dbdump_teardown(cfg)
 1173 
 1174 def do_client_compat(server_versions):
 1175     debug('Server versions: %s' % server_versions)
 1176     (server_protocol, server_version) = server_versions.split(',')
 1177     compat = {
 1178         'client': get_protocol_info(PROTOCOL, True),
 1179         'server': get_protocol_info(server_protocol, False)
 1180     }
 1181 
 1182     return compat
 1183 
 1184 
 1185 def do_client_scrub():
 1186     debug("Do client scrub loop")
 1187 
 1188     if os.getuid():
 1189         if is_client:
 1190             raise Exception('client not running as root')
 1191         else:
 1192             warn('--cleanup should be run as root on client')
 1193             info('No cleanup performed')
 1194     else:
 1195         scrubbed = False
 1196 
 1197         # Go through and see if any come from existing safekeep processes
 1198         pattern = re.compile(r"_snap_safekeep-(\d+)-")
 1199         for (volume, group) in lvm_snap_information():
 1200             matches = pattern.search(volume)
 1201             if matches is not None:
 1202                 pid = matches.group(1)
 1203                 # Look up /proc/<pid>/cmdline to see what process is running
 1204                 proc_file = "/proc/" + pid + "/cmdline"
 1205                 if pid != current_pid and os.path.exists(proc_file):
 1206                     fin = open(proc_file, "r")
 1207                     try:
 1208                         (cmd, arg0, args) = fin.read().split('\0', 2)
 1209                     except:
 1210                         arg0 = ''
 1211                     fin.close()
 1212                     if os.path.basename(arg0) == "safekeep":
 1213                         raise Exception('another safekeep process running: pid %s' % pid)
 1214 
 1215         if os.environ['PATH'][-1] == ':':
 1216             os.environ['PATH'] += '/sbin:/usr/sbin:/usr/local/sbin:'
 1217         else:
 1218             os.environ['PATH'] += ':/sbin:/usr/sbin:/usr/local/sbin'
 1219 
 1220         # Go through and unmount anythings that are still hanging around
 1221 
 1222         debug("Cleaning up existing mounts")
 1223         for (device, mountpoint, mounttype, mountoptions) in mount_information(True):
 1224             if mountpoint.startswith('/mnt/safekeep-'):
 1225                 info("Removing mount %s" % mountpoint)
 1226                 ret = spawn(['umount', mountpoint])
 1227                 if ret:
 1228                     warn('Can not unmount the snapshot: %s' % mountpoint)
 1229                 if fnmatch.fnmatch(device, '*_snap_safekeep-*'):
 1230                     info("Removing snapshot %s" % device)
 1231                     ret = do_lvremove(device)
 1232                     if ret:
 1233                         warn('Can not tear down snapshot: %s' % device)
 1234                 scrubbed = True
 1235 
 1236         # Now cleanup any snapshots still hanging around
 1237 
 1238         debug("Cleaning up remaining snapshots")
 1239         for (volume, group) in lvm_snap_information():
 1240             device = os.path.join('/dev', group, volume)
 1241             info("Removing snapshot %s" % device)
 1242             ret = do_lvremove(device)
 1243             if ret:
 1244                 warn('Can not tear down snapshot: %s' % device)
 1245             scrubbed = True
 1246 
 1247         # Now cleanup any safekeep directories and script files still hanging around
 1248 
 1249         debug("Cleaning up remaining safekeep directories")
 1250         if os.path.isdir('/mnt'):
 1251             for ent in os.listdir('/mnt'):
 1252                 mountpoint = os.path.join('/mnt', ent)
 1253                 if ent.startswith('safekeep-') and os.path.isdir(mountpoint):
 1254                     info("Removing rbind directory %s" % mountpoint)
 1255                     try:
 1256                         os.rmdir(mountpoint)
 1257                     except OSError as e:
 1258                         warn('Failed to remove: %s: %s' % (mountpoint, e))
 1259 
 1260         debug("Cleaning up remaining safekeep script files")
 1261         script_dir = tempfile.gettempdir()
 1262         if os.path.isdir(script_dir):
 1263             for ent in os.listdir(script_dir):
 1264                 if fnmatch.fnmatch(ent, 'safekeep-[0-9][0-9]*-') and not fnmatch.fnmatch(ent, 'safekeep-%d-' % current_pid):
 1265                     script_file = '%s/%s' % (script_dir, ent)
 1266                     if os.path.isdir(script_file):
 1267                         temp_dir = script_file
 1268                         for ent in os.listdir(temp_dir):
 1269                             script_file = '%s/%s' % (temp_dir, ent)
 1270                             info("Removing script file %s" % script_file)
 1271                             try:
 1272                                 os.remove('%s' % script_file)
 1273                             except OSError as e:
 1274                                 warn('Failed to remove: %s: %s' % (script_file, e))
 1275                         info("Removing script directory %s" % temp_dir)
 1276                         try:
 1277                             os.rmdir(temp_dir)
 1278                         except OSError as e:
 1279                             warn('Failed to remove: %s: %s' % (temp_dir, e))
 1280                     else:
 1281                         info("Removing script file %s" % script_file)
 1282                         try:
 1283                             os.remove('%s' % script_file)
 1284                         except OSError as e:
 1285                             warn('Failed to remove: %s: %s' % (script_file, e))
 1286 
 1287         if not scrubbed:
 1288             info('No cleanup required')
 1289 
 1290         # This has to be rerun to see if it has been successful
 1291         if lvm_snap_information():
 1292             return False
 1293         return True
 1294 
 1295     return False
 1296 
 1297 def do_client():
 1298     debug("Do client main loop")
 1299     should_cleanup = True
 1300     bdir = '/'
 1301     cfg = do_client_config_parse('<backup/>', 'def')
 1302     ex = None
 1303     script_file = None
 1304     script_dir = None
 1305     try:
 1306         while True:
 1307             try:
 1308                 line = sys.stdin.readline()
 1309                 if line.startswith('ALOHA'):
 1310                     compat = do_client_compat(line.strip().split(':', 1)[1])
 1311                     send_status = ('CL_STATUS' in compat['server']['caps'])
 1312                     send('OK %s, %s' % (PROTOCOL, VERSION))
 1313                 elif line.startswith('DEFAULT'):
 1314                     for opts in line.strip().split(':', 1)[1].split(','):
 1315                         opt, val = opts.strip().split('=')
 1316                         if opt == 'snapshot.size':
 1317                             global default_snapshot
 1318                             default_snapshot = val
 1319                     if send_status:
 1320                         send('OK 0')
 1321                     else:
 1322                         send('OK')
 1323                 elif line.startswith('CONFIG'):
 1324                     cfg = do_client_config(line)
 1325                     if cfg['script'] and cfg['run_on'] == 'client':
 1326                         if ':' in cfg['script']:
 1327                             (script_loc, script) = cfg['script'].split(':', 1)
 1328                         else:
 1329                             (script_loc, script) = ('client', cfg['script'])
 1330                         if script_loc == 'server':
 1331                             if not script_dir:
 1332                                 script_dir = tempfile.mkdtemp(prefix="safekeep-%d-" % current_pid)
 1333                                 tempfile.tempdir = script_dir
 1334                             script = os.path.basename(script)
 1335                             (fd, cfg['script']) = tempfile.mkstemp(prefix="%s-" % script, dir=script_dir)
 1336                             script_file = os.fdopen(fd, 'w')
 1337                             if send_status:
 1338                                 send('OK %d %s' % (0, cfg['script']))
 1339                             else:
 1340                                 send('OK %s' % cfg['script'])
 1341                         elif script_loc == 'client':
 1342                             cfg['script'] = script
 1343                             ret = client_side_script('STARTUP', cfg, bdir)
 1344                             if ret:
 1345                                 send('ERROR Client-side setup script failed: %s' % ret)
 1346                             elif send_status:
 1347                                 send('OK 0')
 1348                             else:
 1349                                 send('OK')
 1350                         else:
 1351                             warn('Unknown script location %s for script %s' % (script_loc, script))
 1352                             if send_status:
 1353                                 send('OK 1')
 1354                             else:
 1355                                 send('OK')
 1356                     elif send_status:
 1357                         send('OK 0')
 1358                     else:
 1359                         send('OK')
 1360                 elif line.startswith('SCRIPT'):
 1361                     if do_client_remote_script(script_file, cfg, line):
 1362                         ret = client_side_script('STARTUP', cfg, bdir)
 1363                         if ret:
 1364                             send('ERROR Client-side setup script failed: %s' % ret)
 1365                         elif send_status:
 1366                             send('OK 0')
 1367                         else:
 1368                             send('OK')
 1369                     else:
 1370                         script_file = None
 1371                         cfg['script'] = None
 1372                         if send_status:
 1373                             send('OK 0')
 1374                         else:
 1375                             send('OK')
 1376                 elif line.startswith('SETUP'):
 1377                     status = 0
 1378                     ret = client_side_script('PRE-SETUP', cfg, bdir)
 1379                     if ret: status += 1
 1380                     bdir, ret = do_client_setup(cfg)
 1381                     if ret: status += 1
 1382                     ret = client_side_script('POST-SETUP', cfg, bdir)
 1383                     if ret: status += 1
 1384                     if send_status:
 1385                         send('OK %d %s' % (status, bdir))
 1386                     else:
 1387                         send('OK %s' % bdir)
 1388                 elif line.startswith('CLEANUP'):
 1389                     status = 0
 1390                     path = line[7:].strip()
 1391                     if path == bdir: should_cleanup = False
 1392                     do_client_cleanup(cfg, path)
 1393                     ret = client_side_script('POST-BACKUP', cfg, bdir)
 1394                     if ret: status += 1
 1395                     if send_status:
 1396                         send('OK %d' % status)
 1397                     else:
 1398                         send('OK')
 1399                 elif line.startswith('SCRUB'):
 1400                     status = 0
 1401                     do_client_scrub()
 1402                     ret = client_side_script('POST-SCRUB', cfg, bdir)
 1403                     if ret: status += 1
 1404                     if send_status:
 1405                         send('OK %d' % status)
 1406                     else:
 1407                         send('OK')
 1408                 elif not line:
 1409                     break
 1410                 else:
 1411                     send('ERROR Unknown command: %s' % line)
 1412                     break
 1413             except Exception as e:
 1414                 ex = e
 1415                 break
 1416     finally:
 1417         if should_cleanup:
 1418             do_client_cleanup(cfg, bdir)
 1419         if script_file:
 1420             if not script_file.closed: script_file.close()
 1421             os.remove(cfg['script'])
 1422         if script_dir:
 1423             tempfile.tempdir = None
 1424             try:
 1425                 os.rmdir(script_dir)
 1426             except OSError as e:
 1427                 warn('Failed to remove: %s: %s' % (script_dir, e))
 1428 
 1429     if ex:
 1430         send('TRACEBACK ' + str(ex)  + '>>>' + stacktrace().replace('\n', '###'))
 1431 
 1432 ######################################################################
 1433 # Server implementation
 1434 ######################################################################
 1435 
 1436 def executable_lookup(cfg, executable, location):
 1437     key = 'exec_' + location
 1438 
 1439     for option in cfg['options']:
 1440         if executable in option:
 1441             if key in option[executable]:
 1442                 return option[executable][key]
 1443     return executable
 1444 
 1445 def do_server_getanswer(cout, cl_status):
 1446     while True:
 1447         line = cout.readline()
 1448         if line.startswith('OK'):
 1449             if cl_status:
 1450                 status_s = line[2:-1].strip()
 1451                 if ' ' in status_s:
 1452                     status_s, ret = status_s.split(None, 1)
 1453                 else:
 1454                     ret = None
 1455                 return(int(status_s), ret)
 1456             else:
 1457                 return (0, line[2:-1].strip())
 1458         elif line.startswith('ERROR'):
 1459             raise ClientException(line[5:].strip())
 1460         elif line.startswith('TRACEBACK'):
 1461             i = line.find('>>>')
 1462             raise ClientException(line[10:i].strip(), line[i+3:].replace('###', '\n').rstrip())
 1463         elif not line:
 1464             raise Exception('client died unexpectedly')
 1465         else:
 1466             log(line[:-1])
 1467 
 1468 def do_server_rdiff(cfg, bdir, nice, ionice, force):
 1469     args = []
 1470 
 1471     if nice:
 1472         args.extend(['nice', '-n' + str(nice)])
 1473 
 1474     ionice_cmd = 'ionice'
 1475     if ionice and ionice != 'none':
 1476         ionice_out = try_to_run([ionice_cmd, '-h'])
 1477         if ionice_out is not None:
 1478             ionice_args = []
 1479             if ionice == 'idle':
 1480                 ionice_args.extend(['-c3'])
 1481             else:
 1482                 ionice_args.extend(['-c2', '-n%s' % (ionice)])
 1483 
 1484             if ''.join(ionice_out).find('-t') > 0:
 1485                 ionice_args.extend(['-t'])
 1486 
 1487             if try_to_run([ionice_cmd] + ionice_args + ['/bin/true']) is not None:
 1488                 args.append(ionice_cmd)
 1489                 args.extend(ionice_args)
 1490             else:
 1491                 warn('ionice(1) fails, ignoring ionice.adjustment')
 1492         else:
 1493             warn('ionice(1) not available, ignoring ionice.adjustment')
 1494 
 1495     # handle bandwidth limiting via trickle
 1496     def get_bw(vals, d):
 1497         return vals.get(d) or vals.get('overall')
 1498     def get_bandwidth(cfg, d):
 1499         return get_bw(cfg['bw'], d) or get_bw(default_bandwidth, d)
 1500     trickle = []
 1501     limit_dl = get_bandwidth(cfg, 'download')
 1502     limit_ul = get_bandwidth(cfg, 'upload')
 1503     if limit_dl or limit_ul:
 1504         trickle.extend([trickle_cmd])
 1505         if verbosity_trickle: trickle.extend([verbosity_trickle])
 1506         if limit_dl:
 1507             trickle.extend(['-d', str(limit_dl)])
 1508         if limit_ul:
 1509             trickle.extend(['-u', str(limit_ul)])
 1510     if len(trickle):
 1511         if try_to_run([trickle_cmd, '-V']) is None:
 1512             warn('Trickle not available, bandwidth limiting disabled')
 1513             trickle = []
 1514     args.extend(trickle)
 1515 
 1516     args.extend([executable_lookup(cfg, 'rdiff-backup', 'local')])
 1517 
 1518     if cfg['host']:
 1519         basessh = 'ssh -oStrictHostKeyChecking=%s' % (ssh_StrictHostKeyChecking)
 1520         if cfg['port']: basessh += ' -p %s' % cfg['port']
 1521         schema = '%s %s -i %s %%s %s --server' % (basessh, verbosity_ssh, cfg['key_data'], executable_lookup(cfg, 'rdiff-backup', 'remote'))
 1522         args.extend(['--remote-schema', schema])
 1523 
 1524     if force:
 1525         args.extend(['--force'])
 1526 
 1527     if backup_tempdir:
 1528         args.extend(['--tempdir', backup_tempdir])
 1529 
 1530     options_append = []
 1531 
 1532     special_files = []
 1533     if cfg['data_options'].get('exclude-devices') == 'true':
 1534         special_files.extend(['--exclude-device-files'])
 1535     if cfg['data_options'].get('exclude-sockets') == 'true':
 1536         special_files.extend(['--exclude-sockets'])
 1537     if cfg['data_options'].get('exclude-fifos') == 'true':
 1538         special_files.extend(['--exclude-fifos'])
 1539 
 1540     for option in cfg['options']:
 1541         if 'special-files' in option:
 1542             if 'include' in option['special-files']:
 1543                 if option['special-files']['include'].lower() in ('true', 'yes', '1'):
 1544                     special_files = ['--include-special-files']
 1545 
 1546         # Note if we ever add other backends this section should only be run
 1547         # when rback-diff is the current option.
 1548 
 1549         if 'rdiff-backup' in option:
 1550             if 'append' in option['rdiff-backup']:
 1551                 options_append.extend(option['rdiff-backup']['append'].split(None))
 1552 
 1553     args.extend(special_files)
 1554     args.extend(options_append)
 1555 
 1556     for clude in cfg['cludes']:
 1557         opt = '--' + clude['type']
 1558         if clude['path']:
 1559             args.extend([opt, reroot(bdir, clude['path'])])
 1560         if clude['glob']:
 1561             args.extend([opt, reroot(bdir, clude['glob'])])
 1562         if clude['regexp']:
 1563             args.extend([opt + '-regexp', bdir + clude['regexp']])
 1564 
 1565     userhost = ''
 1566     if cfg['host']:
 1567         userhost = '%s@%s::' % (cfg['user'], cfg['host'])
 1568     args.extend([userhost + bdir, cfg['dir']])
 1569     ret = spawn(args)
 1570     if ret:
 1571         raise Exception('Failed to run %s' % executable_lookup(cfg, 'rdiff-backup', 'local'))
 1572 
 1573 def do_server_rdiff_cleanup(cfg):
 1574     args = [executable_lookup(cfg, 'rdiff-backup', 'local')]
 1575     if backup_tempdir:
 1576         args.extend(['--tempdir', backup_tempdir])
 1577     args.extend(['--check-destination-dir', cfg['dir']])
 1578     ret = spawn(args)
 1579     if ret:
 1580         warn('Failed to cleanup old data, please fix the problem manually')
 1581 
 1582 def do_server_data_cleanup(cfg):
 1583     args = [executable_lookup(cfg, 'rdiff-backup', 'local')]
 1584     if backup_tempdir:
 1585         args.extend(['--tempdir', backup_tempdir])
 1586     args.extend(['--force', '--remove-older-than', cfg['retention'], cfg['dir']])
 1587     ret = spawn(args)
 1588     if ret:
 1589         warn('Failed to cleanup old data, please fix the problem manually')
 1590 
 1591 def get_protocol_info(protocol, is_client):
 1592     (major_s, minor_s) = protocol.strip().split('.')
 1593     major = int(major_s)
 1594     minor = int(minor_s)
 1595 
 1596     caps = []
 1597     if major == 1:
 1598         if minor >= 3:
 1599             caps.append('DEFAULT')
 1600         if minor >= 4:
 1601             caps.append('CL_STATUS')
 1602 
 1603     return {
 1604         'version': (major, minor),
 1605         'caps': caps
 1606     }
 1607 
 1608 def do_server_compat(client_versions):
 1609     (client_protocol, client_version) = client_versions.split(',')
 1610     compat = {
 1611         'client': get_protocol_info(client_protocol, True),
 1612         'server': get_protocol_info(PROTOCOL, False)
 1613     }
 1614 
 1615     (server_major, server_minor) = PROTOCOL.split('.')
 1616     if compat['server']['version'][0] != compat['client']['version'][0]:
 1617         raise Exception('Incompatible protocols: %s <> %s' % (PROTOCOL, client_protocol))
 1618     elif compat['server']['version'][1] > compat['client']['version'][1]:
 1619         info('Protocol mismatch, but compatible: %s <> %s' % (PROTOCOL, client_protocol))
 1620     return compat
 1621 
 1622 def do_server(cfgs, ids, nice, ionice, force, cleanup):
 1623     global statistics, client_defaults, error_counter, warning_counter
 1624     debug("Do server main loop")
 1625     output_done = False
 1626     for cfg in sorted(iter(cfgs.values()), key=lambda cfg: cfg['id']):
 1627         cfg_id = cfg['id']
 1628         if ids and cfg_id not in ids: continue
 1629         info('------------------------------------------------------------------')
 1630         stats = {}
 1631         info('Server backup starting for client %s' % cfg_id)
 1632         stats['id'] = cfg_id
 1633         output_done = True
 1634 
 1635         cleaned_up = not cleanup
 1636         try:
 1637             cin = None
 1638             cout = None
 1639             fbm = None
 1640             setup_errs = 0
 1641             if cfg['host']:
 1642                 if not os.path.isfile(cfg['key_ctrl']):
 1643                     raise Exception('Client %(id)s missing ctrl key %(key_ctrl)s' % cfg)
 1644                 if not os.path.isfile(cfg['key_data']):
 1645                     raise Exception('Client %(id)s missing data key %(id)s' % cfg)
 1646 
 1647             datadir = os.path.join(os.getcwd(), cfg['dir'])
 1648             if not os.path.isdir(datadir):
 1649                 try:
 1650                     os.makedirs(datadir)
 1651                 except EnvironmentError as ex:
 1652                     raise Exception('Can not create data store dir: %s: %s' % (datadir, ex))
 1653 
 1654             rdiff_logdir = os.path.join(datadir, 'rdiff-backup-data')
 1655 
 1656             backup_log = os.path.join(rdiff_logdir, 'backup.log')
 1657             if os.path.isfile(backup_log):
 1658                 fbm = open(backup_log, 'a')
 1659                 try:
 1660                     fcntl.flock(fbm.fileno(), fcntl.LOCK_EX|fcntl.LOCK_NB)
 1661                 except IOError as ex:
 1662                     warn('Another backup or cleanup is in progress for client %s' % cfg_id)
 1663                     raise Exception('Another backup or cleanup is in progress')
 1664 
 1665             if cfg['retention'] and os.path.isdir(rdiff_logdir) and not cleanup:
 1666                 do_server_data_cleanup(cfg)
 1667 
 1668             cmd = []
 1669             if cfg['host']:
 1670                 cmd.extend(['ssh'])
 1671                 if verbosity_ssh: cmd.extend([verbosity_ssh])
 1672                 if cfg['port']: cmd.extend(['-p', cfg['port']])
 1673                 cmd.extend(['-oStrictHostKeyChecking=%s' % (ssh_StrictHostKeyChecking)])
 1674                 cmd.extend(['-T', '-i', cfg['key_ctrl'], '-l', cfg['user'], cfg['host']])
 1675             cmd.extend(['safekeep', '--client'])
 1676 
 1677             subp = subprocess.Popen(cmd, stdin=PIPE, stdout=PIPE, encoding='utf-8', errors='backslashreplace')
 1678             cin = subp.stdin
 1679             cout = subp.stdout
 1680 
 1681             cin.write('ALOHA: %s, %s\n' % (PROTOCOL, VERSION))
 1682             cin.flush()
 1683             client_versions = (do_server_getanswer(cout, False))[1]
 1684             compat = do_server_compat(client_versions)
 1685             cl_status = ('CL_STATUS' in compat['client']['caps'])
 1686 
 1687             # This test will need to be improved for later PROTOCOL versions.
 1688             if ('DEFAULT' in compat['client']['caps']) and len(client_defaults):
 1689                 cin.write('DEFAULT: %s\n' % (','.join(client_defaults)))
 1690                 cin.flush()
 1691                 setup_errs += (do_server_getanswer(cout, cl_status))[0]
 1692 
 1693             cin.write('CONFIG: %d: %s\n' % (len(cfg['text'].splitlines()), cfg_id))
 1694             cin.write(cfg['text'] + '\n')
 1695             cin.flush()
 1696             errs, remote_script = do_server_getanswer(cout, cl_status)
 1697             setup_errs += errs
 1698             # run a server-side hook as the first thing
 1699             if cfg['script'] and cfg['run_on'] == 'server':
 1700                 ret = server_side_script('STARTUP', cfg, datadir)
 1701                 if ret:
 1702                     error('Server-side setup script failed: %s' % ret)
 1703                     raise Exception('Server-side setup script failed: %s' % ret)
 1704 
 1705             if cfg['script'] and cfg['script'].startswith('server:')\
 1706                     and remote_script and cfg['run_on'] == 'client':
 1707                 local_script = cfg['script'].split(':', 1)[1]
 1708                 if os.path.isfile(local_script):
 1709                     ret = check_script_permissions(local_script)
 1710                     if not ret:
 1711                         debug("Transferring script: %s -> %s" % (local_script, remote_script))
 1712                         fscript = open(local_script)
 1713                         lines = fscript.readlines()
 1714                         fscript.close()
 1715                         cin.write('SCRIPT: %s: %d\n' % (local_script, len(lines)))
 1716                         cin.writelines(lines)
 1717                     else:
 1718                         error('Illegal script specified: %s: %s' % (local_script, ret))
 1719                         cin.write('SCRIPT: %s: %d\n' % ('-', 0))
 1720                 else:
 1721                     warn('No server based script found: %s' % local_script)
 1722                     cin.write('SCRIPT: %s: %d\n' % ('-', 0))
 1723                 cin.flush()
 1724                 setup_errs += (do_server_getanswer(cout, cl_status))[0]
 1725             if cleanup:
 1726                 cleaned_up = False
 1727                 cin.write('SCRUB\n')
 1728                 cin.flush()
 1729                 setup_errs += (do_server_getanswer(cout, cl_status))[0]
 1730                 bdir = '/'  # Fake directory for the rest of the cleanup
 1731                 errs = 0
 1732                 stats['state'] = 'CLEAN'
 1733                 # run a server-side hook following client SCRUB
 1734                 if cfg['script'] and cfg['run_on'] == 'server':
 1735                     setup_errs += server_side_script('POST-SCRUB', cfg, datadir)
 1736             else:
 1737                 # run a server-side hook prior to client SETUP
 1738                 if cfg['script'] and cfg['run_on'] == 'server':
 1739                     setup_errs += server_side_script('PRE-SETUP', cfg, datadir)
 1740 
 1741                 cin.write('SETUP\n')
 1742                 cin.flush()
 1743                 errs, bdir = do_server_getanswer(cout, cl_status)
 1744                 setup_errs += errs
 1745 
 1746                 # run a server-side hook following client SETUP
 1747                 if cfg['script'] and cfg['run_on'] == 'server':
 1748                     setup_errs += server_side_script('POST-SETUP', cfg, datadir)
 1749 
 1750                 if os.path.isdir(rdiff_logdir):
 1751                     rdiff_logpre = os.listdir(rdiff_logdir)
 1752                 else:
 1753                     rdiff_logpre = []
 1754 
 1755                 if fbm:
 1756                     backup_marker = '=== Backup session on %s ===' % time.asctime()
 1757                     fbm.write(backup_marker + '\n')
 1758                 else:
 1759                     backup_marker = None
 1760 
 1761                 cleaned_up = False
 1762                 do_server_rdiff(cfg, bdir, nice, ionice, force)
 1763                 cleaned_up = True
 1764 
 1765                 errs = 0
 1766                 if os.path.isdir(rdiff_logdir):
 1767                     info_file(backup_log, backup_marker, None)
 1768                     rdiff_logpost = os.listdir(rdiff_logdir)
 1769                     for lfn in rdiff_logpost:
 1770                         if lfn.startswith('session_statistics.') and lfn.endswith('.data') and lfn not in rdiff_logpre:
 1771                             errs += info_file(os.path.join(rdiff_logdir, lfn), None, stats)
 1772                             stats['state'] = 'OK'
 1773                             stats['Errors'] = str(errs + setup_errs)
 1774                 else:
 1775                     warn('Log dir does not exist.')
 1776                     warning_counter += 1
 1777                     stats['state'] = 'MISSING'
 1778 
 1779             cin.write('CLEANUP %s\n' % bdir)
 1780             cin.flush()
 1781             setup_errs += (do_server_getanswer(cout, cl_status))[0]
 1782 
 1783             # run a server-side hook as the last thing
 1784             if cfg['script'] and cfg['run_on'] == 'server':
 1785                 setup_errs += server_side_script('POST-BACKUP', cfg, datadir)
 1786 
 1787             if (errs + setup_errs) == 0:
 1788                 info('Server backup for client %s: OK' % cfg_id)
 1789             elif setup_errs == 0:
 1790                 warning_counter += 1
 1791                 stats['state'] = 'WARNING'
 1792                 info('Server backup for client %s: OK (%d WARNINGS)' % (cfg_id, errs))
 1793             else:
 1794                 warning_counter += 1
 1795                 stats['state'] = 'SETUP WARNING'
 1796                 info('Server backup for client %s: OK (%d WARNINGS/%d SETUP WARNINGS)' % (cfg_id, errs, setup_errs))
 1797 
 1798         except Exception as ex:
 1799             if cleanup:
 1800                 info('Client-side cleanup for client %s: FAILED' % cfg_id)
 1801                 if isinstance(ex, ClientException):
 1802                     if ex.traceback and verbosity_level > 2:
 1803                         info(ex.traceback)
 1804                     else:
 1805                         info('Reason: %s' % (ex or ''))
 1806             else:
 1807                 if isinstance(ex, ClientException):
 1808                     error('Client %s: FAILED due to: %s' % (cfg_id, ex or ''))
 1809                     if ex.traceback and verbosity_level > 2: error(ex.traceback)
 1810                 else:
 1811                     error('Server backup for client %s: FAILED' % cfg_id, ex)
 1812             error_counter += 1
 1813             stats['state'] = 'FAILED'
 1814 
 1815         statistics.append(stats)
 1816 
 1817         # Shutdown client
 1818         if fbm: fbm.close()
 1819         if cout: cout.close()
 1820         if cin: cin.close()
 1821 
 1822         if not cleaned_up:
 1823             do_server_rdiff_cleanup(cfg)
 1824             cleaned_up = True
 1825 
 1826     if output_done:
 1827         info('------------------------------------------------------------------')
 1828     debug('Server backup done')
 1829 
 1830 def do_list(cfgs, ids, list_type, list_date, list_parsable):
 1831     global statistics, error_counter
 1832     debug("Do server listing main loop")
 1833     output_done = False
 1834     for cfg in sorted(iter(cfgs.values()), key=lambda cfg: cfg['id']):
 1835         cfg_id = cfg['id']
 1836         if ids and cfg_id not in ids: continue
 1837         stats = {}
 1838         if list_parsable:
 1839             info('Client: %s' % cfg_id)
 1840         else:
 1841             info('------------------------------------------------------------------')
 1842             info('Server listing for client %s' % cfg_id)
 1843         stats['id'] = cfg_id
 1844         output_done = True
 1845 
 1846         args = [executable_lookup(cfg, 'rdiff-backup', 'local')]
 1847 
 1848         if list_type == 'increments':
 1849             args.extend(['--list-increments'])
 1850         elif list_type == 'sizes':
 1851             args.extend(['--list-increment-sizes'])
 1852         elif list_type == 'changed':
 1853             args.extend(['--list-changed-since', list_date])
 1854         elif list_type == 'attime':
 1855             args.extend(['--list-at-time', list_date])
 1856         else:
 1857             assert False, 'Unknown list type: ' + list_type
 1858 
 1859         if list_parsable:
 1860             args.extend(['--parsable-output'])
 1861 
 1862         args.extend([cfg['dir']])
 1863         # Call a low level routine to get the data back as well.
 1864         ret, lines = _spawn(args)
 1865         if ret:
 1866             error('Failed to run %s' % executable_lookup(cfg, 'rdiff-backup', 'local'))
 1867             error_counter += 1
 1868             stats['state'] = 'FAILED'
 1869         else:
 1870             stats['state'] = 'OK'
 1871             if list_type == 'increments':
 1872                 if list_parsable:
 1873                     stats['Increments'] = str(len(lines) - 1)
 1874                     date_time = lines[len(lines) - 1].split(None, 1)[0]
 1875                     stats['CurrentMirror'] = time.ctime(int(date_time))
 1876                     date_time = lines[0].split(None, 1)[0]
 1877                     stats['OldestIncrement'] = time.ctime(int(date_time))
 1878                 else:
 1879                     increments = len(lines) - 2
 1880                     stats['Increments'] = str(increments)
 1881                     stats['CurrentMirror'] = lines[len(lines) - 1].strip().split(None, 2)[2]
 1882                     if increments == 0:
 1883                         stats['OldestIncrement'] = lines[1].strip().split(None, 2)[2]
 1884                     else:
 1885                         stats['OldestIncrement'] = lines[1].strip().split(None, 1)[1]
 1886             elif list_type == 'sizes':
 1887                 stats['Increments'] = str(len(lines) - 3)
 1888                 stats['CurrentMirror'] = lines[2].split('   ', 1)[0]
 1889                 stats['OldestIncrement'] = lines[len(lines) - 1].split('   ', 1)[0]
 1890 
 1891         statistics.append(stats)
 1892 
 1893     if output_done and not list_parsable:
 1894         info('------------------------------------------------------------------')
 1895     debug('Server listing done')
 1896 
 1897 def do_keys(cfgs, ids, nice_rem, identity, status, dump, deploy):
 1898     for cfg in sorted(iter(cfgs.values()), key=lambda cfg: cfg['id']):
 1899         cfg_id = cfg['id']
 1900         if ids and cfg_id not in ids: continue
 1901         info('Handling keys for client: %s' % cfg_id)
 1902         if not cfg['host']:
 1903             info('%s: Client is local, it needs no keys' % cfg_id)
 1904             continue
 1905 
 1906         nice = cfg['nice'] or nice_rem
 1907         if nice:
 1908             nice_cmd = 'nice -n%s ' % (nice)
 1909         else:
 1910             nice_cmd = ''
 1911 
 1912         cmds = ['safekeep --client', '%s --server --restrict-read-only /' % executable_lookup(cfg, 'rdiff-backup', 'remote')]
 1913         privatekeyfiles = [cfg.get('key_ctrl'), cfg.get('key_data')]
 1914         output = []
 1915         keys_ok = False
 1916         for (cmd, privatekeyfile) in zip(cmds, privatekeyfiles):
 1917             publickeyfile = privatekeyfile + '.pub'
 1918             if not os.path.isfile(privatekeyfile):
 1919                 if os.path.isfile(publickeyfile):
 1920                     error('%s: Public key exists %s, but private key is missing. Skipping client.' % (cfg_id, publickeyfile))
 1921                     break
 1922                 if dump:
 1923                     print('%s: Key does not exist: %s.' % (cfg_id, privatekeyfile))
 1924                     break
 1925                 if status:
 1926                     print('%s: Key does not exist: %s. Will be generated.' % (cfg_id, privatekeyfile))
 1927                     break
 1928                 if deploy:
 1929                     info('%s: Key do not exist, generating it now: %s' % (cfg_id, privatekeyfile))
 1930                     if ssh_keygen_bits:
 1931                         keygen_bits = '-b %d' % ssh_keygen_bits
 1932                     else:
 1933                         keygen_bits = ''
 1934                     gencmd = 'ssh-keygen -q %s -t %s -N "" -C "SafeKeep auto generated key at %s@%s" -f %s' % (keygen_bits, ssh_keygen_type, backup_user, os.uname()[1], privatekeyfile)
 1935                     if backup_user != work_user:
 1936                         gencmd = 'su -s /bin/sh -c %s - %s' % (shlex.quote(gencmd), backup_user)
 1937                     debug(gencmd)
 1938                     if spawn(gencmd):
 1939                         error('%s: Failed to generate key %s. Skipping client.' % (cfg_id, privatekeyfile))
 1940                         break
 1941             if not os.path.isfile(publickeyfile):
 1942                 error('%s: Private key exists %s, but public key is missing. Skipping client.' % (cfg_id, privatekeyfile))
 1943                 break
 1944             fin = open(publickeyfile, 'r')
 1945             publickey = fin.read()
 1946             fin.close()
 1947             line = 'command="%s%s",no-port-forwarding,no-X11-forwarding,no-agent-forwarding,no-pty %s' % (nice_cmd, cmd, publickey.strip())
 1948             output.append(line)
 1949         else:
 1950             keys_ok = True
 1951 
 1952         if not keys_ok:
 1953             continue
 1954 
 1955         if dump:
 1956             print(output)
 1957 
 1958         basessh = ['ssh', '-oStrictHostKeyChecking=%s' % (ssh_StrictHostKeyChecking) ]
 1959         if cfg['port']: basessh.append('-p %s' % cfg['port'])
 1960         if identity: basessh.append('-i %s' % (shlex.quote(identity)))
 1961 
 1962         if status or deploy:
 1963             cmd = basessh + ['%s@%s' % (cfg['user'], cfg['host']), "if test -f .ssh/authorized_keys; then cat .ssh/authorized_keys; fi"]
 1964             authtext = call(cmd)
 1965             if authtext is None:
 1966                 error('%s: Failed to read the %s@%s:~/.ssh/authorized_keys file.' % (cfg_id, cfg['user'], cfg['host']))
 1967                 continue
 1968             auth_keys = parse_authorized_keys(authtext)
 1969             this_keys = parse_authorized_keys(output)
 1970             new_keys = []
 1971             for this_key in this_keys:
 1972                 for auth_key in auth_keys:
 1973                     if this_key[2] == auth_key[2]: break
 1974                 else:
 1975                     new_keys.append(this_key)
 1976             if not new_keys:
 1977                 if status:
 1978                     print('%s: Client is up to date.' % cfg_id)
 1979                 continue
 1980 
 1981             if status:
 1982                 print('%s: Keys will be deployed on the client.' % cfg_id)
 1983             if deploy:
 1984                 cmd = basessh + ['%s@%s' % (cfg['user'], cfg['host']), "umask 077; test -d .ssh || mkdir .ssh; cat >> .ssh/authorized_keys"]
 1985                 keys = '%s\n' % '\n'.join([key[4] for key in new_keys])
 1986                 out = call(cmd, stdin=keys)
 1987                 if out is None:
 1988                     error('Failed to deliver the keys to the client')
 1989 
 1990 # parses authozied_keys, see sshd(8) man page for details
 1991 def parse_authorized_keys(keystext):
 1992     keys = []
 1993     for line in keystext:
 1994         line = line.strip()
 1995         if not line or line[0] == '#': continue
 1996         if line[0] in '0123456789':
 1997             warn('SSH Protocol 1 keys are ignored: %s' % line)
 1998             continue
 1999         opts = ''
 2000         if line.split(None, 1)[0] not in SSH_KEY_TYPES:
 2001             in_str = False
 2002             in_esc = False
 2003             for i, c in enumerate(line):
 2004                 if in_str:
 2005                     if in_esc: in_esc = False
 2006                     elif c == '\'': in_esc = True
 2007                     elif c == '"': in_str = False
 2008                 else:
 2009                     if c == ' ':
 2010                         rest = line[i:].strip()
 2011                         break
 2012                     elif c == '"': in_str = True
 2013                 opts += c
 2014             else:
 2015                 info('Invalid key line, ignoring: %s' % line)
 2016                 continue
 2017         else:
 2018             rest = line
 2019 
 2020         if rest[0] in '0123456789':
 2021             warn('SSH Protocol 1 keys are ignored: %s' % line)
 2022             continue
 2023 
 2024         parts = rest.split(None, 2)
 2025         if len(parts) < 2:
 2026             error('Invalid key line, skipping: %s' % line)
 2027             continue
 2028 
 2029         ssh_type = parts[0]
 2030         if ssh_type not in SSH_KEY_TYPES:
 2031             error('Invalid key type "%s", skipping: %s' % (ssh_type, line))
 2032             continue
 2033 
 2034         base46enc = parts[1]
 2035 
 2036         if len(parts) == 2:
 2037             comment = None
 2038         else:
 2039             comment = parts[2]
 2040 
 2041         keys.append((opts, ssh_type, base46enc, comment, line))
 2042 
 2043     return keys
 2044 
 2045 ######################################################################
 2046 # Main routine
 2047 ######################################################################
 2048 
 2049 def usage(exitcode=None):
 2050     print('usage: %s --server [common options] [server options] <client-id>*' % (sys.argv[0]))
 2051     print('       %s --keys [common options] [keys options] <client-id>*' % (sys.argv[0]))
 2052     print('       %s --list [common options] [list options] <client-id>*' % (sys.argv[0]))
 2053     print()
 2054     print('mode selection (you must pick one):')
 2055     print('--server            launch in server mode')
 2056     print('--keys              launch in keys management mode')
 2057     print('--list              list previous backup status')
 2058     print()
 2059     print('common options:')
 2060     print('-c, --conf=FILE     use the FILE configuration file')
 2061     print('-h, --help          show this help message and exit')
 2062     print('-q, --quiet         decreases the verbosity level')
 2063     print('-v, --verbose       increases the verbosity level')
 2064     print('-V, --version       show the version number and exit')
 2065     print('--noemail           disables the sending of email')
 2066     print()
 2067     print('server options:')
 2068     print('--force             force backup destination overwriting, dangerous!')
 2069     print('--cleanup           perform cleanup actions after a failure')
 2070     print('-t, --tempdir=DIR   set tempdir to DIR for rdiff-backup')
 2071     print()
 2072     print('keys options:')
 2073     print('-i FILE             use FILE as identity for RSA/DSA authentication')
 2074     print('--status            display the key status for the clients (default)')
 2075     print('--print             display the authorization keys')
 2076     print('--deploy            deploy the authorization keys')
 2077     print()
 2078     print('list options:')
 2079     print('--increments        list number and dates of increments')
 2080     print('--parsable-output   tailor output for parsing by other programs')
 2081     print('--sizes             list sizes of all the increments')
 2082     print('--changed=time      list files that have changed since time')
 2083     print('--at-time=time      list files in the archive at given time')
 2084     if exitcode is not None: sys.exit(exitcode)
 2085 
 2086 def main():
 2087     try:
 2088         opts, args = getopt.getopt(sys.argv[1:], 'c:e:i:hs:t:qvV',
 2089                                    ['conf=', 'client', 'deploy',
 2090                                     'email=', 'force', 'help', 'keys',
 2091                                     'list', 'increments', 'sizes',
 2092                                     'parsable-output', 'changed=', 'at-time=',
 2093                                     'noemail', 'cleanup',
 2094                                     'print', 'quiet', 'server', 'smtp=',
 2095                                     'status', 'tempdir=', 'verbose', 'version'])
 2096     except getopt.GetoptError:
 2097         usage(2)
 2098 
 2099     global backup_user, backup_tempdir, client_user, home_dir, base_dir, config_file
 2100     global verbosity_level
 2101 
 2102     mode = None
 2103     email = {}
 2104     cfgfile = None
 2105     cfglocs = []
 2106     verbosity = 0
 2107     force = False
 2108     cleanup = False
 2109     noemail = False
 2110     list_type = None
 2111     list_parsable = False
 2112     list_date = None
 2113     identity = None
 2114     keys_status = False
 2115     keys_print = False
 2116     keys_deploy = False
 2117     nice_srv = None
 2118 
 2119     if os.getuid():
 2120         user_path = os.path.expanduser('~/.safekeep')
 2121         if os.path.exists(user_path) and os.path.isdir(user_path):
 2122             config_file = user_path + '/safekeep.conf'
 2123 
 2124     for o, a in opts:
 2125         if o in ('-c', '--conf'):
 2126             if os.path.isdir(a) or a.endswith(config_ext):
 2127                 warn('Adding client config files/dirs via this switch is deprecated')
 2128                 cfglocs.append(a)
 2129             elif cfgfile is None:
 2130                 cfgfile = a
 2131             else:
 2132                 error('A main configuration file can be specified only once!')
 2133                 sys.exit(2)
 2134         elif o in ('-e', '--email'):
 2135             warn('The -e/--email options are deprecated and will be removed in the future')
 2136             warn('Please use the %s instead' % (config_file))
 2137             if 'to' in email:
 2138                 email['to'].append(a)
 2139             else:
 2140                 email['to'] = a
 2141         elif o in ('-h', '--help'):
 2142             usage(0)
 2143         elif o in ('-s', '--smtp'):
 2144             warn('The -s/--smtp options are deprecated and will be removed in the future')
 2145             warn('Please use the %s instead' % (config_file))
 2146             email['smtp'] = a
 2147         elif o in ('--server', ):
 2148             if mode: usage(2)
 2149             mode = 'server'
 2150         elif o in ('--list', ):
 2151             if mode: usage(2)
 2152             mode = 'list'
 2153         elif o in ('--client', ):
 2154             if mode: usage(2)
 2155             mode = 'client'
 2156         elif o in ('--keys', ):
 2157             if mode: usage(2)
 2158             mode = 'keys'
 2159         elif o in ('--force', ):
 2160             force = True
 2161         elif o in ('--cleanup', ):
 2162             cleanup = True
 2163         elif o in ('--noemail', ):
 2164             noemail = True
 2165         elif o in ('-t', '--tempdir'):
 2166             backup_tempdir = a
 2167         elif o in ('--increments', ):
 2168             if list_type: usage(2)
 2169             list_type = 'increments'
 2170         elif o in ('--sizes', ):
 2171             if list_type: usage(2)
 2172             list_type = 'sizes'
 2173         elif o in ('--parsable-output', ):
 2174             list_parsable = True
 2175         elif o in ('--changed', ):
 2176             if list_type: usage(2)
 2177             list_type = 'changed'
 2178             list_date = a
 2179         elif o in ('--at-time', ):
 2180             if list_type: usage(2)
 2181             list_type = 'attime'
 2182             list_date = a
 2183         elif o in ('-i', ):
 2184             identity = a
 2185         elif o in ('--status', ):
 2186             keys_status = True
 2187         elif o in ('--print', ):
 2188             keys_print = True
 2189         elif o in ('--deploy', ):
 2190             keys_deploy = True
 2191         elif o in ('-q', '--quiet'):
 2192             verbosity -= 1
 2193         elif o in ('-v', '--verbose'):
 2194             verbosity += 1
 2195         elif o in ('-V', '--version'):
 2196             print('safekeep', VERSION)
 2197             return
 2198 
 2199     if mode is None:
 2200         usage(2)
 2201 
 2202     if mode != 'keys' and (identity or keys_status or keys_print or keys_deploy):
 2203         usage(2)
 2204 
 2205     if mode != 'list' and (list_type or list_date or list_parsable):
 2206         usage(2)
 2207 
 2208     if mode != 'server' and ('to' in email or 'smtp' in email):
 2209         usage(2)
 2210 
 2211     if not mode in ('server', 'client') and cleanup:
 2212         usage(2)
 2213 
 2214     if mode == 'client' and cfglocs:
 2215         usage(2)
 2216 
 2217     if mode != 'client':
 2218         if cfgfile is None and os.path.isfile(config_file):
 2219             cfgfile = config_file
 2220         if cfgfile and os.path.isfile(cfgfile):
 2221             props = parse_prop_file(cfgfile)
 2222         else:
 2223             if cfgfile:
 2224                 warn('Configuration file does not exist, skipping: %s' % cfgfile)
 2225             else:
 2226                 cfgfile = config_file
 2227             props = {}
 2228 
 2229         def get_int(p):
 2230             v = props.get(p)
 2231             if v is not None and v != '':
 2232                 return int(v)
 2233             return None
 2234 
 2235         if 'backup.user' in props:
 2236             backup_user = props['backup.user']
 2237         if 'backup.tempdir' in props and not backup_tempdir:
 2238             backup_tempdir = props['backup.tempdir']
 2239         if 'base.dir' in props:
 2240             base_dir = props['base.dir']
 2241         if 'client.user' in props:
 2242             client_user = props['client.user']
 2243         if not noemail:
 2244             for prop in [prop for prop in props if prop.startswith('email.')]:
 2245                 if prop == 'email.to':
 2246                     email['to'] = props[prop].split(',')
 2247                 elif prop == 'email.format':
 2248                     email['format'] = props[prop]
 2249                     if email['format'] not in ('text', 'html'):
 2250                         error('CONFIG ERROR: invalid email format type: %s' % email['format'])
 2251                         sys.exit(2)
 2252                 elif prop == 'email.summary':
 2253                     if props[prop].lower() in ('true', 'yes', '1'):
 2254                         email['summary'] = props[prop]
 2255                 else:
 2256                     proplist = prop.split('.')
 2257                     if len(proplist) > 2:
 2258                         if proplist[1] not in email: email[proplist[1]] = {}
 2259                         if prop == 'email.smtp.port':
 2260                             val = get_int(prop)
 2261                             if not val or val <= 0:
 2262                                 error('CONFIG ERROR: invalid email.smtp.port value: %s: must be a postive numeric value' % props[prop])
 2263                             email[proplist[1]][proplist[2]] = val
 2264                         else:
 2265                             email[proplist[1]][proplist[2]] = props[prop]
 2266                     else:
 2267                         email[proplist[1]] = props[prop]
 2268         else:
 2269             email = {}
 2270         nice_def = get_int('nice.adjustment')
 2271         if nice_def is None: nice_def = 10
 2272         nice_srv = get_int('nice.adjustment.server') or nice_def
 2273         nice_cln = get_int('nice.adjustment.client') or nice_def
 2274         ionice_def = props.get('ionice.adjustment')
 2275         if ionice_def is None: ionice_def = 'idle'
 2276         if ionice_def == '': ionice_def = 'none'
 2277 
 2278         global default_bandwidth
 2279         default_bandwidth['overall'] = get_int('bandwidth.limit') or 0
 2280         default_bandwidth['download'] = get_int('bandwidth.limit.download') or 0
 2281         default_bandwidth['upload'] = get_int('bandwidth.limit.upload') or 0
 2282 
 2283         global default_snapshot, client_defaults
 2284         default_snapshot = props.get('snapshot.size')
 2285         if default_snapshot:
 2286             if default_snapshot.endswith('%'):
 2287                 default_snapshot += 'FREE'
 2288             client_defaults.append('snapshot.size=%s' % default_snapshot)
 2289 
 2290         global ssh_keygen_type, ssh_keygen_bits, ssh_StrictHostKeyChecking
 2291         if 'ssh.keygen.type' in props:
 2292             ssh_keygen_type = props['ssh.keygen.type']
 2293             if ssh_keygen_type not in SSH_TYPES:
 2294                 error('CONFIG ERROR: invalid ssh.keygen.type: %s' % props['ssh.keygen.type'])
 2295                 sys.exit(2)
 2296         if 'ssh.keygen.bits' in props:
 2297             if props['ssh.keygen.bits']:
 2298                 try:
 2299                     ssh_keygen_bits = get_int('ssh.keygen.bits')
 2300                 except:
 2301                     ssh_keygen_bits = -1
 2302                 if ssh_keygen_bits <= 0:
 2303                     error('CONFIG ERROR: invalid ssh.keygen.bit value: %s' % props['ssh.keygen.bits'])
 2304                     sys.exit(2)
 2305             else:
 2306                 # For cases where no bit size is required
 2307                 ssh_keygen_bits = 0
 2308         if 'ssh.strict_hostkey_checking' in props:
 2309             ssh_StrictHostKeyChecking = props['ssh.strict_hostkey_checking']
 2310             if ssh_StrictHostKeyChecking not in SSH_STRICT_HOSTKEY_CHECK_OPTS:
 2311                 error('CONFIG ERROR: invalid ssh.strict_hostkey_checking value: %s' % props['ssh.strict_hostkey_checking'])
 2312                 sys.exit(2)
 2313 
 2314         if len(cfglocs) == 0:
 2315             locs = os.path.join(os.path.dirname(cfgfile), 'backup.d')
 2316             if os.path.isdir(locs): cfglocs.append(locs)
 2317 
 2318     if backup_user and backup_user != work_user:
 2319         (user, pswd, uid, gid, gecos, home_dir, shell) = pwd.getpwnam(backup_user)
 2320         if mode != 'keys':
 2321             try:
 2322                 os.setregid(gid, gid)
 2323                 os.setreuid(uid, uid)
 2324             except OSError as ex:
 2325                 warn("Cannot setreuid(): " + str(ex))
 2326             os.environ['HOME'] = home_dir
 2327     else:
 2328         backup_user = work_user
 2329         home_dir = os.getenv('HOME', '/')
 2330 
 2331     if not base_dir:
 2332         base_dir = home_dir
 2333 
 2334     if len(cfglocs) > 0:
 2335         try:
 2336             verbosity_level = 1 + verbosity
 2337             cfgs = parse_locs(cfglocs)
 2338         except Exception as ex:
 2339             if isinstance(ex, ConfigException):
 2340                 error('CONFIG ERROR: %s' % (ex or ''), ex)
 2341             else:
 2342                 error('ERROR: %s' % (ex or ''), ex)
 2343             send_notification(email, mode)
 2344             sys.exit(2)
 2345     else:
 2346         cfgs = {}
 2347 
 2348     if mode == 'client':
 2349         if len(args) > 0: usage(2)
 2350     else:
 2351         ok = True
 2352         for arg in args:
 2353             if arg in cfgs: continue
 2354             error('Unknown client ID: %s' % arg)
 2355             if os.path.isfile(arg):
 2356                 error('It appears to be a file, configuration files are passed via the -c/--conf switch.')
 2357             ok = False
 2358         if not ok:
 2359             send_notification(email, mode)
 2360             sys.exit(2)
 2361 
 2362     try:
 2363         global is_client, verbosity_ssh, verbosity_trickle
 2364 
 2365         if verbosity > 2:
 2366             verbosity_trickle = verbosity_ssh = '-' + (verbosity-2) * 'v'
 2367         if mode == 'server':
 2368 
 2369             if backup_tempdir:
 2370                 if not os.path.isabs(backup_tempdir):
 2371                     info('backup.tempdir %s is relative to base.dir %s' % (backup_tempdir, base_dir))
 2372                     backup_tempdir = os.path.join(base_dir, backup_tempdir)
 2373 
 2374                 if not os.path.isdir(backup_tempdir):
 2375                     error('CONFIG ERROR: backup.tempdir %s is not a valid directory' % backup_tempdir)
 2376                     sys.exit(2)
 2377                 elif not os.access(backup_tempdir, os.W_OK):
 2378                     error('CONFIG ERROR: backup.tempdir %s is not writeable' % backup_tempdir)
 2379                     sys.exit(2)
 2380 
 2381             is_client = False
 2382             verbosity_level = 1 + verbosity
 2383             do_server(cfgs, args, nice_srv, ionice_def, force, cleanup)
 2384         elif mode == 'list':
 2385             if list_type is None:
 2386                 list_type = 'increments'
 2387             is_client = False
 2388             verbosity_level = 2 + verbosity
 2389             do_list(cfgs, args, list_type, list_date, list_parsable)
 2390         elif mode == 'client':
 2391             if cleanup:
 2392                 is_client = False
 2393                 verbosity_level = 1 + verbosity
 2394                 do_client_scrub()
 2395             else:
 2396                 is_client = True
 2397                 verbosity_level = 3 + verbosity
 2398                 do_client()
 2399         elif mode == 'keys':
 2400             is_client = False
 2401             verbosity_level = 1 + verbosity
 2402             if not keys_status and not keys_print and not keys_deploy:
 2403                 keys_status = True
 2404             do_keys(cfgs, args, nice_cln, identity, keys_status, keys_print, keys_deploy)
 2405         else:
 2406             assert False, 'Unknown mode: %s' % (mode)
 2407     except Exception as ex:
 2408         error('ERROR: %s' % (ex or ''), ex)
 2409 
 2410     send_notification(email, mode)
 2411 
 2412 if __name__ == '__main__':
 2413     main()
 2414 
 2415 # vim: et ts=8 sw=4 sts=4