2
0

test_sqlite.py 15 KB

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