test_database.py 12 KB

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