diff --git a/0001-tserong-suse.com-We-don-t-have-python-systemd-so-not.patch b/0001-tserong-suse.com-We-don-t-have-python-systemd-so-not.patch index 7b3243c..d437ae5 100644 --- a/0001-tserong-suse.com-We-don-t-have-python-systemd-so-not.patch +++ b/0001-tserong-suse.com-We-don-t-have-python-systemd-so-not.patch @@ -1,7 +1,7 @@ -From f9dbfde1c3e7782d78f6b0b2b6b564f61749941f Mon Sep 17 00:00:00 2001 +From 3f8257601cb1224221e10e712c0377254714f6fc Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Klaus=20K=C3=A4mpf?= Date: Wed, 20 Jan 2016 11:00:15 +0100 -Subject: [PATCH 01/12] tserong@suse.com -- We don't have python-systemd, so +Subject: [PATCH 01/13] tserong@suse.com -- We don't have python-systemd, so notify can't work --- @@ -9,7 +9,7 @@ Subject: [PATCH 01/12] tserong@suse.com -- We don't have python-systemd, so 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/pkg/salt-master.service b/pkg/salt-master.service -index 0eadf88..2b0f326 100644 +index 0eadf88a3837..2b0f32623571 100644 --- a/pkg/salt-master.service +++ b/pkg/salt-master.service @@ -4,8 +4,7 @@ After=network.target @@ -23,5 +23,5 @@ index 0eadf88..2b0f326 100644 KillMode=process -- -2.1.4 +2.8.3 diff --git a/0002-Run-salt-master-as-dedicated-salt-user.patch b/0002-Run-salt-master-as-dedicated-salt-user.patch index 7d5d101..0f717e5 100644 --- a/0002-Run-salt-master-as-dedicated-salt-user.patch +++ b/0002-Run-salt-master-as-dedicated-salt-user.patch @@ -1,7 +1,7 @@ -From af193a109fcae502c4cdd47507aea9f67d809b4b Mon Sep 17 00:00:00 2001 +From e86b0bf279c7faea457dfb8152fe9f5c829e42be Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Klaus=20K=C3=A4mpf?= Date: Wed, 20 Jan 2016 11:01:06 +0100 -Subject: [PATCH 02/12] Run salt master as dedicated salt user +Subject: [PATCH 02/13] Run salt master as dedicated salt user --- conf/master | 3 ++- @@ -9,7 +9,7 @@ Subject: [PATCH 02/12] Run salt master as dedicated salt user 2 files changed, 5 insertions(+), 1 deletion(-) diff --git a/conf/master b/conf/master -index aae46ef..064828a 100644 +index 1bbeb50c51b4..54d5f8f3ca75 100644 --- a/conf/master +++ b/conf/master @@ -25,7 +25,8 @@ @@ -23,7 +23,7 @@ index aae46ef..064828a 100644 # The port used by the communication interface. The ret (return) port is the # interface used for the file server, authentication, job returns, etc. diff --git a/pkg/salt-common.logrotate b/pkg/salt-common.logrotate -index 3cd0023..8d970c0 100644 +index 3cd002308e83..8d970c0a64d0 100644 --- a/pkg/salt-common.logrotate +++ b/pkg/salt-common.logrotate @@ -1,4 +1,5 @@ @@ -49,5 +49,5 @@ index 3cd0023..8d970c0 100644 missingok rotate 7 -- -2.1.4 +2.8.3 diff --git a/0003-Check-if-byte-strings-are-properly-encoded-in-UTF-8.patch b/0003-Check-if-byte-strings-are-properly-encoded-in-UTF-8.patch index 6d285ef..47ac6c0 100644 --- a/0003-Check-if-byte-strings-are-properly-encoded-in-UTF-8.patch +++ b/0003-Check-if-byte-strings-are-properly-encoded-in-UTF-8.patch @@ -1,4 +1,4 @@ -From 6035aef0c80ae12a068bee7613c5b7f7f48aa9d3 Mon Sep 17 00:00:00 2001 +From cad9f1a8fda2a4d8c666abcf74e03e7c6a8eb6be Mon Sep 17 00:00:00 2001 From: Bo Maryniuk Date: Mon, 18 Jan 2016 16:28:48 +0100 Subject: [PATCH 03/12] Check if byte strings are properly encoded in UTF-8 @@ -9,10 +9,10 @@ Rename keywords arguments variable to a default name. 1 file changed, 6 insertions(+), 5 deletions(-) diff --git a/salt/modules/zypper.py b/salt/modules/zypper.py -index fecb671..27b00d5 100644 +index 7dd73dd..92c604e 100644 --- a/salt/modules/zypper.py +++ b/salt/modules/zypper.py -@@ -164,9 +164,9 @@ def info_installed(*names, **kwargs): +@@ -335,9 +335,9 @@ def info_installed(*names, **kwargs): summary, description. :param errors: @@ -25,7 +25,7 @@ index fecb671..27b00d5 100644 Valid attributes are: ignore, report -@@ -179,7 +179,8 @@ def info_installed(*names, **kwargs): +@@ -350,7 +350,8 @@ def info_installed(*names, **kwargs): salt '*' pkg.info_installed ... salt '*' pkg.info_installed attr=version,vendor salt '*' pkg.info_installed ... attr=version,vendor @@ -35,7 +35,7 @@ index fecb671..27b00d5 100644 ''' ret = dict() for pkg_name, pkg_nfo in __salt__['lowpkg.info'](*names, **kwargs).items(): -@@ -190,7 +191,7 @@ def info_installed(*names, **kwargs): +@@ -361,7 +362,7 @@ def info_installed(*names, **kwargs): # Check, if string is encoded in a proper UTF-8 value_ = value.decode('UTF-8', 'ignore').encode('UTF-8', 'ignore') if value != value_: @@ -45,5 +45,5 @@ index fecb671..27b00d5 100644 if key == 'source_rpm': t_nfo['source'] = value -- -2.1.4 +2.9.2 diff --git a/0004-do-not-generate-a-date-in-a-comment-to-prevent-rebui.patch b/0004-do-not-generate-a-date-in-a-comment-to-prevent-rebui.patch index f21d0b1..d2dbf85 100644 --- a/0004-do-not-generate-a-date-in-a-comment-to-prevent-rebui.patch +++ b/0004-do-not-generate-a-date-in-a-comment-to-prevent-rebui.patch @@ -1,7 +1,7 @@ -From a2ffa8e54f3cd8dba3c4b73cad086a6b93fb3a41 Mon Sep 17 00:00:00 2001 +From a831380a6d7f323c9824d26d6f6e5966b10cbc6e Mon Sep 17 00:00:00 2001 From: Michael Calmer Date: Fri, 4 Mar 2016 09:51:22 +0100 -Subject: [PATCH 04/12] do not generate a date in a comment to prevent rebuilds +Subject: [PATCH 04/13] do not generate a date in a comment to prevent rebuilds (bsc#969407) --- @@ -9,10 +9,10 @@ Subject: [PATCH 04/12] do not generate a date in a comment to prevent rebuilds 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/setup.py b/setup.py -index 742eae5..d2dd8f7 100755 +index c84c19824a6f..819ec2b875d1 100755 --- a/setup.py +++ b/setup.py -@@ -605,8 +605,7 @@ class Clean(clean): +@@ -618,8 +618,7 @@ class Clean(clean): INSTALL_VERSION_TEMPLATE = '''\ @@ -23,5 +23,5 @@ index 742eae5..d2dd8f7 100755 from salt.version import SaltStackVersion -- -2.1.4 +2.8.3 diff --git a/0005-Use-SHA256-hash-type-by-default.patch b/0005-Use-SHA256-hash-type-by-default.patch index 875379f..7f2d0eb 100644 --- a/0005-Use-SHA256-hash-type-by-default.patch +++ b/0005-Use-SHA256-hash-type-by-default.patch @@ -1,4 +1,4 @@ -From d5fc00efc2f73018c4c6bf3bea03648dfd1340fc Mon Sep 17 00:00:00 2001 +From 03732c6879c45596b0a9a0e6500957ec4f9d8415 Mon Sep 17 00:00:00 2001 From: Bo Maryniuk Date: Thu, 17 Mar 2016 12:30:23 +0100 Subject: [PATCH 05/12] Use SHA256 hash type by default @@ -10,10 +10,10 @@ Subject: [PATCH 05/12] Use SHA256 hash type by default 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/conf/master b/conf/master -index 064828a..5e75b15 100644 +index 8bbe490..118f53b 100644 --- a/conf/master +++ b/conf/master -@@ -474,7 +474,7 @@ syndic_user: salt +@@ -509,7 +509,7 @@ syndic_user: salt # # Prior to changing this value, the master should be stopped and all Salt # caches should be cleared. @@ -23,10 +23,10 @@ index 064828a..5e75b15 100644 # The buffer size in the file server can be adjusted here: #file_buffer_size: 1048576 diff --git a/conf/minion b/conf/minion -index b408942..32b0d0a 100644 +index 3ba8ba0..8e4dbd0 100644 --- a/conf/minion +++ b/conf/minion -@@ -451,7 +451,7 @@ +@@ -542,7 +542,7 @@ # # Warning: Prior to changing this value, the minion should be stopped and all # Salt caches should be cleared. @@ -36,10 +36,10 @@ index b408942..32b0d0a 100644 # The Salt pillar is searched for locally if file_client is set to local. If # this is the case, and pillar data is defined, then the pillar_roots need to diff --git a/conf/proxy b/conf/proxy -index e6ca631..e697357 100644 +index 7b835e2..ab500f9 100644 --- a/conf/proxy +++ b/conf/proxy -@@ -427,7 +427,7 @@ +@@ -435,7 +435,7 @@ # # Warning: Prior to changing this value, the minion should be stopped and all # Salt caches should be cleared. @@ -49,5 +49,5 @@ index e6ca631..e697357 100644 # The Salt pillar is searched for locally if file_client is set to local. If # this is the case, and pillar data is defined, then the pillar_roots need to -- -2.1.4 +2.9.2 diff --git a/0006-Create-salt-proxy-instantiated-service-file.patch b/0006-Create-salt-proxy-instantiated-service-file.patch index 45c9aa6..1b21d83 100644 --- a/0006-Create-salt-proxy-instantiated-service-file.patch +++ b/0006-Create-salt-proxy-instantiated-service-file.patch @@ -1,7 +1,7 @@ -From e2236cc77888d3c359c23dfb47a57e1e057864bb Mon Sep 17 00:00:00 2001 +From 46476c37aa00d9cbbd0ac58e56b7e1c134d33b13 Mon Sep 17 00:00:00 2001 From: Christian McHugh Date: Thu, 10 Mar 2016 13:25:01 -0600 -Subject: [PATCH 6/7] Create salt-proxy instantiated service file +Subject: [PATCH 06/13] Create salt-proxy instantiated service file Add a systemd service file for salt-proxy. @@ -15,7 +15,7 @@ Instantiate a new proxy service with proxyid=p8000: diff --git a/pkg/salt-proxy@.service b/pkg/salt-proxy@.service new file mode 100644 -index 0000000..f97120a +index 000000000000..f97120a05589 --- /dev/null +++ b/pkg/salt-proxy@.service @@ -0,0 +1,12 @@ @@ -32,5 +32,5 @@ index 0000000..f97120a +[Install] +WantedBy=multi-user.target -- -2.8.2 +2.8.3 diff --git a/0007-Add-SUSE-Manager-plugin.patch b/0007-Add-SUSE-Manager-plugin.patch index a6d39f0..7fe0933 100644 --- a/0007-Add-SUSE-Manager-plugin.patch +++ b/0007-Add-SUSE-Manager-plugin.patch @@ -1,7 +1,7 @@ -From df87ac3485ff8b5013e720435905afda6b53ada8 Mon Sep 17 00:00:00 2001 +From 65a32350589712835294bb5e671c42ef1d331df8 Mon Sep 17 00:00:00 2001 From: Bo Maryniuk Date: Mon, 9 May 2016 10:33:44 +0200 -Subject: [PATCH 7/7] Add SUSE Manager plugin +Subject: [PATCH 07/13] Add SUSE Manager plugin * Add unit test to the libzypp drift detector plugin --- @@ -17,7 +17,7 @@ Subject: [PATCH 7/7] Add SUSE Manager plugin diff --git a/scripts/zypper/plugins/commit/README.md b/scripts/zypper/plugins/commit/README.md new file mode 100644 -index 0000000..01c8917 +index 000000000000..01c8917c8e0a --- /dev/null +++ b/scripts/zypper/plugins/commit/README.md @@ -0,0 +1,3 @@ @@ -26,7 +26,7 @@ index 0000000..01c8917 +Plugins here are required to interact with SUSE Manager in conjunction of SaltStack and Zypper. diff --git a/scripts/zypper/plugins/commit/susemanager b/scripts/zypper/plugins/commit/susemanager new file mode 100755 -index 0000000..268298b +index 000000000000..268298b10811 --- /dev/null +++ b/scripts/zypper/plugins/commit/susemanager @@ -0,0 +1,59 @@ @@ -91,7 +91,7 @@ index 0000000..268298b +DriftDetector().main() diff --git a/tests/unit/zypp_plugins_test.py b/tests/unit/zypp_plugins_test.py new file mode 100644 -index 0000000..6075288 +index 000000000000..6075288aad39 --- /dev/null +++ b/tests/unit/zypp_plugins_test.py @@ -0,0 +1,51 @@ @@ -148,7 +148,7 @@ index 0000000..6075288 + run_tests(ZyppPluginsTestCase, needs_daemon=False) diff --git a/tests/zypp_plugin.py b/tests/zypp_plugin.py new file mode 100644 -index 0000000..218f703 +index 000000000000..218f70381146 --- /dev/null +++ b/tests/zypp_plugin.py @@ -0,0 +1,64 @@ @@ -217,5 +217,5 @@ index 0000000..218f703 + ''' + self.closed = True -- -2.8.2 +2.8.3 diff --git a/0008-Prevent-several-minion-processes-on-the-same-machine.patch b/0008-Prevent-several-minion-processes-on-the-same-machine.patch deleted file mode 100644 index 07a4506..0000000 --- a/0008-Prevent-several-minion-processes-on-the-same-machine.patch +++ /dev/null @@ -1,25 +0,0 @@ -From 46713681bda8e45667691fbda6efe808c81574b1 Mon Sep 17 00:00:00 2001 -From: Bo Maryniuk -Date: Mon, 23 May 2016 17:29:15 +0200 -Subject: [PATCH 8/8] Prevent several minion processes on the same machine - ---- - pkg/suse/salt-minion | 2 +- - 1 file changed, 1 insertion(+), 1 deletion(-) - -diff --git a/pkg/suse/salt-minion b/pkg/suse/salt-minion -index c476fab..28cf32c 100644 ---- a/pkg/suse/salt-minion -+++ b/pkg/suse/salt-minion -@@ -55,7 +55,7 @@ RETVAL=0 - start() { - echo -n $"Starting salt-minion daemon: " - if [ -f $SUSE_RELEASE ]; then -- startproc -f -p /var/run/$SERVICE.pid $SALTMINION -d $MINION_ARGS -+ startproc -p /var/run/$SERVICE.pid $SALTMINION -d $MINION_ARGS - rc_status -v - elif [ -e $DEBIAN_VERSION ]; then - if [ -f $LOCKFILE ]; then --- -2.8.3 - diff --git a/0008-checksum-validation-when-zypper-pkg.download.patch b/0008-checksum-validation-when-zypper-pkg.download.patch new file mode 100644 index 0000000..2558fe1 --- /dev/null +++ b/0008-checksum-validation-when-zypper-pkg.download.patch @@ -0,0 +1,43 @@ +From c80528b9aad5305b06a07cadf752a45392d4a147 Mon Sep 17 00:00:00 2001 +From: =?UTF-8?q?Pablo=20Su=C3=A1rez=20Hern=C3=A1ndez?= + +Date: Tue, 24 May 2016 11:01:55 +0100 +Subject: [PATCH 08/12] checksum validation when zypper pkg.download + +check the signature of downloaded RPM files + +bugfix: showing errors when a package download fails using zypper pkg.download + +Renamed check_sig to checksum and some refactoring + +simpler rpm.checksum function +--- + salt/modules/zypper.py | 5 ++++- + 1 file changed, 4 insertions(+), 1 deletion(-) + +diff --git a/salt/modules/zypper.py b/salt/modules/zypper.py +index 92c604e..547d72b 100644 +--- a/salt/modules/zypper.py ++++ b/salt/modules/zypper.py +@@ -1596,14 +1596,17 @@ def download(*packages, **kwargs): + pkg_ret = {} + for dld_result in __zypper__.xml.call('download', *packages).getElementsByTagName("download-result"): + repo = dld_result.getElementsByTagName("repository")[0] ++ path = dld_result.getElementsByTagName("localfile")[0].getAttribute("path") + pkg_info = { + 'repository-name': repo.getAttribute('name'), + 'repository-alias': repo.getAttribute('alias'), ++ 'path': path, + } + key = _get_first_aggregate_text( + dld_result.getElementsByTagName('name') + ) +- pkg_ret[key] = pkg_info ++ if __salt__['lowpkg.checksum'](pkg_info['path']): ++ pkg_ret[key] = pkg_info + + if pkg_ret: + failed = [pkg for pkg in packages if pkg not in pkg_ret] +-- +2.8.2 + diff --git a/0009-checksum-validation-when-zypper-pkg.download.patch b/0009-checksum-validation-when-zypper-pkg.download.patch deleted file mode 100644 index c8f8f63..0000000 --- a/0009-checksum-validation-when-zypper-pkg.download.patch +++ /dev/null @@ -1,74 +0,0 @@ -From d27af7dee61e83165bbd9adb9f0b6dc467907faa Mon Sep 17 00:00:00 2001 -From: =?UTF-8?q?Pablo=20Su=C3=A1rez=20Hern=C3=A1ndez?= - -Date: Tue, 24 May 2016 11:01:55 +0100 -Subject: [PATCH 09/11] checksum validation when zypper pkg.download - -check the signature of downloaded RPM files - -bugfix: showing errors when a package download fails using zypper pkg.download - -Renamed check_sig to checksum and some refactoring - -simpler rpm.checksum function ---- - salt/modules/rpm.py | 26 ++++++++++++++++++++++++++ - salt/modules/zypper.py | 6 +++++- - 2 files changed, 31 insertions(+), 1 deletion(-) - -diff --git a/salt/modules/rpm.py b/salt/modules/rpm.py -index 1469368..4991f24 100644 ---- a/salt/modules/rpm.py -+++ b/salt/modules/rpm.py -@@ -602,3 +602,29 @@ def version_cmp(ver1, ver2): - log.warning("Failed to compare version '{0}' to '{1}' using RPM: {2}".format(ver1, ver2, exc)) - - return salt.utils.version_cmp(ver1, ver2) -+ -+ -+def checksum(*paths): -+ ''' -+ Return if the signature of a RPM file is valid. -+ -+ CLI Example: -+ -+ .. code-block:: bash -+ -+ salt '*' lowpkg.checksum /path/to/package1.rpm -+ salt '*' lowpkg.checksum /path/to/package1.rpm /path/to/package2.rpm -+ ''' -+ ret = dict() -+ -+ if not paths: -+ raise CommandExecutionError("No package files has been specified.") -+ -+ for package_file in paths: -+ ret[package_file] = (bool(__salt__['file.file_exists'](package_file)) and -+ not __salt__['cmd.retcode'](["rpm", "-K", "--quiet", package_file], -+ ignore_retcode=True, -+ output_loglevel='trace', -+ python_shell=False)) -+ -+ return ret -diff --git a/salt/modules/zypper.py b/salt/modules/zypper.py -index f9538e5..39b071b 100644 ---- a/salt/modules/zypper.py -+++ b/salt/modules/zypper.py -@@ -1534,9 +1534,13 @@ def download(*packages, **kwargs): - 'repository-alias': repo.getAttribute("alias"), - 'path': dld_result.getElementsByTagName("localfile")[0].getAttribute("path"), - } -- pkg_ret[_get_first_aggregate_text(dld_result.getElementsByTagName("name"))] = pkg_info -+ if __salt__['lowpkg.checksum'](pkg_info['path']): -+ pkg_ret[_get_first_aggregate_text(dld_result.getElementsByTagName("name"))] = pkg_info - - if pkg_ret: -+ failed = [pkg for pkg in packages if pkg not in pkg_ret] -+ if failed: -+ pkg_ret['_error'] = ('The following package(s) failed to download: {0}'.format(', '.join(failed))) - return pkg_ret - - raise CommandExecutionError("Unable to download packages: {0}.".format(', '.join(packages))) --- -2.8.2 - diff --git a/0009-unit-tests-for-rpm.checksum-and-zypper.download.patch b/0009-unit-tests-for-rpm.checksum-and-zypper.download.patch new file mode 100644 index 0000000..587e749 --- /dev/null +++ b/0009-unit-tests-for-rpm.checksum-and-zypper.download.patch @@ -0,0 +1,26 @@ +From f186137c0eb7a14697037bf46ec6a12287b9e0c9 Mon Sep 17 00:00:00 2001 +From: =?UTF-8?q?Pablo=20Su=C3=A1rez=20Hern=C3=A1ndez?= + +Date: Wed, 25 May 2016 17:08:16 +0100 +Subject: [PATCH 09/12] unit tests for rpm.checksum() and zypper.download() + +lint issue fixed +--- + tests/unit/modules/zypper_test.py | 1 + + 1 file changed, 1 insertion(+) + +diff --git a/tests/unit/modules/zypper_test.py b/tests/unit/modules/zypper_test.py +index 308bf94..2f2b323 100644 +--- a/tests/unit/modules/zypper_test.py ++++ b/tests/unit/modules/zypper_test.py +@@ -387,6 +387,7 @@ class ZypperTestCase(TestCase): + + test_out = { + 'nmap': { ++ 'path': u'/var/cache/zypp/packages/SLE-12-x86_64-Pool/x86_64/nmap-6.46-1.72.x86_64.rpm', + 'repository-alias': u'SLE-12-x86_64-Pool', + 'repository-name': u'SLE-12-x86_64-Pool' + } +-- +2.9.2 + diff --git a/0010-snapper-execution-module.patch b/0010-snapper-execution-module.patch new file mode 100644 index 0000000..dc7f7ff --- /dev/null +++ b/0010-snapper-execution-module.patch @@ -0,0 +1,1278 @@ +From 501eda9c64b6e8b4b66c0bf98592331248742ea0 Mon Sep 17 00:00:00 2001 +From: Pablo Suarez Hernandez +Date: Mon, 4 Jul 2016 16:26:33 +0100 +Subject: [PATCH 10/12] snapper execution module + +snapper state module + +snapper module unit tests + +some pylint fixes + +more unit tests + +Fix for snapper.diff when files are created or deleted + +fix diff unit test while creating text file + +passing *args and **kwargs to function when snapper.run + +unit test for snapper.diff with binary file + +load snapper state only if snapper module is loaded + +Fix for _get_jid_snapshots if snapshots doesn't exist + +pylint fixes + +pylint: some fixes + +Variable renaming. Pylint fixes + +Fix in inline comments + +Fix for pylint: W1699 + +some fixes and comments improvement + +Prevent module failing if Snapper does not exist in D-Bus + +Added function for baseline creation + +Allow tag reference for baseline_snapshot state +--- + salt/modules/snapper.py | 687 +++++++++++++++++++++++++++++++++++++ + salt/states/snapper.py | 195 +++++++++++ + tests/unit/modules/snapper_test.py | 324 +++++++++++++++++ + 3 files changed, 1206 insertions(+) + create mode 100644 salt/modules/snapper.py + create mode 100644 salt/states/snapper.py + create mode 100644 tests/unit/modules/snapper_test.py + +diff --git a/salt/modules/snapper.py b/salt/modules/snapper.py +new file mode 100644 +index 0000000..9a73820 +--- /dev/null ++++ b/salt/modules/snapper.py +@@ -0,0 +1,687 @@ ++# -*- coding: utf-8 -*- ++''' ++Module to manage filesystem snapshots with snapper ++ ++:codeauthor: Duncan Mac-Vicar P. ++:codeauthor: Pablo Suárez Hernández ++ ++:depends: ``dbus`` Python module. ++:depends: ``snapper`` http://snapper.io, available in most distros ++:maturity: new ++:platform: Linux ++''' ++ ++from __future__ import absolute_import ++ ++import logging ++import os ++import time ++import difflib ++from pwd import getpwuid ++ ++from salt.exceptions import CommandExecutionError ++import salt.utils ++ ++ ++try: ++ import dbus # pylint: disable=wrong-import-order ++ HAS_DBUS = True ++except ImportError: ++ HAS_DBUS = False ++ ++ ++DBUS_STATUS_MAP = { ++ 1: "created", ++ 2: "deleted", ++ 4: "type changed", ++ 8: "modified", ++ 16: "permission changed", ++ 32: "owner changed", ++ 64: "group changed", ++ 128: "extended attributes changed", ++ 256: "ACL info changed", ++} ++ ++SNAPPER_DBUS_OBJECT = 'org.opensuse.Snapper' ++SNAPPER_DBUS_PATH = '/org/opensuse/Snapper' ++SNAPPER_DBUS_INTERFACE = 'org.opensuse.Snapper' ++ ++log = logging.getLogger(__name__) # pylint: disable=invalid-name ++ ++bus = None # pylint: disable=invalid-name ++snapper = None # pylint: disable=invalid-name ++ ++if HAS_DBUS: ++ bus = dbus.SystemBus() # pylint: disable=invalid-name ++ if SNAPPER_DBUS_OBJECT in bus.list_activatable_names(): ++ snapper = dbus.Interface(bus.get_object(SNAPPER_DBUS_OBJECT, # pylint: disable=invalid-name ++ SNAPPER_DBUS_PATH), ++ dbus_interface=SNAPPER_DBUS_INTERFACE) ++ ++ ++def __virtual__(): ++ if not HAS_DBUS: ++ return (False, 'The snapper module cannot be loaded:' ++ ' missing python dbus module') ++ elif not snapper: ++ return (False, 'The snapper module cannot be loaded:' ++ ' missing snapper') ++ return 'snapper' ++ ++ ++def _snapshot_to_data(snapshot): ++ ''' ++ Returns snapshot data from a D-Bus response. ++ ++ A snapshot D-Bus response is a dbus.Struct containing the ++ information related to a snapshot: ++ ++ [id, type, pre_snapshot, timestamp, user, description, ++ cleanup_algorithm, userdata] ++ ++ id: dbus.UInt32 ++ type: dbus.UInt16 ++ pre_snapshot: dbus.UInt32 ++ timestamp: dbus.Int64 ++ user: dbus.UInt32 ++ description: dbus.String ++ cleaup_algorithm: dbus.String ++ userdata: dbus.Dictionary ++ ''' ++ data = {} ++ ++ data['id'] = snapshot[0] ++ data['type'] = ['single', 'pre', 'post'][snapshot[1]] ++ if data['type'] == 'post': ++ data['pre'] = snapshot[2] ++ ++ if snapshot[3] != -1: ++ data['timestamp'] = snapshot[3] ++ else: ++ data['timestamp'] = int(time.time()) ++ ++ data['user'] = getpwuid(snapshot[4])[0] ++ data['description'] = snapshot[5] ++ data['cleanup'] = snapshot[6] ++ ++ data['userdata'] = {} ++ for key, value in snapshot[7].items(): ++ data['userdata'][key] = value ++ ++ return data ++ ++ ++def _dbus_exception_to_reason(exc, args): ++ ''' ++ Returns a error message from a snapper DBusException ++ ''' ++ error = exc.get_dbus_name() ++ if error == 'error.unknown_config': ++ return "Unknown configuration '{0}'".format(args['config']) ++ elif error == 'error.illegal_snapshot': ++ return 'Invalid snapshot' ++ else: ++ return exc.get_dbus_name() ++ ++ ++def list_snapshots(config='root'): ++ ''' ++ List available snapshots ++ ++ CLI example: ++ ++ .. code-block:: bash ++ ++ salt '*' snapper.list_snapshots config=myconfig ++ ''' ++ try: ++ snapshots = snapper.ListSnapshots(config) ++ return [_snapshot_to_data(s) for s in snapshots] ++ except dbus.DBusException as exc: ++ raise CommandExecutionError( ++ 'Error encountered while listing snapshots: {0}' ++ .format(_dbus_exception_to_reason(exc, locals())) ++ ) ++ ++ ++def get_snapshot(number=0, config='root'): ++ ''' ++ Get detailed information about a given snapshot ++ ++ CLI example: ++ ++ .. code-block:: bash ++ ++ salt '*' snapper.get_snapshot 1 ++ ''' ++ try: ++ snapshot = snapper.GetSnapshot(config, int(number)) ++ return _snapshot_to_data(snapshot) ++ except dbus.DBusException as exc: ++ raise CommandExecutionError( ++ 'Error encountered while retrieving snapshot: {0}' ++ .format(_dbus_exception_to_reason(exc, locals())) ++ ) ++ ++ ++def list_configs(): ++ ''' ++ List all available configs ++ ++ CLI example: ++ ++ .. code-block:: bash ++ ++ salt '*' snapper.list_configs ++ ''' ++ try: ++ configs = snapper.ListConfigs() ++ return dict((config[0], config[2]) for config in configs) ++ except dbus.DBusException as exc: ++ raise CommandExecutionError( ++ 'Error encountered while listing configurations: {0}' ++ .format(_dbus_exception_to_reason(exc, locals())) ++ ) ++ ++ ++def _config_filter(value): ++ if isinstance(value, bool): ++ return 'yes' if value else 'no' ++ return value ++ ++ ++def set_config(name='root', **kwargs): ++ ''' ++ Set configuration values ++ ++ CLI example: ++ ++ .. code-block:: bash ++ ++ salt '*' snapper.set_config SYNC_ACL=True ++ ++ Keys are case insensitive as they will be always uppercased to ++ snapper convention. The above example is equivalent to: ++ ++ .. code-block:: bash ++ salt '*' snapper.set_config sync_acl=True ++ ''' ++ try: ++ data = dict((k.upper(), _config_filter(v)) for k, v in ++ kwargs.items() if not k.startswith('__')) ++ snapper.SetConfig(name, data) ++ except dbus.DBusException as exc: ++ raise CommandExecutionError( ++ 'Error encountered while setting configuration {0}: {1}' ++ .format(name, _dbus_exception_to_reason(exc, locals())) ++ ) ++ return True ++ ++ ++def _get_last_snapshot(config='root'): ++ ''' ++ Returns the last existing created snapshot ++ ''' ++ snapshot_list = sorted(list_snapshots(config), key=lambda x: x['id']) ++ return snapshot_list[-1] ++ ++ ++def status_to_string(dbus_status): ++ ''' ++ Converts a numeric dbus snapper status into a string ++ ''' ++ status_tuple = ( ++ dbus_status & 0b000000001, dbus_status & 0b000000010, dbus_status & 0b000000100, ++ dbus_status & 0b000001000, dbus_status & 0b000010000, dbus_status & 0b000100000, ++ dbus_status & 0b001000000, dbus_status & 0b010000000, dbus_status & 0b100000000 ++ ) ++ ++ return [DBUS_STATUS_MAP[status] for status in status_tuple if status] ++ ++ ++def get_config(name='root'): ++ ''' ++ Retrieves all values from a given configuration ++ ++ CLI example: ++ ++ .. code-block:: bash ++ ++ salt '*' snapper.get_config ++ ''' ++ try: ++ config = snapper.GetConfig(name) ++ return config ++ except dbus.DBusException as exc: ++ raise CommandExecutionError( ++ 'Error encountered while retrieving configuration: {0}' ++ .format(_dbus_exception_to_reason(exc, locals())) ++ ) ++ ++ ++def create_snapshot(config='root', snapshot_type='single', pre_number=None, ++ description=None, cleanup_algorithm='number', userdata=None, ++ **kwargs): ++ ''' ++ Creates an snapshot ++ ++ config ++ Configuration name. ++ snapshot_type ++ Specifies the type of the new snapshot. Possible values are ++ single, pre and post. ++ pre_number ++ For post snapshots the number of the pre snapshot must be ++ provided. ++ description ++ Description for the snapshot. If not given, the salt job will be used. ++ cleanup_algorithm ++ Set the cleanup algorithm for the snapshot. ++ ++ number ++ Deletes old snapshots when a certain number of snapshots ++ is reached. ++ timeline ++ Deletes old snapshots but keeps a number of hourly, ++ daily, weekly, monthly and yearly snapshots. ++ empty-pre-post ++ Deletes pre/post snapshot pairs with empty diffs. ++ userdata ++ Set userdata for the snapshot (key-value pairs). ++ ++ Returns the number of the created snapshot. ++ ++ .. code-block:: bash ++ salt '*' snapper.create_snapshot ++ ''' ++ if not userdata: ++ userdata = {} ++ ++ jid = kwargs.get('__pub_jid') ++ if description is None and jid is not None: ++ description = 'salt job {0}'.format(jid) ++ ++ if jid is not None: ++ userdata['salt_jid'] = jid ++ ++ new_nr = None ++ try: ++ if snapshot_type == 'single': ++ new_nr = snapper.CreateSingleSnapshot(config, description, ++ cleanup_algorithm, userdata) ++ elif snapshot_type == 'pre': ++ new_nr = snapper.CreatePreSnapshot(config, description, ++ cleanup_algorithm, userdata) ++ elif snapshot_type == 'post': ++ if pre_number is None: ++ raise CommandExecutionError( ++ "pre snapshot number 'pre_number' needs to be" ++ "specified for snapshots of the 'post' type") ++ new_nr = snapper.CreatePostSnapshot(config, pre_number, description, ++ cleanup_algorithm, userdata) ++ else: ++ raise CommandExecutionError( ++ "Invalid snapshot type '{0}'", format(snapshot_type)) ++ except dbus.DBusException as exc: ++ raise CommandExecutionError( ++ 'Error encountered while listing changed files: {0}' ++ .format(_dbus_exception_to_reason(exc, locals())) ++ ) ++ return new_nr ++ ++ ++def _get_num_interval(config, num_pre, num_post): ++ ''' ++ Returns numerical interval based on optionals num_pre, num_post values ++ ''' ++ post = int(num_post) if num_post else 0 ++ pre = int(num_pre) if num_pre is not None else _get_last_snapshot(config)['id'] ++ return pre, post ++ ++ ++def _is_text_file(filename): ++ ''' ++ Checks if a file is a text file ++ ''' ++ type_of_file = os.popen('file -bi {0}'.format(filename), 'r').read() ++ return type_of_file.startswith('text') ++ ++ ++def run(function, *args, **kwargs): ++ ''' ++ Runs a function from an execution module creating pre and post snapshots ++ and associating the salt job id with those snapshots for easy undo and ++ cleanup. ++ ++ function ++ Salt function to call. ++ ++ config ++ Configuration name. (default: "root") ++ ++ description ++ A description for the snapshots. (default: None) ++ ++ userdata ++ Data to include in the snapshot metadata. (default: None) ++ ++ cleanup_algorithm ++ Snapper cleanup algorithm. (default: "number") ++ ++ `*args` ++ args for the function to call. (default: None) ++ ++ `**kwargs` ++ kwargs for the function to call (default: None) ++ ++ .. code-block:: bash ++ salt '*' snapper.run file.append args='["/etc/motd", "some text"]' ++ ++ This would run append text to /etc/motd using the file.append ++ module, and will create two snapshots, pre and post with the associated ++ metadata. The jid will be available as salt_jid in the userdata of the ++ snapshot. ++ ++ You can immediately see the changes ++ ''' ++ config = kwargs.pop("config", "root") ++ description = kwargs.pop("description", "snapper.run[{0}]".format(function)) ++ cleanup_algorithm = kwargs.pop("cleanup_algorithm", "number") ++ userdata = kwargs.pop("userdata", {}) ++ ++ func_kwargs = dict((k, v) for k, v in kwargs.items() if not k.startswith('__')) ++ kwargs = dict((k, v) for k, v in kwargs.items() if k.startswith('__')) ++ ++ pre_nr = __salt__['snapper.create_snapshot']( ++ config=config, ++ snapshot_type='pre', ++ description=description, ++ cleanup_algorithm=cleanup_algorithm, ++ userdata=userdata, ++ **kwargs) ++ ++ if function not in __salt__: ++ raise CommandExecutionError( ++ 'function "{0}" does not exist'.format(function) ++ ) ++ ++ try: ++ ret = __salt__[function](*args, **func_kwargs) ++ except CommandExecutionError as exc: ++ ret = "\n".join([str(exc), __salt__[function].__doc__]) ++ ++ __salt__['snapper.create_snapshot']( ++ config=config, ++ snapshot_type='post', ++ pre_number=pre_nr, ++ description=description, ++ cleanup_algorithm=cleanup_algorithm, ++ userdata=userdata, ++ **kwargs) ++ return ret ++ ++ ++def status(config='root', num_pre=None, num_post=None): ++ ''' ++ Returns a comparison between two snapshots ++ ++ config ++ Configuration name. ++ ++ num_pre ++ first snapshot ID to compare. Default is last snapshot ++ ++ num_post ++ last snapshot ID to compare. Default is 0 (current state) ++ ++ CLI example: ++ ++ .. code-block:: bash ++ ++ salt '*' snapper.status ++ salt '*' snapper.status num_pre=19 num_post=20 ++ ''' ++ try: ++ pre, post = _get_num_interval(config, num_pre, num_post) ++ snapper.CreateComparison(config, int(pre), int(post)) ++ files = snapper.GetFiles(config, int(pre), int(post)) ++ status_ret = {} ++ for file in files: ++ status_ret[file[0]] = {'status': status_to_string(file[1])} ++ return status_ret ++ except dbus.DBusException as exc: ++ raise CommandExecutionError( ++ 'Error encountered while listing changed files: {0}' ++ .format(_dbus_exception_to_reason(exc, locals())) ++ ) ++ ++ ++def changed_files(config='root', num_pre=None, num_post=None): ++ ''' ++ Returns the files changed between two snapshots ++ ++ config ++ Configuration name. ++ ++ num_pre ++ first snapshot ID to compare. Default is last snapshot ++ ++ num_post ++ last snapshot ID to compare. Default is 0 (current state) ++ ++ CLI example: ++ ++ .. code-block:: bash ++ ++ salt '*' snapper.changed_files ++ salt '*' snapper.changed_files num_pre=19 num_post=20 ++ ''' ++ return status(config, num_pre, num_post).keys() ++ ++ ++def undo(config='root', files=None, num_pre=None, num_post=None): ++ ''' ++ Undo all file changes that happened between num_pre and num_post, leaving ++ the files into the state of num_pre. ++ ++ .. warning:: ++ If one of the files has changes after num_post, they will be overwriten ++ The snapshots are used to determine the file list, but the current ++ version of the files will be overwritten by the versions in num_pre. ++ ++ You to undo changes between num_pre and the current version of the ++ files use num_post=0. ++ ''' ++ pre, post = _get_num_interval(config, num_pre, num_post) ++ ++ changes = status(config, pre, post) ++ changed = set(changes.keys()) ++ requested = set(files or changed) ++ ++ if not requested.issubset(changed): ++ raise CommandExecutionError( ++ 'Given file list contains files that are not present' ++ 'in the changed filelist: {0}'.format(changed - requested)) ++ ++ cmdret = __salt__['cmd.run']('snapper undochange {0}..{1} {2}'.format( ++ pre, post, ' '.join(requested))) ++ components = cmdret.split(' ') ++ ret = {} ++ for comp in components: ++ key, val = comp.split(':') ++ ret[key] = val ++ return ret ++ ++ ++def _get_jid_snapshots(jid, config='root'): ++ ''' ++ Returns pre/post snapshots made by a given Salt jid ++ ++ Looks for 'salt_jid' entries into snapshots userdata which are created ++ when 'snapper.run' is executed. ++ ''' ++ jid_snapshots = [x for x in list_snapshots(config) if x['userdata'].get("salt_jid") == jid] ++ pre_snapshot = [x for x in jid_snapshots if x['type'] == "pre"] ++ post_snapshot = [x for x in jid_snapshots if x['type'] == "post"] ++ ++ if not pre_snapshot or not post_snapshot: ++ raise CommandExecutionError("Jid '{0}' snapshots not found".format(jid)) ++ ++ return ( ++ pre_snapshot[0]['id'], ++ post_snapshot[0]['id'] ++ ) ++ ++ ++def undo_jid(jid, config='root'): ++ ''' ++ Undo the changes applied by a salt job ++ ++ jid ++ The job id to lookup ++ ++ config ++ Configuration name. ++ ++ CLI example: ++ ++ .. code-block:: bash ++ ++ salt '*' snapper.undo_jid jid=20160607130930720112 ++ ''' ++ pre_snapshot, post_snapshot = _get_jid_snapshots(jid, config=config) ++ return undo(config, num_pre=pre_snapshot, num_post=post_snapshot) ++ ++ ++def diff(config='root', filename=None, num_pre=None, num_post=None): ++ ''' ++ Returns the differences between two snapshots ++ ++ config ++ Configuration name. ++ ++ filename ++ if not provided the showing differences between snapshots for ++ all "text" files ++ ++ num_pre ++ first snapshot ID to compare. Default is last snapshot ++ ++ num_post ++ last snapshot ID to compare. Default is 0 (current state) ++ ++ CLI example: ++ ++ .. code-block:: bash ++ ++ salt '*' snapper.diff ++ salt '*' snapper.diff filename=/var/log/snapper.log num_pre=19 num_post=20 ++ ''' ++ try: ++ pre, post = _get_num_interval(config, num_pre, num_post) ++ ++ files = changed_files(config, pre, post) ++ if filename: ++ files = [filename] if filename in files else [] ++ ++ pre_mount = snapper.MountSnapshot(config, pre, False) if pre else "" ++ post_mount = snapper.MountSnapshot(config, post, False) if post else "" ++ ++ files_diff = dict() ++ for filepath in [filepath for filepath in files if not os.path.isdir(filepath)]: ++ pre_file = pre_mount + filepath ++ post_file = post_mount + filepath ++ ++ if os.path.isfile(pre_file): ++ pre_file_exists = True ++ pre_file_content = salt.utils.fopen(pre_file).readlines() ++ else: ++ pre_file_content = [] ++ pre_file_exists = False ++ ++ if os.path.isfile(post_file): ++ post_file_exists = True ++ post_file_content = salt.utils.fopen(post_file).readlines() ++ else: ++ post_file_content = [] ++ post_file_exists = False ++ ++ if _is_text_file(pre_file) or _is_text_file(post_file): ++ files_diff[filepath] = { ++ 'comment': "text file changed", ++ 'diff': ''.join(difflib.unified_diff(pre_file_content, ++ post_file_content, ++ fromfile=pre_file, ++ tofile=post_file))} ++ ++ if pre_file_exists and not post_file_exists: ++ files_diff[filepath]['comment'] = "text file deleted" ++ if not pre_file_exists and post_file_exists: ++ files_diff[filepath]['comment'] = "text file created" ++ ++ elif not _is_text_file(pre_file) and not _is_text_file(post_file): ++ # This is a binary file ++ files_diff[filepath] = {'comment': "binary file changed"} ++ if pre_file_exists: ++ files_diff[filepath]['old_sha256_digest'] = __salt__['hashutil.sha256_digest'](''.join(pre_file_content)) ++ if post_file_exists: ++ files_diff[filepath]['new_sha256_digest'] = __salt__['hashutil.sha256_digest'](''.join(post_file_content)) ++ if post_file_exists and not pre_file_exists: ++ files_diff[filepath]['comment'] = "binary file created" ++ if pre_file_exists and not post_file_exists: ++ files_diff[filepath]['comment'] = "binary file deleted" ++ ++ if pre: ++ snapper.UmountSnapshot(config, pre, False) ++ if post: ++ snapper.UmountSnapshot(config, post, False) ++ return files_diff ++ except dbus.DBusException as exc: ++ raise CommandExecutionError( ++ 'Error encountered while showing differences between snapshots: {0}' ++ .format(_dbus_exception_to_reason(exc, locals())) ++ ) ++ ++ ++def diff_jid(jid, config='root'): ++ ''' ++ Returns the changes applied by a `jid` ++ ++ jid ++ The job id to lookup ++ ++ config ++ Configuration name. ++ ++ CLI example: ++ ++ .. code-block:: bash ++ ++ salt '*' snapper.diff_jid jid=20160607130930720112 ++ ''' ++ pre_snapshot, post_snapshot = _get_jid_snapshots(jid, config=config) ++ return diff(config, num_pre=pre_snapshot, num_post=post_snapshot) ++ ++ ++def create_baseline(tag="baseline", config='root'): ++ ''' ++ Creates a snapshot marked as baseline ++ ++ tag ++ Tag name for the baseline ++ ++ config ++ Configuration name. ++ ++ CLI example: ++ ++ .. code-block:: bash ++ ++ salt '*' snapper.create_baseline ++ salt '*' snapper.create_baseline my_custom_baseline ++ ''' ++ return __salt__['snapper.create_snapshot'](config=config, ++ snapshot_type='single', ++ description="baseline snapshot", ++ cleanup_algorithm="number", ++ userdata={"baseline_tag": tag}) +diff --git a/salt/states/snapper.py b/salt/states/snapper.py +new file mode 100644 +index 0000000..2711550 +--- /dev/null ++++ b/salt/states/snapper.py +@@ -0,0 +1,195 @@ ++# -*- coding: utf-8 -*- ++''' ++Managing implicit state and baselines using snapshots ++===================================================== ++ ++Salt can manage state against explicitly defined state, for example ++if your minion state is defined by: ++ ++.. code-block:: yaml ++ ++ /etc/config_file: ++ file.managed: ++ - source: salt://configs/myconfig ++ ++If someone modifies this file, the next application of the highstate will ++allow the admin to correct this deviation and the file will be corrected. ++ ++Now, what happens if somebody creates a file ``/etc/new_config_file`` and ++deletes ``/etc/important_config_file``? Unless you have a explicit rule, this ++change will go unnoticed. ++ ++The snapper state module allows you to manage state implicitly, in addition ++to explicit rules, in order to define a baseline and iterate with explicit ++rules as they show that they work in production. ++ ++The workflow is: once you have a workin and audited system, you would create ++your baseline snapshot (eg. with ``salt tgt snapper.create_snapshot``) and ++define in your state this baseline using the identifier of the snapshot ++(in this case: 20): ++ ++.. code-block:: yaml ++ ++ my_baseline: ++ snapper.baseline_snapshot: ++ - number: 20 ++ - ignore: ++ - /var/log ++ - /var/cache ++ ++ ++If you have this state, and you haven't done changes to the system since the ++snapshot, and you add a user, the state will show you the changes (including ++full diffs) to ``/etc/passwd``, ``/etc/shadow``, etc if you call it ++with ``test=True`` and will undo all changes if you call it without. ++ ++This allows you to add more explicit state knowing that you are starting from a ++very well defined state, and that you can audit any change that is not part ++of your explicit configuration. ++ ++So after you made this your state, you decided to introduce a change in your ++configuration: ++ ++.. code-block:: yaml ++ ++ my_baseline: ++ snapper.baseline_snapshot: ++ - number: 20 ++ - ignore: ++ - /var/log ++ - /var/cache ++ ++ hosts_entry: ++ file.blockreplace: ++ - name: /etc/hosts ++ - content: 'First line of content' ++ - append_if_not_found: True ++ ++ ++The change in ``/etc/hosts`` will be done after any other change that deviates ++from the specified snapshot are reverted. This could be for example, ++modifications to the ``/etc/passwd`` file or changes in the ``/etc/hosts`` ++that could render your the ``hosts_entry`` rule void or dangerous. ++ ++Once you take a new snapshot and you update the baseline snapshot number to ++include the change in ``/etc/hosts`` the ``hosts_entry`` rule will basically ++do nothing. You are free to leave it there for documentation, to ensure that ++the change is made in case the snapshot is wrong, but if you remove anything ++that comes after the ``snapper.baseline_snapshot`` as it will have no effect: ++ by the moment the state is evaluated, the baseline state was already applied ++and include this change. ++ ++.. warning:: ++ Make sure you specify the baseline state before other rules, otherwise ++ the baseline state will revert all changes if they are not present in ++ the snapshot. ++ ++.. warning:: ++ Do not specify more than one baseline rule as only the last one will ++ affect the result. ++ ++:codeauthor: Duncan Mac-Vicar P. ++:codeauthor: Pablo Suárez Hernández ++ ++:maturity: new ++:platform: Linux ++''' ++ ++from __future__ import absolute_import ++ ++import os ++ ++ ++def __virtual__(): ++ ''' ++ Only load if the snapper module is available in __salt__ ++ ''' ++ return 'snapper' if 'snapper.diff' in __salt__ else False ++ ++ ++def _get_baseline_from_tag(tag): ++ ''' ++ Returns the last created baseline snapshot marked with `tag` ++ ''' ++ last_snapshot = None ++ for snapshot in __salt__['snapper.list_snapshots'](): ++ if tag == snapshot['userdata'].get("baseline_tag"): ++ if not last_snapshot or last_snapshot['timestamp'] < snapshot['timestamp']: ++ last_snapshot = snapshot ++ return last_snapshot ++ ++ ++def baseline_snapshot(name, number=None, tag=None, config='root', ignore=None): ++ ''' ++ Enforces that no file is modified comparing against a previously ++ defined snapshot identified by number. ++ ++ ignore ++ List of files to ignore ++ ''' ++ if not ignore: ++ ignore = [] ++ ++ ret = {'changes': {}, ++ 'comment': '', ++ 'name': name, ++ 'result': True} ++ ++ if number is None and tag is None: ++ ret.update({'result': False, ++ 'comment': 'Snapshot tag or number must be specified'}) ++ return ret ++ ++ if number and tag: ++ ret.update({'result': False, ++ 'comment': 'Cannot use snapshot tag and number at the same time'}) ++ return ret ++ ++ if tag: ++ snapshot = _get_baseline_from_tag(tag) ++ if not snapshot: ++ ret.update({'result': False, ++ 'comment': 'Baseline tag "{0}" not found'.format(tag)}) ++ return ret ++ number = snapshot['id'] ++ ++ status = __salt__['snapper.status']( ++ config, num_pre=number, num_post=0) ++ ++ for target in ignore: ++ if os.path.isfile(target): ++ status.pop(target, None) ++ elif os.path.isdir(target): ++ for target_file in [target_file for target_file in status.keys() if target_file.startswith(target)]: ++ status.pop(target_file, None) ++ ++ for file in status: ++ status[file]['actions'] = status[file].pop("status") ++ ++ # Only include diff for modified files ++ if "modified" in status[file]['actions']: ++ status[file].update(__salt__['snapper.diff'](config, ++ num_pre=0, ++ num_post=number, ++ filename=file)[file]) ++ ++ if __opts__['test'] and status: ++ ret['pchanges'] = ret["changes"] ++ ret['changes'] = {} ++ ret['comment'] = "{0} files changes are set to be undone".format(len(status.keys())) ++ ret['result'] = None ++ elif __opts__['test'] and not status: ++ ret['changes'] = {} ++ ret['comment'] = "Nothing to be done" ++ ret['result'] = True ++ elif not __opts__['test'] and status: ++ undo = __salt__['snapper.undo'](config, num_pre=number, num_post=0, ++ files=status.keys()) ++ ret['changes']['sumary'] = undo ++ ret['changes']['files'] = status ++ ret['result'] = True ++ else: ++ ret['comment'] = "No changes were done" ++ ret['result'] = True ++ ++ return ret +diff --git a/tests/unit/modules/snapper_test.py b/tests/unit/modules/snapper_test.py +new file mode 100644 +index 0000000..f27b2ba +--- /dev/null ++++ b/tests/unit/modules/snapper_test.py +@@ -0,0 +1,324 @@ ++# -*- coding: utf-8 -*- ++''' ++Unit tests for the Snapper module ++ ++:codeauthor: Duncan Mac-Vicar P. ++:codeauthor: Pablo Suárez Hernández ++''' ++ ++from __future__ import absolute_import ++ ++from salttesting import TestCase ++from salttesting.mock import ( ++ MagicMock, ++ patch, ++ mock_open, ++) ++ ++from salt.exceptions import CommandExecutionError ++from salttesting.helpers import ensure_in_syspath ++ensure_in_syspath('../../') ++ ++from salt.modules import snapper ++ ++# Globals ++snapper.__salt__ = dict() ++ ++DBUS_RET = { ++ 'ListSnapshots': [ ++ [42, 1, 0, 1457006571, ++ 0, 'Some description', '', ++ {'userdata1': 'userval1', 'salt_jid': '20160607130930720112'}], ++ [43, 2, 42, 1457006572, ++ 0, 'Blah Blah', '', ++ {'userdata2': 'userval2', 'salt_jid': '20160607130930720112'}] ++ ], ++ 'ListConfigs': [ ++ [u'root', u'/', { ++ u'SUBVOLUME': u'/', u'NUMBER_MIN_AGE': u'1800', ++ u'TIMELINE_LIMIT_YEARLY': u'4-10', u'NUMBER_LIMIT_IMPORTANT': u'10', ++ u'FSTYPE': u'btrfs', u'TIMELINE_LIMIT_MONTHLY': u'4-10', ++ u'ALLOW_GROUPS': u'', u'EMPTY_PRE_POST_MIN_AGE': u'1800', ++ u'EMPTY_PRE_POST_CLEANUP': u'yes', u'BACKGROUND_COMPARISON': u'yes', ++ u'TIMELINE_LIMIT_HOURLY': u'4-10', u'ALLOW_USERS': u'', ++ u'TIMELINE_LIMIT_WEEKLY': u'0', u'TIMELINE_CREATE': u'no', ++ u'NUMBER_CLEANUP': u'yes', u'TIMELINE_CLEANUP': u'yes', ++ u'SPACE_LIMIT': u'0.5', u'NUMBER_LIMIT': u'10', ++ u'TIMELINE_MIN_AGE': u'1800', u'TIMELINE_LIMIT_DAILY': u'4-10', ++ u'SYNC_ACL': u'no', u'QGROUP': u'1/0'} ++ ] ++ ], ++ 'GetFiles': [ ++ ['/root/.viminfo', 8], ++ ['/tmp/foo', 52], ++ ['/tmp/foo2', 1], ++ ['/tmp/foo3', 2], ++ ['/var/log/snapper.log', 8], ++ ['/var/cache/salt/minion/extmods/modules/snapper.py', 8], ++ ['/var/cache/salt/minion/extmods/modules/snapper.pyc', 8], ++ ], ++} ++ ++FILE_CONTENT = { ++ '/tmp/foo': { ++ "pre": "dummy text", ++ "post": "another foobar" ++ }, ++ '/tmp/foo2': { ++ "post": "another foobar" ++ } ++} ++ ++MODULE_RET = { ++ 'SNAPSHOTS': [ ++ { ++ 'userdata': {'userdata1': 'userval1', 'salt_jid': '20160607130930720112'}, ++ 'description': 'Some description', 'timestamp': 1457006571, ++ 'cleanup': '', 'user': 'root', 'type': 'pre', 'id': 42 ++ }, ++ { ++ 'pre': 42, ++ 'userdata': {'userdata2': 'userval2', 'salt_jid': '20160607130930720112'}, ++ 'description': 'Blah Blah', 'timestamp': 1457006572, ++ 'cleanup': '', 'user': 'root', 'type': 'post', 'id': 43 ++ } ++ ], ++ 'LISTCONFIGS': { ++ u'root': { ++ u'SUBVOLUME': u'/', u'NUMBER_MIN_AGE': u'1800', ++ u'TIMELINE_LIMIT_YEARLY': u'4-10', u'NUMBER_LIMIT_IMPORTANT': u'10', ++ u'FSTYPE': u'btrfs', u'TIMELINE_LIMIT_MONTHLY': u'4-10', ++ u'ALLOW_GROUPS': u'', u'EMPTY_PRE_POST_MIN_AGE': u'1800', ++ u'EMPTY_PRE_POST_CLEANUP': u'yes', u'BACKGROUND_COMPARISON': u'yes', ++ u'TIMELINE_LIMIT_HOURLY': u'4-10', u'ALLOW_USERS': u'', ++ u'TIMELINE_LIMIT_WEEKLY': u'0', u'TIMELINE_CREATE': u'no', ++ u'NUMBER_CLEANUP': u'yes', u'TIMELINE_CLEANUP': u'yes', ++ u'SPACE_LIMIT': u'0.5', u'NUMBER_LIMIT': u'10', ++ u'TIMELINE_MIN_AGE': u'1800', u'TIMELINE_LIMIT_DAILY': u'4-10', ++ u'SYNC_ACL': u'no', u'QGROUP': u'1/0' ++ } ++ }, ++ 'GETFILES': { ++ '/root/.viminfo': {'status': ['modified']}, ++ '/tmp/foo': {'status': ['type changed', 'permission changed', 'owner changed']}, ++ '/tmp/foo2': {'status': ['created']}, ++ '/tmp/foo3': {'status': ['deleted']}, ++ '/var/log/snapper.log': {'status': ['modified']}, ++ '/var/cache/salt/minion/extmods/modules/snapper.py': {'status': ['modified']}, ++ '/var/cache/salt/minion/extmods/modules/snapper.pyc': {'status': ['modified']}, ++ }, ++ 'DIFF': { ++ '/tmp/foo': { ++ 'comment': 'text file changed', ++ 'diff': "--- /.snapshots/55/snapshot/tmp/foo\n" ++ "+++ /tmp/foo\n" ++ "@@ -1 +1 @@\n" ++ "-dummy text" ++ "+another foobar" ++ }, ++ '/tmp/foo2': { ++ 'comment': 'text file created', ++ 'diff': "--- /.snapshots/55/snapshot/tmp/foo2\n" ++ "+++ /tmp/foo2\n" ++ "@@ -0,0 +1 @@\n" ++ "+another foobar", ++ }, ++ '/tmp/foo3': { ++ 'comment': 'binary file changed', ++ 'old_sha256_digest': 'e61f8b762d83f3b4aeb3689564b0ffbe54fa731a69a1e208dc9440ce0f69d19b', ++ 'new_sha256_digest': 'f18f971f1517449208a66589085ddd3723f7f6cefb56c141e3d97ae49e1d87fa', ++ } ++ } ++} ++ ++ ++class SnapperTestCase(TestCase): ++ def setUp(self): ++ self.dbus_mock = MagicMock() ++ self.DBusExceptionMock = MagicMock() # pylint: disable=invalid-name ++ self.dbus_mock.configure_mock(DBusException=self.DBusExceptionMock) ++ snapper.dbus = self.dbus_mock ++ snapper.snapper = MagicMock() ++ ++ def test__snapshot_to_data(self): ++ data = snapper._snapshot_to_data(DBUS_RET['ListSnapshots'][0]) # pylint: disable=protected-access ++ self.assertEqual(data['id'], 42) ++ self.assertNotIn('pre', data) ++ self.assertEqual(data['type'], 'pre') ++ self.assertEqual(data['user'], 'root') ++ self.assertEqual(data['timestamp'], 1457006571) ++ self.assertEqual(data['description'], 'Some description') ++ self.assertEqual(data['cleanup'], '') ++ self.assertEqual(data['userdata']['userdata1'], 'userval1') ++ ++ @patch('salt.modules.snapper.snapper.ListSnapshots', MagicMock(return_value=DBUS_RET['ListSnapshots'])) ++ def test_list_snapshots(self): ++ self.assertEqual(snapper.list_snapshots(), MODULE_RET["SNAPSHOTS"]) ++ ++ @patch('salt.modules.snapper.snapper.GetSnapshot', MagicMock(return_value=DBUS_RET['ListSnapshots'][0])) ++ def test_get_snapshot(self): ++ self.assertEqual(snapper.get_snapshot(), MODULE_RET["SNAPSHOTS"][0]) ++ self.assertEqual(snapper.get_snapshot(number=42), MODULE_RET["SNAPSHOTS"][0]) ++ self.assertNotEqual(snapper.get_snapshot(number=42), MODULE_RET["SNAPSHOTS"][1]) ++ ++ @patch('salt.modules.snapper.snapper.ListConfigs', MagicMock(return_value=DBUS_RET['ListConfigs'])) ++ def test_list_configs(self): ++ self.assertEqual(snapper.list_configs(), MODULE_RET["LISTCONFIGS"]) ++ ++ @patch('salt.modules.snapper.snapper.GetConfig', MagicMock(return_value=DBUS_RET['ListConfigs'][0])) ++ def test_get_config(self): ++ self.assertEqual(snapper.get_config(), DBUS_RET["ListConfigs"][0]) ++ ++ @patch('salt.modules.snapper.snapper.SetConfig', MagicMock()) ++ def test_set_config(self): ++ opts = {'sync_acl': True, 'dummy': False, 'foobar': 1234} ++ self.assertEqual(snapper.set_config(opts), True) ++ ++ def test_status_to_string(self): ++ self.assertEqual(snapper.status_to_string(1), ["created"]) ++ self.assertEqual(snapper.status_to_string(2), ["deleted"]) ++ self.assertEqual(snapper.status_to_string(4), ["type changed"]) ++ self.assertEqual(snapper.status_to_string(8), ["modified"]) ++ self.assertEqual(snapper.status_to_string(16), ["permission changed"]) ++ self.assertListEqual(snapper.status_to_string(24), ["modified", "permission changed"]) ++ self.assertEqual(snapper.status_to_string(32), ["owner changed"]) ++ self.assertEqual(snapper.status_to_string(64), ["group changed"]) ++ self.assertListEqual(snapper.status_to_string(97), ["created", "owner changed", "group changed"]) ++ self.assertEqual(snapper.status_to_string(128), ["extended attributes changed"]) ++ self.assertEqual(snapper.status_to_string(256), ["ACL info changed"]) ++ ++ @patch('salt.modules.snapper.snapper.CreateSingleSnapshot', MagicMock(return_value=1234)) ++ @patch('salt.modules.snapper.snapper.CreatePreSnapshot', MagicMock(return_value=1234)) ++ @patch('salt.modules.snapper.snapper.CreatePostSnapshot', MagicMock(return_value=1234)) ++ def test_create_snapshot(self): ++ for snapshot_type in ['pre', 'post', 'single']: ++ opts = { ++ '__pub_jid': 20160607130930720112, ++ 'type': snapshot_type, ++ 'description': 'Test description', ++ 'cleanup_algorithm': 'number', ++ 'pre_number': 23, ++ } ++ self.assertEqual(snapper.create_snapshot(**opts), 1234) ++ ++ @patch('salt.modules.snapper._get_last_snapshot', MagicMock(return_value={'id': 42})) ++ def test__get_num_interval(self): ++ self.assertEqual(snapper._get_num_interval(config=None, num_pre=None, num_post=None), (42, 0)) # pylint: disable=protected-access ++ self.assertEqual(snapper._get_num_interval(config=None, num_pre=None, num_post=50), (42, 50)) # pylint: disable=protected-access ++ self.assertEqual(snapper._get_num_interval(config=None, num_pre=42, num_post=50), (42, 50)) # pylint: disable=protected-access ++ ++ def test_run(self): ++ patch_dict = { ++ 'snapper.create_snapshot': MagicMock(return_value=43), ++ 'test.ping': MagicMock(return_value=True), ++ } ++ with patch.dict(snapper.__salt__, patch_dict): ++ self.assertEqual(snapper.run("test.ping"), True) ++ self.assertRaises(CommandExecutionError, snapper.run, "unknown.func") ++ ++ @patch('salt.modules.snapper._get_num_interval', MagicMock(return_value=(42, 43))) ++ @patch('salt.modules.snapper.snapper.GetComparison', MagicMock()) ++ @patch('salt.modules.snapper.snapper.GetFiles', MagicMock(return_value=DBUS_RET['GetFiles'])) ++ def test_status(self): ++ self.assertItemsEqual(snapper.status(), MODULE_RET['GETFILES']) ++ self.assertItemsEqual(snapper.status(num_pre="42", num_post=43), MODULE_RET['GETFILES']) ++ self.assertItemsEqual(snapper.status(num_pre=42), MODULE_RET['GETFILES']) ++ self.assertItemsEqual(snapper.status(num_post=43), MODULE_RET['GETFILES']) ++ ++ @patch('salt.modules.snapper.status', MagicMock(return_value=MODULE_RET['GETFILES'])) ++ def test_changed_files(self): ++ self.assertEqual(snapper.changed_files(), MODULE_RET['GETFILES'].keys()) ++ ++ @patch('salt.modules.snapper._get_num_interval', MagicMock(return_value=(42, 43))) ++ @patch('salt.modules.snapper.status', MagicMock(return_value=MODULE_RET['GETFILES'])) ++ def test_undo(self): ++ cmd_ret = 'create:0 modify:1 delete:0' ++ with patch.dict(snapper.__salt__, {'cmd.run': MagicMock(return_value=cmd_ret)}): ++ module_ret = {'create': '0', 'delete': '0', 'modify': '1'} ++ self.assertEqual(snapper.undo(files=['/tmp/foo']), module_ret) ++ ++ cmd_ret = 'create:1 modify:1 delete:0' ++ with patch.dict(snapper.__salt__, {'cmd.run': MagicMock(return_value=cmd_ret)}): ++ module_ret = {'create': '1', 'delete': '0', 'modify': '1'} ++ self.assertEqual(snapper.undo(files=['/tmp/foo', '/tmp/foo2']), module_ret) ++ ++ cmd_ret = 'create:1 modify:1 delete:1' ++ with patch.dict(snapper.__salt__, {'cmd.run': MagicMock(return_value=cmd_ret)}): ++ module_ret = {'create': '1', 'delete': '1', 'modify': '1'} ++ self.assertEqual(snapper.undo(files=['/tmp/foo', '/tmp/foo2', '/tmp/foo3']), module_ret) ++ ++ @patch('salt.modules.snapper.list_snapshots', MagicMock(return_value=MODULE_RET['SNAPSHOTS'])) ++ def test__get_jid_snapshots(self): ++ self.assertEqual( ++ snapper._get_jid_snapshots("20160607130930720112"), # pylint: disable=protected-access ++ (MODULE_RET['SNAPSHOTS'][0]['id'], MODULE_RET['SNAPSHOTS'][1]['id']) ++ ) ++ ++ @patch('salt.modules.snapper._get_jid_snapshots', MagicMock(return_value=(42, 43))) ++ @patch('salt.modules.snapper.undo', MagicMock(return_value='create:1 modify:1 delete:1')) ++ def test_undo_jid(self): ++ self.assertEqual(snapper.undo_jid(20160607130930720112), 'create:1 modify:1 delete:1') ++ ++ @patch('salt.modules.snapper._get_num_interval', MagicMock(return_value=(42, 43))) ++ @patch('salt.modules.snapper.snapper.MountSnapshot', MagicMock(side_effect=["/.snapshots/55/snapshot", ""])) ++ @patch('salt.modules.snapper.snapper.UmountSnapshot', MagicMock(return_value="")) ++ @patch('os.path.isdir', MagicMock(return_value=False)) ++ @patch('salt.modules.snapper.changed_files', MagicMock(return_value=["/tmp/foo2"])) ++ @patch('salt.modules.snapper._is_text_file', MagicMock(return_value=True)) ++ @patch('os.path.isfile', MagicMock(side_effect=[False, True])) ++ @patch('salt.utils.fopen', mock_open(read_data=FILE_CONTENT["/tmp/foo2"]['post'])) ++ def test_diff_text_file(self): ++ self.assertEqual(snapper.diff(), {"/tmp/foo2": MODULE_RET['DIFF']['/tmp/foo2']}) ++ ++ @patch('salt.modules.snapper._get_num_interval', MagicMock(return_value=(55, 0))) ++ @patch('salt.modules.snapper.snapper.MountSnapshot', MagicMock( ++ side_effect=["/.snapshots/55/snapshot", "", "/.snapshots/55/snapshot", ""])) ++ @patch('salt.modules.snapper.snapper.UmountSnapshot', MagicMock(return_value="")) ++ @patch('salt.modules.snapper.changed_files', MagicMock(return_value=["/tmp/foo", "/tmp/foo2"])) ++ @patch('salt.modules.snapper._is_text_file', MagicMock(return_value=True)) ++ @patch('os.path.isfile', MagicMock(side_effect=[True, True, False, True])) ++ @patch('os.path.isdir', MagicMock(return_value=False)) ++ def test_diff_text_files(self): ++ fopen_effect = [ ++ mock_open(read_data=FILE_CONTENT["/tmp/foo"]['pre']).return_value, ++ mock_open(read_data=FILE_CONTENT["/tmp/foo"]['post']).return_value, ++ mock_open(read_data=FILE_CONTENT["/tmp/foo2"]['post']).return_value, ++ ] ++ with patch('salt.utils.fopen') as fopen_mock: ++ fopen_mock.side_effect = fopen_effect ++ module_ret = { ++ "/tmp/foo": MODULE_RET['DIFF']["/tmp/foo"], ++ "/tmp/foo2": MODULE_RET['DIFF']["/tmp/foo2"], ++ } ++ self.assertEqual(snapper.diff(), module_ret) ++ ++ @patch('salt.modules.snapper._get_num_interval', MagicMock(return_value=(55, 0))) ++ @patch('salt.modules.snapper.snapper.MountSnapshot', MagicMock( ++ side_effect=["/.snapshots/55/snapshot", "", "/.snapshots/55/snapshot", ""])) ++ @patch('salt.modules.snapper.snapper.UmountSnapshot', MagicMock(return_value="")) ++ @patch('salt.modules.snapper.changed_files', MagicMock(return_value=["/tmp/foo3"])) ++ @patch('salt.modules.snapper._is_text_file', MagicMock(return_value=False)) ++ @patch('os.path.isfile', MagicMock(side_effect=[True, True])) ++ @patch('os.path.isdir', MagicMock(return_value=False)) ++ @patch.dict(snapper.__salt__, { ++ 'hashutil.sha256_digest': MagicMock(side_effect=[ ++ "e61f8b762d83f3b4aeb3689564b0ffbe54fa731a69a1e208dc9440ce0f69d19b", ++ "f18f971f1517449208a66589085ddd3723f7f6cefb56c141e3d97ae49e1d87fa", ++ ]) ++ }) ++ def test_diff_binary_files(self): ++ fopen_effect = [ ++ mock_open(read_data="dummy binary").return_value, ++ mock_open(read_data="dummy binary").return_value, ++ ] ++ with patch('salt.utils.fopen') as fopen_mock: ++ fopen_mock.side_effect = fopen_effect ++ module_ret = { ++ "/tmp/foo3": MODULE_RET['DIFF']["/tmp/foo3"], ++ } ++ self.assertEqual(snapper.diff(), module_ret) ++ ++ ++if __name__ == '__main__': ++ from integration import run_tests ++ run_tests(SnapperTestCase, needs_daemon=False) +-- +2.9.2 + diff --git a/0010-unit-tests-for-rpm.checksum-and-zypper.download.patch b/0010-unit-tests-for-rpm.checksum-and-zypper.download.patch deleted file mode 100644 index 9a3461e..0000000 --- a/0010-unit-tests-for-rpm.checksum-and-zypper.download.patch +++ /dev/null @@ -1,105 +0,0 @@ -From 2742ee76ccc50cd4f84e44861ef82ec5f3b5234a Mon Sep 17 00:00:00 2001 -From: =?UTF-8?q?Pablo=20Su=C3=A1rez=20Hern=C3=A1ndez?= - -Date: Wed, 25 May 2016 17:08:16 +0100 -Subject: [PATCH 10/11] unit tests for rpm.checksum() and zypper.download() - -lint issue fixed ---- - tests/unit/modules/rpm_test.py | 16 ++++++++++++++++ - tests/unit/modules/zypp/zypper-download.xml | 19 +++++++++++++++++++ - tests/unit/modules/zypper_test.py | 25 +++++++++++++++++++++++++ - 3 files changed, 60 insertions(+) - create mode 100644 tests/unit/modules/zypp/zypper-download.xml - -diff --git a/tests/unit/modules/rpm_test.py b/tests/unit/modules/rpm_test.py -index f180736..4042137 100644 ---- a/tests/unit/modules/rpm_test.py -+++ b/tests/unit/modules/rpm_test.py -@@ -95,6 +95,22 @@ class RpmTestCase(TestCase): - self.assertDictEqual(rpm.owner('/usr/bin/python', '/usr/bin/vim'), - ret) - -+ # 'checksum' function tests: 1 -+ -+ def test_checksum(self): -+ ''' -+ Test if checksum validate as expected -+ ''' -+ ret = { -+ "file1.rpm": True, -+ "file2.rpm": False, -+ "file3.rpm": False, -+ } -+ -+ mock = MagicMock(side_effect=[True, 0, True, 1, False, 0]) -+ with patch.dict(rpm.__salt__, {'file.file_exists': mock, 'cmd.retcode': mock}): -+ self.assertDictEqual(rpm.checksum("file1.rpm", "file2.rpm", "file3.rpm"), ret) -+ - @patch('salt.modules.rpm.HAS_RPM', True) - def test_version_cmp_rpm(self): - ''' -diff --git a/tests/unit/modules/zypp/zypper-download.xml b/tests/unit/modules/zypp/zypper-download.xml -new file mode 100644 -index 0000000..eeea0a5 ---- /dev/null -+++ b/tests/unit/modules/zypp/zypper-download.xml -@@ -0,0 +1,19 @@ -+ -+ -+ Loading repository data... -+ Reading installed packages... -+ Argument resolves to no package: foo -+ -+ -+ -+ package -+ nmap -+ -+ x86_64 -+ -+ -+ -+ -+ -+ download: Done. -+ -diff --git a/tests/unit/modules/zypper_test.py b/tests/unit/modules/zypper_test.py -index 4e735cd..9ec2b83 100644 ---- a/tests/unit/modules/zypper_test.py -+++ b/tests/unit/modules/zypper_test.py -@@ -354,6 +354,31 @@ class ZypperTestCase(TestCase): - self.assertTrue(pkgs.get(pkg_name)) - self.assertEqual(pkgs[pkg_name], pkg_version) - -+ def test_download(self): -+ ''' -+ Test package download -+ :return: -+ ''' -+ download_out = { -+ 'stdout': get_test_data('zypper-download.xml'), -+ 'stderr': None, -+ 'retcode': 0 -+ } -+ -+ test_out = { -+ 'nmap': { -+ 'path': u'/var/cache/zypp/packages/SLE-12-x86_64-Pool/x86_64/nmap-6.46-1.72.x86_64.rpm', -+ 'repository-alias': u'SLE-12-x86_64-Pool', -+ 'repository-name': u'SLE-12-x86_64-Pool' -+ } -+ } -+ -+ with patch.dict(zypper.__salt__, {'cmd.run_all': MagicMock(return_value=download_out)}): -+ with patch.dict(zypper.__salt__, {'lowpkg.checksum': MagicMock(return_value=True)}): -+ self.assertEqual(zypper.download("nmap"), test_out) -+ test_out['_error'] = "The following package(s) failed to download: foo" -+ self.assertEqual(zypper.download("nmap", "foo"), test_out) -+ - def test_remove_purge(self): - ''' - Test package removal --- -2.8.2 - diff --git a/0011-fix-salt-summary-to-count-not-responding-minions-cor.patch b/0011-fix-salt-summary-to-count-not-responding-minions-cor.patch new file mode 100644 index 0000000..fad1d7c --- /dev/null +++ b/0011-fix-salt-summary-to-count-not-responding-minions-cor.patch @@ -0,0 +1,29 @@ +From e3969a80bdf2d0af0c87463af859d8daf314f018 Mon Sep 17 00:00:00 2001 +From: Michael Calmer +Date: Tue, 21 Jun 2016 13:12:48 +0200 +Subject: [PATCH 11/12] fix salt --summary to count not responding minions + correctly (bsc#972311) + +In case a minion is not responding a dict is returned instead of a string. +--- + salt/cli/salt.py | 4 +++- + 1 file changed, 3 insertions(+), 1 deletion(-) + +diff --git a/salt/cli/salt.py b/salt/cli/salt.py +index baeaf23..ec18f2e 100644 +--- a/salt/cli/salt.py ++++ b/salt/cli/salt.py +@@ -264,7 +264,9 @@ class SaltCMD(parsers.SaltCMDOptionParser): + not_connected_minions = [] + failed_minions = [] + for each_minion in ret: +- minion_ret = ret[each_minion].get('ret') ++ minion_ret = ret[each_minion] ++ if (isinstance(minion_ret, dict) and 'ret' in minion_ret): ++ minion_ret = ret[each_minion].get('ret') + if ( + isinstance(minion_ret, string_types) + and minion_ret.startswith("Minion did not return") +-- +2.9.2 + diff --git a/0011-jobs.exit_success-allow-to-check-if-a-job-has-execut.patch b/0011-jobs.exit_success-allow-to-check-if-a-job-has-execut.patch deleted file mode 100644 index 2614e9f..0000000 --- a/0011-jobs.exit_success-allow-to-check-if-a-job-has-execut.patch +++ /dev/null @@ -1,53 +0,0 @@ -From e0f15c5292869549b5c80997ccb3282961be8e49 Mon Sep 17 00:00:00 2001 -From: =?UTF-8?q?Pablo=20Su=C3=A1rez=20Hern=C3=A1ndez?= - -Date: Tue, 24 May 2016 09:21:43 +0100 -Subject: [PATCH 11/11] jobs.exit_success allow to check if a job has executed - and exit successfully - -jobs.exit_success() now works parsing the results of jobs.lookup_id() ---- - salt/runners/jobs.py | 27 +++++++++++++++++++++++++++ - 1 file changed, 27 insertions(+) - -diff --git a/salt/runners/jobs.py b/salt/runners/jobs.py -index 57d0324..e2b8737 100644 ---- a/salt/runners/jobs.py -+++ b/salt/runners/jobs.py -@@ -488,6 +488,33 @@ def print_job(jid, ext_source=None, outputter=None): - return ret - - -+def exit_success(jid, ext_source=None): -+ ''' -+ Check if a job has been executed and exit successfully -+ -+ jid -+ The jid to look up. -+ ext_source -+ The external job cache to use. Default: `None`. -+ -+ CLI Example: -+ .. code-block:: bash -+ salt-run jobs.exit_success 20160520145827701627 -+ ''' -+ ret = dict() -+ -+ data = lookup_jid( -+ jid, -+ ext_source=ext_source -+ ) -+ -+ for minion in data: -+ if "retcode" in data[minion]: -+ ret[minion] = True if not data[minion]['retcode'] else False -+ -+ return ret -+ -+ - def last_run(ext_source=None, - outputter=None, - metadata=None, --- -2.8.2 - diff --git a/0012-Fix-pkgrepo.managed-gpgkey-argument-bsc-979448.patch b/0012-Fix-pkgrepo.managed-gpgkey-argument-bsc-979448.patch deleted file mode 100644 index 760aef7..0000000 --- a/0012-Fix-pkgrepo.managed-gpgkey-argument-bsc-979448.patch +++ /dev/null @@ -1,325 +0,0 @@ -From b0e1ba5158cc4b54102bac200ae343935eeb2db5 Mon Sep 17 00:00:00 2001 -From: =?UTF-8?q?Mihai=20Dinc=C4=83?= -Date: Mon, 23 May 2016 23:15:29 +0200 -Subject: [PATCH 12/12] Fix pkgrepo.managed gpgkey argument (bsc#979448) - -* Call zypper refresh after adding/modifying a repository - -* Calling `zypper --gpg-auto-import-keys refresh` is required after -adding/modifying a repository because `--gpg-auto-import-keys` doesn't -do anything when called with `zypper ar` or `zypper mr`. -Without calling `zypper --gpg-auto-import-keys refresh` here, calling -`zypper ref` after adding/removing would still ask for -accepting/rejecting the gpg key. - -* Update test method names to pass pylint - -* Reduce dicts and lists to one line where possible - -* Reverse if conditions and rename variable - -* Assert only gpgautoimport: True works - -* Improve zypper_patcher_config looks - -* DRY test ---- - salt/modules/zypper.py | 24 +++-- - tests/unit/modules/zypper_test.py | 213 ++++++++++++++++++++++++++++++++++++++ - 2 files changed, 229 insertions(+), 8 deletions(-) - -diff --git a/salt/modules/zypper.py b/salt/modules/zypper.py -index 39b071b..2c72448 100644 ---- a/salt/modules/zypper.py -+++ b/salt/modules/zypper.py -@@ -767,6 +767,8 @@ def mod_repo(repo, **kwargs): - - # Modify added or existing repo according to the options - cmd_opt = [] -+ global_cmd_opt = [] -+ call_refresh = False - - if 'enabled' in kwargs: - cmd_opt.append(kwargs['enabled'] and '--enable' or '--disable') -@@ -780,21 +782,27 @@ def mod_repo(repo, **kwargs): - if 'gpgcheck' in kwargs: - cmd_opt.append(kwargs['gpgcheck'] and '--gpgcheck' or '--no-gpgcheck') - -- if kwargs.get('gpgautoimport') is True: -- cmd_opt.append('--gpg-auto-import-keys') -- - if 'priority' in kwargs: - cmd_opt.append("--priority={0}".format(kwargs.get('priority', DEFAULT_PRIORITY))) - - if 'humanname' in kwargs: - cmd_opt.append("--name='{0}'".format(kwargs.get('humanname'))) - -- if cmd_opt: -- cmd_opt.append(repo) -- __zypper__.refreshable.xml.call('mr', *cmd_opt) -+ if kwargs.get('gpgautoimport') is True: -+ global_cmd_opt.append('--gpg-auto-import-keys') -+ call_refresh = True - -- # If repo nor added neither modified, error should be thrown -- if not added and not cmd_opt: -+ if cmd_opt: -+ cmd_opt = global_cmd_opt + ['mr'] + cmd_opt + [repo] -+ __zypper__.refreshable.xml.call(*cmd_opt) -+ -+ if call_refresh: -+ # when used with "zypper ar --refresh" or "zypper mr --refresh" -+ # --gpg-auto-import-keys is not doing anything -+ # so we need to specifically refresh here with --gpg-auto-import-keys -+ refresh_opts = global_cmd_opt + ['refresh'] + [repo] -+ __zypper__.xml.call(*refresh_opts) -+ elif not added and not cmd_opt: - raise CommandExecutionError( - 'Specified arguments did not result in modification of repo' - ) -diff --git a/tests/unit/modules/zypper_test.py b/tests/unit/modules/zypper_test.py -index 9ec2b83..c4f7597 100644 ---- a/tests/unit/modules/zypper_test.py -+++ b/tests/unit/modules/zypper_test.py -@@ -9,7 +9,9 @@ from __future__ import absolute_import - # Import Salt Testing Libs - from salttesting import TestCase, skipIf - from salttesting.mock import ( -+ Mock, - MagicMock, -+ call, - patch, - NO_MOCK, - NO_MOCK_REASON -@@ -54,10 +56,26 @@ zypper.rpm = None - - @skipIf(NO_MOCK, NO_MOCK_REASON) - class ZypperTestCase(TestCase): -+ - ''' - Test cases for salt.modules.zypper - ''' - -+ def setUp(self): -+ self.new_repo_config = dict( -+ name='mock-repo-name', -+ url='http://repo.url/some/path' -+ ) -+ side_effect = [ -+ Mock(**{'sections.return_value': []}), -+ Mock(**{'sections.return_value': [self.new_repo_config['name']]}) -+ ] -+ self.zypper_patcher_config = { -+ '_get_configured_repos': Mock(side_effect=side_effect), -+ '__zypper__': Mock(), -+ 'get_repo': Mock() -+ } -+ - def test_list_upgrades(self): - ''' - List package upgrades -@@ -438,6 +456,201 @@ class ZypperTestCase(TestCase): - self.assertEqual(r_info['enabled'], alias == 'SLE12-SP1-x86_64-Update') - self.assertEqual(r_info['autorefresh'], alias == 'SLE12-SP1-x86_64-Update') - -+ def test_repo_add_nomod_noref(self): -+ ''' -+ Test mod_repo adds the new repo and nothing else -+ -+ :return: -+ ''' -+ zypper_patcher = patch.multiple( -+ 'salt.modules.zypper', **self.zypper_patcher_config) -+ -+ url = self.new_repo_config['url'] -+ name = self.new_repo_config['name'] -+ with zypper_patcher: -+ zypper.mod_repo(name, **{'url': url}) -+ self.assertEqual( -+ zypper.__zypper__.xml.call.call_args_list, -+ [call('ar', url, name)] -+ ) -+ zypper.__zypper__.refreshable.xml.call.assert_not_called() -+ -+ def test_repo_noadd_nomod_noref(self): -+ ''' -+ Test mod_repo detects the repo already exists, -+ no modification was requested and no refresh requested either -+ -+ :return: -+ ''' -+ url = self.new_repo_config['url'] -+ name = self.new_repo_config['name'] -+ self.zypper_patcher_config['_get_configured_repos'] = Mock( -+ **{'return_value.sections.return_value': [name]} -+ ) -+ zypper_patcher = patch.multiple( -+ 'salt.modules.zypper', **self.zypper_patcher_config) -+ -+ with zypper_patcher: -+ with self.assertRaisesRegexp( -+ Exception, -+ 'Specified arguments did not result in modification of repo' -+ ): -+ zypper.mod_repo(name, **{'url': url}) -+ with self.assertRaisesRegexp( -+ Exception, -+ 'Specified arguments did not result in modification of repo' -+ ): -+ zypper.mod_repo(name, **{'url': url, 'gpgautoimport': 'a'}) -+ -+ zypper.__zypper__.xml.call.assert_not_called() -+ zypper.__zypper__.refreshable.xml.call.assert_not_called() -+ -+ def test_repo_add_mod_noref(self): -+ ''' -+ Test mod_repo adds the new repo and call modify to update autorefresh -+ -+ :return: -+ ''' -+ zypper_patcher = patch.multiple( -+ 'salt.modules.zypper', **self.zypper_patcher_config) -+ -+ url = self.new_repo_config['url'] -+ name = self.new_repo_config['name'] -+ with zypper_patcher: -+ zypper.mod_repo(name, **{'url': url, 'refresh': True}) -+ self.assertEqual( -+ zypper.__zypper__.xml.call.call_args_list, -+ [call('ar', url, name)] -+ ) -+ zypper.__zypper__.refreshable.xml.call.assert_called_once_with( -+ 'mr', '--refresh', name -+ ) -+ -+ def test_repo_noadd_mod_noref(self): -+ ''' -+ Test mod_repo detects the repository exists, -+ calls modify to update 'autorefresh' but does not call refresh -+ -+ :return: -+ ''' -+ url = self.new_repo_config['url'] -+ name = self.new_repo_config['name'] -+ self.zypper_patcher_config['_get_configured_repos'] = Mock( -+ **{'return_value.sections.return_value': [name]}) -+ zypper_patcher = patch.multiple( -+ 'salt.modules.zypper', **self.zypper_patcher_config) -+ with zypper_patcher: -+ zypper.mod_repo(name, **{'url': url, 'refresh': True}) -+ zypper.__zypper__.xml.call.assert_not_called() -+ zypper.__zypper__.refreshable.xml.call.assert_called_once_with( -+ 'mr', '--refresh', name -+ ) -+ -+ def test_repo_add_nomod_ref(self): -+ ''' -+ Test mod_repo adds the new repo and refreshes the repo with -+ `zypper --gpg-auto-import-keys refresh ` -+ -+ :return: -+ ''' -+ zypper_patcher = patch.multiple( -+ 'salt.modules.zypper', **self.zypper_patcher_config) -+ -+ url = self.new_repo_config['url'] -+ name = self.new_repo_config['name'] -+ with zypper_patcher: -+ zypper.mod_repo(name, **{'url': url, 'gpgautoimport': True}) -+ self.assertEqual( -+ zypper.__zypper__.xml.call.call_args_list, -+ [ -+ call('ar', url, name), -+ call('--gpg-auto-import-keys', 'refresh', name) -+ ] -+ ) -+ zypper.__zypper__.refreshable.xml.call.assert_not_called() -+ -+ def test_repo_noadd_nomod_ref(self): -+ ''' -+ Test mod_repo detects the repo already exists, -+ has nothing to modify and refreshes the repo with -+ `zypper --gpg-auto-import-keys refresh ` -+ -+ :return: -+ ''' -+ url = self.new_repo_config['url'] -+ name = self.new_repo_config['name'] -+ self.zypper_patcher_config['_get_configured_repos'] = Mock( -+ **{'return_value.sections.return_value': [name]} -+ ) -+ zypper_patcher = patch.multiple( -+ 'salt.modules.zypper', **self.zypper_patcher_config) -+ -+ with zypper_patcher: -+ zypper.mod_repo(name, **{'url': url, 'gpgautoimport': True}) -+ self.assertEqual( -+ zypper.__zypper__.xml.call.call_args_list, -+ [call('--gpg-auto-import-keys', 'refresh', name)] -+ ) -+ zypper.__zypper__.refreshable.xml.call.assert_not_called() -+ -+ def test_repo_add_mod_ref(self): -+ ''' -+ Test mod_repo adds the new repo, -+ calls modify to update 'autorefresh' and refreshes the repo with -+ `zypper --gpg-auto-import-keys refresh ` -+ -+ :return: -+ ''' -+ zypper_patcher = patch.multiple( -+ 'salt.modules.zypper', **self.zypper_patcher_config) -+ -+ url = self.new_repo_config['url'] -+ name = self.new_repo_config['name'] -+ with zypper_patcher: -+ zypper.mod_repo( -+ name, -+ **{'url': url, 'refresh': True, 'gpgautoimport': True} -+ ) -+ self.assertEqual( -+ zypper.__zypper__.xml.call.call_args_list, -+ [ -+ call('ar', url, name), -+ call('--gpg-auto-import-keys', 'refresh', name) -+ ] -+ ) -+ zypper.__zypper__.refreshable.xml.call.assert_called_once_with( -+ '--gpg-auto-import-keys', 'mr', '--refresh', name -+ ) -+ -+ def test_repo_noadd_mod_ref(self): -+ ''' -+ Test mod_repo detects the repo already exists, -+ calls modify to update 'autorefresh' and refreshes the repo with -+ `zypper --gpg-auto-import-keys refresh ` -+ -+ :return: -+ ''' -+ url = self.new_repo_config['url'] -+ name = self.new_repo_config['name'] -+ self.zypper_patcher_config['_get_configured_repos'] = Mock( -+ **{'return_value.sections.return_value': [name]} -+ ) -+ zypper_patcher = patch.multiple( -+ 'salt.modules.zypper', **self.zypper_patcher_config) -+ -+ with zypper_patcher: -+ zypper.mod_repo( -+ name, -+ **{'url': url, 'refresh': True, 'gpgautoimport': True} -+ ) -+ self.assertEqual( -+ zypper.__zypper__.xml.call.call_args_list, -+ [call('--gpg-auto-import-keys', 'refresh', name)] -+ ) -+ zypper.__zypper__.refreshable.xml.call.assert_called_once_with( -+ '--gpg-auto-import-keys', 'mr', '--refresh', name -+ ) -+ - if __name__ == '__main__': - from integration import run_tests - run_tests(ZypperTestCase, needs_daemon=False) --- -2.8.3 - diff --git a/0012-Run-salt-api-as-user-salt-bsc-990029.patch b/0012-Run-salt-api-as-user-salt-bsc-990029.patch new file mode 100644 index 0000000..ab45df9 --- /dev/null +++ b/0012-Run-salt-api-as-user-salt-bsc-990029.patch @@ -0,0 +1,24 @@ +From af2ff55739187c59dc04e396b7787301a49a9dba Mon Sep 17 00:00:00 2001 +From: Michael Calmer +Date: Fri, 29 Jul 2016 10:50:21 +0200 +Subject: [PATCH 12/12] Run salt-api as user salt (bsc#990029) + +--- + pkg/salt-api.service | 1 + + 1 file changed, 1 insertion(+) + +diff --git a/pkg/salt-api.service b/pkg/salt-api.service +index 72379ba..9be2cb8 100644 +--- a/pkg/salt-api.service ++++ b/pkg/salt-api.service +@@ -3,6 +3,7 @@ Description=The Salt API + After=network.target + + [Service] ++User=salt + Type=simple + LimitNOFILE=8192 + ExecStart=/usr/bin/salt-api +-- +2.9.2 + diff --git a/0013-Deprecate-status.uptime-one-version-later.patch b/0013-Deprecate-status.uptime-one-version-later.patch new file mode 100644 index 0000000..a1e38cf --- /dev/null +++ b/0013-Deprecate-status.uptime-one-version-later.patch @@ -0,0 +1,25 @@ +From d19c9591c1dfbafec24a7d76402dcc9e2b17b047 Mon Sep 17 00:00:00 2001 +From: Bo Maryniuk +Date: Thu, 4 Aug 2016 11:28:19 +0200 +Subject: [PATCH 13/13] Deprecate status.uptime one version later + +--- + salt/modules/status.py | 2 +- + 1 file changed, 1 insertion(+), 1 deletion(-) + +diff --git a/salt/modules/status.py b/salt/modules/status.py +index 029426b..cebd57b 100644 +--- a/salt/modules/status.py ++++ b/salt/modules/status.py +@@ -132,7 +132,7 @@ def custom(): + return ret + + +-@with_deprecated(globals(), "Boron") ++@with_deprecated(globals(), "Carbon") + def uptime(): + ''' + Return the uptime for this system. +-- +2.9.2 + diff --git a/0014-Add-ignore_repo_failure-option-to-suppress-zypper-s-.patch b/0014-Add-ignore_repo_failure-option-to-suppress-zypper-s-.patch new file mode 100644 index 0000000..0680718 --- /dev/null +++ b/0014-Add-ignore_repo_failure-option-to-suppress-zypper-s-.patch @@ -0,0 +1,82 @@ +From f860f7ccb3dba6b8f0cef61e2d9658a3116e3c3c Mon Sep 17 00:00:00 2001 +From: Bo Maryniuk +Date: Mon, 15 Aug 2016 15:03:53 +0200 +Subject: [PATCH 14/15] Add ignore_repo_failure option to suppress zypper's + exit code 106 on unavailable repos + +--- + salt/modules/zypper.py | 23 ++++++++++++++++++++--- + 1 file changed, 20 insertions(+), 3 deletions(-) + +diff --git a/salt/modules/zypper.py b/salt/modules/zypper.py +index 547d72b..1c60f0f 100644 +--- a/salt/modules/zypper.py ++++ b/salt/modules/zypper.py +@@ -100,6 +100,18 @@ class _Zypper(object): + self.__no_lock = False + self.__no_raise = False + self.__refresh = False ++ self.__ignore_repo_failure = False ++ ++ def __call__(self, *args, **kwargs): ++ ''' ++ :param args: ++ :param kwargs: ++ :return: ++ ''' ++ # Ignore exit code for 106 (repo is not available) ++ if 'no_repo_failure' in kwargs: ++ self.__ignore_repo_failure = kwargs['no_repo_failure'] ++ return self + + def __getattr__(self, item): + ''' +@@ -275,7 +287,7 @@ class _Zypper(object): + __salt__['event.fire_master']({'success': not len(self.error_msg), + 'info': self.error_msg or 'Zypper has been released'}, + self.TAG_RELEASED) +- if self.error_msg and not self.__no_raise: ++ if self.error_msg and not self.__no_raise and not self.__ignore_repo_failure: + raise CommandExecutionError('Zypper command failure: {0}'.format(self.error_msg)) + + return self._is_xml_mode() and dom.parseString(self.__call_result['stdout']) or self.__call_result['stdout'] +@@ -863,6 +875,7 @@ def install(name=None, + downloadonly=None, + skip_verify=False, + version=None, ++ ignore_repo_failure=False, + **kwargs): + ''' + Install the passed package(s), add refresh=True to force a 'zypper refresh' +@@ -929,6 +942,10 @@ def install(name=None, + + salt '*' pkg.install sources='[{"foo": "salt://foo.rpm"},{"bar": "salt://bar.rpm"}]' + ++ ignore_repo_failure ++ Zypper returns error code 106 if one of the repositories are not available for various reasons. ++ In case to set strict check, this parameter needs to be set to True. Default: False. ++ + + Returns a dict containing the new package names and versions:: + +@@ -1000,7 +1017,7 @@ def install(name=None, + while targets: + cmd = cmd_install + targets[:500] + targets = targets[500:] +- for line in __zypper__.call(*cmd).splitlines(): ++ for line in __zypper__(no_repo_failure=ignore_repo_failure).call(*cmd).splitlines(): + match = re.match(r"^The selected package '([^']+)'.+has lower version", line) + if match: + downgrades.append(match.group(1)) +@@ -1008,7 +1025,7 @@ def install(name=None, + while downgrades: + cmd = cmd_install + ['--force'] + downgrades[:500] + downgrades = downgrades[500:] +- __zypper__.call(*cmd) ++ __zypper__(no_repo_failure=ignore_repo_failure).call(*cmd) + + __context__.pop('pkg.list_pkgs', None) + new = list_pkgs() +-- +2.9.3 + diff --git a/0015-Remove-zypper-s-raise-exception-if-mod_repo-has-no-a.patch b/0015-Remove-zypper-s-raise-exception-if-mod_repo-has-no-a.patch new file mode 100644 index 0000000..67075fe --- /dev/null +++ b/0015-Remove-zypper-s-raise-exception-if-mod_repo-has-no-a.patch @@ -0,0 +1,78 @@ +From 0809c60c693eb5d2e9569c24d995818097c6920d Mon Sep 17 00:00:00 2001 +From: Bo Maryniuk +Date: Mon, 15 Aug 2016 17:35:01 +0200 +Subject: [PATCH 15/15] Remove zypper's raise exception if mod_repo has no + arguments and/or no changes + +* Fix Unit test for suppressing the exception removal on non-modified repos +--- + salt/modules/zypper.py | 11 +++++++---- + tests/unit/modules/zypper_test.py | 15 +++------------ + 2 files changed, 10 insertions(+), 16 deletions(-) + +diff --git a/salt/modules/zypper.py b/salt/modules/zypper.py +index 1c60f0f..1ec903e 100644 +--- a/salt/modules/zypper.py ++++ b/salt/modules/zypper.py +@@ -824,6 +824,7 @@ def mod_repo(repo, **kwargs): + cmd_opt = global_cmd_opt + ['mr'] + cmd_opt + [repo] + __zypper__.refreshable.xml.call(*cmd_opt) + ++ comment = None + if call_refresh: + # when used with "zypper ar --refresh" or "zypper mr --refresh" + # --gpg-auto-import-keys is not doing anything +@@ -831,11 +832,13 @@ def mod_repo(repo, **kwargs): + refresh_opts = global_cmd_opt + ['refresh'] + [repo] + __zypper__.xml.call(*refresh_opts) + elif not added and not cmd_opt: +- raise CommandExecutionError( +- 'Specified arguments did not result in modification of repo' +- ) ++ comment = 'Specified arguments did not result in modification of repo' ++ ++ repo = get_repo(repo) ++ if comment: ++ repo['comment'] = comment + +- return get_repo(repo) ++ return repo + + + def refresh_db(): +diff --git a/tests/unit/modules/zypper_test.py b/tests/unit/modules/zypper_test.py +index 2f2b323..7caa707 100644 +--- a/tests/unit/modules/zypper_test.py ++++ b/tests/unit/modules/zypper_test.py +@@ -73,7 +73,7 @@ class ZypperTestCase(TestCase): + self.zypper_patcher_config = { + '_get_configured_repos': Mock(side_effect=side_effect), + '__zypper__': Mock(), +- 'get_repo': Mock() ++ 'get_repo': Mock(return_value={}) + } + + def test_list_upgrades(self): +@@ -493,17 +493,8 @@ class ZypperTestCase(TestCase): + 'salt.modules.zypper', **self.zypper_patcher_config) + + with zypper_patcher: +- with self.assertRaisesRegexp( +- Exception, +- 'Specified arguments did not result in modification of repo' +- ): +- zypper.mod_repo(name, **{'url': url}) +- with self.assertRaisesRegexp( +- Exception, +- 'Specified arguments did not result in modification of repo' +- ): +- zypper.mod_repo(name, **{'url': url, 'gpgautoimport': 'a'}) +- ++ self.assertEqual(zypper.mod_repo(name, **{'url': url}), ++ {'comment': 'Specified arguments did not result in modification of repo'}) + zypper.__zypper__.xml.call.assert_not_called() + zypper.__zypper__.refreshable.xml.call.assert_not_called() + +-- +2.9.3 + diff --git a/0016-Improve-Mock-to-be-flexible-and-able-to-mock-methods.patch b/0016-Improve-Mock-to-be-flexible-and-able-to-mock-methods.patch new file mode 100644 index 0000000..5b557f1 --- /dev/null +++ b/0016-Improve-Mock-to-be-flexible-and-able-to-mock-methods.patch @@ -0,0 +1,83 @@ +From 50865e300e6e90c5cc80c8878949a2f3bcaaeeec Mon Sep 17 00:00:00 2001 +From: Bo Maryniuk +Date: Thu, 25 Aug 2016 16:47:08 +0200 +Subject: [PATCH 16/16] Improve Mock to be flexible and able to mock methods + from the mocked modules + +* Configure importing Mock to handle 'total' method from psutils properly +--- + doc/conf.py | 37 +++++++++++++++++++++++++------------ + 1 file changed, 25 insertions(+), 12 deletions(-) + +diff --git a/doc/conf.py b/doc/conf.py +index 9cefed8..b73ca2a 100644 +--- a/doc/conf.py ++++ b/doc/conf.py +@@ -15,31 +15,40 @@ from sphinx.directives import TocTree + # pylint: disable=R0903 + class Mock(object): + ''' +- Mock out specified imports ++ Mock out specified imports. + + This allows autodoc to do its thing without having oodles of req'd + installed libs. This doesn't work with ``import *`` imports. + ++ This Mock class can be configured to return a specific values at specific names, if required. ++ + http://read-the-docs.readthedocs.org/en/latest/faq.html#i-get-import-errors-on-libraries-that-depend-on-c-modules + ''' +- def __init__(self, *args, **kwargs): +- pass ++ def __init__(self, mapping=None, *args, **kwargs): ++ """ ++ Mapping allows to bypass the Mock object, but actually assign ++ a specific value, expected by a specific attribute returned. ++ """ ++ self.__mapping = mapping or {} + + __all__ = [] + + def __call__(self, *args, **kwargs): +- ret = Mock() + # If mocked function is used as a decorator, expose decorated function. + # if args and callable(args[-1]): + # functools.update_wrapper(ret, args[0]) +- return ret +- +- @classmethod +- def __getattr__(cls, name): +- if name in ('__file__', '__path__'): +- return '/dev/null' ++ return Mock(mapping=self.__mapping) ++ ++ def __getattr__(self, name): ++ #__mapping = {'total': 0} ++ data = None ++ if name in self.__mapping: ++ data = self.__mapping.get(name) ++ elif name in ('__file__', '__path__'): ++ data = '/dev/null' + else: +- return Mock() ++ data = Mock(mapping=self.__mapping) ++ return data + # pylint: enable=R0903 + + MOCK_MODULES = [ +@@ -133,7 +142,11 @@ MOCK_MODULES = [ + ] + + for mod_name in MOCK_MODULES: +- sys.modules[mod_name] = Mock() ++ if mod_name == 'psutil': ++ mock = Mock(mapping={'total': 0}) # Otherwise it will crash Sphinx ++ else: ++ mock = Mock() ++ sys.modules[mod_name] = mock + + def mock_decorator_with_params(*oargs, **okwargs): + ''' +-- +2.9.3 + diff --git a/0017-Check-for-single-quote-before-splitting-on-single-qu.patch b/0017-Check-for-single-quote-before-splitting-on-single-qu.patch new file mode 100644 index 0000000..ae9fb73 --- /dev/null +++ b/0017-Check-for-single-quote-before-splitting-on-single-qu.patch @@ -0,0 +1,35 @@ +From e9b4a199f48abc94be71082c56b6b059c6694dc0 Mon Sep 17 00:00:00 2001 +From: Eric Jackson +Date: Tue, 30 Aug 2016 15:47:07 -0400 +Subject: [PATCH 17/17] Check for single quote before splitting on single quote + +Signed-off-by: Eric Jackson + +Lint for #35916 + +Merges #35916 +--- + salt/modules/zypper.py | 4 ++-- + 1 file changed, 2 insertions(+), 2 deletions(-) + +diff --git a/salt/modules/zypper.py b/salt/modules/zypper.py +index 1ec903e..4bb9a09 100644 +--- a/salt/modules/zypper.py ++++ b/salt/modules/zypper.py +@@ -859,11 +859,11 @@ def refresh_db(): + for line in out.splitlines(): + if not line: + continue +- if line.strip().startswith('Repository'): ++ if line.strip().startswith('Repository') and '\'' in line: + key = line.split('\'')[1].strip() + if 'is up to date' in line: + ret[key] = False +- elif line.strip().startswith('Building'): ++ elif line.strip().startswith('Building') and '\'' in line: + key = line.split('\'')[1].strip() + if 'done' in line: + ret[key] = True +-- +2.8.2 + diff --git a/0018-Unit-tests-fixes-for-2016.3.2.patch b/0018-Unit-tests-fixes-for-2016.3.2.patch new file mode 100644 index 0000000..c64df07 --- /dev/null +++ b/0018-Unit-tests-fixes-for-2016.3.2.patch @@ -0,0 +1,927 @@ +From e5fc36b5fad0683f57022bf2f3c63f453cda5e8d Mon Sep 17 00:00:00 2001 +From: =?UTF-8?q?Pablo=20Su=C3=A1rez=20Hern=C3=A1ndez?= + +Date: Tue, 6 Sep 2016 11:21:05 +0100 +Subject: [PATCH 18/19] Unit tests fixes for 2016.3.2 + +* Fixing skipped boto tests to prevent errors if boto3 does not exists. +* Fix tests that assert CommandExecutionError (#32485) + Trying to assert that an exception was raised using + helper_open.write.assertRaises() is bogus--there is no such method. Use + standard unittest.assertRaises() instead. +* Skip utils_test if timelib is not installed (#32699) + date_cast() throws a RuntimeError, not an ImportError +* Fix tests (#35693) + Fix tests/unit/modules/useradd_test.py::UserAddTestCase::test_info + Fix unit/pyobjects_test.py::MapTests::test_map + Fix tests/unit/pyobjects_test.py::RendererTests::test_extend + Fix tests/unit/pyobjects_test.py::RendererTests::test_requisite_implicit_list +* Fix tests to prevent errors when libcloud is not present +* Fixed _interfaces_ifconfig output for SunOS test +* Fix PortageConfigTestCase in case of portage is not present +* Rename dockerio.py unit tests to dockerio_test.py + These tests have never run automatically because of an incorrect file name. + Added a skipIf on these tests as they are currently non-functioning and the + module they're testing has been deprecated. +* Prevent tests failures if boto does not exists +--- + salt/modules/boto_elb.py | 2 +- + salt/modules/linux_sysctl.py | 6 +- + tests/unit/cloud/clouds/dimensiondata_test.py | 10 ++- + tests/unit/cloud/clouds/gce_test.py | 10 ++- + tests/unit/modules/boto_cloudtrail_test.py | 10 +-- + tests/unit/modules/boto_iot_test.py | 10 +-- + tests/unit/modules/boto_lambda_test.py | 10 +-- + tests/unit/modules/boto_s3_bucket_test.py | 10 +-- + tests/unit/modules/boto_secgroup_test.py | 1 + + tests/unit/modules/boto_vpc_test.py | 14 ++-- + tests/unit/modules/linux_sysctl_test.py | 19 +++-- + tests/unit/modules/mac_sysctl_test.py | 10 +-- + tests/unit/modules/mount_test.py | 14 ++-- + tests/unit/modules/portage_config.py | 10 ++- + tests/unit/modules/puppet_test.py | 15 ++-- + tests/unit/modules/useradd_test.py | 6 +- + tests/unit/pyobjects_test.py | 11 +++ + tests/unit/states/boto_cloudtrail_test.py | 10 +-- + tests/unit/states/boto_iot_test.py | 10 +-- + tests/unit/states/boto_lambda_test.py | 10 +-- + tests/unit/states/boto_s3_bucket_test.py | 10 +-- + tests/unit/states/dockerio.py | 112 ------------------------- + tests/unit/states/dockerio_test.py | 113 ++++++++++++++++++++++++++ + tests/unit/utils/network.py | 12 +-- + tests/unit/utils/utils_test.py | 11 +-- + 25 files changed, 244 insertions(+), 212 deletions(-) + delete mode 100644 tests/unit/states/dockerio.py + create mode 100644 tests/unit/states/dockerio_test.py + +diff --git a/salt/modules/boto_elb.py b/salt/modules/boto_elb.py +index 31df1fc..162abcd 100644 +--- a/salt/modules/boto_elb.py ++++ b/salt/modules/boto_elb.py +@@ -57,6 +57,7 @@ log = logging.getLogger(__name__) + # Import third party libs + try: + import boto ++ import boto.ec2 # pylint: enable=unused-import + # connection settings were added in 2.33.0 + required_boto_version = '2.33.0' + if (_LooseVersion(boto.__version__) < +@@ -64,7 +65,6 @@ try: + msg = 'boto_elb requires boto {0}.'.format(required_boto_version) + logging.debug(msg) + raise ImportError() +- import boto.ec2 + from boto.ec2.elb import HealthCheck + from boto.ec2.elb.attributes import AccessLogAttribute + from boto.ec2.elb.attributes import ConnectionDrainingAttribute +diff --git a/salt/modules/linux_sysctl.py b/salt/modules/linux_sysctl.py +index b016ca6..7702d52 100644 +--- a/salt/modules/linux_sysctl.py ++++ b/salt/modules/linux_sysctl.py +@@ -41,7 +41,11 @@ def _check_systemd_salt_config(): + sysctl_dir = os.path.split(conf)[0] + if not os.path.exists(sysctl_dir): + os.makedirs(sysctl_dir) +- salt.utils.fopen(conf, 'w').close() ++ try: ++ salt.utils.fopen(conf, 'w').close() ++ except (IOError, OSError): ++ msg = 'Could not create file: {0}' ++ raise CommandExecutionError(msg.format(conf)) + return conf + + +diff --git a/tests/unit/cloud/clouds/dimensiondata_test.py b/tests/unit/cloud/clouds/dimensiondata_test.py +index aa7f2c0..ee01d65 100644 +--- a/tests/unit/cloud/clouds/dimensiondata_test.py ++++ b/tests/unit/cloud/clouds/dimensiondata_test.py +@@ -8,7 +8,13 @@ + + # Import Python libs + from __future__ import absolute_import +-import libcloud.security ++ ++try: ++ import libcloud.security ++ HAS_LIBCLOUD = True ++except ImportError: ++ HAS_LIBCLOUD = False ++ + import platform + import os + +@@ -44,7 +50,7 @@ ON_SUSE = True if 'SuSE' in platform.dist() else False + ON_MAC = True if 'Darwin' in platform.system() else False + + if not os.path.exists('/etc/ssl/certs/YaST-CA.pem') and ON_SUSE: +- if os.path.isfile('/etc/ssl/ca-bundle.pem'): ++ if os.path.isfile('/etc/ssl/ca-bundle.pem') and HAS_LIBCLOUD: + libcloud.security.CA_CERTS_PATH.append('/etc/ssl/ca-bundle.pem') + else: + HAS_CERTS = False +diff --git a/tests/unit/cloud/clouds/gce_test.py b/tests/unit/cloud/clouds/gce_test.py +index 87824eb..c90f8ab 100644 +--- a/tests/unit/cloud/clouds/gce_test.py ++++ b/tests/unit/cloud/clouds/gce_test.py +@@ -8,7 +8,13 @@ + + # Import Python libs + from __future__ import absolute_import +-import libcloud.security ++ ++try: ++ import libcloud.security ++ HAS_LIBCLOUD = True ++except ImportError: ++ HAS_LIBCLOUD = False ++ + import platform + import os + +@@ -51,7 +57,7 @@ ON_SUSE = True if 'SuSE' in platform.dist() else False + ON_MAC = True if 'Darwin' in platform.system() else False + + if not os.path.exists('/etc/ssl/certs/YaST-CA.pem') and ON_SUSE: +- if os.path.isfile('/etc/ssl/ca-bundle.pem'): ++ if os.path.isfile('/etc/ssl/ca-bundle.pem') and HAS_LIBCLOUD: + libcloud.security.CA_CERTS_PATH.append('/etc/ssl/ca-bundle.pem') + else: + HAS_CERTS = False +diff --git a/tests/unit/modules/boto_cloudtrail_test.py b/tests/unit/modules/boto_cloudtrail_test.py +index 2f86101..264a795 100644 +--- a/tests/unit/modules/boto_cloudtrail_test.py ++++ b/tests/unit/modules/boto_cloudtrail_test.py +@@ -103,6 +103,11 @@ if _has_required_boto(): + StopLoggingTime=None) + + ++@skipIf(HAS_BOTO is False, 'The boto module must be installed.') ++@skipIf(_has_required_boto() is False, 'The boto3 module must be greater than' ++ ' or equal to version {0}' ++ .format(required_boto3_version)) ++@skipIf(NO_MOCK, NO_MOCK_REASON) + class BotoCloudTrailTestCaseBase(TestCase): + conn = None + +@@ -128,11 +133,6 @@ class BotoCloudTrailTestCaseMixin(object): + pass + + +-@skipIf(HAS_BOTO is False, 'The boto module must be installed.') +-@skipIf(_has_required_boto() is False, 'The boto3 module must be greater than' +- ' or equal to version {0}' +- .format(required_boto3_version)) +-@skipIf(NO_MOCK, NO_MOCK_REASON) + class BotoCloudTrailTestCase(BotoCloudTrailTestCaseBase, BotoCloudTrailTestCaseMixin): + ''' + TestCase for salt.modules.boto_cloudtrail module +diff --git a/tests/unit/modules/boto_iot_test.py b/tests/unit/modules/boto_iot_test.py +index 73c362f..520bfe9 100644 +--- a/tests/unit/modules/boto_iot_test.py ++++ b/tests/unit/modules/boto_iot_test.py +@@ -103,6 +103,11 @@ if _has_required_boto(): + ruleDisabled=True) + + ++@skipIf(HAS_BOTO is False, 'The boto module must be installed.') ++@skipIf(_has_required_boto() is False, 'The boto3 module must be greater than' ++ ' or equal to version {0}' ++ .format(required_boto3_version)) ++@skipIf(NO_MOCK, NO_MOCK_REASON) + class BotoIoTTestCaseBase(TestCase): + conn = None + +@@ -128,11 +133,6 @@ class BotoIoTTestCaseMixin(object): + pass + + +-@skipIf(HAS_BOTO is False, 'The boto module must be installed.') +-@skipIf(_has_required_boto() is False, 'The boto3 module must be greater than' +- ' or equal to version {0}' +- .format(required_boto3_version)) +-@skipIf(NO_MOCK, NO_MOCK_REASON) + class BotoIoTPolicyTestCase(BotoIoTTestCaseBase, BotoIoTTestCaseMixin): + ''' + TestCase for salt.modules.boto_iot module +diff --git a/tests/unit/modules/boto_lambda_test.py b/tests/unit/modules/boto_lambda_test.py +index 01ca245..ad7fb33 100644 +--- a/tests/unit/modules/boto_lambda_test.py ++++ b/tests/unit/modules/boto_lambda_test.py +@@ -109,6 +109,11 @@ def _has_required_boto(): + return True + + ++@skipIf(HAS_BOTO is False, 'The boto module must be installed.') ++@skipIf(_has_required_boto() is False, 'The boto3 module must be greater than' ++ ' or equal to version {0}' ++ .format(required_boto3_version)) ++@skipIf(NO_MOCK, NO_MOCK_REASON) + class BotoLambdaTestCaseBase(TestCase): + conn = None + +@@ -145,11 +150,6 @@ class BotoLambdaTestCaseMixin(object): + pass + + +-@skipIf(HAS_BOTO is False, 'The boto module must be installed.') +-@skipIf(_has_required_boto() is False, 'The boto3 module must be greater than' +- ' or equal to version {0}' +- .format(required_boto3_version)) +-@skipIf(NO_MOCK, NO_MOCK_REASON) + class BotoLambdaFunctionTestCase(BotoLambdaTestCaseBase, BotoLambdaTestCaseMixin): + ''' + TestCase for salt.modules.boto_lambda module +diff --git a/tests/unit/modules/boto_s3_bucket_test.py b/tests/unit/modules/boto_s3_bucket_test.py +index f4b1992..5e7d6be 100644 +--- a/tests/unit/modules/boto_s3_bucket_test.py ++++ b/tests/unit/modules/boto_s3_bucket_test.py +@@ -205,6 +205,11 @@ if _has_required_boto(): + } + + ++@skipIf(HAS_BOTO is False, 'The boto module must be installed.') ++@skipIf(_has_required_boto() is False, 'The boto3 module must be greater than' ++ ' or equal to version {0}' ++ .format(required_boto3_version)) ++@skipIf(NO_MOCK, NO_MOCK_REASON) + class BotoS3BucketTestCaseBase(TestCase): + conn = None + +@@ -230,11 +235,6 @@ class BotoS3BucketTestCaseMixin(object): + pass + + +-@skipIf(HAS_BOTO is False, 'The boto module must be installed.') +-@skipIf(_has_required_boto() is False, 'The boto3 module must be greater than' +- ' or equal to version {0}' +- .format(required_boto3_version)) +-@skipIf(NO_MOCK, NO_MOCK_REASON) + class BotoS3BucketTestCase(BotoS3BucketTestCaseBase, BotoS3BucketTestCaseMixin): + ''' + TestCase for salt.modules.boto_s3_bucket module +diff --git a/tests/unit/modules/boto_secgroup_test.py b/tests/unit/modules/boto_secgroup_test.py +index cc88568..7fd51ad 100644 +--- a/tests/unit/modules/boto_secgroup_test.py ++++ b/tests/unit/modules/boto_secgroup_test.py +@@ -23,6 +23,7 @@ import salt.loader + from salt.ext.six.moves import range # pylint: disable=redefined-builtin + try: + import boto ++ import boto.ec2 # pylint: enable=unused-import + HAS_BOTO = True + except ImportError: + HAS_BOTO = False +diff --git a/tests/unit/modules/boto_vpc_test.py b/tests/unit/modules/boto_vpc_test.py +index 64c7976..162bcae 100644 +--- a/tests/unit/modules/boto_vpc_test.py ++++ b/tests/unit/modules/boto_vpc_test.py +@@ -124,6 +124,13 @@ def _has_required_moto(): + context = {} + + ++@skipIf(NO_MOCK, NO_MOCK_REASON) ++@skipIf(HAS_BOTO is False, 'The boto module must be installed.') ++@skipIf(HAS_MOTO is False, 'The moto module must be installed.') ++@skipIf(_has_required_boto() is False, 'The boto module must be greater than' ++ ' or equal to version {0}' ++ .format(required_boto_version)) ++@skipIf(_has_required_moto() is False, 'The moto version must be >= to version {0}'.format(required_moto_version)) + class BotoVpcTestCaseBase(TestCase): + def setUp(self): + boto_vpc.__context__ = {} +@@ -249,13 +256,6 @@ class BotoVpcTestCaseMixin(object): + return rtbl + + +-@skipIf(NO_MOCK, NO_MOCK_REASON) +-@skipIf(HAS_BOTO is False, 'The boto module must be installed.') +-@skipIf(HAS_MOTO is False, 'The moto module must be installed.') +-@skipIf(_has_required_boto() is False, 'The boto module must be greater than' +- ' or equal to version {0}' +- .format(required_boto_version)) +-@skipIf(_has_required_moto() is False, 'The moto version must be >= to version {0}'.format(required_moto_version)) + class BotoVpcTestCase(BotoVpcTestCaseBase, BotoVpcTestCaseMixin): + ''' + TestCase for salt.modules.boto_vpc module +diff --git a/tests/unit/modules/linux_sysctl_test.py b/tests/unit/modules/linux_sysctl_test.py +index 89bea83..1eca7d5 100644 +--- a/tests/unit/modules/linux_sysctl_test.py ++++ b/tests/unit/modules/linux_sysctl_test.py +@@ -84,17 +84,22 @@ class LinuxSysctlTestCase(TestCase): + self.assertEqual(linux_sysctl.assign( + 'net.ipv4.ip_forward', 1), ret) + +- @patch('os.path.isfile', MagicMock(return_value=False)) + def test_persist_no_conf_failure(self): + ''' + Tests adding of config file failure + ''' +- with patch('salt.utils.fopen', mock_open()) as m_open: +- helper_open = m_open() +- helper_open.write.assertRaises(CommandExecutionError, +- linux_sysctl.persist, +- 'net.ipv4.ip_forward', +- 1, config=None) ++ asn_cmd = {'pid': 1337, 'retcode': 0, ++ 'stderr': "sysctl: permission denied", 'stdout': ''} ++ mock_asn_cmd = MagicMock(return_value=asn_cmd) ++ cmd = "sysctl -w net.ipv4.ip_forward=1" ++ mock_cmd = MagicMock(return_value=cmd) ++ with patch.dict(linux_sysctl.__salt__, {'cmd.run_stdout': mock_cmd, ++ 'cmd.run_all': mock_asn_cmd}): ++ with patch('salt.utils.fopen', mock_open()) as m_open: ++ self.assertRaises(CommandExecutionError, ++ linux_sysctl.persist, ++ 'net.ipv4.ip_forward', ++ 1, config=None) + + @patch('os.path.isfile', MagicMock(return_value=False)) + @patch('os.path.exists', MagicMock(return_value=True)) +diff --git a/tests/unit/modules/mac_sysctl_test.py b/tests/unit/modules/mac_sysctl_test.py +index e90ec64..533397b 100644 +--- a/tests/unit/modules/mac_sysctl_test.py ++++ b/tests/unit/modules/mac_sysctl_test.py +@@ -72,11 +72,11 @@ class DarwinSysctlTestCase(TestCase): + Tests adding of config file failure + ''' + with patch('salt.utils.fopen', mock_open()) as m_open: +- helper_open = m_open() +- helper_open.write.assertRaises(CommandExecutionError, +- mac_sysctl.persist, +- 'net.inet.icmp.icmplim', +- 50, config=None) ++ m_open.side_effect = IOError(13, 'Permission denied', '/file') ++ self.assertRaises(CommandExecutionError, ++ mac_sysctl.persist, ++ 'net.inet.icmp.icmplim', ++ 50, config=None) + + @patch('os.path.isfile', MagicMock(return_value=False)) + def test_persist_no_conf_success(self): +diff --git a/tests/unit/modules/mount_test.py b/tests/unit/modules/mount_test.py +index 290c368..b2cf904 100644 +--- a/tests/unit/modules/mount_test.py ++++ b/tests/unit/modules/mount_test.py +@@ -141,10 +141,10 @@ class MountTestCase(TestCase): + with patch.dict(mount.__grains__, {'kernel': ''}): + with patch.object(mount, 'fstab', mock_fstab): + with patch('salt.utils.fopen', mock_open()) as m_open: +- helper_open = m_open() +- helper_open.write.assertRaises(CommandExecutionError, +- mount.rm_fstab, +- config=None) ++ m_open.side_effect = IOError(13, 'Permission denied:', '/file') ++ self.assertRaises(CommandExecutionError, ++ mount.rm_fstab, ++ 'name', 'device') + + def test_set_fstab(self): + ''' +@@ -180,11 +180,7 @@ class MountTestCase(TestCase): + + mock = MagicMock(return_value={'name': 'name'}) + with patch.object(mount, 'fstab', mock): +- with patch('salt.utils.fopen', mock_open()) as m_open: +- helper_open = m_open() +- helper_open.write.assertRaises(CommandExecutionError, +- mount.rm_automaster, +- 'name', 'device') ++ self.assertTrue(mount.rm_automaster('name', 'device')) + + def test_set_automaster(self): + ''' +diff --git a/tests/unit/modules/portage_config.py b/tests/unit/modules/portage_config.py +index 8da1ebe..6275442 100644 +--- a/tests/unit/modules/portage_config.py ++++ b/tests/unit/modules/portage_config.py +@@ -11,7 +11,7 @@ from __future__ import absolute_import + # Import Salt Testing libs + from salttesting import skipIf, TestCase + from salttesting.helpers import ensure_in_syspath +-from salttesting.mock import NO_MOCK, NO_MOCK_REASON ++from salttesting.mock import NO_MOCK, NO_MOCK_REASON, MagicMock + ensure_in_syspath('../../') + + # Import salt libs +@@ -20,6 +20,10 @@ from salt.modules import portage_config + + @skipIf(NO_MOCK, NO_MOCK_REASON) + class PortageConfigTestCase(TestCase): ++ class DummyAtom(object): ++ def __init__(self, atom): ++ self.cp, self.repo = atom.split("::") if "::" in atom else (atom, None) ++ + def test_get_config_file_wildcards(self): + pairs = [ + ('*/*::repo', '/etc/portage/package.mask/repo'), +@@ -29,7 +33,11 @@ class PortageConfigTestCase(TestCase): + ('cat/pkg::repo', '/etc/portage/package.mask/cat/pkg'), + ] + ++ portage_config.portage = MagicMock() + for (atom, expected) in pairs: ++ dummy_atom = self.DummyAtom(atom) ++ portage_config.portage.dep.Atom = MagicMock(return_value=dummy_atom) ++ portage_config._p_to_cp = MagicMock(return_value=dummy_atom.cp) + self.assertEqual(portage_config._get_config_file('mask', atom), expected) + + if __name__ == '__main__': +diff --git a/tests/unit/modules/puppet_test.py b/tests/unit/modules/puppet_test.py +index 02bc2e1..2cdd696 100644 +--- a/tests/unit/modules/puppet_test.py ++++ b/tests/unit/modules/puppet_test.py +@@ -85,10 +85,12 @@ class PuppetTestCase(TestCase): + with patch('salt.utils.fopen', mock_open()): + self.assertTrue(puppet.disable()) + +- with patch('salt.utils.fopen', mock_open()) as m_open: +- helper_open = m_open() +- helper_open.write.assertRaises(CommandExecutionError, +- puppet.disable) ++ try: ++ with patch('salt.utils.fopen', mock_open()) as m_open: ++ m_open.side_effect = IOError(13, 'Permission denied:', '/file') ++ self.assertRaises(CommandExecutionError, puppet.disable) ++ except StopIteration: ++ pass + + def test_status(self): + ''' +@@ -145,9 +147,8 @@ class PuppetTestCase(TestCase): + self.assertDictEqual(puppet.summary(), {'resources': 1}) + + with patch('salt.utils.fopen', mock_open()) as m_open: +- helper_open = m_open() +- helper_open.write.assertRaises(CommandExecutionError, +- puppet.summary) ++ m_open.side_effect = IOError(13, 'Permission denied:', '/file') ++ self.assertRaises(CommandExecutionError, puppet.summary) + + def test_plugin_sync(self): + ''' +diff --git a/tests/unit/modules/useradd_test.py b/tests/unit/modules/useradd_test.py +index 7e646b6..cc9e610 100644 +--- a/tests/unit/modules/useradd_test.py ++++ b/tests/unit/modules/useradd_test.py +@@ -326,7 +326,7 @@ class UserAddTestCase(TestCase): + ''' + Test the user information + ''' +- self.assertEqual(useradd.info('salt'), {}) ++ self.assertEqual(useradd.info('username-that-doesnt-exist'), {}) + + mock = MagicMock(return_value=pwd.struct_passwd(('_TEST_GROUP', + '*', +@@ -336,9 +336,7 @@ class UserAddTestCase(TestCase): + '/var/virusmails', + '/usr/bin/false'))) + with patch.object(pwd, 'getpwnam', mock): +- mock = MagicMock(return_value='Group Name') +- with patch.object(useradd, 'list_groups', mock): +- self.assertEqual(useradd.info('salt')['name'], '_TEST_GROUP') ++ self.assertEqual(useradd.info('username-that-doesnt-exist')['name'], '_TEST_GROUP') + + # 'list_groups' function tests: 1 + +diff --git a/tests/unit/pyobjects_test.py b/tests/unit/pyobjects_test.py +index f1c3e29..3eb4bd5 100644 +--- a/tests/unit/pyobjects_test.py ++++ b/tests/unit/pyobjects_test.py +@@ -54,10 +54,18 @@ include('http') + + extend_template = '''#!pyobjects + include('http') ++ ++from salt.utils.pyobjects import StateFactory ++Service = StateFactory('service') ++ + Service.running(extend('apache'), watch=[{'file': '/etc/file'}]) + ''' + + map_template = '''#!pyobjects ++from salt.utils.pyobjects import StateFactory ++Service = StateFactory('service') ++ ++ + class Samba(Map): + __merge__ = 'samba:lookup' + +@@ -127,6 +135,9 @@ from salt://password.sls import password + ''' + + requisite_implicit_list_template = '''#!pyobjects ++from salt.utils.pyobjects import StateFactory ++Service = StateFactory('service') ++ + with Pkg.installed("pkg"): + Service.running("service", watch=File("file"), require=Cmd("cmd")) + ''' +diff --git a/tests/unit/states/boto_cloudtrail_test.py b/tests/unit/states/boto_cloudtrail_test.py +index 48fbd32..9e6dd95 100644 +--- a/tests/unit/states/boto_cloudtrail_test.py ++++ b/tests/unit/states/boto_cloudtrail_test.py +@@ -104,6 +104,11 @@ if _has_required_boto(): + StopLoggingTime=None) + + ++@skipIf(HAS_BOTO is False, 'The boto module must be installed.') ++@skipIf(_has_required_boto() is False, 'The boto3 module must be greater than' ++ ' or equal to version {0}' ++ .format(required_boto3_version)) ++@skipIf(NO_MOCK, NO_MOCK_REASON) + class BotoCloudTrailStateTestCaseBase(TestCase): + conn = None + +@@ -124,11 +129,6 @@ class BotoCloudTrailStateTestCaseBase(TestCase): + session_instance.client.return_value = self.conn + + +-@skipIf(HAS_BOTO is False, 'The boto module must be installed.') +-@skipIf(_has_required_boto() is False, 'The boto3 module must be greater than' +- ' or equal to version {0}' +- .format(required_boto3_version)) +-@skipIf(NO_MOCK, NO_MOCK_REASON) + class BotoCloudTrailTestCase(BotoCloudTrailStateTestCaseBase, BotoCloudTrailTestCaseMixin): + ''' + TestCase for salt.modules.boto_cloudtrail state.module +diff --git a/tests/unit/states/boto_iot_test.py b/tests/unit/states/boto_iot_test.py +index 8c2549d..81d68c8 100644 +--- a/tests/unit/states/boto_iot_test.py ++++ b/tests/unit/states/boto_iot_test.py +@@ -103,6 +103,11 @@ if _has_required_boto(): + principal = 'arn:aws:iot:us-east-1:1234:cert/21fc104aaaf6043f5756c1b57bda84ea8395904c43f28517799b19e4c42514' + + ++@skipIf(HAS_BOTO is False, 'The boto module must be installed.') ++@skipIf(_has_required_boto() is False, 'The boto3 module must be greater than' ++ ' or equal to version {0}' ++ .format(required_boto3_version)) ++@skipIf(NO_MOCK, NO_MOCK_REASON) + class BotoIoTStateTestCaseBase(TestCase): + conn = None + +@@ -123,11 +128,6 @@ class BotoIoTStateTestCaseBase(TestCase): + session_instance.client.return_value = self.conn + + +-@skipIf(HAS_BOTO is False, 'The boto module must be installed.') +-@skipIf(_has_required_boto() is False, 'The boto3 module must be greater than' +- ' or equal to version {0}' +- .format(required_boto3_version)) +-@skipIf(NO_MOCK, NO_MOCK_REASON) + class BotoIoTPolicyTestCase(BotoIoTStateTestCaseBase, BotoIoTTestCaseMixin): + ''' + TestCase for salt.modules.boto_iot state.module +diff --git a/tests/unit/states/boto_lambda_test.py b/tests/unit/states/boto_lambda_test.py +index 4557aed..7b02391 100644 +--- a/tests/unit/states/boto_lambda_test.py ++++ b/tests/unit/states/boto_lambda_test.py +@@ -101,6 +101,11 @@ def _has_required_boto(): + return True + + ++@skipIf(HAS_BOTO is False, 'The boto module must be installed.') ++@skipIf(_has_required_boto() is False, 'The boto3 module must be greater than' ++ ' or equal to version {0}' ++ .format(required_boto3_version)) ++@skipIf(NO_MOCK, NO_MOCK_REASON) + class BotoLambdaStateTestCaseBase(TestCase): + conn = None + +@@ -121,11 +126,6 @@ class BotoLambdaStateTestCaseBase(TestCase): + session_instance.client.return_value = self.conn + + +-@skipIf(HAS_BOTO is False, 'The boto module must be installed.') +-@skipIf(_has_required_boto() is False, 'The boto3 module must be greater than' +- ' or equal to version {0}' +- .format(required_boto3_version)) +-@skipIf(NO_MOCK, NO_MOCK_REASON) + class BotoLambdaFunctionTestCase(BotoLambdaStateTestCaseBase, BotoLambdaTestCaseMixin): + ''' + TestCase for salt.modules.boto_lambda state.module +diff --git a/tests/unit/states/boto_s3_bucket_test.py b/tests/unit/states/boto_s3_bucket_test.py +index 4049e9a..03c406f 100644 +--- a/tests/unit/states/boto_s3_bucket_test.py ++++ b/tests/unit/states/boto_s3_bucket_test.py +@@ -277,6 +277,11 @@ if _has_required_boto(): + } + + ++@skipIf(HAS_BOTO is False, 'The boto module must be installed.') ++@skipIf(_has_required_boto() is False, 'The boto3 module must be greater than' ++ ' or equal to version {0}' ++ .format(required_boto3_version)) ++@skipIf(NO_MOCK, NO_MOCK_REASON) + class BotoS3BucketStateTestCaseBase(TestCase): + conn = None + +@@ -297,11 +302,6 @@ class BotoS3BucketStateTestCaseBase(TestCase): + session_instance.client.return_value = self.conn + + +-@skipIf(HAS_BOTO is False, 'The boto module must be installed.') +-@skipIf(_has_required_boto() is False, 'The boto3 module must be greater than' +- ' or equal to version {0}' +- .format(required_boto3_version)) +-@skipIf(NO_MOCK, NO_MOCK_REASON) + class BotoS3BucketTestCase(BotoS3BucketStateTestCaseBase, BotoS3BucketTestCaseMixin): + ''' + TestCase for salt.modules.boto_s3_bucket state.module +diff --git a/tests/unit/states/dockerio.py b/tests/unit/states/dockerio.py +deleted file mode 100644 +index c73b633..0000000 +--- a/tests/unit/states/dockerio.py ++++ /dev/null +@@ -1,112 +0,0 @@ +-# -*- coding: utf-8 -*- +- +-# Import Python libs +-from __future__ import absolute_import +-from contextlib import contextmanager +- +-# Import Salt Testing libs +-from salttesting import skipIf, TestCase +-from salttesting.mock import NO_MOCK, NO_MOCK_REASON, MagicMock +- +- +-@contextmanager +-def provision_state(module, fixture): +- previous_dict = getattr(module, '__salt__', {}).copy() +- try: +- module.__dict__.setdefault('__salt__', {}).update(fixture) +- yield +- finally: +- setattr(module, '__salt__', previous_dict) +- +- +-@skipIf(NO_MOCK, NO_MOCK_REASON) +-class DockerStateTestCase(TestCase): +- def test_docker_run_success(self): +- from salt.states import dockerio +- salt_fixture = {'docker.retcode': MagicMock(return_value=0), +- 'docker.run_all': MagicMock( +- return_value={'stdout': '.\n..\n', +- 'stderr': '', +- 'status': True, +- 'comment': 'Success', +- 'retcode': 0})} +- +- with provision_state(dockerio, salt_fixture): +- result = dockerio.run('ls /', 'ubuntu') +- +- self.assertEqual(result, {'name': 'ls /', +- 'result': True, +- 'comment': 'Success', +- 'changes': {}}) +- +- def test_docker_run_failure(self): +- from salt.states import dockerio +- salt_fixture = {'docker.retcode': MagicMock(return_value=0), +- 'docker.run_all': MagicMock( +- return_value={'stdout': '', +- 'stderr': 'Error', +- 'status': False, +- 'comment': 'Failure', +- 'retcode': 1})} +- +- with provision_state(dockerio, salt_fixture): +- result = dockerio.run('ls /', 'ubuntu') +- +- self.assertEqual(result, {'name': 'ls /', +- 'result': False, +- 'comment': 'Failure', +- 'changes': {}}) +- +- def test_docker_run_onlyif(self): +- from salt.states import dockerio +- salt_fixture = {'docker.retcode': MagicMock(return_value=1), +- 'docker.run_all': None} +- with provision_state(dockerio, salt_fixture): +- result = dockerio.run('ls /', 'ubuntu', +- onlyif='ls -l') +- self.assertEqual(result, {'name': 'ls /', +- 'result': True, +- 'comment': 'onlyif execution failed', +- 'changes': {}}) +- +- def test_docker_run_unless(self): +- from salt.states import dockerio +- salt_fixture = {'docker.retcode': MagicMock(return_value=0), +- 'docker.run_all': None} +- with provision_state(dockerio, salt_fixture): +- result = dockerio.run('ls /', 'ubuntu', +- unless='ls -l') +- self.assertEqual(result, {'name': 'ls /', +- 'result': True, +- 'comment': 'unless execution succeeded', +- 'changes': {}}) +- +- def test_docker_run_docked_onlyif(self): +- from salt.states import dockerio +- salt_fixture = {'docker.retcode': MagicMock(return_value=1), +- 'docker.run_all': None} +- with provision_state(dockerio, salt_fixture): +- result = dockerio.run('ls /', 'ubuntu', +- docked_onlyif='ls -l') +- self.assertEqual(result, {'name': 'ls /', +- 'result': True, +- 'comment': 'docked_onlyif execution failed', +- 'changes': {}}) +- +- def test_docker_run_docked_unless(self): +- from salt.states import dockerio +- salt_fixture = {'docker.retcode': MagicMock(return_value=0), +- 'docker.run_all': None} +- with provision_state(dockerio, salt_fixture): +- result = dockerio.run('ls /', 'ubuntu', +- docked_unless='ls -l') +- self.assertEqual(result, {'name': 'ls /', +- 'result': True, +- 'comment': ('docked_unless execution' +- ' succeeded'), +- 'changes': {}}) +- +- +-if __name__ == '__main__': +- from integration import run_tests +- run_tests(DockerStateTestCase, needs_daemon=False) +diff --git a/tests/unit/states/dockerio_test.py b/tests/unit/states/dockerio_test.py +new file mode 100644 +index 0000000..54f51be +--- /dev/null ++++ b/tests/unit/states/dockerio_test.py +@@ -0,0 +1,113 @@ ++# -*- coding: utf-8 -*- ++ ++# Import Python libs ++from __future__ import absolute_import ++from contextlib import contextmanager ++ ++# Import Salt Testing libs ++from salttesting import skipIf, TestCase ++from salttesting.mock import NO_MOCK, NO_MOCK_REASON, MagicMock ++ ++ ++@contextmanager ++def provision_state(module, fixture): ++ previous_dict = getattr(module, '__salt__', {}).copy() ++ try: ++ module.__dict__.setdefault('__salt__', {}).update(fixture) ++ yield ++ finally: ++ setattr(module, '__salt__', previous_dict) ++ ++ ++@skipIf(NO_MOCK, NO_MOCK_REASON) ++@skipIf(True, 'Skipped: This module has been deprecated.') ++class DockerStateTestCase(TestCase): ++ def test_docker_run_success(self): ++ from salt.states import dockerio ++ salt_fixture = {'docker.retcode': MagicMock(return_value=0), ++ 'docker.run_all': MagicMock( ++ return_value={'stdout': '.\n..\n', ++ 'stderr': '', ++ 'status': True, ++ 'comment': 'Success', ++ 'retcode': 0})} ++ ++ with provision_state(dockerio, salt_fixture): ++ result = dockerio.run('ls /', 'ubuntu') ++ ++ self.assertEqual(result, {'name': 'ls /', ++ 'result': True, ++ 'comment': 'Success', ++ 'changes': {}}) ++ ++ def test_docker_run_failure(self): ++ from salt.states import dockerio ++ salt_fixture = {'docker.retcode': MagicMock(return_value=0), ++ 'docker.run_all': MagicMock( ++ return_value={'stdout': '', ++ 'stderr': 'Error', ++ 'status': False, ++ 'comment': 'Failure', ++ 'retcode': 1})} ++ ++ with provision_state(dockerio, salt_fixture): ++ result = dockerio.run('ls /', 'ubuntu') ++ ++ self.assertEqual(result, {'name': 'ls /', ++ 'result': False, ++ 'comment': 'Failure', ++ 'changes': {}}) ++ ++ def test_docker_run_onlyif(self): ++ from salt.states import dockerio ++ salt_fixture = {'docker.retcode': MagicMock(return_value=1), ++ 'docker.run_all': None} ++ with provision_state(dockerio, salt_fixture): ++ result = dockerio.run('ls /', 'ubuntu', ++ onlyif='ls -l') ++ self.assertEqual(result, {'name': 'ls /', ++ 'result': True, ++ 'comment': 'onlyif execution failed', ++ 'changes': {}}) ++ ++ def test_docker_run_unless(self): ++ from salt.states import dockerio ++ salt_fixture = {'docker.retcode': MagicMock(return_value=0), ++ 'docker.run_all': None} ++ with provision_state(dockerio, salt_fixture): ++ result = dockerio.run('ls /', 'ubuntu', ++ unless='ls -l') ++ self.assertEqual(result, {'name': 'ls /', ++ 'result': True, ++ 'comment': 'unless execution succeeded', ++ 'changes': {}}) ++ ++ def test_docker_run_docked_onlyif(self): ++ from salt.states import dockerio ++ salt_fixture = {'docker.retcode': MagicMock(return_value=1), ++ 'docker.run_all': None} ++ with provision_state(dockerio, salt_fixture): ++ result = dockerio.run('ls /', 'ubuntu', ++ docked_onlyif='ls -l') ++ self.assertEqual(result, {'name': 'ls /', ++ 'result': True, ++ 'comment': 'docked_onlyif execution failed', ++ 'changes': {}}) ++ ++ def test_docker_run_docked_unless(self): ++ from salt.states import dockerio ++ salt_fixture = {'docker.retcode': MagicMock(return_value=0), ++ 'docker.run_all': None} ++ with provision_state(dockerio, salt_fixture): ++ result = dockerio.run('ls /', 'ubuntu', ++ docked_unless='ls -l') ++ self.assertEqual(result, {'name': 'ls /', ++ 'result': True, ++ 'comment': ('docked_unless execution' ++ ' succeeded'), ++ 'changes': {}}) ++ ++ ++if __name__ == '__main__': ++ from integration import run_tests ++ run_tests(DockerStateTestCase, needs_daemon=False) +diff --git a/tests/unit/utils/network.py b/tests/unit/utils/network.py +index 89db848..72ca857 100644 +--- a/tests/unit/utils/network.py ++++ b/tests/unit/utils/network.py +@@ -151,15 +151,16 @@ class NetworkTestCase(TestCase): + self.assertEqual(interfaces, + {'ilbext0': {'inet': [{'address': '10.10.11.11', + 'broadcast': '10.10.11.31', ++ 'netmask': '255.255.255.224'}, ++ {'address': '10.10.11.12', ++ 'broadcast': '10.10.11.31', + 'netmask': '255.255.255.224'}], +- 'inet6': [{'address': '::', +- 'prefixlen': '0'}], ++ 'inet6': [], + 'up': True}, + 'ilbint0': {'inet': [{'address': '10.6.0.11', + 'broadcast': '10.6.0.255', + 'netmask': '255.255.255.0'}], +- 'inet6': [{'address': '::', +- 'prefixlen': '0'}], ++ 'inet6': [], + 'up': True}, + 'lo0': {'inet': [{'address': '127.0.0.1', + 'netmask': '255.0.0.0'}], +@@ -174,8 +175,7 @@ class NetworkTestCase(TestCase): + 'up': True}, + 'vpn0': {'inet': [{'address': '10.6.0.14', + 'netmask': '255.0.0.0'}], +- 'inet6': [{'address': '::', +- 'prefixlen': '0'}], ++ 'inet6': [], + 'up': True}} + ) + +diff --git a/tests/unit/utils/utils_test.py b/tests/unit/utils/utils_test.py +index 261af69..11f0baf 100644 +--- a/tests/unit/utils/utils_test.py ++++ b/tests/unit/utils/utils_test.py +@@ -527,14 +527,9 @@ class UtilsTestCase(TestCase): + ret = utils.date_cast('Mon Dec 23 10:19:15 MST 2013') + expected_ret = datetime.datetime(2013, 12, 23, 10, 19, 15) + self.assertEqual(ret, expected_ret) +- except ImportError: +- try: +- ret = utils.date_cast('Mon Dec 23 10:19:15 MST 2013') +- expected_ret = datetime.datetime(2013, 12, 23, 10, 19, 15) +- self.assertEqual(ret, expected_ret) +- except RuntimeError: +- # Unparseable without timelib installed +- self.skipTest('\'timelib\' is not installed') ++ except RuntimeError: ++ # Unparseable without timelib installed ++ self.skipTest('\'timelib\' is not installed') + + @skipIf(not HAS_TIMELIB, '\'timelib\' is not installed') + def test_date_format(self): +-- +2.8.2 + diff --git a/0019-Fix-snapper_test-for-python26.patch b/0019-Fix-snapper_test-for-python26.patch new file mode 100644 index 0000000..91c773f --- /dev/null +++ b/0019-Fix-snapper_test-for-python26.patch @@ -0,0 +1,113 @@ +From df1f88c51a40e69935830d1664a46dadf514dc69 Mon Sep 17 00:00:00 2001 +From: Justin Anderson +Date: Tue, 23 Aug 2016 15:02:31 -0600 +Subject: [PATCH 19/19] Fix snapper_test for python26 + +* Use assertCountEqual instead of assertItemsEqual for Python 3 +* Skip one Snapper test on 2.6 + There's a slight difference in the diff comparison but we should be + able to catch true failures here with 2.7. +--- + tests/unit/modules/snapper_test.py | 41 ++++++++++++++++++++++++++++++-------- + 1 file changed, 33 insertions(+), 8 deletions(-) + +diff --git a/tests/unit/modules/snapper_test.py b/tests/unit/modules/snapper_test.py +index f27b2ba..43f8898 100644 +--- a/tests/unit/modules/snapper_test.py ++++ b/tests/unit/modules/snapper_test.py +@@ -6,19 +6,26 @@ Unit tests for the Snapper module + :codeauthor: Pablo Suárez Hernández + ''' + ++# Import Python libs + from __future__ import absolute_import ++import sys + +-from salttesting import TestCase ++# Import Salt Testing libs ++from salttesting import TestCase, skipIf + from salttesting.mock import ( ++ NO_MOCK, ++ NO_MOCK_REASON, + MagicMock, + patch, + mock_open, + ) +- +-from salt.exceptions import CommandExecutionError + from salttesting.helpers import ensure_in_syspath ++ + ensure_in_syspath('../../') + ++# Import Salt libs ++import salt.ext.six as six ++from salt.exceptions import CommandExecutionError + from salt.modules import snapper + + # Globals +@@ -123,6 +130,13 @@ MODULE_RET = { + "@@ -0,0 +1 @@\n" + "+another foobar", + }, ++ '/tmp/foo26': { ++ 'comment': 'text file created', ++ 'diff': "--- /.snapshots/55/snapshot/tmp/foo2 \n" ++ "+++ /tmp/foo2 \n" ++ "@@ -1,0 +1,1 @@\n" ++ "+another foobar", ++ }, + '/tmp/foo3': { + 'comment': 'binary file changed', + 'old_sha256_digest': 'e61f8b762d83f3b4aeb3689564b0ffbe54fa731a69a1e208dc9440ce0f69d19b', +@@ -132,6 +146,7 @@ MODULE_RET = { + } + + ++@skipIf(NO_MOCK, NO_MOCK_REASON) + class SnapperTestCase(TestCase): + def setUp(self): + self.dbus_mock = MagicMock() +@@ -220,10 +235,16 @@ class SnapperTestCase(TestCase): + @patch('salt.modules.snapper.snapper.GetComparison', MagicMock()) + @patch('salt.modules.snapper.snapper.GetFiles', MagicMock(return_value=DBUS_RET['GetFiles'])) + def test_status(self): +- self.assertItemsEqual(snapper.status(), MODULE_RET['GETFILES']) +- self.assertItemsEqual(snapper.status(num_pre="42", num_post=43), MODULE_RET['GETFILES']) +- self.assertItemsEqual(snapper.status(num_pre=42), MODULE_RET['GETFILES']) +- self.assertItemsEqual(snapper.status(num_post=43), MODULE_RET['GETFILES']) ++ if six.PY3: ++ self.assertCountEqual(snapper.status(), MODULE_RET['GETFILES']) ++ self.assertCountEqual(snapper.status(num_pre="42", num_post=43), MODULE_RET['GETFILES']) ++ self.assertCountEqual(snapper.status(num_pre=42), MODULE_RET['GETFILES']) ++ self.assertCountEqual(snapper.status(num_post=43), MODULE_RET['GETFILES']) ++ else: ++ self.assertItemsEqual(snapper.status(), MODULE_RET['GETFILES']) ++ self.assertItemsEqual(snapper.status(num_pre="42", num_post=43), MODULE_RET['GETFILES']) ++ self.assertItemsEqual(snapper.status(num_pre=42), MODULE_RET['GETFILES']) ++ self.assertItemsEqual(snapper.status(num_post=43), MODULE_RET['GETFILES']) + + @patch('salt.modules.snapper.status', MagicMock(return_value=MODULE_RET['GETFILES'])) + def test_changed_files(self): +@@ -268,7 +289,10 @@ class SnapperTestCase(TestCase): + @patch('os.path.isfile', MagicMock(side_effect=[False, True])) + @patch('salt.utils.fopen', mock_open(read_data=FILE_CONTENT["/tmp/foo2"]['post'])) + def test_diff_text_file(self): +- self.assertEqual(snapper.diff(), {"/tmp/foo2": MODULE_RET['DIFF']['/tmp/foo2']}) ++ if sys.version_info < (2, 7): ++ self.assertEqual(snapper.diff(), {"/tmp/foo2": MODULE_RET['DIFF']['/tmp/foo26']}) ++ else: ++ self.assertEqual(snapper.diff(), {"/tmp/foo2": MODULE_RET['DIFF']['/tmp/foo2']}) + + @patch('salt.modules.snapper._get_num_interval', MagicMock(return_value=(55, 0))) + @patch('salt.modules.snapper.snapper.MountSnapshot', MagicMock( +@@ -278,6 +302,7 @@ class SnapperTestCase(TestCase): + @patch('salt.modules.snapper._is_text_file', MagicMock(return_value=True)) + @patch('os.path.isfile', MagicMock(side_effect=[True, True, False, True])) + @patch('os.path.isdir', MagicMock(return_value=False)) ++ @skipIf(sys.version_info < (2, 7), 'Python 2.7 required to compare diff properly') + def test_diff_text_files(self): + fopen_effect = [ + mock_open(read_data=FILE_CONTENT["/tmp/foo"]['pre']).return_value, +-- +2.8.2 + diff --git a/0020-Integration-tests-fixes-for-2016.3.2.patch b/0020-Integration-tests-fixes-for-2016.3.2.patch new file mode 100644 index 0000000..ca9e0d2 --- /dev/null +++ b/0020-Integration-tests-fixes-for-2016.3.2.patch @@ -0,0 +1,107 @@ +From 65dba3e9ab088087943f67367dc27fce00e4cd92 Mon Sep 17 00:00:00 2001 +From: =?UTF-8?q?Pablo=20Su=C3=A1rez=20Hern=C3=A1ndez?= + +Date: Tue, 13 Sep 2016 11:05:35 +0100 +Subject: [PATCH 20/20] Integration tests fixes for 2016.3.2 + +* Fix PillarModuleTest::test_pillar_items: 'info' does not exist in pillar +* Fixing integration tests if azure is not present +* Fixing integration tests failures if 'git' command is missing + Skip git state integration tests if 'git' does not exists + Prevent OSError if 'git' command not found during _git_version() +--- + tests/integration/cloud/providers/msazure.py | 17 +++++++++-------- + tests/integration/modules/git.py | 15 +++++++++------ + tests/integration/modules/pillar.py | 1 - + tests/integration/states/git.py | 2 +- + 4 files changed, 19 insertions(+), 16 deletions(-) + +diff --git a/tests/integration/cloud/providers/msazure.py b/tests/integration/cloud/providers/msazure.py +index c4934e4..4e53add 100644 +--- a/tests/integration/cloud/providers/msazure.py ++++ b/tests/integration/cloud/providers/msazure.py +@@ -53,14 +53,15 @@ def __has_required_azure(): + ''' + Returns True/False if the required version of the Azure SDK is installed. + ''' +- if hasattr(azure, '__version__'): +- version = LooseVersion(azure.__version__) +- else: +- version = LooseVersion(azure.common.__version__) +- if HAS_AZURE is True and REQUIRED_AZURE <= version: +- return True +- else: +- return False ++ if HAS_AZURE: ++ if hasattr(azure, '__version__'): ++ version = LooseVersion(azure.__version__) ++ else: ++ version = LooseVersion(azure.common.__version__) ++ ++ if REQUIRED_AZURE <= version: ++ return True ++ return False + + + @skipIf(HAS_AZURE is False, 'These tests require the Azure Python SDK to be installed.') +diff --git a/tests/integration/modules/git.py b/tests/integration/modules/git.py +index 7c40664..f5c15f3 100644 +--- a/tests/integration/modules/git.py ++++ b/tests/integration/modules/git.py +@@ -36,12 +36,15 @@ log = logging.getLogger(__name__) + + + def _git_version(): +- git_version = subprocess.Popen( +- ['git', '--version'], +- shell=False, +- close_fds=True, +- stdout=subprocess.PIPE, +- stderr=subprocess.PIPE).communicate()[0] ++ try: ++ git_version = subprocess.Popen( ++ ['git', '--version'], ++ shell=False, ++ close_fds=True, ++ stdout=subprocess.PIPE, ++ stderr=subprocess.PIPE).communicate()[0] ++ except OSError: ++ return False + if not git_version: + log.debug('Git not installed') + return False +diff --git a/tests/integration/modules/pillar.py b/tests/integration/modules/pillar.py +index b081f76..3d55b4c 100644 +--- a/tests/integration/modules/pillar.py ++++ b/tests/integration/modules/pillar.py +@@ -119,7 +119,6 @@ class PillarModuleTest(integration.ModuleCase): + from pillar.items + ''' + get_items = self.run_function('pillar.items') +- self.assertDictContainsSubset({'info': 'bar'}, get_items) + self.assertDictContainsSubset({'monty': 'python'}, get_items) + self.assertDictContainsSubset( + {'knights': ['Lancelot', 'Galahad', 'Bedevere', 'Robin']}, +diff --git a/tests/integration/states/git.py b/tests/integration/states/git.py +index a78b271..87681ba 100644 +--- a/tests/integration/states/git.py ++++ b/tests/integration/states/git.py +@@ -20,6 +20,7 @@ import integration + import salt.utils + + ++@skip_if_binaries_missing('git') + class GitTest(integration.ModuleCase, integration.SaltReturnAssertsMixIn): + ''' + Validate the git state +@@ -254,7 +255,6 @@ class GitTest(integration.ModuleCase, integration.SaltReturnAssertsMixIn): + finally: + shutil.rmtree(name, ignore_errors=True) + +- @skip_if_binaries_missing('git') + def test_config_set_value_with_space_character(self): + ''' + git.config +-- +2.8.2 + diff --git a/html.tar.bz2 b/html.tar.bz2 new file mode 100644 index 0000000..d7f2b7d --- /dev/null +++ b/html.tar.bz2 @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:0899b89ef230b42097f7c147a9babb30d65eac7968bd05318eac42ec9d8a7ec9 +size 5486695 diff --git a/salt-2015.8.10.tar.gz b/salt-2015.8.10.tar.gz deleted file mode 100644 index 598236b..0000000 --- a/salt-2015.8.10.tar.gz +++ /dev/null @@ -1,3 +0,0 @@ -version https://git-lfs.github.com/spec/v1 -oid sha256:4a81273ab4b01e0f1d29b28ab99a16eb94e5c430107b05491c94f3baf8b95c99 -size 6972776 diff --git a/salt-2016.3.2.tar.gz b/salt-2016.3.2.tar.gz new file mode 100644 index 0000000..06d00a6 --- /dev/null +++ b/salt-2016.3.2.tar.gz @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:6a84b724d02b0dba438dea57650724064675d82620e66749bd2fe8f21da71be0 +size 8014793 diff --git a/salt.changes b/salt.changes index 33eadb4..2211cdc 100644 --- a/salt.changes +++ b/salt.changes @@ -1,3 +1,178 @@ +------------------------------------------------------------------- +Tue Sep 27 23:55:03 UTC 2016 - mrueckert@suse.de + +- splitting out the susemanager integration plugins into their own + subpackages. ATM this only contains the zypp plugin to tell + susemanager about manually installed packages. + +------------------------------------------------------------------- +Tue Sep 13 15:07:44 UTC 2016 - pablo.suarezhernandez@suse.com + +- Unit and integration tests fixes for 2016.3.2 + Add: + * 0018-Unit-tests-fixes-for-2016.3.2.patch + * 0019-Fix-snapper_test-for-python26.patch + * 0020-Integration-tests-fixes-for-2016.3.2.patch + +------------------------------------------------------------------- +Fri Sep 2 09:50:57 UTC 2016 - pablo.suarezhernandez@suse.com + +- Prevent pkg.install failure for expired keys (bsc#996455) + Add: + * 0017-Check-for-single-quote-before-splitting-on-single-qu.patch + +------------------------------------------------------------------- +Tue Aug 30 12:24:45 UTC 2016 - bmaryniuk@suse.com + +- Required D-Bus and generating machine ID where it is missing + +------------------------------------------------------------------- +Fri Aug 26 13:11:58 UTC 2016 - bmaryniuk@suse.com + +- Fix sphinx crashes when documentation is being generated + Add script for documentation update. + + Add: + * 0016-Improve-Mock-to-be-flexible-and-able-to-mock-methods.patch + * update-documentation.sh + +------------------------------------------------------------------- +Tue Aug 16 12:01:59 UTC 2016 - bmaryniuk@suse.com + +- Fix pkg.installed refresh repo failure (bsc#993549) + Fix salt.states.pkgrepo.management no change failure (bsc#990440) + Add: + * 0014-Add-ignore_repo_failure-option-to-suppress-zypper-s-.patch + * 0015-Remove-zypper-s-raise-exception-if-mod_repo-has-no-a.patch + +------------------------------------------------------------------- +Tue Aug 9 13:40:37 UTC 2016 - bmaryniuk@suse.com + +- Deprecate status.uptime one version later + Add: + * 0013-Deprecate-status.uptime-one-version-later.patch + +------------------------------------------------------------------- +Mon Aug 8 16:25:04 UTC 2016 - pablo.suarezhernandez@suse.com + +- Fix for 0008-checksum-validation-when-zypper-pkg.download.patch + after upstream merging conflict. Prevent zypper test failure. + Update: + * 0008-checksum-validation-when-zypper-pkg.download.patch + +------------------------------------------------------------------- +Thu Aug 4 14:56:42 UTC 2016 - mihai.dinca@suse.com + +- Update to v2016.3.2 + see https://docs.saltstack.com/en/latest/topics/releases/2016.3.2.html +- Removed Patches, applied upstream + * 0010-Getting-the-os-grain-from-CPE_NAME-inside-etc-os-rel.patch + * 0011-snapper-execution-module.patch + * 0012-Force-minion-exit-on-mis-configuration-read.patch + * 0013-fix-salt-summary-to-count-not-responding-minions-cor.patch + * 0014-Move-log-message-from-INFO-to-DEBUG.patch + * 0016-Run-salt-api-as-user-salt-bsc-990029.patch + * 0017-fix-beacon-list-to-include-all-beacons-being-process.patch +- Added Patches back + * 0010-snapper-execution-module.patch + * 0011-fix-salt-summary-to-count-not-responding-minions-cor.patch + * 0012-Run-salt-api-as-user-salt-bsc-990029.patch + +------------------------------------------------------------------- +Wed Aug 3 14:50:35 UTC 2016 - pablo.suarezhernandez@suse.com + +- Fix beacon list to include all beacons being process + Add: + * 0017-fix-beacon-list-to-include-all-beacons-being-process.patch + +------------------------------------------------------------------- +Fri Jul 29 11:48:37 UTC 2016 - kkaempf@suse.com + +- Redo patches, sync with github.com/openSUSE/salt + +- Renamed patches + * 0016-Getting-the-os-grain-from-CPE_NAME-inside-etc-os-rel.patch + -> 0010-Getting-the-os-grain-from-CPE_NAME-inside-etc-os-rel.patch + * 0017-snapper-execution-module.patch + -> 0011-snapper-execution-module.patch + * 0018-Force-minion-exit-on-mis-configuration-read.patch + -> 0012-Force-minion-exit-on-mis-configuration-read.patch + +- Added back (fix upstream merge problems): + * 0008-checksum-validation-when-zypper-pkg.download.patch + * 0009-unit-tests-for-rpm.checksum-and-zypper.download.patch + +- fix salt --summary to count not responding minions correctly + (bsc#972311) + * 0013-fix-salt-summary-to-count-not-responding-minions-cor.patch + +- Move log message from INFO to DEBUG (bsc#985661) + * 0014-Move-log-message-from-INFO-to-DEBUG.patch + +- pkg.list_products on "registerrelease" and "productline" + returns boolean.False if empty (bsc#989193) + * 0015-Bugfix-return-boolean-only-for-isbase-and-installed-.patch + +- Run salt-api as user salt like the master (bsc#990029) + * 0016-Run-salt-api-as-user-salt-bsc-990029.patch + +------------------------------------------------------------------- +Fri Jul 22 15:16:16 UTC 2016 - darin@darins.net + +- Update to v2016.3.1 + see https://docs.saltstack.com/en/latest/topics/releases/2016.3.1.html +- Removed Patches, applied upstream + * 0008-Prevent-several-minion-processes-on-the-same-machine.patch + * 0009-checksum-validation-when-zypper-pkg.download.patch + * 0010-unit-tests-for-rpm.checksum-and-zypper.download.patch + * 0011-jobs.exit_success-allow-to-check-if-a-job-has-execut.patch + * 0012-Fix-pkgrepo.managed-gpgkey-argument-bsc-979448.patch + * 0013-fix-groupadd-module-for-sles11-systems.patch + * 0014-Fix-crashing-Maintenence-process.patch + * 0015-Insert-no-refresh-before-install-in-Zypper.patch + +------------------------------------------------------------------- +Tue Jul 19 15:03:10 UTC 2016 - bmaryniuk@suse.com + +- Bugfix: Exit on configuration read (bsc#985667) + Add: + * 0018-Force-minion-exit-on-mis-configuration-read.patch + +------------------------------------------------------------------- +Mon Jul 18 08:32:04 UTC 2016 - pablo.suarezhernandez@suse.com + +- Backport: Snapper module for Salt. + Add: + * 0017-snapper-execution-module.patch + +------------------------------------------------------------------- +Wed Jun 22 07:56:44 UTC 2016 - pablo.suarezhernandez@suse.com + +- Bugfix: Fixed behavior for SUSE OS grains (bsc#970669) + Bugfix: Salt os_family does not detect SLES for SAP (bsc#983017) + Add: + * 0016-Getting-the-os-grain-from-CPE_NAME-inside-etc-os-rel.patch + +------------------------------------------------------------------- +Mon Jun 20 11:26:29 UTC 2016 - thipp@suse.com + +- Insert --no-refresh before install in Zypper + Add: + * 0015-Insert-no-refresh-before-install-in-Zypper.patch + +------------------------------------------------------------------- +Wed Jun 15 11:38:26 UTC 2016 - kkaempf@suse.com + +- Update to v2016.3.0 + see https://docs.saltstack.com/en/latest/topics/releases/2016.3.0.html + + * backwards-incompatible changes: + - The default path for the extension_modules master config option + has been changed. + +- add 0014-Fix-crashing-Maintenence-process.patch + see release notes + ------------------------------------------------------------------- Wed Jun 1 09:52:40 UTC 2016 - mihai.dinca@suse.com diff --git a/salt.spec b/salt.spec index 3b75630..b513450 100644 --- a/salt.spec +++ b/salt.spec @@ -34,45 +34,80 @@ %bcond_with test %bcond_with raet %bcond_without docs +%bcond_with builddocs Name: salt -Version: 2015.8.10 +Version: 2016.3.2 Release: 0 Summary: A parallel remote execution system License: Apache-2.0 Group: System/Monitoring Url: http://saltstack.org/ # Git: https://github.com/openSUSE/salt.git -Source0: https://pypi.io/packages/source/s/%{name}/%{name}-%{version}.tar.gz +Source0: https://github.com/saltstack/salt/releases/download/v%{version}/%{name}-%{version}.tar.gz Source1: README.SUSE Source2: salt-tmpfiles.d +Source3: html.tar.bz2 # PATCH-FIX-OPENSUSE use-forking-daemon.patch tserong@suse.com -- We don't have python-systemd, so notify can't work +# We do not upstream this patch because this is something that we have to fix on our side Patch1: 0001-tserong-suse.com-We-don-t-have-python-systemd-so-not.patch # PATCH-FIX-OPENSUSE use-salt-user-for-master.patch -- Run salt master as dedicated salt user +# We do not upstream this patch because this is suse custom configuration Patch2: 0002-Run-salt-master-as-dedicated-salt-user.patch # PATCH-FIX-OPENSUSE https://github.com/saltstack/salt/pull/30424 +# We do not upstream this patch because it has been fixed upstream +# (see: https://trello.com/c/wh96lCD4/1528-get-rid-of-0003-check-if-byte-strings-are-properly-encoded-in-utf-8-patch-in-the-salt-package) Patch3: 0003-Check-if-byte-strings-are-properly-encoded-in-UTF-8.patch # PATCH-FIX-OPENSUSE prevent rebuilds in OBS +# We do not upstream this patch because the issue is on our side Patch4: 0004-do-not-generate-a-date-in-a-comment-to-prevent-rebui.patch # PATCH-FIX-OPENSUSE - Upstream default hash type is set to MD5, while we require SHA256 (bsc#955373) +# PR https://github.com/saltstack/salt/pull/35341 (15.08.2016 - not merged yet) Patch5: 0005-Use-SHA256-hash-type-by-default.patch # PATCH-FIX_UPSTREAM https://github.com/saltstack/salt/pull/31798 +# PR already merged. This will be gone in the next version Patch6: 0006-Create-salt-proxy-instantiated-service-file.patch # PATCH-FIX-OPENSUSE Generate events from the Salt minion, # if Zypper has been used outside the Salt infrastructure +# We do not upstream this because this is for SUSE only (15.08.2016) Patch7: 0007-Add-SUSE-Manager-plugin.patch -# PATCH-FIX_UPSTREAM https://github.com/saltstack/salt/pull/33464 -Patch8: 0008-Prevent-several-minion-processes-on-the-same-machine.patch # PATCH-FIX_UPSTREAM https://github.com/saltstack/salt/pull/33469 -Patch9: 0009-checksum-validation-when-zypper-pkg.download.patch +# PR already merged. This will be gone in the next version +Patch8: 0008-checksum-validation-when-zypper-pkg.download.patch # PATCH-FIX_UPSTREAM https://github.com/saltstack/salt/pull/33501 -Patch10: 0010-unit-tests-for-rpm.checksum-and-zypper.download.patch -# PATCH-FIX_UPSTREAM https://github.com/saltstack/salt/pull/33465 -Patch11: 0011-jobs.exit_success-allow-to-check-if-a-job-has-execut.patch -# PATCH-FIX_UPSTREAM https://github.com/saltstack/salt/pull/33432 -# https://github.com/saltstack/salt/pull/33581 -Patch12: 0012-Fix-pkgrepo.managed-gpgkey-argument-bsc-979448.patch +# PR already merged. This will be gone in the next version +Patch9: 0009-unit-tests-for-rpm.checksum-and-zypper.download.patch +# PATCH-FIX-UPSTREAM https://github.com/saltstack/salt/pull/34599 (backport from dev) +# PR already merged. This will be gone in the next version +Patch10: 0010-snapper-execution-module.patch +# PATCH-FIX_UPSTREAM https://github.com/saltstack/salt/pull/34165 +Patch11: 0011-fix-salt-summary-to-count-not-responding-minions-cor.patch +# PATCH-FIX-OPENSUSE +# We do not upstream this patch because this is suse custom configuration +Patch12: 0012-Run-salt-api-as-user-salt-bsc-990029.patch +# PATCH-FIX_UPSTREAM https://github.com/saltstack/salt/pull/35196 +# PR already merged. This will be gone in the next version +Patch13: 0013-Deprecate-status.uptime-one-version-later.patch +# PATCH-FIX-UPSTREAM https://github.com/saltstack/salt/pull/35448 +Patch14: 0014-Add-ignore_repo_failure-option-to-suppress-zypper-s-.patch +# PATCH-FIX-UPSTREAM https://github.com/saltstack/salt/pull/35451 +Patch15: 0015-Remove-zypper-s-raise-exception-if-mod_repo-has-no-a.patch +# PATCH-FIX-UPSTREAM https://github.com/saltstack/salt/pull/35763 +Patch16: 0016-Improve-Mock-to-be-flexible-and-able-to-mock-methods.patch +# PATCH-FIX-UPSTREAM https://github.com/saltstack/salt/pull/36000 +Patch17: 0017-Check-for-single-quote-before-splitting-on-single-qu.patch +# PATCH-FIX-UPSTREAM https://github.com/saltstack/salt/pull/36139 +# https://github.com/saltstack/salt/pull/36158 +# https://github.com/saltstack/salt/pull/36227 +Patch18: 0018-Unit-tests-fixes-for-2016.3.2.patch +# PATCH-FIX-UPSTREAM https://github.com/saltstack/salt/pull/35715 +# https://github.com/saltstack/salt/pull/35983 +# https://github.com/saltstack/salt/pull/34826 +Patch19: 0019-Fix-snapper_test-for-python26.patch +# PATCH-FIX-UPSTREAM https://github.com/saltstack/salt/pull/36263 +Patch20: 0020-Integration-tests-fixes-for-2016.3.2.patch + BuildRoot: %{_tmppath}/%{name}-%{version}-build BuildRequires: logrotate @@ -111,17 +146,27 @@ BuildRequires: python-salt-testing >= 2015.2.16 BuildRequires: python-unittest2 BuildRequires: python-xml %endif -%if %{with docs} -#for docs +%if %{with builddocs} BuildRequires: python-sphinx %endif +%if 0%{?suse_version} > 1010 +BuildRequires: fdupes +%endif Requires(pre): %{_sbindir}/groupadd Requires(pre): %{_sbindir}/useradd + %if 0%{?suse_version} Requires(pre): %fillup_prereq Requires(pre): pwdutils %endif + +%if 0%{?suse_version} +Requires(pre): dbus-1 +%else +Requires(pre): dbus +%endif + Requires: logrotate Requires: python # @@ -414,6 +459,20 @@ Zsh command line completion support for %{name}. %endif +%if 0%{?suse_version} +%package susemanager +Summary: SUSE Manager Integration +Group: System/Management +Requires: %{name} = %{version}-%{release} +%if 0%{?suse_version} > 1110 +BuildArch: noarch +%endif + +%description susemanager +SUSE Manager Integration for various tools on the system. + +%endif + %prep %setup -q -n salt-%{version} cp %{S:1} . @@ -427,18 +486,33 @@ cp %{S:1} . # This is SUSE-only patch %if 0%{?suse_version} %patch7 -p1 -%patch8 -p1 %endif - +%patch8 -p1 %patch9 -p1 %patch10 -p1 %patch11 -p1 %patch12 -p1 +%patch13 -p1 +%patch14 -p1 +%patch15 -p1 +%patch16 -p1 +%patch17 -p1 +%patch18 -p1 +%patch19 -p1 +%patch20 -p1 %build python setup.py --salt-transport=both build -%if %{with docs} +%if %{with docs} && %{without builddocs} +# extract docs from the tarball +mkdir -p doc/_build +pushd doc/_build/ +tar xfv %{S:3} +popd +%endif + +%if %{with docs} && %{with builddocs} ## documentation cd doc && make html && rm _build/html/.buildinfo && rm _build/html/_images/proxy_minions.png && cd _build/html && chmod -R -x+X * %endif @@ -546,6 +620,11 @@ install -Dpm 0644 pkg/fish-completions/* %{buildroot}%{fish_completions_dir} echo "transport: raet" > %{buildroot}%{_sysconfdir}/salt/master.d/transport-raet.conf echo "transport: raet" > %{buildroot}%{_sysconfdir}/salt/minion.d/transport-raet.conf +%if 0%{?suse_version} > 1020 +%fdupes %{buildroot}%{_docdir} +%fdupes %{buildroot}%{python_sitelib} +%endif + %check %if %{with test} python setup.py test --runtests-opts=-u @@ -555,9 +634,50 @@ python setup.py test --runtests-opts=-u getent group salt >/dev/null || %{_sbindir}/groupadd -r salt getent passwd salt >/dev/null || %{_sbindir}/useradd -r -g salt -d /srv/salt -s /bin/false -c "salt-master daemon" salt -%if %{with systemd} %post +%if %{with systemd} systemd-tmpfiles --create /usr/lib/tmpfiles.d/salt.conf || true +%else +dbus-uuidgen --ensure +%endif + +%preun proxy +%if %{with systemd} +%service_del_preun salt-proxy@.service +%else +%if 0%{?suse_version} +%stop_on_removal salt-proxy +%else + if [ $1 -eq 0 ] ; then + /sbin/service salt-proxy stop >/dev/null 2>&1 + /sbin/chkconfig --del salt-proxy + fi +%endif +%endif + +%pre proxy +%if %{with systemd} +%service_add_pre salt-proxy@.service +%endif + +%post proxy +%if %{with systemd} +%service_add_post salt-proxy@.service +%fillup_only +%else +%if 0%{?suse_version} +%fillup_and_insserv +%endif +%endif + +%postun proxy +%if %{with systemd} +%service_del_postun salt-proxy@.service +%else +%if 0%{?suse_version} +%insserv_cleanup +%restart_on_update salt-proxy +%endif %endif %preun syndic @@ -781,13 +901,9 @@ fi %dir %attr(0750, root, root) %{_sysconfdir}/salt/minion.d/ %dir %attr(0750, root, root) %{_sysconfdir}/salt/pki/minion/ %dir %attr(0750, root, root) %{_localstatedir}/cache/salt/minion/ +#%dir %ghost %attr(0750, root, salt) %{_localstatedir}/run/salt/minion %{_sbindir}/rcsalt-minion -# Install plugin only on SUSE machines -%if 0%{?suse_version} -%{_prefix}/lib/zypp/plugins/commit/susemanager -%endif - %if %{with systemd} %{_unitdir}/salt-minion.service %else @@ -840,6 +956,7 @@ fi %dir %attr(0750, salt, salt) %{_localstatedir}/cache/salt/master/roots/ %dir %attr(0750, salt, salt) %{_localstatedir}/cache/salt/master/syndics/ %dir %attr(0750, salt, salt) %{_localstatedir}/cache/salt/master/tokens/ +#%dir %ghost %attr(0750, salt, salt) %{_localstatedir}/run/salt/master/ %files raet %defattr(-,root,root,-) @@ -853,17 +970,19 @@ fi %{_bindir}/salt-unity %{_mandir}/man1/salt-unity.1.gz %{_mandir}/man1/salt-call.1.gz +%{_mandir}/man1/spm.1.gz %config(noreplace) %{_sysconfdir}/logrotate.d/salt %{python_sitelib}/* %exclude %{python_sitelib}/salt/cloud/deploy/*.sh %attr(755,root,root)%{python_sitelib}/salt/cloud/deploy/*.sh %doc LICENSE AUTHORS README.rst HACKING.rst README.SUSE # -%dir %attr(0750, root, salt) %{_sysconfdir}/salt -%dir %attr(0750, root, salt) %{_sysconfdir}/salt/pki -%dir %attr(0750, salt, salt) %{_localstatedir}/log/salt -%dir %attr(0750, root, salt) %{_localstatedir}/cache/salt -%dir %attr(0750, root, salt) /srv/spm +%dir %attr(0750, root, salt) %{_sysconfdir}/salt +%dir %attr(0750, root, salt) %{_sysconfdir}/salt/pki +%dir %attr(0750, salt, salt) %{_localstatedir}/log/salt +%dir %attr(0750, root, salt) %{_localstatedir}/cache/salt +#%dir %ghost %attr(0750, root, salt) %{_localstatedir}/run/salt +%dir %attr(0750, root, salt) /srv/spm %if %{with systemd} /usr/lib/tmpfiles.d/salt.conf %endif @@ -896,4 +1015,11 @@ fi %dir %{fish_dir} %endif +# Install plugin only on SUSE machines +%if 0%{?suse_version} +%files susemanager +%defattr(-,root,root) +%{_prefix}/lib/zypp/plugins/commit/susemanager +%endif + %changelog diff --git a/update-documentation.sh b/update-documentation.sh new file mode 100644 index 0000000..0f35957 --- /dev/null +++ b/update-documentation.sh @@ -0,0 +1,64 @@ +#!/bin/bash +# +# Update html.tar.bz2 documentation tarball +# Author: Bo Maryniuk +# + +function check_env() { + for cmd in "sphinx-build" "make" "quilt"; do + if [ -z "$(which $cmd 2>/dev/null)" ]; then + echo "Error: '$cmd' is missing." + exit 1; + fi + done +} + +function quilt_setup() { + quilt setup salt.spec + cd $1 + quilt push -a +} + +function build_docs() { + cd $1 + make html + rm _build/html/.buildinfo + cd _build/html + chmod -R -x+X * + cd .. + tar cvf - html | bzip2 > /tmp/html.tar.bz2 +} + +function write_changelog() { + mv salt.changes salt.changes.previous + TIME=$(date -u +'%a %b %d %T %Z %Y') + MAIL=$1 + SEP="-------------------------------------------------------------------" + cat < salt.changes +$SEP +$TIME - $MAIL + +- Updated html.tar.bz2 documentation tarball. + +EOF + cat salt.changes.previous >> salt.changes + rm salt.changes.previous +} + +if [ -z "$1" ]; then + echo "Usage: $0 " + exit 1; +fi + +check_env; +START=$(pwd) +SRC_DIR="salt-$(cat salt.spec | grep ^Version: | cut -d: -f2 | sed -e 's/[[:blank:]]//g')"; +quilt_setup $SRC_DIR +build_docs doc + +cd $START +rm -rf $SRC_DIR +mv /tmp/html.tar.bz2 $START + +echo "Done" +echo "---------------"