diff --git a/CHANGELOG.md b/CHANGELOG.md index 1ac7808b3b90..4b6fbc953aa8 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -7,6 +7,31 @@ Versions are `MAJOR.PATCH`. # Changelog + +## 3006.4 (2023-10-16) + +### Security + +- Fix CVE-2023-34049 by ensuring we do not use a predictable name for the script and correctly check returncode of scp command. + This only impacts salt-ssh users using the pre-flight option. [#cve-2023-34049](https://github.com/saltstack/salt/issues/cve-2023-34049) +- Update to `gitpython>=3.1.35` due to https://github.com/advisories/GHSA-wfm5-v35h-vwf4 and https://github.com/advisories/GHSA-cwvm-v4w8-q58c [#65163](https://github.com/saltstack/salt/issues/65163) +- Bump to `cryptography==41.0.4` due to https://github.com/advisories/GHSA-v8gr-m533-ghj9 [#65268](https://github.com/saltstack/salt/issues/65268) +- Upgrade relenv to 0.13.12 to address CVE-2023-4807 [#65316](https://github.com/saltstack/salt/issues/65316) +- Bump to `urllib3==1.26.17` or `urllib3==2.0.6` due to https://github.com/advisories/GHSA-v845-jxx5-vc9f [#65334](https://github.com/saltstack/salt/issues/65334) +- Bump to `gitpython==3.1.37` due to https://github.com/advisories/GHSA-cwvm-v4w8-q58c [#65383](https://github.com/saltstack/salt/issues/65383) + + +## 3005.4 (2023-10-16) + +### Security + +- Fix CVE-2023-34049 by ensuring we do not use a predictable name for the script and correctly check returncode of scp command. + This only impacts salt-ssh users using the pre-flight option. (cve-2023-34049) +- Bump to `cryptography==41.0.4` due to https://github.com/advisories/GHSA-v8gr-m533-ghj9 (#65267) +- Bump to `urllib3==1.26.17` or `urllib3==2.0.6` due to https://github.com/advisories/GHSA-v845-jxx5-vc9f (#65334) +- Bump to `gitpython==3.1.37` due to https://github.com/advisories/GHSA-cwvm-v4w8-q58c (#65383) + + ## Salt v3005.3 (2023-09-14) ### Fixed diff --git a/changelog/64572.fixed.md b/changelog/64572.fixed.md new file mode 100644 index 000000000000..d9916bb29faf --- /dev/null +++ b/changelog/64572.fixed.md @@ -0,0 +1 @@ +Move salt.ufw to correct location /etc/ufw/applications.d/ diff --git a/doc/man/salt-api.1 b/doc/man/salt-api.1 index 1b084240c54a..cfbe64d172ad 100644 --- a/doc/man/salt-api.1 +++ b/doc/man/salt-api.1 @@ -27,7 +27,7 @@ level margin: \\n[rst2man-indent\\n[rst2man-indent-level]] .\" new: \\n[rst2man-indent\\n[rst2man-indent-level]] .in \\n[rst2man-indent\\n[rst2man-indent-level]]u .. -.TH "SALT-API" "1" "Generated on September 06, 2023 at 04:52:57 PM UTC." "3006.3" "Salt" +.TH "SALT-API" "1" "Generated on October 16, 2023 at 05:24:47 PM UTC." "3006.4" "Salt" .SH NAME salt-api \- salt-api Command .sp diff --git a/doc/man/salt-call.1 b/doc/man/salt-call.1 index 56c5c5c83191..2a964330511f 100644 --- a/doc/man/salt-call.1 +++ b/doc/man/salt-call.1 @@ -27,7 +27,7 @@ level margin: \\n[rst2man-indent\\n[rst2man-indent-level]] .\" new: \\n[rst2man-indent\\n[rst2man-indent-level]] .in \\n[rst2man-indent\\n[rst2man-indent-level]]u .. -.TH "SALT-CALL" "1" "Generated on September 06, 2023 at 04:52:57 PM UTC." "3006.3" "Salt" +.TH "SALT-CALL" "1" "Generated on October 16, 2023 at 05:24:47 PM UTC." "3006.4" "Salt" .SH NAME salt-call \- salt-call Documentation .SH SYNOPSIS diff --git a/doc/man/salt-cloud.1 b/doc/man/salt-cloud.1 index 757a3e1eb9d7..e3866a63e31e 100644 --- a/doc/man/salt-cloud.1 +++ b/doc/man/salt-cloud.1 @@ -27,7 +27,7 @@ level margin: \\n[rst2man-indent\\n[rst2man-indent-level]] .\" new: \\n[rst2man-indent\\n[rst2man-indent-level]] .in \\n[rst2man-indent\\n[rst2man-indent-level]]u .. -.TH "SALT-CLOUD" "1" "Generated on September 06, 2023 at 04:52:57 PM UTC." "3006.3" "Salt" +.TH "SALT-CLOUD" "1" "Generated on October 16, 2023 at 05:24:47 PM UTC." "3006.4" "Salt" .SH NAME salt-cloud \- Salt Cloud Command .sp diff --git a/doc/man/salt-cp.1 b/doc/man/salt-cp.1 index e3386d6d343e..0ad964aaf7b5 100644 --- a/doc/man/salt-cp.1 +++ b/doc/man/salt-cp.1 @@ -27,7 +27,7 @@ level margin: \\n[rst2man-indent\\n[rst2man-indent-level]] .\" new: \\n[rst2man-indent\\n[rst2man-indent-level]] .in \\n[rst2man-indent\\n[rst2man-indent-level]]u .. -.TH "SALT-CP" "1" "Generated on September 06, 2023 at 04:52:57 PM UTC." "3006.3" "Salt" +.TH "SALT-CP" "1" "Generated on October 16, 2023 at 05:24:47 PM UTC." "3006.4" "Salt" .SH NAME salt-cp \- salt-cp Documentation .sp diff --git a/doc/man/salt-key.1 b/doc/man/salt-key.1 index 7aca0353f865..913c2cf5b9ed 100644 --- a/doc/man/salt-key.1 +++ b/doc/man/salt-key.1 @@ -27,7 +27,7 @@ level margin: \\n[rst2man-indent\\n[rst2man-indent-level]] .\" new: \\n[rst2man-indent\\n[rst2man-indent-level]] .in \\n[rst2man-indent\\n[rst2man-indent-level]]u .. -.TH "SALT-KEY" "1" "Generated on September 06, 2023 at 04:52:57 PM UTC." "3006.3" "Salt" +.TH "SALT-KEY" "1" "Generated on October 16, 2023 at 05:24:47 PM UTC." "3006.4" "Salt" .SH NAME salt-key \- salt-key Documentation .SH SYNOPSIS diff --git a/doc/man/salt-master.1 b/doc/man/salt-master.1 index 1762910298ed..b8bd9056ff33 100644 --- a/doc/man/salt-master.1 +++ b/doc/man/salt-master.1 @@ -27,7 +27,7 @@ level margin: \\n[rst2man-indent\\n[rst2man-indent-level]] .\" new: \\n[rst2man-indent\\n[rst2man-indent-level]] .in \\n[rst2man-indent\\n[rst2man-indent-level]]u .. -.TH "SALT-MASTER" "1" "Generated on September 06, 2023 at 04:52:57 PM UTC." "3006.3" "Salt" +.TH "SALT-MASTER" "1" "Generated on October 16, 2023 at 05:24:47 PM UTC." "3006.4" "Salt" .SH NAME salt-master \- salt-master Documentation .sp diff --git a/doc/man/salt-minion.1 b/doc/man/salt-minion.1 index 62aeafada86e..5fb106bf3f29 100644 --- a/doc/man/salt-minion.1 +++ b/doc/man/salt-minion.1 @@ -27,7 +27,7 @@ level margin: \\n[rst2man-indent\\n[rst2man-indent-level]] .\" new: \\n[rst2man-indent\\n[rst2man-indent-level]] .in \\n[rst2man-indent\\n[rst2man-indent-level]]u .. -.TH "SALT-MINION" "1" "Generated on September 06, 2023 at 04:52:57 PM UTC." "3006.3" "Salt" +.TH "SALT-MINION" "1" "Generated on October 16, 2023 at 05:24:47 PM UTC." "3006.4" "Salt" .SH NAME salt-minion \- salt-minion Documentation .sp diff --git a/doc/man/salt-proxy.1 b/doc/man/salt-proxy.1 index 1475d70de9e0..97fdfa6c6ae5 100644 --- a/doc/man/salt-proxy.1 +++ b/doc/man/salt-proxy.1 @@ -27,7 +27,7 @@ level margin: \\n[rst2man-indent\\n[rst2man-indent-level]] .\" new: \\n[rst2man-indent\\n[rst2man-indent-level]] .in \\n[rst2man-indent\\n[rst2man-indent-level]]u .. -.TH "SALT-PROXY" "1" "Generated on September 06, 2023 at 04:52:57 PM UTC." "3006.3" "Salt" +.TH "SALT-PROXY" "1" "Generated on October 16, 2023 at 05:24:47 PM UTC." "3006.4" "Salt" .SH NAME salt-proxy \- salt-proxy Documentation .sp diff --git a/doc/man/salt-run.1 b/doc/man/salt-run.1 index 0efc3d39e35f..6954234ae0c0 100644 --- a/doc/man/salt-run.1 +++ b/doc/man/salt-run.1 @@ -27,7 +27,7 @@ level margin: \\n[rst2man-indent\\n[rst2man-indent-level]] .\" new: \\n[rst2man-indent\\n[rst2man-indent-level]] .in \\n[rst2man-indent\\n[rst2man-indent-level]]u .. -.TH "SALT-RUN" "1" "Generated on September 06, 2023 at 04:52:57 PM UTC." "3006.3" "Salt" +.TH "SALT-RUN" "1" "Generated on October 16, 2023 at 05:24:47 PM UTC." "3006.4" "Salt" .SH NAME salt-run \- salt-run Documentation .sp diff --git a/doc/man/salt-ssh.1 b/doc/man/salt-ssh.1 index 743f004ec500..b085a6a6cde6 100644 --- a/doc/man/salt-ssh.1 +++ b/doc/man/salt-ssh.1 @@ -27,7 +27,7 @@ level margin: \\n[rst2man-indent\\n[rst2man-indent-level]] .\" new: \\n[rst2man-indent\\n[rst2man-indent-level]] .in \\n[rst2man-indent\\n[rst2man-indent-level]]u .. -.TH "SALT-SSH" "1" "Generated on September 06, 2023 at 04:52:57 PM UTC." "3006.3" "Salt" +.TH "SALT-SSH" "1" "Generated on October 16, 2023 at 05:24:47 PM UTC." "3006.4" "Salt" .SH NAME salt-ssh \- salt-ssh Documentation .SH SYNOPSIS diff --git a/doc/man/salt-syndic.1 b/doc/man/salt-syndic.1 index 3a71a02eee7f..5e26b223e349 100644 --- a/doc/man/salt-syndic.1 +++ b/doc/man/salt-syndic.1 @@ -27,7 +27,7 @@ level margin: \\n[rst2man-indent\\n[rst2man-indent-level]] .\" new: \\n[rst2man-indent\\n[rst2man-indent-level]] .in \\n[rst2man-indent\\n[rst2man-indent-level]]u .. -.TH "SALT-SYNDIC" "1" "Generated on September 06, 2023 at 04:52:57 PM UTC." "3006.3" "Salt" +.TH "SALT-SYNDIC" "1" "Generated on October 16, 2023 at 05:24:47 PM UTC." "3006.4" "Salt" .SH NAME salt-syndic \- salt-syndic Documentation .sp diff --git a/doc/man/salt.1 b/doc/man/salt.1 index 08cf0bc7b416..852508174cc9 100644 --- a/doc/man/salt.1 +++ b/doc/man/salt.1 @@ -27,7 +27,7 @@ level margin: \\n[rst2man-indent\\n[rst2man-indent-level]] .\" new: \\n[rst2man-indent\\n[rst2man-indent-level]] .in \\n[rst2man-indent\\n[rst2man-indent-level]]u .. -.TH "SALT" "1" "Generated on September 06, 2023 at 04:52:57 PM UTC." "3006.3" "Salt" +.TH "SALT" "1" "Generated on October 16, 2023 at 05:24:47 PM UTC." "3006.4" "Salt" .SH NAME salt \- salt .SH SYNOPSIS diff --git a/doc/man/salt.7 b/doc/man/salt.7 index 051ad6a0e759..37909803cad1 100644 --- a/doc/man/salt.7 +++ b/doc/man/salt.7 @@ -27,7 +27,7 @@ level margin: \\n[rst2man-indent\\n[rst2man-indent-level]] .\" new: \\n[rst2man-indent\\n[rst2man-indent-level]] .in \\n[rst2man-indent\\n[rst2man-indent-level]]u .. -.TH "SALT" "7" "Generated on September 06, 2023 at 04:52:57 PM UTC." "3006.3" "Salt" +.TH "SALT" "7" "Generated on October 16, 2023 at 05:24:47 PM UTC." "3006.4" "Salt" .SH NAME salt \- Salt Documentation .SH SALT PROJECT @@ -194128,7 +194128,7 @@ Passes through all the parameters described in the \fI\%utils.http.query function\fP: .INDENT 7.0 .TP -.B salt.utils.http.query(url, method=\(aqGET\(aq, params=None, data=None, data_file=None, header_dict=None, header_list=None, header_file=None, username=None, password=None, auth=None, decode=False, decode_type=\(aqauto\(aq, status=False, headers=False, text=False, cookies=None, cookie_jar=None, cookie_format=\(aqlwp\(aq, persist_session=False, session_cookie_jar=None, data_render=False, data_renderer=None, header_render=False, header_renderer=None, template_dict=None, test=False, test_url=None, node=\(aqminion\(aq, port=80, opts=None, backend=None, ca_bundle=None, verify_ssl=None, cert=None, text_out=None, headers_out=None, decode_out=None, stream=False, streaming_callback=None, header_callback=None, handle=False, agent=\(aqSalt/3006.3\(aq, hide_fields=None, raise_error=True, formdata=False, formdata_fieldname=None, formdata_filename=None, decode_body=True, **kwargs) +.B salt.utils.http.query(url, method=\(aqGET\(aq, params=None, data=None, data_file=None, header_dict=None, header_list=None, header_file=None, username=None, password=None, auth=None, decode=False, decode_type=\(aqauto\(aq, status=False, headers=False, text=False, cookies=None, cookie_jar=None, cookie_format=\(aqlwp\(aq, persist_session=False, session_cookie_jar=None, data_render=False, data_renderer=None, header_render=False, header_renderer=None, template_dict=None, test=False, test_url=None, node=\(aqminion\(aq, port=80, opts=None, backend=None, ca_bundle=None, verify_ssl=None, cert=None, text_out=None, headers_out=None, decode_out=None, stream=False, streaming_callback=None, header_callback=None, handle=False, agent=\(aqSalt/3006.4\(aq, hide_fields=None, raise_error=True, formdata=False, formdata_fieldname=None, formdata_filename=None, decode_body=True, **kwargs) Query a resource, and decode the return data .UNINDENT .INDENT 7.0 @@ -457626,7 +457626,7 @@ installed2 .UNINDENT .INDENT 0.0 .TP -.B salt.states.zcbuildout.installed(name, config=\(aqbuildout.cfg\(aq, quiet=False, parts=None, user=None, env=(), buildout_ver=None, test_release=False, distribute=None, new_st=None, offline=False, newest=False, python=\(aq/opt/actions\-runner/_work/salt/salt/.tools\-venvs/py3.10/docs/bin/python\(aq, debug=False, verbose=False, unless=None, onlyif=None, use_vt=False, loglevel=\(aqdebug\(aq, **kwargs) +.B salt.states.zcbuildout.installed(name, config=\(aqbuildout.cfg\(aq, quiet=False, parts=None, user=None, env=(), buildout_ver=None, test_release=False, distribute=None, new_st=None, offline=False, newest=False, python=\(aq/opt/actions\-runner/_work/salt\-priv/salt\-priv/.tools\-venvs/py3.10/docs/bin/python\(aq, debug=False, verbose=False, unless=None, onlyif=None, use_vt=False, loglevel=\(aqdebug\(aq, **kwargs) Install buildout in a specific directory .sp It is a thin wrapper to modules.buildout.buildout @@ -477556,6 +477556,25 @@ Addresses multiple CVEs in Python\(aqs dependencies: \fI\%https://docs.python.or .IP \(bu 2 Update to \fBgitpython>=3.1.32\fP due to \fI\%https://github.com/advisories/GHSA\-pr76\-5cm5\-w9cj\fP \fI\%#64988\fP .UNINDENT +(release\-3006.4)= +.SS Salt 3006.4 release notes +.SS Changelog +.SS Security +.INDENT 0.0 +.IP \(bu 2 +Fix CVE\-2023\-34049 by ensuring we do not use a predictable name for the script and correctly check returncode of scp command. +This only impacts salt\-ssh users using the pre\-flight option. \fI\%#cve\-2023\-34049\fP +.IP \(bu 2 +Update to \fBgitpython>=3.1.35\fP due to \fI\%https://github.com/advisories/GHSA\-wfm5\-v35h\-vwf4\fP and \fI\%https://github.com/advisories/GHSA\-cwvm\-v4w8\-q58c\fP \fI\%#65163\fP +.IP \(bu 2 +Bump to \fBcryptography==41.0.4\fP due to \fI\%https://github.com/advisories/GHSA\-v8gr\-m533\-ghj9\fP \fI\%#65268\fP +.IP \(bu 2 +Upgrade relenv to 0.13.12 to address CVE\-2023\-4807 \fI\%#65316\fP +.IP \(bu 2 +Bump to \fBurllib3==1.26.17\fP or \fBurllib3==2.0.6\fP due to \fI\%https://github.com/advisories/GHSA\-v845\-jxx5\-vc9f\fP \fI\%#65334\fP +.IP \(bu 2 +Bump to \fBgitpython==3.1.37\fP due to \fI\%https://github.com/advisories/GHSA\-cwvm\-v4w8\-q58c\fP \fI\%#65383\fP +.UNINDENT .sp See \fI\%Install a release candidate\fP for more information about installing an RC when one is available. diff --git a/doc/man/spm.1 b/doc/man/spm.1 index 9353636a20b8..686ce512eeba 100644 --- a/doc/man/spm.1 +++ b/doc/man/spm.1 @@ -27,7 +27,7 @@ level margin: \\n[rst2man-indent\\n[rst2man-indent-level]] .\" new: \\n[rst2man-indent\\n[rst2man-indent-level]] .in \\n[rst2man-indent\\n[rst2man-indent-level]]u .. -.TH "SPM" "1" "Generated on September 06, 2023 at 04:52:57 PM UTC." "3006.3" "Salt" +.TH "SPM" "1" "Generated on October 16, 2023 at 05:24:47 PM UTC." "3006.4" "Salt" .SH NAME spm \- Salt Package Manager Command .sp diff --git a/doc/topics/releases/3005.4.rst b/doc/topics/releases/3005.4.rst new file mode 100644 index 000000000000..18f904e74a26 --- /dev/null +++ b/doc/topics/releases/3005.4.rst @@ -0,0 +1,17 @@ +.. _release-3005-4: + +========================= +Salt 3005.4 Release Notes +========================= + +Version 3005.4 is a CVE security fix release for :ref:`3005 `. + + +Security +-------- + +- Fix CVE-2023-34049 by ensuring we do not use a predictable name for the script and correctly check returncode of scp command. + This only impacts salt-ssh users using the pre-flight option. (cve-2023-34049) +- Bump to `cryptography==41.0.4` due to https://github.com/advisories/GHSA-v8gr-m533-ghj9 (#65267) +- Bump to `urllib3==1.26.17` or `urllib3==2.0.6` due to https://github.com/advisories/GHSA-v845-jxx5-vc9f (#65334) +- Bump to `gitpython==3.1.37` due to https://github.com/advisories/GHSA-cwvm-v4w8-q58c (#65383) diff --git a/doc/topics/releases/3006.4.md b/doc/topics/releases/3006.4.md new file mode 100644 index 000000000000..83478f1cc994 --- /dev/null +++ b/doc/topics/releases/3006.4.md @@ -0,0 +1,29 @@ +(release-3006.4)= +# Salt 3006.4 release notes + + + + + + + +## Changelog + +### Security + +- Fix CVE-2023-34049 by ensuring we do not use a predictable name for the script and correctly check returncode of scp command. + This only impacts salt-ssh users using the pre-flight option. [#cve-2023-34049](https://github.com/saltstack/salt/issues/cve-2023-34049) +- Update to `gitpython>=3.1.35` due to https://github.com/advisories/GHSA-wfm5-v35h-vwf4 and https://github.com/advisories/GHSA-cwvm-v4w8-q58c [#65163](https://github.com/saltstack/salt/issues/65163) +- Bump to `cryptography==41.0.4` due to https://github.com/advisories/GHSA-v8gr-m533-ghj9 [#65268](https://github.com/saltstack/salt/issues/65268) +- Upgrade relenv to 0.13.12 to address CVE-2023-4807 [#65316](https://github.com/saltstack/salt/issues/65316) +- Bump to `urllib3==1.26.17` or `urllib3==2.0.6` due to https://github.com/advisories/GHSA-v845-jxx5-vc9f [#65334](https://github.com/saltstack/salt/issues/65334) +- Bump to `gitpython==3.1.37` due to https://github.com/advisories/GHSA-cwvm-v4w8-q58c [#65383](https://github.com/saltstack/salt/issues/65383) diff --git a/doc/topics/releases/templates/3006.4.md.template b/doc/topics/releases/templates/3006.4.md.template new file mode 100644 index 000000000000..a11ede9bf4c8 --- /dev/null +++ b/doc/topics/releases/templates/3006.4.md.template @@ -0,0 +1,14 @@ +(release-3006.4)= +# Salt 3006.4 release notes{{ unreleased }} +{{ warning }} + + + + +## Changelog +{{ changelog }} diff --git a/pkg/debian/changelog b/pkg/debian/changelog index bd27fccf007a..e45d8665d162 100644 --- a/pkg/debian/changelog +++ b/pkg/debian/changelog @@ -1,3 +1,19 @@ +salt (3006.4) stable; urgency=medium + + + # Security + + * Fix CVE-2023-34049 by ensuring we do not use a predictable name for the script and correctly check returncode of scp command. + This only impacts salt*ssh users using the pre-flight option. [#cve-2023-34049](https://github.com/saltstack/salt/issues/cve-2023-34049) + * Update to `gitpython>=3.1.35` due to https://github.com/advisories/GHSA-wfm5-v35h-vwf4 and https://github.com/advisories/GHSA-cwvm-v4w8-q58c [#65163](https://github.com/saltstack/salt/issues/65163) + * Bump to `cryptography==41.0.4` due to https://github.com/advisories/GHSA-v8gr-m533-ghj9 [#65268](https://github.com/saltstack/salt/issues/65268) + * Upgrade relenv to 0.13.12 to address CVE-2023-4807 [#65316](https://github.com/saltstack/salt/issues/65316) + * Bump to `urllib3==1.26.17` or `urllib3==2.0.6` due to https://github.com/advisories/GHSA-v845-jxx5-vc9f [#65334](https://github.com/saltstack/salt/issues/65334) + * Bump to `gitpython==3.1.37` due to https://github.com/advisories/GHSA-cwvm-v4w8-q58c [#65383](https://github.com/saltstack/salt/issues/65383) + + + -- Salt Project Packaging Mon, 16 Oct 2023 17:22:41 +0000 + salt (3006.3) stable; urgency=medium diff --git a/pkg/debian/salt-master.dirs b/pkg/debian/salt-master.dirs index 542db04259fb..aba501b4379e 100644 --- a/pkg/debian/salt-master.dirs +++ b/pkg/debian/salt-master.dirs @@ -1,5 +1,4 @@ /etc/salt/master.d -/etc/ufw/applications.d/salt-master /etc/salt/pki/master/minions /etc/salt/pki/master/minions_autosign /etc/salt/pki/master/minions_denied diff --git a/pkg/debian/salt-master.install b/pkg/debian/salt-master.install index aad95813286b..809b5141b1db 100644 --- a/pkg/debian/salt-master.install +++ b/pkg/debian/salt-master.install @@ -1,2 +1,2 @@ pkg/common/salt-master.service /lib/systemd/system -pkg/common/salt.ufw /etc/ufw/applications.d/salt-master +pkg/common/salt.ufw /etc/ufw/applications.d diff --git a/pkg/rpm/salt.spec b/pkg/rpm/salt.spec index eaeae33320fa..6cc4e85c23c3 100644 --- a/pkg/rpm/salt.spec +++ b/pkg/rpm/salt.spec @@ -31,7 +31,7 @@ %define fish_dir %{_datadir}/fish/vendor_functions.d Name: salt -Version: 3006.3 +Version: 3006.4 Release: 0 Summary: A parallel remote execution system Group: System Environment/Daemons @@ -563,6 +563,19 @@ fi %changelog +* Mon Oct 16 2023 Salt Project Packaging - 3006.4 + +# Security + +- Fix CVE-2023-34049 by ensuring we do not use a predictable name for the script and correctly check returncode of scp command. + This only impacts salt-ssh users using the pre-flight option. [#cve-2023-34049](https://github.com/saltstack/salt/issues/cve-2023-34049) +- Update to `gitpython>=3.1.35` due to https://github.com/advisories/GHSA-wfm5-v35h-vwf4 and https://github.com/advisories/GHSA-cwvm-v4w8-q58c [#65163](https://github.com/saltstack/salt/issues/65163) +- Bump to `cryptography==41.0.4` due to https://github.com/advisories/GHSA-v8gr-m533-ghj9 [#65268](https://github.com/saltstack/salt/issues/65268) +- Upgrade relenv to 0.13.12 to address CVE-2023-4807 [#65316](https://github.com/saltstack/salt/issues/65316) +- Bump to `urllib3==1.26.17` or `urllib3==2.0.6` due to https://github.com/advisories/GHSA-v845-jxx5-vc9f [#65334](https://github.com/saltstack/salt/issues/65334) +- Bump to `gitpython==3.1.37` due to https://github.com/advisories/GHSA-cwvm-v4w8-q58c [#65383](https://github.com/saltstack/salt/issues/65383) + + * Wed Sep 06 2023 Salt Project Packaging - 3006.3 # Removed diff --git a/pkg/tests/integration/test_salt_ufw.py b/pkg/tests/integration/test_salt_ufw.py new file mode 100644 index 000000000000..2164de85c572 --- /dev/null +++ b/pkg/tests/integration/test_salt_ufw.py @@ -0,0 +1,38 @@ +import pathlib + +import pytest + + +@pytest.mark.skip_on_windows +@pytest.mark.skip_if_binaries_missing("ufw") +def test_salt_ufw(salt_master, salt_call_cli, install_salt): + """ + Test salt.ufw for Debian/Ubuntu salt-master + """ + if install_salt.distro_id not in ("debian", "ubuntu"): + pytest.skip("Only tests Debian / Ubuntu packages") + + # check that the salt_master is running + assert salt_master.is_running() + + ufw_master_path = pathlib.Path("/etc/ufw/applications.d/salt.ufw") + assert ufw_master_path.exists() + assert ufw_master_path.is_file() + + ufw_list_cmd = "/usr/sbin/ufw app list" + ret = salt_call_cli.run("--local", "cmd.run", ufw_list_cmd) + assert "Available applications" in ret.stdout + assert "Salt" in ret.stdout + ufw_upd_cmd = "/usr/sbin/ufw app update Salt" + ret = salt_call_cli.run("--local", "cmd.run", ufw_upd_cmd) + assert ret.returncode == 0 + expected_info = """Profile: Salt +Title: salt +Description: fast and powerful configuration management and remote +execution + +Ports: + 4505,4506/tcp""" + ufw_info_cmd = "/usr/sbin/ufw app info Salt" + ret = salt_call_cli.run("--local", "cmd.run", ufw_info_cmd) + assert expected_info in ret.data diff --git a/requirements/darwin.txt b/requirements/darwin.txt index 0a1f727066a1..841a6ed03a38 100644 --- a/requirements/darwin.txt +++ b/requirements/darwin.txt @@ -6,7 +6,7 @@ apache-libcloud>=2.4.0 cherrypy>=17.4.1 gitpython>=3.1.35 cryptography>=41.0.3 -gitpython>=3.1.30 +gitpython>=3.1.37 idna>=2.8 linode-python>=1.1.1 pyasn1>=0.4.8 diff --git a/requirements/static/ci/common.in b/requirements/static/ci/common.in index 9cb531ad9d5e..9d56e995c98c 100644 --- a/requirements/static/ci/common.in +++ b/requirements/static/ci/common.in @@ -15,7 +15,7 @@ clustershell croniter>=0.3.0,!=0.3.22"; sys_platform != 'win32' dnspython etcd3-py==0.1.6 -gitpython>=3.1.35 +gitpython>=3.1.37 jmespath jsonschema junos-eznc; sys_platform != 'win32' diff --git a/requirements/static/ci/py3.10/cloud.txt b/requirements/static/ci/py3.10/cloud.txt index eae62e34bdcd..c872cedd0ddf 100644 --- a/requirements/static/ci/py3.10/cloud.txt +++ b/requirements/static/ci/py3.10/cloud.txt @@ -67,7 +67,7 @@ smbprotocol==1.10.1 # via # -r requirements/static/ci/cloud.in # pypsexec -urllib3==1.26.14 +urllib3==1.26.18 # via # -c requirements/static/ci/py3.10/linux.txt # requests diff --git a/requirements/static/ci/py3.10/darwin.txt b/requirements/static/ci/py3.10/darwin.txt index 2c009ac2623b..5eaceba5c113 100644 --- a/requirements/static/ci/py3.10/darwin.txt +++ b/requirements/static/ci/py3.10/darwin.txt @@ -140,7 +140,7 @@ gitdb==4.0.10 # via # -c requirements/static/ci/../pkg/py3.10/darwin.txt # gitpython -gitpython==3.1.35 +gitpython==3.1.40 # via # -c requirements/static/ci/../pkg/py3.10/darwin.txt # -r requirements/darwin.txt @@ -480,7 +480,7 @@ six==1.16.0 # transitions # vcert # websocket-client -smmap==3.0.2 +smmap==5.0.0 # via # -c requirements/static/ci/../pkg/py3.10/darwin.txt # gitdb @@ -519,14 +519,14 @@ ttp==0.9.5 # ttp-templates types-pyyaml==6.0.1 # via responses -typing-extensions==4.6.3 +typing-extensions==4.8.0 # via # -c requirements/static/ci/../pkg/py3.10/darwin.txt # napalm # pydantic # pytest-shell-utilities # pytest-system-statistics -urllib3==1.26.14 +urllib3==1.26.18 # via # -c requirements/static/ci/../pkg/py3.10/darwin.txt # botocore diff --git a/requirements/static/ci/py3.10/docs.txt b/requirements/static/ci/py3.10/docs.txt index fc0200f58f1b..6b259aeacac6 100644 --- a/requirements/static/ci/py3.10/docs.txt +++ b/requirements/static/ci/py3.10/docs.txt @@ -146,13 +146,13 @@ tempora==5.3.0 # via # -c requirements/static/ci/py3.10/linux.txt # portend -typing-extensions==4.6.3 +typing-extensions==4.8.0 # via # -c requirements/static/ci/py3.10/linux.txt # pydantic uc-micro-py==1.0.2 # via linkify-it-py -urllib3==1.26.14 +urllib3==1.26.18 # via # -c requirements/static/ci/py3.10/linux.txt # requests diff --git a/requirements/static/ci/py3.10/freebsd.txt b/requirements/static/ci/py3.10/freebsd.txt index 8679b6344497..1e82d9cc718b 100644 --- a/requirements/static/ci/py3.10/freebsd.txt +++ b/requirements/static/ci/py3.10/freebsd.txt @@ -135,7 +135,7 @@ geomet==0.2.1.post1 # via cassandra-driver gitdb==4.0.10 # via gitpython -gitpython==3.1.35 +gitpython==3.1.40 # via -r requirements/static/ci/common.in google-auth==2.19.1 # via kubernetes @@ -509,14 +509,14 @@ ttp==0.9.5 # ttp-templates types-pyyaml==6.0.1 # via responses -typing-extensions==4.6.3 +typing-extensions==4.8.0 # via # -c requirements/static/ci/../pkg/py3.10/freebsd.txt # napalm # pydantic # pytest-shell-utilities # pytest-system-statistics -urllib3==1.26.14 +urllib3==1.26.18 # via # -c requirements/static/ci/../pkg/py3.10/freebsd.txt # botocore diff --git a/requirements/static/ci/py3.10/linux.txt b/requirements/static/ci/py3.10/linux.txt index d4947890ae6c..e135bf843ce0 100644 --- a/requirements/static/ci/py3.10/linux.txt +++ b/requirements/static/ci/py3.10/linux.txt @@ -151,7 +151,7 @@ geomet==0.2.1.post1 # via cassandra-driver gitdb==4.0.10 # via gitpython -gitpython==3.1.35 +gitpython==3.1.40 # via -r requirements/static/ci/common.in google-auth==2.19.1 # via kubernetes @@ -573,14 +573,14 @@ twilio==8.2.2 # via -r requirements/static/ci/linux.in types-pyyaml==6.0.1 # via responses -typing-extensions==4.6.3 +typing-extensions==4.8.0 # via # -c requirements/static/ci/../pkg/py3.10/linux.txt # napalm # pydantic # pytest-shell-utilities # pytest-system-statistics -urllib3==1.26.14 +urllib3==1.26.18 # via # -c requirements/static/ci/../pkg/py3.10/linux.txt # botocore diff --git a/requirements/static/ci/py3.10/tools.txt b/requirements/static/ci/py3.10/tools.txt index e3ab1ce93815..6eb18846050c 100644 --- a/requirements/static/ci/py3.10/tools.txt +++ b/requirements/static/ci/py3.10/tools.txt @@ -52,9 +52,9 @@ s3transfer==0.6.1 # via boto3 six==1.16.0 # via python-dateutil -typing-extensions==4.6.3 +typing-extensions==4.8.0 # via python-tools-scripts -urllib3==1.26.14 +urllib3==1.26.18 # via # botocore # requests diff --git a/requirements/static/ci/py3.10/windows.txt b/requirements/static/ci/py3.10/windows.txt index 16e3aa1cbf40..50ccbe99a82f 100644 --- a/requirements/static/ci/py3.10/windows.txt +++ b/requirements/static/ci/py3.10/windows.txt @@ -133,7 +133,7 @@ gitdb==4.0.10 # via # -c requirements/static/ci/../pkg/py3.10/windows.txt # gitpython -gitpython==3.1.35 +gitpython==3.1.40 # via # -c requirements/static/ci/../pkg/py3.10/windows.txt # -r requirements/static/ci/common.in @@ -434,7 +434,7 @@ six==1.15.0 # pyvmomi # pywinrm # websocket-client -smmap==4.0.0 +smmap==5.0.0 # via # -c requirements/static/ci/../pkg/py3.10/windows.txt # gitdb @@ -460,13 +460,13 @@ tornado==6.3.3 # -r requirements/base.txt types-pyyaml==6.0.1 # via responses -typing-extensions==4.6.3 +typing-extensions==4.8.0 # via # -c requirements/static/ci/../pkg/py3.10/windows.txt # pydantic # pytest-shell-utilities # pytest-system-statistics -urllib3==1.26.14 +urllib3==1.26.18 # via # -c requirements/static/ci/../pkg/py3.10/windows.txt # -r requirements/windows.txt diff --git a/requirements/static/ci/py3.11/cloud.txt b/requirements/static/ci/py3.11/cloud.txt index 69b47df8fecf..6f51f688fe78 100644 --- a/requirements/static/ci/py3.11/cloud.txt +++ b/requirements/static/ci/py3.11/cloud.txt @@ -67,7 +67,7 @@ smbprotocol==1.10.1 # via # -r requirements/static/ci/cloud.in # pypsexec -urllib3==1.26.14 +urllib3==1.26.18 # via # -c requirements/static/ci/py3.11/linux.txt # requests diff --git a/requirements/static/ci/py3.11/darwin.txt b/requirements/static/ci/py3.11/darwin.txt index 4a596e9d465f..1da2293b513f 100644 --- a/requirements/static/ci/py3.11/darwin.txt +++ b/requirements/static/ci/py3.11/darwin.txt @@ -138,7 +138,7 @@ gitdb==4.0.10 # via # -c requirements/static/ci/../pkg/py3.11/darwin.txt # gitpython -gitpython==3.1.35 +gitpython==3.1.40 # via # -c requirements/static/ci/../pkg/py3.11/darwin.txt # -r requirements/darwin.txt @@ -476,7 +476,7 @@ six==1.16.0 # transitions # vcert # websocket-client -smmap==3.0.2 +smmap==5.0.0 # via # -c requirements/static/ci/../pkg/py3.11/darwin.txt # gitdb @@ -513,7 +513,7 @@ ttp==0.9.5 # ttp-templates types-pyyaml==6.0.1 # via responses -typing-extensions==4.6.3 +typing-extensions==4.8.0 # via # -c requirements/static/ci/../pkg/py3.11/darwin.txt # napalm diff --git a/requirements/static/ci/py3.11/docs.txt b/requirements/static/ci/py3.11/docs.txt index db014fac0b38..8f207e03fe96 100644 --- a/requirements/static/ci/py3.11/docs.txt +++ b/requirements/static/ci/py3.11/docs.txt @@ -146,13 +146,13 @@ tempora==5.3.0 # via # -c requirements/static/ci/py3.11/linux.txt # portend -typing-extensions==4.6.3 +typing-extensions==4.8.0 # via # -c requirements/static/ci/py3.11/linux.txt # pydantic uc-micro-py==1.0.2 # via linkify-it-py -urllib3==1.26.14 +urllib3==1.26.18 # via # -c requirements/static/ci/py3.11/linux.txt # requests diff --git a/requirements/static/ci/py3.11/freebsd.txt b/requirements/static/ci/py3.11/freebsd.txt index 9e0748b02d65..23c75e019b91 100644 --- a/requirements/static/ci/py3.11/freebsd.txt +++ b/requirements/static/ci/py3.11/freebsd.txt @@ -133,7 +133,7 @@ geomet==0.2.1.post1 # via cassandra-driver gitdb==4.0.10 # via gitpython -gitpython==3.1.35 +gitpython==3.1.40 # via -r requirements/static/ci/common.in google-auth==2.19.1 # via kubernetes @@ -505,14 +505,14 @@ ttp==0.9.5 # ttp-templates types-pyyaml==6.0.1 # via responses -typing-extensions==4.6.3 +typing-extensions==4.8.0 # via # -c requirements/static/ci/../pkg/py3.11/freebsd.txt # napalm # pydantic # pytest-shell-utilities # pytest-system-statistics -urllib3==1.26.14 +urllib3==1.26.18 # via # -c requirements/static/ci/../pkg/py3.11/freebsd.txt # botocore diff --git a/requirements/static/ci/py3.11/linux.txt b/requirements/static/ci/py3.11/linux.txt index ea1d14dcb367..c49a5203b30b 100644 --- a/requirements/static/ci/py3.11/linux.txt +++ b/requirements/static/ci/py3.11/linux.txt @@ -147,7 +147,7 @@ geomet==0.2.1.post1 # via cassandra-driver gitdb==4.0.10 # via gitpython -gitpython==3.1.35 +gitpython==3.1.40 # via -r requirements/static/ci/common.in google-auth==2.19.1 # via kubernetes @@ -567,14 +567,14 @@ twilio==8.2.2 # via -r requirements/static/ci/linux.in types-pyyaml==6.0.1 # via responses -typing-extensions==4.6.3 +typing-extensions==4.8.0 # via # -c requirements/static/ci/../pkg/py3.11/linux.txt # napalm # pydantic # pytest-shell-utilities # pytest-system-statistics -urllib3==1.26.14 +urllib3==1.26.18 # via # -c requirements/static/ci/../pkg/py3.11/linux.txt # botocore diff --git a/requirements/static/ci/py3.11/tools.txt b/requirements/static/ci/py3.11/tools.txt index b95fb4532bbf..50685cb72c31 100644 --- a/requirements/static/ci/py3.11/tools.txt +++ b/requirements/static/ci/py3.11/tools.txt @@ -52,7 +52,7 @@ s3transfer==0.6.1 # via boto3 six==1.16.0 # via python-dateutil -urllib3==1.26.14 +urllib3==1.26.18 # via # botocore # requests diff --git a/requirements/static/ci/py3.11/windows.txt b/requirements/static/ci/py3.11/windows.txt index b87b69665f68..5829b45296c2 100644 --- a/requirements/static/ci/py3.11/windows.txt +++ b/requirements/static/ci/py3.11/windows.txt @@ -131,7 +131,7 @@ gitdb==4.0.10 # via # -c requirements/static/ci/../pkg/py3.11/windows.txt # gitpython -gitpython==3.1.35 +gitpython==3.1.40 # via # -c requirements/static/ci/../pkg/py3.11/windows.txt # -r requirements/static/ci/common.in @@ -432,7 +432,7 @@ six==1.15.0 # pyvmomi # pywinrm # websocket-client -smmap==4.0.0 +smmap==5.0.0 # via # -c requirements/static/ci/../pkg/py3.11/windows.txt # gitdb @@ -456,13 +456,13 @@ tornado==6.3.3 # -r requirements/base.txt types-pyyaml==6.0.1 # via responses -typing-extensions==4.6.3 +typing-extensions==4.8.0 # via # -c requirements/static/ci/../pkg/py3.11/windows.txt # pydantic # pytest-shell-utilities # pytest-system-statistics -urllib3==1.26.14 +urllib3==1.26.18 # via # -c requirements/static/ci/../pkg/py3.11/windows.txt # -r requirements/windows.txt diff --git a/requirements/static/ci/py3.8/cloud.txt b/requirements/static/ci/py3.8/cloud.txt index 02d0dffce3b4..d4b0c1552250 100644 --- a/requirements/static/ci/py3.8/cloud.txt +++ b/requirements/static/ci/py3.8/cloud.txt @@ -67,7 +67,7 @@ smbprotocol==1.10.1 # via # -r requirements/static/ci/cloud.in # pypsexec -urllib3==1.26.14 +urllib3==1.26.18 # via # -c requirements/static/ci/py3.8/linux.txt # requests diff --git a/requirements/static/ci/py3.8/docs.txt b/requirements/static/ci/py3.8/docs.txt index 6de6d8963533..56d0e0df50eb 100644 --- a/requirements/static/ci/py3.8/docs.txt +++ b/requirements/static/ci/py3.8/docs.txt @@ -155,13 +155,13 @@ tempora==5.3.0 # via # -c requirements/static/ci/py3.8/linux.txt # portend -typing-extensions==4.6.3 +typing-extensions==4.8.0 # via # -c requirements/static/ci/py3.8/linux.txt # pydantic uc-micro-py==1.0.2 # via linkify-it-py -urllib3==1.26.14 +urllib3==1.26.18 # via # -c requirements/static/ci/py3.8/linux.txt # requests diff --git a/requirements/static/ci/py3.8/freebsd.txt b/requirements/static/ci/py3.8/freebsd.txt index 7e3ed54ec5d5..dd2ea129e1da 100644 --- a/requirements/static/ci/py3.8/freebsd.txt +++ b/requirements/static/ci/py3.8/freebsd.txt @@ -135,7 +135,7 @@ geomet==0.2.1.post1 # via cassandra-driver gitdb==4.0.10 # via gitpython -gitpython==3.1.35 +gitpython==3.1.40 # via -r requirements/static/ci/common.in google-auth==2.19.1 # via kubernetes @@ -513,14 +513,14 @@ ttp==0.9.5 # ttp-templates types-pyyaml==6.0.1 # via responses -typing-extensions==4.6.3 +typing-extensions==4.8.0 # via # -c requirements/static/ci/../pkg/py3.8/freebsd.txt # napalm # pydantic # pytest-shell-utilities # pytest-system-statistics -urllib3==1.26.14 +urllib3==1.26.18 # via # -c requirements/static/ci/../pkg/py3.8/freebsd.txt # botocore diff --git a/requirements/static/ci/py3.8/linux.txt b/requirements/static/ci/py3.8/linux.txt index cf85eddf7862..bf777549034c 100644 --- a/requirements/static/ci/py3.8/linux.txt +++ b/requirements/static/ci/py3.8/linux.txt @@ -151,7 +151,7 @@ geomet==0.2.1.post1 # via cassandra-driver gitdb==4.0.10 # via gitpython -gitpython==3.1.35 +gitpython==3.1.40 # via -r requirements/static/ci/common.in google-auth==2.19.1 # via kubernetes @@ -577,14 +577,14 @@ twilio==8.2.2 # via -r requirements/static/ci/linux.in types-pyyaml==6.0.1 # via responses -typing-extensions==4.6.3 +typing-extensions==4.8.0 # via # -c requirements/static/ci/../pkg/py3.8/linux.txt # napalm # pydantic # pytest-shell-utilities # pytest-system-statistics -urllib3==1.26.14 +urllib3==1.26.18 # via # -c requirements/static/ci/../pkg/py3.8/linux.txt # botocore diff --git a/requirements/static/ci/py3.8/windows.txt b/requirements/static/ci/py3.8/windows.txt index 61692dc740c5..c28338068972 100644 --- a/requirements/static/ci/py3.8/windows.txt +++ b/requirements/static/ci/py3.8/windows.txt @@ -133,7 +133,7 @@ gitdb==4.0.10 # via # -c requirements/static/ci/../pkg/py3.8/windows.txt # gitpython -gitpython==3.1.35 +gitpython==3.1.40 # via # -c requirements/static/ci/../pkg/py3.8/windows.txt # -r requirements/static/ci/common.in @@ -439,7 +439,7 @@ six==1.15.0 # pyvmomi # pywinrm # websocket-client -smmap==4.0.0 +smmap==5.0.0 # via # -c requirements/static/ci/../pkg/py3.8/windows.txt # gitdb @@ -465,13 +465,13 @@ tornado==6.3.3 # -r requirements/base.txt types-pyyaml==6.0.1 # via responses -typing-extensions==4.6.3 +typing-extensions==4.8.0 # via # -c requirements/static/ci/../pkg/py3.8/windows.txt # pydantic # pytest-shell-utilities # pytest-system-statistics -urllib3==1.26.14 +urllib3==1.26.18 # via # -c requirements/static/ci/../pkg/py3.8/windows.txt # -r requirements/windows.txt diff --git a/requirements/static/ci/py3.9/cloud.txt b/requirements/static/ci/py3.9/cloud.txt index 9680743a5076..edaa86a90152 100644 --- a/requirements/static/ci/py3.9/cloud.txt +++ b/requirements/static/ci/py3.9/cloud.txt @@ -67,7 +67,7 @@ smbprotocol==1.10.1 # via # -r requirements/static/ci/cloud.in # pypsexec -urllib3==1.26.14 +urllib3==1.26.18 # via # -c requirements/static/ci/py3.9/linux.txt # requests diff --git a/requirements/static/ci/py3.9/darwin.txt b/requirements/static/ci/py3.9/darwin.txt index 81bf5659f3d1..98c588d360c6 100644 --- a/requirements/static/ci/py3.9/darwin.txt +++ b/requirements/static/ci/py3.9/darwin.txt @@ -140,7 +140,7 @@ gitdb==4.0.10 # via # -c requirements/static/ci/../pkg/py3.9/darwin.txt # gitpython -gitpython==3.1.35 +gitpython==3.1.40 # via # -c requirements/static/ci/../pkg/py3.9/darwin.txt # -r requirements/darwin.txt @@ -480,7 +480,7 @@ six==1.16.0 # transitions # vcert # websocket-client -smmap==3.0.2 +smmap==5.0.0 # via # -c requirements/static/ci/../pkg/py3.9/darwin.txt # gitdb @@ -519,14 +519,14 @@ ttp==0.9.5 # ttp-templates types-pyyaml==6.0.1 # via responses -typing-extensions==4.6.3 +typing-extensions==4.8.0 # via # -c requirements/static/ci/../pkg/py3.9/darwin.txt # napalm # pydantic # pytest-shell-utilities # pytest-system-statistics -urllib3==1.26.14 +urllib3==1.26.18 # via # -c requirements/static/ci/../pkg/py3.9/darwin.txt # botocore diff --git a/requirements/static/ci/py3.9/docs.txt b/requirements/static/ci/py3.9/docs.txt index c66a6e72f4df..286e6b441d4d 100644 --- a/requirements/static/ci/py3.9/docs.txt +++ b/requirements/static/ci/py3.9/docs.txt @@ -150,13 +150,13 @@ tempora==5.3.0 # via # -c requirements/static/ci/py3.9/linux.txt # portend -typing-extensions==4.6.3 +typing-extensions==4.8.0 # via # -c requirements/static/ci/py3.9/linux.txt # pydantic uc-micro-py==1.0.2 # via linkify-it-py -urllib3==1.26.14 +urllib3==1.26.18 # via # -c requirements/static/ci/py3.9/linux.txt # requests diff --git a/requirements/static/ci/py3.9/freebsd.txt b/requirements/static/ci/py3.9/freebsd.txt index 2fef8a457139..e0aa21e0a9c7 100644 --- a/requirements/static/ci/py3.9/freebsd.txt +++ b/requirements/static/ci/py3.9/freebsd.txt @@ -135,7 +135,7 @@ geomet==0.2.1.post1 # via cassandra-driver gitdb==4.0.10 # via gitpython -gitpython==3.1.35 +gitpython==3.1.40 # via -r requirements/static/ci/common.in google-auth==2.19.1 # via kubernetes @@ -509,14 +509,14 @@ ttp==0.9.5 # ttp-templates types-pyyaml==6.0.1 # via responses -typing-extensions==4.6.3 +typing-extensions==4.8.0 # via # -c requirements/static/ci/../pkg/py3.9/freebsd.txt # napalm # pydantic # pytest-shell-utilities # pytest-system-statistics -urllib3==1.26.14 +urllib3==1.26.18 # via # -c requirements/static/ci/../pkg/py3.9/freebsd.txt # botocore diff --git a/requirements/static/ci/py3.9/linux.txt b/requirements/static/ci/py3.9/linux.txt index 20e1779d6706..90e455216b7c 100644 --- a/requirements/static/ci/py3.9/linux.txt +++ b/requirements/static/ci/py3.9/linux.txt @@ -151,7 +151,7 @@ geomet==0.2.1.post1 # via cassandra-driver gitdb==4.0.10 # via gitpython -gitpython==3.1.35 +gitpython==3.1.40 # via -r requirements/static/ci/common.in google-auth==2.19.1 # via kubernetes @@ -575,14 +575,14 @@ twilio==8.2.2 # via -r requirements/static/ci/linux.in types-pyyaml==6.0.1 # via responses -typing-extensions==4.6.3 +typing-extensions==4.8.0 # via # -c requirements/static/ci/../pkg/py3.9/linux.txt # napalm # pydantic # pytest-shell-utilities # pytest-system-statistics -urllib3==1.26.14 +urllib3==1.26.18 # via # -c requirements/static/ci/../pkg/py3.9/linux.txt # botocore diff --git a/requirements/static/ci/py3.9/tools.txt b/requirements/static/ci/py3.9/tools.txt index 3d0c679aaf5e..f5ce6c7954d9 100644 --- a/requirements/static/ci/py3.9/tools.txt +++ b/requirements/static/ci/py3.9/tools.txt @@ -52,9 +52,9 @@ s3transfer==0.6.1 # via boto3 six==1.16.0 # via python-dateutil -typing-extensions==4.6.3 +typing-extensions==4.8.0 # via python-tools-scripts -urllib3==1.26.14 +urllib3==1.26.18 # via # botocore # requests diff --git a/requirements/static/ci/py3.9/windows.txt b/requirements/static/ci/py3.9/windows.txt index db9f51e024f9..ff156b3285ee 100644 --- a/requirements/static/ci/py3.9/windows.txt +++ b/requirements/static/ci/py3.9/windows.txt @@ -133,7 +133,7 @@ gitdb==4.0.10 # via # -c requirements/static/ci/../pkg/py3.9/windows.txt # gitpython -gitpython==3.1.35 +gitpython==3.1.40 # via # -c requirements/static/ci/../pkg/py3.9/windows.txt # -r requirements/static/ci/common.in @@ -435,7 +435,7 @@ six==1.15.0 # pyvmomi # pywinrm # websocket-client -smmap==4.0.0 +smmap==5.0.0 # via # -c requirements/static/ci/../pkg/py3.9/windows.txt # gitdb @@ -461,13 +461,13 @@ tornado==6.3.3 # -r requirements/base.txt types-pyyaml==6.0.1 # via responses -typing-extensions==4.6.3 +typing-extensions==4.8.0 # via # -c requirements/static/ci/../pkg/py3.9/windows.txt # pydantic # pytest-shell-utilities # pytest-system-statistics -urllib3==1.26.14 +urllib3==1.26.18 # via # -c requirements/static/ci/../pkg/py3.9/windows.txt # -r requirements/windows.txt diff --git a/requirements/static/pkg/py3.10/darwin.txt b/requirements/static/pkg/py3.10/darwin.txt index 0346f0cee588..729320f336c3 100644 --- a/requirements/static/pkg/py3.10/darwin.txt +++ b/requirements/static/pkg/py3.10/darwin.txt @@ -29,7 +29,7 @@ distro==1.8.0 # via -r requirements/base.txt gitdb==4.0.10 # via gitpython -gitpython==3.1.35 +gitpython==3.1.40 # via -r requirements/darwin.txt idna==3.4 # via @@ -105,7 +105,7 @@ setproctitle==1.3.2 # via -r requirements/darwin.txt six==1.16.0 # via python-dateutil -smmap==3.0.2 +smmap==5.0.0 # via gitdb tempora==5.3.0 # via portend @@ -113,9 +113,9 @@ timelib==0.3.0 # via -r requirements/darwin.txt tornado==6.3.3 # via -r requirements/base.txt -typing-extensions==4.6.3 +typing-extensions==4.8.0 # via pydantic -urllib3==1.26.14 +urllib3==1.26.18 # via requests vultr==1.0.1 # via -r requirements/darwin.txt diff --git a/requirements/static/pkg/py3.10/freebsd.txt b/requirements/static/pkg/py3.10/freebsd.txt index bbebd9e46afd..650b425e99e4 100644 --- a/requirements/static/pkg/py3.10/freebsd.txt +++ b/requirements/static/pkg/py3.10/freebsd.txt @@ -100,9 +100,9 @@ timelib==0.3.0 # via -r requirements/static/pkg/freebsd.in tornado==6.3.3 # via -r requirements/base.txt -typing-extensions==4.6.3 +typing-extensions==4.8.0 # via pydantic -urllib3==1.26.14 +urllib3==1.26.18 # via requests zc.lockfile==3.0.post1 # via cherrypy diff --git a/requirements/static/pkg/py3.10/linux.txt b/requirements/static/pkg/py3.10/linux.txt index 0b460b9c2267..995f431042e3 100644 --- a/requirements/static/pkg/py3.10/linux.txt +++ b/requirements/static/pkg/py3.10/linux.txt @@ -100,9 +100,9 @@ timelib==0.3.0 # via -r requirements/static/pkg/linux.in tornado==6.3.3 # via -r requirements/base.txt -typing-extensions==4.6.3 +typing-extensions==4.8.0 # via pydantic -urllib3==1.26.14 +urllib3==1.26.18 # via requests zc.lockfile==3.0.post1 # via cherrypy diff --git a/requirements/static/pkg/py3.10/windows.txt b/requirements/static/pkg/py3.10/windows.txt index 33aebdf33dfc..3340f3788296 100644 --- a/requirements/static/pkg/py3.10/windows.txt +++ b/requirements/static/pkg/py3.10/windows.txt @@ -34,7 +34,7 @@ distro==1.8.0 # via -r requirements/base.txt gitdb==4.0.10 # via gitpython -gitpython==3.1.35 +gitpython==3.1.40 # via -r requirements/windows.txt idna==3.4 # via requests @@ -119,7 +119,7 @@ setproctitle==1.3.2 # via -r requirements/windows.txt six==1.15.0 # via python-dateutil -smmap==4.0.0 +smmap==5.0.0 # via gitdb tempora==5.3.0 # via portend @@ -127,9 +127,9 @@ timelib==0.3.0 # via -r requirements/windows.txt tornado==6.3.3 # via -r requirements/base.txt -typing-extensions==4.6.3 +typing-extensions==4.8.0 # via pydantic -urllib3==1.26.14 +urllib3==1.26.18 # via # -r requirements/windows.txt # requests diff --git a/requirements/static/pkg/py3.11/darwin.txt b/requirements/static/pkg/py3.11/darwin.txt index c8a9a99f9a84..a896f3ce2662 100644 --- a/requirements/static/pkg/py3.11/darwin.txt +++ b/requirements/static/pkg/py3.11/darwin.txt @@ -29,7 +29,7 @@ distro==1.8.0 # via -r requirements/base.txt gitdb==4.0.10 # via gitpython -gitpython==3.1.35 +gitpython==3.1.40 # via -r requirements/darwin.txt idna==3.4 # via @@ -105,7 +105,7 @@ setproctitle==1.3.2 # via -r requirements/darwin.txt six==1.16.0 # via python-dateutil -smmap==3.0.2 +smmap==5.0.0 # via gitdb tempora==5.3.0 # via portend @@ -113,7 +113,7 @@ timelib==0.3.0 # via -r requirements/darwin.txt tornado==6.3.3 # via -r requirements/base.txt -typing-extensions==4.6.3 +typing-extensions==4.8.0 # via pydantic urllib3==1.26.14 # via requests diff --git a/requirements/static/pkg/py3.11/freebsd.txt b/requirements/static/pkg/py3.11/freebsd.txt index 8bf7177e2d1a..219958e1367a 100644 --- a/requirements/static/pkg/py3.11/freebsd.txt +++ b/requirements/static/pkg/py3.11/freebsd.txt @@ -100,9 +100,9 @@ timelib==0.3.0 # via -r requirements/static/pkg/freebsd.in tornado==6.3.3 # via -r requirements/base.txt -typing-extensions==4.6.3 +typing-extensions==4.8.0 # via pydantic -urllib3==1.26.14 +urllib3==1.26.18 # via requests zc.lockfile==3.0.post1 # via cherrypy diff --git a/requirements/static/pkg/py3.11/linux.txt b/requirements/static/pkg/py3.11/linux.txt index 28d32ae04f6c..8756f9592c4f 100644 --- a/requirements/static/pkg/py3.11/linux.txt +++ b/requirements/static/pkg/py3.11/linux.txt @@ -100,9 +100,9 @@ timelib==0.3.0 # via -r requirements/static/pkg/linux.in tornado==6.3.3 # via -r requirements/base.txt -typing-extensions==4.6.3 +typing-extensions==4.8.0 # via pydantic -urllib3==1.26.14 +urllib3==1.26.18 # via requests zc.lockfile==3.0.post1 # via cherrypy diff --git a/requirements/static/pkg/py3.11/windows.txt b/requirements/static/pkg/py3.11/windows.txt index 894eeb2fb6ff..b559416e682f 100644 --- a/requirements/static/pkg/py3.11/windows.txt +++ b/requirements/static/pkg/py3.11/windows.txt @@ -34,7 +34,7 @@ distro==1.8.0 # via -r requirements/base.txt gitdb==4.0.10 # via gitpython -gitpython==3.1.35 +gitpython==3.1.40 # via -r requirements/windows.txt idna==3.4 # via requests @@ -119,7 +119,7 @@ setproctitle==1.3.2 # via -r requirements/windows.txt six==1.15.0 # via python-dateutil -smmap==4.0.0 +smmap==5.0.0 # via gitdb tempora==5.3.0 # via portend @@ -127,9 +127,9 @@ timelib==0.3.0 # via -r requirements/windows.txt tornado==6.3.3 # via -r requirements/base.txt -typing-extensions==4.6.3 +typing-extensions==4.8.0 # via pydantic -urllib3==1.26.14 +urllib3==1.26.18 # via # -r requirements/windows.txt # requests diff --git a/requirements/static/pkg/py3.8/freebsd.txt b/requirements/static/pkg/py3.8/freebsd.txt index 9a4d6c9515f9..b172891671a6 100644 --- a/requirements/static/pkg/py3.8/freebsd.txt +++ b/requirements/static/pkg/py3.8/freebsd.txt @@ -102,9 +102,9 @@ timelib==0.3.0 # via -r requirements/static/pkg/freebsd.in tornado==6.3.3 # via -r requirements/base.txt -typing-extensions==4.6.3 +typing-extensions==4.8.0 # via pydantic -urllib3==1.26.14 +urllib3==1.26.18 # via requests zc.lockfile==3.0.post1 # via cherrypy diff --git a/requirements/static/pkg/py3.8/linux.txt b/requirements/static/pkg/py3.8/linux.txt index 80726f0fb688..e06b76dbf74e 100644 --- a/requirements/static/pkg/py3.8/linux.txt +++ b/requirements/static/pkg/py3.8/linux.txt @@ -102,9 +102,9 @@ timelib==0.3.0 # via -r requirements/static/pkg/linux.in tornado==6.3.3 # via -r requirements/base.txt -typing-extensions==4.6.3 +typing-extensions==4.8.0 # via pydantic -urllib3==1.26.14 +urllib3==1.26.18 # via requests zc.lockfile==3.0.post1 # via cherrypy diff --git a/requirements/static/pkg/py3.8/windows.txt b/requirements/static/pkg/py3.8/windows.txt index 9e3384790e9b..e8fe4ea58c49 100644 --- a/requirements/static/pkg/py3.8/windows.txt +++ b/requirements/static/pkg/py3.8/windows.txt @@ -34,7 +34,7 @@ distro==1.8.0 # via -r requirements/base.txt gitdb==4.0.10 # via gitpython -gitpython==3.1.35 +gitpython==3.1.40 # via -r requirements/windows.txt idna==3.4 # via requests @@ -122,7 +122,7 @@ setproctitle==1.3.2 # via -r requirements/windows.txt six==1.15.0 # via python-dateutil -smmap==4.0.0 +smmap==5.0.0 # via gitdb tempora==5.3.0 # via portend @@ -130,9 +130,9 @@ timelib==0.3.0 # via -r requirements/windows.txt tornado==6.3.3 # via -r requirements/base.txt -typing-extensions==4.6.3 +typing-extensions==4.8.0 # via pydantic -urllib3==1.26.14 +urllib3==1.26.18 # via # -r requirements/windows.txt # requests diff --git a/requirements/static/pkg/py3.9/darwin.txt b/requirements/static/pkg/py3.9/darwin.txt index da1faf9abc67..70e390c7e516 100644 --- a/requirements/static/pkg/py3.9/darwin.txt +++ b/requirements/static/pkg/py3.9/darwin.txt @@ -29,7 +29,7 @@ distro==1.8.0 # via -r requirements/base.txt gitdb==4.0.10 # via gitpython -gitpython==3.1.35 +gitpython==3.1.40 # via -r requirements/darwin.txt idna==3.4 # via @@ -105,7 +105,7 @@ setproctitle==1.3.2 # via -r requirements/darwin.txt six==1.16.0 # via python-dateutil -smmap==3.0.2 +smmap==5.0.0 # via gitdb tempora==5.3.0 # via portend @@ -113,9 +113,9 @@ timelib==0.3.0 # via -r requirements/darwin.txt tornado==6.3.3 # via -r requirements/base.txt -typing-extensions==4.6.3 +typing-extensions==4.8.0 # via pydantic -urllib3==1.26.14 +urllib3==1.26.18 # via requests vultr==1.0.1 # via -r requirements/darwin.txt diff --git a/requirements/static/pkg/py3.9/freebsd.txt b/requirements/static/pkg/py3.9/freebsd.txt index 4d59e1c3ad45..8e080c520a2c 100644 --- a/requirements/static/pkg/py3.9/freebsd.txt +++ b/requirements/static/pkg/py3.9/freebsd.txt @@ -100,9 +100,9 @@ timelib==0.3.0 # via -r requirements/static/pkg/freebsd.in tornado==6.3.3 # via -r requirements/base.txt -typing-extensions==4.6.3 +typing-extensions==4.8.0 # via pydantic -urllib3==1.26.14 +urllib3==1.26.18 # via requests zc.lockfile==3.0.post1 # via cherrypy diff --git a/requirements/static/pkg/py3.9/linux.txt b/requirements/static/pkg/py3.9/linux.txt index 61267c4640f7..13c10e9459db 100644 --- a/requirements/static/pkg/py3.9/linux.txt +++ b/requirements/static/pkg/py3.9/linux.txt @@ -100,9 +100,9 @@ timelib==0.3.0 # via -r requirements/static/pkg/linux.in tornado==6.3.3 # via -r requirements/base.txt -typing-extensions==4.6.3 +typing-extensions==4.8.0 # via pydantic -urllib3==1.26.14 +urllib3==1.26.18 # via requests zc.lockfile==3.0.post1 # via cherrypy diff --git a/requirements/static/pkg/py3.9/windows.txt b/requirements/static/pkg/py3.9/windows.txt index be2a05a34e72..8db61f1ea892 100644 --- a/requirements/static/pkg/py3.9/windows.txt +++ b/requirements/static/pkg/py3.9/windows.txt @@ -34,7 +34,7 @@ distro==1.8.0 # via -r requirements/base.txt gitdb==4.0.10 # via gitpython -gitpython==3.1.35 +gitpython==3.1.40 # via -r requirements/windows.txt idna==3.4 # via requests @@ -120,7 +120,7 @@ setproctitle==1.3.2 # via -r requirements/windows.txt six==1.15.0 # via python-dateutil -smmap==4.0.0 +smmap==5.0.0 # via gitdb tempora==5.3.0 # via portend @@ -128,9 +128,9 @@ timelib==0.3.0 # via -r requirements/windows.txt tornado==6.3.3 # via -r requirements/base.txt -typing-extensions==4.6.3 +typing-extensions==4.8.0 # via pydantic -urllib3==1.26.14 +urllib3==1.26.18 # via # -r requirements/windows.txt # requests diff --git a/requirements/windows.txt b/requirements/windows.txt index b1df1a7f6348..81f44689c75c 100644 --- a/requirements/windows.txt +++ b/requirements/windows.txt @@ -9,7 +9,7 @@ pythonnet>=3.0.1 certifi>=2022.12.07 cffi>=1.14.5 cherrypy>=18.6.1 -gitpython>=3.1.35 +gitpython>=3.1.37 cryptography>=41.0.3 lxml>=4.6.3 pyasn1>=0.4.8 diff --git a/salt/client/ssh/__init__.py b/salt/client/ssh/__init__.py index 0d9ac9509b64..067d4575f9bd 100644 --- a/salt/client/ssh/__init__.py +++ b/salt/client/ssh/__init__.py @@ -11,9 +11,11 @@ import logging import multiprocessing import os +import pathlib import queue import re import shlex +import shutil import subprocess import sys import tarfile @@ -467,7 +469,14 @@ def key_deploy(self, host, ret): if target.get("passwd", False) or self.opts["ssh_passwd"]: self._key_deploy_run(host, target, False) return ret - if ret[host].get("stderr", "").count("Permission denied"): + stderr = ret[host].get("stderr", "") + # -failed to upload file- is detecting scp errors + # Errors to ignore when Permission denied is in the stderr. For example + # scp can get a permission denied on the target host, but they where + # able to accurate authenticate against the box + ignore_err = ["failed to upload file"] + check_err = [x for x in ignore_err if stderr.count(x)] + if "Permission denied" in stderr and not check_err: target = self.targets[host] # permission denied, attempt to auto deploy ssh key print( @@ -1007,11 +1016,30 @@ def run_ssh_pre_flight(self): """ Run our pre_flight script before running any ssh commands """ - script = os.path.join(tempfile.gettempdir(), self.ssh_pre_file) - - self.shell.send(self.ssh_pre_flight, script) - - return self.execute_script(script, script_args=self.ssh_pre_flight_args) + with tempfile.NamedTemporaryFile() as temp: + # ensure we use copyfile to not copy the file attributes + # we want to ensure we use the perms set by the secure + # NamedTemporaryFile + try: + shutil.copyfile(self.ssh_pre_flight, temp.name) + except OSError as err: + return ( + "", + "Could not copy pre flight script to temporary path", + 1, + ) + target_script = f".{pathlib.Path(temp.name).name}" + log.trace("Copying the pre flight script to target") + stdout, stderr, retcode = self.shell.send(temp.name, target_script) + if retcode != 0: + # We could not copy the script to the target + log.error("Could not copy the pre flight script to target") + return stdout, stderr, retcode + + log.trace("Executing the pre flight script on target") + return self.execute_script( + target_script, script_args=self.ssh_pre_flight_args + ) def check_thin_dir(self): """ @@ -1388,18 +1416,20 @@ def shim_cmd(self, cmd_str, extension="py"): return self.shell.exec_cmd(cmd_str) # Write the shim to a temporary file in the default temp directory - with tempfile.NamedTemporaryFile( - mode="w+b", prefix="shim_", delete=False - ) as shim_tmp_file: + with tempfile.NamedTemporaryFile(mode="w+b", delete=False) as shim_tmp_file: shim_tmp_file.write(salt.utils.stringutils.to_bytes(cmd_str)) # Copy shim to target system, under $HOME/. - target_shim_file = ".{}.{}".format( - binascii.hexlify(os.urandom(6)).decode("ascii"), extension - ) + target_shim_file = f".{pathlib.Path(shim_tmp_file.name).name}" + if self.winrm: target_shim_file = saltwinshell.get_target_shim_file(self, target_shim_file) - self.shell.send(shim_tmp_file.name, target_shim_file, makedirs=True) + stdout, stderr, retcode = self.shell.send( + shim_tmp_file.name, target_shim_file, makedirs=True + ) + if retcode != 0: + log.error("Could not copy the shim script to target") + return stdout, stderr, retcode # Remove our shim file try: diff --git a/tests/integration/ssh/test_pre_flight.py b/tests/integration/ssh/test_pre_flight.py deleted file mode 100644 index 1598b3d51b5d..000000000000 --- a/tests/integration/ssh/test_pre_flight.py +++ /dev/null @@ -1,132 +0,0 @@ -""" -Test for ssh_pre_flight roster option -""" - -import os - -import pytest - -import salt.utils.files -from tests.support.case import SSHCase -from tests.support.runtests import RUNTIME_VARS - - -class SSHPreFlightTest(SSHCase): - """ - Test ssh_pre_flight roster option - """ - - def setUp(self): - super().setUp() - self.roster = os.path.join(RUNTIME_VARS.TMP, "pre_flight_roster") - self.data = { - "ssh_pre_flight": os.path.join(RUNTIME_VARS.TMP, "ssh_pre_flight.sh") - } - self.test_script = os.path.join( - RUNTIME_VARS.TMP, "test-pre-flight-script-worked.txt" - ) - - def _create_roster(self, pre_flight_script_args=None): - data = dict(self.data) - if pre_flight_script_args: - data["ssh_pre_flight_args"] = pre_flight_script_args - - self.custom_roster(self.roster, data) - - with salt.utils.files.fopen(data["ssh_pre_flight"], "w") as fp_: - fp_.write("touch {}".format(self.test_script)) - - @pytest.mark.slow_test - def test_ssh_pre_flight(self): - """ - test ssh when ssh_pre_flight is set - ensure the script runs successfully - """ - self._create_roster() - assert self.run_function("test.ping", roster_file=self.roster) - - assert os.path.exists(self.test_script) - - @pytest.mark.slow_test - def test_ssh_run_pre_flight(self): - """ - test ssh when --pre-flight is passed to salt-ssh - to ensure the script runs successfully - """ - self._create_roster() - # make sure we previously ran a command so the thin dir exists - self.run_function("test.ping", wipe=False) - assert not os.path.exists(self.test_script) - - assert self.run_function( - "test.ping", ssh_opts="--pre-flight", roster_file=self.roster, wipe=False - ) - assert os.path.exists(self.test_script) - - @pytest.mark.slow_test - def test_ssh_run_pre_flight_args(self): - """ - test ssh when --pre-flight is passed to salt-ssh - to ensure the script runs successfully passing some args - """ - self._create_roster(pre_flight_script_args="foobar test") - # make sure we previously ran a command so the thin dir exists - self.run_function("test.ping", wipe=False) - assert not os.path.exists(self.test_script) - - assert self.run_function( - "test.ping", ssh_opts="--pre-flight", roster_file=self.roster, wipe=False - ) - assert os.path.exists(self.test_script) - - @pytest.mark.slow_test - def test_ssh_run_pre_flight_args_prevent_injection(self): - """ - test ssh when --pre-flight is passed to salt-ssh - and evil arguments are used in order to produce shell injection - """ - injected_file = os.path.join(RUNTIME_VARS.TMP, "injection") - self._create_roster( - pre_flight_script_args="foobar; echo injected > {}".format(injected_file) - ) - # make sure we previously ran a command so the thin dir exists - self.run_function("test.ping", wipe=False) - assert not os.path.exists(self.test_script) - assert not os.path.isfile(injected_file) - - assert self.run_function( - "test.ping", ssh_opts="--pre-flight", roster_file=self.roster, wipe=False - ) - - assert not os.path.isfile( - injected_file - ), "File injection suceeded. This shouldn't happend" - - @pytest.mark.slow_test - def test_ssh_run_pre_flight_failure(self): - """ - test ssh_pre_flight when there is a failure - in the script. - """ - self._create_roster() - with salt.utils.files.fopen(self.data["ssh_pre_flight"], "w") as fp_: - fp_.write("exit 2") - - ret = self.run_function( - "test.ping", ssh_opts="--pre-flight", roster_file=self.roster, wipe=False - ) - assert ret["retcode"] == 2 - - def tearDown(self): - """ - make sure to clean up any old ssh directories - """ - files = [ - self.roster, - self.data["ssh_pre_flight"], - self.test_script, - os.path.join(RUNTIME_VARS.TMP, "injection"), - ] - for fp_ in files: - if os.path.exists(fp_): - os.remove(fp_) diff --git a/tests/pytests/integration/_logging/test_logging.py b/tests/pytests/integration/_logging/test_logging.py new file mode 100644 index 000000000000..a0fa779308b7 --- /dev/null +++ b/tests/pytests/integration/_logging/test_logging.py @@ -0,0 +1,106 @@ +import logging +import os + +import pytest + +import salt._logging.impl as log_impl +from tests.support.mock import MagicMock, patch + +pytestmark = [ + pytest.mark.skip_on_windows(reason="Temporarily skipped on the newer golden images") +] + + +log = logging.getLogger(__name__) + + +@pytest.fixture +def configure_loader_modules(): + return {log_impl: {}} + + +def log_nameToLevel(name): + """ + Return the numeric representation of textual logging level + """ + # log level values + CRITICAL = 50 + FATAL = CRITICAL + ERROR = 40 + WARNING = 30 + WARN = WARNING + INFO = 20 + DEBUG = 10 + NOTSET = 0 + + _nameToLevel = { + "CRITICAL": CRITICAL, + "FATAL": FATAL, + "ERROR": ERROR, + "WARN": WARNING, + "WARNING": WARNING, + "INFO": INFO, + "DEBUG": DEBUG, + "NOTSET": NOTSET, + } + return _nameToLevel.get(name, None) + + +def test_lowest_log_level(): + ret = log_impl.get_lowest_log_level() + assert ret is None + + log_impl.set_lowest_log_level(log_nameToLevel("DEBUG")) + ret = log_impl.get_lowest_log_level() + assert ret is log_nameToLevel("DEBUG") + + log_impl.set_lowest_log_level(log_nameToLevel("WARNING")) + ret = log_impl.get_lowest_log_level() + assert ret is log_nameToLevel("WARNING") + + opts = {"log_level": "ERROR", "log_level_logfile": "INFO"} + log_impl.set_lowest_log_level_by_opts(opts) + ret = log_impl.get_lowest_log_level() + assert ret is log_nameToLevel("INFO") + + +def test_get_logging_level_from_string(caplog): + ret = log_impl.get_logging_level_from_string(None) + assert ret is log_nameToLevel("WARNING") + + ret = log_impl.get_logging_level_from_string(log_nameToLevel("DEBUG")) + assert ret is log_nameToLevel("DEBUG") + + ret = log_impl.get_logging_level_from_string("CRITICAL") + assert ret is log_nameToLevel("CRITICAL") + + caplog.clear() + with caplog.at_level(logging.WARNING): + msg = "Could not translate the logging level string 'BADLEVEL' into an actual logging level integer. Returning 'logging.ERROR'." + ret = log_impl.get_logging_level_from_string("BADLEVEL") + assert ret is log_nameToLevel("ERROR") + assert msg in caplog.text + + +def test_logfile_handler(caplog): + caplog.clear() + with caplog.at_level(logging.WARNING): + ret = log_impl.is_logfile_handler_configured() + assert ret is False + + msg = "log_path setting is set to `None`. Nothing else to do" + log_path = None + assert log_impl.setup_logfile_handler(log_path) is None + assert msg in caplog.text + + +def test_in_mainprocess(): + ret = log_impl.in_mainprocess() + assert ret is True + + curr_pid = os.getpid() + with patch( + "os.getpid", MagicMock(side_effect=[AttributeError, curr_pid, curr_pid]) + ): + ret = log_impl.in_mainprocess() + assert ret is True diff --git a/tests/pytests/integration/ssh/test_pre_flight.py b/tests/pytests/integration/ssh/test_pre_flight.py new file mode 100644 index 000000000000..09c65d294309 --- /dev/null +++ b/tests/pytests/integration/ssh/test_pre_flight.py @@ -0,0 +1,315 @@ +""" +Test for ssh_pre_flight roster option +""" + +try: + import grp + import pwd +except ImportError: + # windows stacktraces on import of these modules + pass +import os +import pathlib +import shutil +import subprocess + +import pytest +import yaml +from saltfactories.utils import random_string + +import salt.utils.files + +pytestmark = pytest.mark.skip_on_windows(reason="Salt-ssh not available on Windows") + + +def _custom_roster(roster_file, roster_data): + with salt.utils.files.fopen(roster_file, "r") as fp: + data = salt.utils.yaml.safe_load(fp) + for key, item in roster_data.items(): + data["localhost"][key] = item + with salt.utils.files.fopen(roster_file, "w") as fp: + yaml.safe_dump(data, fp) + + +@pytest.fixture +def _create_roster(salt_ssh_roster_file, tmp_path): + ret = {} + ret["roster"] = salt_ssh_roster_file + ret["data"] = {"ssh_pre_flight": str(tmp_path / "ssh_pre_flight.sh")} + ret["test_script"] = str(tmp_path / "test-pre-flight-script-worked.txt") + ret["thin_dir"] = tmp_path / "thin_dir" + + with salt.utils.files.fopen(salt_ssh_roster_file, "r") as fp: + data = salt.utils.yaml.safe_load(fp) + pre_flight_script = ret["data"]["ssh_pre_flight"] + data["localhost"]["ssh_pre_flight"] = pre_flight_script + data["localhost"]["thin_dir"] = str(ret["thin_dir"]) + with salt.utils.files.fopen(salt_ssh_roster_file, "w") as fp: + yaml.safe_dump(data, fp) + + with salt.utils.files.fopen(pre_flight_script, "w") as fp: + fp.write("touch {}".format(ret["test_script"])) + + yield ret + if ret["thin_dir"].exists(): + shutil.rmtree(ret["thin_dir"]) + + +@pytest.mark.slow_test +def test_ssh_pre_flight(salt_ssh_cli, caplog, _create_roster): + """ + test ssh when ssh_pre_flight is set + ensure the script runs successfully + """ + ret = salt_ssh_cli.run("test.ping") + assert ret.returncode == 0 + + assert pathlib.Path(_create_roster["test_script"]).exists() + + +@pytest.mark.slow_test +def test_ssh_run_pre_flight(salt_ssh_cli, _create_roster): + """ + test ssh when --pre-flight is passed to salt-ssh + to ensure the script runs successfully + """ + # make sure we previously ran a command so the thin dir exists + ret = salt_ssh_cli.run("test.ping") + assert pathlib.Path(_create_roster["test_script"]).exists() + + # Now remeove the script to ensure pre_flight doesn't run + # without --pre-flight + pathlib.Path(_create_roster["test_script"]).unlink() + + assert salt_ssh_cli.run("test.ping").returncode == 0 + assert not pathlib.Path(_create_roster["test_script"]).exists() + + # Now ensure + ret = salt_ssh_cli.run( + "test.ping", + "--pre-flight", + ) + assert ret.returncode == 0 + assert pathlib.Path(_create_roster["test_script"]).exists() + + +@pytest.mark.slow_test +def test_ssh_run_pre_flight_args(salt_ssh_cli, _create_roster): + """ + test ssh when --pre-flight is passed to salt-ssh + to ensure the script runs successfully passing some args + """ + _custom_roster(salt_ssh_cli.roster_file, {"ssh_pre_flight_args": "foobar test"}) + # Create pre_flight script that accepts args + test_script = _create_roster["test_script"] + test_script_1 = pathlib.Path(test_script + "-foobar") + test_script_2 = pathlib.Path(test_script + "-test") + with salt.utils.files.fopen(_create_roster["data"]["ssh_pre_flight"], "w") as fp: + fp.write( + f""" + touch {str(test_script)}-$1 + touch {str(test_script)}-$2 + """ + ) + ret = salt_ssh_cli.run("test.ping") + assert ret.returncode == 0 + assert test_script_1.exists() + assert test_script_2.exists() + pathlib.Path(test_script_1).unlink() + pathlib.Path(test_script_2).unlink() + + ret = salt_ssh_cli.run("test.ping") + assert ret.returncode == 0 + assert not test_script_1.exists() + assert not test_script_2.exists() + + ret = salt_ssh_cli.run( + "test.ping", + "--pre-flight", + ) + assert ret.returncode == 0 + assert test_script_1.exists() + assert test_script_2.exists() + + +@pytest.mark.slow_test +def test_ssh_run_pre_flight_args_prevent_injection( + salt_ssh_cli, _create_roster, tmp_path +): + """ + test ssh when --pre-flight is passed to salt-ssh + and evil arguments are used in order to produce shell injection + """ + injected_file = tmp_path / "injection" + _custom_roster( + salt_ssh_cli.roster_file, + {"ssh_pre_flight_args": f"foobar; echo injected > {str(injected_file)}"}, + ) + # Create pre_flight script that accepts args + test_script = _create_roster["test_script"] + test_script_1 = pathlib.Path(test_script + "-echo") + test_script_2 = pathlib.Path(test_script + "-foobar;") + with salt.utils.files.fopen(_create_roster["data"]["ssh_pre_flight"], "w") as fp: + fp.write( + f""" + touch {str(test_script)}-$1 + touch {str(test_script)}-$2 + """ + ) + + # make sure we previously ran a command so the thin dir exists + ret = salt_ssh_cli.run("test.ping") + assert ret.returncode == 0 + assert test_script_1.exists() + assert test_script_2.exists() + test_script_1.unlink() + test_script_2.unlink() + assert not injected_file.is_file() + + ret = salt_ssh_cli.run( + "test.ping", + "--pre-flight", + ) + assert ret.returncode == 0 + + assert test_script_1.exists() + assert test_script_2.exists() + assert not pathlib.Path( + injected_file + ).is_file(), "File injection suceeded. This shouldn't happend" + + +@pytest.mark.flaky(max_runs=4) +@pytest.mark.slow_test +def test_ssh_run_pre_flight_failure(salt_ssh_cli, _create_roster): + """ + test ssh_pre_flight when there is a failure + in the script. + """ + with salt.utils.files.fopen(_create_roster["data"]["ssh_pre_flight"], "w") as fp_: + fp_.write("exit 2") + + ret = salt_ssh_cli.run( + "test.ping", + "--pre-flight", + ) + assert ret.data["retcode"] == 2 + + +@pytest.fixture +def account(): + username = random_string("test-account-", uppercase=False) + with pytest.helpers.create_account(username=username) as account: + yield account + + +@pytest.mark.slow_test +def test_ssh_pre_flight_script(salt_ssh_cli, caplog, _create_roster, tmp_path, account): + """ + Test to ensure user cannot create and run a script + with the expected pre_flight script path on target. + """ + try: + script = pathlib.Path.home() / "hacked" + tmp_preflight = pathlib.Path("/tmp", "ssh_pre_flight.sh") + tmp_preflight.write_text(f"touch {script}") + os.chown(tmp_preflight, account.info.uid, account.info.gid) + ret = salt_ssh_cli.run("test.ping") + assert not script.is_file() + assert ret.returncode == 0 + assert ret.stdout == '{\n"localhost": true\n}\n' + finally: + for _file in [script, tmp_preflight]: + if _file.is_file(): + _file.unlink() + + +def demote(user_uid, user_gid): + def result(): + # os.setgid does not remove group membership, so we remove them here so they are REALLY non-root + os.setgroups([]) + os.setgid(user_gid) + os.setuid(user_uid) + + return result + + +@pytest.mark.slow_test +def test_ssh_pre_flight_perms(salt_ssh_cli, caplog, _create_roster, account): + """ + Test to ensure standard user cannot run pre flight script + on target when user sets wrong permissions (777) on + ssh_pre_flight script. + """ + try: + script = pathlib.Path("/tmp", "itworked") + preflight = pathlib.Path("/ssh_pre_flight.sh") + preflight.write_text(f"touch {str(script)}") + tmp_preflight = pathlib.Path("/tmp", preflight.name) + + _custom_roster(salt_ssh_cli.roster_file, {"ssh_pre_flight": str(preflight)}) + preflight.chmod(0o0777) + run_script = pathlib.Path("/run_script") + run_script.write_text( + f""" + x=1 + while [ $x -le 200000 ]; do + SCRIPT=`bash {str(tmp_preflight)} 2> /dev/null; echo $?` + if [ ${{SCRIPT}} == 0 ]; then + break + fi + x=$(( $x + 1 )) + done + """ + ) + run_script.chmod(0o0777) + # pylint: disable=W1509 + ret = subprocess.Popen( + ["sh", f"{run_script}"], + preexec_fn=demote(account.info.uid, account.info.gid), + stdout=None, + stderr=None, + stdin=None, + universal_newlines=True, + ) + # pylint: enable=W1509 + ret = salt_ssh_cli.run("test.ping") + assert ret.returncode == 0 + + # Lets make sure a different user other than root + # Didn't run the script + assert os.stat(script).st_uid != account.info.uid + assert script.is_file() + finally: + for _file in [script, preflight, tmp_preflight, run_script]: + if _file.is_file(): + _file.unlink() + + +@pytest.mark.slow_test +def test_ssh_run_pre_flight_target_file_perms(salt_ssh_cli, _create_roster, tmp_path): + """ + test ssh_pre_flight to ensure the target pre flight script + has the correct perms + """ + perms_file = tmp_path / "perms" + with salt.utils.files.fopen(_create_roster["data"]["ssh_pre_flight"], "w") as fp_: + fp_.write( + f""" + SCRIPT_NAME=$0 + stat -L -c "%a %G %U" $SCRIPT_NAME > {perms_file} + """ + ) + + ret = salt_ssh_cli.run( + "test.ping", + "--pre-flight", + ) + assert ret.returncode == 0 + with salt.utils.files.fopen(perms_file) as fp: + data = fp.read() + assert data.split()[0] == "600" + uid = os.getuid() + gid = os.getgid() + assert data.split()[1] == grp.getgrgid(gid).gr_name + assert data.split()[2] == pwd.getpwuid(uid).pw_name diff --git a/tests/pytests/unit/client/ssh/test_single.py b/tests/pytests/unit/client/ssh/test_single.py index f97519d5cc22..3f5459a08422 100644 --- a/tests/pytests/unit/client/ssh/test_single.py +++ b/tests/pytests/unit/client/ssh/test_single.py @@ -1,6 +1,5 @@ -import os +import logging import re -import tempfile from textwrap import dedent import pytest @@ -16,6 +15,8 @@ from salt.client import ssh from tests.support.mock import MagicMock, call, patch +log = logging.getLogger(__name__) + @pytest.fixture def opts(tmp_path): @@ -59,7 +60,7 @@ def test_single_opts(opts, target): fsclient=None, thin=salt.utils.thin.thin_path(opts["cachedir"]), mine=False, - **target + **target, ) assert single.shell._ssh_opts() == "" @@ -87,7 +88,7 @@ def test_run_with_pre_flight(opts, target, tmp_path): fsclient=None, thin=salt.utils.thin.thin_path(opts["cachedir"]), mine=False, - **target + **target, ) cmd_ret = ("Success", "", 0) @@ -122,7 +123,7 @@ def test_run_with_pre_flight_with_args(opts, target, tmp_path): fsclient=None, thin=salt.utils.thin.thin_path(opts["cachedir"]), mine=False, - **target + **target, ) cmd_ret = ("Success", "foobar", 0) @@ -156,7 +157,7 @@ def test_run_with_pre_flight_stderr(opts, target, tmp_path): fsclient=None, thin=salt.utils.thin.thin_path(opts["cachedir"]), mine=False, - **target + **target, ) cmd_ret = ("", "Error running script", 1) @@ -190,7 +191,7 @@ def test_run_with_pre_flight_script_doesnot_exist(opts, target, tmp_path): fsclient=None, thin=salt.utils.thin.thin_path(opts["cachedir"]), mine=False, - **target + **target, ) cmd_ret = ("Success", "", 0) @@ -224,7 +225,7 @@ def test_run_with_pre_flight_thin_dir_exists(opts, target, tmp_path): fsclient=None, thin=salt.utils.thin.thin_path(opts["cachedir"]), mine=False, - **target + **target, ) cmd_ret = ("", "", 0) @@ -242,6 +243,39 @@ def test_run_with_pre_flight_thin_dir_exists(opts, target, tmp_path): assert ret == cmd_ret +def test_run_ssh_pre_flight(opts, target, tmp_path): + """ + test Single.run_ssh_pre_flight function + """ + target["ssh_pre_flight"] = str(tmp_path / "script.sh") + single = ssh.Single( + opts, + opts["argv"], + "localhost", + mods={}, + fsclient=None, + thin=salt.utils.thin.thin_path(opts["cachedir"]), + mine=False, + **target, + ) + + cmd_ret = ("Success", "", 0) + mock_flight = MagicMock(return_value=cmd_ret) + mock_cmd = MagicMock(return_value=cmd_ret) + patch_flight = patch("salt.client.ssh.Single.run_ssh_pre_flight", mock_flight) + patch_cmd = patch("salt.client.ssh.Single.cmd_block", mock_cmd) + patch_exec_cmd = patch( + "salt.client.ssh.shell.Shell.exec_cmd", return_value=("", "", 1) + ) + patch_os = patch("os.path.exists", side_effect=[True]) + + with patch_os, patch_flight, patch_cmd, patch_exec_cmd: + ret = single.run() + mock_cmd.assert_called() + mock_flight.assert_called() + assert ret == cmd_ret + + def test_execute_script(opts, target, tmp_path): """ test Single.execute_script() @@ -255,7 +289,7 @@ def test_execute_script(opts, target, tmp_path): thin=salt.utils.thin.thin_path(opts["cachedir"]), mine=False, winrm=False, - **target + **target, ) exp_ret = ("Success", "", 0) @@ -268,12 +302,12 @@ def test_execute_script(opts, target, tmp_path): assert ret == exp_ret assert mock_cmd.call_count == 2 assert [ - call("/bin/sh '{}'".format(script)), - call("rm '{}'".format(script)), + call(f"/bin/sh '{script}'"), + call(f"rm '{script}'"), ] == mock_cmd.call_args_list -def test_shim_cmd(opts, target): +def test_shim_cmd(opts, target, tmp_path): """ test Single.shim_cmd() """ @@ -287,7 +321,7 @@ def test_shim_cmd(opts, target): mine=False, winrm=False, tty=True, - **target + **target, ) exp_ret = ("Success", "", 0) @@ -295,21 +329,24 @@ def test_shim_cmd(opts, target): patch_cmd = patch("salt.client.ssh.shell.Shell.exec_cmd", mock_cmd) patch_send = patch("salt.client.ssh.shell.Shell.send", return_value=("", "", 0)) patch_rand = patch("os.urandom", return_value=b"5\xd9l\xca\xc2\xff") + tmp_file = tmp_path / "tmp_file" + mock_tmp = MagicMock() + patch_tmp = patch("tempfile.NamedTemporaryFile", mock_tmp) + mock_tmp.return_value.__enter__.return_value.name = tmp_file - with patch_cmd, patch_rand, patch_send: + with patch_cmd, patch_tmp, patch_send: ret = single.shim_cmd(cmd_str="echo test") assert ret == exp_ret assert [ - call("/bin/sh '.35d96ccac2ff.py'"), - call("rm '.35d96ccac2ff.py'"), + call(f"/bin/sh '.{tmp_file.name}'"), + call(f"rm '.{tmp_file.name}'"), ] == mock_cmd.call_args_list -def test_run_ssh_pre_flight(opts, target, tmp_path): +def test_shim_cmd_copy_fails(opts, target, caplog): """ - test Single.run_ssh_pre_flight + test Single.shim_cmd() when copying the file fails """ - target["ssh_pre_flight"] = str(tmp_path / "script.sh") single = ssh.Single( opts, opts["argv"], @@ -320,24 +357,202 @@ def test_run_ssh_pre_flight(opts, target, tmp_path): mine=False, winrm=False, tty=True, - **target + **target, ) - exp_ret = ("Success", "", 0) - mock_cmd = MagicMock(return_value=exp_ret) + ret_cmd = ("Success", "", 0) + mock_cmd = MagicMock(return_value=ret_cmd) patch_cmd = patch("salt.client.ssh.shell.Shell.exec_cmd", mock_cmd) - patch_send = patch("salt.client.ssh.shell.Shell.send", return_value=exp_ret) - exp_tmp = os.path.join( - tempfile.gettempdir(), os.path.basename(target["ssh_pre_flight"]) + ret_send = ("", "General error in file copy", 1) + patch_send = patch("salt.client.ssh.shell.Shell.send", return_value=ret_send) + patch_rand = patch("os.urandom", return_value=b"5\xd9l\xca\xc2\xff") + + with patch_cmd, patch_rand, patch_send: + ret = single.shim_cmd(cmd_str="echo test") + assert ret == ret_send + assert "Could not copy the shim script to target" in caplog.text + mock_cmd.assert_not_called() + + +def test_run_ssh_pre_flight_no_connect(opts, target, tmp_path, caplog): + """ + test Single.run_ssh_pre_flight when you + cannot connect to the target + """ + pre_flight = tmp_path / "script.sh" + pre_flight.write_text("") + target["ssh_pre_flight"] = str(pre_flight) + single = ssh.Single( + opts, + opts["argv"], + "localhost", + mods={}, + fsclient=None, + thin=salt.utils.thin.thin_path(opts["cachedir"]), + mine=False, + winrm=False, + tty=True, + **target, ) + mock_exec_cmd = MagicMock(return_value=("", "", 1)) + patch_exec_cmd = patch("salt.client.ssh.shell.Shell.exec_cmd", mock_exec_cmd) + tmp_file = tmp_path / "tmp_file" + mock_tmp = MagicMock() + patch_tmp = patch("tempfile.NamedTemporaryFile", mock_tmp) + mock_tmp.return_value.__enter__.return_value.name = tmp_file + ret_send = ( + "", + "ssh: connect to host 192.168.1.186 port 22: No route to host\nscp: Connection closed\n", + 255, + ) + send_mock = MagicMock(return_value=ret_send) + patch_send = patch("salt.client.ssh.shell.Shell.send", send_mock) + + with caplog.at_level(logging.TRACE): + with patch_send, patch_exec_cmd, patch_tmp: + ret = single.run_ssh_pre_flight() + assert "Copying the pre flight script" in caplog.text + assert "Could not copy the pre flight script to target" in caplog.text + assert ret == ret_send + assert send_mock.call_args_list[0][0][0] == tmp_file + target_script = send_mock.call_args_list[0][0][1] + assert re.search(r".[a-z0-9]+", target_script) + mock_exec_cmd.assert_not_called() + + +def test_run_ssh_pre_flight_permission_denied(opts, target, tmp_path): + """ + test Single.run_ssh_pre_flight when you + cannot copy script to the target due to + a permission denied error + """ + pre_flight = tmp_path / "script.sh" + pre_flight.write_text("") + target["ssh_pre_flight"] = str(pre_flight) + single = ssh.Single( + opts, + opts["argv"], + "localhost", + mods={}, + fsclient=None, + thin=salt.utils.thin.thin_path(opts["cachedir"]), + mine=False, + winrm=False, + tty=True, + **target, + ) + mock_exec_cmd = MagicMock(return_value=("", "", 1)) + patch_exec_cmd = patch("salt.client.ssh.shell.Shell.exec_cmd", mock_exec_cmd) + tmp_file = tmp_path / "tmp_file" + mock_tmp = MagicMock() + patch_tmp = patch("tempfile.NamedTemporaryFile", mock_tmp) + mock_tmp.return_value.__enter__.return_value.name = tmp_file + ret_send = ( + "", + 'scp: dest open "/tmp/preflight.sh": Permission denied\nscp: failed to upload file /etc/salt/preflight.sh to /tmp/preflight.sh\n', + 255, + ) + send_mock = MagicMock(return_value=ret_send) + patch_send = patch("salt.client.ssh.shell.Shell.send", send_mock) - with patch_cmd, patch_send: + with patch_send, patch_exec_cmd, patch_tmp: ret = single.run_ssh_pre_flight() - assert ret == exp_ret - assert [ - call("/bin/sh '{}'".format(exp_tmp)), - call("rm '{}'".format(exp_tmp)), - ] == mock_cmd.call_args_list + assert ret == ret_send + assert send_mock.call_args_list[0][0][0] == tmp_file + target_script = send_mock.call_args_list[0][0][1] + assert re.search(r".[a-z0-9]+", target_script) + mock_exec_cmd.assert_not_called() + + +def test_run_ssh_pre_flight_connect(opts, target, tmp_path, caplog): + """ + test Single.run_ssh_pre_flight when you + can connect to the target + """ + pre_flight = tmp_path / "script.sh" + pre_flight.write_text("") + target["ssh_pre_flight"] = str(pre_flight) + single = ssh.Single( + opts, + opts["argv"], + "localhost", + mods={}, + fsclient=None, + thin=salt.utils.thin.thin_path(opts["cachedir"]), + mine=False, + winrm=False, + tty=True, + **target, + ) + ret_exec_cmd = ("", "", 1) + mock_exec_cmd = MagicMock(return_value=ret_exec_cmd) + patch_exec_cmd = patch("salt.client.ssh.shell.Shell.exec_cmd", mock_exec_cmd) + tmp_file = tmp_path / "tmp_file" + mock_tmp = MagicMock() + patch_tmp = patch("tempfile.NamedTemporaryFile", mock_tmp) + mock_tmp.return_value.__enter__.return_value.name = tmp_file + ret_send = ( + "", + "\rroot@192.168.1.187's password: \n\rpreflight.sh 0% 0 0.0KB/s --:-- ETA\rpreflight.sh 100% 20 2.7KB/s 00:00 \n", + 0, + ) + send_mock = MagicMock(return_value=ret_send) + patch_send = patch("salt.client.ssh.shell.Shell.send", send_mock) + + with caplog.at_level(logging.TRACE): + with patch_send, patch_exec_cmd, patch_tmp: + ret = single.run_ssh_pre_flight() + + assert "Executing the pre flight script on target" in caplog.text + assert ret == ret_exec_cmd + assert send_mock.call_args_list[0][0][0] == tmp_file + target_script = send_mock.call_args_list[0][0][1] + assert re.search(r".[a-z0-9]+", target_script) + mock_exec_cmd.assert_called() + + +def test_run_ssh_pre_flight_shutil_fails(opts, target, tmp_path): + """ + test Single.run_ssh_pre_flight when cannot + copyfile with shutil + """ + pre_flight = tmp_path / "script.sh" + pre_flight.write_text("") + target["ssh_pre_flight"] = str(pre_flight) + single = ssh.Single( + opts, + opts["argv"], + "localhost", + mods={}, + fsclient=None, + thin=salt.utils.thin.thin_path(opts["cachedir"]), + mine=False, + winrm=False, + tty=True, + **target, + ) + ret_exec_cmd = ("", "", 1) + mock_exec_cmd = MagicMock(return_value=ret_exec_cmd) + patch_exec_cmd = patch("salt.client.ssh.shell.Shell.exec_cmd", mock_exec_cmd) + tmp_file = tmp_path / "tmp_file" + mock_tmp = MagicMock() + patch_tmp = patch("tempfile.NamedTemporaryFile", mock_tmp) + mock_tmp.return_value.__enter__.return_value.name = tmp_file + send_mock = MagicMock() + mock_shutil = MagicMock(side_effect=IOError("Permission Denied")) + patch_shutil = patch("shutil.copyfile", mock_shutil) + patch_send = patch("salt.client.ssh.shell.Shell.send", send_mock) + + with patch_send, patch_exec_cmd, patch_tmp, patch_shutil: + ret = single.run_ssh_pre_flight() + + assert ret == ( + "", + "Could not copy pre flight script to temporary path", + 1, + ) + mock_exec_cmd.assert_not_called() + send_mock.assert_not_called() @pytest.mark.skip_on_windows(reason="SSH_PY_SHIM not set on windows") @@ -355,7 +570,7 @@ def test_cmd_run_set_path(opts, target): fsclient=None, thin=salt.utils.thin.thin_path(opts["cachedir"]), mine=False, - **target + **target, ) ret = single._cmd_str() @@ -376,7 +591,7 @@ def test_cmd_run_not_set_path(opts, target): fsclient=None, thin=salt.utils.thin.thin_path(opts["cachedir"]), mine=False, - **target + **target, ) ret = single._cmd_str() @@ -395,7 +610,7 @@ def test_cmd_block_python_version_error(opts, target): thin=salt.utils.thin.thin_path(opts["cachedir"]), mine=False, winrm=False, - **target + **target, ) mock_shim = MagicMock( return_value=(("", "ERROR: Unable to locate appropriate python command\n", 10)) @@ -434,7 +649,9 @@ def test_run_with_pre_flight_args(opts, target, test_opts, tmp_path): and script successfully runs """ opts["ssh_run_pre_flight"] = True - target["ssh_pre_flight"] = str(tmp_path / "script.sh") + pre_flight_script = tmp_path / "script.sh" + pre_flight_script.write_text("") + target["ssh_pre_flight"] = str(pre_flight_script) if test_opts[0] is not None: target["ssh_pre_flight_args"] = test_opts[0] @@ -448,7 +665,7 @@ def test_run_with_pre_flight_args(opts, target, test_opts, tmp_path): fsclient=None, thin=salt.utils.thin.thin_path(opts["cachedir"]), mine=False, - **target + **target, ) cmd_ret = ("Success", "", 0) @@ -456,14 +673,15 @@ def test_run_with_pre_flight_args(opts, target, test_opts, tmp_path): mock_exec_cmd = MagicMock(return_value=("", "", 0)) patch_cmd = patch("salt.client.ssh.Single.cmd_block", mock_cmd) patch_exec_cmd = patch("salt.client.ssh.shell.Shell.exec_cmd", mock_exec_cmd) - patch_shell_send = patch("salt.client.ssh.shell.Shell.send", return_value=None) + patch_shell_send = patch( + "salt.client.ssh.shell.Shell.send", return_value=("", "", 0) + ) patch_os = patch("os.path.exists", side_effect=[True]) with patch_os, patch_cmd, patch_exec_cmd, patch_shell_send: - ret = single.run() - assert mock_exec_cmd.mock_calls[0].args[ - 0 - ] == "/bin/sh '/tmp/script.sh'{}".format(expected_args) + single.run() + script_args = mock_exec_cmd.mock_calls[0].args[0] + assert re.search(r"\/bin\/sh '.[a-z0-9]+", script_args) @pytest.mark.slow_test diff --git a/tests/pytests/unit/client/ssh/test_ssh.py b/tests/pytests/unit/client/ssh/test_ssh.py index 2be96ab195e7..329448919285 100644 --- a/tests/pytests/unit/client/ssh/test_ssh.py +++ b/tests/pytests/unit/client/ssh/test_ssh.py @@ -339,3 +339,113 @@ def test_extra_filerefs(tmp_path, opts): with patch("salt.roster.get_roster_file", MagicMock(return_value=roster)): ssh_obj = client._prep_ssh(**ssh_opts) assert ssh_obj.opts.get("extra_filerefs", None) == "salt://foobar" + + +def test_key_deploy_permission_denied_scp(tmp_path, opts): + """ + test "key_deploy" function when + permission denied authentication error + when attempting to use scp to copy file + to target + """ + host = "localhost" + passwd = "password" + usr = "ssh-usr" + opts["ssh_user"] = usr + opts["tgt"] = host + + ssh_ret = { + host: { + "stdout": "\rroot@192.168.1.187's password: \n\rroot@192.168.1.187's password: \n\rroot@192.168.1.187's password: \n", + "stderr": "Permission denied, please try again.\nPermission denied, please try again.\nroot@192.168.1.187: Permission denied (publickey,gssapi-keyex,gssapi-with-micimport pudb; pu.dbassword).\nscp: Connection closed\n", + "retcode": 255, + } + } + key_run_ret = { + "localhost": { + "jid": "20230922155652279959", + "return": "test", + "retcode": 0, + "id": "test", + "fun": "cmd.run", + "fun_args": ["echo test"], + } + } + patch_roster_file = patch("salt.roster.get_roster_file", MagicMock(return_value="")) + with patch_roster_file: + client = ssh.SSH(opts) + patch_input = patch("builtins.input", side_effect=["y"]) + patch_getpass = patch("getpass.getpass", return_value=["password"]) + mock_key_run = MagicMock(return_value=key_run_ret) + patch_key_run = patch("salt.client.ssh.SSH._key_deploy_run", mock_key_run) + with patch_input, patch_getpass, patch_key_run: + ret = client.key_deploy(host, ssh_ret) + assert mock_key_run.call_args_list[0][0] == ( + host, + {"passwd": [passwd], "host": host, "user": usr}, + True, + ) + assert ret == key_run_ret + assert mock_key_run.call_count == 1 + + +def test_key_deploy_permission_denied_file_scp(tmp_path, opts): + """ + test "key_deploy" function when permission denied + due to not having access to copy the file to the target + We do not want to deploy the key, because this is not + an authentication to the target error. + """ + host = "localhost" + passwd = "password" + usr = "ssh-usr" + opts["ssh_user"] = usr + opts["tgt"] = host + + mock_key_run = MagicMock(return_value=False) + patch_key_run = patch("salt.client.ssh.SSH._key_deploy_run", mock_key_run) + + ssh_ret = { + "localhost": { + "stdout": "", + "stderr": 'scp: dest open "/tmp/preflight.sh": Permission denied\nscp: failed to upload file /etc/salt/preflight.sh to /tmp/preflight.sh\n', + "retcode": 1, + } + } + patch_roster_file = patch("salt.roster.get_roster_file", MagicMock(return_value="")) + with patch_roster_file: + client = ssh.SSH(opts) + ret = client.key_deploy(host, ssh_ret) + assert ret == ssh_ret + assert mock_key_run.call_count == 0 + + +def test_key_deploy_no_permission_denied(tmp_path, opts): + """ + test "key_deploy" function when no permission denied + is returned + """ + host = "localhost" + passwd = "password" + usr = "ssh-usr" + opts["ssh_user"] = usr + opts["tgt"] = host + + mock_key_run = MagicMock(return_value=False) + patch_key_run = patch("salt.client.ssh.SSH._key_deploy_run", mock_key_run) + ssh_ret = { + "localhost": { + "jid": "20230922161937998385", + "return": "test", + "retcode": 0, + "id": "test", + "fun": "cmd.run", + "fun_args": ["echo test"], + } + } + patch_roster_file = patch("salt.roster.get_roster_file", MagicMock(return_value="")) + with patch_roster_file: + client = ssh.SSH(opts) + ret = client.key_deploy(host, ssh_ret) + assert ret == ssh_ret + assert mock_key_run.call_count == 0 diff --git a/tests/pytests/unit/modules/test_deb_postgres.py b/tests/pytests/unit/modules/test_deb_postgres.py new file mode 100644 index 000000000000..f125f96707a9 --- /dev/null +++ b/tests/pytests/unit/modules/test_deb_postgres.py @@ -0,0 +1,165 @@ +import logging + +import pytest + +import salt.modules.deb_postgres as deb_postgres +from tests.support.mock import Mock, patch + +log = logging.getLogger(__name__) + +pytestmark = [ + pytest.mark.skip_unless_on_linux(reason="Only supported on Linux family"), +] + + +@pytest.fixture +def get_lscuster(): + return """\ +8.4 main 5432 online postgres /srv/8.4/main \ + /var/log/postgresql/postgresql-8.4-main.log +9.1 main 5433 online postgres /srv/9.1/main \ + /var/log/postgresql/postgresql-9.1-main.log +""" + + +@pytest.fixture +def configure_loader_modules(get_lscuster): + return { + deb_postgres: { + "__salt__": { + "config.option": Mock(), + "cmd.run_all": Mock(return_value={"stdout": get_lscuster}), + "file.chown": Mock(), + "file.remove": Mock(), + } + } + } + + +def test_cluster_create(): + with patch("salt.utils.path.which", Mock(return_value="/usr/bin/pg_createcluster")): + expected_cmdstr = ( + "/usr/bin/pg_createcluster " + "--port 5432 --locale fr_FR --encoding UTF-8 " + "--datadir /opt/postgresql " + "9.3 main" + ) + + deb_postgres.cluster_create( + "9.3", + "main", + port="5432", + locale="fr_FR", + encoding="UTF-8", + datadir="/opt/postgresql", + ) + assert deb_postgres.__salt__["cmd.run_all"].call_args[0][0] == expected_cmdstr + + +def test_cluster_create_with_initdb_options(): + with patch("salt.utils.path.which", Mock(return_value="/usr/bin/pg_createcluster")): + expected_cmdstr = ( + "/usr/bin/pg_createcluster " + "--port 5432 --locale fr_FR --encoding UTF-8 " + "--datadir /opt/postgresql " + "11 main " + "-- " + "--allow-group-access " + "--data-checksums " + "--wal-segsize 32" + ) + + deb_postgres.cluster_create( + "11", + "main", + port="5432", + locale="fr_FR", + encoding="UTF-8", + datadir="/opt/postgresql", + allow_group_access=True, + data_checksums=True, + wal_segsize="32", + ) + assert deb_postgres.__salt__["cmd.run_all"].call_args[0][0] == expected_cmdstr + + +def test_cluster_create_with_float(): + with patch("salt.utils.path.which", Mock(return_value="/usr/bin/pg_createcluster")): + expected_cmdstr = ( + "/usr/bin/pg_createcluster " + "--port 5432 --locale fr_FR --encoding UTF-8 " + "--datadir /opt/postgresql " + "9.3 main" + ) + + deb_postgres.cluster_create( + 9.3, + "main", + port="5432", + locale="fr_FR", + encoding="UTF-8", + datadir="/opt/postgresql", + ) + assert deb_postgres.__salt__["cmd.run_all"].call_args[0][0] == expected_cmdstr + + +def test_parse_pg_lsclusters(get_lscuster): + with patch("salt.utils.path.which", Mock(return_value="/usr/bin/pg_lsclusters")): + stdout = get_lscuster + maxDiff = None + expected = { + "8.4/main": { + "port": 5432, + "status": "online", + "user": "postgres", + "datadir": "/srv/8.4/main", + "log": "/var/log/postgresql/postgresql-8.4-main.log", + }, + "9.1/main": { + "port": 5433, + "status": "online", + "user": "postgres", + "datadir": "/srv/9.1/main", + "log": "/var/log/postgresql/postgresql-9.1-main.log", + }, + } + assert deb_postgres._parse_pg_lscluster(stdout) == expected + + +def test_cluster_list(): + with patch("salt.utils.path.which", Mock(return_value="/usr/bin/pg_lsclusters")): + return_list = deb_postgres.cluster_list() + assert ( + deb_postgres.__salt__["cmd.run_all"].call_args[0][0] + == "/usr/bin/pg_lsclusters --no-header" + ) + + return_dict = deb_postgres.cluster_list(verbose=True) + assert isinstance(return_dict, dict) + + +def test_cluster_exists(): + assert deb_postgres.cluster_exists("8.4") + assert deb_postgres.cluster_exists("8.4", "main") + assert not deb_postgres.cluster_exists("3.4", "main") + + +def test_cluster_delete(): + with patch("salt.utils.path.which", Mock(return_value="/usr/bin/pg_dropcluster")): + deb_postgres.cluster_remove("9.3", "main") + assert ( + deb_postgres.__salt__["cmd.run_all"].call_args[0][0] + == "/usr/bin/pg_dropcluster 9.3 main" + ) + + deb_postgres.cluster_remove("9.3", "main", stop=True) + assert ( + deb_postgres.__salt__["cmd.run_all"].call_args[0][0] + == "/usr/bin/pg_dropcluster --stop 9.3 main" + ) + + deb_postgres.cluster_remove(9.3, "main", stop=True) + assert ( + deb_postgres.__salt__["cmd.run_all"].call_args[0][0] + == "/usr/bin/pg_dropcluster --stop 9.3 main" + ) diff --git a/tests/pytests/unit/modules/test_postgres.py b/tests/pytests/unit/modules/test_postgres.py index 393c087d1644..b9178fa038ed 100644 --- a/tests/pytests/unit/modules/test_postgres.py +++ b/tests/pytests/unit/modules/test_postgres.py @@ -1,8 +1,17 @@ +import datetime +import re + import pytest import salt.modules.config as configmod import salt.modules.postgres as postgres -from tests.support.mock import MagicMock, patch +from salt.exceptions import SaltInvocationError +from tests.support.mock import MagicMock, Mock, call, patch + +pytestmark = [ + pytest.mark.skip_unless_on_linux(reason="Only supported on Linux family"), +] + # 'md5' + md5('password' + 'username') md5_pw = "md55a231fcdb710d73268c4f44283487ba2" @@ -13,9 +22,47 @@ "LzAh/MGUdjYkdbDzcOKpfGwa3WwPUsyGcY+TEnSpcto=" ) -test_privileges_list_function_csv = ( - 'name\n"{baruwatest=X/baruwatest,bayestest=r/baruwatest,baruwa=X*/baruwatest}"\n' -) + +@pytest.fixture +def get_test_privileges_list_function_csv(): + return """name +"{baruwatest=X/baruwatest,bayestest=r/baruwatest,baruwa=X*/baruwatest}" +""" + + +@pytest.fixture +def get_test_list_db_csv(): + return """Name,Owner,Encoding,Collate,Ctype,Access privileges,Tablespace +template1,postgres,LATIN1,en_US,en_US,"{=c/postgres,postgres=CTc/postgres}",pg_default +template0,postgres,LATIN1,en_US,en_US,"{=c/postgres,postgres=CTc/postgres}",pg_default +postgres,postgres,LATIN1,en_US,en_US,,pg_default +test_db,postgres,LATIN1,en_US,en_US,,pg_default +""" + + +@pytest.fixture +def get_test_list_schema_csv(): + return """name,owner,acl +public,postgres,"{postgres=UC/postgres,=UC/postgres}" +pg_toast,postgres,"" +""" + + +@pytest.fixture +def get_test_list_language_csv(): + return "Name\ninternal\nc\nsql\nplpgsql\n" + + +@pytest.fixture +def get_test_privileges_list_table_csv(): + return """name +"{baruwatest=arwdDxt/baruwatest,bayestest=arwd/baruwatest,baruwa=a*r*w*d*D*x*t*/baruwatest}" +""" + + +@pytest.fixture +def get_test_privileges_list_group_csv(): + return "rolname,admin_option\nbaruwa,f\nbaruwatest2,t\nbaruwatest,f\n" @pytest.fixture @@ -73,11 +120,11 @@ def test_verify_password(role, password, verifier, method, result): assert postgres._verify_password(role, password, verifier, method) == result -def test_has_privileges_with_function(): +def test_has_privileges_with_function(get_test_privileges_list_function_csv): with patch( "salt.modules.postgres._run_psql", MagicMock( - return_value={"retcode": 0, "stdout": test_privileges_list_function_csv} + return_value={"retcode": 0, "stdout": get_test_privileges_list_function_csv} ), ), patch("salt.utils.path.which", MagicMock(return_value="/usr/bin/pgsql")): ret = postgres.has_privileges( @@ -181,3 +228,2340 @@ def test__run_initdb_with_timeout(): with patch.dict(configmod.__opts__, {"postgres.pass": None}): postgres._run_initdb("fakename", runas="saltuser") cmd_run_mock.assert_called_with(cmd_str, timeout=0, **kwargs) + + +def test_run_psql(): + postgres._run_psql('echo "hi"') + cmd = postgres.__salt__["cmd.run_all"] + assert cmd.call_args[1]["runas"] == "postgres" + + +def test_db_alter(): + with patch( + "salt.modules.postgres._run_psql", Mock(return_value={"retcode": 0}) + ), patch("salt.utils.path.which", MagicMock(return_value="/usr/bin/pgsql")): + ret = postgres.db_alter( + "dbname", + user="testuser", + host="testhost", + port="testport", + maintenance_db="maint_db", + password="foo", + tablespace="testspace", + owner="otheruser", + runas="foo", + ) + assert ret is True + + postgres._run_psql.assert_has_calls( + [ + call( + [ + "/usr/bin/pgsql", + "--no-align", + "--no-readline", + "--no-psqlrc", + "--no-password", + "--username", + "testuser", + "--host", + "testhost", + "--port", + "testport", + "--dbname", + "maint_db", + "-c", + 'ALTER DATABASE "dbname" OWNER TO "otheruser"', + ], + host="testhost", + user="testuser", + password="foo", + runas="foo", + port="testport", + ), + call( + [ + "/usr/bin/pgsql", + "--no-align", + "--no-readline", + "--no-psqlrc", + "--no-password", + "--username", + "testuser", + "--host", + "testhost", + "--port", + "testport", + "--dbname", + "maint_db", + "-c", + 'ALTER DATABASE "dbname" SET TABLESPACE "testspace"', + ], + host="testhost", + user="testuser", + password="foo", + runas="foo", + port="testport", + ), + ] + ) + + +def test_db_alter_owner_recurse(): + with patch( + "salt.modules.postgres.owner_to", Mock(return_value={"retcode": None}) + ), patch("salt.utils.path.which", MagicMock(return_value="/usr/bin/pgsql")): + postgres.db_alter( + "dbname", + user="testuser", + host="testhost", + port="testport", + maintenance_db="maint_db", + password="foo", + tablespace="testspace", + owner="otheruser", + owner_recurse=True, + runas="foo", + ) + postgres.owner_to.assert_called_once_with( + "dbname", + "otheruser", + user="testuser", + host="testhost", + port="testport", + password="foo", + runas="foo", + ) + + +def test_db_create(): + with patch( + "salt.modules.postgres._run_psql", Mock(return_value={"retcode": 0}) + ), patch("salt.utils.path.which", MagicMock(return_value="/usr/bin/pgsql")): + postgres.db_create( + "dbname", + user="testuser", + host="testhost", + port="testport", + maintenance_db="maint_db", + password="foo", + tablespace="testspace", + owner="otheruser", + runas="foo", + ) + + postgres._run_psql.assert_called_once_with( + [ + "/usr/bin/pgsql", + "--no-align", + "--no-readline", + "--no-psqlrc", + "--no-password", + "--username", + "testuser", + "--host", + "testhost", + "--port", + "testport", + "--dbname", + "maint_db", + "-c", + 'CREATE DATABASE "dbname" WITH TABLESPACE = "testspace" ' + 'OWNER = "otheruser"', + ], + host="testhost", + user="testuser", + password="foo", + runas="foo", + port="testport", + ) + + +def test_db_create_empty_string_param(): + with patch( + "salt.modules.postgres._run_psql", Mock(return_value={"retcode": 0}) + ), patch("salt.utils.path.which", MagicMock(return_value="/usr/bin/pgsql")): + postgres.db_create( + "dbname", + lc_collate="", + encoding="utf8", + user="testuser", + host="testhost", + port=1234, + maintenance_db="maint_db", + password="foo", + ) + + postgres._run_psql.assert_called_once_with( + [ + "/usr/bin/pgsql", + "--no-align", + "--no-readline", + "--no-psqlrc", + "--no-password", + "--username", + "testuser", + "--host", + "testhost", + "--port", + "1234", + "--dbname", + "maint_db", + "-c", + "CREATE DATABASE \"dbname\" WITH ENCODING = 'utf8' LC_COLLATE = ''", + ], + host="testhost", + password="foo", + port=1234, + runas=None, + user="testuser", + ) + + +def test_db_create_with_trivial_sql_injection(): + with patch( + "salt.modules.postgres._run_psql", Mock(return_value={"retcode": 0}) + ), patch("salt.utils.path.which", MagicMock(return_value="/usr/bin/pgsql")): + pytest.raises( + SaltInvocationError, + postgres.db_create, + "dbname", + lc_collate="foo' ENCODING='utf8", + ) + + +def test_db_exists(get_test_list_db_csv): + with patch( + "salt.modules.postgres._run_psql", + Mock(return_value={"retcode": 0, "stdout": get_test_list_db_csv}), + ), patch("salt.utils.path.which", MagicMock(return_value="/usr/bin/pgsql")): + ret = postgres.db_exists( + "test_db", + user="testuser", + host="testhost", + port="testport", + maintenance_db="maint_db", + password="foo", + runas="foo", + ) + assert ret is True + + +def test_db_list(get_test_list_db_csv): + with patch( + "salt.modules.postgres._run_psql", + Mock(return_value={"retcode": 0, "stdout": get_test_list_db_csv}), + ), patch("salt.utils.path.which", MagicMock(return_value="/usr/bin/pgsql")): + ret = postgres.db_list( + user="testuser", + host="testhost", + port="testport", + maintenance_db="maint_db", + password="foo", + runas="foo", + ) + assert ret == { + "test_db": { + "Encoding": "LATIN1", + "Ctype": "en_US", + "Tablespace": "pg_default", + "Collate": "en_US", + "Owner": "postgres", + "Access privileges": "", + }, + "template1": { + "Encoding": "LATIN1", + "Ctype": "en_US", + "Tablespace": "pg_default", + "Collate": "en_US", + "Owner": "postgres", + "Access privileges": "{=c/postgres,postgres=CTc/postgres}", + }, + "template0": { + "Encoding": "LATIN1", + "Ctype": "en_US", + "Tablespace": "pg_default", + "Collate": "en_US", + "Owner": "postgres", + "Access privileges": "{=c/postgres,postgres=CTc/postgres}", + }, + "postgres": { + "Encoding": "LATIN1", + "Ctype": "en_US", + "Tablespace": "pg_default", + "Collate": "en_US", + "Owner": "postgres", + "Access privileges": "", + }, + } + + +def test_db_remove(): + with patch( + "salt.modules.postgres._run_psql", Mock(return_value={"retcode": 0}) + ), patch("salt.utils.path.which", MagicMock(return_value="/usr/bin/pgsql")): + postgres.db_remove( + "test_db", + user="testuser", + host="testhost", + port="testport", + maintenance_db="maint_db", + password="foo", + runas="foo", + ) + + calls = ( + call( + [ + "/usr/bin/pgsql", + "--no-align", + "--no-readline", + "--no-psqlrc", + "--no-password", + "--username", + "testuser", + "--host", + "testhost", + "--port", + "testport", + "--dbname", + "maint_db", + "-c", + 'REVOKE CONNECT ON DATABASE "test_db" FROM public;', + ], + host="testhost", + password="foo", + port="testport", + runas="foo", + user="testuser", + ), + call( + [ + "/usr/bin/pgsql", + "--no-align", + "--no-readline", + "--no-psqlrc", + "--no-password", + "--username", + "testuser", + "--host", + "testhost", + "--port", + "testport", + "--dbname", + "maint_db", + "-c", + "SELECT pid, pg_terminate_backend(pid) FROM pg_stat_activity" + " WHERE datname = 'test_db' AND pid <> pg_backend_pid();", + ], + host="testhost", + password="foo", + port="testport", + runas="foo", + user="testuser", + ), + call( + [ + "/usr/bin/pgsql", + "--no-align", + "--no-readline", + "--no-psqlrc", + "--no-password", + "--username", + "testuser", + "--host", + "testhost", + "--port", + "testport", + "--dbname", + "maint_db", + "-c", + 'DROP DATABASE "test_db";', + ], + host="testhost", + password="foo", + port="testport", + runas="foo", + user="testuser", + ), + ) + + postgres._run_psql.assert_has_calls(calls, any_order=True) + + +def test_group_create(): + with patch( + "salt.modules.postgres._run_psql", Mock(return_value={"retcode": 0}) + ), patch("salt.utils.path.which", MagicMock(return_value="/usr/bin/pgsql")), patch( + "salt.modules.postgres.user_exists", Mock(return_value=False) + ): + postgres.group_create( + "testgroup", + user="testuser", + host="testhost", + port="testport", + maintenance_db="maint_db", + password="foo", + createdb=False, + encrypted=False, + superuser=False, + replication=False, + rolepassword="testrolepass", + groups="testgroup", + runas="foo", + ) + # postgres._run_psql.call_args[0][0] will contain the list of CLI args. + # The first 14 elements of this list are initial args used in all (or + # virtually all) commands run through _run_psql(), so the actual SQL + # query will be in the 15th argument. + assert postgres._run_psql.call_args[0][0][14].startswith("CREATE ROLE") + + +def test_group_remove(): + with patch( + "salt.modules.postgres._run_psql", Mock(return_value={"retcode": 0}) + ), patch("salt.utils.path.which", MagicMock(return_value="/usr/bin/pgsql")), patch( + "salt.modules.postgres.user_exists", Mock(return_value=True) + ): + postgres.group_remove( + "testgroup", + user="testuser", + host="testhost", + port="testport", + maintenance_db="maint_db", + password="foo", + runas="foo", + ) + postgres._run_psql.assert_called_once_with( + [ + "/usr/bin/pgsql", + "--no-align", + "--no-readline", + "--no-psqlrc", + "--no-password", + "--username", + "testuser", + "--host", + "testhost", + "--port", + "testport", + "--dbname", + "maint_db", + "-c", + 'DROP ROLE "testgroup"', + ], + host="testhost", + user="testuser", + password="foo", + runas="foo", + port="testport", + ) + + +def test_group_update(): + with patch( + "salt.modules.postgres._run_psql", Mock(return_value={"retcode": 0}) + ), patch( + "salt.modules.postgres.role_get", + Mock(return_value={"superuser": False}), + ): + postgres.group_update( + "testgroup", + user='"testuser"', + host="testhost", + port="testport", + maintenance_db="maint_db", + password="foo", + createdb=False, + encrypted=False, + replication=False, + rolepassword="test_role_pass", + groups="testgroup", + runas="foo", + ) + # postgres._run_psql.call_args[0][0] will contain the list of CLI args. + # The first 14 elements of this list are initial args used in all (or + # virtually all) commands run through _run_psql(), so the actual SQL + # query will be in the 15th argument. + assert re.match( + 'ALTER.* "testgroup" .* UNENCRYPTED PASSWORD', + postgres._run_psql.call_args[0][0][14], + ) + + +def test_user_create(): + with patch( + "salt.modules.postgres._run_psql", Mock(return_value={"retcode": 0}) + ), patch("salt.modules.postgres.user_exists", Mock(return_value=False)): + postgres.user_create( + "testuser", + user="testuser", + host="testhost", + port="testport", + maintenance_db="maint_test", + password="test_pass", + login=True, + createdb=False, + createroles=False, + encrypted=False, + superuser=False, + replication=False, + rolepassword="test_role_pass", + valid_until="2042-07-01", + groups="test_groups", + runas="foo", + ) + # postgres._run_psql.call_args[0][0] will contain the list of CLI args. + # The first 14 elements of this list are initial args used in all (or + # virtually all) commands run through _run_psql(), so the actual SQL + # query will be in the 15th argument. + call = postgres._run_psql.call_args[0][0][14] + assert re.match('CREATE ROLE "testuser"', call) + for i in ( + "INHERIT", + "NOCREATEDB", + "NOCREATEROLE", + "NOSUPERUSER", + "NOREPLICATION", + "LOGIN", + "UNENCRYPTED", + "PASSWORD", + "VALID UNTIL", + ): + assert i in call, f"{i} not in {call}" + + +def test_user_exists(): + with patch( + "salt.modules.postgres._run_psql", Mock(return_value={"retcode": 0}) + ), patch("salt.modules.postgres.version", Mock(return_value="9.1")), patch( + "salt.modules.postgres.psql_query", + Mock( + return_value=[ + { + "name": "test_user", + "superuser": "t", + "inherits privileges": "t", + "can create roles": "t", + "can create databases": "t", + "can update system catalogs": "t", + "can login": "t", + "replication": None, + "password": "test_password", + "connections": "-1", + "groups": "", + "expiry time": "", + "defaults variables": None, + } + ] + ), + ): + ret = postgres.user_exists( + "test_user", + user="test_user", + host="test_host", + port="test_port", + maintenance_db="maint_db", + password="test_password", + runas="foo", + ) + assert ret is True + + +def test_user_list(): + with patch( + "salt.modules.postgres._run_psql", Mock(return_value={"retcode": 0}) + ), patch("salt.modules.postgres.version", Mock(return_value="9.1")), patch( + "salt.modules.postgres.psql_query", + Mock( + return_value=[ + { + "name": "test_user", + "superuser": "t", + "inherits privileges": "t", + "can create roles": "t", + "can create databases": "t", + "can update system catalogs": "t", + "can login": "t", + "replication": None, + "connections": "-1", + "groups": "", + "expiry time": "2017-08-16 08:57:46", + "defaults variables": None, + } + ] + ), + ): + ret = postgres.user_list( + "test_user", + host="test_host", + port="test_port", + maintenance_db="maint_db", + password="test_password", + runas="foo", + ) + + assert ret == { + "test_user": { + "superuser": True, + "defaults variables": None, + "can create databases": True, + "can create roles": True, + "connections": None, + "replication": None, + "expiry time": datetime.datetime(2017, 8, 16, 8, 57, 46), + "can login": True, + "can update system catalogs": True, + "groups": [], + "inherits privileges": True, + } + } + + +def test_user_remove(): + with patch( + "salt.modules.postgres._run_psql", Mock(return_value={"retcode": 0}) + ), patch("salt.utils.path.which", MagicMock(return_value="/usr/bin/pgsql")), patch( + "salt.modules.postgres.version", Mock(return_value="9.1") + ), patch( + "salt.modules.postgres.user_exists", Mock(return_value=True) + ): + postgres.user_remove( + "testuser", + user="testuser", + host="testhost", + port="testport", + maintenance_db="maint_db", + password="testpassword", + runas="foo", + ) + postgres._run_psql.assert_called_once_with( + [ + "/usr/bin/pgsql", + "--no-align", + "--no-readline", + "--no-psqlrc", + "--no-password", + "--username", + "testuser", + "--host", + "testhost", + "--port", + "testport", + "--dbname", + "maint_db", + "-c", + 'DROP ROLE "testuser"', + ], + host="testhost", + port="testport", + user="testuser", + password="testpassword", + runas="foo", + ) + + +def test_user_update(): + with patch( + "salt.modules.postgres._run_psql", Mock(return_value={"retcode": 0}) + ), patch( + "salt.modules.postgres.role_get", + Mock(return_value={"superuser": False}), + ): + postgres.user_update( + "test_username", + user="test_user", + host="test_host", + port="test_port", + maintenance_db="test_maint", + password="test_pass", + createdb=False, + createroles=False, + encrypted=False, + inherit=True, + login=True, + replication=False, + rolepassword="test_role_pass", + valid_until="2017-07-01", + groups="test_groups", + runas="foo", + ) + # postgres._run_psql.call_args[0][0] will contain the list of CLI args. + # The first 14 elements of this list are initial args used in all (or + # virtually all) commands run through _run_psql(), so the actual SQL + # query will be in the 15th argument. + assert re.match( + 'ALTER ROLE "test_username" WITH INHERIT NOCREATEDB ' + "NOCREATEROLE NOREPLICATION LOGIN " + "UNENCRYPTED PASSWORD ['\"]{0,5}test_role_pass['\"]{0,5} " + "VALID UNTIL '2017-07-01';" + ' GRANT "test_groups" TO "test_username"', + postgres._run_psql.call_args[0][0][14], + ) + + +def test_user_update2(): + with patch( + "salt.modules.postgres._run_psql", Mock(return_value={"retcode": 0}) + ), patch( + "salt.modules.postgres.role_get", + Mock(return_value={"superuser": False}), + ): + postgres.user_update( + "test_username", + user="test_user", + host="test_host", + port="test_port", + maintenance_db="test_maint", + password="test_pass", + createdb=False, + createroles=True, + encrypted=False, + inherit=True, + login=True, + replication=False, + groups="test_groups", + runas="foo", + ) + # postgres._run_psql.call_args[0][0] will contain the list of CLI args. + # The first 14 elements of this list are initial args used in all (or + # virtually all) commands run through _run_psql(), so the actual SQL + # query will be in the 15th argument. + assert re.match( + 'ALTER ROLE "test_username" WITH INHERIT NOCREATEDB ' + "CREATEROLE NOREPLICATION LOGIN;" + ' GRANT "test_groups" TO "test_username"', + postgres._run_psql.call_args[0][0][14], + ) + + +def test_user_update3(): + with patch( + "salt.modules.postgres._run_psql", Mock(return_value={"retcode": 0}) + ), patch( + "salt.modules.postgres.role_get", + Mock(return_value={"superuser": False}), + ): + postgres.user_update( + "test_username", + user="test_user", + host="test_host", + port="test_port", + maintenance_db="test_maint", + password="test_pass", + createdb=False, + createroles=True, + encrypted=False, + inherit=True, + login=True, + rolepassword=False, + replication=False, + groups="test_groups", + runas="foo", + ) + # postgres._run_psql.call_args[0][0] will contain the list of CLI args. + # The first 14 elements of this list are initial args used in all (or + # virtually all) commands run through _run_psql(), so the actual SQL + # query will be in the 15th argument. + assert re.match( + 'ALTER ROLE "test_username" WITH INHERIT NOCREATEDB ' + "CREATEROLE NOREPLICATION LOGIN NOPASSWORD;" + ' GRANT "test_groups" TO "test_username"', + postgres._run_psql.call_args[0][0][14], + ) + + +def test_user_update_encrypted_passwd(): + with patch( + "salt.modules.postgres._run_psql", Mock(return_value={"retcode": 0}) + ), patch( + "salt.modules.postgres.role_get", + Mock(return_value={"superuser": False}), + ): + postgres.user_update( + "test_username", + user="test_user", + host="test_host", + port="test_port", + maintenance_db="test_maint", + password="test_pass", + createdb=False, + createroles=True, + encrypted=True, + inherit=True, + login=True, + rolepassword="foobar", + replication=False, + groups="test_groups", + runas="foo", + ) + # postgres._run_psql.call_args[0][0] will contain the list of CLI args. + # The first 14 elements of this list are initial args used in all (or + # virtually all) commands run through _run_psql(), so the actual SQL + # query will be in the 15th argument. + assert re.match( + 'ALTER ROLE "test_username" WITH INHERIT NOCREATEDB ' + "CREATEROLE NOREPLICATION LOGIN " + "ENCRYPTED PASSWORD " + "['\"]{0,5}md531c27e68d3771c392b52102c01be1da1['\"]{0,5}" + '; GRANT "test_groups" TO "test_username"', + postgres._run_psql.call_args[0][0][14], + ) + + +def test_version(): + with patch( + "salt.modules.postgres._run_psql", + Mock(return_value={"retcode": 0, "stdout": "9.1.9"}), + ): + postgres.version( + user="test_user", + host="test_host", + port="test_port", + maintenance_db="test_maint", + password="test_pass", + runas="foo", + ) + # postgres._run_psql.call_args[0][0] will contain the list of CLI args. + # The first 14 elements of this list are initial args used in all (or + # virtually all) commands run through _run_psql(), so the actual SQL + # query will be in the 15th argument. + assert re.match( + "SELECT setting FROM pg_catalog.pg_settings", + postgres._run_psql.call_args[0][0][14], + ) + + +def test_installed_extensions(): + with patch( + "salt.modules.postgres.psql_query", + Mock(return_value=[{"extname": "foo", "extversion": "1"}]), + ): + exts = postgres.installed_extensions() + assert exts == {"foo": {"extversion": "1", "extname": "foo"}} + + +def test_available_extensions(): + with patch( + "salt.modules.postgres.psql_query", + Mock(return_value=[{"name": "foo", "default_version": "1"}]), + ): + exts = postgres.available_extensions() + assert exts == {"foo": {"default_version": "1", "name": "foo"}} + + +def test_drop_extension2(): + with patch( + "salt.modules.postgres.installed_extensions", Mock(side_effect=[{}, {}]) + ): + with patch( + "salt.modules.postgres._psql_prepare_and_run", Mock(return_value=None) + ): + with patch( + "salt.modules.postgres.available_extensions", + Mock(return_value={"foo": {"default_version": "1", "name": "foo"}}), + ): + assert postgres.drop_extension("foo") + + +def test_drop_extension3(): + with patch( + "salt.modules.postgres.installed_extensions", + Mock(side_effect=[{"foo": {"extversion": "1", "extname": "foo"}}, {}]), + ): + with patch( + "salt.modules.postgres._psql_prepare_and_run", Mock(return_value=None) + ): + with patch( + "salt.modules.postgres.available_extensions", + Mock(return_value={"foo": {"default_version": "1", "name": "foo"}}), + ): + assert postgres.drop_extension("foo") + + +def test_drop_extension1(): + with patch( + "salt.modules.postgres.installed_extensions", + Mock( + side_effect=[ + {"foo": {"extversion": "1", "extname": "foo"}}, + {"foo": {"extversion": "1", "extname": "foo"}}, + ] + ), + ): + with patch( + "salt.modules.postgres._psql_prepare_and_run", Mock(return_value=None) + ): + with patch( + "salt.modules.postgres.available_extensions", + Mock(return_value={"foo": {"default_version": "1", "name": "foo"}}), + ): + assert not postgres.drop_extension("foo") + + +def test_create_mtdata(): + with patch( + "salt.modules.postgres.installed_extensions", + Mock( + return_value={ + "foo": { + "extversion": "0.8", + "extrelocatable": "t", + "schema_name": "foo", + "extname": "foo", + } + }, + ), + ): + with patch( + "salt.modules.postgres.available_extensions", + Mock(return_value={"foo": {"default_version": "1.4", "name": "foo"}}), + ): + ret = postgres.create_metadata("foo", schema="bar", ext_version="1.4") + assert postgres._EXTENSION_INSTALLED in ret + assert postgres._EXTENSION_TO_UPGRADE in ret + assert postgres._EXTENSION_TO_MOVE in ret + + ret = postgres.create_metadata("foo", schema="foo", ext_version="0.4") + assert postgres._EXTENSION_INSTALLED in ret + assert postgres._EXTENSION_TO_UPGRADE not in ret + assert postgres._EXTENSION_TO_MOVE not in ret + + ret = postgres.create_metadata("foo") + assert postgres._EXTENSION_INSTALLED in ret + assert postgres._EXTENSION_TO_UPGRADE not in ret + assert postgres._EXTENSION_TO_MOVE not in ret + + ret = postgres.create_metadata("foobar") + assert postgres._EXTENSION_NOT_INSTALLED in ret + assert postgres._EXTENSION_INSTALLED not in ret + assert postgres._EXTENSION_TO_UPGRADE not in ret + assert postgres._EXTENSION_TO_MOVE not in ret + + +def test_create_extension_newerthan(): + """ + scenario of creating upgrading extensions with possible schema and + version specifications + """ + with patch( + "salt.modules.postgres.create_metadata", + Mock( + side_effect=[ + # create succeeded + [postgres._EXTENSION_NOT_INSTALLED], + [postgres._EXTENSION_INSTALLED], + [postgres._EXTENSION_NOT_INSTALLED], + [postgres._EXTENSION_INSTALLED], + # create failed + [postgres._EXTENSION_NOT_INSTALLED], + [postgres._EXTENSION_NOT_INSTALLED], + # move+upgrade succeeded + [ + postgres._EXTENSION_TO_MOVE, + postgres._EXTENSION_TO_UPGRADE, + postgres._EXTENSION_INSTALLED, + ], + [postgres._EXTENSION_INSTALLED], + # move succeeded + [postgres._EXTENSION_TO_MOVE, postgres._EXTENSION_INSTALLED], + [postgres._EXTENSION_INSTALLED], + # upgrade succeeded + [postgres._EXTENSION_TO_UPGRADE, postgres._EXTENSION_INSTALLED], + [postgres._EXTENSION_INSTALLED], + # upgrade failed + [postgres._EXTENSION_TO_UPGRADE, postgres._EXTENSION_INSTALLED], + [postgres._EXTENSION_TO_UPGRADE, postgres._EXTENSION_INSTALLED], + # move failed + [postgres._EXTENSION_TO_MOVE, postgres._EXTENSION_INSTALLED], + [postgres._EXTENSION_TO_MOVE, postgres._EXTENSION_INSTALLED], + ] + ), + ): + with patch( + "salt.modules.postgres._psql_prepare_and_run", Mock(return_value=None) + ): + with patch( + "salt.modules.postgres.available_extensions", + Mock(return_value={"foo": {"default_version": "1.4", "name": "foo"}}), + ): + assert postgres.create_extension("foo") + assert re.match( + 'CREATE EXTENSION IF NOT EXISTS "foo" ;', + postgres._psql_prepare_and_run.call_args[0][0][1], + ) + + assert postgres.create_extension( + "foo", schema="a", ext_version="b", from_version="c" + ) + assert re.match( + 'CREATE EXTENSION IF NOT EXISTS "foo" ' + 'WITH SCHEMA "a" VERSION b FROM c ;', + postgres._psql_prepare_and_run.call_args[0][0][1], + ) + assert not postgres.create_extension("foo") + + ret = postgres.create_extension("foo", ext_version="a", schema="b") + assert ret is True + assert re.match( + 'ALTER EXTENSION "foo" SET SCHEMA "b";' + ' ALTER EXTENSION "foo" UPDATE TO a;', + postgres._psql_prepare_and_run.call_args[0][0][1], + ) + + ret = postgres.create_extension("foo", ext_version="a", schema="b") + assert ret is True + assert re.match( + 'ALTER EXTENSION "foo" SET SCHEMA "b";', + postgres._psql_prepare_and_run.call_args[0][0][1], + ) + + ret = postgres.create_extension("foo", ext_version="a", schema="b") + assert ret is True + assert re.match( + 'ALTER EXTENSION "foo" UPDATE TO a;', + postgres._psql_prepare_and_run.call_args[0][0][1], + ) + assert not postgres.create_extension("foo", ext_version="a", schema="b") + assert not postgres.create_extension("foo", ext_version="a", schema="b") + + +def test_encrypt_passwords(): + assert postgres._maybe_encrypt_password("foo", "bar", False) == "bar" + assert ( + postgres._maybe_encrypt_password("foo", "bar", True) + == "md596948aad3fcae80c08a35c9b5958cd89" + ) + + +def test_schema_list(get_test_list_schema_csv): + with patch( + "salt.modules.postgres._run_psql", + Mock(return_value={"retcode": 0, "stdout": get_test_list_schema_csv}), + ): + ret = postgres.schema_list( + "maint_db", + db_user="testuser", + db_host="testhost", + db_port="testport", + db_password="foo", + ) + assert ret == { + "public": { + "acl": "{postgres=UC/postgres,=UC/postgres}", + "owner": "postgres", + }, + "pg_toast": {"acl": "", "owner": "postgres"}, + } + + +def test_schema_exists(): + with patch("salt.modules.postgres._run_psql", Mock(return_value={"retcode": 0})): + with patch( + "salt.modules.postgres.psql_query", + Mock( + return_value=[ + { + "name": "public", + "acl": "{postgres=UC/postgres,=UC/postgres}", + "owner": "postgres", + } + ] + ), + ): + ret = postgres.schema_exists("template1", "public") + assert ret is True + + +def test_schema_get(): + with patch("salt.modules.postgres._run_psql", Mock(return_value={"retcode": 0})): + with patch( + "salt.modules.postgres.psql_query", + Mock( + return_value=[ + { + "name": "public", + "acl": "{postgres=UC/postgres,=UC/postgres}", + "owner": "postgres", + } + ] + ), + ): + ret = postgres.schema_get("template1", "public") + assert ret == { + "acl": "{postgres=UC/postgres,=UC/postgres}", + "owner": "postgres", + } + + +def test_schema_get_again(): + with patch("salt.modules.postgres._run_psql", Mock(return_value={"retcode": 0})): + with patch( + "salt.modules.postgres.psql_query", + Mock( + return_value=[ + { + "name": "public", + "acl": "{postgres=UC/postgres,=UC/postgres}", + "owner": "postgres", + } + ] + ), + ): + ret = postgres.schema_get("template1", "pg_toast") + assert ret is None + + +def test_schema_create(): + with patch( + "salt.modules.postgres._run_psql", Mock(return_value={"retcode": 0}) + ), patch("salt.utils.path.which", MagicMock(return_value="/usr/bin/pgsql")): + with patch("salt.modules.postgres.schema_exists", Mock(return_value=False)): + postgres.schema_create( + "maint_db", + "testschema", + user="user", + db_host="testhost", + db_port="testport", + db_user="testuser", + db_password="testpassword", + ) + postgres._run_psql.assert_called_once_with( + [ + "/usr/bin/pgsql", + "--no-align", + "--no-readline", + "--no-psqlrc", + "--no-password", + "--username", + "testuser", + "--host", + "testhost", + "--port", + "testport", + "--dbname", + "maint_db", + "-c", + 'CREATE SCHEMA "testschema"', + ], + host="testhost", + port="testport", + password="testpassword", + user="testuser", + runas="user", + ) + + +def test_schema_create2(): + with patch("salt.modules.postgres.schema_exists", Mock(return_value=True)): + ret = postgres.schema_create( + "test_db", + "test_schema", + user="user", + db_host="test_host", + db_port="test_port", + db_user="test_user", + db_password="test_password", + ) + assert ret is False + + +def test_schema_remove(): + with patch( + "salt.modules.postgres._run_psql", Mock(return_value={"retcode": 0}) + ), patch("salt.utils.path.which", MagicMock(return_value="/usr/bin/pgsql")): + with patch("salt.modules.postgres.schema_exists", Mock(return_value=True)): + postgres.schema_remove( + "maint_db", + "testschema", + user="user", + db_host="testhost", + db_port="testport", + db_user="testuser", + db_password="testpassword", + ) + postgres._run_psql.assert_called_once_with( + [ + "/usr/bin/pgsql", + "--no-align", + "--no-readline", + "--no-psqlrc", + "--no-password", + "--username", + "testuser", + "--host", + "testhost", + "--port", + "testport", + "--dbname", + "maint_db", + "-c", + 'DROP SCHEMA "testschema"', + ], + host="testhost", + port="testport", + password="testpassword", + user="testuser", + runas="user", + ) + + +def test_schema_remove2(): + with patch("salt.modules.postgres.schema_exists", Mock(return_value=False)): + ret = postgres.schema_remove( + "test_db", + "test_schema", + user="user", + db_host="test_host", + db_port="test_port", + db_user="test_user", + db_password="test_password", + ) + assert ret is False + + +def test_language_list(get_test_list_language_csv): + """ + Test language listing + """ + with patch( + "salt.modules.postgres._run_psql", + Mock(return_value={"retcode": 0, "stdout": get_test_list_language_csv}), + ): + ret = postgres.language_list( + "testdb", + user="testuser", + host="testhost", + port="testport", + password="foo", + ) + assert ret == { + "c": "c", + "internal": "internal", + "plpgsql": "plpgsql", + "sql": "sql", + } + + +def test_language_exists(): + """ + Test language existence check + """ + with patch( + "salt.modules.postgres._run_psql", Mock(return_value={"retcode": 0}) + ), patch( + "salt.modules.postgres.psql_query", + Mock( + return_value=[ + {"Name": "internal"}, + {"Name": "c"}, + {"Name": "sql"}, + {"Name": "plpgsql"}, + ] + ), + ), patch( + "salt.modules.postgres.language_exists", Mock(return_value=True) + ): + ret = postgres.language_exists("sql", "testdb") + assert ret is True + + +def test_language_create(): + """ + Test language creation - does not exist in db + """ + with patch( + "salt.modules.postgres._run_psql", Mock(return_value={"retcode": 0}) + ), patch("salt.utils.path.which", MagicMock(return_value="/usr/bin/pgsql")): + with patch("salt.modules.postgres.language_exists", Mock(return_value=False)): + postgres.language_create( + "plpythonu", + "testdb", + runas="user", + host="testhost", + port="testport", + user="testuser", + password="testpassword", + ) + postgres._run_psql.assert_called_once_with( + [ + "/usr/bin/pgsql", + "--no-align", + "--no-readline", + "--no-psqlrc", + "--no-password", + "--username", + "testuser", + "--host", + "testhost", + "--port", + "testport", + "--dbname", + "testdb", + "-c", + "CREATE LANGUAGE plpythonu", + ], + host="testhost", + port="testport", + password="testpassword", + user="testuser", + runas="user", + ) + + +def test_language_create_exists(): + """ + Test language creation - already exists in db + """ + with patch("salt.modules.postgres.language_exists", Mock(return_value=True)): + ret = postgres.language_create( + "plpythonu", + "testdb", + runas="user", + host="testhost", + port="testport", + user="testuser", + password="testpassword", + ) + assert ret is False + + +def test_language_remove(): + """ + Test language removal - exists in db + """ + with patch( + "salt.modules.postgres._run_psql", Mock(return_value={"retcode": 0}) + ), patch("salt.utils.path.which", MagicMock(return_value="/usr/bin/pgsql")): + with patch("salt.modules.postgres.language_exists", Mock(return_value=True)): + postgres.language_remove( + "plpgsql", + "testdb", + runas="user", + host="testhost", + port="testport", + user="testuser", + password="testpassword", + ) + postgres._run_psql.assert_called_once_with( + [ + "/usr/bin/pgsql", + "--no-align", + "--no-readline", + "--no-psqlrc", + "--no-password", + "--username", + "testuser", + "--host", + "testhost", + "--port", + "testport", + "--dbname", + "testdb", + "-c", + "DROP LANGUAGE plpgsql", + ], + host="testhost", + port="testport", + password="testpassword", + user="testuser", + runas="user", + ) + + +def test_language_remove_non_exist(): + """ + Test language removal - does not exist in db + """ + with patch("salt.modules.postgres.language_exists", Mock(return_value=False)): + ret = postgres.language_remove( + "plpgsql", + "testdb", + runas="user", + host="testhost", + port="testport", + user="testuser", + password="testpassword", + ) + assert ret is False + + +def test_privileges_list_table(get_test_privileges_list_table_csv): + """ + Test privilege listing on a table + """ + with patch( + "salt.modules.postgres._run_psql", + Mock(return_value={"retcode": 0, "stdout": get_test_privileges_list_table_csv}), + ), patch("salt.utils.path.which", MagicMock(return_value="/usr/bin/pgsql")): + ret = postgres.privileges_list( + "awl", + "table", + maintenance_db="db_name", + runas="user", + host="testhost", + port="testport", + user="testuser", + password="testpassword", + ) + expected = { + "bayestest": { + "INSERT": False, + "UPDATE": False, + "SELECT": False, + "DELETE": False, + }, + "baruwa": { + "INSERT": True, + "TRUNCATE": True, + "UPDATE": True, + "TRIGGER": True, + "REFERENCES": True, + "SELECT": True, + "DELETE": True, + }, + "baruwatest": { + "INSERT": False, + "TRUNCATE": False, + "UPDATE": False, + "TRIGGER": False, + "REFERENCES": False, + "SELECT": False, + "DELETE": False, + }, + } + assert ret == expected + + query = ( + "COPY (SELECT relacl AS name FROM pg_catalog.pg_class c " + "JOIN pg_catalog.pg_namespace n ON n.oid = c.relnamespace " + "WHERE nspname = 'public' AND relname = 'awl' AND relkind in ('r', 'v') " + "ORDER BY relname) TO STDOUT WITH CSV HEADER" + ) + + postgres._run_psql.assert_called_once_with( + [ + "/usr/bin/pgsql", + "--no-align", + "--no-readline", + "--no-psqlrc", + "--no-password", + "--username", + "testuser", + "--host", + "testhost", + "--port", + "testport", + "--dbname", + "db_name", + "-v", + "datestyle=ISO,MDY", + "-c", + query, + ], + host="testhost", + port="testport", + password="testpassword", + user="testuser", + runas="user", + ) + + +def test_privileges_list_group(get_test_privileges_list_group_csv): + """ + Test privilege listing on a group + """ + with patch( + "salt.modules.postgres._run_psql", + Mock(return_value={"retcode": 0, "stdout": get_test_privileges_list_group_csv}), + ), patch("salt.utils.path.which", MagicMock(return_value="/usr/bin/pgsql")): + ret = postgres.privileges_list( + "admin", + "group", + maintenance_db="db_name", + runas="user", + host="testhost", + port="testport", + user="testuser", + password="testpassword", + ) + expected = { + "baruwa": False, + "baruwatest": False, + "baruwatest2": True, + } + assert ret == expected + + query = ( + "COPY (SELECT rolname, admin_option " + "FROM pg_catalog.pg_auth_members m JOIN pg_catalog.pg_roles r " + "ON m.member=r.oid WHERE m.roleid IN (SELECT oid FROM " + "pg_catalog.pg_roles WHERE rolname='admin') ORDER BY rolname) " + "TO STDOUT WITH CSV HEADER" + ) + + postgres._run_psql.assert_called_once_with( + [ + "/usr/bin/pgsql", + "--no-align", + "--no-readline", + "--no-psqlrc", + "--no-password", + "--username", + "testuser", + "--host", + "testhost", + "--port", + "testport", + "--dbname", + "db_name", + "-v", + "datestyle=ISO,MDY", + "-c", + query, + ], + host="testhost", + port="testport", + password="testpassword", + user="testuser", + runas="user", + ) + + +def test_has_privileges_on_table(get_test_privileges_list_table_csv): + """ + Test privilege checks on table + """ + with patch( + "salt.modules.postgres._run_psql", + Mock(return_value={"retcode": 0, "stdout": get_test_privileges_list_table_csv}), + ): + ret = postgres.has_privileges( + "baruwa", + "awl", + "table", + "SELECT,INSERT", + grant_option=True, + maintenance_db="db_name", + runas="user", + host="testhost", + port="testport", + user="testuser", + password="testpassword", + ) + assert ret is True + + ret = postgres.has_privileges( + "baruwa", + "awl", + "table", + "ALL", + grant_option=True, + maintenance_db="db_name", + runas="user", + host="testhost", + port="testport", + user="testuser", + password="testpassword", + ) + assert ret is True + + ret = postgres.has_privileges( + "baruwa", + "awl", + "table", + "ALL", + grant_option=False, + maintenance_db="db_name", + runas="user", + host="testhost", + port="testport", + user="testuser", + password="testpassword", + ) + assert ret is True + + ret = postgres.has_privileges( + "bayestest", + "awl", + "table", + "SELECT,INSERT,TRUNCATE", + maintenance_db="db_name", + runas="user", + host="testhost", + port="testport", + user="testuser", + password="testpassword", + ) + assert ret is False + + ret = postgres.has_privileges( + "bayestest", + "awl", + "table", + "SELECT,INSERT", + maintenance_db="db_name", + runas="user", + host="testhost", + port="testport", + user="testuser", + password="testpassword", + ) + assert ret is True + + +def test_has_privileges_on_group(get_test_privileges_list_group_csv): + """ + Test privilege checks on group + """ + with patch( + "salt.modules.postgres._run_psql", + Mock(return_value={"retcode": 0, "stdout": get_test_privileges_list_group_csv}), + ): + ret = postgres.has_privileges( + "baruwa", + "admin", + "group", + maintenance_db="db_name", + runas="user", + host="testhost", + port="testport", + user="testuser", + password="testpassword", + ) + assert ret is True + + ret = postgres.has_privileges( + "baruwa", + "admin", + "group", + grant_option=True, + maintenance_db="db_name", + runas="user", + host="testhost", + port="testport", + user="testuser", + password="testpassword", + ) + assert ret is False + + ret = postgres.has_privileges( + "tony", + "admin", + "group", + maintenance_db="db_name", + runas="user", + host="testhost", + port="testport", + user="testuser", + password="testpassword", + ) + assert ret is False + + +def test_privileges_grant_table(): + """ + Test granting privileges on table + """ + with patch( + "salt.modules.postgres._run_psql", Mock(return_value={"retcode": 0}) + ), patch("salt.utils.path.which", MagicMock(return_value="/usr/bin/pgsql")): + with patch("salt.modules.postgres.has_privileges", Mock(return_value=False)): + ret = postgres.privileges_grant( + "baruwa", + "awl", + "table", + "ALL", + grant_option=True, + maintenance_db="db_name", + runas="user", + host="testhost", + port="testport", + user="testuser", + password="testpassword", + ) + + query = 'GRANT ALL ON TABLE public."awl" TO "baruwa" WITH GRANT OPTION' + + postgres._run_psql.assert_called_once_with( + [ + "/usr/bin/pgsql", + "--no-align", + "--no-readline", + "--no-psqlrc", + "--no-password", + "--username", + "testuser", + "--host", + "testhost", + "--port", + "testport", + "--dbname", + "db_name", + "-c", + query, + ], + host="testhost", + port="testport", + password="testpassword", + user="testuser", + runas="user", + ) + + with patch( + "salt.modules.postgres._run_psql", Mock(return_value={"retcode": 0}) + ), patch("salt.utils.path.which", MagicMock(return_value="/usr/bin/pgsql")), patch( + "salt.modules.postgres.has_privileges", Mock(return_value=False) + ): + ret = postgres.privileges_grant( + "baruwa", + "awl", + "table", + "ALL", + maintenance_db="db_name", + runas="user", + host="testhost", + port="testport", + user="testuser", + password="testpassword", + ) + + query = 'GRANT ALL ON TABLE public."awl" TO "baruwa"' + + postgres._run_psql.assert_called_once_with( + [ + "/usr/bin/pgsql", + "--no-align", + "--no-readline", + "--no-psqlrc", + "--no-password", + "--username", + "testuser", + "--host", + "testhost", + "--port", + "testport", + "--dbname", + "db_name", + "-c", + query, + ], + host="testhost", + port="testport", + password="testpassword", + user="testuser", + runas="user", + ) + + # Test grant on all tables + with patch( + "salt.modules.postgres._run_psql", Mock(return_value={"retcode": 0}) + ), patch("salt.utils.path.which", MagicMock(return_value="/usr/bin/pgsql")), patch( + "salt.modules.postgres.has_privileges", Mock(return_value=False) + ): + ret = postgres.privileges_grant( + "baruwa", + "ALL", + "table", + "SELECT", + maintenance_db="db_name", + runas="user", + host="testhost", + port="testport", + user="testuser", + password="testpassword", + ) + + query = 'GRANT SELECT ON ALL TABLES IN SCHEMA public TO "baruwa"' + + postgres._run_psql.assert_called_once_with( + [ + "/usr/bin/pgsql", + "--no-align", + "--no-readline", + "--no-psqlrc", + "--no-password", + "--username", + "testuser", + "--host", + "testhost", + "--port", + "testport", + "--dbname", + "db_name", + "-c", + query, + ], + host="testhost", + port="testport", + password="testpassword", + user="testuser", + runas="user", + ) + + +def test_privileges_grant_group(): + """ + Test granting privileges on group + """ + with patch( + "salt.modules.postgres._run_psql", Mock(return_value={"retcode": 0}) + ), patch("salt.utils.path.which", MagicMock(return_value="/usr/bin/pgsql")), patch( + "salt.modules.postgres.has_privileges", Mock(return_value=False) + ): + ret = postgres.privileges_grant( + "baruwa", + "admins", + "group", + grant_option=True, + maintenance_db="db_name", + runas="user", + host="testhost", + port="testport", + user="testuser", + password="testpassword", + ) + + query = 'GRANT admins TO "baruwa" WITH ADMIN OPTION' + + postgres._run_psql.assert_called_once_with( + [ + "/usr/bin/pgsql", + "--no-align", + "--no-readline", + "--no-psqlrc", + "--no-password", + "--username", + "testuser", + "--host", + "testhost", + "--port", + "testport", + "--dbname", + "db_name", + "-c", + query, + ], + host="testhost", + port="testport", + password="testpassword", + user="testuser", + runas="user", + ) + + with patch( + "salt.modules.postgres._run_psql", Mock(return_value={"retcode": 0}) + ), patch("salt.utils.path.which", MagicMock(return_value="/usr/bin/pgsql")), patch( + "salt.modules.postgres.has_privileges", Mock(return_value=False) + ): + ret = postgres.privileges_grant( + "baruwa", + "admins", + "group", + maintenance_db="db_name", + runas="user", + host="testhost", + port="testport", + user="testuser", + password="testpassword", + ) + + query = 'GRANT admins TO "baruwa"' + + postgres._run_psql.assert_called_once_with( + [ + "/usr/bin/pgsql", + "--no-align", + "--no-readline", + "--no-psqlrc", + "--no-password", + "--username", + "testuser", + "--host", + "testhost", + "--port", + "testport", + "--dbname", + "db_name", + "-c", + query, + ], + host="testhost", + port="testport", + password="testpassword", + user="testuser", + runas="user", + ) + + +def test_privileges_revoke_table(): + """ + Test revoking privileges on table + """ + with patch( + "salt.modules.postgres._run_psql", Mock(return_value={"retcode": 0}) + ), patch("salt.utils.path.which", MagicMock(return_value="/usr/bin/pgsql")), patch( + "salt.modules.postgres.has_privileges", Mock(return_value=True) + ): + ret = postgres.privileges_revoke( + "baruwa", + "awl", + "table", + "ALL", + maintenance_db="db_name", + runas="user", + host="testhost", + port="testport", + user="testuser", + password="testpassword", + ) + + query = "REVOKE ALL ON TABLE public.awl FROM baruwa" + + postgres._run_psql.assert_called_once_with( + [ + "/usr/bin/pgsql", + "--no-align", + "--no-readline", + "--no-psqlrc", + "--no-password", + "--username", + "testuser", + "--host", + "testhost", + "--port", + "testport", + "--dbname", + "db_name", + "-c", + query, + ], + host="testhost", + port="testport", + password="testpassword", + user="testuser", + runas="user", + ) + + +def test_privileges_revoke_group(): + """ + Test revoking privileges on group + """ + with patch( + "salt.modules.postgres._run_psql", Mock(return_value={"retcode": 0}) + ), patch("salt.utils.path.which", MagicMock(return_value="/usr/bin/pgsql")), patch( + "salt.modules.postgres.has_privileges", Mock(return_value=True) + ): + ret = postgres.privileges_revoke( + "baruwa", + "admins", + "group", + maintenance_db="db_name", + runas="user", + host="testhost", + port="testport", + user="testuser", + password="testpassword", + ) + + query = "REVOKE admins FROM baruwa" + + postgres._run_psql.assert_called_once_with( + [ + "/usr/bin/pgsql", + "--no-align", + "--no-readline", + "--no-psqlrc", + "--no-password", + "--username", + "testuser", + "--host", + "testhost", + "--port", + "testport", + "--dbname", + "db_name", + "-c", + query, + ], + host="testhost", + port="testport", + password="testpassword", + user="testuser", + runas="user", + ) + + +def test_datadir_init(): + """ + Test Initializing a postgres data directory + """ + with patch("salt.modules.postgres._run_initdb", Mock(return_value={"retcode": 0})): + with patch("salt.modules.postgres.datadir_exists", Mock(return_value=False)): + name = "/var/lib/pgsql/data" + ret = postgres.datadir_init( + name, user="postgres", password="test", runas="postgres" + ) + postgres._run_initdb.assert_called_once_with( + name, + auth="password", + encoding="UTF8", + locale=None, + password="test", + runas="postgres", + checksums=False, + waldir=None, + user="postgres", + ) + assert ret is True + + +def test_datadir_exists(): + """ + Test Checks if postgres data directory has been initialized + """ + with patch("os.path.isfile", Mock(return_value=True)): + name = "/var/lib/pgsql/data" + ret = postgres.datadir_exists(name) + assert ret is True + + +@pytest.mark.parametrize( + "v1,v2,result", + ( + ("8.5", "9.5", True), + ("8.5", "8.6", True), + ("8.5.2", "8.5.3", True), + ("9.5", "8.5", False), + ("9.5", "9.6", True), + ("9.5.0", "9.5.1", True), + ("9.5", "9.5.1", True), + ("9.5.1", "9.5", False), + ("9.5b", "9.5a", False), + ("10a", "10b", True), + ("1.2.3.4", "1.2.3.5", True), + ("10dev", "10next", True), + ("10next", "10dev", False), + ), +) +def test_pg_is_older_ext_ver(v1, v2, result): + """ + Test Checks if postgres extension version string is older + """ + assert postgres._pg_is_older_ext_ver(v1, v2) is result + + +def test_tablespace_create(): + with patch( + "salt.modules.postgres._run_psql", Mock(return_value={"retcode": 0}) + ), patch("salt.utils.path.which", MagicMock(return_value="/usr/bin/pgsql")): + postgres.tablespace_create( + "test_tablespace", + "/tmp/postgres_test_tablespace", + user="testuser", + host="testhost", + port="testport", + maintenance_db="maint_db", + password="foo", + owner="otheruser", + runas="foo", + ) + + postgres._run_psql.assert_called_once_with( + [ + "/usr/bin/pgsql", + "--no-align", + "--no-readline", + "--no-psqlrc", + "--no-password", + "--username", + "testuser", + "--host", + "testhost", + "--port", + "testport", + "--dbname", + "maint_db", + "-c", + 'CREATE TABLESPACE "test_tablespace" OWNER "otheruser" LOCATION \'/tmp/postgres_test_tablespace\' ', + ], + runas="foo", + password="foo", + host="testhost", + port="testport", + user="testuser", + ) + + +def test_tablespace_list(): + with patch( + "salt.modules.postgres._run_psql", Mock(return_value={"retcode": 0}) + ), patch( + "salt.utils.path.which", MagicMock(return_value="/usr/bin/pgsql") + ), patch.dict( + postgres.__salt__, + { + "postgres.psql_query": MagicMock( + return_value=[ + { + "Name": "pg_global", + "Owner": "postgres", + "ACL": "", + "Opts": "", + "Location": "", + }, + { + "Name": "pg_default", + "Owner": "postgres", + "ACL": "", + "Opts": "", + "Location": "", + }, + { + "Name": "test_tablespace", + "Owner": "testuser", + "ACL": "", + "Opts": "", + "Location": "/tmp/posgrest_test_tablespace", + }, + ] + ), + }, + ): + ret = postgres.tablespace_list( + user="testuser", + host="testhost", + port="testport", + maintenance_db="maint_db", + password="foo", + runas="foo", + ) + + expected_data = { + "pg_global": {"Owner": "postgres", "ACL": "", "Opts": "", "Location": ""}, + "pg_default": {"Owner": "postgres", "ACL": "", "Opts": "", "Location": ""}, + "test_tablespace": { + "Owner": "testuser", + "ACL": "", + "Opts": "", + "Location": "/tmp/posgrest_test_tablespace", + }, + } + assert ret == expected_data + + +def test_tablespace_exists_true(): + with patch( + "salt.modules.postgres._run_psql", Mock(return_value={"retcode": 0}) + ), patch( + "salt.utils.path.which", MagicMock(return_value="/usr/bin/pgsql") + ), patch.dict( + postgres.__salt__, + { + "postgres.psql_query": MagicMock( + return_value=[ + { + "Name": "pg_global", + "Owner": "postgres", + "ACL": "", + "Opts": "", + "Location": "", + }, + { + "Name": "pg_default", + "Owner": "postgres", + "ACL": "", + "Opts": "", + "Location": "", + }, + { + "Name": "test_tablespace", + "Owner": "testuser", + "ACL": "", + "Opts": "", + "Location": "/tmp/posgrest_test_tablespace", + }, + ] + ), + }, + ): + ret = postgres.tablespace_exists( + "test_tablespace", + user="testuser", + host="testhost", + port="testport", + maintenance_db="maint_db", + password="foo", + runas="foo", + ) + assert ret is True + + +def test_tablespace_exists_false(): + with patch( + "salt.modules.postgres._run_psql", Mock(return_value={"retcode": 0}) + ), patch( + "salt.utils.path.which", MagicMock(return_value="/usr/bin/pgsql") + ), patch.dict( + postgres.__salt__, + { + "postgres.psql_query": MagicMock( + return_value=[ + { + "Name": "pg_global", + "Owner": "postgres", + "ACL": "", + "Opts": "", + "Location": "", + }, + { + "Name": "pg_default", + "Owner": "postgres", + "ACL": "", + "Opts": "", + "Location": "", + }, + { + "Name": "test_tablespace", + "Owner": "testuser", + "ACL": "", + "Opts": "", + "Location": "/tmp/posgrest_test_tablespace", + }, + ] + ), + }, + ): + ret = postgres.tablespace_exists( + "bad_test_tablespace", + user="testuser", + host="testhost", + port="testport", + maintenance_db="maint_db", + password="foo", + runas="foo", + ) + assert ret is False + + +def test_tablespace_alter_new_owner(): + with patch( + "salt.modules.postgres._run_psql", Mock(return_value={"retcode": 0}) + ), patch("salt.utils.path.which", MagicMock(return_value="/usr/bin/pgsql")): + postgres.tablespace_alter( + "test_tablespace", + user="testuser", + host="testhost", + port="testport", + maintenance_db="maint_db", + password="foo", + runas="foo", + new_owner="testuser", + ) + + postgres._run_psql.assert_called_once_with( + [ + "/usr/bin/pgsql", + "--no-align", + "--no-readline", + "--no-psqlrc", + "--no-password", + "--username", + "testuser", + "--host", + "testhost", + "--port", + "testport", + "--dbname", + "maint_db", + "-c", + 'ALTER TABLESPACE "test_tablespace" OWNER TO "testuser"', + ], + runas="foo", + password="foo", + host="testhost", + port="testport", + user="testuser", + ) + + +def test_tablespace_alter_new_name(): + with patch( + "salt.modules.postgres._run_psql", Mock(return_value={"retcode": 0}) + ), patch("salt.utils.path.which", MagicMock(return_value="/usr/bin/pgsql")): + postgres.tablespace_alter( + "test_tablespace", + user="testuser", + host="testhost", + port="testport", + maintenance_db="maint_db", + password="foo", + runas="foo", + new_name="test_tablespace2", + ) + + postgres._run_psql.assert_called_once_with( + [ + "/usr/bin/pgsql", + "--no-align", + "--no-readline", + "--no-psqlrc", + "--no-password", + "--username", + "testuser", + "--host", + "testhost", + "--port", + "testport", + "--dbname", + "maint_db", + "-c", + 'ALTER TABLESPACE "test_tablespace" RENAME TO "test_tablespace2"', + ], + runas="foo", + password="foo", + host="testhost", + port="testport", + user="testuser", + ) + + +def test_tablespace_remove(): + with patch( + "salt.modules.postgres._run_psql", Mock(return_value={"retcode": 0}) + ), patch("salt.utils.path.which", MagicMock(return_value="/usr/bin/pgsql")): + postgres.tablespace_remove( + "test_tablespace", + user="testuser", + host="testhost", + port="testport", + maintenance_db="maint_db", + password="foo", + runas="foo", + ) + + postgres._run_psql.assert_called_once_with( + [ + "/usr/bin/pgsql", + "--no-align", + "--no-readline", + "--no-psqlrc", + "--no-password", + "--username", + "testuser", + "--host", + "testhost", + "--port", + "testport", + "--dbname", + "maint_db", + "-c", + 'DROP TABLESPACE "test_tablespace"', + ], + runas="foo", + password="foo", + host="testhost", + port="testport", + user="testuser", + ) diff --git a/tests/unit/modules/test_deb_postgres.py b/tests/unit/modules/test_deb_postgres.py deleted file mode 100644 index 37e276fdffda..000000000000 --- a/tests/unit/modules/test_deb_postgres.py +++ /dev/null @@ -1,184 +0,0 @@ -import salt.modules.deb_postgres as deb_postgres -from tests.support.mixins import LoaderModuleMockMixin -from tests.support.mock import Mock, patch -from tests.support.unit import TestCase - -LSCLUSTER = """\ -8.4 main 5432 online postgres /srv/8.4/main \ - /var/log/postgresql/postgresql-8.4-main.log -9.1 main 5433 online postgres /srv/9.1/main \ - /var/log/postgresql/postgresql-9.1-main.log -""" - - -class PostgresClusterTestCase(TestCase, LoaderModuleMockMixin): - def setup_loader_modules(self): - self.cmd_run_all_mock = Mock(return_value={"stdout": LSCLUSTER}) - self.addCleanup(delattr, self, "cmd_run_all_mock") - patcher = patch( - "salt.utils.path.which", Mock(return_value="/usr/bin/pg_createcluster") - ) - patcher.start() - self.addCleanup(patcher.stop) - return { - deb_postgres: { - "__salt__": { - "config.option": Mock(), - "cmd.run_all": self.cmd_run_all_mock, - "file.chown": Mock(), - "file.remove": Mock(), - } - } - } - - def test_cluster_create(self): - deb_postgres.cluster_create( - "9.3", - "main", - port="5432", - locale="fr_FR", - encoding="UTF-8", - datadir="/opt/postgresql", - ) - cmdstr = ( - "/usr/bin/pg_createcluster " - "--port 5432 --locale fr_FR --encoding UTF-8 " - "--datadir /opt/postgresql " - "9.3 main" - ) - self.assertEqual(cmdstr, self.cmd_run_all_mock.call_args[0][0]) - - def test_cluster_create_with_initdb_options(self): - deb_postgres.cluster_create( - "11", - "main", - port="5432", - locale="fr_FR", - encoding="UTF-8", - datadir="/opt/postgresql", - allow_group_access=True, - data_checksums=True, - wal_segsize="32", - ) - cmdstr = ( - "/usr/bin/pg_createcluster " - "--port 5432 --locale fr_FR --encoding UTF-8 " - "--datadir /opt/postgresql " - "11 main " - "-- " - "--allow-group-access " - "--data-checksums " - "--wal-segsize 32" - ) - self.assertEqual(cmdstr, self.cmd_run_all_mock.call_args[0][0]) - - def test_cluster_create_with_float(self): - deb_postgres.cluster_create( - 9.3, - "main", - port="5432", - locale="fr_FR", - encoding="UTF-8", - datadir="/opt/postgresql", - ) - cmdstr = ( - "/usr/bin/pg_createcluster " - "--port 5432 --locale fr_FR --encoding UTF-8 " - "--datadir /opt/postgresql " - "9.3 main" - ) - self.assertEqual(cmdstr, self.cmd_run_all_mock.call_args[0][0]) - - -class PostgresLsClusterTestCase(TestCase, LoaderModuleMockMixin): - def setup_loader_modules(self): - self.cmd_run_all_mock = Mock(return_value={"stdout": LSCLUSTER}) - self.addCleanup(delattr, self, "cmd_run_all_mock") - patcher = patch( - "salt.utils.path.which", Mock(return_value="/usr/bin/pg_lsclusters") - ) - patcher.start() - self.addCleanup(patcher.stop) - return { - deb_postgres: { - "__salt__": { - "config.option": Mock(), - "cmd.run_all": self.cmd_run_all_mock, - "file.chown": Mock(), - "file.remove": Mock(), - } - } - } - - def test_parse_pg_lsclusters(self): - stdout = LSCLUSTER - self.maxDiff = None - self.assertDictEqual( - { - "8.4/main": { - "port": 5432, - "status": "online", - "user": "postgres", - "datadir": "/srv/8.4/main", - "log": "/var/log/postgresql/postgresql-8.4-main.log", - }, - "9.1/main": { - "port": 5433, - "status": "online", - "user": "postgres", - "datadir": "/srv/9.1/main", - "log": "/var/log/postgresql/postgresql-9.1-main.log", - }, - }, - deb_postgres._parse_pg_lscluster(stdout), - ) - - def test_cluster_list(self): - return_list = deb_postgres.cluster_list() - self.assertEqual( - "/usr/bin/pg_lsclusters --no-header", self.cmd_run_all_mock.call_args[0][0] - ) - return_dict = deb_postgres.cluster_list(verbose=True) - self.assertIsInstance(return_dict, dict) - - def test_cluster_exists(self): - self.assertTrue(deb_postgres.cluster_exists("8.4") is True) - self.assertTrue(deb_postgres.cluster_exists("8.4", "main") is True) - self.assertFalse(deb_postgres.cluster_exists("3.4", "main")) - - -class PostgresDeleteClusterTestCase(TestCase, LoaderModuleMockMixin): - def setup_loader_modules(self): - self.cmd_run_all_mock = Mock(return_value={"stdout": LSCLUSTER}) - self.addCleanup(delattr, self, "cmd_run_all_mock") - patcher = patch( - "salt.utils.path.which", Mock(return_value="/usr/bin/pg_dropcluster") - ) - patcher.start() - self.addCleanup(patcher.stop) - return { - deb_postgres: { - "__salt__": { - "config.option": Mock(), - "cmd.run_all": self.cmd_run_all_mock, - "file.chown": Mock(), - "file.remove": Mock(), - } - } - } - - def test_cluster_delete(self): - deb_postgres.cluster_remove("9.3", "main") - self.assertEqual( - "/usr/bin/pg_dropcluster 9.3 main", self.cmd_run_all_mock.call_args[0][0] - ) - deb_postgres.cluster_remove("9.3", "main", stop=True) - self.assertEqual( - "/usr/bin/pg_dropcluster --stop 9.3 main", - self.cmd_run_all_mock.call_args[0][0], - ) - deb_postgres.cluster_remove(9.3, "main", stop=True) - self.assertEqual( - "/usr/bin/pg_dropcluster --stop 9.3 main", - self.cmd_run_all_mock.call_args[0][0], - ) diff --git a/tests/unit/modules/test_postgres.py b/tests/unit/modules/test_postgres.py deleted file mode 100644 index 6f77fc039029..000000000000 --- a/tests/unit/modules/test_postgres.py +++ /dev/null @@ -1,2086 +0,0 @@ -import datetime -import logging -import re - -import salt.modules.postgres as postgres -from salt.exceptions import SaltInvocationError -from tests.support.mixins import LoaderModuleMockMixin -from tests.support.mock import Mock, call, patch -from tests.support.unit import TestCase - -test_list_db_csv = ( - "Name,Owner,Encoding,Collate,Ctype,Access privileges,Tablespace\n" - "template1,postgres,LATIN1,en_US,en_US" - ',"{=c/postgres,postgres=CTc/postgres}",pg_default\n' - "template0,postgres,LATIN1,en_US,en_US" - ',"{=c/postgres,postgres=CTc/postgres}",pg_default\n' - "postgres,postgres,LATIN1,en_US,en_US,,pg_default\n" - "test_db,postgres,LATIN1,en_US,en_US,,pg_default" -) - -test_list_schema_csv = ( - "name,owner,acl\n" - 'public,postgres,"{postgres=UC/postgres,=UC/postgres}"\n' - 'pg_toast,postgres,""' -) - -test_list_language_csv = "Name\ninternal\nc\nsql\nplpgsql\n" - -test_privileges_list_table_csv = ( - "name\n" - '"{baruwatest=arwdDxt/baruwatest,bayestest=arwd/baruwatest,baruwa=a*r*w*d*D*x*t*/baruwatest}"\n' -) - -test_privileges_list_group_csv = ( - "rolname,admin_option\nbaruwa,f\nbaruwatest2,t\nbaruwatest,f\n" -) - -log = logging.getLogger(__name__) - - -class PostgresTestCase(TestCase, LoaderModuleMockMixin): - def setup_loader_modules(self): - patcher = patch("salt.utils.path.which", Mock(return_value="/usr/bin/pgsql")) - patcher.start() - self.addCleanup(patcher.stop) - return { - postgres: { - "__grains__": {"os_family": "Linux"}, - "__salt__": { - "config.option": Mock(), - "cmd.run_all": Mock(), - "file.chown": Mock(), - "file.remove": Mock(), - }, - } - } - - def test_run_psql(self): - postgres._run_psql('echo "hi"') - cmd = postgres.__salt__["cmd.run_all"] - - self.assertEqual("postgres", cmd.call_args[1]["runas"]) - - def test_db_alter(self): - with patch( - "salt.modules.postgres._run_psql", Mock(return_value={"retcode": 0}) - ): - postgres.db_alter( - "dbname", - user="testuser", - host="testhost", - port="testport", - maintenance_db="maint_db", - password="foo", - tablespace="testspace", - owner="otheruser", - runas="foo", - ) - postgres._run_psql.assert_has_calls( - [ - call( - [ - "/usr/bin/pgsql", - "--no-align", - "--no-readline", - "--no-psqlrc", - "--no-password", - "--username", - "testuser", - "--host", - "testhost", - "--port", - "testport", - "--dbname", - "maint_db", - "-c", - 'ALTER DATABASE "dbname" OWNER TO "otheruser"', - ], - host="testhost", - user="testuser", - password="foo", - runas="foo", - port="testport", - ), - call( - [ - "/usr/bin/pgsql", - "--no-align", - "--no-readline", - "--no-psqlrc", - "--no-password", - "--username", - "testuser", - "--host", - "testhost", - "--port", - "testport", - "--dbname", - "maint_db", - "-c", - 'ALTER DATABASE "dbname" SET TABLESPACE "testspace"', - ], - host="testhost", - user="testuser", - password="foo", - runas="foo", - port="testport", - ), - ] - ) - - def test_db_alter_owner_recurse(self): - with patch( - "salt.modules.postgres.owner_to", Mock(return_value={"retcode": None}) - ): - postgres.db_alter( - "dbname", - user="testuser", - host="testhost", - port="testport", - maintenance_db="maint_db", - password="foo", - tablespace="testspace", - owner="otheruser", - owner_recurse=True, - runas="foo", - ) - postgres.owner_to.assert_called_once_with( - "dbname", - "otheruser", - user="testuser", - host="testhost", - port="testport", - password="foo", - runas="foo", - ) - - def test_db_create(self): - with patch( - "salt.modules.postgres._run_psql", Mock(return_value={"retcode": 0}) - ): - postgres.db_create( - "dbname", - user="testuser", - host="testhost", - port="testport", - maintenance_db="maint_db", - password="foo", - tablespace="testspace", - owner="otheruser", - runas="foo", - ) - - postgres._run_psql.assert_called_once_with( - [ - "/usr/bin/pgsql", - "--no-align", - "--no-readline", - "--no-psqlrc", - "--no-password", - "--username", - "testuser", - "--host", - "testhost", - "--port", - "testport", - "--dbname", - "maint_db", - "-c", - 'CREATE DATABASE "dbname" WITH TABLESPACE = "testspace" ' - 'OWNER = "otheruser"', - ], - host="testhost", - user="testuser", - password="foo", - runas="foo", - port="testport", - ) - - def test_db_create_empty_string_param(self): - with patch( - "salt.modules.postgres._run_psql", Mock(return_value={"retcode": 0}) - ): - postgres.db_create( - "dbname", - lc_collate="", - encoding="utf8", - user="testuser", - host="testhost", - port=1234, - maintenance_db="maint_db", - password="foo", - ) - - postgres._run_psql.assert_called_once_with( - [ - "/usr/bin/pgsql", - "--no-align", - "--no-readline", - "--no-psqlrc", - "--no-password", - "--username", - "testuser", - "--host", - "testhost", - "--port", - "1234", - "--dbname", - "maint_db", - "-c", - "CREATE DATABASE \"dbname\" WITH ENCODING = 'utf8' LC_COLLATE = ''", - ], - host="testhost", - password="foo", - port=1234, - runas=None, - user="testuser", - ) - - def test_db_create_with_trivial_sql_injection(self): - with patch( - "salt.modules.postgres._run_psql", Mock(return_value={"retcode": 0}) - ): - self.assertRaises( - SaltInvocationError, - postgres.db_create, - "dbname", - lc_collate="foo' ENCODING='utf8", - ) - - def test_db_exists(self): - with patch( - "salt.modules.postgres._run_psql", - Mock(return_value={"retcode": 0, "stdout": test_list_db_csv}), - ): - ret = postgres.db_exists( - "test_db", - user="testuser", - host="testhost", - port="testport", - maintenance_db="maint_db", - password="foo", - runas="foo", - ) - self.assertTrue(ret) - - def test_db_list(self): - with patch( - "salt.modules.postgres._run_psql", - Mock(return_value={"retcode": 0, "stdout": test_list_db_csv}), - ): - ret = postgres.db_list( - user="testuser", - host="testhost", - port="testport", - maintenance_db="maint_db", - password="foo", - runas="foo", - ) - self.assertDictEqual( - ret, - { - "test_db": { - "Encoding": "LATIN1", - "Ctype": "en_US", - "Tablespace": "pg_default", - "Collate": "en_US", - "Owner": "postgres", - "Access privileges": "", - }, - "template1": { - "Encoding": "LATIN1", - "Ctype": "en_US", - "Tablespace": "pg_default", - "Collate": "en_US", - "Owner": "postgres", - "Access privileges": "{=c/postgres,postgres=CTc/postgres}", - }, - "template0": { - "Encoding": "LATIN1", - "Ctype": "en_US", - "Tablespace": "pg_default", - "Collate": "en_US", - "Owner": "postgres", - "Access privileges": "{=c/postgres,postgres=CTc/postgres}", - }, - "postgres": { - "Encoding": "LATIN1", - "Ctype": "en_US", - "Tablespace": "pg_default", - "Collate": "en_US", - "Owner": "postgres", - "Access privileges": "", - }, - }, - ) - - def test_db_remove(self): - with patch( - "salt.modules.postgres._run_psql", Mock(return_value={"retcode": 0}) - ): - postgres.db_remove( - "test_db", - user="testuser", - host="testhost", - port="testport", - maintenance_db="maint_db", - password="foo", - runas="foo", - ) - - calls = ( - call( - [ - "/usr/bin/pgsql", - "--no-align", - "--no-readline", - "--no-psqlrc", - "--no-password", - "--username", - "testuser", - "--host", - "testhost", - "--port", - "testport", - "--dbname", - "maint_db", - "-c", - 'REVOKE CONNECT ON DATABASE "test_db" FROM public;', - ], - host="testhost", - password="foo", - port="testport", - runas="foo", - user="testuser", - ), - call( - [ - "/usr/bin/pgsql", - "--no-align", - "--no-readline", - "--no-psqlrc", - "--no-password", - "--username", - "testuser", - "--host", - "testhost", - "--port", - "testport", - "--dbname", - "maint_db", - "-c", - "SELECT pid, pg_terminate_backend(pid) FROM pg_stat_activity" - " WHERE datname = 'test_db' AND pid <> pg_backend_pid();", - ], - host="testhost", - password="foo", - port="testport", - runas="foo", - user="testuser", - ), - call( - [ - "/usr/bin/pgsql", - "--no-align", - "--no-readline", - "--no-psqlrc", - "--no-password", - "--username", - "testuser", - "--host", - "testhost", - "--port", - "testport", - "--dbname", - "maint_db", - "-c", - 'DROP DATABASE "test_db";', - ], - host="testhost", - password="foo", - port="testport", - runas="foo", - user="testuser", - ), - ) - - postgres._run_psql.assert_has_calls(calls, any_order=True) - - def test_group_create(self): - with patch( - "salt.modules.postgres._run_psql", Mock(return_value={"retcode": 0}) - ): - with patch("salt.modules.postgres.user_exists", Mock(return_value=False)): - postgres.group_create( - "testgroup", - user="testuser", - host="testhost", - port="testport", - maintenance_db="maint_db", - password="foo", - createdb=False, - encrypted=False, - superuser=False, - replication=False, - rolepassword="testrolepass", - groups="testgroup", - runas="foo", - ) - # postgres._run_psql.call_args[0][0] will contain the list of CLI args. - # The first 14 elements of this list are initial args used in all (or - # virtually all) commands run through _run_psql(), so the actual SQL - # query will be in the 15th argument. - self.assertTrue( - postgres._run_psql.call_args[0][0][14].startswith("CREATE ROLE") - ) - - def test_group_remove(self): - with patch( - "salt.modules.postgres._run_psql", Mock(return_value={"retcode": 0}) - ): - with patch("salt.modules.postgres.user_exists", Mock(return_value=True)): - postgres.group_remove( - "testgroup", - user="testuser", - host="testhost", - port="testport", - maintenance_db="maint_db", - password="foo", - runas="foo", - ) - postgres._run_psql.assert_called_once_with( - [ - "/usr/bin/pgsql", - "--no-align", - "--no-readline", - "--no-psqlrc", - "--no-password", - "--username", - "testuser", - "--host", - "testhost", - "--port", - "testport", - "--dbname", - "maint_db", - "-c", - 'DROP ROLE "testgroup"', - ], - host="testhost", - user="testuser", - password="foo", - runas="foo", - port="testport", - ) - - def test_group_update(self): - with patch( - "salt.modules.postgres._run_psql", Mock(return_value={"retcode": 0}) - ): - with patch( - "salt.modules.postgres.role_get", - Mock(return_value={"superuser": False}), - ): - postgres.group_update( - "testgroup", - user='"testuser"', - host="testhost", - port="testport", - maintenance_db="maint_db", - password="foo", - createdb=False, - encrypted=False, - replication=False, - rolepassword="test_role_pass", - groups="testgroup", - runas="foo", - ) - # postgres._run_psql.call_args[0][0] will contain the list of CLI args. - # The first 14 elements of this list are initial args used in all (or - # virtually all) commands run through _run_psql(), so the actual SQL - # query will be in the 15th argument. - self.assertTrue( - re.match( - 'ALTER.* "testgroup" .* UNENCRYPTED PASSWORD', - postgres._run_psql.call_args[0][0][14], - ) - ) - - def test_user_create(self): - with patch( - "salt.modules.postgres._run_psql", Mock(return_value={"retcode": 0}) - ): - with patch("salt.modules.postgres.user_exists", Mock(return_value=False)): - postgres.user_create( - "testuser", - user="testuser", - host="testhost", - port="testport", - maintenance_db="maint_test", - password="test_pass", - login=True, - createdb=False, - createroles=False, - encrypted=False, - superuser=False, - replication=False, - rolepassword="test_role_pass", - valid_until="2042-07-01", - groups="test_groups", - runas="foo", - ) - # postgres._run_psql.call_args[0][0] will contain the list of CLI args. - # The first 14 elements of this list are initial args used in all (or - # virtually all) commands run through _run_psql(), so the actual SQL - # query will be in the 15th argument. - call = postgres._run_psql.call_args[0][0][14] - self.assertTrue(re.match('CREATE ROLE "testuser"', call)) - for i in ( - "INHERIT", - "NOCREATEDB", - "NOCREATEROLE", - "NOSUPERUSER", - "NOREPLICATION", - "LOGIN", - "UNENCRYPTED", - "PASSWORD", - "VALID UNTIL", - ): - self.assertTrue(i in call, "{} not in {}".format(i, call)) - - def test_user_exists(self): - with patch( - "salt.modules.postgres._run_psql", Mock(return_value={"retcode": 0}) - ): - with patch("salt.modules.postgres.version", Mock(return_value="9.1")): - with patch( - "salt.modules.postgres.psql_query", - Mock( - return_value=[ - { - "name": "test_user", - "superuser": "t", - "inherits privileges": "t", - "can create roles": "t", - "can create databases": "t", - "can update system catalogs": "t", - "can login": "t", - "replication": None, - "password": "test_password", - "connections": "-1", - "groups": "", - "expiry time": "", - "defaults variables": None, - } - ] - ), - ): - ret = postgres.user_exists( - "test_user", - user="test_user", - host="test_host", - port="test_port", - maintenance_db="maint_db", - password="test_password", - runas="foo", - ) - self.assertTrue(ret) - - def test_user_list(self): - with patch( - "salt.modules.postgres._run_psql", Mock(return_value={"retcode": 0}) - ): - with patch("salt.modules.postgres.version", Mock(return_value="9.1")): - with patch( - "salt.modules.postgres.psql_query", - Mock( - return_value=[ - { - "name": "test_user", - "superuser": "t", - "inherits privileges": "t", - "can create roles": "t", - "can create databases": "t", - "can update system catalogs": "t", - "can login": "t", - "replication": None, - "connections": "-1", - "groups": "", - "expiry time": "2017-08-16 08:57:46", - "defaults variables": None, - } - ] - ), - ): - ret = postgres.user_list( - "test_user", - host="test_host", - port="test_port", - maintenance_db="maint_db", - password="test_password", - runas="foo", - ) - - self.assertDictEqual( - ret, - { - "test_user": { - "superuser": True, - "defaults variables": None, - "can create databases": True, - "can create roles": True, - "connections": None, - "replication": None, - "expiry time": datetime.datetime( - 2017, 8, 16, 8, 57, 46 - ), - "can login": True, - "can update system catalogs": True, - "groups": [], - "inherits privileges": True, - } - }, - ) - - def test_user_remove(self): - with patch( - "salt.modules.postgres._run_psql", Mock(return_value={"retcode": 0}) - ): - with patch("salt.modules.postgres.version", Mock(return_value="9.1")): - with patch( - "salt.modules.postgres.user_exists", Mock(return_value=True) - ): - postgres.user_remove( - "testuser", - user="testuser", - host="testhost", - port="testport", - maintenance_db="maint_db", - password="testpassword", - runas="foo", - ) - postgres._run_psql.assert_called_once_with( - [ - "/usr/bin/pgsql", - "--no-align", - "--no-readline", - "--no-psqlrc", - "--no-password", - "--username", - "testuser", - "--host", - "testhost", - "--port", - "testport", - "--dbname", - "maint_db", - "-c", - 'DROP ROLE "testuser"', - ], - host="testhost", - port="testport", - user="testuser", - password="testpassword", - runas="foo", - ) - - def test_user_update(self): - with patch( - "salt.modules.postgres._run_psql", Mock(return_value={"retcode": 0}) - ): - with patch( - "salt.modules.postgres.role_get", - Mock(return_value={"superuser": False}), - ): - postgres.user_update( - "test_username", - user="test_user", - host="test_host", - port="test_port", - maintenance_db="test_maint", - password="test_pass", - createdb=False, - createroles=False, - encrypted=False, - inherit=True, - login=True, - replication=False, - rolepassword="test_role_pass", - valid_until="2017-07-01", - groups="test_groups", - runas="foo", - ) - # postgres._run_psql.call_args[0][0] will contain the list of CLI args. - # The first 14 elements of this list are initial args used in all (or - # virtually all) commands run through _run_psql(), so the actual SQL - # query will be in the 15th argument. - self.assertTrue( - re.match( - 'ALTER ROLE "test_username" WITH INHERIT NOCREATEDB ' - "NOCREATEROLE NOREPLICATION LOGIN " - "UNENCRYPTED PASSWORD ['\"]{0,5}test_role_pass['\"]{0,5} " - "VALID UNTIL '2017-07-01';" - ' GRANT "test_groups" TO "test_username"', - postgres._run_psql.call_args[0][0][14], - ) - ) - - def test_user_update2(self): - with patch( - "salt.modules.postgres._run_psql", Mock(return_value={"retcode": 0}) - ): - with patch( - "salt.modules.postgres.role_get", - Mock(return_value={"superuser": False}), - ): - postgres.user_update( - "test_username", - user="test_user", - host="test_host", - port="test_port", - maintenance_db="test_maint", - password="test_pass", - createdb=False, - createroles=True, - encrypted=False, - inherit=True, - login=True, - replication=False, - groups="test_groups", - runas="foo", - ) - # postgres._run_psql.call_args[0][0] will contain the list of CLI args. - # The first 14 elements of this list are initial args used in all (or - # virtually all) commands run through _run_psql(), so the actual SQL - # query will be in the 15th argument. - self.assertTrue( - re.match( - 'ALTER ROLE "test_username" WITH INHERIT NOCREATEDB ' - "CREATEROLE NOREPLICATION LOGIN;" - ' GRANT "test_groups" TO "test_username"', - postgres._run_psql.call_args[0][0][14], - ) - ) - - def test_user_update3(self): - with patch( - "salt.modules.postgres._run_psql", Mock(return_value={"retcode": 0}) - ): - with patch( - "salt.modules.postgres.role_get", - Mock(return_value={"superuser": False}), - ): - postgres.user_update( - "test_username", - user="test_user", - host="test_host", - port="test_port", - maintenance_db="test_maint", - password="test_pass", - createdb=False, - createroles=True, - encrypted=False, - inherit=True, - login=True, - rolepassword=False, - replication=False, - groups="test_groups", - runas="foo", - ) - # postgres._run_psql.call_args[0][0] will contain the list of CLI args. - # The first 14 elements of this list are initial args used in all (or - # virtually all) commands run through _run_psql(), so the actual SQL - # query will be in the 15th argument. - self.assertTrue( - re.match( - 'ALTER ROLE "test_username" WITH INHERIT NOCREATEDB ' - "CREATEROLE NOREPLICATION LOGIN NOPASSWORD;" - ' GRANT "test_groups" TO "test_username"', - postgres._run_psql.call_args[0][0][14], - ) - ) - - def test_user_update_encrypted_passwd(self): - with patch( - "salt.modules.postgres._run_psql", Mock(return_value={"retcode": 0}) - ): - with patch( - "salt.modules.postgres.role_get", - Mock(return_value={"superuser": False}), - ): - postgres.user_update( - "test_username", - user="test_user", - host="test_host", - port="test_port", - maintenance_db="test_maint", - password="test_pass", - createdb=False, - createroles=True, - encrypted=True, - inherit=True, - login=True, - rolepassword="foobar", - replication=False, - groups="test_groups", - runas="foo", - ) - # postgres._run_psql.call_args[0][0] will contain the list of CLI args. - # The first 14 elements of this list are initial args used in all (or - # virtually all) commands run through _run_psql(), so the actual SQL - # query will be in the 15th argument. - self.assertTrue( - re.match( - 'ALTER ROLE "test_username" WITH INHERIT NOCREATEDB ' - "CREATEROLE NOREPLICATION LOGIN " - "ENCRYPTED PASSWORD " - "['\"]{0,5}md531c27e68d3771c392b52102c01be1da1['\"]{0,5}" - '; GRANT "test_groups" TO "test_username"', - postgres._run_psql.call_args[0][0][14], - ) - ) - - def test_version(self): - with patch( - "salt.modules.postgres._run_psql", - Mock(return_value={"retcode": 0, "stdout": "9.1.9"}), - ): - postgres.version( - user="test_user", - host="test_host", - port="test_port", - maintenance_db="test_maint", - password="test_pass", - runas="foo", - ) - # postgres._run_psql.call_args[0][0] will contain the list of CLI args. - # The first 14 elements of this list are initial args used in all (or - # virtually all) commands run through _run_psql(), so the actual SQL - # query will be in the 15th argument. - self.assertTrue( - re.match( - "SELECT setting FROM pg_catalog.pg_settings", - postgres._run_psql.call_args[0][0][14], - ) - ) - - def test_installed_extensions(self): - with patch( - "salt.modules.postgres.psql_query", - Mock(return_value=[{"extname": "foo", "extversion": "1"}]), - ): - exts = postgres.installed_extensions() - self.assertEqual(exts, {"foo": {"extversion": "1", "extname": "foo"}}) - - def test_available_extensions(self): - with patch( - "salt.modules.postgres.psql_query", - Mock(return_value=[{"name": "foo", "default_version": "1"}]), - ): - exts = postgres.available_extensions() - self.assertEqual(exts, {"foo": {"default_version": "1", "name": "foo"}}) - - def test_drop_extension2(self): - with patch( - "salt.modules.postgres.installed_extensions", Mock(side_effect=[{}, {}]) - ): - with patch( - "salt.modules.postgres._psql_prepare_and_run", Mock(return_value=None) - ): - with patch( - "salt.modules.postgres.available_extensions", - Mock(return_value={"foo": {"default_version": "1", "name": "foo"}}), - ): - self.assertEqual(postgres.drop_extension("foo"), True) - - def test_drop_extension3(self): - with patch( - "salt.modules.postgres.installed_extensions", - Mock(side_effect=[{"foo": {"extversion": "1", "extname": "foo"}}, {}]), - ): - with patch( - "salt.modules.postgres._psql_prepare_and_run", Mock(return_value=None) - ): - with patch( - "salt.modules.postgres.available_extensions", - Mock(return_value={"foo": {"default_version": "1", "name": "foo"}}), - ): - self.assertEqual(postgres.drop_extension("foo"), True) - - def test_drop_extension1(self): - with patch( - "salt.modules.postgres.installed_extensions", - Mock( - side_effect=[ - {"foo": {"extversion": "1", "extname": "foo"}}, - {"foo": {"extversion": "1", "extname": "foo"}}, - ] - ), - ): - with patch( - "salt.modules.postgres._psql_prepare_and_run", Mock(return_value=None) - ): - with patch( - "salt.modules.postgres.available_extensions", - Mock(return_value={"foo": {"default_version": "1", "name": "foo"}}), - ): - self.assertEqual(postgres.drop_extension("foo"), False) - - def test_create_mtdata(self): - with patch( - "salt.modules.postgres.installed_extensions", - Mock( - return_value={ - "foo": { - "extversion": "0.8", - "extrelocatable": "t", - "schema_name": "foo", - "extname": "foo", - } - }, - ), - ): - with patch( - "salt.modules.postgres.available_extensions", - Mock(return_value={"foo": {"default_version": "1.4", "name": "foo"}}), - ): - ret = postgres.create_metadata("foo", schema="bar", ext_version="1.4") - self.assertTrue(postgres._EXTENSION_INSTALLED in ret) - self.assertTrue(postgres._EXTENSION_TO_UPGRADE in ret) - self.assertTrue(postgres._EXTENSION_TO_MOVE in ret) - ret = postgres.create_metadata("foo", schema="foo", ext_version="0.4") - self.assertTrue(postgres._EXTENSION_INSTALLED in ret) - self.assertFalse(postgres._EXTENSION_TO_UPGRADE in ret) - self.assertFalse(postgres._EXTENSION_TO_MOVE in ret) - ret = postgres.create_metadata("foo") - self.assertTrue(postgres._EXTENSION_INSTALLED in ret) - self.assertFalse(postgres._EXTENSION_TO_UPGRADE in ret) - self.assertFalse(postgres._EXTENSION_TO_MOVE in ret) - ret = postgres.create_metadata("foobar") - self.assertTrue(postgres._EXTENSION_NOT_INSTALLED in ret) - self.assertFalse(postgres._EXTENSION_INSTALLED in ret) - self.assertFalse(postgres._EXTENSION_TO_UPGRADE in ret) - self.assertFalse(postgres._EXTENSION_TO_MOVE in ret) - - def test_create_extension_newerthan(self): - """ - scenario of creating upgrading extensions with possible schema and - version specifications - """ - with patch( - "salt.modules.postgres.create_metadata", - Mock( - side_effect=[ - # create succeeded - [postgres._EXTENSION_NOT_INSTALLED], - [postgres._EXTENSION_INSTALLED], - [postgres._EXTENSION_NOT_INSTALLED], - [postgres._EXTENSION_INSTALLED], - # create failed - [postgres._EXTENSION_NOT_INSTALLED], - [postgres._EXTENSION_NOT_INSTALLED], - # move+upgrade succeeded - [ - postgres._EXTENSION_TO_MOVE, - postgres._EXTENSION_TO_UPGRADE, - postgres._EXTENSION_INSTALLED, - ], - [postgres._EXTENSION_INSTALLED], - # move succeeded - [postgres._EXTENSION_TO_MOVE, postgres._EXTENSION_INSTALLED], - [postgres._EXTENSION_INSTALLED], - # upgrade succeeded - [postgres._EXTENSION_TO_UPGRADE, postgres._EXTENSION_INSTALLED], - [postgres._EXTENSION_INSTALLED], - # upgrade failed - [postgres._EXTENSION_TO_UPGRADE, postgres._EXTENSION_INSTALLED], - [postgres._EXTENSION_TO_UPGRADE, postgres._EXTENSION_INSTALLED], - # move failed - [postgres._EXTENSION_TO_MOVE, postgres._EXTENSION_INSTALLED], - [postgres._EXTENSION_TO_MOVE, postgres._EXTENSION_INSTALLED], - ] - ), - ): - with patch( - "salt.modules.postgres._psql_prepare_and_run", Mock(return_value=None) - ): - with patch( - "salt.modules.postgres.available_extensions", - Mock( - return_value={"foo": {"default_version": "1.4", "name": "foo"}} - ), - ): - self.assertTrue(postgres.create_extension("foo")) - self.assertTrue( - re.match( - 'CREATE EXTENSION IF NOT EXISTS "foo" ;', - postgres._psql_prepare_and_run.call_args[0][0][1], - ) - ) - self.assertTrue( - postgres.create_extension( - "foo", schema="a", ext_version="b", from_version="c" - ) - ) - self.assertTrue( - re.match( - 'CREATE EXTENSION IF NOT EXISTS "foo" ' - 'WITH SCHEMA "a" VERSION b FROM c ;', - postgres._psql_prepare_and_run.call_args[0][0][1], - ) - ) - self.assertFalse(postgres.create_extension("foo")) - ret = postgres.create_extension("foo", ext_version="a", schema="b") - self.assertTrue(ret) - self.assertTrue( - re.match( - 'ALTER EXTENSION "foo" SET SCHEMA "b";' - ' ALTER EXTENSION "foo" UPDATE TO a;', - postgres._psql_prepare_and_run.call_args[0][0][1], - ) - ) - ret = postgres.create_extension("foo", ext_version="a", schema="b") - self.assertTrue(ret) - self.assertTrue( - re.match( - 'ALTER EXTENSION "foo" SET SCHEMA "b";', - postgres._psql_prepare_and_run.call_args[0][0][1], - ) - ) - ret = postgres.create_extension("foo", ext_version="a", schema="b") - self.assertTrue(ret) - self.assertTrue( - re.match( - 'ALTER EXTENSION "foo" UPDATE TO a;', - postgres._psql_prepare_and_run.call_args[0][0][1], - ) - ) - self.assertFalse( - postgres.create_extension("foo", ext_version="a", schema="b") - ) - self.assertFalse( - postgres.create_extension("foo", ext_version="a", schema="b") - ) - - def test_encrypt_passwords(self): - self.assertEqual(postgres._maybe_encrypt_password("foo", "bar", False), "bar") - self.assertEqual( - postgres._maybe_encrypt_password("foo", "bar", True), - "md596948aad3fcae80c08a35c9b5958cd89", - ) - - def test_schema_list(self): - with patch( - "salt.modules.postgres._run_psql", - Mock(return_value={"retcode": 0, "stdout": test_list_schema_csv}), - ): - ret = postgres.schema_list( - "maint_db", - db_user="testuser", - db_host="testhost", - db_port="testport", - db_password="foo", - ) - self.assertDictEqual( - ret, - { - "public": { - "acl": "{postgres=UC/postgres,=UC/postgres}", - "owner": "postgres", - }, - "pg_toast": {"acl": "", "owner": "postgres"}, - }, - ) - - def test_schema_exists(self): - with patch( - "salt.modules.postgres._run_psql", Mock(return_value={"retcode": 0}) - ): - with patch( - "salt.modules.postgres.psql_query", - Mock( - return_value=[ - { - "name": "public", - "acl": "{postgres=UC/postgres,=UC/postgres}", - "owner": "postgres", - } - ] - ), - ): - ret = postgres.schema_exists("template1", "public") - self.assertTrue(ret) - - def test_schema_get(self): - with patch( - "salt.modules.postgres._run_psql", Mock(return_value={"retcode": 0}) - ): - with patch( - "salt.modules.postgres.psql_query", - Mock( - return_value=[ - { - "name": "public", - "acl": "{postgres=UC/postgres,=UC/postgres}", - "owner": "postgres", - } - ] - ), - ): - ret = postgres.schema_get("template1", "public") - self.assertTrue(ret) - - def test_schema_get_again(self): - with patch( - "salt.modules.postgres._run_psql", Mock(return_value={"retcode": 0}) - ): - with patch( - "salt.modules.postgres.psql_query", - Mock( - return_value=[ - { - "name": "public", - "acl": "{postgres=UC/postgres,=UC/postgres}", - "owner": "postgres", - } - ] - ), - ): - ret = postgres.schema_get("template1", "pg_toast") - self.assertFalse(ret) - - def test_schema_create(self): - with patch( - "salt.modules.postgres._run_psql", Mock(return_value={"retcode": 0}) - ): - with patch("salt.modules.postgres.schema_exists", Mock(return_value=False)): - postgres.schema_create( - "maint_db", - "testschema", - user="user", - db_host="testhost", - db_port="testport", - db_user="testuser", - db_password="testpassword", - ) - postgres._run_psql.assert_called_once_with( - [ - "/usr/bin/pgsql", - "--no-align", - "--no-readline", - "--no-psqlrc", - "--no-password", - "--username", - "testuser", - "--host", - "testhost", - "--port", - "testport", - "--dbname", - "maint_db", - "-c", - 'CREATE SCHEMA "testschema"', - ], - host="testhost", - port="testport", - password="testpassword", - user="testuser", - runas="user", - ) - - def test_schema_create2(self): - with patch("salt.modules.postgres.schema_exists", Mock(return_value=True)): - ret = postgres.schema_create( - "test_db", - "test_schema", - user="user", - db_host="test_host", - db_port="test_port", - db_user="test_user", - db_password="test_password", - ) - self.assertFalse(ret) - - def test_schema_remove(self): - with patch( - "salt.modules.postgres._run_psql", Mock(return_value={"retcode": 0}) - ): - with patch("salt.modules.postgres.schema_exists", Mock(return_value=True)): - postgres.schema_remove( - "maint_db", - "testschema", - user="user", - db_host="testhost", - db_port="testport", - db_user="testuser", - db_password="testpassword", - ) - postgres._run_psql.assert_called_once_with( - [ - "/usr/bin/pgsql", - "--no-align", - "--no-readline", - "--no-psqlrc", - "--no-password", - "--username", - "testuser", - "--host", - "testhost", - "--port", - "testport", - "--dbname", - "maint_db", - "-c", - 'DROP SCHEMA "testschema"', - ], - host="testhost", - port="testport", - password="testpassword", - user="testuser", - runas="user", - ) - - def test_schema_remove2(self): - with patch("salt.modules.postgres.schema_exists", Mock(return_value=False)): - ret = postgres.schema_remove( - "test_db", - "test_schema", - user="user", - db_host="test_host", - db_port="test_port", - db_user="test_user", - db_password="test_password", - ) - self.assertFalse(ret) - - def test_language_list(self): - """ - Test language listing - """ - with patch( - "salt.modules.postgres._run_psql", - Mock(return_value={"retcode": 0, "stdout": test_list_language_csv}), - ): - ret = postgres.language_list( - "testdb", - user="testuser", - host="testhost", - port="testport", - password="foo", - ) - self.assertDictEqual( - ret, - {"c": "c", "internal": "internal", "plpgsql": "plpgsql", "sql": "sql"}, - ) - - def test_language_exists(self): - """ - Test language existence check - """ - with patch( - "salt.modules.postgres._run_psql", Mock(return_value={"retcode": 0}) - ): - with patch( - "salt.modules.postgres.psql_query", - Mock( - return_value=[ - {"Name": "internal"}, - {"Name": "c"}, - {"Name": "sql"}, - {"Name": "plpgsql"}, - ] - ), - ): - with patch( - "salt.modules.postgres.language_exists", Mock(return_value=True) - ): - ret = postgres.language_exists("sql", "testdb") - self.assertTrue(ret) - - def test_language_create(self): - """ - Test language creation - does not exist in db - """ - with patch( - "salt.modules.postgres._run_psql", Mock(return_value={"retcode": 0}) - ): - with patch( - "salt.modules.postgres.language_exists", Mock(return_value=False) - ): - postgres.language_create( - "plpythonu", - "testdb", - runas="user", - host="testhost", - port="testport", - user="testuser", - password="testpassword", - ) - postgres._run_psql.assert_called_once_with( - [ - "/usr/bin/pgsql", - "--no-align", - "--no-readline", - "--no-psqlrc", - "--no-password", - "--username", - "testuser", - "--host", - "testhost", - "--port", - "testport", - "--dbname", - "testdb", - "-c", - "CREATE LANGUAGE plpythonu", - ], - host="testhost", - port="testport", - password="testpassword", - user="testuser", - runas="user", - ) - - def test_language_create_exists(self): - """ - Test language creation - already exists in db - """ - with patch("salt.modules.postgres.language_exists", Mock(return_value=True)): - ret = postgres.language_create( - "plpythonu", - "testdb", - runas="user", - host="testhost", - port="testport", - user="testuser", - password="testpassword", - ) - self.assertFalse(ret) - - def test_language_remove(self): - """ - Test language removal - exists in db - """ - with patch( - "salt.modules.postgres._run_psql", Mock(return_value={"retcode": 0}) - ): - with patch( - "salt.modules.postgres.language_exists", Mock(return_value=True) - ): - postgres.language_remove( - "plpgsql", - "testdb", - runas="user", - host="testhost", - port="testport", - user="testuser", - password="testpassword", - ) - postgres._run_psql.assert_called_once_with( - [ - "/usr/bin/pgsql", - "--no-align", - "--no-readline", - "--no-psqlrc", - "--no-password", - "--username", - "testuser", - "--host", - "testhost", - "--port", - "testport", - "--dbname", - "testdb", - "-c", - "DROP LANGUAGE plpgsql", - ], - host="testhost", - port="testport", - password="testpassword", - user="testuser", - runas="user", - ) - - def test_language_remove_non_exist(self): - """ - Test language removal - does not exist in db - """ - with patch("salt.modules.postgres.language_exists", Mock(return_value=False)): - ret = postgres.language_remove( - "plpgsql", - "testdb", - runas="user", - host="testhost", - port="testport", - user="testuser", - password="testpassword", - ) - self.assertFalse(ret) - - def test_privileges_list_table(self): - """ - Test privilege listing on a table - """ - with patch( - "salt.modules.postgres._run_psql", - Mock(return_value={"retcode": 0, "stdout": test_privileges_list_table_csv}), - ): - ret = postgres.privileges_list( - "awl", - "table", - maintenance_db="db_name", - runas="user", - host="testhost", - port="testport", - user="testuser", - password="testpassword", - ) - expected = { - "bayestest": { - "INSERT": False, - "UPDATE": False, - "SELECT": False, - "DELETE": False, - }, - "baruwa": { - "INSERT": True, - "TRUNCATE": True, - "UPDATE": True, - "TRIGGER": True, - "REFERENCES": True, - "SELECT": True, - "DELETE": True, - }, - "baruwatest": { - "INSERT": False, - "TRUNCATE": False, - "UPDATE": False, - "TRIGGER": False, - "REFERENCES": False, - "SELECT": False, - "DELETE": False, - }, - } - - self.assertDictEqual(ret, expected) - - query = ( - "COPY (SELECT relacl AS name FROM pg_catalog.pg_class c " - "JOIN pg_catalog.pg_namespace n ON n.oid = c.relnamespace " - "WHERE nspname = 'public' AND relname = 'awl' AND relkind in ('r', 'v') " - "ORDER BY relname) TO STDOUT WITH CSV HEADER" - ) - - postgres._run_psql.assert_called_once_with( - [ - "/usr/bin/pgsql", - "--no-align", - "--no-readline", - "--no-psqlrc", - "--no-password", - "--username", - "testuser", - "--host", - "testhost", - "--port", - "testport", - "--dbname", - "db_name", - "-v", - "datestyle=ISO,MDY", - "-c", - query, - ], - host="testhost", - port="testport", - password="testpassword", - user="testuser", - runas="user", - ) - - def test_privileges_list_group(self): - """ - Test privilege listing on a group - """ - with patch( - "salt.modules.postgres._run_psql", - Mock(return_value={"retcode": 0, "stdout": test_privileges_list_group_csv}), - ): - ret = postgres.privileges_list( - "admin", - "group", - maintenance_db="db_name", - runas="user", - host="testhost", - port="testport", - user="testuser", - password="testpassword", - ) - expected = { - "baruwa": False, - "baruwatest": False, - "baruwatest2": True, - } - - self.assertDictEqual(ret, expected) - - query = ( - "COPY (SELECT rolname, admin_option " - "FROM pg_catalog.pg_auth_members m JOIN pg_catalog.pg_roles r " - "ON m.member=r.oid WHERE m.roleid IN (SELECT oid FROM " - "pg_catalog.pg_roles WHERE rolname='admin') ORDER BY rolname) " - "TO STDOUT WITH CSV HEADER" - ) - - postgres._run_psql.assert_called_once_with( - [ - "/usr/bin/pgsql", - "--no-align", - "--no-readline", - "--no-psqlrc", - "--no-password", - "--username", - "testuser", - "--host", - "testhost", - "--port", - "testport", - "--dbname", - "db_name", - "-v", - "datestyle=ISO,MDY", - "-c", - query, - ], - host="testhost", - port="testport", - password="testpassword", - user="testuser", - runas="user", - ) - - def test_has_privileges_on_table(self): - """ - Test privilege checks on table - """ - with patch( - "salt.modules.postgres._run_psql", - Mock(return_value={"retcode": 0, "stdout": test_privileges_list_table_csv}), - ): - ret = postgres.has_privileges( - "baruwa", - "awl", - "table", - "SELECT,INSERT", - grant_option=True, - maintenance_db="db_name", - runas="user", - host="testhost", - port="testport", - user="testuser", - password="testpassword", - ) - - self.assertTrue(ret) - - ret = postgres.has_privileges( - "baruwa", - "awl", - "table", - "ALL", - grant_option=True, - maintenance_db="db_name", - runas="user", - host="testhost", - port="testport", - user="testuser", - password="testpassword", - ) - - self.assertTrue(ret) - - ret = postgres.has_privileges( - "baruwa", - "awl", - "table", - "ALL", - grant_option=False, - maintenance_db="db_name", - runas="user", - host="testhost", - port="testport", - user="testuser", - password="testpassword", - ) - - self.assertTrue(ret) - - ret = postgres.has_privileges( - "bayestest", - "awl", - "table", - "SELECT,INSERT,TRUNCATE", - maintenance_db="db_name", - runas="user", - host="testhost", - port="testport", - user="testuser", - password="testpassword", - ) - - self.assertFalse(ret) - - ret = postgres.has_privileges( - "bayestest", - "awl", - "table", - "SELECT,INSERT", - maintenance_db="db_name", - runas="user", - host="testhost", - port="testport", - user="testuser", - password="testpassword", - ) - - self.assertTrue(ret) - - def test_has_privileges_on_group(self): - """ - Test privilege checks on group - """ - with patch( - "salt.modules.postgres._run_psql", - Mock(return_value={"retcode": 0, "stdout": test_privileges_list_group_csv}), - ): - ret = postgres.has_privileges( - "baruwa", - "admin", - "group", - maintenance_db="db_name", - runas="user", - host="testhost", - port="testport", - user="testuser", - password="testpassword", - ) - - self.assertTrue(ret) - - ret = postgres.has_privileges( - "baruwa", - "admin", - "group", - grant_option=True, - maintenance_db="db_name", - runas="user", - host="testhost", - port="testport", - user="testuser", - password="testpassword", - ) - - self.assertFalse(ret) - - ret = postgres.has_privileges( - "tony", - "admin", - "group", - maintenance_db="db_name", - runas="user", - host="testhost", - port="testport", - user="testuser", - password="testpassword", - ) - - self.assertFalse(ret) - - def test_privileges_grant_table(self): - """ - Test granting privileges on table - """ - with patch( - "salt.modules.postgres._run_psql", Mock(return_value={"retcode": 0}) - ): - with patch( - "salt.modules.postgres.has_privileges", Mock(return_value=False) - ): - ret = postgres.privileges_grant( - "baruwa", - "awl", - "table", - "ALL", - grant_option=True, - maintenance_db="db_name", - runas="user", - host="testhost", - port="testport", - user="testuser", - password="testpassword", - ) - - query = 'GRANT ALL ON TABLE public."awl" TO "baruwa" WITH GRANT OPTION' - - postgres._run_psql.assert_called_once_with( - [ - "/usr/bin/pgsql", - "--no-align", - "--no-readline", - "--no-psqlrc", - "--no-password", - "--username", - "testuser", - "--host", - "testhost", - "--port", - "testport", - "--dbname", - "db_name", - "-c", - query, - ], - host="testhost", - port="testport", - password="testpassword", - user="testuser", - runas="user", - ) - - with patch( - "salt.modules.postgres._run_psql", Mock(return_value={"retcode": 0}) - ): - with patch( - "salt.modules.postgres.has_privileges", Mock(return_value=False) - ): - ret = postgres.privileges_grant( - "baruwa", - "awl", - "table", - "ALL", - maintenance_db="db_name", - runas="user", - host="testhost", - port="testport", - user="testuser", - password="testpassword", - ) - - query = 'GRANT ALL ON TABLE public."awl" TO "baruwa"' - - postgres._run_psql.assert_called_once_with( - [ - "/usr/bin/pgsql", - "--no-align", - "--no-readline", - "--no-psqlrc", - "--no-password", - "--username", - "testuser", - "--host", - "testhost", - "--port", - "testport", - "--dbname", - "db_name", - "-c", - query, - ], - host="testhost", - port="testport", - password="testpassword", - user="testuser", - runas="user", - ) - - # Test grant on all tables - with patch( - "salt.modules.postgres._run_psql", Mock(return_value={"retcode": 0}) - ): - with patch( - "salt.modules.postgres.has_privileges", Mock(return_value=False) - ): - ret = postgres.privileges_grant( - "baruwa", - "ALL", - "table", - "SELECT", - maintenance_db="db_name", - runas="user", - host="testhost", - port="testport", - user="testuser", - password="testpassword", - ) - - query = 'GRANT SELECT ON ALL TABLES IN SCHEMA public TO "baruwa"' - - postgres._run_psql.assert_called_once_with( - [ - "/usr/bin/pgsql", - "--no-align", - "--no-readline", - "--no-psqlrc", - "--no-password", - "--username", - "testuser", - "--host", - "testhost", - "--port", - "testport", - "--dbname", - "db_name", - "-c", - query, - ], - host="testhost", - port="testport", - password="testpassword", - user="testuser", - runas="user", - ) - - def test_privileges_grant_group(self): - """ - Test granting privileges on group - """ - with patch( - "salt.modules.postgres._run_psql", Mock(return_value={"retcode": 0}) - ): - with patch( - "salt.modules.postgres.has_privileges", Mock(return_value=False) - ): - ret = postgres.privileges_grant( - "baruwa", - "admins", - "group", - grant_option=True, - maintenance_db="db_name", - runas="user", - host="testhost", - port="testport", - user="testuser", - password="testpassword", - ) - - query = 'GRANT admins TO "baruwa" WITH ADMIN OPTION' - - postgres._run_psql.assert_called_once_with( - [ - "/usr/bin/pgsql", - "--no-align", - "--no-readline", - "--no-psqlrc", - "--no-password", - "--username", - "testuser", - "--host", - "testhost", - "--port", - "testport", - "--dbname", - "db_name", - "-c", - query, - ], - host="testhost", - port="testport", - password="testpassword", - user="testuser", - runas="user", - ) - - with patch( - "salt.modules.postgres._run_psql", Mock(return_value={"retcode": 0}) - ): - with patch( - "salt.modules.postgres.has_privileges", Mock(return_value=False) - ): - ret = postgres.privileges_grant( - "baruwa", - "admins", - "group", - maintenance_db="db_name", - runas="user", - host="testhost", - port="testport", - user="testuser", - password="testpassword", - ) - - query = 'GRANT admins TO "baruwa"' - - postgres._run_psql.assert_called_once_with( - [ - "/usr/bin/pgsql", - "--no-align", - "--no-readline", - "--no-psqlrc", - "--no-password", - "--username", - "testuser", - "--host", - "testhost", - "--port", - "testport", - "--dbname", - "db_name", - "-c", - query, - ], - host="testhost", - port="testport", - password="testpassword", - user="testuser", - runas="user", - ) - - def test_privileges_revoke_table(self): - """ - Test revoking privileges on table - """ - with patch( - "salt.modules.postgres._run_psql", Mock(return_value={"retcode": 0}) - ): - with patch("salt.modules.postgres.has_privileges", Mock(return_value=True)): - ret = postgres.privileges_revoke( - "baruwa", - "awl", - "table", - "ALL", - maintenance_db="db_name", - runas="user", - host="testhost", - port="testport", - user="testuser", - password="testpassword", - ) - - query = "REVOKE ALL ON TABLE public.awl FROM baruwa" - - postgres._run_psql.assert_called_once_with( - [ - "/usr/bin/pgsql", - "--no-align", - "--no-readline", - "--no-psqlrc", - "--no-password", - "--username", - "testuser", - "--host", - "testhost", - "--port", - "testport", - "--dbname", - "db_name", - "-c", - query, - ], - host="testhost", - port="testport", - password="testpassword", - user="testuser", - runas="user", - ) - - def test_privileges_revoke_group(self): - """ - Test revoking privileges on group - """ - with patch( - "salt.modules.postgres._run_psql", Mock(return_value={"retcode": 0}) - ): - with patch("salt.modules.postgres.has_privileges", Mock(return_value=True)): - ret = postgres.privileges_revoke( - "baruwa", - "admins", - "group", - maintenance_db="db_name", - runas="user", - host="testhost", - port="testport", - user="testuser", - password="testpassword", - ) - - query = "REVOKE admins FROM baruwa" - - postgres._run_psql.assert_called_once_with( - [ - "/usr/bin/pgsql", - "--no-align", - "--no-readline", - "--no-psqlrc", - "--no-password", - "--username", - "testuser", - "--host", - "testhost", - "--port", - "testport", - "--dbname", - "db_name", - "-c", - query, - ], - host="testhost", - port="testport", - password="testpassword", - user="testuser", - runas="user", - ) - - def test_datadir_init(self): - """ - Test Initializing a postgres data directory - """ - with patch( - "salt.modules.postgres._run_initdb", Mock(return_value={"retcode": 0}) - ): - with patch( - "salt.modules.postgres.datadir_exists", Mock(return_value=False) - ): - name = "/var/lib/pgsql/data" - ret = postgres.datadir_init( - name, user="postgres", password="test", runas="postgres" - ) - postgres._run_initdb.assert_called_once_with( - name, - auth="password", - encoding="UTF8", - locale=None, - password="test", - runas="postgres", - checksums=False, - waldir=None, - user="postgres", - ) - self.assertTrue(ret) - - def test_datadir_exists(self): - """ - Test Checks if postgres data directory has been initialized - """ - with patch("os.path.isfile", Mock(return_value=True)): - name = "/var/lib/pgsql/data" - ret = postgres.datadir_exists(name) - self.assertTrue(ret) - - def test_pg_is_older_ext_ver(self): - """ - Test Checks if postgres extension version string is older - """ - self.assertTrue(postgres._pg_is_older_ext_ver("8.5", "9.5")) - self.assertTrue(postgres._pg_is_older_ext_ver("8.5", "8.6")) - self.assertTrue(postgres._pg_is_older_ext_ver("8.5.2", "8.5.3")) - self.assertFalse(postgres._pg_is_older_ext_ver("9.5", "8.5")) - self.assertTrue(postgres._pg_is_older_ext_ver("9.5", "9.6")) - self.assertTrue(postgres._pg_is_older_ext_ver("9.5.0", "9.5.1")) - self.assertTrue(postgres._pg_is_older_ext_ver("9.5", "9.5.1")) - self.assertFalse(postgres._pg_is_older_ext_ver("9.5.1", "9.5")) - self.assertFalse(postgres._pg_is_older_ext_ver("9.5b", "9.5a")) - self.assertTrue(postgres._pg_is_older_ext_ver("10a", "10b")) - self.assertTrue(postgres._pg_is_older_ext_ver("1.2.3.4", "1.2.3.5")) - self.assertTrue(postgres._pg_is_older_ext_ver("10dev", "10next")) - self.assertFalse(postgres._pg_is_older_ext_ver("10next", "10dev"))