test_mongodb.py 18 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586
  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. )
  202. def test_restore_data_source_dump_runs_mongorestore_with_hostname_and_port():
  203. hook_config = [
  204. {'name': 'foo', 'hostname': 'database.example.org', 'port': 5433, 'schemas': None}
  205. ]
  206. extract_process = flexmock(stdout=flexmock())
  207. flexmock(module).should_receive('make_dump_path')
  208. flexmock(module.dump).should_receive('make_data_source_dump_filename')
  209. flexmock(module).should_receive('execute_command_with_processes').with_args(
  210. [
  211. 'mongorestore',
  212. '--archive',
  213. '--drop',
  214. '--host',
  215. 'database.example.org',
  216. '--port',
  217. '5433',
  218. ],
  219. processes=[extract_process],
  220. output_log_level=logging.DEBUG,
  221. input_file=extract_process.stdout,
  222. ).once()
  223. module.restore_data_source_dump(
  224. hook_config,
  225. {},
  226. 'test.yaml',
  227. data_source=hook_config[0],
  228. dry_run=False,
  229. extract_process=extract_process,
  230. connection_params={
  231. 'hostname': None,
  232. 'port': None,
  233. 'username': None,
  234. 'password': None,
  235. },
  236. )
  237. def test_restore_data_source_dump_runs_mongorestore_with_username_and_password():
  238. hook_config = [
  239. {
  240. 'name': 'foo',
  241. 'username': 'mongo',
  242. 'password': 'trustsome1',
  243. 'authentication_database': 'admin',
  244. 'schemas': None,
  245. }
  246. ]
  247. extract_process = flexmock(stdout=flexmock())
  248. flexmock(module).should_receive('make_dump_path')
  249. flexmock(module.dump).should_receive('make_data_source_dump_filename')
  250. flexmock(module).should_receive('execute_command_with_processes').with_args(
  251. [
  252. 'mongorestore',
  253. '--archive',
  254. '--drop',
  255. '--username',
  256. 'mongo',
  257. '--password',
  258. 'trustsome1',
  259. '--authenticationDatabase',
  260. 'admin',
  261. ],
  262. processes=[extract_process],
  263. output_log_level=logging.DEBUG,
  264. input_file=extract_process.stdout,
  265. ).once()
  266. module.restore_data_source_dump(
  267. hook_config,
  268. {},
  269. 'test.yaml',
  270. data_source=hook_config[0],
  271. dry_run=False,
  272. extract_process=extract_process,
  273. connection_params={
  274. 'hostname': None,
  275. 'port': None,
  276. 'username': None,
  277. 'password': None,
  278. },
  279. )
  280. def test_restore_data_source_dump_with_connection_params_uses_connection_params_for_restore():
  281. hook_config = [
  282. {
  283. 'name': 'foo',
  284. 'username': 'mongo',
  285. 'password': 'trustsome1',
  286. 'authentication_database': 'admin',
  287. 'restore_hostname': 'restorehost',
  288. 'restore_port': 'restoreport',
  289. 'restore_username': 'restoreusername',
  290. 'restore_password': 'restorepassword',
  291. 'schemas': None,
  292. }
  293. ]
  294. extract_process = flexmock(stdout=flexmock())
  295. flexmock(module).should_receive('make_dump_path')
  296. flexmock(module.dump).should_receive('make_data_source_dump_filename')
  297. flexmock(module).should_receive('execute_command_with_processes').with_args(
  298. [
  299. 'mongorestore',
  300. '--archive',
  301. '--drop',
  302. '--host',
  303. 'clihost',
  304. '--port',
  305. 'cliport',
  306. '--username',
  307. 'cliusername',
  308. '--password',
  309. 'clipassword',
  310. '--authenticationDatabase',
  311. 'admin',
  312. ],
  313. processes=[extract_process],
  314. output_log_level=logging.DEBUG,
  315. input_file=extract_process.stdout,
  316. ).once()
  317. module.restore_data_source_dump(
  318. hook_config,
  319. {},
  320. 'test.yaml',
  321. data_source=hook_config[0],
  322. dry_run=False,
  323. extract_process=extract_process,
  324. connection_params={
  325. 'hostname': 'clihost',
  326. 'port': 'cliport',
  327. 'username': 'cliusername',
  328. 'password': 'clipassword',
  329. },
  330. )
  331. def test_restore_data_source_dump_without_connection_params_uses_restore_params_in_config_for_restore():
  332. hook_config = [
  333. {
  334. 'name': 'foo',
  335. 'username': 'mongo',
  336. 'password': 'trustsome1',
  337. 'authentication_database': 'admin',
  338. 'schemas': None,
  339. 'restore_hostname': 'restorehost',
  340. 'restore_port': 'restoreport',
  341. 'restore_username': 'restoreuser',
  342. 'restore_password': 'restorepass',
  343. }
  344. ]
  345. extract_process = flexmock(stdout=flexmock())
  346. flexmock(module).should_receive('make_dump_path')
  347. flexmock(module.dump).should_receive('make_data_source_dump_filename')
  348. flexmock(module).should_receive('execute_command_with_processes').with_args(
  349. [
  350. 'mongorestore',
  351. '--archive',
  352. '--drop',
  353. '--host',
  354. 'restorehost',
  355. '--port',
  356. 'restoreport',
  357. '--username',
  358. 'restoreuser',
  359. '--password',
  360. 'restorepass',
  361. '--authenticationDatabase',
  362. 'admin',
  363. ],
  364. processes=[extract_process],
  365. output_log_level=logging.DEBUG,
  366. input_file=extract_process.stdout,
  367. ).once()
  368. module.restore_data_source_dump(
  369. hook_config,
  370. {},
  371. 'test.yaml',
  372. data_source=hook_config[0],
  373. dry_run=False,
  374. extract_process=extract_process,
  375. connection_params={
  376. 'hostname': None,
  377. 'port': None,
  378. 'username': None,
  379. 'password': None,
  380. },
  381. )
  382. def test_restore_data_source_dump_runs_mongorestore_with_options():
  383. hook_config = [{'name': 'foo', 'restore_options': '--harder', 'schemas': None}]
  384. extract_process = flexmock(stdout=flexmock())
  385. flexmock(module).should_receive('make_dump_path')
  386. flexmock(module.dump).should_receive('make_data_source_dump_filename')
  387. flexmock(module).should_receive('execute_command_with_processes').with_args(
  388. ['mongorestore', '--archive', '--drop', '--harder'],
  389. processes=[extract_process],
  390. output_log_level=logging.DEBUG,
  391. input_file=extract_process.stdout,
  392. ).once()
  393. module.restore_data_source_dump(
  394. hook_config,
  395. {},
  396. 'test.yaml',
  397. data_source=hook_config[0],
  398. dry_run=False,
  399. extract_process=extract_process,
  400. connection_params={
  401. 'hostname': None,
  402. 'port': None,
  403. 'username': None,
  404. 'password': None,
  405. },
  406. )
  407. def test_restore_databases_dump_runs_mongorestore_with_schemas():
  408. hook_config = [{'name': 'foo', 'schemas': ['bar', 'baz']}]
  409. extract_process = flexmock(stdout=flexmock())
  410. flexmock(module).should_receive('make_dump_path')
  411. flexmock(module.dump).should_receive('make_data_source_dump_filename')
  412. flexmock(module).should_receive('execute_command_with_processes').with_args(
  413. [
  414. 'mongorestore',
  415. '--archive',
  416. '--drop',
  417. '--nsInclude',
  418. 'bar',
  419. '--nsInclude',
  420. 'baz',
  421. ],
  422. processes=[extract_process],
  423. output_log_level=logging.DEBUG,
  424. input_file=extract_process.stdout,
  425. ).once()
  426. module.restore_data_source_dump(
  427. hook_config,
  428. {},
  429. 'test.yaml',
  430. data_source=hook_config[0],
  431. dry_run=False,
  432. extract_process=extract_process,
  433. connection_params={
  434. 'hostname': None,
  435. 'port': None,
  436. 'username': None,
  437. 'password': None,
  438. },
  439. )
  440. def test_restore_data_source_dump_runs_psql_for_all_database_dump():
  441. hook_config = [{'name': 'all', 'schemas': None}]
  442. extract_process = flexmock(stdout=flexmock())
  443. flexmock(module).should_receive('make_dump_path')
  444. flexmock(module.dump).should_receive('make_data_source_dump_filename')
  445. flexmock(module).should_receive('execute_command_with_processes').with_args(
  446. ['mongorestore', '--archive'],
  447. processes=[extract_process],
  448. output_log_level=logging.DEBUG,
  449. input_file=extract_process.stdout,
  450. ).once()
  451. module.restore_data_source_dump(
  452. hook_config,
  453. {},
  454. 'test.yaml',
  455. data_source=hook_config[0],
  456. dry_run=False,
  457. extract_process=extract_process,
  458. connection_params={
  459. 'hostname': None,
  460. 'port': None,
  461. 'username': None,
  462. 'password': None,
  463. },
  464. )
  465. def test_restore_data_source_dump_with_dry_run_skips_restore():
  466. hook_config = [{'name': 'foo', 'schemas': None}]
  467. flexmock(module).should_receive('make_dump_path')
  468. flexmock(module.dump).should_receive('make_data_source_dump_filename')
  469. flexmock(module).should_receive('execute_command_with_processes').never()
  470. module.restore_data_source_dump(
  471. hook_config,
  472. {},
  473. 'test.yaml',
  474. data_source={'name': 'foo'},
  475. dry_run=True,
  476. extract_process=flexmock(),
  477. connection_params={
  478. 'hostname': None,
  479. 'port': None,
  480. 'username': None,
  481. 'password': None,
  482. },
  483. )
  484. def test_restore_data_source_dump_without_extract_process_restores_from_disk():
  485. hook_config = [{'name': 'foo', 'format': 'directory', 'schemas': None}]
  486. flexmock(module).should_receive('make_dump_path')
  487. flexmock(module.dump).should_receive('make_data_source_dump_filename').and_return('/dump/path')
  488. flexmock(module).should_receive('execute_command_with_processes').with_args(
  489. ['mongorestore', '--dir', '/dump/path', '--drop'],
  490. processes=[],
  491. output_log_level=logging.DEBUG,
  492. input_file=None,
  493. ).once()
  494. module.restore_data_source_dump(
  495. hook_config,
  496. {},
  497. 'test.yaml',
  498. data_source={'name': 'foo'},
  499. dry_run=False,
  500. extract_process=None,
  501. connection_params={
  502. 'hostname': None,
  503. 'port': None,
  504. 'username': None,
  505. 'password': None,
  506. },
  507. )