test_sqlite.py 17 KB

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