2
0

test_database.py 12 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387
  1. import json
  2. import os
  3. import shutil
  4. import subprocess
  5. import sys
  6. import tempfile
  7. import pytest
  8. def write_configuration(
  9. source_directory,
  10. config_path,
  11. repository_path,
  12. borgmatic_source_directory,
  13. postgresql_dump_format='custom',
  14. mongodb_dump_format='archive',
  15. ):
  16. '''
  17. Write out borgmatic configuration into a file at the config path. Set the options so as to work
  18. for testing. This includes injecting the given repository path, borgmatic source directory for
  19. storing database dumps, dump format (for PostgreSQL), and encryption passphrase.
  20. '''
  21. config = f'''
  22. source_directories:
  23. - {source_directory}
  24. repositories:
  25. - path: {repository_path}
  26. borgmatic_source_directory: {borgmatic_source_directory}
  27. encryption_passphrase: "test"
  28. postgresql_databases:
  29. - name: test
  30. hostname: postgresql
  31. username: postgres
  32. password: test
  33. format: {postgresql_dump_format}
  34. - name: all
  35. hostname: postgresql
  36. username: postgres
  37. password: test
  38. - name: all
  39. format: custom
  40. hostname: postgresql
  41. username: postgres
  42. password: test
  43. mysql_databases:
  44. - name: test
  45. hostname: mysql
  46. username: root
  47. password: test
  48. - name: all
  49. hostname: mysql
  50. username: root
  51. password: test
  52. - name: all
  53. format: sql
  54. hostname: mysql
  55. username: root
  56. password: test
  57. mongodb_databases:
  58. - name: test
  59. hostname: mongodb
  60. username: root
  61. password: test
  62. authentication_database: admin
  63. format: {mongodb_dump_format}
  64. - name: all
  65. hostname: mongodb
  66. username: root
  67. password: test
  68. sqlite_databases:
  69. - name: sqlite_test
  70. path: /tmp/sqlite_test.db
  71. '''
  72. with open(config_path, 'w') as config_file:
  73. config_file.write(config)
  74. def write_custom_restore_configuration(
  75. source_directory,
  76. config_path,
  77. repository_path,
  78. borgmatic_source_directory,
  79. postgresql_dump_format='custom',
  80. mongodb_dump_format='archive',
  81. ):
  82. '''
  83. Write out borgmatic configuration into a file at the config path. Set the options so as to work
  84. for testing with custom restore options. This includes a custom restore_hostname, restore_port,
  85. restore_username, restore_password and restore_path.
  86. '''
  87. config = f'''
  88. source_directories:
  89. - {source_directory}
  90. repositories:
  91. - path: {repository_path}
  92. borgmatic_source_directory: {borgmatic_source_directory}
  93. encryption_passphrase: "test"
  94. postgresql_databases:
  95. - name: test
  96. hostname: postgresql
  97. username: postgres
  98. password: test
  99. format: {postgresql_dump_format}
  100. restore_hostname: postgresql2
  101. restore_port: 5433
  102. restore_username: postgres2
  103. restore_password: test2
  104. mysql_databases:
  105. - name: test
  106. hostname: mysql
  107. username: root
  108. password: test
  109. restore_hostname: mysql2
  110. restore_port: 3307
  111. restore_username: root
  112. restore_password: test2
  113. mongodb_databases:
  114. - name: test
  115. hostname: mongodb
  116. username: root
  117. password: test
  118. authentication_database: admin
  119. format: {mongodb_dump_format}
  120. restore_hostname: mongodb2
  121. restore_port: 27018
  122. restore_username: root2
  123. restore_password: test2
  124. sqlite_databases:
  125. - name: sqlite_test
  126. path: /tmp/sqlite_test.db
  127. restore_path: /tmp/sqlite_test2.db
  128. '''
  129. with open(config_path, 'w') as config_file:
  130. config_file.write(config)
  131. def write_simple_custom_restore_configuration(
  132. source_directory,
  133. config_path,
  134. repository_path,
  135. borgmatic_source_directory,
  136. postgresql_dump_format='custom',
  137. ):
  138. '''
  139. Write out borgmatic configuration into a file at the config path. Set the options so as to work
  140. for testing with custom restore options, but this time using CLI arguments. This includes a
  141. custom restore_hostname, restore_port, restore_username and restore_password as we only test
  142. these options for PostgreSQL.
  143. '''
  144. config = f'''
  145. source_directories:
  146. - {source_directory}
  147. repositories:
  148. - path: {repository_path}
  149. borgmatic_source_directory: {borgmatic_source_directory}
  150. encryption_passphrase: "test"
  151. postgresql_databases:
  152. - name: test
  153. hostname: postgresql
  154. username: postgres
  155. password: test
  156. format: {postgresql_dump_format}
  157. '''
  158. with open(config_path, 'w') as config_file:
  159. config_file.write(config)
  160. def test_database_dump_and_restore():
  161. # Create a Borg repository.
  162. temporary_directory = tempfile.mkdtemp()
  163. repository_path = os.path.join(temporary_directory, 'test.borg')
  164. borgmatic_source_directory = os.path.join(temporary_directory, '.borgmatic')
  165. # Write out a special file to ensure that it gets properly excluded and Borg doesn't hang on it.
  166. os.mkfifo(os.path.join(temporary_directory, 'special_file'))
  167. original_working_directory = os.getcwd()
  168. try:
  169. config_path = os.path.join(temporary_directory, 'test.yaml')
  170. write_configuration(
  171. temporary_directory, config_path, repository_path, borgmatic_source_directory
  172. )
  173. subprocess.check_call(
  174. ['borgmatic', '-v', '2', '--config', config_path, 'rcreate', '--encryption', 'repokey']
  175. )
  176. # Run borgmatic to generate a backup archive including a database dump.
  177. subprocess.check_call(['borgmatic', 'create', '--config', config_path, '-v', '2'])
  178. # Get the created archive name.
  179. output = subprocess.check_output(
  180. ['borgmatic', '--config', config_path, 'list', '--json']
  181. ).decode(sys.stdout.encoding)
  182. parsed_output = json.loads(output)
  183. assert len(parsed_output) == 1
  184. assert len(parsed_output[0]['archives']) == 1
  185. archive_name = parsed_output[0]['archives'][0]['archive']
  186. # Restore the database from the archive.
  187. subprocess.check_call(
  188. ['borgmatic', '-v', '2', '--config', config_path, 'restore', '--archive', archive_name]
  189. )
  190. finally:
  191. os.chdir(original_working_directory)
  192. shutil.rmtree(temporary_directory)
  193. def test_database_dump_and_restore_with_restore_cli_arguments():
  194. # Create a Borg repository.
  195. temporary_directory = tempfile.mkdtemp()
  196. repository_path = os.path.join(temporary_directory, 'test.borg')
  197. borgmatic_source_directory = os.path.join(temporary_directory, '.borgmatic')
  198. original_working_directory = os.getcwd()
  199. try:
  200. config_path = os.path.join(temporary_directory, 'test.yaml')
  201. write_simple_custom_restore_configuration(
  202. temporary_directory, config_path, repository_path, borgmatic_source_directory
  203. )
  204. subprocess.check_call(
  205. ['borgmatic', '-v', '2', '--config', config_path, 'rcreate', '--encryption', 'repokey']
  206. )
  207. # Run borgmatic to generate a backup archive including a database dump.
  208. subprocess.check_call(['borgmatic', 'create', '--config', config_path, '-v', '2'])
  209. # Get the created archive name.
  210. output = subprocess.check_output(
  211. ['borgmatic', '--config', config_path, 'list', '--json']
  212. ).decode(sys.stdout.encoding)
  213. parsed_output = json.loads(output)
  214. assert len(parsed_output) == 1
  215. assert len(parsed_output[0]['archives']) == 1
  216. archive_name = parsed_output[0]['archives'][0]['archive']
  217. # Restore the database from the archive.
  218. subprocess.check_call(
  219. [
  220. 'borgmatic',
  221. '-v',
  222. '2',
  223. '--config',
  224. config_path,
  225. 'restore',
  226. '--archive',
  227. archive_name,
  228. '--hostname',
  229. 'postgresql2',
  230. '--port',
  231. '5433',
  232. '--username',
  233. 'postgres2',
  234. '--password',
  235. 'test2',
  236. ]
  237. )
  238. finally:
  239. os.chdir(original_working_directory)
  240. shutil.rmtree(temporary_directory)
  241. def test_database_dump_and_restore_with_restore_configuration_options():
  242. # Create a Borg repository.
  243. temporary_directory = tempfile.mkdtemp()
  244. repository_path = os.path.join(temporary_directory, 'test.borg')
  245. borgmatic_source_directory = os.path.join(temporary_directory, '.borgmatic')
  246. original_working_directory = os.getcwd()
  247. try:
  248. config_path = os.path.join(temporary_directory, 'test.yaml')
  249. write_custom_restore_configuration(
  250. temporary_directory, config_path, repository_path, borgmatic_source_directory
  251. )
  252. subprocess.check_call(
  253. ['borgmatic', '-v', '2', '--config', config_path, 'rcreate', '--encryption', 'repokey']
  254. )
  255. # Run borgmatic to generate a backup archive including a database dump.
  256. subprocess.check_call(['borgmatic', 'create', '--config', config_path, '-v', '2'])
  257. # Get the created archive name.
  258. output = subprocess.check_output(
  259. ['borgmatic', '--config', config_path, 'list', '--json']
  260. ).decode(sys.stdout.encoding)
  261. parsed_output = json.loads(output)
  262. assert len(parsed_output) == 1
  263. assert len(parsed_output[0]['archives']) == 1
  264. archive_name = parsed_output[0]['archives'][0]['archive']
  265. # Restore the database from the archive.
  266. subprocess.check_call(
  267. ['borgmatic', '-v', '2', '--config', config_path, 'restore', '--archive', archive_name]
  268. )
  269. finally:
  270. os.chdir(original_working_directory)
  271. shutil.rmtree(temporary_directory)
  272. def test_database_dump_and_restore_with_directory_format():
  273. # Create a Borg repository.
  274. temporary_directory = tempfile.mkdtemp()
  275. repository_path = os.path.join(temporary_directory, 'test.borg')
  276. borgmatic_source_directory = os.path.join(temporary_directory, '.borgmatic')
  277. original_working_directory = os.getcwd()
  278. try:
  279. config_path = os.path.join(temporary_directory, 'test.yaml')
  280. write_configuration(
  281. temporary_directory,
  282. config_path,
  283. repository_path,
  284. borgmatic_source_directory,
  285. postgresql_dump_format='directory',
  286. mongodb_dump_format='directory',
  287. )
  288. subprocess.check_call(
  289. ['borgmatic', '-v', '2', '--config', config_path, 'rcreate', '--encryption', 'repokey']
  290. )
  291. # Run borgmatic to generate a backup archive including a database dump.
  292. subprocess.check_call(['borgmatic', 'create', '--config', config_path, '-v', '2'])
  293. # Restore the database from the archive.
  294. subprocess.check_call(
  295. ['borgmatic', '--config', config_path, 'restore', '--archive', 'latest']
  296. )
  297. finally:
  298. os.chdir(original_working_directory)
  299. shutil.rmtree(temporary_directory)
  300. def test_database_dump_with_error_causes_borgmatic_to_exit():
  301. # Create a Borg repository.
  302. temporary_directory = tempfile.mkdtemp()
  303. repository_path = os.path.join(temporary_directory, 'test.borg')
  304. borgmatic_source_directory = os.path.join(temporary_directory, '.borgmatic')
  305. original_working_directory = os.getcwd()
  306. try:
  307. config_path = os.path.join(temporary_directory, 'test.yaml')
  308. write_configuration(
  309. temporary_directory, config_path, repository_path, borgmatic_source_directory
  310. )
  311. subprocess.check_call(
  312. ['borgmatic', '-v', '2', '--config', config_path, 'rcreate', '--encryption', 'repokey']
  313. )
  314. # Run borgmatic with a config override such that the database dump fails.
  315. with pytest.raises(subprocess.CalledProcessError):
  316. subprocess.check_call(
  317. [
  318. 'borgmatic',
  319. 'create',
  320. '--config',
  321. config_path,
  322. '-v',
  323. '2',
  324. '--override',
  325. "hooks.postgresql_databases=[{'name': 'nope'}]", # noqa: FS003
  326. ]
  327. )
  328. finally:
  329. os.chdir(original_working_directory)
  330. shutil.rmtree(temporary_directory)