test_sqlite.py 14 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435
  1. import logging
  2. from flexmock import flexmock
  3. from borgmatic.hooks.data_source import sqlite as module
  4. def test_use_streaming_true_for_any_databases():
  5. assert module.use_streaming(
  6. databases=[flexmock(), flexmock()],
  7. config=flexmock(),
  8. )
  9. def test_use_streaming_false_for_no_databases():
  10. assert not module.use_streaming(databases=[], config=flexmock())
  11. def test_dump_data_sources_logs_and_skips_if_dump_already_exists():
  12. databases = [{'path': '/path/to/database', 'name': 'database'}]
  13. flexmock(module).should_receive('make_dump_path').and_return('/run/borgmatic')
  14. flexmock(module.dump).should_receive('make_data_source_dump_filename').and_return(
  15. '/run/borgmatic/database'
  16. )
  17. flexmock(module.os.path).should_receive('exists').and_return(True)
  18. flexmock(module.dump).should_receive('create_named_pipe_for_dump').never()
  19. flexmock(module).should_receive('execute_command').never()
  20. assert (
  21. module.dump_data_sources(
  22. databases,
  23. {},
  24. config_paths=('test.yaml',),
  25. borgmatic_runtime_directory='/run/borgmatic',
  26. patterns=[],
  27. dry_run=False,
  28. )
  29. == []
  30. )
  31. def test_dump_data_sources_dumps_each_database():
  32. databases = [
  33. {'path': '/path/to/database1', 'name': 'database1'},
  34. {'path': '/path/to/database2', 'name': 'database2'},
  35. ]
  36. processes = [flexmock(), flexmock()]
  37. flexmock(module).should_receive('make_dump_path').and_return('/run/borgmatic')
  38. flexmock(module.dump).should_receive('make_data_source_dump_filename').and_return(
  39. '/run/borgmatic/database'
  40. )
  41. flexmock(module.os.path).should_receive('exists').and_return(False)
  42. flexmock(module.dump).should_receive('create_named_pipe_for_dump')
  43. flexmock(module).should_receive('execute_command').and_return(processes[0]).and_return(
  44. processes[1]
  45. )
  46. assert (
  47. module.dump_data_sources(
  48. databases,
  49. {},
  50. config_paths=('test.yaml',),
  51. borgmatic_runtime_directory='/run/borgmatic',
  52. patterns=[],
  53. dry_run=False,
  54. )
  55. == processes
  56. )
  57. def test_dump_data_sources_with_path_injection_attack_gets_escaped():
  58. databases = [
  59. {'path': '/path/to/database1; naughty-command', 'name': 'database1'},
  60. ]
  61. processes = [flexmock()]
  62. flexmock(module).should_receive('make_dump_path').and_return('/run/borgmatic')
  63. flexmock(module.dump).should_receive('make_data_source_dump_filename').and_return(
  64. '/run/borgmatic/database'
  65. )
  66. flexmock(module.os.path).should_receive('exists').and_return(False)
  67. flexmock(module.dump).should_receive('create_named_pipe_for_dump')
  68. flexmock(module).should_receive('execute_command').with_args(
  69. (
  70. 'sqlite3',
  71. "'/path/to/database1; naughty-command'",
  72. '.dump',
  73. '>',
  74. '/run/borgmatic/database',
  75. ),
  76. shell=True,
  77. run_to_completion=False,
  78. ).and_return(processes[0])
  79. assert (
  80. module.dump_data_sources(
  81. databases,
  82. {},
  83. config_paths=('test.yaml',),
  84. borgmatic_runtime_directory='/run/borgmatic',
  85. patterns=[],
  86. dry_run=False,
  87. )
  88. == processes
  89. )
  90. def test_dump_data_sources_runs_non_default_sqlite_with_path_injection_attack_gets_escaped():
  91. databases = [
  92. {
  93. 'path': '/path/to/database1; naughty-command',
  94. 'name': 'database1',
  95. 'sqlite_command': 'custom_sqlite *',
  96. },
  97. ]
  98. processes = [flexmock()]
  99. flexmock(module).should_receive('make_dump_path').and_return('/run/borgmatic')
  100. flexmock(module.dump).should_receive('make_data_source_dump_filename').and_return(
  101. '/run/borgmatic/database'
  102. )
  103. flexmock(module.os.path).should_receive('exists').and_return(False)
  104. flexmock(module.dump).should_receive('create_named_pipe_for_dump')
  105. flexmock(module).should_receive('execute_command').with_args(
  106. (
  107. 'custom_sqlite', # custom sqlite command
  108. "'*'", # Should get shell escaped to prevent injection attacks.
  109. "'/path/to/database1; naughty-command'",
  110. '.dump',
  111. '>',
  112. '/run/borgmatic/database',
  113. ),
  114. shell=True,
  115. run_to_completion=False,
  116. ).and_return(processes[0])
  117. assert (
  118. module.dump_data_sources(
  119. databases,
  120. {},
  121. config_paths=('test.yaml',),
  122. borgmatic_runtime_directory='/run/borgmatic',
  123. patterns=[],
  124. dry_run=False,
  125. )
  126. == processes
  127. )
  128. def test_dump_data_sources_with_non_existent_path_warns_and_dumps_database():
  129. databases = [
  130. {'path': '/path/to/database1', 'name': 'database1'},
  131. ]
  132. processes = [flexmock()]
  133. flexmock(module).should_receive('make_dump_path').and_return('/run/borgmatic')
  134. flexmock(module.logger).should_receive('warning').once()
  135. flexmock(module.dump).should_receive('make_data_source_dump_filename').and_return(
  136. '/run/borgmatic'
  137. )
  138. flexmock(module.os.path).should_receive('exists').and_return(False)
  139. flexmock(module.dump).should_receive('create_named_pipe_for_dump')
  140. flexmock(module).should_receive('execute_command').and_return(processes[0])
  141. assert (
  142. module.dump_data_sources(
  143. databases,
  144. {},
  145. config_paths=('test.yaml',),
  146. borgmatic_runtime_directory='/run/borgmatic',
  147. patterns=[],
  148. dry_run=False,
  149. )
  150. == processes
  151. )
  152. def test_dump_data_sources_with_name_all_warns_and_dumps_all_databases():
  153. databases = [
  154. {'path': '/path/to/database1', 'name': 'all'},
  155. ]
  156. processes = [flexmock()]
  157. flexmock(module).should_receive('make_dump_path').and_return('/run/borgmatic')
  158. flexmock(module.logger).should_receive(
  159. 'warning'
  160. ).twice() # once for the name=all, once for the non-existent path
  161. flexmock(module.dump).should_receive('make_data_source_dump_filename').and_return(
  162. '/run/borgmatic/database'
  163. )
  164. flexmock(module.os.path).should_receive('exists').and_return(False)
  165. flexmock(module.dump).should_receive('create_named_pipe_for_dump')
  166. flexmock(module).should_receive('execute_command').and_return(processes[0])
  167. assert (
  168. module.dump_data_sources(
  169. databases,
  170. {},
  171. config_paths=('test.yaml',),
  172. borgmatic_runtime_directory='/run/borgmatic',
  173. patterns=[],
  174. dry_run=False,
  175. )
  176. == processes
  177. )
  178. def test_dump_data_sources_does_not_dump_if_dry_run():
  179. databases = [{'path': '/path/to/database', 'name': 'database'}]
  180. flexmock(module).should_receive('make_dump_path').and_return('/run/borgmatic')
  181. flexmock(module.dump).should_receive('make_data_source_dump_filename').and_return(
  182. '/run/borgmatic'
  183. )
  184. flexmock(module.os.path).should_receive('exists').and_return(False)
  185. flexmock(module.dump).should_receive('create_named_pipe_for_dump').never()
  186. flexmock(module).should_receive('execute_command').never()
  187. assert (
  188. module.dump_data_sources(
  189. databases,
  190. {},
  191. config_paths=('test.yaml',),
  192. borgmatic_runtime_directory='/run/borgmatic',
  193. patterns=[],
  194. dry_run=True,
  195. )
  196. == []
  197. )
  198. def test_restore_data_source_dump_restores_database():
  199. hook_config = [{'path': '/path/to/database', 'name': 'database'}, {'name': 'other'}]
  200. extract_process = flexmock(stdout=flexmock())
  201. flexmock(module).should_receive('execute_command_with_processes').with_args(
  202. (
  203. 'sqlite3',
  204. '/path/to/database',
  205. ),
  206. processes=[extract_process],
  207. output_log_level=logging.DEBUG,
  208. input_file=extract_process.stdout,
  209. ).once()
  210. flexmock(module.os).should_receive('remove').once()
  211. module.restore_data_source_dump(
  212. hook_config,
  213. {},
  214. data_source=hook_config[0],
  215. dry_run=False,
  216. extract_process=extract_process,
  217. connection_params={'restore_path': None},
  218. borgmatic_runtime_directory='/run/borgmatic',
  219. )
  220. def test_restore_data_source_dump_runs_non_default_sqlite_restores_database():
  221. hook_config = [
  222. {
  223. 'path': '/path/to/database',
  224. 'name': 'database',
  225. 'sqlite_restore_command': 'custom_sqlite *',
  226. },
  227. {'name': 'other'},
  228. ]
  229. extract_process = flexmock(stdout=flexmock())
  230. flexmock(module).should_receive('execute_command_with_processes').with_args(
  231. (
  232. 'custom_sqlite',
  233. "'*'", # Should get shell escaped to prevent injection attacks.
  234. '/path/to/database',
  235. ),
  236. processes=[extract_process],
  237. output_log_level=logging.DEBUG,
  238. input_file=extract_process.stdout,
  239. ).once()
  240. flexmock(module.os).should_receive('remove').once()
  241. module.restore_data_source_dump(
  242. hook_config,
  243. {},
  244. data_source=hook_config[0],
  245. dry_run=False,
  246. extract_process=extract_process,
  247. connection_params={'restore_path': None},
  248. borgmatic_runtime_directory='/run/borgmatic',
  249. )
  250. def test_restore_data_source_dump_with_connection_params_uses_connection_params_for_restore():
  251. hook_config = [
  252. {'path': '/path/to/database', 'name': 'database', 'restore_path': 'config/path/to/database'}
  253. ]
  254. extract_process = flexmock(stdout=flexmock())
  255. flexmock(module).should_receive('execute_command_with_processes').with_args(
  256. (
  257. 'sqlite3',
  258. 'cli/path/to/database',
  259. ),
  260. processes=[extract_process],
  261. output_log_level=logging.DEBUG,
  262. input_file=extract_process.stdout,
  263. ).once()
  264. flexmock(module.os).should_receive('remove').once()
  265. module.restore_data_source_dump(
  266. hook_config,
  267. {},
  268. data_source={'name': 'database'},
  269. dry_run=False,
  270. extract_process=extract_process,
  271. connection_params={'restore_path': 'cli/path/to/database'},
  272. borgmatic_runtime_directory='/run/borgmatic',
  273. )
  274. def test_restore_data_source_dump_runs_non_default_sqlite_with_connection_params_uses_connection_params_for_restore():
  275. hook_config = [
  276. {'path': '/path/to/database', 'name': 'database', 'restore_path': 'config/path/to/database'}
  277. ]
  278. extract_process = flexmock(stdout=flexmock())
  279. flexmock(module).should_receive('execute_command_with_processes').with_args(
  280. (
  281. 'custom_sqlite',
  282. 'cli/path/to/database',
  283. ),
  284. processes=[extract_process],
  285. output_log_level=logging.DEBUG,
  286. input_file=extract_process.stdout,
  287. ).once()
  288. flexmock(module.os).should_receive('remove').once()
  289. module.restore_data_source_dump(
  290. hook_config,
  291. {},
  292. data_source={
  293. 'name': 'database',
  294. 'sqlite_restore_command': 'custom_sqlite',
  295. },
  296. dry_run=False,
  297. extract_process=extract_process,
  298. connection_params={'restore_path': 'cli/path/to/database'},
  299. borgmatic_runtime_directory='/run/borgmatic',
  300. )
  301. def test_restore_data_source_dump_without_connection_params_uses_restore_params_in_config_for_restore():
  302. hook_config = [
  303. {'path': '/path/to/database', 'name': 'database', 'restore_path': 'config/path/to/database'}
  304. ]
  305. extract_process = flexmock(stdout=flexmock())
  306. flexmock(module).should_receive('execute_command_with_processes').with_args(
  307. (
  308. 'sqlite3',
  309. 'config/path/to/database',
  310. ),
  311. processes=[extract_process],
  312. output_log_level=logging.DEBUG,
  313. input_file=extract_process.stdout,
  314. ).once()
  315. flexmock(module.os).should_receive('remove').once()
  316. module.restore_data_source_dump(
  317. hook_config,
  318. {},
  319. data_source=hook_config[0],
  320. dry_run=False,
  321. extract_process=extract_process,
  322. connection_params={'restore_path': None},
  323. borgmatic_runtime_directory='/run/borgmatic',
  324. )
  325. def test_restore_data_source_dump_runs_non_default_sqlite_without_connection_params_uses_restore_params_in_config_for_restore():
  326. hook_config = [
  327. {
  328. 'path': '/path/to/database',
  329. 'name': 'database',
  330. 'sqlite_restore_command': 'custom_sqlite',
  331. 'restore_path': 'config/path/to/database',
  332. }
  333. ]
  334. extract_process = flexmock(stdout=flexmock())
  335. flexmock(module).should_receive('execute_command_with_processes').with_args(
  336. (
  337. 'custom_sqlite',
  338. 'config/path/to/database',
  339. ),
  340. processes=[extract_process],
  341. output_log_level=logging.DEBUG,
  342. input_file=extract_process.stdout,
  343. ).once()
  344. flexmock(module.os).should_receive('remove').once()
  345. module.restore_data_source_dump(
  346. hook_config,
  347. {},
  348. data_source=hook_config[0],
  349. dry_run=False,
  350. extract_process=extract_process,
  351. connection_params={'restore_path': None},
  352. borgmatic_runtime_directory='/run/borgmatic',
  353. )
  354. def test_restore_data_source_dump_does_not_restore_database_if_dry_run():
  355. hook_config = [{'path': '/path/to/database', 'name': 'database'}]
  356. extract_process = flexmock(stdout=flexmock())
  357. flexmock(module).should_receive('execute_command_with_processes').never()
  358. flexmock(module.os).should_receive('remove').never()
  359. module.restore_data_source_dump(
  360. hook_config,
  361. {},
  362. data_source={'name': 'database'},
  363. dry_run=True,
  364. extract_process=extract_process,
  365. connection_params={'restore_path': None},
  366. borgmatic_runtime_directory='/run/borgmatic',
  367. )