borgmatic.py 5.8 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164
  1. from argparse import ArgumentParser
  2. import logging
  3. import os
  4. from subprocess import CalledProcessError
  5. import sys
  6. from borgmatic.borg import check, create, prune
  7. from borgmatic.commands import hook
  8. from borgmatic.config import collect, convert, validate
  9. from borgmatic.signals import configure_signals
  10. from borgmatic.verbosity import VERBOSITY_SOME, VERBOSITY_LOTS, verbosity_to_log_level
  11. logger = logging.getLogger(__name__)
  12. LEGACY_CONFIG_PATH = '/etc/borgmatic/config'
  13. def parse_arguments(*arguments):
  14. '''
  15. Given command-line arguments with which this script was invoked, parse the arguments and return
  16. them as an ArgumentParser instance.
  17. '''
  18. parser = ArgumentParser(
  19. description=
  20. '''
  21. A simple wrapper script for the Borg backup software that creates and prunes backups.
  22. If none of the --prune, --create, or --check options are given, then borgmatic defaults
  23. to all three: prune, create, and check archives.
  24. '''
  25. )
  26. parser.add_argument(
  27. '-c', '--config',
  28. nargs='+',
  29. dest='config_paths',
  30. default=collect.DEFAULT_CONFIG_PATHS,
  31. help='Configuration filenames or directories, defaults to: {}'.format(' '.join(collect.DEFAULT_CONFIG_PATHS)),
  32. )
  33. parser.add_argument(
  34. '--excludes',
  35. dest='excludes_filename',
  36. help='Deprecated in favor of exclude_patterns within configuration',
  37. )
  38. parser.add_argument(
  39. '-p', '--prune',
  40. dest='prune',
  41. action='store_true',
  42. help='Prune archives according to the retention policy',
  43. )
  44. parser.add_argument(
  45. '-C', '--create',
  46. dest='create',
  47. action='store_true',
  48. help='Create archives (actually perform backups)',
  49. )
  50. parser.add_argument(
  51. '-k', '--check',
  52. dest='check',
  53. action='store_true',
  54. help='Check archives for consistency',
  55. )
  56. parser.add_argument(
  57. '-n', '--dry-run',
  58. dest='dry_run',
  59. action='store_true',
  60. help='Go through the motions, but do not actually write any changes to the repository',
  61. )
  62. parser.add_argument(
  63. '-v', '--verbosity',
  64. type=int,
  65. help='Display verbose progress (1 for some, 2 for lots)',
  66. )
  67. args = parser.parse_args(arguments)
  68. # If any of the three action flags in the given parse arguments have been explicitly requested,
  69. # leave them as-is. Otherwise, assume defaults: Mutate the given arguments to enable all the
  70. # actions.
  71. if not args.prune and not args.create and not args.check:
  72. args.prune = True
  73. args.create = True
  74. args.check = True
  75. return args
  76. def run_configuration(config_filename, args): # pragma: no cover
  77. '''
  78. Parse a single configuration file, and execute its defined pruning, backups, and/or consistency
  79. checks.
  80. '''
  81. logger.info('{}: Parsing configuration file'.format(config_filename))
  82. config = validate.parse_configuration(config_filename, validate.schema_filename())
  83. (location, storage, retention, consistency, hooks) = (
  84. config.get(section_name, {})
  85. for section_name in ('location', 'storage', 'retention', 'consistency', 'hooks')
  86. )
  87. try:
  88. local_path = location.get('local_path', 'borg')
  89. remote_path = location.get('remote_path')
  90. create.initialize_environment(storage)
  91. hook.execute_hook(hooks.get('before_backup'), config_filename, 'pre-backup')
  92. for unexpanded_repository in location['repositories']:
  93. repository = os.path.expanduser(unexpanded_repository)
  94. dry_run_label = ' (dry run; not making any changes)' if args.dry_run else ''
  95. if args.prune:
  96. logger.info('{}: Pruning archives{}'.format(repository, dry_run_label))
  97. prune.prune_archives(
  98. args.verbosity,
  99. args.dry_run,
  100. repository,
  101. retention,
  102. local_path=local_path,
  103. remote_path=remote_path,
  104. )
  105. if args.create:
  106. logger.info('{}: Creating archive{}'.format(repository, dry_run_label))
  107. create.create_archive(
  108. args.verbosity,
  109. args.dry_run,
  110. repository,
  111. location,
  112. storage,
  113. local_path=local_path,
  114. remote_path=remote_path,
  115. )
  116. if args.check:
  117. logger.info('{}: Running consistency checks'.format(repository))
  118. check.check_archives(
  119. args.verbosity,
  120. repository,
  121. consistency,
  122. local_path=local_path,
  123. remote_path=remote_path,
  124. )
  125. hook.execute_hook(hooks.get('after_backup'), config_filename, 'post-backup')
  126. except (OSError, CalledProcessError):
  127. hook.execute_hook(hooks.get('on_error'), config_filename, 'on-error')
  128. raise
  129. def main(): # pragma: no cover
  130. try:
  131. configure_signals()
  132. args = parse_arguments(*sys.argv[1:])
  133. logging.basicConfig(level=verbosity_to_log_level(args.verbosity), format='%(message)s')
  134. config_filenames = tuple(collect.collect_config_filenames(args.config_paths))
  135. logger.debug('Ensuring legacy configuration is upgraded')
  136. convert.guard_configuration_upgraded(LEGACY_CONFIG_PATH, config_filenames)
  137. if len(config_filenames) == 0:
  138. raise ValueError('Error: No configuration files found in: {}'.format(' '.join(args.config_paths)))
  139. for config_filename in config_filenames:
  140. run_configuration(config_filename, args)
  141. except (ValueError, OSError, CalledProcessError) as error:
  142. print(error, file=sys.stderr)
  143. sys.exit(1)