test_mongodb.py 30 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757758759760761762763764765766767768769770771772773774775776777778779780781782783784785786787788789790791792793794795796797798799800801802803804805806807808809810811812813814815816817818819820821822823824825826827828829830831832833834835836837838839840841842843844845846847848849850851852853854855856857858859860861862863864865866867868869870871872873874875876877878879880881882883884885886887888889890891892893894895896897898899900901902903904905906907908909910911912913914915916917918919920921922923924925926927928929930931932933
  1. import logging
  2. from flexmock import flexmock
  3. from borgmatic.hooks.data_source import mongodb as module
  4. def test_use_streaming_true_for_any_non_directory_format_databases():
  5. assert module.use_streaming(
  6. databases=[{'format': 'stuff'}, {'format': 'directory'}, {}],
  7. config=flexmock(),
  8. )
  9. def test_use_streaming_false_for_all_directory_format_databases():
  10. assert not module.use_streaming(
  11. databases=[{'format': 'directory'}, {'format': 'directory'}],
  12. config=flexmock(),
  13. )
  14. def test_use_streaming_false_for_no_databases():
  15. assert not module.use_streaming(databases=[], config=flexmock())
  16. def test_dump_data_sources_runs_mongodump_for_each_database():
  17. databases = [{'name': 'foo'}, {'name': 'bar'}]
  18. processes = [flexmock(), flexmock()]
  19. flexmock(module).should_receive('make_dump_path').and_return('')
  20. flexmock(module.dump).should_receive('make_data_source_dump_filename').and_return(
  21. 'databases/localhost/foo',
  22. ).and_return('databases/localhost/bar')
  23. flexmock(module.dump).should_receive('create_named_pipe_for_dump')
  24. for name, process in zip(('foo', 'bar'), processes):
  25. flexmock(module).should_receive('execute_command').with_args(
  26. ('mongodump', '--db', name, '--archive', '>', f'databases/localhost/{name}'),
  27. shell=True,
  28. run_to_completion=False,
  29. working_directory=None,
  30. ).and_return(process).once()
  31. flexmock(module.dump).should_receive('write_data_source_dumps_metadata').with_args(
  32. '/run/borgmatic',
  33. 'mongodb_databases',
  34. [
  35. module.borgmatic.actions.restore.Dump('mongodb_databases', 'foo'),
  36. module.borgmatic.actions.restore.Dump('mongodb_databases', 'bar'),
  37. ],
  38. ).once()
  39. flexmock(module.borgmatic.hooks.data_source.config).should_receive('inject_pattern').with_args(
  40. object,
  41. module.borgmatic.borg.pattern.Pattern(
  42. '/run/borgmatic/mongodb_databases',
  43. source=module.borgmatic.borg.pattern.Pattern_source.HOOK,
  44. ),
  45. ).once()
  46. assert (
  47. module.dump_data_sources(
  48. databases,
  49. {},
  50. config_paths=('test.yaml',),
  51. borgmatic_runtime_directory='/run/borgmatic',
  52. patterns=[],
  53. dry_run=False,
  54. )
  55. == processes
  56. )
  57. def test_dump_data_sources_with_dry_run_skips_mongodump():
  58. databases = [{'name': 'foo'}, {'name': 'bar'}]
  59. flexmock(module).should_receive('make_dump_path').and_return('')
  60. flexmock(module.dump).should_receive('make_data_source_dump_filename').and_return(
  61. 'databases/localhost/foo',
  62. ).and_return('databases/localhost/bar')
  63. flexmock(module.dump).should_receive('create_named_pipe_for_dump').never()
  64. flexmock(module).should_receive('execute_command').never()
  65. flexmock(module.dump).should_receive('write_data_source_dumps_metadata').never()
  66. flexmock(module.borgmatic.hooks.data_source.config).should_receive('inject_pattern').never()
  67. assert (
  68. module.dump_data_sources(
  69. databases,
  70. {},
  71. config_paths=('test.yaml',),
  72. borgmatic_runtime_directory='/run/borgmatic',
  73. patterns=[],
  74. dry_run=True,
  75. )
  76. == []
  77. )
  78. def test_dump_data_sources_runs_mongodump_with_hostname_and_port():
  79. databases = [{'name': 'foo', 'hostname': 'database.example.org', 'port': 27018}]
  80. process = flexmock()
  81. flexmock(module).should_receive('make_dump_path').and_return('')
  82. flexmock(module.dump).should_receive('make_data_source_dump_filename').and_return(
  83. 'databases/database.example.org/foo',
  84. )
  85. flexmock(module.dump).should_receive('create_named_pipe_for_dump')
  86. flexmock(module).should_receive('execute_command').with_args(
  87. (
  88. 'mongodump',
  89. '--host',
  90. 'database.example.org',
  91. '--port',
  92. '27018',
  93. '--db',
  94. 'foo',
  95. '--archive',
  96. '>',
  97. 'databases/database.example.org/foo',
  98. ),
  99. shell=True,
  100. run_to_completion=False,
  101. working_directory=None,
  102. ).and_return(process).once()
  103. flexmock(module.dump).should_receive('write_data_source_dumps_metadata').with_args(
  104. '/run/borgmatic',
  105. 'mongodb_databases',
  106. [
  107. module.borgmatic.actions.restore.Dump(
  108. 'mongodb_databases', 'foo', 'database.example.org', 27018
  109. ),
  110. ],
  111. ).once()
  112. flexmock(module.borgmatic.hooks.data_source.config).should_receive('inject_pattern').with_args(
  113. object,
  114. module.borgmatic.borg.pattern.Pattern(
  115. '/run/borgmatic/mongodb_databases',
  116. source=module.borgmatic.borg.pattern.Pattern_source.HOOK,
  117. ),
  118. ).once()
  119. assert module.dump_data_sources(
  120. databases,
  121. {},
  122. config_paths=('test.yaml',),
  123. borgmatic_runtime_directory='/run/borgmatic',
  124. patterns=[],
  125. dry_run=False,
  126. ) == [process]
  127. def test_dump_data_sources_runs_mongodump_with_username_and_password():
  128. databases = [
  129. {
  130. 'name': 'foo',
  131. 'username': 'mongo',
  132. 'password': 'trustsome1',
  133. 'authentication_database': 'admin',
  134. },
  135. ]
  136. process = flexmock()
  137. flexmock(module).should_receive('make_dump_path').and_return('')
  138. flexmock(module.dump).should_receive('make_data_source_dump_filename').and_return(
  139. 'databases/localhost/foo',
  140. )
  141. flexmock(module.borgmatic.hooks.credential.parse).should_receive(
  142. 'resolve_credential',
  143. ).replace_with(lambda value, config: value)
  144. flexmock(module).should_receive('make_password_config_file').with_args('trustsome1').and_return(
  145. '/dev/fd/99',
  146. )
  147. flexmock(module.dump).should_receive('create_named_pipe_for_dump')
  148. flexmock(module).should_receive('execute_command').with_args(
  149. (
  150. 'mongodump',
  151. '--username',
  152. 'mongo',
  153. '--config',
  154. '/dev/fd/99',
  155. '--authenticationDatabase',
  156. 'admin',
  157. '--db',
  158. 'foo',
  159. '--archive',
  160. '>',
  161. 'databases/localhost/foo',
  162. ),
  163. shell=True,
  164. run_to_completion=False,
  165. working_directory=None,
  166. ).and_return(process).once()
  167. flexmock(module.dump).should_receive('write_data_source_dumps_metadata').with_args(
  168. '/run/borgmatic',
  169. 'mongodb_databases',
  170. [
  171. module.borgmatic.actions.restore.Dump('mongodb_databases', 'foo'),
  172. ],
  173. ).once()
  174. flexmock(module.borgmatic.hooks.data_source.config).should_receive('inject_pattern').with_args(
  175. object,
  176. module.borgmatic.borg.pattern.Pattern(
  177. '/run/borgmatic/mongodb_databases',
  178. source=module.borgmatic.borg.pattern.Pattern_source.HOOK,
  179. ),
  180. ).once()
  181. assert module.dump_data_sources(
  182. databases,
  183. {},
  184. config_paths=('test.yaml',),
  185. borgmatic_runtime_directory='/run/borgmatic',
  186. patterns=[],
  187. dry_run=False,
  188. ) == [process]
  189. def test_dump_data_sources_runs_mongodump_with_directory_format():
  190. databases = [{'name': 'foo', 'format': 'directory'}]
  191. flexmock(module).should_receive('make_dump_path').and_return('')
  192. flexmock(module.dump).should_receive('make_data_source_dump_filename').and_return(
  193. 'databases/localhost/foo',
  194. )
  195. flexmock(module.dump).should_receive('create_parent_directory_for_dump')
  196. flexmock(module.dump).should_receive('create_named_pipe_for_dump').never()
  197. flexmock(module).should_receive('execute_command').with_args(
  198. ('mongodump', '--out', 'databases/localhost/foo', '--db', 'foo'),
  199. shell=True,
  200. working_directory=None,
  201. ).and_return(flexmock()).once()
  202. flexmock(module.dump).should_receive('write_data_source_dumps_metadata').with_args(
  203. '/run/borgmatic',
  204. 'mongodb_databases',
  205. [
  206. module.borgmatic.actions.restore.Dump('mongodb_databases', 'foo'),
  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/mongodb_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. == []
  226. )
  227. def test_dump_data_sources_runs_mongodump_with_options():
  228. databases = [{'name': 'foo', 'options': '--stuff=such'}]
  229. process = flexmock()
  230. flexmock(module).should_receive('make_dump_path').and_return('')
  231. flexmock(module.dump).should_receive('make_data_source_dump_filename').and_return(
  232. 'databases/localhost/foo',
  233. )
  234. flexmock(module.dump).should_receive('create_named_pipe_for_dump')
  235. flexmock(module).should_receive('execute_command').with_args(
  236. (
  237. 'mongodump',
  238. '--db',
  239. 'foo',
  240. '--stuff=such',
  241. '--archive',
  242. '>',
  243. 'databases/localhost/foo',
  244. ),
  245. shell=True,
  246. run_to_completion=False,
  247. working_directory=None,
  248. ).and_return(process).once()
  249. flexmock(module.dump).should_receive('write_data_source_dumps_metadata').with_args(
  250. '/run/borgmatic',
  251. 'mongodb_databases',
  252. [
  253. module.borgmatic.actions.restore.Dump('mongodb_databases', 'foo'),
  254. ],
  255. ).once()
  256. flexmock(module.borgmatic.hooks.data_source.config).should_receive('inject_pattern').with_args(
  257. object,
  258. module.borgmatic.borg.pattern.Pattern(
  259. '/run/borgmatic/mongodb_databases',
  260. source=module.borgmatic.borg.pattern.Pattern_source.HOOK,
  261. ),
  262. ).once()
  263. assert module.dump_data_sources(
  264. databases,
  265. {},
  266. config_paths=('test.yaml',),
  267. borgmatic_runtime_directory='/run/borgmatic',
  268. patterns=[],
  269. dry_run=False,
  270. ) == [process]
  271. def test_dump_data_sources_runs_mongodumpall_for_all_databases():
  272. databases = [{'name': 'all'}]
  273. process = flexmock()
  274. flexmock(module).should_receive('make_dump_path').and_return('')
  275. flexmock(module.dump).should_receive('make_data_source_dump_filename').and_return(
  276. 'databases/localhost/all',
  277. )
  278. flexmock(module.dump).should_receive('create_named_pipe_for_dump')
  279. flexmock(module).should_receive('execute_command').with_args(
  280. ('mongodump', '--archive', '>', 'databases/localhost/all'),
  281. shell=True,
  282. run_to_completion=False,
  283. working_directory=None,
  284. ).and_return(process).once()
  285. flexmock(module.dump).should_receive('write_data_source_dumps_metadata').with_args(
  286. '/run/borgmatic',
  287. 'mongodb_databases',
  288. [
  289. module.borgmatic.actions.restore.Dump('mongodb_databases', 'all'),
  290. ],
  291. ).once()
  292. flexmock(module.borgmatic.hooks.data_source.config).should_receive('inject_pattern').with_args(
  293. object,
  294. module.borgmatic.borg.pattern.Pattern(
  295. '/run/borgmatic/mongodb_databases',
  296. source=module.borgmatic.borg.pattern.Pattern_source.HOOK,
  297. ),
  298. ).once()
  299. assert module.dump_data_sources(
  300. databases,
  301. {},
  302. config_paths=('test.yaml',),
  303. borgmatic_runtime_directory='/run/borgmatic',
  304. patterns=[],
  305. dry_run=False,
  306. ) == [process]
  307. def test_make_password_config_file_writes_password_to_pipe():
  308. read_file_descriptor = 99
  309. write_file_descriptor = flexmock()
  310. flexmock(module.os).should_receive('pipe').and_return(
  311. (read_file_descriptor, write_file_descriptor),
  312. )
  313. flexmock(module.os).should_receive('write').with_args(
  314. write_file_descriptor,
  315. b'password: trustsome1',
  316. ).once()
  317. flexmock(module.os).should_receive('close')
  318. flexmock(module.os).should_receive('set_inheritable')
  319. assert module.make_password_config_file('trustsome1') == '/dev/fd/99'
  320. def test_build_dump_command_with_username_injection_attack_gets_escaped():
  321. database = {'name': 'test', 'username': 'bob; naughty-command'}
  322. flexmock(module.borgmatic.hooks.credential.parse).should_receive(
  323. 'resolve_credential',
  324. ).replace_with(lambda value, config: value)
  325. command = module.build_dump_command(database, {}, dump_filename='test', dump_format='archive')
  326. assert "'bob; naughty-command'" in command
  327. def test_restore_data_source_dump_runs_mongorestore():
  328. hook_config = [{'name': 'foo', 'schemas': None}, {'name': 'bar'}]
  329. extract_process = flexmock(stdout=flexmock())
  330. flexmock(module).should_receive('make_dump_path')
  331. flexmock(module.dump).should_receive('make_data_source_dump_filename')
  332. flexmock(module.borgmatic.hooks.credential.parse).should_receive(
  333. 'resolve_credential',
  334. ).replace_with(lambda value, config: value)
  335. flexmock(module).should_receive('execute_command_with_processes').with_args(
  336. ['mongorestore', '--archive', '--drop'],
  337. processes=[extract_process],
  338. output_log_level=logging.DEBUG,
  339. input_file=extract_process.stdout,
  340. working_directory=None,
  341. ).once()
  342. module.restore_data_source_dump(
  343. hook_config,
  344. {},
  345. data_source={'name': 'foo'},
  346. dry_run=False,
  347. extract_process=extract_process,
  348. connection_params={
  349. 'hostname': None,
  350. 'port': None,
  351. 'username': None,
  352. 'password': None,
  353. },
  354. borgmatic_runtime_directory='/run/borgmatic',
  355. )
  356. def test_restore_data_source_dump_runs_mongorestore_with_hostname_and_port():
  357. hook_config = [
  358. {'name': 'foo', 'hostname': 'database.example.org', 'port': 27018, 'schemas': None},
  359. ]
  360. extract_process = flexmock(stdout=flexmock())
  361. flexmock(module).should_receive('make_dump_path')
  362. flexmock(module.dump).should_receive('make_data_source_dump_filename')
  363. flexmock(module.borgmatic.hooks.credential.parse).should_receive(
  364. 'resolve_credential',
  365. ).replace_with(lambda value, config: value)
  366. flexmock(module).should_receive('execute_command_with_processes').with_args(
  367. [
  368. 'mongorestore',
  369. '--archive',
  370. '--drop',
  371. '--host',
  372. 'database.example.org',
  373. '--port',
  374. '27018',
  375. ],
  376. processes=[extract_process],
  377. output_log_level=logging.DEBUG,
  378. input_file=extract_process.stdout,
  379. working_directory=None,
  380. ).once()
  381. module.restore_data_source_dump(
  382. hook_config,
  383. {},
  384. data_source=hook_config[0],
  385. dry_run=False,
  386. extract_process=extract_process,
  387. connection_params={
  388. 'hostname': None,
  389. 'port': None,
  390. 'username': None,
  391. 'password': None,
  392. },
  393. borgmatic_runtime_directory='/run/borgmatic',
  394. )
  395. def test_restore_data_source_dump_runs_mongorestore_with_username_and_password():
  396. hook_config = [
  397. {
  398. 'name': 'foo',
  399. 'username': 'mongo',
  400. 'password': 'trustsome1',
  401. 'authentication_database': 'admin',
  402. 'schemas': None,
  403. },
  404. ]
  405. extract_process = flexmock(stdout=flexmock())
  406. flexmock(module).should_receive('make_dump_path')
  407. flexmock(module.dump).should_receive('make_data_source_dump_filename')
  408. flexmock(module.borgmatic.hooks.credential.parse).should_receive(
  409. 'resolve_credential',
  410. ).replace_with(lambda value, config: value)
  411. flexmock(module).should_receive('make_password_config_file').with_args('trustsome1').and_return(
  412. '/dev/fd/99',
  413. )
  414. flexmock(module).should_receive('execute_command_with_processes').with_args(
  415. [
  416. 'mongorestore',
  417. '--archive',
  418. '--drop',
  419. '--username',
  420. 'mongo',
  421. '--config',
  422. '/dev/fd/99',
  423. '--authenticationDatabase',
  424. 'admin',
  425. ],
  426. processes=[extract_process],
  427. output_log_level=logging.DEBUG,
  428. input_file=extract_process.stdout,
  429. working_directory=None,
  430. ).once()
  431. module.restore_data_source_dump(
  432. hook_config,
  433. {},
  434. data_source=hook_config[0],
  435. dry_run=False,
  436. extract_process=extract_process,
  437. connection_params={
  438. 'hostname': None,
  439. 'port': None,
  440. 'username': None,
  441. 'password': None,
  442. },
  443. borgmatic_runtime_directory='/run/borgmatic',
  444. )
  445. def test_restore_data_source_dump_with_connection_params_uses_connection_params_for_restore():
  446. hook_config = [
  447. {
  448. 'name': 'foo',
  449. 'username': 'mongo',
  450. 'password': 'trustsome1',
  451. 'authentication_database': 'admin',
  452. 'restore_hostname': 'restorehost',
  453. 'restore_port': 'restoreport',
  454. 'restore_username': 'restoreusername',
  455. 'restore_password': 'restorepassword',
  456. 'schemas': None,
  457. },
  458. ]
  459. extract_process = flexmock(stdout=flexmock())
  460. flexmock(module).should_receive('make_dump_path')
  461. flexmock(module.dump).should_receive('make_data_source_dump_filename')
  462. flexmock(module.borgmatic.hooks.credential.parse).should_receive(
  463. 'resolve_credential',
  464. ).replace_with(lambda value, config: value)
  465. flexmock(module).should_receive('make_password_config_file').with_args(
  466. 'clipassword',
  467. ).and_return('/dev/fd/99')
  468. flexmock(module).should_receive('execute_command_with_processes').with_args(
  469. [
  470. 'mongorestore',
  471. '--archive',
  472. '--drop',
  473. '--host',
  474. 'clihost',
  475. '--port',
  476. 'cliport',
  477. '--username',
  478. 'cliusername',
  479. '--config',
  480. '/dev/fd/99',
  481. '--authenticationDatabase',
  482. 'admin',
  483. ],
  484. processes=[extract_process],
  485. output_log_level=logging.DEBUG,
  486. input_file=extract_process.stdout,
  487. working_directory=None,
  488. ).once()
  489. module.restore_data_source_dump(
  490. hook_config,
  491. {},
  492. data_source=hook_config[0],
  493. dry_run=False,
  494. extract_process=extract_process,
  495. connection_params={
  496. 'hostname': 'clihost',
  497. 'port': 'cliport',
  498. 'username': 'cliusername',
  499. 'password': 'clipassword',
  500. },
  501. borgmatic_runtime_directory='/run/borgmatic',
  502. )
  503. def test_restore_data_source_dump_without_connection_params_uses_restore_params_in_config_for_restore():
  504. hook_config = [
  505. {
  506. 'name': 'foo',
  507. 'username': 'mongo',
  508. 'password': 'trustsome1',
  509. 'authentication_database': 'admin',
  510. 'schemas': None,
  511. 'restore_hostname': 'restorehost',
  512. 'restore_port': 'restoreport',
  513. 'restore_username': 'restoreuser',
  514. 'restore_password': 'restorepass',
  515. },
  516. ]
  517. extract_process = flexmock(stdout=flexmock())
  518. flexmock(module).should_receive('make_dump_path')
  519. flexmock(module.dump).should_receive('make_data_source_dump_filename')
  520. flexmock(module.borgmatic.hooks.credential.parse).should_receive(
  521. 'resolve_credential',
  522. ).replace_with(lambda value, config: value)
  523. flexmock(module).should_receive('make_password_config_file').with_args(
  524. 'restorepass',
  525. ).and_return('/dev/fd/99')
  526. flexmock(module).should_receive('execute_command_with_processes').with_args(
  527. [
  528. 'mongorestore',
  529. '--archive',
  530. '--drop',
  531. '--host',
  532. 'restorehost',
  533. '--port',
  534. 'restoreport',
  535. '--username',
  536. 'restoreuser',
  537. '--config',
  538. '/dev/fd/99',
  539. '--authenticationDatabase',
  540. 'admin',
  541. ],
  542. processes=[extract_process],
  543. output_log_level=logging.DEBUG,
  544. input_file=extract_process.stdout,
  545. working_directory=None,
  546. ).once()
  547. module.restore_data_source_dump(
  548. hook_config,
  549. {},
  550. data_source=hook_config[0],
  551. dry_run=False,
  552. extract_process=extract_process,
  553. connection_params={
  554. 'hostname': None,
  555. 'port': None,
  556. 'username': None,
  557. 'password': None,
  558. },
  559. borgmatic_runtime_directory='/run/borgmatic',
  560. )
  561. def test_restore_data_source_dump_runs_mongorestore_with_options():
  562. hook_config = [{'name': 'foo', 'restore_options': '--harder', 'schemas': None}]
  563. extract_process = flexmock(stdout=flexmock())
  564. flexmock(module).should_receive('make_dump_path')
  565. flexmock(module.dump).should_receive('make_data_source_dump_filename')
  566. flexmock(module.borgmatic.hooks.credential.parse).should_receive(
  567. 'resolve_credential',
  568. ).replace_with(lambda value, config: value)
  569. flexmock(module).should_receive('execute_command_with_processes').with_args(
  570. ['mongorestore', '--archive', '--drop', '--harder'],
  571. processes=[extract_process],
  572. output_log_level=logging.DEBUG,
  573. input_file=extract_process.stdout,
  574. working_directory=None,
  575. ).once()
  576. module.restore_data_source_dump(
  577. hook_config,
  578. {},
  579. data_source=hook_config[0],
  580. dry_run=False,
  581. extract_process=extract_process,
  582. connection_params={
  583. 'hostname': None,
  584. 'port': None,
  585. 'username': None,
  586. 'password': None,
  587. },
  588. borgmatic_runtime_directory='/run/borgmatic',
  589. )
  590. def test_restore_databases_dump_runs_mongorestore_with_schemas():
  591. hook_config = [{'name': 'foo', 'schemas': ['bar', 'baz']}]
  592. extract_process = flexmock(stdout=flexmock())
  593. flexmock(module).should_receive('make_dump_path')
  594. flexmock(module.dump).should_receive('make_data_source_dump_filename')
  595. flexmock(module.borgmatic.hooks.credential.parse).should_receive(
  596. 'resolve_credential',
  597. ).replace_with(lambda value, config: value)
  598. flexmock(module).should_receive('execute_command_with_processes').with_args(
  599. [
  600. 'mongorestore',
  601. '--archive',
  602. '--drop',
  603. '--nsInclude',
  604. 'bar',
  605. '--nsInclude',
  606. 'baz',
  607. ],
  608. processes=[extract_process],
  609. output_log_level=logging.DEBUG,
  610. input_file=extract_process.stdout,
  611. working_directory=None,
  612. ).once()
  613. module.restore_data_source_dump(
  614. hook_config,
  615. {},
  616. data_source=hook_config[0],
  617. dry_run=False,
  618. extract_process=extract_process,
  619. connection_params={
  620. 'hostname': None,
  621. 'port': None,
  622. 'username': None,
  623. 'password': None,
  624. },
  625. borgmatic_runtime_directory='/run/borgmatic',
  626. )
  627. def test_restore_data_source_dump_runs_psql_for_all_database_dump():
  628. hook_config = [{'name': 'all', 'schemas': None}]
  629. extract_process = flexmock(stdout=flexmock())
  630. flexmock(module).should_receive('make_dump_path')
  631. flexmock(module.dump).should_receive('make_data_source_dump_filename')
  632. flexmock(module.borgmatic.hooks.credential.parse).should_receive(
  633. 'resolve_credential',
  634. ).replace_with(lambda value, config: value)
  635. flexmock(module).should_receive('execute_command_with_processes').with_args(
  636. ['mongorestore', '--archive'],
  637. processes=[extract_process],
  638. output_log_level=logging.DEBUG,
  639. input_file=extract_process.stdout,
  640. working_directory=None,
  641. ).once()
  642. module.restore_data_source_dump(
  643. hook_config,
  644. {},
  645. data_source=hook_config[0],
  646. dry_run=False,
  647. extract_process=extract_process,
  648. connection_params={
  649. 'hostname': None,
  650. 'port': None,
  651. 'username': None,
  652. 'password': None,
  653. },
  654. borgmatic_runtime_directory='/run/borgmatic',
  655. )
  656. def test_restore_data_source_dump_with_dry_run_skips_restore():
  657. hook_config = [{'name': 'foo', 'schemas': None}]
  658. flexmock(module).should_receive('make_dump_path')
  659. flexmock(module.dump).should_receive('make_data_source_dump_filename')
  660. flexmock(module.borgmatic.hooks.credential.parse).should_receive(
  661. 'resolve_credential',
  662. ).replace_with(lambda value, config: value)
  663. flexmock(module).should_receive('execute_command_with_processes').never()
  664. module.restore_data_source_dump(
  665. hook_config,
  666. {},
  667. data_source={'name': 'foo'},
  668. dry_run=True,
  669. extract_process=flexmock(),
  670. connection_params={
  671. 'hostname': None,
  672. 'port': None,
  673. 'username': None,
  674. 'password': None,
  675. },
  676. borgmatic_runtime_directory='/run/borgmatic',
  677. )
  678. def test_restore_data_source_dump_without_extract_process_restores_from_disk():
  679. hook_config = [{'name': 'foo', 'format': 'directory', 'schemas': None}]
  680. flexmock(module).should_receive('make_dump_path')
  681. flexmock(module.dump).should_receive('make_data_source_dump_filename').and_return('/dump/path')
  682. flexmock(module.borgmatic.hooks.credential.parse).should_receive(
  683. 'resolve_credential',
  684. ).replace_with(lambda value, config: value)
  685. flexmock(module).should_receive('execute_command_with_processes').with_args(
  686. ['mongorestore', '--dir', '/dump/path', '--drop'],
  687. processes=[],
  688. output_log_level=logging.DEBUG,
  689. input_file=None,
  690. working_directory=None,
  691. ).once()
  692. module.restore_data_source_dump(
  693. hook_config,
  694. {},
  695. data_source={'name': 'foo'},
  696. dry_run=False,
  697. extract_process=None,
  698. connection_params={
  699. 'hostname': None,
  700. 'port': None,
  701. 'username': None,
  702. 'password': None,
  703. },
  704. borgmatic_runtime_directory='/run/borgmatic',
  705. )
  706. def test_dump_data_sources_uses_custom_mongodump_command():
  707. flexmock(module.borgmatic.hooks.command).should_receive('Before_after_hooks').and_return(
  708. flexmock(),
  709. )
  710. databases = [{'name': 'foo', 'mongodump_command': 'custom_mongodump'}]
  711. process = flexmock()
  712. flexmock(module).should_receive('make_dump_path').and_return('')
  713. flexmock(module.dump).should_receive('make_data_source_dump_filename').and_return(
  714. 'databases/localhost/foo',
  715. )
  716. flexmock(module.dump).should_receive('create_named_pipe_for_dump')
  717. flexmock(module).should_receive('execute_command').with_args(
  718. (
  719. 'custom_mongodump',
  720. '--db',
  721. 'foo',
  722. '--archive',
  723. '>',
  724. 'databases/localhost/foo',
  725. ),
  726. shell=True,
  727. run_to_completion=False,
  728. working_directory=None,
  729. ).and_return(process).once()
  730. flexmock(module.dump).should_receive('write_data_source_dumps_metadata').with_args(
  731. '/run/borgmatic',
  732. 'mongodb_databases',
  733. [
  734. module.borgmatic.actions.restore.Dump('mongodb_databases', 'foo'),
  735. ],
  736. ).once()
  737. assert module.dump_data_sources(
  738. databases,
  739. {},
  740. config_paths=('test.yaml',),
  741. borgmatic_runtime_directory='/run/borgmatic',
  742. patterns=[],
  743. dry_run=False,
  744. ) == [process]
  745. def test_build_dump_command_prevents_shell_injection():
  746. database = {
  747. 'name': 'testdb; rm -rf /', # Malicious input
  748. 'hostname': 'localhost',
  749. 'port': 27017,
  750. 'username': 'user',
  751. 'password': 'password',
  752. 'mongodump_command': 'mongodump',
  753. 'options': '--gzip',
  754. }
  755. config = {}
  756. dump_filename = '/path/to/dump'
  757. dump_format = 'archive'
  758. command = module.build_dump_command(database, config, dump_filename, dump_format)
  759. # Ensure the malicious input is properly escaped and does not execute
  760. assert 'testdb; rm -rf /' not in command
  761. assert any(
  762. 'testdb' in part for part in command
  763. ) # Check if 'testdb' is in any part of the tuple
  764. def test_restore_data_source_dump_uses_custom_mongorestore_command():
  765. hook_config = [
  766. {
  767. 'name': 'foo',
  768. 'mongorestore_command': 'custom_mongorestore',
  769. 'schemas': None,
  770. 'restore_options': '--gzip',
  771. },
  772. ]
  773. extract_process = flexmock(stdout=flexmock())
  774. flexmock(module).should_receive('make_dump_path')
  775. flexmock(module.dump).should_receive('make_data_source_dump_filename')
  776. flexmock(module.borgmatic.hooks.credential.parse).should_receive(
  777. 'resolve_credential',
  778. ).replace_with(lambda value, config: value)
  779. flexmock(module).should_receive('execute_command_with_processes').with_args(
  780. [
  781. 'custom_mongorestore', # Should use custom command instead of default
  782. '--archive',
  783. '--drop',
  784. '--gzip', # Should include restore options
  785. ],
  786. processes=[extract_process],
  787. output_log_level=logging.DEBUG,
  788. input_file=extract_process.stdout,
  789. working_directory=None,
  790. ).once()
  791. module.restore_data_source_dump(
  792. hook_config,
  793. {},
  794. data_source=hook_config[0],
  795. dry_run=False,
  796. extract_process=extract_process,
  797. connection_params={
  798. 'hostname': None,
  799. 'port': None,
  800. 'username': None,
  801. 'password': None,
  802. },
  803. borgmatic_runtime_directory='/run/borgmatic',
  804. )
  805. def test_build_restore_command_prevents_shell_injection():
  806. database = {
  807. 'name': 'testdb; rm -rf /', # Malicious input
  808. 'restore_hostname': 'localhost',
  809. 'restore_port': 27017,
  810. 'restore_username': 'user',
  811. 'restore_password': 'password',
  812. 'mongorestore_command': 'mongorestore',
  813. 'restore_options': '--gzip',
  814. }
  815. config = {}
  816. dump_filename = '/path/to/dump'
  817. connection_params = {
  818. 'hostname': None,
  819. 'port': None,
  820. 'username': None,
  821. 'password': None,
  822. }
  823. extract_process = None
  824. command = module.build_restore_command(
  825. extract_process,
  826. database,
  827. config,
  828. dump_filename,
  829. connection_params,
  830. )
  831. # Ensure the malicious input is properly escaped and does not execute
  832. assert 'rm -rf /' not in command
  833. assert ';' not in command