test_mongodb.py 19 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596
  1. import logging
  2. from flexmock import flexmock
  3. from borgmatic.hooks 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. log_prefix=flexmock(),
  9. )
  10. def test_use_streaming_false_for_all_directory_format_databases():
  11. assert not module.use_streaming(
  12. databases=[{'format': 'directory'}, {'format': 'directory'}],
  13. config=flexmock(),
  14. log_prefix=flexmock(),
  15. )
  16. def test_use_streaming_false_for_no_databases():
  17. assert not module.use_streaming(databases=[], config=flexmock(), log_prefix=flexmock())
  18. def test_dump_data_sources_runs_mongodump_for_each_database():
  19. databases = [{'name': 'foo'}, {'name': 'bar'}]
  20. processes = [flexmock(), flexmock()]
  21. flexmock(module).should_receive('make_dump_path').and_return('')
  22. flexmock(module.dump).should_receive('make_data_source_dump_filename').and_return(
  23. 'databases/localhost/foo'
  24. ).and_return('databases/localhost/bar')
  25. flexmock(module.dump).should_receive('create_named_pipe_for_dump')
  26. for name, process in zip(('foo', 'bar'), processes):
  27. flexmock(module).should_receive('execute_command').with_args(
  28. ('mongodump', '--db', name, '--archive', '>', f'databases/localhost/{name}'),
  29. shell=True,
  30. run_to_completion=False,
  31. ).and_return(process).once()
  32. assert (
  33. module.dump_data_sources(
  34. databases, {}, 'test.yaml', borgmatic_runtime_directory='/run/borgmatic', dry_run=False
  35. )
  36. == processes
  37. )
  38. def test_dump_data_sources_with_dry_run_skips_mongodump():
  39. databases = [{'name': 'foo'}, {'name': 'bar'}]
  40. flexmock(module).should_receive('make_dump_path').and_return('')
  41. flexmock(module.dump).should_receive('make_data_source_dump_filename').and_return(
  42. 'databases/localhost/foo'
  43. ).and_return('databases/localhost/bar')
  44. flexmock(module.dump).should_receive('create_named_pipe_for_dump').never()
  45. flexmock(module).should_receive('execute_command').never()
  46. assert (
  47. module.dump_data_sources(
  48. databases, {}, 'test.yaml', borgmatic_runtime_directory='/run/borgmatic', dry_run=True
  49. )
  50. == []
  51. )
  52. def test_dump_data_sources_runs_mongodump_with_hostname_and_port():
  53. databases = [{'name': 'foo', 'hostname': 'database.example.org', 'port': 5433}]
  54. process = flexmock()
  55. flexmock(module).should_receive('make_dump_path').and_return('')
  56. flexmock(module.dump).should_receive('make_data_source_dump_filename').and_return(
  57. 'databases/database.example.org/foo'
  58. )
  59. flexmock(module.dump).should_receive('create_named_pipe_for_dump')
  60. flexmock(module).should_receive('execute_command').with_args(
  61. (
  62. 'mongodump',
  63. '--host',
  64. 'database.example.org',
  65. '--port',
  66. '5433',
  67. '--db',
  68. 'foo',
  69. '--archive',
  70. '>',
  71. 'databases/database.example.org/foo',
  72. ),
  73. shell=True,
  74. run_to_completion=False,
  75. ).and_return(process).once()
  76. assert module.dump_data_sources(
  77. databases, {}, 'test.yaml', borgmatic_runtime_directory='/run/borgmatic', dry_run=False
  78. ) == [process]
  79. def test_dump_data_sources_runs_mongodump_with_username_and_password():
  80. databases = [
  81. {
  82. 'name': 'foo',
  83. 'username': 'mongo',
  84. 'password': 'trustsome1',
  85. 'authentication_database': 'admin',
  86. }
  87. ]
  88. process = flexmock()
  89. flexmock(module).should_receive('make_dump_path').and_return('')
  90. flexmock(module.dump).should_receive('make_data_source_dump_filename').and_return(
  91. 'databases/localhost/foo'
  92. )
  93. flexmock(module.dump).should_receive('create_named_pipe_for_dump')
  94. flexmock(module).should_receive('execute_command').with_args(
  95. (
  96. 'mongodump',
  97. '--username',
  98. 'mongo',
  99. '--password',
  100. 'trustsome1',
  101. '--authenticationDatabase',
  102. 'admin',
  103. '--db',
  104. 'foo',
  105. '--archive',
  106. '>',
  107. 'databases/localhost/foo',
  108. ),
  109. shell=True,
  110. run_to_completion=False,
  111. ).and_return(process).once()
  112. assert module.dump_data_sources(
  113. databases, {}, 'test.yaml', borgmatic_runtime_directory='/run/borgmatic', dry_run=False
  114. ) == [process]
  115. def test_dump_data_sources_runs_mongodump_with_directory_format():
  116. databases = [{'name': 'foo', 'format': 'directory'}]
  117. flexmock(module).should_receive('make_dump_path').and_return('')
  118. flexmock(module.dump).should_receive('make_data_source_dump_filename').and_return(
  119. 'databases/localhost/foo'
  120. )
  121. flexmock(module.dump).should_receive('create_parent_directory_for_dump')
  122. flexmock(module.dump).should_receive('create_named_pipe_for_dump').never()
  123. flexmock(module).should_receive('execute_command').with_args(
  124. ('mongodump', '--out', 'databases/localhost/foo', '--db', 'foo'),
  125. shell=True,
  126. ).and_return(flexmock()).once()
  127. assert (
  128. module.dump_data_sources(
  129. databases, {}, 'test.yaml', borgmatic_runtime_directory='/run/borgmatic', dry_run=False
  130. )
  131. == []
  132. )
  133. def test_dump_data_sources_runs_mongodump_with_options():
  134. databases = [{'name': 'foo', 'options': '--stuff=such'}]
  135. process = flexmock()
  136. flexmock(module).should_receive('make_dump_path').and_return('')
  137. flexmock(module.dump).should_receive('make_data_source_dump_filename').and_return(
  138. 'databases/localhost/foo'
  139. )
  140. flexmock(module.dump).should_receive('create_named_pipe_for_dump')
  141. flexmock(module).should_receive('execute_command').with_args(
  142. (
  143. 'mongodump',
  144. '--db',
  145. 'foo',
  146. '--stuff=such',
  147. '--archive',
  148. '>',
  149. 'databases/localhost/foo',
  150. ),
  151. shell=True,
  152. run_to_completion=False,
  153. ).and_return(process).once()
  154. assert module.dump_data_sources(
  155. databases, {}, 'test.yaml', borgmatic_runtime_directory='/run/borgmatic', dry_run=False
  156. ) == [process]
  157. def test_dump_data_sources_runs_mongodumpall_for_all_databases():
  158. databases = [{'name': 'all'}]
  159. process = flexmock()
  160. flexmock(module).should_receive('make_dump_path').and_return('')
  161. flexmock(module.dump).should_receive('make_data_source_dump_filename').and_return(
  162. 'databases/localhost/all'
  163. )
  164. flexmock(module.dump).should_receive('create_named_pipe_for_dump')
  165. flexmock(module).should_receive('execute_command').with_args(
  166. ('mongodump', '--archive', '>', 'databases/localhost/all'),
  167. shell=True,
  168. run_to_completion=False,
  169. ).and_return(process).once()
  170. assert module.dump_data_sources(
  171. databases, {}, 'test.yaml', borgmatic_runtime_directory='/run/borgmatic', dry_run=False
  172. ) == [process]
  173. def test_build_dump_command_with_username_injection_attack_gets_escaped():
  174. database = {'name': 'test', 'username': 'bob; naughty-command'}
  175. command = module.build_dump_command(database, dump_filename='test', dump_format='archive')
  176. assert "'bob; naughty-command'" in command
  177. def test_restore_data_source_dump_runs_mongorestore():
  178. hook_config = [{'name': 'foo', 'schemas': None}, {'name': 'bar'}]
  179. extract_process = flexmock(stdout=flexmock())
  180. flexmock(module).should_receive('make_dump_path')
  181. flexmock(module.dump).should_receive('make_data_source_dump_filename')
  182. flexmock(module).should_receive('execute_command_with_processes').with_args(
  183. ['mongorestore', '--archive', '--drop'],
  184. processes=[extract_process],
  185. output_log_level=logging.DEBUG,
  186. input_file=extract_process.stdout,
  187. ).once()
  188. module.restore_data_source_dump(
  189. hook_config,
  190. {},
  191. 'test.yaml',
  192. data_source={'name': 'foo'},
  193. dry_run=False,
  194. extract_process=extract_process,
  195. connection_params={
  196. 'hostname': None,
  197. 'port': None,
  198. 'username': None,
  199. 'password': None,
  200. },
  201. borgmatic_runtime_directory='/run/borgmatic',
  202. )
  203. def test_restore_data_source_dump_runs_mongorestore_with_hostname_and_port():
  204. hook_config = [
  205. {'name': 'foo', 'hostname': 'database.example.org', 'port': 5433, 'schemas': None}
  206. ]
  207. extract_process = flexmock(stdout=flexmock())
  208. flexmock(module).should_receive('make_dump_path')
  209. flexmock(module.dump).should_receive('make_data_source_dump_filename')
  210. flexmock(module).should_receive('execute_command_with_processes').with_args(
  211. [
  212. 'mongorestore',
  213. '--archive',
  214. '--drop',
  215. '--host',
  216. 'database.example.org',
  217. '--port',
  218. '5433',
  219. ],
  220. processes=[extract_process],
  221. output_log_level=logging.DEBUG,
  222. input_file=extract_process.stdout,
  223. ).once()
  224. module.restore_data_source_dump(
  225. hook_config,
  226. {},
  227. 'test.yaml',
  228. data_source=hook_config[0],
  229. dry_run=False,
  230. extract_process=extract_process,
  231. connection_params={
  232. 'hostname': None,
  233. 'port': None,
  234. 'username': None,
  235. 'password': None,
  236. },
  237. borgmatic_runtime_directory='/run/borgmatic',
  238. )
  239. def test_restore_data_source_dump_runs_mongorestore_with_username_and_password():
  240. hook_config = [
  241. {
  242. 'name': 'foo',
  243. 'username': 'mongo',
  244. 'password': 'trustsome1',
  245. 'authentication_database': 'admin',
  246. 'schemas': None,
  247. }
  248. ]
  249. extract_process = flexmock(stdout=flexmock())
  250. flexmock(module).should_receive('make_dump_path')
  251. flexmock(module.dump).should_receive('make_data_source_dump_filename')
  252. flexmock(module).should_receive('execute_command_with_processes').with_args(
  253. [
  254. 'mongorestore',
  255. '--archive',
  256. '--drop',
  257. '--username',
  258. 'mongo',
  259. '--password',
  260. 'trustsome1',
  261. '--authenticationDatabase',
  262. 'admin',
  263. ],
  264. processes=[extract_process],
  265. output_log_level=logging.DEBUG,
  266. input_file=extract_process.stdout,
  267. ).once()
  268. module.restore_data_source_dump(
  269. hook_config,
  270. {},
  271. 'test.yaml',
  272. data_source=hook_config[0],
  273. dry_run=False,
  274. extract_process=extract_process,
  275. connection_params={
  276. 'hostname': None,
  277. 'port': None,
  278. 'username': None,
  279. 'password': None,
  280. },
  281. borgmatic_runtime_directory='/run/borgmatic',
  282. )
  283. def test_restore_data_source_dump_with_connection_params_uses_connection_params_for_restore():
  284. hook_config = [
  285. {
  286. 'name': 'foo',
  287. 'username': 'mongo',
  288. 'password': 'trustsome1',
  289. 'authentication_database': 'admin',
  290. 'restore_hostname': 'restorehost',
  291. 'restore_port': 'restoreport',
  292. 'restore_username': 'restoreusername',
  293. 'restore_password': 'restorepassword',
  294. 'schemas': None,
  295. }
  296. ]
  297. extract_process = flexmock(stdout=flexmock())
  298. flexmock(module).should_receive('make_dump_path')
  299. flexmock(module.dump).should_receive('make_data_source_dump_filename')
  300. flexmock(module).should_receive('execute_command_with_processes').with_args(
  301. [
  302. 'mongorestore',
  303. '--archive',
  304. '--drop',
  305. '--host',
  306. 'clihost',
  307. '--port',
  308. 'cliport',
  309. '--username',
  310. 'cliusername',
  311. '--password',
  312. 'clipassword',
  313. '--authenticationDatabase',
  314. 'admin',
  315. ],
  316. processes=[extract_process],
  317. output_log_level=logging.DEBUG,
  318. input_file=extract_process.stdout,
  319. ).once()
  320. module.restore_data_source_dump(
  321. hook_config,
  322. {},
  323. 'test.yaml',
  324. data_source=hook_config[0],
  325. dry_run=False,
  326. extract_process=extract_process,
  327. connection_params={
  328. 'hostname': 'clihost',
  329. 'port': 'cliport',
  330. 'username': 'cliusername',
  331. 'password': 'clipassword',
  332. },
  333. borgmatic_runtime_directory='/run/borgmatic',
  334. )
  335. def test_restore_data_source_dump_without_connection_params_uses_restore_params_in_config_for_restore():
  336. hook_config = [
  337. {
  338. 'name': 'foo',
  339. 'username': 'mongo',
  340. 'password': 'trustsome1',
  341. 'authentication_database': 'admin',
  342. 'schemas': None,
  343. 'restore_hostname': 'restorehost',
  344. 'restore_port': 'restoreport',
  345. 'restore_username': 'restoreuser',
  346. 'restore_password': 'restorepass',
  347. }
  348. ]
  349. extract_process = flexmock(stdout=flexmock())
  350. flexmock(module).should_receive('make_dump_path')
  351. flexmock(module.dump).should_receive('make_data_source_dump_filename')
  352. flexmock(module).should_receive('execute_command_with_processes').with_args(
  353. [
  354. 'mongorestore',
  355. '--archive',
  356. '--drop',
  357. '--host',
  358. 'restorehost',
  359. '--port',
  360. 'restoreport',
  361. '--username',
  362. 'restoreuser',
  363. '--password',
  364. 'restorepass',
  365. '--authenticationDatabase',
  366. 'admin',
  367. ],
  368. processes=[extract_process],
  369. output_log_level=logging.DEBUG,
  370. input_file=extract_process.stdout,
  371. ).once()
  372. module.restore_data_source_dump(
  373. hook_config,
  374. {},
  375. 'test.yaml',
  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_options():
  388. hook_config = [{'name': 'foo', 'restore_options': '--harder', 'schemas': None}]
  389. extract_process = flexmock(stdout=flexmock())
  390. flexmock(module).should_receive('make_dump_path')
  391. flexmock(module.dump).should_receive('make_data_source_dump_filename')
  392. flexmock(module).should_receive('execute_command_with_processes').with_args(
  393. ['mongorestore', '--archive', '--drop', '--harder'],
  394. processes=[extract_process],
  395. output_log_level=logging.DEBUG,
  396. input_file=extract_process.stdout,
  397. ).once()
  398. module.restore_data_source_dump(
  399. hook_config,
  400. {},
  401. 'test.yaml',
  402. data_source=hook_config[0],
  403. dry_run=False,
  404. extract_process=extract_process,
  405. connection_params={
  406. 'hostname': None,
  407. 'port': None,
  408. 'username': None,
  409. 'password': None,
  410. },
  411. borgmatic_runtime_directory='/run/borgmatic',
  412. )
  413. def test_restore_databases_dump_runs_mongorestore_with_schemas():
  414. hook_config = [{'name': 'foo', 'schemas': ['bar', 'baz']}]
  415. extract_process = flexmock(stdout=flexmock())
  416. flexmock(module).should_receive('make_dump_path')
  417. flexmock(module.dump).should_receive('make_data_source_dump_filename')
  418. flexmock(module).should_receive('execute_command_with_processes').with_args(
  419. [
  420. 'mongorestore',
  421. '--archive',
  422. '--drop',
  423. '--nsInclude',
  424. 'bar',
  425. '--nsInclude',
  426. 'baz',
  427. ],
  428. processes=[extract_process],
  429. output_log_level=logging.DEBUG,
  430. input_file=extract_process.stdout,
  431. ).once()
  432. module.restore_data_source_dump(
  433. hook_config,
  434. {},
  435. 'test.yaml',
  436. data_source=hook_config[0],
  437. dry_run=False,
  438. extract_process=extract_process,
  439. connection_params={
  440. 'hostname': None,
  441. 'port': None,
  442. 'username': None,
  443. 'password': None,
  444. },
  445. borgmatic_runtime_directory='/run/borgmatic',
  446. )
  447. def test_restore_data_source_dump_runs_psql_for_all_database_dump():
  448. hook_config = [{'name': 'all', 'schemas': None}]
  449. extract_process = flexmock(stdout=flexmock())
  450. flexmock(module).should_receive('make_dump_path')
  451. flexmock(module.dump).should_receive('make_data_source_dump_filename')
  452. flexmock(module).should_receive('execute_command_with_processes').with_args(
  453. ['mongorestore', '--archive'],
  454. processes=[extract_process],
  455. output_log_level=logging.DEBUG,
  456. input_file=extract_process.stdout,
  457. ).once()
  458. module.restore_data_source_dump(
  459. hook_config,
  460. {},
  461. 'test.yaml',
  462. data_source=hook_config[0],
  463. dry_run=False,
  464. extract_process=extract_process,
  465. connection_params={
  466. 'hostname': None,
  467. 'port': None,
  468. 'username': None,
  469. 'password': None,
  470. },
  471. borgmatic_runtime_directory='/run/borgmatic',
  472. )
  473. def test_restore_data_source_dump_with_dry_run_skips_restore():
  474. hook_config = [{'name': 'foo', 'schemas': None}]
  475. flexmock(module).should_receive('make_dump_path')
  476. flexmock(module.dump).should_receive('make_data_source_dump_filename')
  477. flexmock(module).should_receive('execute_command_with_processes').never()
  478. module.restore_data_source_dump(
  479. hook_config,
  480. {},
  481. 'test.yaml',
  482. data_source={'name': 'foo'},
  483. dry_run=True,
  484. extract_process=flexmock(),
  485. connection_params={
  486. 'hostname': None,
  487. 'port': None,
  488. 'username': None,
  489. 'password': None,
  490. },
  491. borgmatic_runtime_directory='/run/borgmatic',
  492. )
  493. def test_restore_data_source_dump_without_extract_process_restores_from_disk():
  494. hook_config = [{'name': 'foo', 'format': 'directory', 'schemas': None}]
  495. flexmock(module).should_receive('make_dump_path')
  496. flexmock(module.dump).should_receive('make_data_source_dump_filename').and_return('/dump/path')
  497. flexmock(module).should_receive('execute_command_with_processes').with_args(
  498. ['mongorestore', '--dir', '/dump/path', '--drop'],
  499. processes=[],
  500. output_log_level=logging.DEBUG,
  501. input_file=None,
  502. ).once()
  503. module.restore_data_source_dump(
  504. hook_config,
  505. {},
  506. 'test.yaml',
  507. data_source={'name': 'foo'},
  508. dry_run=False,
  509. extract_process=None,
  510. connection_params={
  511. 'hostname': None,
  512. 'port': None,
  513. 'username': None,
  514. 'password': None,
  515. },
  516. borgmatic_runtime_directory='/run/borgmatic',
  517. )