test_mongodb.py 18 KB

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