test_postgresql.py 41 KB

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