test_mongodb.py 30 KB

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