test_mongodb.py 28 KB

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