bootstrap.py 5.1 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142
  1. import json
  2. import logging
  3. import os
  4. import borgmatic.borg.extract
  5. import borgmatic.borg.repo_list
  6. import borgmatic.config.paths
  7. import borgmatic.config.validate
  8. import borgmatic.hooks.command
  9. logger = logging.getLogger(__name__)
  10. def make_bootstrap_config(bootstrap_arguments):
  11. '''
  12. Given the bootstrap arguments as an argparse.Namespace, return a corresponding config dict.
  13. '''
  14. return {
  15. 'borgmatic_source_directory': bootstrap_arguments.borgmatic_source_directory,
  16. 'local_path': bootstrap_arguments.local_path,
  17. 'remote_path': bootstrap_arguments.remote_path,
  18. # In case the repo has been moved or is accessed from a different path at the point of
  19. # bootstrapping.
  20. 'relocated_repo_access_is_ok': True,
  21. 'ssh_command': bootstrap_arguments.ssh_command,
  22. 'user_runtime_directory': bootstrap_arguments.user_runtime_directory,
  23. }
  24. def load_config_paths_from_archive(
  25. repository_path,
  26. archive_name,
  27. config,
  28. local_borg_version,
  29. global_arguments,
  30. borgmatic_runtime_directory,
  31. ):
  32. '''
  33. Given a repository path, an archive name, a configuration dict, the local Borg version, the
  34. global arguments as an argparse.Namespace, and the borgmatic runtime directory, return the
  35. config paths from the manifest.json file in the borgmatic source directory or runtime directory
  36. within the repository archive.
  37. Raise ValueError if the manifest JSON is missing, can't be decoded, or doesn't contain the
  38. expected configuration path data.
  39. '''
  40. # Probe for the manifest file in multiple locations, as the default location has moved to the
  41. # borgmatic runtime directory (which gets stored as just "/borgmatic" with Borg 1.4+). But we
  42. # still want to support reading the manifest from previously created archives as well.
  43. for base_directory in (
  44. 'borgmatic',
  45. borgmatic.config.paths.make_runtime_directory_glob(borgmatic_runtime_directory),
  46. borgmatic.config.paths.get_borgmatic_source_directory(config),
  47. ):
  48. borgmatic_manifest_path = 'sh:' + os.path.join(
  49. base_directory,
  50. 'bootstrap',
  51. 'manifest.json',
  52. )
  53. extract_process = borgmatic.borg.extract.extract_archive(
  54. global_arguments.dry_run,
  55. repository_path,
  56. archive_name,
  57. [borgmatic_manifest_path],
  58. config,
  59. local_borg_version,
  60. global_arguments,
  61. local_path=config.get('local_path'),
  62. remote_path=config.get('remote_path'),
  63. extract_to_stdout=True,
  64. )
  65. manifest_json = extract_process.stdout.read()
  66. if manifest_json:
  67. break
  68. else:
  69. raise ValueError(
  70. 'Cannot read configuration paths from archive due to missing archive or bootstrap manifest',
  71. )
  72. try:
  73. manifest_data = json.loads(manifest_json)
  74. except json.JSONDecodeError as error:
  75. raise ValueError(
  76. f'Cannot read configuration paths from archive due to invalid bootstrap manifest JSON: {error}',
  77. )
  78. try:
  79. return manifest_data['config_paths']
  80. except KeyError:
  81. raise ValueError(
  82. 'Cannot read configuration paths from archive due to invalid bootstrap manifest',
  83. )
  84. def run_bootstrap(bootstrap_arguments, global_arguments, local_borg_version):
  85. '''
  86. Run the "bootstrap" action for the given repository.
  87. Raise ValueError if the bootstrap configuration could not be loaded.
  88. Raise CalledProcessError or OSError if Borg could not be run.
  89. '''
  90. config = make_bootstrap_config(bootstrap_arguments)
  91. archive_name = borgmatic.borg.repo_list.resolve_archive_name(
  92. bootstrap_arguments.repository,
  93. bootstrap_arguments.archive,
  94. config,
  95. local_borg_version,
  96. global_arguments,
  97. local_path=bootstrap_arguments.local_path,
  98. remote_path=bootstrap_arguments.remote_path,
  99. )
  100. with borgmatic.config.paths.Runtime_directory(config) as borgmatic_runtime_directory:
  101. manifest_config_paths = load_config_paths_from_archive(
  102. bootstrap_arguments.repository,
  103. archive_name,
  104. config,
  105. local_borg_version,
  106. global_arguments,
  107. borgmatic_runtime_directory,
  108. )
  109. logger.info(f"Bootstrapping config paths: {', '.join(manifest_config_paths)}")
  110. borgmatic.borg.extract.extract_archive(
  111. global_arguments.dry_run,
  112. bootstrap_arguments.repository,
  113. archive_name,
  114. [config_path.lstrip(os.path.sep) for config_path in manifest_config_paths],
  115. # Only add progress here and not the extract_archive() call above, because progress
  116. # conflicts with extract_to_stdout.
  117. dict(config, progress=bootstrap_arguments.progress or False),
  118. local_borg_version,
  119. global_arguments,
  120. local_path=bootstrap_arguments.local_path,
  121. remote_path=bootstrap_arguments.remote_path,
  122. extract_to_stdout=False,
  123. destination_path=bootstrap_arguments.destination,
  124. strip_components=bootstrap_arguments.strip_components,
  125. )