2
0

test_mongodb.py 17 KB

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