config.py 6.3 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167
  1. import json
  2. import logging
  3. import shutil
  4. import subprocess
  5. import borgmatic.borg.pattern
  6. from borgmatic.execute import execute_command_and_capture_output
  7. IS_A_HOOK = False
  8. logger = logging.getLogger(__name__)
  9. def resolve_database_option(option, data_source, connection_params=None, restore=False):
  10. '''
  11. Resolves a database option from the given data source configuration dict and
  12. connection parameters dict. If restore is set to True it will consider the
  13. `restore_<option>` instead.
  14. Returns the resolved option or None. Can raise a ValueError if the hostname lookup
  15. results in a container IP check.
  16. '''
  17. # Special case `hostname` since it overlaps with `container`
  18. if option == 'hostname':
  19. return get_hostname_from_config(data_source, connection_params, restore)
  20. if connection_params and (value := connection_params.get(option)):
  21. return value
  22. if restore and f'restore_{option}' in data_source:
  23. return data_source[f'restore_{option}']
  24. return data_source.get(option)
  25. def get_hostname_from_config(data_source, connection_params=None, restore=False):
  26. '''
  27. Specialisation of `resolve_database_option` to handle the extra complexity of
  28. the hostname option to also handle containers.
  29. Returns a hostname/IP or raises an ValueError if a container IP lookup fails.
  30. '''
  31. # connection params win, full stop
  32. if connection_params:
  33. if container := connection_params.get('container'):
  34. return get_ip_from_container(container)
  35. if hostname := connection_params.get('hostname'):
  36. return hostname
  37. # ... then try the restore config
  38. if restore:
  39. if 'restore_container' in data_source:
  40. return get_ip_from_container(data_source['restore_container'])
  41. if 'restore_hostname' in data_source:
  42. return data_source['restore_hostname']
  43. # ... and finally fall back to the normal options
  44. if 'container' in data_source:
  45. return get_ip_from_container(data_source['container'])
  46. return data_source.get('hostname')
  47. def get_ip_from_container(container):
  48. '''
  49. Determine the IP for a given container name via podman and docker.
  50. Returns an IP or raises a ValueError if the lookup fails.
  51. '''
  52. engines = (shutil.which(engine) for engine in ('docker', 'podman'))
  53. engines = [engine for engine in engines if engine]
  54. if not engines:
  55. raise ValueError("Neither 'docker' nor 'podman' could be found on the system")
  56. last_error = None
  57. for engine in engines:
  58. try:
  59. output = execute_command_and_capture_output(
  60. (
  61. engine,
  62. 'container',
  63. 'inspect',
  64. '--format={{json .NetworkSettings}}',
  65. container,
  66. )
  67. )
  68. except subprocess.CalledProcessError as error:
  69. last_error = error
  70. logger.debug(f"Could not find container '{container}' with engine '{engine}'")
  71. continue # Container does not exist
  72. try:
  73. network_data = json.loads(output.strip())
  74. except json.JSONDecodeError as e:
  75. raise ValueError(f'Could not decode JSON output from {engine}') from e
  76. if main_ip := network_data.get('IPAddress'):
  77. return main_ip
  78. # No main IP found, try the networks
  79. for network in network_data.get('Networks', {}).values():
  80. if ip := network.get('IPAddress'):
  81. return ip
  82. if last_error:
  83. raise last_error
  84. raise ValueError(
  85. f"Could not determine ip address for container '{container}'; running in host mode or userspace networking?"
  86. )
  87. def inject_pattern(patterns, data_source_pattern):
  88. '''
  89. Given a list of borgmatic.borg.pattern.Pattern instances representing the configured patterns,
  90. insert the given data source pattern at the start of the list. The idea is that borgmatic is
  91. injecting its own custom pattern specific to a data source hook into the user's configured
  92. patterns so that the hook's data gets included in the backup.
  93. As part of this injection, if the data source pattern is a root pattern, also insert an
  94. "include" version of the given root pattern, in an attempt to preempt any of the user's
  95. configured exclude patterns that may follow.
  96. '''
  97. if data_source_pattern.type == borgmatic.borg.pattern.Pattern_type.ROOT:
  98. patterns.insert(
  99. 0,
  100. borgmatic.borg.pattern.Pattern(
  101. path=data_source_pattern.path,
  102. type=borgmatic.borg.pattern.Pattern_type.INCLUDE,
  103. style=data_source_pattern.style,
  104. device=data_source_pattern.device,
  105. source=borgmatic.borg.pattern.Pattern_source.HOOK,
  106. ),
  107. )
  108. patterns.insert(0, data_source_pattern)
  109. def replace_pattern(patterns, pattern_to_replace, data_source_pattern):
  110. '''
  111. Given a list of borgmatic.borg.pattern.Pattern instances representing the configured patterns,
  112. replace the given pattern with the given data source pattern. The idea is that borgmatic is
  113. replacing a configured pattern with its own modified pattern specific to a data source hook so
  114. that the hook's data gets included in the backup.
  115. As part of this replacement, if the data source pattern is a root pattern, also insert an
  116. "include" version of the given root pattern right after the replaced pattern, in an attempt to
  117. preempt any of the user's configured exclude patterns that may follow.
  118. If the pattern to replace can't be found in the given patterns, then just inject the data source
  119. pattern at the start of the list.
  120. '''
  121. try:
  122. index = patterns.index(pattern_to_replace)
  123. except ValueError:
  124. inject_pattern(patterns, data_source_pattern)
  125. return
  126. patterns[index] = data_source_pattern
  127. if data_source_pattern.type == borgmatic.borg.pattern.Pattern_type.ROOT:
  128. patterns.insert(
  129. index + 1,
  130. borgmatic.borg.pattern.Pattern(
  131. path=data_source_pattern.path,
  132. type=borgmatic.borg.pattern.Pattern_type.INCLUDE,
  133. style=data_source_pattern.style,
  134. device=data_source_pattern.device,
  135. source=borgmatic.borg.pattern.Pattern_source.HOOK,
  136. ),
  137. )