"Fossies" - the Fresh Open Source Software Archive

Member "Tardis-1.2.1/src/Tardis/Sonic.py" (9 Jun 2021, 34137 Bytes) of package /linux/privat/Tardis-1.2.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. For more information about "Sonic.py" see the Fossies "Dox" file reference documentation and the latest Fossies "Diffs" side-by-side code changes report: 1.1.5_vs_1.2.1.

    1 # vim: set et sw=4 sts=4 fileencoding=utf-8:
    2 #
    3 # Tardis: A Backup System
    4 # Copyright 2013-2020, Eric Koldinger, All Rights Reserved.
    5 # kolding@washington.edu
    6 #
    7 # Redistribution and use in source and binary forms, with or without
    8 # modification, are permitted provided that the following conditions are met:
    9 #
   10 #     * Redistributions of source code must retain the above copyright
   11 #       notice, this list of conditions and the following disclaimer.
   12 #     * Redistributions in binary form must reproduce the above copyright
   13 #       notice, this list of conditions and the following disclaimer in the
   14 #       documentation and/or other materials provided with the distribution.
   15 #     * Neither the name of the copyright holder nor the
   16 #       names of its contributors may be used to endorse or promote products
   17 #       derived from this software without specific prior written permission.
   18 #
   19 # THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
   20 # AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
   21 # IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE
   22 # ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE
   23 # LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR
   24 # CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF
   25 # SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS
   26 # INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN
   27 # CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE)
   28 # ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE
   29 # POSSIBILITY OF SUCH DAMAGE.
   30 
   31 import logging
   32 import argparse
   33 import os
   34 import os.path
   35 import sys
   36 import time
   37 import datetime
   38 import pprint
   39 import urllib.parse
   40 import srp
   41 
   42 import parsedatetime
   43 import passwordmeter
   44 
   45 import Tardis
   46 import Tardis.Util as Util
   47 import Tardis.Defaults as Defaults
   48 import Tardis.TardisDB as TardisDB
   49 import Tardis.TardisCrypto as TardisCrypto
   50 import Tardis.CacheDir as CacheDir
   51 import Tardis.RemoteDB as RemoteDB
   52 import Tardis.Regenerator as Regenerator
   53 import Tardis.Config as Config
   54 
   55 current      = Defaults.getDefault('TARDIS_RECENT_SET')
   56 
   57 # Config keys which can be gotten or set.
   58 configKeys = ['Formats', 'Priorities', 'KeepDays', 'ForceFull', 'SaveFull', 'MaxDeltaChain', 'MaxChangePercent', 'VacuumInterval', 'AutoPurge', 'Disabled', 'SaveConfig']
   59 # Extra keys that we print when everything is requested
   60 sysKeys    = ['ClientID', 'SchemaVersion', 'FilenameKey', 'ContentKey', 'CryptoScheme']
   61 
   62 logger = None
   63 args = None
   64 
   65 def getDB(password, new=False, allowRemote=True, allowUpgrade=False):
   66     loc = urllib.parse.urlparse(args.database)
   67     # This is basically the same code as in Util.setupDataConnection().  Should consider moving to it.
   68     if (loc.scheme == 'http') or (loc.scheme == 'https'):
   69         if not allowRemote:
   70             raise Exception("This command cannot be executed remotely.  You must execute it on the server directly.")
   71         # If no port specified, insert the port
   72         if loc.port is None:
   73             netloc = loc.netloc + ":" + Defaults.getDefault('TARDIS_REMOTE_PORT')
   74             dbLoc = urllib.parse.urlunparse((loc.scheme, netloc, loc.path, loc.params, loc.query, loc.fragment))
   75         else:
   76             dbLoc = args.database
   77         tardisdb = RemoteDB.RemoteDB(dbLoc, args.client)
   78         cache = tardisdb
   79     else:
   80         basedir = os.path.join(args.database, args.client)
   81         if not args.dbdir:
   82             dbdir = os.path.join(args.database, args.client)
   83         else:
   84             dbdir = os.path.join(args.dbdir, args.client)
   85         dbfile = os.path.join(dbdir, args.dbname)
   86         if new and os.path.exists(dbfile):
   87             raise Exception("Database for client %s already exists." % (args.client))
   88 
   89         cache = CacheDir.CacheDir(basedir, 2, 2, create=new)
   90         schema = args.schema if new else None
   91         tardisdb = TardisDB.TardisDB(dbfile, backup=False, initialize=schema, allow_upgrade=allowUpgrade)
   92 
   93     if tardisdb.needsAuthentication():
   94         if password is None:
   95             password = Util.getPassword(args.password, args.passwordfile, args.passwordprog, prompt="Password for %s: " % (args.client), allowNone=False, confirm=False)
   96         Util.authenticate(tardisdb, args.client, password)
   97 
   98         scheme = tardisdb.getCryptoScheme()
   99         crypt = TardisCrypto.getCrypto(scheme, password, args.client)
  100         logger.info("Using crypto scheme %s", TardisCrypto.getCryptoNames(int(scheme)))
  101     else:
  102         crypt = TardisCrypto.getCrypto(0, None, None)
  103 
  104     return (tardisdb, cache, crypt)
  105 
  106 def createClient(password):
  107     try:
  108         (db, _, crypt) = getDB(None, True, allowRemote=False)
  109         if password:
  110             setPassword(password)
  111         return 0
  112     except TardisDB.AuthenticationException as e:
  113         logger.error("Authentication failed.  Bad password")
  114         if args.exceptions:
  115             logger.exception(e)
  116         return 1
  117     except Exception as e:
  118         logger.error(str(e))
  119         if args.exceptions:
  120             logger.exception(e)
  121         return 1
  122 
  123 def setPassword(password):
  124     try:
  125         (db, _, _) = getDB(None)
  126         crypt = TardisCrypto.getCrypto(TardisCrypto.defaultCryptoScheme, password)
  127         crypt.genKeys()
  128         (f, c) = crypt.getKeys()
  129         (salt, vkey) = srp.create_salted_verification_key(args.client, password)
  130         if args.keys:
  131             db.beginTransaction()
  132             db.setSrpValues(salt, vkey)
  133             db.setConfigValue('CryptoScheme', crypt.getCryptoScheme())
  134             Util.saveKeys(args.keys, db.getConfigValue('ClientID'), f, c)
  135         else:
  136             db.setKeys(salt, vkey, f, c)
  137             db.setConfigValue('CryptoScheme', crypt.getCryptoScheme())
  138         return 0
  139     except TardisDB.NotAuthenticated:
  140         logger.error('Client %s already has a password', args.client)
  141         if args.exceptions:
  142             logger.exception(e)
  143         return 1
  144     except TardisDB.AuthenticationFailed as e:
  145         logger.error("Authentication failed.  Bad password")
  146         if args.exceptions:
  147             logger.exception(e)
  148         return 1
  149     except Exception as e:
  150         logger.error(str(e))
  151         if args.exceptions:
  152             logger.exception(e)
  153         return 1
  154 
  155 def changePassword(crypt, oldpw) :
  156     try:
  157         (db, _, crypt) = getDB(oldpw)
  158 
  159         # Get the new password
  160         try:
  161             newpw = Util.getPassword(args.newpw, args.newpwf, args.newpwp, prompt="New Password for %s: " % (args.client),
  162                                      allowNone=False, confirm=True, strength=True)
  163         except Exception as e:
  164             logger.critical(str(e))
  165             if args.exceptions:
  166                 logger.exception(e)
  167             return -1
  168 
  169         scheme = db.getConfigValue('CryptoScheme', 1)
  170         crypt2 = TardisCrypto.getCrypto(scheme, newpw, args.client)
  171 
  172         # Load the keys, and insert them into the crypt object, to decyrpt them
  173         if args.keys:
  174             (f, c) = Util.loadKeys(args.keys, db.getConfigValue('ClientID'))
  175             # No need to check here, loadKeys() throws exception if nothing set.
  176         else:
  177             (f, c) = db.getKeys()
  178             if f is None or c is None:
  179                 logger.critical("No keys loaded from database.  Please specify --keys as appropriate")
  180                 raise Exception("No keys loaded")
  181         crypt.setKeys(f, c)
  182 
  183         # Grab the keys from one crypt object.
  184         # Need to do this because getKeys/setKeys assumes they're encrypted, and we need the raw
  185         # versions
  186         crypt2._filenameKey = crypt._filenameKey
  187         crypt2._contentKey  = crypt._contentKey
  188         # Now get the encrypted versions
  189         (f, c) = crypt2.getKeys()
  190 
  191         (salt, vkey) = srp.create_salted_verification_key(args.client, newpw)
  192 
  193         if args.keys:
  194             db.beginTransaction()
  195             db.setSrpValues(salt, vkey)
  196             Util.saveKeys(args.keys, db.getConfigValue('ClientID'), f, c)
  197             db.commit()
  198         else:
  199             db.setKeys(salt, vkey, f, c)
  200         return 0
  201     except Exception as e:
  202         logger.error(str(e))
  203         if args.exceptions:
  204             logger.exception(e)
  205         return 1
  206 
  207 def moveKeys(db, crypt):
  208     try:
  209         if args.keys is None:
  210             logger.error("Must specify key file for key manipulation")
  211             return 1
  212         clientId = db.getConfigValue('ClientID')
  213         salt, vkey = db.getSrpValues()
  214         #(db, _) = getDB(crypt)
  215         if args.extract:
  216             (f, c) = db.getKeys()
  217             if not (f and c):
  218                 raise Exception("Unable to retrieve keys from server.  Aborting.")
  219             Util.saveKeys(args.keys, clientId, f, c)
  220             if args.deleteKeys:
  221                 db.setKeys(salt, vkey, None, None)
  222         elif args.insert:
  223             (f, c) = Util.loadKeys(args.keys, clientId)
  224             logger.info("Keys: F: %s C: %s", f, c)
  225             if not (f and c):
  226                 raise Exception("Unable to retrieve keys from key database.  Aborting.")
  227             db.setKeys(salt, vkey, f, c)
  228             if args.deleteKeys:
  229                 Util.saveKeys(args.keys, clientId, None, None)
  230         return 0
  231     except TardisDB.AuthenticationException as e:
  232         logger.error("Authentication failed.  Bad password")
  233         return 1
  234     except Exception as e:
  235         logger.error(e)
  236         if args.exceptions:
  237             logger.exception(e)
  238         return 1
  239 
  240 _cmdLineHash = {}
  241 _regenerator = None
  242 def getCommandLine(db, commandLineCksum):
  243     global _cmdLineHash, _regenerator
  244     if commandLineCksum is None:
  245         return None
  246     if commandLineCksum in _cmdLineHash:
  247         return _cmdLineHash[commandLineCksum]
  248     if commandLineCksum:
  249         data = _regenerator.recoverChecksum(commandLineCksum).read().strip()
  250         _cmdLineHash[commandLineCksum] = data
  251         return data
  252     else:
  253         return None
  254 
  255 
  256 def listBSets(db, crypt, cache):
  257     global _regenerator
  258     try:
  259         if args.longinfo:
  260             _regenerator = Regenerator.Regenerator(cache, db, crypt)
  261 
  262         last = db.lastBackupSet()
  263         print("%-30s %-4s %-6s %3s  %-5s  %-24s  %-7s %6s %5s %8s  %s" % ("Name", "Id", "Comp", "Pri", "Full", "Start", "Runtime", "Files", "Delta", "Size", ""))
  264         sets = list(db.listBackupSets())
  265 
  266         if args.minpriority:
  267             sets = list(filter(lambda x: x['priority'] >= args.minpriority, sets))
  268         sets = sets[-(args.number):]
  269 
  270         for bset in sets:
  271             t = time.strftime("%d %b, %Y %I:%M:%S %p", time.localtime(float(bset['starttime'])))
  272             if bset['endtime'] is not None:
  273                 duration = str(datetime.timedelta(seconds = (int(float(bset['endtime']) - float(bset['starttime'])))))
  274             else:
  275                 duration = ''
  276             completed = 'Comp' if bset['completed'] else 'Incomp'
  277             full      = 'Full' if bset['full'] else 'Delta'
  278             if bset['backupset'] == last['backupset']:
  279                 status = current
  280             elif bset['errormsg']:
  281                 status = bset['errormsg']
  282             else:
  283                 status = ''
  284             #isCurrent = current if bset['backupset'] == last['backupset'] else ''
  285             size = Util.fmtSize(bset['bytesreceived'], formats=['', 'KB', 'MB', 'GB', 'TB'])
  286 
  287             print("%-30s %-4d %-6s %3d  %-5s  %-24s  %-7s %6s %5s %8s  %s" % (bset['name'], bset['backupset'], completed, bset['priority'], full, t, duration, bset['filesfull'], bset['filesdelta'], size, status))
  288             if args.longinfo:
  289                 commandLine = getCommandLine(db, bset['commandline'])
  290                 if commandLine:
  291                     print("    Command Line: %s" % (commandLine.decode('utf-8')))
  292                     print()
  293     except TardisDB.AuthenticationException as e:
  294         logger.error("Authentication failed.  Bad password")
  295         return 1
  296     except Exception as e:
  297         logger.error(e)
  298         if args.exceptions:
  299             logger.exception(e)
  300         return 1
  301 
  302 # cache of paths we've already calculated.
  303 # the root (0, 0,) is always prepopulated
  304 _paths = {(0, 0): '/'}
  305 
  306 def _decryptFilename(name, crypt):
  307     return crypt.decryptFilename(name) if crypt else name
  308 
  309 def _path(db, crypt, bset, inode):
  310     global _paths
  311     if inode in _paths:
  312         return _paths[inode]
  313     else:
  314         fInfo = db.getFileInfoByInode(inode, bset)
  315         if fInfo:
  316             parent = (fInfo['parent'], fInfo['parentdev'])
  317             prefix = _path(db, crypt, bset, parent)
  318 
  319             name = _decryptFilename(fInfo['name'], crypt)
  320             path = os.path.join(prefix, name)
  321             _paths[inode] = path
  322             return path
  323         else:
  324             return ''
  325 
  326 def listFiles(db, crypt):
  327     #print args
  328     info = getBackupSet(db, args.backup, args.date, defaultCurrent=True)
  329     #print info, info['backupset']
  330     lastDir = '/'
  331     lastDirInode = (-1, -1)
  332     bset = info['backupset']
  333     files = db.getNewFiles(info['backupset'], args.previous)
  334     for fInfo in files:
  335         name = _decryptFilename(fInfo['name'], crypt)
  336         
  337         if not args.dirs and fInfo['dir']:
  338             continue
  339         dirInode = (fInfo['parent'], fInfo['parentdev'])
  340         if dirInode == lastDirInode:
  341             path = lastDir
  342         else:
  343             path = _path(db, crypt, bset, dirInode)
  344             lastDirInode = dirInode
  345             lastDir = path
  346             if not args.fullname:
  347                 print("%s:" % (path))
  348         if args.status:
  349             status = '[New]   ' if fInfo['chainlength'] == 0 else '[Delta] '
  350         else:
  351             status = ''
  352         if args.fullname:
  353             name = os.path.join(path, name)
  354 
  355         if args.long:
  356             mode  = Util.filemode(fInfo['mode'])
  357             group = Util.getGroupName(fInfo['gid'])
  358             owner = Util.getUserId(fInfo['uid'])
  359             mtime = Util.formatTime(fInfo['mtime'])
  360             if fInfo['size'] is not None:
  361                 if args.human:
  362                     size = "%8s" % Util.fmtSize(fInfo['size'], formats=['','KB','MB','GB', 'TB', 'PB'])
  363                 else:
  364                     size = "%8d" % int(fInfo['size'])
  365             else:
  366                 size = ''           
  367             print('  %s%9s %-8s %-8s %8s %12s' % (status, mode, owner, group, size, mtime), end=' ')
  368             if args.cksums:
  369                 print(' %32s ' % (fInfo['checksum'] or ''), end=' ')
  370             if args.chnlen:
  371                 print(' %4s ' % (fInfo['chainlength']), end=' ')
  372             if args.inode:
  373                 print(' %-16s ' % ("(%s, %s)" % (fInfo['device'], fInfo['inode'])), end=' ')
  374 
  375             print(name)
  376         else:
  377             print("    %s" % status, end=' ')
  378             if args.cksums:
  379                 print(' %32s ' % (fInfo['checksum'] or ''), end=' ')
  380             if args.chnlen:
  381                 print(' %4s ' % (fInfo['chainlength']), end=' ')
  382             if args.inode:
  383                 print(' %-16s ' % ("(%s, %s)" % (fInfo['device'], fInfo['inode'])), end=' ')
  384             print(name)
  385 
  386 
  387 def _bsetInfo(db, info):
  388     print("Backupset       : %s (%d)" % ((info['name']), info['backupset']))
  389     print("Completed       : %s" % ('True' if info['completed'] else 'False'))
  390     t = time.strftime("%d %b, %Y %I:%M:%S %p", time.localtime(float(info['starttime'])))
  391     print("StartTime       : %s" % (t))
  392     if info['endtime'] is not None:
  393         t = time.strftime("%d %b, %Y %I:%M:%S %p", time.localtime(float(info['endtime'])))
  394         duration = str(datetime.timedelta(seconds = (int(float(info['endtime']) - float(info['starttime'])))))
  395         print("EndTime         : %s" % (t))
  396         print("Duration        : %s" % (duration))
  397     print("SW Versions     : C:%s S:%s" % (info['clientversion'], info['serverversion']))
  398     print("Client IP       : %s" % (info['clientip']))
  399     details = db.getBackupSetDetails(info['backupset'])
  400     (files, dirs, size, newInfo, endInfo) = details
  401     print("Files           : %d" % (files))
  402     print("Directories     : %d" % (dirs))
  403     print("Total Size      : %s" % (Util.fmtSize(size)))
  404 
  405     print("New Files       : %d" % (newInfo[0]))
  406     print("New File Size   : %s" % (Util.fmtSize(newInfo[1])))
  407     print("New File Space  : %s" % (Util.fmtSize(newInfo[2])))
  408 
  409     print("Purgeable Files : %d" % (endInfo[0]))
  410     print("Purgeable Size  : %s" % (Util.fmtSize(endInfo[1])))
  411     print("Purgeable Space : %s" % (Util.fmtSize(endInfo[2])))
  412 
  413 def bsetInfo(db):
  414     printed = False
  415     if args.backup or args.date:
  416         info = getBackupSet(db, args.backup, args.date)
  417         if info:
  418             _bsetInfo(db, info)
  419             printed = True
  420     else:
  421         first = True
  422         for info in db.listBackupSets():
  423             if not first:
  424                 print("------------------------------------------------")
  425             _bsetInfo(db, info)
  426             first = False
  427             printed = True
  428     if printed:
  429         print("\n * Purgeable numbers are estimates only")
  430 
  431 def confirm():
  432     if not args.confirm:
  433         return True
  434     else:
  435         print("Proceed (y/n): ", end='', flush=True)
  436         yesno = sys.stdin.readline().strip().upper()
  437         return yesno == 'YES' or yesno == 'Y'
  438 
  439 def purge(db, cache):
  440     bset = getBackupSet(db, args.backup, args.date, True)
  441     if bset is None:
  442         logger.error("No backup set found")
  443         sys.exit(1)
  444     # List the sets we're going to delete
  445     if args.incomplete:
  446         pSets = db.listPurgeIncomplete(args.priority, bset['endtime'], bset['backupset'])
  447     else:
  448         pSets = db.listPurgeSets(args.priority, bset['endtime'], bset['backupset'])
  449 
  450     names = [str(x['name']) for x in pSets]
  451     logger.debug("Names: %s", names)
  452     if len(names) == 0:
  453         print("No matching sets")
  454         return
  455 
  456     print("Sets to be deleted:")
  457     pprint.pprint(names)
  458 
  459     if confirm():
  460         if args.incomplete:
  461             (filesDeleted, setsDeleted) = db.purgeIncomplete(args.priority, bset['endtime'], bset['backupset'])
  462         else:
  463             (filesDeleted, setsDeleted) = db.purgeSets(args.priority, bset['endtime'], bset['backupset'])
  464         print("Purged %d sets, containing %d files" % (setsDeleted, filesDeleted))
  465         removeOrphans(db, cache)
  466 
  467 def deleteBsets(db, cache):
  468     if not args.backups:
  469         logger.error("No backup sets specified")
  470         sys.exit(0)
  471     bsets = []
  472     for i in args.backups:
  473         bset = getBackupSet(db, i, None)
  474         if bset is None:
  475             logger.error("No backup set found for %s", i)
  476             sys.exit(1)
  477         bsets.append(bset)
  478 
  479     names = [b['name'] for b in bsets]
  480     print("Sets to be deleted: %s" % (names))
  481     if confirm():
  482         filesDeleted = 0
  483         for bset in bsets:
  484             filesDeleted = filesDeleted + db.deleteBackupSet(bset['backupset'])
  485         print("Deleted %d files" % (filesDeleted))
  486         if args.purge:
  487             removeOrphans(db, cache)
  488 
  489 def removeOrphans(db, cache):
  490     if hasattr(cache, 'removeOrphans'):
  491         r = cache.removeOrphans()
  492         logger.debug("Remove Orphans: %s %s", type(r), r)
  493         count = r['count']
  494         size = r['size']
  495         rounds = r['rounds']
  496     else:
  497         count, size, rounds = Util.removeOrphans(db, cache)
  498     print("Removed %d orphans, for %s, in %d rounds" % (count, Util.fmtSize(size), rounds))
  499 
  500 def _printConfigKey(db, key):
  501     value = db.getConfigValue(key)
  502     print("%-18s: %s" % (key, value))
  503 
  504 
  505 def getConfig(db):
  506     keys = args.configKeys
  507     if keys is None:
  508         keys = configKeys
  509         if args.sysKeys:
  510             keys = sysKeys + keys
  511 
  512     for i in keys:
  513         _printConfigKey(db, i)
  514 
  515 def setConfig(db):
  516     print("Old Value: ", end=' ')
  517     _printConfigKey(db, args.key)
  518     db.setConfigValue(args.key, args.value)
  519 
  520 def setPriority(db):
  521     info = getBackupSet(db, args.backup, args.date, defaultCurrent=True)
  522     db.setPriority(info['backupset'], args.priority)
  523 
  524 def renameSet(db):
  525     info = getBackupSet(db, args.backup, args.date, defaultCurrent=True)
  526     result = db.setBackupSetName(args.newname, info['priority'], info['backupset'])
  527     if not result:
  528         logger.error("Unable to rename %s to %s", info['name'], args.newname)
  529     return result
  530 
  531 def parseArgs():
  532     global args, minPwStrength
  533 
  534     parser = argparse.ArgumentParser(description='Tardis Sonic Screwdriver Utility Program', fromfile_prefix_chars='@', formatter_class=Util.HelpFormatter, add_help=False)
  535 
  536     (args, remaining) = Config.parseConfigOptions(parser)
  537     c = Config.config
  538     t = args.job
  539 
  540     # Shared parser
  541     bsetParser = argparse.ArgumentParser(add_help=False)
  542     bsetgroup = bsetParser.add_mutually_exclusive_group()
  543     bsetgroup.add_argument("--backup", "-b", help="Backup set to use", dest='backup', default=None)
  544     bsetgroup.add_argument("--date", "-d",   help="Use last backupset before date", dest='date', default=None)
  545 
  546     purgeParser = argparse.ArgumentParser(add_help=False)
  547     purgeParser.add_argument('--priority',       dest='priority',   default=0, type=int,                   help='Maximum priority backupset to purge')
  548     purgeParser.add_argument('--incomplete',     dest='incomplete', default=False, action='store_true',    help='Purge only incomplete backup sets')
  549     bsetgroup = purgeParser.add_mutually_exclusive_group()
  550     bsetgroup.add_argument("--date", "-d",     dest='date',       default=None,                            help="Purge sets before this date")
  551     bsetgroup.add_argument("--backup", "-b",   dest='backup',     default=None,                            help="Purge sets before this set")
  552 
  553     deleteParser = argparse.ArgumentParser(add_help=False)
  554     #deleteParser.add_argument("--backup", "-b",  dest='backup',     default=None,                          help="Purge sets before this set")
  555     deleteParser.add_argument("--purge", "-p", dest='purge', default=True, action=Util.StoreBoolean,        help="Delete files in the backupset")
  556     deleteParser.add_argument("backups", nargs="*", default=None, help="Backup sets to delete")
  557 
  558     cnfParser = argparse.ArgumentParser(add_help=False)
  559     cnfParser.add_argument('--confirm',          dest='confirm', action=Util.StoreBoolean, default=True,   help='Confirm deletes and purges')
  560 
  561     keyParser = argparse.ArgumentParser(add_help=False)
  562     keyGroup = keyParser.add_mutually_exclusive_group(required=True)
  563     keyGroup.add_argument('--extract',          dest='extract', default=False, action='store_true',         help='Extract keys from database')
  564     keyGroup.add_argument('--insert',           dest='insert', default=False, action='store_true',          help='Insert keys from database')
  565     keyParser.add_argument('--delete',          dest='deleteKeys', default=False, action=Util.StoreBoolean, help='Delete keys from server or database')
  566 
  567     filesParser = argparse.ArgumentParser(add_help=False)
  568     filesParser.add_argument('--long', '-l',    dest='long', default=False, action=Util.StoreBoolean,           help='Long format')
  569     filesParser.add_argument('--fullpath', '-f',    dest='fullname', default=False, action=Util.StoreBoolean,   help='Print full path name in names')
  570     filesParser.add_argument('--previous',      dest='previous', default=False, action=Util.StoreBoolean,       help="Include files that first appear in the set, but weren't added here")
  571     filesParser.add_argument('--dirs',          dest='dirs', default=False, action=Util.StoreBoolean,           help='Include directories in list')
  572     filesParser.add_argument('--status',        dest='status', default=False, action=Util.StoreBoolean,         help='Include status (new/delta) in list')
  573     filesParser.add_argument('--human', '-H',   dest='human', default=False, action=Util.StoreBoolean,          help='Print sizes in human readable form')
  574     filesParser.add_argument('--checksums', '-c', dest='cksums', default=False, action=Util.StoreBoolean,       help='Print checksums')
  575     filesParser.add_argument('--chainlen', '-L', dest='chnlen', default=False, action=Util.StoreBoolean,        help='Print chainlengths')
  576     filesParser.add_argument('--inode', '-i',   dest='inode', default=False, action=Util.StoreBoolean,          help='Print inodes')
  577 
  578     common = argparse.ArgumentParser(add_help=False)
  579     Config.addPasswordOptions(common, addscheme=True)
  580     Config.addCommonOptions(common)
  581 
  582     create = argparse.ArgumentParser(add_help=False)
  583     create.add_argument('--schema',                 dest='schema',          default=c.get(t, 'Schema'), help='Path to the schema to use (Default: %(default)s)')
  584 
  585     newPassParser = argparse.ArgumentParser(add_help=False)
  586     newpassgrp = newPassParser.add_argument_group("New Password specification options")
  587     npwgroup = newpassgrp.add_mutually_exclusive_group()
  588     npwgroup.add_argument('--newpassword',      dest='newpw', default=None, nargs='?', const=True,  help='Change to this password')
  589     npwgroup.add_argument('--newpassword-file', dest='newpwf', default=None,                        help='Read new password from file')
  590     npwgroup.add_argument('--newpassword-prog', dest='newpwp', default=None,                        help='Use the specified command to generate the new password on stdout')
  591 
  592     configKeyParser = argparse.ArgumentParser(add_help=False)
  593     configKeyParser.add_argument('--key',       dest='configKeys', choices=configKeys, action='append',    help='Configuration key to retrieve.  None for all keys')
  594     configKeyParser.add_argument('--sys',       dest='sysKeys', default=False, action=Util.StoreBoolean,   help='List System Keys as well as configurable ones')
  595 
  596     configValueParser = argparse.ArgumentParser(add_help=False)
  597     configValueParser.add_argument('--key',     dest='key', choices=configKeys, required=True,      help='Configuration key to set')
  598     configValueParser.add_argument('--value',   dest='value', required=True,                        help='Configuration value to access')
  599 
  600     priorityParser = argparse.ArgumentParser(add_help=False)
  601     priorityParser.add_argument('--priority',   dest='priority', type=int, required=True,           help='New priority backup set')
  602 
  603     renameParser = argparse.ArgumentParser(add_help=False)
  604     renameParser.add_argument('--name',         dest='newname', required=True,                      help='New name')
  605 
  606     listParser = argparse.ArgumentParser(add_help=False)
  607     listParser.add_argument('--long', '-l',     dest='longinfo', default=False, action=Util.StoreBoolean,   help='Print long info')
  608     listParser.add_argument('--minpriority',    dest='minpriority', default=0, type=int,            help='Minimum priority to list')
  609     listParser.add_argument('--number', '-n',   dest='number', default=sys.maxsize, type=int,       help='Maximum number to show')
  610 
  611     subs = parser.add_subparsers(help="Commands", dest='command')
  612     subs.add_parser('create',       parents=[common, create],                               help='Create a client database')
  613     subs.add_parser('setpass',      parents=[common],                                       help='Set a password')
  614     subs.add_parser('chpass',       parents=[common, newPassParser],                        help='Change a password')
  615     subs.add_parser('keys',         parents=[common, keyParser],                            help='Move keys to/from server and key file')
  616     subs.add_parser('list',         parents=[common, listParser],                           help='List backup sets')
  617     subs.add_parser('files',        parents=[common, filesParser, bsetParser],              help='List new files in a backup set')
  618     subs.add_parser('info',         parents=[common, bsetParser],                           help='Print info on backup sets')
  619     subs.add_parser('purge',        parents=[common, purgeParser, cnfParser],               help='Purge old backup sets')
  620     subs.add_parser('delete',       parents=[common, deleteParser, cnfParser],              help='Delete a backup set')
  621     subs.add_parser('orphans',      parents=[common],                                       help='Delete orphan files')
  622     subs.add_parser('getconfig',    parents=[common, configKeyParser],                      help='Get Config Value')
  623     subs.add_parser('setconfig',    parents=[common, configValueParser],                    help='Set Config Value')
  624     subs.add_parser('priority',     parents=[common, priorityParser, bsetParser],           help='Set backupset priority')
  625     subs.add_parser('rename',       parents=[common, renameParser, bsetParser],             help='Rename a backup set')
  626     subs.add_parser('upgrade',      parents=[common],                                       help='Update the database schema')
  627 
  628     parser.add_argument('--exceptions',         dest='exceptions', default=False, action=Util.StoreBoolean,   help='Log exception messages')
  629     parser.add_argument('--verbose', '-v',      dest='verbose', default=0, action='count', help='Be verbose.  Add before usb command')
  630     parser.add_argument('--version',            action='version', version='%(prog)s ' + Tardis.__versionstring__,    help='Show the version')
  631     parser.add_argument('--help', '-h',         action='help')
  632 
  633     Util.addGenCompletions(parser)
  634 
  635     args = parser.parse_args(remaining)
  636     if args.command == None:
  637         parser.print_help()
  638         sys.exit(0)
  639 
  640     # And load the required strength for new passwords.  NOT specifiable on the command line.
  641     #minPwStrength = c.getfloat(t, 'PwStrMin')
  642     return args
  643 
  644 def getBackupSet(db, backup, date, defaultCurrent=False):
  645     bInfo = None
  646     if date:
  647         cal = parsedatetime.Calendar()
  648         (then, success) = cal.parse(date)
  649         if success:
  650             timestamp = time.mktime(then)
  651             logger.debug("Using time: %s", time.asctime(then))
  652             bInfo = db.getBackupSetInfoForTime(timestamp)
  653             if bInfo and bInfo['backupset'] != 1:
  654                 bset = bInfo['backupset']
  655                 logger.debug("Using backupset: %s %d", bInfo['name'], bInfo['backupset'])
  656             else:
  657                 logger.critical("No backupset at date: %s (%s)", date, time.asctime(then))
  658                 bInfo = None
  659         else:
  660             logger.critical("Could not parse date string: %s", date)
  661     elif backup:
  662         try:
  663             bset = int(backup)
  664             logger.debug("Using integer value: %d", bset)
  665             bInfo = db.getBackupSetInfoById(bset)
  666         except ValueError:
  667             logger.debug("Using string value: %s", backup)
  668             if backup == current:
  669                 bInfo = db.lastBackupSet()
  670             else:
  671                 bInfo = db.getBackupSetInfo(backup)
  672             if not bInfo:
  673                 logger.critical("No backupset at for name: %s", backup)
  674     elif defaultCurrent:
  675         bInfo = db.lastBackupSet()
  676     return bInfo
  677 
  678 def main():
  679     global logger
  680     parseArgs()
  681     logger = Util.setupLogging(args.verbose)
  682 
  683     # Commands which cannot be executed on remote databases
  684     allowRemote = args.command not in ['create', 'upgrade']
  685 
  686     db      = None
  687     crypt   = None
  688     cache   = None
  689     try:
  690         confirm = args.command in ['setpass', 'create']
  691         allowNone = args.command not in ['setpass', 'chpass']
  692         try:
  693             password = Util.getPassword(args.password, args.passwordfile, args.passwordprog, prompt="Password for %s: " % (args.client), allowNone=allowNone, confirm=confirm)
  694         except Exception as e:
  695             logger.critical(str(e))
  696             if args.exceptions:
  697                 logger.exception(e)
  698             return -1
  699             
  700         if args.command == 'create':
  701             if password and not Util.checkPasswordStrength(password):
  702                 return -1
  703             return createClient(password)
  704 
  705         if args.command == 'setpass':
  706             if not Util.checkPasswordStrength(password):
  707                 return -1
  708 
  709             return setPassword(password)
  710 
  711         if args.command == 'chpass':
  712             return changePassword(crypt, password)
  713 
  714         upgrade = (args.command == 'upgrade')
  715 
  716         try:
  717             (db, cache, crypt) = getDB(password, allowRemote=allowRemote, allowUpgrade=upgrade)
  718 
  719             if crypt and args.command != 'keys':
  720                 if args.keys:
  721                     (f, c) = Util.loadKeys(args.keys, db.getConfigValue('ClientID'))
  722                 else:
  723                     (f, c) = db.getKeys()
  724                 crypt.setKeys(f, c)
  725         except TardisDB.AuthenticationException as e:
  726             logger.error("Authentication failed.  Bad password")
  727             if args.exceptions:
  728                 logger.exception(e)
  729             sys.exit(1)
  730         except Exception as e:
  731             logger.critical("Unable to connect to database: %s", e)
  732             if args.exceptions:
  733                 logger.exception(e)
  734             sys.exit(1)
  735 
  736         if args.command == 'keys':
  737             return moveKeys(db, crypt)
  738         elif args.command == 'list':
  739             return listBSets(db, crypt, cache)
  740         elif args.command == 'files':
  741             return listFiles(db, crypt)
  742         elif args.command == 'info':
  743             return bsetInfo(db)
  744         elif args.command == 'purge':
  745             return purge(db, cache)
  746         elif args.command == 'delete':
  747             return deleteBsets(db, cache)
  748         elif args.command == 'priority':
  749             return setPriority(db)
  750         elif args.command == 'rename':
  751             return renameSet(db)
  752         elif args.command == 'getconfig':
  753             return getConfig(db)
  754         elif args.command == 'setconfig':
  755             return setConfig(db)
  756         elif args.command == 'orphans':
  757             return removeOrphans(db, cache)
  758         elif args.command == 'upgrade':
  759             return
  760     except KeyboardInterrupt:
  761         pass
  762     except TardisDB.AuthenticationException as e:
  763         logger.error("Authentication failed.  Bad password")
  764         sys.exit(1)
  765     except Exception as e:
  766         logger.error("Caught exception: %s", str(e))
  767         if args.exceptions:
  768             logger.exception(e)
  769     finally:
  770         if db:
  771             db.close()
  772 
  773 if __name__ == "__main__":
  774     main()