shared.py 6.6 KB

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