test_sqlite.py 18 KB

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