test_sqlite.py 17 KB

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