test_postgresql.py 41 KB

1234567891011121314151617181920212223242526272829303132333435363738394041424344454647484950515253545556575859606162636465666768697071727374757677787980818283848586878889909192939495969798991001011021031041051061071081091101111121131141151161171181191201211221231241251261271281291301311321331341351361371381391401411421431441451461471481491501511521531541551561571581591601611621631641651661671681691701711721731741751761771781791801811821831841851861871881891901911921931941951961971981992002012022032042052062072082092102112122132142152162172182192202212222232242252262272282292302312322332342352362372382392402412422432442452462472482492502512522532542552562572582592602612622632642652662672682692702712722732742752762772782792802812822832842852862872882892902912922932942952962972982993003013023033043053063073083093103113123133143153163173183193203213223233243253263273283293303313323333343353363373383393403413423433443453463473483493503513523533543553563573583593603613623633643653663673683693703713723733743753763773783793803813823833843853863873883893903913923933943953963973983994004014024034044054064074084094104114124134144154164174184194204214224234244254264274284294304314324334344354364374384394404414424434444454464474484494504514524534544554564574584594604614624634644654664674684694704714724734744754764774784794804814824834844854864874884894904914924934944954964974984995005015025035045055065075085095105115125135145155165175185195205215225235245255265275285295305315325335345355365375385395405415425435445455465475485495505515525535545555565575585595605615625635645655665675685695705715725735745755765775785795805815825835845855865875885895905915925935945955965975985996006016026036046056066076086096106116126136146156166176186196206216226236246256266276286296306316326336346356366376386396406416426436446456466476486496506516526536546556566576586596606616626636646656666676686696706716726736746756766776786796806816826836846856866876886896906916926936946956966976986997007017027037047057067077087097107117127137147157167177187197207217227237247257267277287297307317327337347357367377387397407417427437447457467477487497507517527537547557567577587597607617627637647657667677687697707717727737747757767777787797807817827837847857867877887897907917927937947957967977987998008018028038048058068078088098108118128138148158168178188198208218228238248258268278288298308318328338348358368378388398408418428438448458468478488498508518528538548558568578588598608618628638648658668678688698708718728738748758768778788798808818828838848858868878888898908918928938948958968978988999009019029039049059069079089099109119129139149159169179189199209219229239249259269279289299309319329339349359369379389399409419429439449459469479489499509519529539549559569579589599609619629639649659669679689699709719729739749759769779789799809819829839849859869879889899909919929939949959969979989991000100110021003100410051006100710081009101010111012101310141015101610171018101910201021102210231024102510261027102810291030103110321033103410351036103710381039104010411042104310441045104610471048104910501051105210531054105510561057105810591060106110621063106410651066106710681069107010711072107310741075107610771078107910801081108210831084108510861087108810891090109110921093109410951096109710981099110011011102110311041105110611071108110911101111111211131114111511161117111811191120112111221123112411251126112711281129113011311132113311341135113611371138113911401141114211431144114511461147114811491150115111521153115411551156115711581159116011611162116311641165116611671168116911701171117211731174117511761177117811791180118111821183118411851186118711881189119011911192119311941195119611971198119912001201120212031204120512061207120812091210121112121213121412151216121712181219122012211222122312241225122612271228122912301231123212331234123512361237123812391240124112421243124412451246124712481249125012511252125312541255125612571258125912601261126212631264126512661267126812691270127112721273127412751276127712781279128012811282128312841285128612871288128912901291129212931294129512961297129812991300130113021303130413051306
  1. import logging
  2. import pytest
  3. from flexmock import flexmock
  4. from borgmatic.hooks.data_source import postgresql as module
  5. def test_make_extra_environment_maps_options_to_environment():
  6. database = {
  7. 'name': 'foo',
  8. 'password': 'pass',
  9. 'ssl_mode': 'require',
  10. 'ssl_cert': 'cert.crt',
  11. 'ssl_key': 'key.key',
  12. 'ssl_root_cert': 'root.crt',
  13. 'ssl_crl': 'crl.crl',
  14. }
  15. expected = {
  16. 'PGPASSWORD': 'pass',
  17. 'PGSSLMODE': 'require',
  18. 'PGSSLCERT': 'cert.crt',
  19. 'PGSSLKEY': 'key.key',
  20. 'PGSSLROOTCERT': 'root.crt',
  21. 'PGSSLCRL': 'crl.crl',
  22. }
  23. extra_env = module.make_extra_environment(database)
  24. assert extra_env == expected
  25. def test_make_extra_environment_with_cli_password_sets_correct_password():
  26. database = {'name': 'foo', 'restore_password': 'trustsome1', 'password': 'anotherpassword'}
  27. extra = module.make_extra_environment(
  28. database, restore_connection_params={'password': 'clipassword'}
  29. )
  30. assert extra['PGPASSWORD'] == 'clipassword'
  31. def test_make_extra_environment_without_cli_password_or_configured_password_does_not_set_password():
  32. database = {'name': 'foo'}
  33. extra = module.make_extra_environment(
  34. database, restore_connection_params={'username': 'someone'}
  35. )
  36. assert 'PGPASSWORD' not in extra
  37. def test_make_extra_environment_without_ssl_mode_does_not_set_ssl_mode():
  38. database = {'name': 'foo'}
  39. extra = module.make_extra_environment(database)
  40. assert 'PGSSLMODE' not in extra
  41. def test_database_names_to_dump_passes_through_individual_database_name():
  42. database = {'name': 'foo'}
  43. assert module.database_names_to_dump(database, flexmock(), dry_run=False) == (
  44. 'foo',
  45. )
  46. def test_database_names_to_dump_passes_through_individual_database_name_with_format():
  47. database = {'name': 'foo', 'format': 'custom'}
  48. assert module.database_names_to_dump(database, flexmock(), dry_run=False) == (
  49. 'foo',
  50. )
  51. def test_database_names_to_dump_passes_through_all_without_format():
  52. database = {'name': 'all'}
  53. assert module.database_names_to_dump(database, flexmock(), dry_run=False) == (
  54. 'all',
  55. )
  56. def test_database_names_to_dump_with_all_and_format_and_dry_run_bails():
  57. database = {'name': 'all', 'format': 'custom'}
  58. flexmock(module).should_receive('execute_command_and_capture_output').never()
  59. assert module.database_names_to_dump(database, flexmock(), dry_run=True) == ()
  60. def test_database_names_to_dump_with_all_and_format_lists_databases():
  61. database = {'name': 'all', 'format': 'custom'}
  62. flexmock(module).should_receive('execute_command_and_capture_output').and_return(
  63. 'foo,test,\nbar,test,"stuff and such"'
  64. )
  65. assert module.database_names_to_dump(database, flexmock(), dry_run=False) == (
  66. 'foo',
  67. 'bar',
  68. )
  69. def test_database_names_to_dump_with_all_and_format_lists_databases_with_hostname_and_port():
  70. database = {'name': 'all', 'format': 'custom', 'hostname': 'localhost', 'port': 1234}
  71. flexmock(module).should_receive('execute_command_and_capture_output').with_args(
  72. (
  73. 'psql',
  74. '--list',
  75. '--no-password',
  76. '--no-psqlrc',
  77. '--csv',
  78. '--tuples-only',
  79. '--host',
  80. 'localhost',
  81. '--port',
  82. '1234',
  83. ),
  84. extra_environment=object,
  85. ).and_return('foo,test,\nbar,test,"stuff and such"')
  86. assert module.database_names_to_dump(database, flexmock(), dry_run=False) == (
  87. 'foo',
  88. 'bar',
  89. )
  90. def test_database_names_to_dump_with_all_and_format_lists_databases_with_username():
  91. database = {'name': 'all', 'format': 'custom', 'username': 'postgres'}
  92. flexmock(module).should_receive('execute_command_and_capture_output').with_args(
  93. (
  94. 'psql',
  95. '--list',
  96. '--no-password',
  97. '--no-psqlrc',
  98. '--csv',
  99. '--tuples-only',
  100. '--username',
  101. 'postgres',
  102. ),
  103. extra_environment=object,
  104. ).and_return('foo,test,\nbar,test,"stuff and such"')
  105. assert module.database_names_to_dump(database, flexmock(), dry_run=False) == (
  106. 'foo',
  107. 'bar',
  108. )
  109. def test_database_names_to_dump_with_all_and_format_lists_databases_with_options():
  110. database = {'name': 'all', 'format': 'custom', 'list_options': '--harder'}
  111. flexmock(module).should_receive('execute_command_and_capture_output').with_args(
  112. ('psql', '--list', '--no-password', '--no-psqlrc', '--csv', '--tuples-only', '--harder'),
  113. extra_environment=object,
  114. ).and_return('foo,test,\nbar,test,"stuff and such"')
  115. assert module.database_names_to_dump(database, flexmock(), dry_run=False) == (
  116. 'foo',
  117. 'bar',
  118. )
  119. def test_database_names_to_dump_with_all_and_format_excludes_particular_databases():
  120. database = {'name': 'all', 'format': 'custom'}
  121. flexmock(module).should_receive('execute_command_and_capture_output').and_return(
  122. 'foo,test,\ntemplate0,test,blah'
  123. )
  124. assert module.database_names_to_dump(database, flexmock(), dry_run=False) == (
  125. 'foo',
  126. )
  127. def test_database_names_to_dump_with_all_and_psql_command_uses_custom_command():
  128. database = {
  129. 'name': 'all',
  130. 'format': 'custom',
  131. 'psql_command': 'docker exec --workdir * mycontainer psql',
  132. }
  133. flexmock(module).should_receive('execute_command_and_capture_output').with_args(
  134. (
  135. 'docker',
  136. 'exec',
  137. '--workdir',
  138. "'*'", # Should get shell escaped to prevent injection attacks.
  139. 'mycontainer',
  140. 'psql',
  141. '--list',
  142. '--no-password',
  143. '--no-psqlrc',
  144. '--csv',
  145. '--tuples-only',
  146. ),
  147. extra_environment=object,
  148. ).and_return('foo,text').once()
  149. assert module.database_names_to_dump(database, flexmock(), dry_run=False) == (
  150. 'foo',
  151. )
  152. def test_use_streaming_true_for_any_non_directory_format_databases():
  153. assert module.use_streaming(
  154. databases=[{'format': 'stuff'}, {'format': 'directory'}, {}],
  155. config=flexmock(),
  156. )
  157. def test_use_streaming_false_for_all_directory_format_databases():
  158. assert not module.use_streaming(
  159. databases=[{'format': 'directory'}, {'format': 'directory'}],
  160. config=flexmock(),
  161. )
  162. def test_use_streaming_false_for_no_databases():
  163. assert not module.use_streaming(databases=[], config=flexmock())
  164. def test_dump_data_sources_runs_pg_dump_for_each_database():
  165. databases = [{'name': 'foo'}, {'name': 'bar'}]
  166. processes = [flexmock(), flexmock()]
  167. flexmock(module).should_receive('make_extra_environment').and_return({'PGSSLMODE': 'disable'})
  168. flexmock(module).should_receive('make_dump_path').and_return('')
  169. flexmock(module).should_receive('database_names_to_dump').and_return(('foo',)).and_return(
  170. ('bar',)
  171. )
  172. flexmock(module.dump).should_receive('make_data_source_dump_filename').and_return(
  173. 'databases/localhost/foo'
  174. ).and_return('databases/localhost/bar')
  175. flexmock(module.os.path).should_receive('exists').and_return(False)
  176. flexmock(module.dump).should_receive('create_named_pipe_for_dump')
  177. for name, process in zip(('foo', 'bar'), processes):
  178. flexmock(module).should_receive('execute_command').with_args(
  179. (
  180. 'pg_dump',
  181. '--no-password',
  182. '--clean',
  183. '--if-exists',
  184. '--format',
  185. 'custom',
  186. name,
  187. '>',
  188. f'databases/localhost/{name}',
  189. ),
  190. shell=True,
  191. extra_environment={'PGSSLMODE': 'disable'},
  192. run_to_completion=False,
  193. ).and_return(process).once()
  194. assert (
  195. module.dump_data_sources(
  196. databases,
  197. {},
  198. config_paths=('test.yaml',),
  199. borgmatic_runtime_directory='/run/borgmatic',
  200. patterns=[],
  201. dry_run=False,
  202. )
  203. == processes
  204. )
  205. def test_dump_data_sources_raises_when_no_database_names_to_dump():
  206. databases = [{'name': 'foo'}, {'name': 'bar'}]
  207. flexmock(module).should_receive('make_extra_environment').and_return({'PGSSLMODE': 'disable'})
  208. flexmock(module).should_receive('make_dump_path').and_return('')
  209. flexmock(module).should_receive('database_names_to_dump').and_return(())
  210. with pytest.raises(ValueError):
  211. module.dump_data_sources(
  212. databases,
  213. {},
  214. config_paths=('test.yaml',),
  215. borgmatic_runtime_directory='/run/borgmatic',
  216. patterns=[],
  217. dry_run=False,
  218. )
  219. def test_dump_data_sources_does_not_raise_when_no_database_names_to_dump():
  220. databases = [{'name': 'foo'}, {'name': 'bar'}]
  221. flexmock(module).should_receive('make_extra_environment').and_return({'PGSSLMODE': 'disable'})
  222. flexmock(module).should_receive('make_dump_path').and_return('')
  223. flexmock(module).should_receive('database_names_to_dump').and_return(())
  224. module.dump_data_sources(
  225. databases,
  226. {},
  227. config_paths=('test.yaml',),
  228. borgmatic_runtime_directory='/run/borgmatic',
  229. patterns=[],
  230. dry_run=True,
  231. ) == []
  232. def test_dump_data_sources_with_duplicate_dump_skips_pg_dump():
  233. databases = [{'name': 'foo'}, {'name': 'bar'}]
  234. flexmock(module).should_receive('make_extra_environment').and_return({'PGSSLMODE': 'disable'})
  235. flexmock(module).should_receive('make_dump_path').and_return('')
  236. flexmock(module).should_receive('database_names_to_dump').and_return(('foo',)).and_return(
  237. ('bar',)
  238. )
  239. flexmock(module.dump).should_receive('make_data_source_dump_filename').and_return(
  240. 'databases/localhost/foo'
  241. ).and_return('databases/localhost/bar')
  242. flexmock(module.os.path).should_receive('exists').and_return(True)
  243. flexmock(module.dump).should_receive('create_named_pipe_for_dump').never()
  244. flexmock(module).should_receive('execute_command').never()
  245. assert (
  246. module.dump_data_sources(
  247. databases,
  248. {},
  249. config_paths=('test.yaml',),
  250. borgmatic_runtime_directory='/run/borgmatic',
  251. patterns=[],
  252. dry_run=False,
  253. )
  254. == []
  255. )
  256. def test_dump_data_sources_with_dry_run_skips_pg_dump():
  257. databases = [{'name': 'foo'}, {'name': 'bar'}]
  258. flexmock(module).should_receive('make_extra_environment').and_return({'PGSSLMODE': 'disable'})
  259. flexmock(module).should_receive('make_dump_path').and_return('')
  260. flexmock(module).should_receive('database_names_to_dump').and_return(('foo',)).and_return(
  261. ('bar',)
  262. )
  263. flexmock(module.dump).should_receive('make_data_source_dump_filename').and_return(
  264. 'databases/localhost/foo'
  265. ).and_return('databases/localhost/bar')
  266. flexmock(module.os.path).should_receive('exists').and_return(False)
  267. flexmock(module.dump).should_receive('create_named_pipe_for_dump').never()
  268. flexmock(module).should_receive('execute_command').never()
  269. assert (
  270. module.dump_data_sources(
  271. databases,
  272. {},
  273. config_paths=('test.yaml',),
  274. borgmatic_runtime_directory='/run/borgmatic',
  275. patterns=[],
  276. dry_run=True,
  277. )
  278. == []
  279. )
  280. def test_dump_data_sources_runs_pg_dump_with_hostname_and_port():
  281. databases = [{'name': 'foo', 'hostname': 'database.example.org', 'port': 5433}]
  282. process = flexmock()
  283. flexmock(module).should_receive('make_extra_environment').and_return({'PGSSLMODE': 'disable'})
  284. flexmock(module).should_receive('make_dump_path').and_return('')
  285. flexmock(module).should_receive('database_names_to_dump').and_return(('foo',))
  286. flexmock(module.dump).should_receive('make_data_source_dump_filename').and_return(
  287. 'databases/database.example.org/foo'
  288. )
  289. flexmock(module.os.path).should_receive('exists').and_return(False)
  290. flexmock(module.dump).should_receive('create_named_pipe_for_dump')
  291. flexmock(module).should_receive('execute_command').with_args(
  292. (
  293. 'pg_dump',
  294. '--no-password',
  295. '--clean',
  296. '--if-exists',
  297. '--host',
  298. 'database.example.org',
  299. '--port',
  300. '5433',
  301. '--format',
  302. 'custom',
  303. 'foo',
  304. '>',
  305. 'databases/database.example.org/foo',
  306. ),
  307. shell=True,
  308. extra_environment={'PGSSLMODE': 'disable'},
  309. run_to_completion=False,
  310. ).and_return(process).once()
  311. assert module.dump_data_sources(
  312. databases,
  313. {},
  314. config_paths=('test.yaml',),
  315. borgmatic_runtime_directory='/run/borgmatic',
  316. patterns=[],
  317. dry_run=False,
  318. ) == [process]
  319. def test_dump_data_sources_runs_pg_dump_with_username_and_password():
  320. databases = [{'name': 'foo', 'username': 'postgres', 'password': 'trustsome1'}]
  321. process = flexmock()
  322. flexmock(module).should_receive('make_extra_environment').and_return(
  323. {'PGPASSWORD': 'trustsome1', 'PGSSLMODE': 'disable'}
  324. )
  325. flexmock(module).should_receive('make_dump_path').and_return('')
  326. flexmock(module).should_receive('database_names_to_dump').and_return(('foo',))
  327. flexmock(module.dump).should_receive('make_data_source_dump_filename').and_return(
  328. 'databases/localhost/foo'
  329. )
  330. flexmock(module.os.path).should_receive('exists').and_return(False)
  331. flexmock(module.dump).should_receive('create_named_pipe_for_dump')
  332. flexmock(module).should_receive('execute_command').with_args(
  333. (
  334. 'pg_dump',
  335. '--no-password',
  336. '--clean',
  337. '--if-exists',
  338. '--username',
  339. 'postgres',
  340. '--format',
  341. 'custom',
  342. 'foo',
  343. '>',
  344. 'databases/localhost/foo',
  345. ),
  346. shell=True,
  347. extra_environment={'PGPASSWORD': 'trustsome1', 'PGSSLMODE': 'disable'},
  348. run_to_completion=False,
  349. ).and_return(process).once()
  350. assert module.dump_data_sources(
  351. databases,
  352. {},
  353. config_paths=('test.yaml',),
  354. borgmatic_runtime_directory='/run/borgmatic',
  355. patterns=[],
  356. dry_run=False,
  357. ) == [process]
  358. def test_dump_data_sources_with_username_injection_attack_gets_escaped():
  359. databases = [{'name': 'foo', 'username': 'postgres; naughty-command', 'password': 'trustsome1'}]
  360. process = flexmock()
  361. flexmock(module).should_receive('make_extra_environment').and_return(
  362. {'PGPASSWORD': 'trustsome1', 'PGSSLMODE': 'disable'}
  363. )
  364. flexmock(module).should_receive('make_dump_path').and_return('')
  365. flexmock(module).should_receive('database_names_to_dump').and_return(('foo',))
  366. flexmock(module.dump).should_receive('make_data_source_dump_filename').and_return(
  367. 'databases/localhost/foo'
  368. )
  369. flexmock(module.os.path).should_receive('exists').and_return(False)
  370. flexmock(module.dump).should_receive('create_named_pipe_for_dump')
  371. flexmock(module).should_receive('execute_command').with_args(
  372. (
  373. 'pg_dump',
  374. '--no-password',
  375. '--clean',
  376. '--if-exists',
  377. '--username',
  378. "'postgres; naughty-command'",
  379. '--format',
  380. 'custom',
  381. 'foo',
  382. '>',
  383. 'databases/localhost/foo',
  384. ),
  385. shell=True,
  386. extra_environment={'PGPASSWORD': 'trustsome1', 'PGSSLMODE': 'disable'},
  387. run_to_completion=False,
  388. ).and_return(process).once()
  389. assert module.dump_data_sources(
  390. databases,
  391. {},
  392. config_paths=('test.yaml',),
  393. borgmatic_runtime_directory='/run/borgmatic',
  394. patterns=[],
  395. dry_run=False,
  396. ) == [process]
  397. def test_dump_data_sources_runs_pg_dump_with_directory_format():
  398. databases = [{'name': 'foo', 'format': 'directory'}]
  399. flexmock(module).should_receive('make_extra_environment').and_return({'PGSSLMODE': 'disable'})
  400. flexmock(module).should_receive('make_dump_path').and_return('')
  401. flexmock(module).should_receive('database_names_to_dump').and_return(('foo',))
  402. flexmock(module.dump).should_receive('make_data_source_dump_filename').and_return(
  403. 'databases/localhost/foo'
  404. )
  405. flexmock(module.os.path).should_receive('exists').and_return(False)
  406. flexmock(module.dump).should_receive('create_parent_directory_for_dump')
  407. flexmock(module.dump).should_receive('create_named_pipe_for_dump').never()
  408. flexmock(module).should_receive('execute_command').with_args(
  409. (
  410. 'pg_dump',
  411. '--no-password',
  412. '--clean',
  413. '--if-exists',
  414. '--format',
  415. 'directory',
  416. '--file',
  417. 'databases/localhost/foo',
  418. 'foo',
  419. ),
  420. shell=True,
  421. extra_environment={'PGSSLMODE': 'disable'},
  422. ).and_return(flexmock()).once()
  423. assert (
  424. module.dump_data_sources(
  425. databases,
  426. {},
  427. config_paths=('test.yaml',),
  428. borgmatic_runtime_directory='/run/borgmatic',
  429. patterns=[],
  430. dry_run=False,
  431. )
  432. == []
  433. )
  434. def test_dump_data_sources_runs_pg_dump_with_options():
  435. databases = [{'name': 'foo', 'options': '--stuff=such'}]
  436. process = flexmock()
  437. flexmock(module).should_receive('make_extra_environment').and_return({'PGSSLMODE': 'disable'})
  438. flexmock(module).should_receive('make_dump_path').and_return('')
  439. flexmock(module).should_receive('database_names_to_dump').and_return(('foo',))
  440. flexmock(module.dump).should_receive('make_data_source_dump_filename').and_return(
  441. 'databases/localhost/foo'
  442. )
  443. flexmock(module.os.path).should_receive('exists').and_return(False)
  444. flexmock(module.dump).should_receive('create_named_pipe_for_dump')
  445. flexmock(module).should_receive('execute_command').with_args(
  446. (
  447. 'pg_dump',
  448. '--no-password',
  449. '--clean',
  450. '--if-exists',
  451. '--format',
  452. 'custom',
  453. '--stuff=such',
  454. 'foo',
  455. '>',
  456. 'databases/localhost/foo',
  457. ),
  458. shell=True,
  459. extra_environment={'PGSSLMODE': 'disable'},
  460. run_to_completion=False,
  461. ).and_return(process).once()
  462. assert module.dump_data_sources(
  463. databases,
  464. {},
  465. config_paths=('test.yaml',),
  466. borgmatic_runtime_directory='/run/borgmatic',
  467. patterns=[],
  468. dry_run=False,
  469. ) == [process]
  470. def test_dump_data_sources_runs_pg_dumpall_for_all_databases():
  471. databases = [{'name': 'all'}]
  472. process = flexmock()
  473. flexmock(module).should_receive('make_extra_environment').and_return({'PGSSLMODE': 'disable'})
  474. flexmock(module).should_receive('make_dump_path').and_return('')
  475. flexmock(module).should_receive('database_names_to_dump').and_return(('all',))
  476. flexmock(module.dump).should_receive('make_data_source_dump_filename').and_return(
  477. 'databases/localhost/all'
  478. )
  479. flexmock(module.os.path).should_receive('exists').and_return(False)
  480. flexmock(module.dump).should_receive('create_named_pipe_for_dump')
  481. flexmock(module).should_receive('execute_command').with_args(
  482. ('pg_dumpall', '--no-password', '--clean', '--if-exists', '>', 'databases/localhost/all'),
  483. shell=True,
  484. extra_environment={'PGSSLMODE': 'disable'},
  485. run_to_completion=False,
  486. ).and_return(process).once()
  487. assert module.dump_data_sources(
  488. databases,
  489. {},
  490. config_paths=('test.yaml',),
  491. borgmatic_runtime_directory='/run/borgmatic',
  492. patterns=[],
  493. dry_run=False,
  494. ) == [process]
  495. def test_dump_data_sources_runs_non_default_pg_dump():
  496. databases = [{'name': 'foo', 'pg_dump_command': 'special_pg_dump --compress *'}]
  497. process = flexmock()
  498. flexmock(module).should_receive('make_extra_environment').and_return({'PGSSLMODE': 'disable'})
  499. flexmock(module).should_receive('make_dump_path').and_return('')
  500. flexmock(module).should_receive('database_names_to_dump').and_return(('foo',))
  501. flexmock(module.dump).should_receive('make_data_source_dump_filename').and_return(
  502. 'databases/localhost/foo'
  503. )
  504. flexmock(module.os.path).should_receive('exists').and_return(False)
  505. flexmock(module.dump).should_receive('create_named_pipe_for_dump')
  506. flexmock(module).should_receive('execute_command').with_args(
  507. (
  508. 'special_pg_dump',
  509. '--compress',
  510. "'*'", # Should get shell escaped to prevent injection attacks.
  511. '--no-password',
  512. '--clean',
  513. '--if-exists',
  514. '--format',
  515. 'custom',
  516. 'foo',
  517. '>',
  518. 'databases/localhost/foo',
  519. ),
  520. shell=True,
  521. extra_environment={'PGSSLMODE': 'disable'},
  522. run_to_completion=False,
  523. ).and_return(process).once()
  524. assert module.dump_data_sources(
  525. databases,
  526. {},
  527. config_paths=('test.yaml',),
  528. borgmatic_runtime_directory='/run/borgmatic',
  529. patterns=[],
  530. dry_run=False,
  531. ) == [process]
  532. def test_restore_data_source_dump_runs_pg_restore():
  533. hook_config = [{'name': 'foo', 'schemas': None}, {'name': 'bar'}]
  534. extract_process = flexmock(stdout=flexmock())
  535. flexmock(module).should_receive('make_extra_environment').and_return({'PGSSLMODE': 'disable'})
  536. flexmock(module).should_receive('make_dump_path')
  537. flexmock(module.dump).should_receive('make_data_source_dump_filename')
  538. flexmock(module).should_receive('execute_command_with_processes').with_args(
  539. (
  540. 'pg_restore',
  541. '--no-password',
  542. '--if-exists',
  543. '--exit-on-error',
  544. '--clean',
  545. '--dbname',
  546. 'foo',
  547. ),
  548. processes=[extract_process],
  549. output_log_level=logging.DEBUG,
  550. input_file=extract_process.stdout,
  551. extra_environment={'PGSSLMODE': 'disable'},
  552. ).once()
  553. flexmock(module).should_receive('execute_command').with_args(
  554. (
  555. 'psql',
  556. '--no-password',
  557. '--no-psqlrc',
  558. '--quiet',
  559. '--dbname',
  560. 'foo',
  561. '--command',
  562. 'ANALYZE',
  563. ),
  564. extra_environment={'PGSSLMODE': 'disable'},
  565. ).once()
  566. module.restore_data_source_dump(
  567. hook_config,
  568. {},
  569. data_source={'name': 'foo'},
  570. dry_run=False,
  571. extract_process=extract_process,
  572. connection_params={
  573. 'hostname': None,
  574. 'port': None,
  575. 'username': None,
  576. 'password': None,
  577. },
  578. borgmatic_runtime_directory='/run/borgmatic',
  579. )
  580. def test_restore_data_source_dump_runs_pg_restore_with_hostname_and_port():
  581. hook_config = [
  582. {'name': 'foo', 'hostname': 'database.example.org', 'port': 5433, 'schemas': None}
  583. ]
  584. extract_process = flexmock(stdout=flexmock())
  585. flexmock(module).should_receive('make_extra_environment').and_return({'PGSSLMODE': 'disable'})
  586. flexmock(module).should_receive('make_dump_path')
  587. flexmock(module.dump).should_receive('make_data_source_dump_filename')
  588. flexmock(module).should_receive('execute_command_with_processes').with_args(
  589. (
  590. 'pg_restore',
  591. '--no-password',
  592. '--if-exists',
  593. '--exit-on-error',
  594. '--clean',
  595. '--dbname',
  596. 'foo',
  597. '--host',
  598. 'database.example.org',
  599. '--port',
  600. '5433',
  601. ),
  602. processes=[extract_process],
  603. output_log_level=logging.DEBUG,
  604. input_file=extract_process.stdout,
  605. extra_environment={'PGSSLMODE': 'disable'},
  606. ).once()
  607. flexmock(module).should_receive('execute_command').with_args(
  608. (
  609. 'psql',
  610. '--no-password',
  611. '--no-psqlrc',
  612. '--quiet',
  613. '--host',
  614. 'database.example.org',
  615. '--port',
  616. '5433',
  617. '--dbname',
  618. 'foo',
  619. '--command',
  620. 'ANALYZE',
  621. ),
  622. extra_environment={'PGSSLMODE': 'disable'},
  623. ).once()
  624. module.restore_data_source_dump(
  625. hook_config,
  626. {},
  627. data_source=hook_config[0],
  628. dry_run=False,
  629. extract_process=extract_process,
  630. connection_params={
  631. 'hostname': None,
  632. 'port': None,
  633. 'username': None,
  634. 'password': None,
  635. },
  636. borgmatic_runtime_directory='/run/borgmatic',
  637. )
  638. def test_restore_data_source_dump_runs_pg_restore_with_username_and_password():
  639. hook_config = [
  640. {'name': 'foo', 'username': 'postgres', 'password': 'trustsome1', 'schemas': None}
  641. ]
  642. extract_process = flexmock(stdout=flexmock())
  643. flexmock(module).should_receive('make_extra_environment').and_return(
  644. {'PGPASSWORD': 'trustsome1', 'PGSSLMODE': 'disable'}
  645. )
  646. flexmock(module).should_receive('make_dump_path')
  647. flexmock(module.dump).should_receive('make_data_source_dump_filename')
  648. flexmock(module).should_receive('execute_command_with_processes').with_args(
  649. (
  650. 'pg_restore',
  651. '--no-password',
  652. '--if-exists',
  653. '--exit-on-error',
  654. '--clean',
  655. '--dbname',
  656. 'foo',
  657. '--username',
  658. 'postgres',
  659. ),
  660. processes=[extract_process],
  661. output_log_level=logging.DEBUG,
  662. input_file=extract_process.stdout,
  663. extra_environment={'PGPASSWORD': 'trustsome1', 'PGSSLMODE': 'disable'},
  664. ).once()
  665. flexmock(module).should_receive('execute_command').with_args(
  666. (
  667. 'psql',
  668. '--no-password',
  669. '--no-psqlrc',
  670. '--quiet',
  671. '--username',
  672. 'postgres',
  673. '--dbname',
  674. 'foo',
  675. '--command',
  676. 'ANALYZE',
  677. ),
  678. extra_environment={'PGPASSWORD': 'trustsome1', 'PGSSLMODE': 'disable'},
  679. ).once()
  680. module.restore_data_source_dump(
  681. hook_config,
  682. {},
  683. data_source=hook_config[0],
  684. dry_run=False,
  685. extract_process=extract_process,
  686. connection_params={
  687. 'hostname': None,
  688. 'port': None,
  689. 'username': None,
  690. 'password': None,
  691. },
  692. borgmatic_runtime_directory='/run/borgmatic',
  693. )
  694. def test_restore_data_source_dump_with_connection_params_uses_connection_params_for_restore():
  695. hook_config = [
  696. {
  697. 'name': 'foo',
  698. 'hostname': 'database.example.org',
  699. 'port': 5433,
  700. 'username': 'postgres',
  701. 'password': 'trustsome1',
  702. 'restore_hostname': 'restorehost',
  703. 'restore_port': 'restoreport',
  704. 'restore_username': 'restoreusername',
  705. 'restore_password': 'restorepassword',
  706. 'schemas': None,
  707. }
  708. ]
  709. extract_process = flexmock(stdout=flexmock())
  710. flexmock(module).should_receive('make_extra_environment').and_return(
  711. {'PGPASSWORD': 'clipassword', 'PGSSLMODE': 'disable'}
  712. )
  713. flexmock(module).should_receive('make_dump_path')
  714. flexmock(module.dump).should_receive('make_data_source_dump_filename')
  715. flexmock(module).should_receive('execute_command_with_processes').with_args(
  716. (
  717. 'pg_restore',
  718. '--no-password',
  719. '--if-exists',
  720. '--exit-on-error',
  721. '--clean',
  722. '--dbname',
  723. 'foo',
  724. '--host',
  725. 'clihost',
  726. '--port',
  727. 'cliport',
  728. '--username',
  729. 'cliusername',
  730. ),
  731. processes=[extract_process],
  732. output_log_level=logging.DEBUG,
  733. input_file=extract_process.stdout,
  734. extra_environment={'PGPASSWORD': 'clipassword', 'PGSSLMODE': 'disable'},
  735. ).once()
  736. flexmock(module).should_receive('execute_command').with_args(
  737. (
  738. 'psql',
  739. '--no-password',
  740. '--no-psqlrc',
  741. '--quiet',
  742. '--host',
  743. 'clihost',
  744. '--port',
  745. 'cliport',
  746. '--username',
  747. 'cliusername',
  748. '--dbname',
  749. 'foo',
  750. '--command',
  751. 'ANALYZE',
  752. ),
  753. extra_environment={'PGPASSWORD': 'clipassword', 'PGSSLMODE': 'disable'},
  754. ).once()
  755. module.restore_data_source_dump(
  756. hook_config,
  757. {},
  758. data_source={'name': 'foo'},
  759. dry_run=False,
  760. extract_process=extract_process,
  761. connection_params={
  762. 'hostname': 'clihost',
  763. 'port': 'cliport',
  764. 'username': 'cliusername',
  765. 'password': 'clipassword',
  766. },
  767. borgmatic_runtime_directory='/run/borgmatic',
  768. )
  769. def test_restore_data_source_dump_without_connection_params_uses_restore_params_in_config_for_restore():
  770. hook_config = [
  771. {
  772. 'name': 'foo',
  773. 'hostname': 'database.example.org',
  774. 'port': 5433,
  775. 'username': 'postgres',
  776. 'password': 'trustsome1',
  777. 'schemas': None,
  778. 'restore_hostname': 'restorehost',
  779. 'restore_port': 'restoreport',
  780. 'restore_username': 'restoreusername',
  781. 'restore_password': 'restorepassword',
  782. }
  783. ]
  784. extract_process = flexmock(stdout=flexmock())
  785. flexmock(module).should_receive('make_extra_environment').and_return(
  786. {'PGPASSWORD': 'restorepassword', 'PGSSLMODE': 'disable'}
  787. )
  788. flexmock(module).should_receive('make_dump_path')
  789. flexmock(module.dump).should_receive('make_data_source_dump_filename')
  790. flexmock(module).should_receive('execute_command_with_processes').with_args(
  791. (
  792. 'pg_restore',
  793. '--no-password',
  794. '--if-exists',
  795. '--exit-on-error',
  796. '--clean',
  797. '--dbname',
  798. 'foo',
  799. '--host',
  800. 'restorehost',
  801. '--port',
  802. 'restoreport',
  803. '--username',
  804. 'restoreusername',
  805. ),
  806. processes=[extract_process],
  807. output_log_level=logging.DEBUG,
  808. input_file=extract_process.stdout,
  809. extra_environment={'PGPASSWORD': 'restorepassword', 'PGSSLMODE': 'disable'},
  810. ).once()
  811. flexmock(module).should_receive('execute_command').with_args(
  812. (
  813. 'psql',
  814. '--no-password',
  815. '--no-psqlrc',
  816. '--quiet',
  817. '--host',
  818. 'restorehost',
  819. '--port',
  820. 'restoreport',
  821. '--username',
  822. 'restoreusername',
  823. '--dbname',
  824. 'foo',
  825. '--command',
  826. 'ANALYZE',
  827. ),
  828. extra_environment={'PGPASSWORD': 'restorepassword', 'PGSSLMODE': 'disable'},
  829. ).once()
  830. module.restore_data_source_dump(
  831. hook_config,
  832. {},
  833. data_source=hook_config[0],
  834. dry_run=False,
  835. extract_process=extract_process,
  836. connection_params={
  837. 'hostname': None,
  838. 'port': None,
  839. 'username': None,
  840. 'password': None,
  841. },
  842. borgmatic_runtime_directory='/run/borgmatic',
  843. )
  844. def test_restore_data_source_dump_runs_pg_restore_with_options():
  845. hook_config = [
  846. {
  847. 'name': 'foo',
  848. 'restore_options': '--harder',
  849. 'analyze_options': '--smarter',
  850. 'schemas': None,
  851. }
  852. ]
  853. extract_process = flexmock(stdout=flexmock())
  854. flexmock(module).should_receive('make_extra_environment').and_return({'PGSSLMODE': 'disable'})
  855. flexmock(module).should_receive('make_dump_path')
  856. flexmock(module.dump).should_receive('make_data_source_dump_filename')
  857. flexmock(module).should_receive('execute_command_with_processes').with_args(
  858. (
  859. 'pg_restore',
  860. '--no-password',
  861. '--if-exists',
  862. '--exit-on-error',
  863. '--clean',
  864. '--dbname',
  865. 'foo',
  866. '--harder',
  867. ),
  868. processes=[extract_process],
  869. output_log_level=logging.DEBUG,
  870. input_file=extract_process.stdout,
  871. extra_environment={'PGSSLMODE': 'disable'},
  872. ).once()
  873. flexmock(module).should_receive('execute_command').with_args(
  874. (
  875. 'psql',
  876. '--no-password',
  877. '--no-psqlrc',
  878. '--quiet',
  879. '--dbname',
  880. 'foo',
  881. '--smarter',
  882. '--command',
  883. 'ANALYZE',
  884. ),
  885. extra_environment={'PGSSLMODE': 'disable'},
  886. ).once()
  887. module.restore_data_source_dump(
  888. hook_config,
  889. {},
  890. data_source=hook_config[0],
  891. dry_run=False,
  892. extract_process=extract_process,
  893. connection_params={
  894. 'hostname': None,
  895. 'port': None,
  896. 'username': None,
  897. 'password': None,
  898. },
  899. borgmatic_runtime_directory='/run/borgmatic',
  900. )
  901. def test_restore_data_source_dump_runs_psql_for_all_database_dump():
  902. hook_config = [{'name': 'all', 'schemas': None}]
  903. extract_process = flexmock(stdout=flexmock())
  904. flexmock(module).should_receive('make_extra_environment').and_return({'PGSSLMODE': 'disable'})
  905. flexmock(module).should_receive('make_dump_path')
  906. flexmock(module.dump).should_receive('make_data_source_dump_filename')
  907. flexmock(module).should_receive('execute_command_with_processes').with_args(
  908. (
  909. 'psql',
  910. '--no-password',
  911. '--no-psqlrc',
  912. ),
  913. processes=[extract_process],
  914. output_log_level=logging.DEBUG,
  915. input_file=extract_process.stdout,
  916. extra_environment={'PGSSLMODE': 'disable'},
  917. ).once()
  918. flexmock(module).should_receive('execute_command').with_args(
  919. ('psql', '--no-password', '--no-psqlrc', '--quiet', '--command', 'ANALYZE'),
  920. extra_environment={'PGSSLMODE': 'disable'},
  921. ).once()
  922. module.restore_data_source_dump(
  923. hook_config,
  924. {},
  925. data_source={'name': 'all'},
  926. dry_run=False,
  927. extract_process=extract_process,
  928. connection_params={
  929. 'hostname': None,
  930. 'port': None,
  931. 'username': None,
  932. 'password': None,
  933. },
  934. borgmatic_runtime_directory='/run/borgmatic',
  935. )
  936. def test_restore_data_source_dump_runs_psql_for_plain_database_dump():
  937. hook_config = [{'name': 'foo', 'format': 'plain', 'schemas': None}]
  938. extract_process = flexmock(stdout=flexmock())
  939. flexmock(module).should_receive('make_extra_environment').and_return({'PGSSLMODE': 'disable'})
  940. flexmock(module).should_receive('make_dump_path')
  941. flexmock(module.dump).should_receive('make_data_source_dump_filename')
  942. flexmock(module).should_receive('execute_command_with_processes').with_args(
  943. ('psql', '--no-password', '--no-psqlrc', '--dbname', 'foo'),
  944. processes=[extract_process],
  945. output_log_level=logging.DEBUG,
  946. input_file=extract_process.stdout,
  947. extra_environment={'PGSSLMODE': 'disable'},
  948. ).once()
  949. flexmock(module).should_receive('execute_command').with_args(
  950. (
  951. 'psql',
  952. '--no-password',
  953. '--no-psqlrc',
  954. '--quiet',
  955. '--dbname',
  956. 'foo',
  957. '--command',
  958. 'ANALYZE',
  959. ),
  960. extra_environment={'PGSSLMODE': 'disable'},
  961. ).once()
  962. module.restore_data_source_dump(
  963. hook_config,
  964. {},
  965. data_source=hook_config[0],
  966. dry_run=False,
  967. extract_process=extract_process,
  968. connection_params={
  969. 'hostname': None,
  970. 'port': None,
  971. 'username': None,
  972. 'password': None,
  973. },
  974. borgmatic_runtime_directory='/run/borgmatic',
  975. )
  976. def test_restore_data_source_dump_runs_non_default_pg_restore_and_psql():
  977. hook_config = [
  978. {
  979. 'name': 'foo',
  980. 'pg_restore_command': 'docker exec --workdir * mycontainer pg_restore',
  981. 'psql_command': 'docker exec --workdir * mycontainer psql',
  982. 'schemas': None,
  983. }
  984. ]
  985. extract_process = flexmock(stdout=flexmock())
  986. flexmock(module).should_receive('make_extra_environment').and_return({'PGSSLMODE': 'disable'})
  987. flexmock(module).should_receive('make_dump_path')
  988. flexmock(module.dump).should_receive('make_data_source_dump_filename')
  989. flexmock(module).should_receive('execute_command_with_processes').with_args(
  990. (
  991. 'docker',
  992. 'exec',
  993. '--workdir',
  994. "'*'", # Should get shell escaped to prevent injection attacks.
  995. 'mycontainer',
  996. 'pg_restore',
  997. '--no-password',
  998. '--if-exists',
  999. '--exit-on-error',
  1000. '--clean',
  1001. '--dbname',
  1002. 'foo',
  1003. ),
  1004. processes=[extract_process],
  1005. output_log_level=logging.DEBUG,
  1006. input_file=extract_process.stdout,
  1007. extra_environment={'PGSSLMODE': 'disable'},
  1008. ).once()
  1009. flexmock(module).should_receive('execute_command').with_args(
  1010. (
  1011. 'docker',
  1012. 'exec',
  1013. '--workdir',
  1014. "'*'", # Should get shell escaped to prevent injection attacks.
  1015. 'mycontainer',
  1016. 'psql',
  1017. '--no-password',
  1018. '--no-psqlrc',
  1019. '--quiet',
  1020. '--dbname',
  1021. 'foo',
  1022. '--command',
  1023. 'ANALYZE',
  1024. ),
  1025. extra_environment={'PGSSLMODE': 'disable'},
  1026. ).once()
  1027. module.restore_data_source_dump(
  1028. hook_config,
  1029. {},
  1030. data_source=hook_config[0],
  1031. dry_run=False,
  1032. extract_process=extract_process,
  1033. connection_params={
  1034. 'hostname': None,
  1035. 'port': None,
  1036. 'username': None,
  1037. 'password': None,
  1038. },
  1039. borgmatic_runtime_directory='/run/borgmatic',
  1040. )
  1041. def test_restore_data_source_dump_with_dry_run_skips_restore():
  1042. hook_config = [{'name': 'foo', 'schemas': None}]
  1043. flexmock(module).should_receive('make_extra_environment').and_return({'PGSSLMODE': 'disable'})
  1044. flexmock(module).should_receive('make_dump_path')
  1045. flexmock(module.dump).should_receive('make_data_source_dump_filename')
  1046. flexmock(module).should_receive('execute_command_with_processes').never()
  1047. module.restore_data_source_dump(
  1048. hook_config,
  1049. {},
  1050. data_source={'name': 'foo'},
  1051. dry_run=True,
  1052. extract_process=flexmock(),
  1053. connection_params={
  1054. 'hostname': None,
  1055. 'port': None,
  1056. 'username': None,
  1057. 'password': None,
  1058. },
  1059. borgmatic_runtime_directory='/run/borgmatic',
  1060. )
  1061. def test_restore_data_source_dump_without_extract_process_restores_from_disk():
  1062. hook_config = [{'name': 'foo', 'schemas': None}]
  1063. flexmock(module).should_receive('make_extra_environment').and_return({'PGSSLMODE': 'disable'})
  1064. flexmock(module).should_receive('make_dump_path')
  1065. flexmock(module.dump).should_receive('make_data_source_dump_filename').and_return('/dump/path')
  1066. flexmock(module).should_receive('execute_command_with_processes').with_args(
  1067. (
  1068. 'pg_restore',
  1069. '--no-password',
  1070. '--if-exists',
  1071. '--exit-on-error',
  1072. '--clean',
  1073. '--dbname',
  1074. 'foo',
  1075. '/dump/path',
  1076. ),
  1077. processes=[],
  1078. output_log_level=logging.DEBUG,
  1079. input_file=None,
  1080. extra_environment={'PGSSLMODE': 'disable'},
  1081. ).once()
  1082. flexmock(module).should_receive('execute_command').with_args(
  1083. (
  1084. 'psql',
  1085. '--no-password',
  1086. '--no-psqlrc',
  1087. '--quiet',
  1088. '--dbname',
  1089. 'foo',
  1090. '--command',
  1091. 'ANALYZE',
  1092. ),
  1093. extra_environment={'PGSSLMODE': 'disable'},
  1094. ).once()
  1095. module.restore_data_source_dump(
  1096. hook_config,
  1097. {},
  1098. data_source={'name': 'foo'},
  1099. dry_run=False,
  1100. extract_process=None,
  1101. connection_params={
  1102. 'hostname': None,
  1103. 'port': None,
  1104. 'username': None,
  1105. 'password': None,
  1106. },
  1107. borgmatic_runtime_directory='/run/borgmatic',
  1108. )
  1109. def test_restore_data_source_dump_with_schemas_restores_schemas():
  1110. hook_config = [{'name': 'foo', 'schemas': ['bar', 'baz']}]
  1111. flexmock(module).should_receive('make_extra_environment').and_return({'PGSSLMODE': 'disable'})
  1112. flexmock(module).should_receive('make_dump_path')
  1113. flexmock(module.dump).should_receive('make_data_source_dump_filename').and_return('/dump/path')
  1114. flexmock(module).should_receive('execute_command_with_processes').with_args(
  1115. (
  1116. 'pg_restore',
  1117. '--no-password',
  1118. '--if-exists',
  1119. '--exit-on-error',
  1120. '--clean',
  1121. '--dbname',
  1122. 'foo',
  1123. '/dump/path',
  1124. '--schema',
  1125. 'bar',
  1126. '--schema',
  1127. 'baz',
  1128. ),
  1129. processes=[],
  1130. output_log_level=logging.DEBUG,
  1131. input_file=None,
  1132. extra_environment={'PGSSLMODE': 'disable'},
  1133. ).once()
  1134. flexmock(module).should_receive('execute_command').with_args(
  1135. (
  1136. 'psql',
  1137. '--no-password',
  1138. '--no-psqlrc',
  1139. '--quiet',
  1140. '--dbname',
  1141. 'foo',
  1142. '--command',
  1143. 'ANALYZE',
  1144. ),
  1145. extra_environment={'PGSSLMODE': 'disable'},
  1146. ).once()
  1147. module.restore_data_source_dump(
  1148. hook_config,
  1149. {},
  1150. data_source=hook_config[0],
  1151. dry_run=False,
  1152. extract_process=None,
  1153. connection_params={
  1154. 'hostname': None,
  1155. 'port': None,
  1156. 'username': None,
  1157. 'password': None,
  1158. },
  1159. borgmatic_runtime_directory='/run/borgmatic',
  1160. )