shared.py 6.5 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219
  1. from datetime import datetime
  2. import os
  3. import re
  4. import platform
  5. import subprocess
  6. from glob import glob
  7. from itertools import chain
  8. from atticmatic.config import Section_format, option
  9. from atticmatic.verbosity import VERBOSITY_SOME, VERBOSITY_LOTS
  10. # Common backend functionality shared by Attic and Borg. As the two backup
  11. # commands diverge, these shared functions will likely need to be replaced
  12. # with non-shared functions within atticmatic.backends.attic and
  13. # atticmatic.backends.borg.
  14. CONFIG_FORMAT = (
  15. Section_format(
  16. 'location',
  17. (
  18. option('source_directories'),
  19. option('repository'),
  20. ),
  21. ),
  22. Section_format(
  23. 'storage',
  24. (
  25. option('encryption_passphrase', required=False),
  26. ),
  27. ),
  28. Section_format(
  29. 'retention',
  30. (
  31. option('keep_within', required=False),
  32. option('keep_hourly', int, required=False),
  33. option('keep_daily', int, required=False),
  34. option('keep_weekly', int, required=False),
  35. option('keep_monthly', int, required=False),
  36. option('keep_yearly', int, required=False),
  37. option('prefix', required=False),
  38. ),
  39. ),
  40. Section_format(
  41. 'consistency',
  42. (
  43. option('checks', required=False),
  44. ),
  45. )
  46. )
  47. def initialize(storage_config, command):
  48. passphrase = storage_config.get('encryption_passphrase')
  49. if passphrase:
  50. os.environ['{}_PASSPHRASE'.format(command.upper())] = passphrase
  51. def create_archive(
  52. excludes_filename, verbosity, storage_config, source_directories, repository, command,
  53. one_file_system=None
  54. ):
  55. '''
  56. Given an excludes filename (or None), a vebosity flag, a storage config dict, a space-separated
  57. list of source directories, a local or remote repository path, and a command to run, create an
  58. attic archive.
  59. '''
  60. sources = re.split('\s+', source_directories)
  61. sources = tuple(chain.from_iterable(glob(x) or [x] for x in sources))
  62. exclude_flags = ('--exclude-from', excludes_filename) if excludes_filename else ()
  63. compression = storage_config.get('compression', None)
  64. compression_flags = ('--compression', compression) if compression else ()
  65. umask = storage_config.get('umask', None)
  66. umask_flags = ('--umask', str(umask)) if umask else ()
  67. one_file_system_flags = ('--one-file-system',) if one_file_system else ()
  68. verbosity_flags = {
  69. VERBOSITY_SOME: ('--stats',),
  70. VERBOSITY_LOTS: ('--verbose', '--stats'),
  71. }.get(verbosity, ())
  72. full_command = (
  73. command, 'create',
  74. '{repo}::{hostname}-{timestamp}'.format(
  75. repo=repository,
  76. hostname=platform.node(),
  77. timestamp=datetime.now().isoformat(),
  78. ),
  79. ) + sources + exclude_flags + compression_flags + one_file_system_flags + \
  80. umask_flags + verbosity_flags
  81. subprocess.check_call(full_command)
  82. def _make_prune_flags(retention_config):
  83. '''
  84. Given a retention config dict mapping from option name to value, tranform it into an iterable of
  85. command-line name-value flag pairs.
  86. For example, given a retention config of:
  87. {'keep_weekly': 4, 'keep_monthly': 6}
  88. This will be returned as an iterable of:
  89. (
  90. ('--keep-weekly', '4'),
  91. ('--keep-monthly', '6'),
  92. )
  93. '''
  94. return (
  95. ('--' + option_name.replace('_', '-'), str(retention_config[option_name]))
  96. for option_name, value in retention_config.items()
  97. )
  98. def prune_archives(verbosity, repository, retention_config, command):
  99. '''
  100. Given a verbosity flag, a local or remote repository path, a retention config dict, and a
  101. command to run, prune attic archives according the the retention policy specified in that
  102. configuration.
  103. '''
  104. verbosity_flags = {
  105. VERBOSITY_SOME: ('--stats',),
  106. VERBOSITY_LOTS: ('--verbose', '--stats'),
  107. }.get(verbosity, ())
  108. full_command = (
  109. command, 'prune',
  110. repository,
  111. ) + tuple(
  112. element
  113. for pair in _make_prune_flags(retention_config)
  114. for element in pair
  115. ) + verbosity_flags
  116. subprocess.check_call(full_command)
  117. DEFAULT_CHECKS = ('repository', 'archives')
  118. def _parse_checks(consistency_config):
  119. '''
  120. Given a consistency config with a space-separated "checks" option, transform it to a tuple of
  121. named checks to run.
  122. For example, given a retention config of:
  123. {'checks': 'repository archives'}
  124. This will be returned as:
  125. ('repository', 'archives')
  126. If no "checks" option is present, return the DEFAULT_CHECKS. If the checks value is the string
  127. "disabled", return an empty tuple, meaning that no checks should be run.
  128. '''
  129. checks = consistency_config.get('checks', '').strip()
  130. if not checks:
  131. return DEFAULT_CHECKS
  132. return tuple(
  133. check for check in consistency_config['checks'].split(' ')
  134. if check.lower() not in ('disabled', '')
  135. )
  136. def _make_check_flags(checks, check_last=None):
  137. '''
  138. Given a parsed sequence of checks, transform it into tuple of command-line flags.
  139. For example, given parsed checks of:
  140. ('repository',)
  141. This will be returned as:
  142. ('--repository-only',)
  143. Additionally, if a check_last value is given, a "--last" flag will be added. Note that only
  144. Borg supports this flag.
  145. '''
  146. last_flag = ('--last', check_last) if check_last else ()
  147. if checks == DEFAULT_CHECKS:
  148. return last_flag
  149. return tuple(
  150. '--{}-only'.format(check) for check in checks
  151. ) + last_flag
  152. def check_archives(verbosity, repository, consistency_config, command):
  153. '''
  154. Given a verbosity flag, a local or remote repository path, a consistency config dict, and a
  155. command to run, check the contained attic archives for consistency.
  156. If there are no consistency checks to run, skip running them.
  157. '''
  158. checks = _parse_checks(consistency_config)
  159. check_last = consistency_config.get('check_last', None)
  160. if not checks:
  161. return
  162. verbosity_flags = {
  163. VERBOSITY_SOME: ('--verbose',),
  164. VERBOSITY_LOTS: ('--verbose',),
  165. }.get(verbosity, ())
  166. full_command = (
  167. command, 'check',
  168. repository,
  169. ) + _make_check_flags(checks, check_last) + verbosity_flags
  170. # The check command spews to stdout/stderr even without the verbose flag. Suppress it.
  171. stdout = None if verbosity_flags else open(os.devnull, 'w')
  172. subprocess.check_call(full_command, stdout=stdout, stderr=subprocess.STDOUT)