test_sqlite.py 14 KB

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