test_sqlite.py 18 KB

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