2
0

test_mongodb.py 28 KB

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