backup.py 3.4 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137
  1. import contextlib
  2. import os
  3. import yaml
  4. import sys
  5. from glob import glob
  6. @contextlib.contextmanager
  7. def pushd(newDir):
  8. previousDir = os.getcwd()
  9. os.chdir(newDir)
  10. try:
  11. yield
  12. finally:
  13. os.chdir(previousDir)
  14. class DependencyException(Exception):
  15. pass
  16. class BackupException(Exception):
  17. pass
  18. def deptree(sources, deps=None):
  19. if deps is None:
  20. deps = []
  21. deferred = []
  22. for name in sources.keys():
  23. source = sources[name]
  24. if "depends" in source and not source["depends"]:
  25. deferred.append(name)
  26. else:
  27. deps.append(name)
  28. while deferred:
  29. name = deferred.pop()
  30. depends = sources[name]["depends"]
  31. # todo - detect dependency loop
  32. if name in depends:
  33. raise DependencyException('Source {} depends upon itself'.format(name))
  34. elif set(depends).issubset(set(deps)):
  35. deps.append(name)
  36. elif not set(depends).issubset(set(deps)):
  37. missing = ', '.join(set(depends).difference(set(deps)))
  38. raise DependencyException(
  39. 'Source {0} has missing dependencies: {1}'.format(name, missing))
  40. else:
  41. deferred.append(name)
  42. return deps
  43. def status(name, value=None):
  44. if not hasattr(status, "_handle"):
  45. status._handle = {}
  46. if value is None:
  47. return status._handle[name] if name in status._handle else None
  48. status._handle[name] = value
  49. def backup(name):
  50. source = main.sources[name]
  51. failed = [x for x in source["depends"] if not status(x)]
  52. if failed:
  53. raise BackupException(
  54. "Unable to backup {0} due to ncomplete backups: {1}".format(
  55. name, failed))
  56. # TODO - handle explicit failures with false
  57. # status(name, False)
  58. status(name, True)
  59. def main(args):
  60. with pushd('etc/backup.d'):
  61. with open("backup.yml") as f:
  62. main.config = yaml.load(f)
  63. sources = {}
  64. for source in main.config['sources']:
  65. source = os.path.realpath(source)
  66. for path in glob('{}/*.yml'.format(source)):
  67. path = os.path.realpath(path)
  68. with pushd(os.path.dirname(path)), open(path) as f:
  69. data = yaml.load(f)
  70. if "depends" not in data:
  71. data["depends"] = []
  72. for i in range(0, len(data["depends"])):
  73. data["depends"][i] = os.path.realpath(
  74. '{}.yml'.format(data["depends"][i]))
  75. sources[path] = data
  76. main.sources = sources
  77. main.deptree = deptree(sources)
  78. errors = []
  79. for name in main.deptree:
  80. try:
  81. backup(name)
  82. except BackupException as ex:
  83. print(ex)
  84. errors.append(ex)
  85. if errors:
  86. raise BackupException("At least one backup failed")
  87. if __name__ == '__main__':
  88. try:
  89. main(sys.argv[1:])
  90. except DependencyException as ex:
  91. print(ex)
  92. sys.exit(1)
  93. except BackupException as ex:
  94. print(ex)
  95. sys.exit(1)
  96. except Exception:
  97. from traceback import format_exc
  98. msg = "Error encountered:\n" + format_exc().strip()
  99. print(msg)
  100. sys.exit(1)