borg.py 6.3 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203
  1. from datetime import datetime
  2. import glob
  3. import itertools
  4. import os
  5. import platform
  6. import re
  7. import subprocess
  8. import tempfile
  9. from borgmatic.verbosity import VERBOSITY_SOME, VERBOSITY_LOTS
  10. # Integration with Borg for actually handling backups.
  11. COMMAND = 'borg'
  12. def initialize(storage_config, command=COMMAND):
  13. passphrase = storage_config.get('encryption_passphrase')
  14. if passphrase:
  15. os.environ['{}_PASSPHRASE'.format(command.upper())] = passphrase
  16. def _write_exclude_file(exclude_patterns=None):
  17. '''
  18. Given a sequence of exclude patterns, write them to a named temporary file and return it. Return
  19. None if no patterns are provided.
  20. '''
  21. if not exclude_patterns:
  22. return None
  23. exclude_file = tempfile.NamedTemporaryFile('w')
  24. exclude_file.write('\n'.join(exclude_patterns))
  25. exclude_file.flush()
  26. return exclude_file
  27. def create_archive(
  28. verbosity, repository, location_config, storage_config, command=COMMAND,
  29. ):
  30. '''
  31. Given a vebosity flag, a storage config dict, a list of source directories, a local or remote
  32. repository path, a list of exclude patterns, and a command to run, create an attic archive.
  33. '''
  34. sources = tuple(
  35. itertools.chain.from_iterable(
  36. glob.glob(directory) or [directory]
  37. for directory in location_config['source_directories']
  38. )
  39. )
  40. exclude_file = _write_exclude_file(location_config.get('exclude_patterns'))
  41. exclude_flags = ('--exclude-from', exclude_file.name) if exclude_file else ()
  42. compression = storage_config.get('compression', None)
  43. compression_flags = ('--compression', compression) if compression else ()
  44. umask = storage_config.get('umask', None)
  45. umask_flags = ('--umask', str(umask)) if umask else ()
  46. one_file_system_flags = ('--one-file-system',) if location_config.get('one_file_system') else ()
  47. remote_path = location_config.get('remote_path')
  48. remote_path_flags = ('--remote-path', remote_path) if remote_path else ()
  49. verbosity_flags = {
  50. VERBOSITY_SOME: ('--info', '--stats',),
  51. VERBOSITY_LOTS: ('--debug', '--list', '--stats'),
  52. }.get(verbosity, ())
  53. full_command = (
  54. command, 'create',
  55. '{repo}::{hostname}-{timestamp}'.format(
  56. repo=repository,
  57. hostname=platform.node(),
  58. timestamp=datetime.now().isoformat(),
  59. ),
  60. ) + sources + exclude_flags + compression_flags + one_file_system_flags + \
  61. remote_path_flags + umask_flags + verbosity_flags
  62. subprocess.check_call(full_command)
  63. def _make_prune_flags(retention_config):
  64. '''
  65. Given a retention config dict mapping from option name to value, tranform it into an iterable of
  66. command-line name-value flag pairs.
  67. For example, given a retention config of:
  68. {'keep_weekly': 4, 'keep_monthly': 6}
  69. This will be returned as an iterable of:
  70. (
  71. ('--keep-weekly', '4'),
  72. ('--keep-monthly', '6'),
  73. )
  74. '''
  75. return (
  76. ('--' + option_name.replace('_', '-'), str(retention_config[option_name]))
  77. for option_name, value in retention_config.items()
  78. )
  79. def prune_archives(verbosity, repository, retention_config, command=COMMAND, remote_path=None):
  80. '''
  81. Given a verbosity flag, a local or remote repository path, a retention config dict, and a
  82. command to run, prune attic archives according the the retention policy specified in that
  83. configuration.
  84. '''
  85. remote_path_flags = ('--remote-path', remote_path) if remote_path else ()
  86. verbosity_flags = {
  87. VERBOSITY_SOME: ('--info', '--stats',),
  88. VERBOSITY_LOTS: ('--debug', '--stats'),
  89. }.get(verbosity, ())
  90. full_command = (
  91. command, 'prune',
  92. repository,
  93. ) + tuple(
  94. element
  95. for pair in _make_prune_flags(retention_config)
  96. for element in pair
  97. ) + remote_path_flags + verbosity_flags
  98. subprocess.check_call(full_command)
  99. DEFAULT_CHECKS = ('repository', 'archives')
  100. def _parse_checks(consistency_config):
  101. '''
  102. Given a consistency config with a "checks" list, transform it to a tuple of named checks to run.
  103. For example, given a retention config of:
  104. {'checks': ['repository', 'archives']}
  105. This will be returned as:
  106. ('repository', 'archives')
  107. If no "checks" option is present, return the DEFAULT_CHECKS. If the checks value is the string
  108. "disabled", return an empty tuple, meaning that no checks should be run.
  109. '''
  110. checks = consistency_config.get('checks', [])
  111. if checks == ['disabled']:
  112. return ()
  113. return tuple(check for check in checks if check.lower() not in ('disabled', '')) or DEFAULT_CHECKS
  114. def _make_check_flags(checks, check_last=None):
  115. '''
  116. Given a parsed sequence of checks, transform it into tuple of command-line flags.
  117. For example, given parsed checks of:
  118. ('repository',)
  119. This will be returned as:
  120. ('--repository-only',)
  121. Additionally, if a check_last value is given, a "--last" flag will be added. Note that only
  122. Borg supports this flag.
  123. '''
  124. last_flag = ('--last', check_last) if check_last else ()
  125. if checks == DEFAULT_CHECKS:
  126. return last_flag
  127. return tuple(
  128. '--{}-only'.format(check) for check in checks
  129. ) + last_flag
  130. def check_archives(verbosity, repository, consistency_config, command=COMMAND, remote_path=None):
  131. '''
  132. Given a verbosity flag, a local or remote repository path, a consistency config dict, and a
  133. command to run, check the contained attic archives for consistency.
  134. If there are no consistency checks to run, skip running them.
  135. '''
  136. checks = _parse_checks(consistency_config)
  137. check_last = consistency_config.get('check_last', None)
  138. if not checks:
  139. return
  140. remote_path_flags = ('--remote-path', remote_path) if remote_path else ()
  141. verbosity_flags = {
  142. VERBOSITY_SOME: ('--info',),
  143. VERBOSITY_LOTS: ('--debug',),
  144. }.get(verbosity, ())
  145. full_command = (
  146. command, 'check',
  147. repository,
  148. ) + _make_check_flags(checks, check_last) + remote_path_flags + verbosity_flags
  149. # The check command spews to stdout/stderr even without the verbose flag. Suppress it.
  150. stdout = None if verbosity_flags else open(os.devnull, 'w')
  151. subprocess.check_call(full_command, stdout=stdout, stderr=subprocess.STDOUT)