Tardis  1.2.1
About: Tardis is a system for making encrypted, incremental backups of filesystems.
  Fossies Dox: Tardis-1.2.1.tar.gz  ("unofficial" and yet experimental doxygen-generated source code documentation)  

Sonic.py
Go to the documentation of this file.
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
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
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
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()
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)
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
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()
def confirm()
Definition: Sonic.py:431
def getDB(password, new=False, allowRemote=True, allowUpgrade=False)
Definition: Sonic.py:65
def getCommandLine(db, commandLineCksum)
Definition: Sonic.py:242
def _decryptFilename(name, crypt)
Definition: Sonic.py:306
def removeOrphans(db, cache)
Definition: Sonic.py:489
def renameSet(db)
Definition: Sonic.py:524
def listBSets(db, crypt, cache)
Definition: Sonic.py:256
def createClient(password)
Definition: Sonic.py:106
def purge(db, cache)
Definition: Sonic.py:439
def bsetInfo(db)
Definition: Sonic.py:413
def parseArgs()
Definition: Sonic.py:531
def changePassword(crypt, oldpw)
Definition: Sonic.py:155
def _printConfigKey(db, key)
Definition: Sonic.py:500
def getConfig(db)
Definition: Sonic.py:505
def deleteBsets(db, cache)
Definition: Sonic.py:467
def moveKeys(db, crypt)
Definition: Sonic.py:207
def setPriority(db)
Definition: Sonic.py:520
def main()
Definition: Sonic.py:678
def listFiles(db, crypt)
Definition: Sonic.py:326
def _path(db, crypt, bset, inode)
Definition: Sonic.py:309
def getBackupSet(db, backup, date, defaultCurrent=False)
Definition: Sonic.py:644
def _bsetInfo(db, info)
Definition: Sonic.py:387
def setPassword(password)
Definition: Sonic.py:123
def setConfig(db)
Definition: Sonic.py:515