diff --git a/_lastrevision b/_lastrevision
index 98cd9fe..cc6fc44 100644
--- a/_lastrevision
+++ b/_lastrevision
@@ -1 +1 @@
-f43b8fb2425e3371decf3cde040c70ed15de375d
\ No newline at end of file
+057b96d7f072dc69b61ca9e3b8d04a890003e58f
\ No newline at end of file
diff --git a/_service b/_service
index c995eeb..998165c 100644
--- a/_service
+++ b/_service
@@ -3,7 +3,7 @@
https://github.com/openSUSE/salt-packaging.git
salt
package
- oxygen-rc1
+ 2018.3.0
git
diff --git a/add-saltssh-multi-version-support-across-python-inte.patch b/add-saltssh-multi-version-support-across-python-inte.patch
index b656153..43652c9 100644
--- a/add-saltssh-multi-version-support-across-python-inte.patch
+++ b/add-saltssh-multi-version-support-across-python-inte.patch
@@ -1,4 +1,4 @@
-From 7d3c1fee891a34f1e521228458ab113c3f6dabe1 Mon Sep 17 00:00:00 2001
+From 36bc22560e050b7afe3d872aed99c0cdb9fde282 Mon Sep 17 00:00:00 2001
From: Bo Maryniuk
Date: Mon, 12 Mar 2018 12:01:39 +0100
Subject: [PATCH] Add SaltSSH multi-version support across Python
@@ -227,16 +227,45 @@ Disable wiping if state is executed
Properly mock a tempfile object
Support Python 2.6 versions
+
+Add digest collector for file trees etc
+
+Bufix: recurse calls damages the configuration (reference problem)
+
+Collect digest of the code
+
+Get code checksum into the shim options
+
+Get all the code content, not just Python sources
+
+Bugfix: Python3 compat - string required instead of bytes
+
+Lintfix: too many empty lines
+
+Lintfix: blocked function used
+
+Bugfix: key error master_tops_first
+
+Fix unit tests for the checksum generator
+
+Use code checksum to update thin archive on client's cache
+
+Lintfix
+
+Set master_top_first to False by default
---
doc/topics/releases/fluorine.rst | 178 +++++++++++
- salt/client/ssh/__init__.py | 20 +-
- salt/client/ssh/ssh_py_shim.py | 88 ++++--
+ salt/client/ssh/__init__.py | 66 ++--
+ salt/client/ssh/ssh_py_shim.py | 95 ++++--
salt/client/ssh/wrapper/__init__.py | 2 +-
+ salt/config/__init__.py | 1 +
salt/modules/zfs.py | 4 +-
salt/modules/zpool.py | 4 +-
- salt/utils/thin.py | 434 +++++++++++++++++++-------
- tests/unit/utils/test_thin.py | 607 ++++++++++++++++++++++++++++++++++++
- 8 files changed, 1182 insertions(+), 155 deletions(-)
+ salt/state.py | 2 +-
+ salt/utils/hashutils.py | 37 +++
+ salt/utils/thin.py | 450 +++++++++++++++++++-------
+ tests/unit/utils/test_thin.py | 612 ++++++++++++++++++++++++++++++++++++
+ 11 files changed, 1265 insertions(+), 186 deletions(-)
create mode 100644 doc/topics/releases/fluorine.rst
create mode 100644 tests/unit/utils/test_thin.py
@@ -425,7 +454,7 @@ index 0000000000..40c69e25cc
+Salt version is also available on the Master machine, although does not need to be directly
+installed together with the older Python interpreter.
diff --git a/salt/client/ssh/__init__.py b/salt/client/ssh/__init__.py
-index f1c1ad9a22..ea5c700830 100644
+index f1c1ad9a22..399facf5c8 100644
--- a/salt/client/ssh/__init__.py
+++ b/salt/client/ssh/__init__.py
@@ -150,14 +150,10 @@ EX_PYTHON_INVALID={EX_THIN_PYTHON_INVALID}
@@ -470,16 +499,71 @@ index f1c1ad9a22..ea5c700830 100644
if kwargs.get('thin_dir'):
self.thin_dir = kwargs['thin_dir']
elif self.winrm:
-@@ -1168,7 +1165,6 @@ class Single(object):
+@@ -1161,38 +1158,39 @@ class Single(object):
+ cachedir = self.opts['_caller_cachedir']
+ else:
+ cachedir = self.opts['cachedir']
+- thin_sum = salt.utils.thin.thin_sum(cachedir, 'sha1')
++ thin_code_digest, thin_sum = salt.utils.thin.thin_sum(cachedir, 'sha1')
+ debug = ''
+ if not self.opts.get('log_level'):
+ self.opts['log_level'] = 'info'
if salt.log.LOG_LEVELS['debug'] >= salt.log.LOG_LEVELS[self.opts.get('log_level', 'info')]:
debug = '1'
arg_str = '''
-OPTIONS = OBJ()
OPTIONS.config = \
"""
- {0}
+-{0}
++{config}
+ """
+-OPTIONS.delimiter = '{1}'
+-OPTIONS.saltdir = '{2}'
+-OPTIONS.checksum = '{3}'
+-OPTIONS.hashfunc = '{4}'
+-OPTIONS.version = '{5}'
+-OPTIONS.ext_mods = '{6}'
+-OPTIONS.wipe = {7}
+-OPTIONS.tty = {8}
+-OPTIONS.cmd_umask = {9}
+-ARGS = {10}\n'''.format(self.minion_config,
+- RSTR,
+- self.thin_dir,
+- thin_sum,
+- 'sha1',
+- salt.version.__version__,
+- self.mods.get('version', ''),
+- self.wipe,
+- self.tty,
+- self.cmd_umask,
+- self.argv)
++OPTIONS.delimiter = '{delimeter}'
++OPTIONS.saltdir = '{saltdir}'
++OPTIONS.checksum = '{checksum}'
++OPTIONS.hashfunc = '{hashfunc}'
++OPTIONS.version = '{version}'
++OPTIONS.ext_mods = '{ext_mods}'
++OPTIONS.wipe = {wipe}
++OPTIONS.tty = {tty}
++OPTIONS.cmd_umask = {cmd_umask}
++OPTIONS.code_checksum = {code_checksum}
++ARGS = {arguments}\n'''.format(config=self.minion_config,
++ delimeter=RSTR,
++ saltdir=self.thin_dir,
++ checksum=thin_sum,
++ hashfunc='sha1',
++ version=salt.version.__version__,
++ ext_mods=self.mods.get('version', ''),
++ wipe=self.wipe,
++ tty=self.tty,
++ cmd_umask=self.cmd_umask,
++ code_checksum=thin_code_digest,
++ arguments=self.argv)
+ py_code = SSH_PY_SHIM.replace('#%%OPTS', arg_str)
+ if six.PY2:
+ py_code_enc = py_code.encode('base64')
diff --git a/salt/client/ssh/ssh_py_shim.py b/salt/client/ssh/ssh_py_shim.py
-index e46220fc80..661a671b81 100644
+index e46220fc80..21d03343b9 100644
--- a/salt/client/ssh/ssh_py_shim.py
+++ b/salt/client/ssh/ssh_py_shim.py
@@ -16,11 +16,13 @@ import sys
@@ -587,7 +671,7 @@ index e46220fc80..661a671b81 100644
def main(argv): # pylint: disable=W0613
'''
Main program body
-@@ -215,30 +261,30 @@ def main(argv): # pylint: disable=W0613
+@@ -215,32 +261,25 @@ def main(argv): # pylint: disable=W0613
if scpstat != 0:
sys.exit(EX_SCP_NOT_FOUND)
@@ -604,33 +688,35 @@ index e46220fc80..661a671b81 100644
- version_path = os.path.normpath(os.path.join(OPTIONS.saltdir, 'version'))
- if not os.path.exists(version_path) or not os.path.isfile(version_path):
-+ if not os.path.exists(OPTIONS.saltdir):
-+ need_deployment()
-+
-+ checksum_path = os.path.normpath(os.path.join(OPTIONS.saltdir, 'thin_checksum'))
-+ if not os.path.exists(checksum_path) or not os.path.isfile(checksum_path):
- sys.stderr.write(
- 'WARNING: Unable to locate current thin '
+- sys.stderr.write(
+- 'WARNING: Unable to locate current thin '
- ' version: {0}.\n'.format(version_path)
-+ ' checksum: {0}.\n'.format(checksum_path)
- )
+- )
++ if not os.path.exists(OPTIONS.saltdir):
need_deployment()
- with open(version_path, 'r') as vpo:
- cur_version = vpo.readline().strip()
- if cur_version != OPTIONS.version:
-+ with open(checksum_path, 'r') as vpo:
-+ cur_checksum = vpo.readline().strip()
-+ if cur_checksum != OPTIONS.checksum:
- sys.stderr.write(
+- sys.stderr.write(
- 'WARNING: current thin version {0}'
-+ 'WARNING: current thin checksum {0}'
- ' is not up-to-date with {1}.\n'.format(
+- ' is not up-to-date with {1}.\n'.format(
- cur_version, OPTIONS.version
-+ cur_checksum, OPTIONS.checksum
- )
- )
+- )
+- )
++
++ code_checksum_path = os.path.normpath(os.path.join(OPTIONS.saltdir, 'code-checksum'))
++ if not os.path.exists(code_checksum_path) or not os.path.isfile(code_checksum_path):
++ sys.stderr.write('WARNING: Unable to locate current code checksum: {0}.\n'.format(code_checksum_path))
++ need_deployment()
++ with open(code_checksum_path, 'r') as vpo:
++ cur_code_cs = vpo.readline().strip()
++ if cur_code_cs != OPTIONS.code_checksum:
++ sys.stderr.write('WARNING: current code checksum {0} is different to {1}.\n'.format(cur_code_cs,
++ OPTIONS.code_checksum))
need_deployment()
-@@ -270,7 +316,7 @@ def main(argv): # pylint: disable=W0613
+ # Salt thin exists and is up-to-date - fall through and use it
+
+@@ -270,7 +309,7 @@ def main(argv): # pylint: disable=W0613
argv_prepared = ARGS
salt_argv = [
@@ -639,7 +725,7 @@ index e46220fc80..661a671b81 100644
salt_call_path,
'--retcode-passthrough',
'--local',
-@@ -303,7 +349,10 @@ def main(argv): # pylint: disable=W0613
+@@ -303,7 +342,10 @@ def main(argv): # pylint: disable=W0613
if OPTIONS.tty:
# Returns bytes instead of string on python 3
stdout, _ = subprocess.Popen(salt_argv, stdout=subprocess.PIPE, stderr=subprocess.PIPE).communicate()
@@ -651,7 +737,7 @@ index e46220fc80..661a671b81 100644
sys.stdout.flush()
if OPTIONS.wipe:
shutil.rmtree(OPTIONS.saltdir)
-@@ -315,5 +364,6 @@ def main(argv): # pylint: disable=W0613
+@@ -315,5 +357,6 @@ def main(argv): # pylint: disable=W0613
if OPTIONS.cmd_umask is not None:
os.umask(old_umask)
@@ -671,6 +757,18 @@ index 04d751b51a..09f9344642 100644
fsclient=self.fsclient,
minion_opts=self.minion_opts,
**self.kwargs
+diff --git a/salt/config/__init__.py b/salt/config/__init__.py
+index df0e1388b7..b3de3820b0 100644
+--- a/salt/config/__init__.py
++++ b/salt/config/__init__.py
+@@ -1652,6 +1652,7 @@ DEFAULT_MASTER_OPTS = {
+ 'state_top': 'top.sls',
+ 'state_top_saltenv': None,
+ 'master_tops': {},
++ 'master_tops_first': False,
+ 'order_masters': False,
+ 'job_cache': True,
+ 'ext_job_cache': '',
diff --git a/salt/modules/zfs.py b/salt/modules/zfs.py
index bc54044b5c..d8fbfc76be 100644
--- a/salt/modules/zfs.py
@@ -705,8 +803,73 @@ index f955175664..5e03418919 100644
@salt.utils.decorators.memoize
+diff --git a/salt/state.py b/salt/state.py
+index 49d68d2edf..8c0b90545c 100644
+--- a/salt/state.py
++++ b/salt/state.py
+@@ -3332,7 +3332,7 @@ class BaseHighState(object):
+ ext_matches = self._master_tops()
+ for saltenv in ext_matches:
+ top_file_matches = matches.get(saltenv, [])
+- if self.opts['master_tops_first']:
++ if self.opts.get('master_tops_first'):
+ first = ext_matches[saltenv]
+ second = top_file_matches
+ else:
+diff --git a/salt/utils/hashutils.py b/salt/utils/hashutils.py
+index 4c9cb4a50c..18f7459d3c 100644
+--- a/salt/utils/hashutils.py
++++ b/salt/utils/hashutils.py
+@@ -9,6 +9,7 @@ import base64
+ import hashlib
+ import hmac
+ import random
++import os
+
+ # Import Salt libs
+ from salt.ext import six
+@@ -163,3 +164,39 @@ def get_hash(path, form='sha256', chunk_size=65536):
+ for chunk in iter(lambda: ifile.read(chunk_size), b''):
+ hash_obj.update(chunk)
+ return hash_obj.hexdigest()
++
++
++class DigestCollector(object):
++ '''
++ Class to collect digest of the file tree.
++ '''
++
++ def __init__(self, form='sha256', buff=0x10000):
++ '''
++ Constructor of the class.
++ :param form:
++ '''
++ self.__digest = hasattr(hashlib, form) and getattr(hashlib, form)() or None
++ if self.__digest is None:
++ raise ValueError('Invalid hash type: {0}'.format(form))
++ self.__buff = buff
++
++ def add(self, path):
++ '''
++ Update digest with the file content by path.
++
++ :param path:
++ :return:
++ '''
++ with salt.utils.files.fopen(path, 'rb') as ifile:
++ for chunk in iter(lambda: ifile.read(self.__buff), b''):
++ self.__digest.update(chunk)
++
++ def digest(self):
++ '''
++ Get digest.
++
++ :return:
++ '''
++
++ return salt.utils.stringutils.to_str(self.__digest.hexdigest() + os.linesep)
diff --git a/salt/utils/thin.py b/salt/utils/thin.py
-index 4c0969ea96..a6990d00b1 100644
+index 4c0969ea96..e4b878eb19 100644
--- a/salt/utils/thin.py
+++ b/salt/utils/thin.py
@@ -8,11 +8,14 @@ from __future__ import absolute_import, print_function, unicode_literals
@@ -849,7 +1012,7 @@ index 4c0969ea96..a6990d00b1 100644
def thin_path(cachedir):
-@@ -101,29 +133,136 @@ def thin_path(cachedir):
+@@ -101,29 +133,137 @@ def thin_path(cachedir):
return os.path.join(cachedir, 'thin', 'thin.tgz')
@@ -878,7 +1041,9 @@ index 4c0969ea96..a6990d00b1 100644
+def _add_dependency(container, obj):
+ '''
+ Add a dependency to the top list.
-+
+
+- tops.append(_six.__file__.replace('.pyc', '.py'))
+- tops.append(backports_abc.__file__.replace('.pyc', '.py'))
+ :param obj:
+ :param is_file:
+ :return:
@@ -893,23 +1058,32 @@ index 4c0969ea96..a6990d00b1 100644
+ '''
+ This function is called externally from the alternative
+ Python interpreter from within _get_tops function.
-+
+
+- if HAS_CERTIFI:
+- tops.append(os.path.dirname(certifi.__file__))
+ :param extra_mods:
+ :param so_mods:
+ :return:
+ '''
+ extra = salt.utils.json.loads(sys.argv[1])
+ tops = get_tops(**extra)
-+
+
+- if HAS_SINGLEDISPATCH:
+- tops.append(singledispatch.__file__.replace('.pyc', '.py'))
+ return salt.utils.json.dumps(tops, ensure_ascii=False)
-+
-+
+
+- if HAS_SINGLEDISPATCH_HELPERS:
+- tops.append(singledispatch_helpers.__file__.replace('.pyc', '.py'))
+
+- if HAS_SSL_MATCH_HOSTNAME:
+- tops.append(os.path.dirname(os.path.dirname(ssl_match_hostname.__file__)))
+def get_ext_tops(config):
+ '''
+ Get top directories for the dependencies, based on external configuration.
+
+ :return:
+ '''
++ config = copy.deepcopy(config)
+ alternatives = {}
+ required = ['jinja2', 'yaml', 'tornado', 'msgpack']
+ tops = []
@@ -958,9 +1132,7 @@ index 4c0969ea96..a6990d00b1 100644
+def _get_ext_namespaces(config):
+ '''
+ Get namespaces from the existing configuration.
-
-- tops.append(_six.__file__.replace('.pyc', '.py'))
-- tops.append(backports_abc.__file__.replace('.pyc', '.py'))
++
+ :param config:
+ :return:
+ '''
@@ -975,22 +1147,14 @@ index 4c0969ea96..a6990d00b1 100644
+ "to what Python's major/minor version it should be constrained.")
+ else:
+ namespaces[ns] = constraint_version
-
-- if HAS_CERTIFI:
-- tops.append(os.path.dirname(certifi.__file__))
++
+ return namespaces
-
-- if HAS_SINGLEDISPATCH:
-- tops.append(singledispatch.__file__.replace('.pyc', '.py'))
-
-- if HAS_SINGLEDISPATCH_HELPERS:
-- tops.append(singledispatch_helpers.__file__.replace('.pyc', '.py'))
++
++
+def get_tops(extra_mods='', so_mods=''):
+ '''
+ Get top directories for the dependencies, based on Python interpreter.
-
-- if HAS_SSL_MATCH_HOSTNAME:
-- tops.append(os.path.dirname(os.path.dirname(ssl_match_hostname.__file__)))
++
+ :param extra_mods:
+ :param so_mods:
+ :return:
@@ -1004,7 +1168,7 @@ index 4c0969ea96..a6990d00b1 100644
for mod in [m for m in extra_mods.split(',') if m]:
if mod not in locals() and mod not in globals():
-@@ -135,28 +274,49 @@ def get_tops(extra_mods='', so_mods=''):
+@@ -135,28 +275,49 @@ def get_tops(extra_mods='', so_mods=''):
tops.append(moddir)
else:
tops.append(os.path.join(moddir, base + '.py'))
@@ -1066,7 +1230,7 @@ index 4c0969ea96..a6990d00b1 100644
'''
Generate the salt-thin tarball and print the location of the tarball
Optional additional mods to include (e.g. mako) can be supplied as a comma
-@@ -171,19 +331,24 @@ def gen_thin(cachedir, extra_mods='', overwrite=False, so_mods='',
+@@ -171,19 +332,26 @@ def gen_thin(cachedir, extra_mods='', overwrite=False, so_mods='',
salt-run thin.generate mako,wempy 1
salt-run thin.generate overwrite=1
'''
@@ -1089,6 +1253,8 @@ index 4c0969ea96..a6990d00b1 100644
pythinver = os.path.join(thindir, '.thin-gen-py-version')
salt_call = os.path.join(thindir, 'salt-call')
+ pymap_cfg = os.path.join(thindir, 'supported-versions')
++ code_checksum = os.path.join(thindir, 'code-checksum')
++ digest_collector = salt.utils.hashutils.DigestCollector()
+
with salt.utils.files.fopen(salt_call, 'wb') as fp_:
- fp_.write(SALTCALL)
@@ -1097,7 +1263,7 @@ index 4c0969ea96..a6990d00b1 100644
if os.path.isfile(thintar):
if not overwrite:
if os.path.isfile(thinver):
-@@ -197,85 +362,88 @@ def gen_thin(cachedir, extra_mods='', overwrite=False, so_mods='',
+@@ -197,85 +365,88 @@ def gen_thin(cachedir, extra_mods='', overwrite=False, so_mods='',
if overwrite:
try:
@@ -1225,7 +1391,7 @@ index 4c0969ea96..a6990d00b1 100644
for py_ver, tops in _six.iteritems(tops_py_version_mapping):
for top in tops:
if absonly and not os.path.isabs(top):
-@@ -291,48 +459,76 @@ def gen_thin(cachedir, extra_mods='', overwrite=False, so_mods='',
+@@ -291,48 +462,80 @@ def gen_thin(cachedir, extra_mods='', overwrite=False, so_mods='',
egg.extractall(tempdir)
top = os.path.join(tempdir, base)
os.chdir(tempdir)
@@ -1249,6 +1415,7 @@ index 4c0969ea96..a6990d00b1 100644
- tfp.add(os.path.join(root, name),
- arcname=os.path.join('py{0}'.format(py_ver), root, name))
- elif compress == 'zip':
++ digest_collector.add(os.path.join(root, name))
+ arcname = os.path.join(site_pkg_dir, root, name)
+ if hasattr(tfp, 'getinfo'):
try:
@@ -1285,6 +1452,7 @@ index 4c0969ea96..a6990d00b1 100644
+ for root, dirs, files in salt.utils.path.os_walk(base, followlinks=True):
+ for name in files:
+ if not name.endswith(('.pyc', '.pyo')):
++ digest_collector.add(os.path.join(root, name))
+ arcname = os.path.join(ns, site_pkg_dir, root, name)
+ if hasattr(tfp, 'getinfo'):
+ try:
@@ -1305,6 +1473,8 @@ index 4c0969ea96..a6990d00b1 100644
with salt.utils.files.fopen(pythinver, 'w+') as fp_:
- fp_.write(str(sys.version_info[0])) # future lint: disable=blacklisted-function
+ fp_.write(str(sys.version_info.major)) # future lint: disable=blacklisted-function
++ with salt.utils.files.fopen(code_checksum, 'w+') as fp_:
++ fp_.write(digest_collector.digest())
os.chdir(os.path.dirname(thinver))
- if compress == 'gzip':
- tfp.add('version')
@@ -1313,7 +1483,7 @@ index 4c0969ea96..a6990d00b1 100644
- tfp.write('version')
- tfp.write('.thin-gen-py-version')
+
-+ for fname in ['version', '.thin-gen-py-version', 'salt-call', 'supported-versions']:
++ for fname in ['version', '.thin-gen-py-version', 'salt-call', 'supported-versions', 'code-checksum']:
+ tfp.add(fname)
+
if start_dir:
@@ -1323,7 +1493,23 @@ index 4c0969ea96..a6990d00b1 100644
return thintar
-@@ -368,7 +564,7 @@ def gen_min(cachedir, extra_mods='', overwrite=False, so_mods='',
+@@ -341,7 +544,14 @@ def thin_sum(cachedir, form='sha1'):
+ Return the checksum of the current thin tarball
+ '''
+ thintar = gen_thin(cachedir)
+- return salt.utils.hashutils.get_hash(thintar, form)
++ code_checksum_path = os.path.join(cachedir, 'thin', 'code-checksum')
++ if os.path.isfile(code_checksum_path):
++ with salt.utils.fopen(code_checksum_path, 'r') as fh:
++ code_checksum = "'{0}'".format(fh.read().strip())
++ else:
++ code_checksum = "'0'"
++
++ return code_checksum, salt.utils.hashutils.get_hash(thintar, form)
+
+
+ def gen_min(cachedir, extra_mods='', overwrite=False, so_mods='',
+@@ -368,7 +578,7 @@ def gen_min(cachedir, extra_mods='', overwrite=False, so_mods='',
pyminver = os.path.join(mindir, '.min-gen-py-version')
salt_call = os.path.join(mindir, 'salt-call')
with salt.utils.files.fopen(salt_call, 'wb') as fp_:
@@ -1334,10 +1520,10 @@ index 4c0969ea96..a6990d00b1 100644
if os.path.isfile(minver):
diff --git a/tests/unit/utils/test_thin.py b/tests/unit/utils/test_thin.py
new file mode 100644
-index 0000000000..8157eefed8
+index 0000000000..549d48a703
--- /dev/null
+++ b/tests/unit/utils/test_thin.py
-@@ -0,0 +1,607 @@
+@@ -0,0 +1,612 @@
+# -*- coding: utf-8 -*-
+'''
+ :codeauthor: :email:`Bo Maryniuk `
@@ -1417,7 +1603,6 @@ index 0000000000..8157eefed8
+
+ return tf
+
-+
+ @patch('salt.exceptions.SaltSystemExit', Exception)
+ @patch('salt.utils.thin.log', MagicMock())
+ @patch('salt.utils.thin.os.path.isfile', MagicMock(return_value=False))
@@ -1532,7 +1717,11 @@ index 0000000000..8157eefed8
+ 'yaml': '/yaml/',
+ 'tornado': '/tornado/tornado.py',
+ 'msgpack': 'msgpack.py'}}}
-+ assert cfg == thin.get_ext_tops(cfg)
++ out = thin.get_ext_tops(cfg)
++ assert out['namespace']['py-version'] == cfg['namespace']['py-version']
++ assert out['namespace']['path'] == cfg['namespace']['path']
++ assert sorted(out['namespace']['dependencies']) == sorted(['/tornado/tornado.py',
++ '/jinja/foo.py', '/yaml/', 'msgpack.py'])
+
+ @patch('salt.utils.thin.sys.argv', [None, '{"foo": "bar"}'])
+ @patch('salt.utils.thin.get_tops', lambda **kw: kw)
@@ -1698,7 +1887,7 @@ index 0000000000..8157eefed8
+
+ :return:
+ '''
-+ assert thin.thin_sum('/cachedir', form='sha256') == 12345
++ assert thin.thin_sum('/cachedir', form='sha256')[1] == 12345
+ thin.salt.utils.hashutils.get_hash.assert_called()
+ assert thin.salt.utils.hashutils.get_hash.call_count == 1
+
@@ -1841,6 +2030,7 @@ index 0000000000..8157eefed8
+ @patch('salt.utils.thin._six.PY3', True)
+ @patch('salt.utils.thin._six.PY2', False)
+ @patch('salt.utils.thin.sys.version_info', _version_info(None, 3, 6))
++ @patch('salt.utils.hashutils.DigestCollector', MagicMock())
+ def test_gen_thin_main_content_files_written_py3(self):
+ '''
+ Test thin.gen_thin function if main content files are written.
@@ -1855,7 +2045,7 @@ index 0000000000..8157eefed8
+ 'py3/root/r1', 'py3/root/r2', 'py3/root/r3', 'py3/root2/r4', 'py3/root2/r5', 'py3/root2/r6',
+ 'pyall/root/r1', 'pyall/root/r2', 'pyall/root/r3', 'pyall/root2/r4', 'pyall/root2/r5', 'pyall/root2/r6'
+ ]
-+ for cl in thin.tarfile.open().method_calls[:-5]:
++ for cl in thin.tarfile.open().method_calls[:-6]:
+ arcname = cl[2].get('arcname')
+ assert arcname in files
+ files.pop(files.index(arcname))
@@ -1890,6 +2080,7 @@ index 0000000000..8157eefed8
+ @patch('salt.utils.thin._six.PY3', True)
+ @patch('salt.utils.thin._six.PY2', False)
+ @patch('salt.utils.thin.sys.version_info', _version_info(None, 3, 6))
++ @patch('salt.utils.hashutils.DigestCollector', MagicMock())
+ def test_gen_thin_ext_alternative_content_files_written_py3(self):
+ '''
+ Test thin.gen_thin function if external alternative content files are written.
@@ -1905,7 +2096,7 @@ index 0000000000..8157eefed8
+ 'namespace/py2/root/r1', 'namespace/py2/root/r2', 'namespace/py2/root/r3',
+ 'namespace/py2/root2/r4', 'namespace/py2/root2/r5', 'namespace/py2/root2/r6'
+ ]
-+ for idx, cl in enumerate(thin.tarfile.open().method_calls[12:-5]):
++ for idx, cl in enumerate(thin.tarfile.open().method_calls[12:-6]):
+ arcname = cl[2].get('arcname')
+ assert arcname in files
+ files.pop(files.index(arcname))
@@ -1946,6 +2137,6 @@ index 0000000000..8157eefed8
+ for t_line in ['second-system-effect:2:7', 'solar-interference:2:6']:
+ assert t_line in out
--
-2.16.2
+2.15.1
diff --git a/fall-back-to-pymysql.patch b/fall-back-to-pymysql.patch
new file mode 100644
index 0000000..067ac2e
--- /dev/null
+++ b/fall-back-to-pymysql.patch
@@ -0,0 +1,317 @@
+From f7ba683153e11be401a5971ba029d0a3964b1ecb Mon Sep 17 00:00:00 2001
+From: Maximilian Meister
+Date: Thu, 5 Apr 2018 13:23:23 +0200
+Subject: [PATCH] fall back to PyMySQL
+
+same is already done in modules (see #26803)
+
+Signed-off-by: Maximilian Meister
+---
+ salt/auth/mysql.py | 25 ++++++++++++++++++++++---
+ salt/cache/mysql_cache.py | 28 +++++++++++++++++++---------
+ salt/modules/mysql.py | 22 ++++++++++------------
+ salt/pillar/mysql.py | 21 ++++++++++++++++-----
+ salt/returners/mysql.py | 29 +++++++++++++++++++++--------
+ tests/unit/pillar/test_mysql.py | 2 +-
+ 6 files changed, 89 insertions(+), 38 deletions(-)
+
+diff --git a/salt/auth/mysql.py b/salt/auth/mysql.py
+index 8bc18a4101..86d00a4373 100644
+--- a/salt/auth/mysql.py
++++ b/salt/auth/mysql.py
+@@ -55,10 +55,29 @@ import logging
+ log = logging.getLogger(__name__)
+
+ try:
++ # Trying to import MySQLdb
+ import MySQLdb
+- HAS_MYSQL = True
++ import MySQLdb.cursors
++ import MySQLdb.converters
++ from MySQLdb.connections import OperationalError
+ except ImportError:
+- HAS_MYSQL = False
++ try:
++ # MySQLdb import failed, try to import PyMySQL
++ import pymysql
++ pymysql.install_as_MySQLdb()
++ import MySQLdb
++ import MySQLdb.cursors
++ import MySQLdb.converters
++ from MySQLdb.err import OperationalError
++ except ImportError:
++ MySQLdb = None
++
++
++def __virtual__():
++ '''
++ Confirm that a python mysql client is installed.
++ '''
++ return bool(MySQLdb), 'No python mysql client installed.' if MySQLdb is None else ''
+
+
+ def __get_connection_info():
+@@ -95,7 +114,7 @@ def auth(username, password):
+ _info['username'],
+ _info['password'],
+ _info['database'])
+- except MySQLdb.OperationalError as e:
++ except OperationalError as e:
+ log.error(e)
+ return False
+
+diff --git a/salt/cache/mysql_cache.py b/salt/cache/mysql_cache.py
+index 9d6aa17987..8b0a942310 100644
+--- a/salt/cache/mysql_cache.py
++++ b/salt/cache/mysql_cache.py
+@@ -46,11 +46,24 @@ value to ``mysql``:
+ from __future__ import absolute_import, print_function, unicode_literals
+ from time import sleep
+ import logging
++
+ try:
++ # Trying to import MySQLdb
+ import MySQLdb
+- HAS_MYSQL = True
++ import MySQLdb.cursors
++ import MySQLdb.converters
++ from MySQLdb.connections import OperationalError
+ except ImportError:
+- HAS_MYSQL = False
++ try:
++ # MySQLdb import failed, try to import PyMySQL
++ import pymysql
++ pymysql.install_as_MySQLdb()
++ import MySQLdb
++ import MySQLdb.cursors
++ import MySQLdb.converters
++ from MySQLdb.err import OperationalError
++ except ImportError:
++ MySQLdb = None
+
+ from salt.exceptions import SaltCacheError
+
+@@ -71,12 +84,9 @@ __func_alias__ = {'ls': 'list'}
+
+ def __virtual__():
+ '''
+- Confirm that python-mysql package is installed.
++ Confirm that a python mysql client is installed.
+ '''
+- if not HAS_MYSQL:
+- return (False, "Please install python-mysql package to use mysql data "
+- "cache driver")
+- return __virtualname__
++ return bool(MySQLdb), 'No python mysql client installed.' if MySQLdb is None else ''
+
+
+ def run_query(conn, query, retries=3):
+@@ -84,13 +94,13 @@ def run_query(conn, query, retries=3):
+ Get a cursor and run a query. Reconnect up to `retries` times if
+ needed.
+ Returns: cursor, affected rows counter
+- Raises: SaltCacheError, AttributeError, MySQLdb.OperationalError
++ Raises: SaltCacheError, AttributeError, OperationalError
+ '''
+ try:
+ cur = conn.cursor()
+ out = cur.execute(query)
+ return cur, out
+- except (AttributeError, MySQLdb.OperationalError) as e:
++ except (AttributeError, OperationalError) as e:
+ if retries == 0:
+ raise
+ # reconnect creating new client
+diff --git a/salt/modules/mysql.py b/salt/modules/mysql.py
+index 0625b02a96..8b17e461ea 100644
+--- a/salt/modules/mysql.py
++++ b/salt/modules/mysql.py
+@@ -51,13 +51,14 @@ import salt.utils.stringutils
+ from salt.ext import six
+ # pylint: disable=import-error
+ from salt.ext.six.moves import range, zip # pylint: disable=no-name-in-module,redefined-builtin
++
+ try:
+- # Try to import MySQLdb
++ # Trying to import MySQLdb
+ import MySQLdb
+ import MySQLdb.cursors
+ import MySQLdb.converters
+ from MySQLdb.constants import FIELD_TYPE, FLAG
+- HAS_MYSQLDB = True
++ from MySQLdb.connections import OperationalError
+ except ImportError:
+ try:
+ # MySQLdb import failed, try to import PyMySQL
+@@ -67,10 +68,9 @@ except ImportError:
+ import MySQLdb.cursors
+ import MySQLdb.converters
+ from MySQLdb.constants import FIELD_TYPE, FLAG
+- HAS_MYSQLDB = True
++ from MySQLdb.err import OperationalError
+ except ImportError:
+- # No MySQL Connector installed, return False
+- HAS_MYSQLDB = False
++ MySQLdb = None
+
+ log = logging.getLogger(__name__)
+
+@@ -195,11 +195,9 @@ And theses could be mixed, in a like query value with args: 'f\_o\%%o`b\'a"r'
+
+ def __virtual__():
+ '''
+- Only load this module if the mysql libraries exist
++ Confirm that a python mysql client is installed.
+ '''
+- if HAS_MYSQLDB:
+- return True
+- return (False, 'The mysql execution module cannot be loaded: neither MySQLdb nor PyMySQL is available.')
++ return bool(MySQLdb), 'No python mysql client installed.' if MySQLdb is None else ''
+
+
+ def __check_table(name, table, **connection_args):
+@@ -331,7 +329,7 @@ def _connect(**kwargs):
+ connargs.pop('passwd')
+ try:
+ dbc = MySQLdb.connect(**connargs)
+- except MySQLdb.OperationalError as exc:
++ except OperationalError as exc:
+ err = 'MySQL Error {0}: {1}'.format(*exc)
+ __context__['mysql.error'] = err
+ log.error(err)
+@@ -647,7 +645,7 @@ def query(database, query, **connection_args):
+ log.debug('Using db: %s to run query %s', database, query)
+ try:
+ affected = _execute(cur, query)
+- except MySQLdb.OperationalError as exc:
++ except OperationalError as exc:
+ err = 'MySQL Error {0}: {1}'.format(*exc)
+ __context__['mysql.error'] = err
+ log.error(err)
+@@ -772,7 +770,7 @@ def status(**connection_args):
+ qry = 'SHOW STATUS'
+ try:
+ _execute(cur, qry)
+- except MySQLdb.OperationalError as exc:
++ except OperationalError as exc:
+ err = 'MySQL Error {0}: {1}'.format(*exc)
+ __context__['mysql.error'] = err
+ log.error(err)
+diff --git a/salt/pillar/mysql.py b/salt/pillar/mysql.py
+index 8029e5c197..d3f9619ad5 100644
+--- a/salt/pillar/mysql.py
++++ b/salt/pillar/mysql.py
+@@ -59,16 +59,27 @@ log = logging.getLogger(__name__)
+
+ # Import third party libs
+ try:
++ # Trying to import MySQLdb
+ import MySQLdb
+- HAS_MYSQL = True
++ import MySQLdb.cursors
++ import MySQLdb.converters
+ except ImportError:
+- HAS_MYSQL = False
++ try:
++ # MySQLdb import failed, try to import PyMySQL
++ import pymysql
++ pymysql.install_as_MySQLdb()
++ import MySQLdb
++ import MySQLdb.cursors
++ import MySQLdb.converters
++ except ImportError:
++ MySQLdb = None
+
+
+ def __virtual__():
+- if not HAS_MYSQL:
+- return False
+- return True
++ '''
++ Confirm that a python mysql client is installed.
++ '''
++ return bool(MySQLdb), 'No python mysql client installed.' if MySQLdb is None else ''
+
+
+ class MySQLExtPillar(SqlBaseExtPillar):
+diff --git a/salt/returners/mysql.py b/salt/returners/mysql.py
+index af6698142b..85892cb06c 100644
+--- a/salt/returners/mysql.py
++++ b/salt/returners/mysql.py
+@@ -155,11 +155,24 @@ import salt.exceptions
+
+ # Import 3rd-party libs
+ from salt.ext import six
++
+ try:
++ # Trying to import MySQLdb
+ import MySQLdb
+- HAS_MYSQL = True
++ import MySQLdb.cursors
++ import MySQLdb.converters
++ from MySQLdb.connections import OperationalError
+ except ImportError:
+- HAS_MYSQL = False
++ try:
++ # MySQLdb import failed, try to import PyMySQL
++ import pymysql
++ pymysql.install_as_MySQLdb()
++ import MySQLdb
++ import MySQLdb.cursors
++ import MySQLdb.converters
++ from MySQLdb.err import OperationalError
++ except ImportError:
++ MySQLdb = None
+
+ log = logging.getLogger(__name__)
+
+@@ -168,10 +181,10 @@ __virtualname__ = 'mysql'
+
+
+ def __virtual__():
+- if not HAS_MYSQL:
+- return False, 'Could not import mysql returner; ' \
+- 'mysql python client is not installed.'
+- return True
++ '''
++ Confirm that a python mysql client is installed.
++ '''
++ return bool(MySQLdb), 'No python mysql client installed.' if MySQLdb is None else ''
+
+
+ def _get_options(ret=None):
+@@ -228,7 +241,7 @@ def _get_serv(ret=None, commit=False):
+ conn = __context__['mysql_returner_conn']
+ conn.ping()
+ connect = False
+- except MySQLdb.connections.OperationalError as exc:
++ except OperationalError as exc:
+ log.debug('OperationalError on ping: %s', exc)
+
+ if connect:
+@@ -254,7 +267,7 @@ def _get_serv(ret=None, commit=False):
+ __context__['mysql_returner_conn'] = conn
+ except TypeError:
+ pass
+- except MySQLdb.connections.OperationalError as exc:
++ except OperationalError as exc:
+ raise salt.exceptions.SaltMasterError('MySQL returner could not connect to database: {exc}'.format(exc=exc))
+
+ cursor = conn.cursor()
+diff --git a/tests/unit/pillar/test_mysql.py b/tests/unit/pillar/test_mysql.py
+index 8d49ac24e2..b72988673d 100644
+--- a/tests/unit/pillar/test_mysql.py
++++ b/tests/unit/pillar/test_mysql.py
+@@ -12,7 +12,7 @@ import salt.pillar.mysql as mysql
+
+
+ @skipIf(NO_MOCK, NO_MOCK_REASON)
+-@skipIf(not mysql.HAS_MYSQL, 'MySQL-python module not installed')
++@skipIf(mysql.MySQLdb is None, 'MySQL-python module not installed')
+ class MysqlPillarTestCase(TestCase):
+ maxDiff = None
+
+--
+2.13.6
+
+
diff --git a/fix-for-errno-0-resolver-error-0-no-error-bsc-108758.patch b/fix-for-errno-0-resolver-error-0-no-error-bsc-108758.patch
new file mode 100644
index 0000000..87b161e
--- /dev/null
+++ b/fix-for-errno-0-resolver-error-0-no-error-bsc-108758.patch
@@ -0,0 +1,91 @@
+From b9cc71639d4e918ef14635124f6991917150de46 Mon Sep 17 00:00:00 2001
+From: Bo Maryniuk
+Date: Wed, 21 Mar 2018 11:10:23 +0100
+Subject: [PATCH] Fix for [Errno 0] Resolver Error 0 (no error)
+ (bsc#1087581)
+
+ * Lintfix: PEP8 ident
+ * Use proper levels of the error handling, use proper log formatting.
+ * Fix unit test for reversed fqdns return data
+---
+ salt/grains/core.py | 19 ++++++++++++-------
+ tests/unit/grains/test_core.py | 32 ++++++++++++++++++++++++++++++++
+ 2 files changed, 44 insertions(+), 7 deletions(-)
+
+diff --git a/salt/grains/core.py b/salt/grains/core.py
+index 17a7d9819a..cd9ba1f29c 100644
+--- a/salt/grains/core.py
++++ b/salt/grains/core.py
+@@ -1900,16 +1900,21 @@ def fqdns():
+ fqdns = set()
+
+ addresses = salt.utils.network.ip_addrs(include_loopback=False,
+- interface_data=_INTERFACES)
++ interface_data=_INTERFACES)
+ addresses.extend(salt.utils.network.ip_addrs6(include_loopback=False,
+- interface_data=_INTERFACES))
+-
++ interface_data=_INTERFACES))
++ err_message = 'Exception during resolving address: %s'
+ for ip in addresses:
+ try:
+- fqdns.add(socket.gethostbyaddr(ip)[0])
+- except (socket.error, socket.herror,
+- socket.gaierror, socket.timeout) as e:
+- log.info("Exception during resolving address: " + str(e))
++ fqdns.add(socket.getfqdn(socket.gethostbyaddr(ip)[0]))
++ except socket.herror as err:
++ if err.errno == 0:
++ # No FQDN for this IP address, so we don't need to know this all the time.
++ log.debug("Unable to resolve address %s: %s", ip, err)
++ else:
++ log.error(err_message, err)
++ except (socket.error, socket.gaierror, socket.timeout) as err:
++ log.error(err_message, err)
+
+ grains['fqdns'] = list(fqdns)
+ return grains
+diff --git a/tests/unit/grains/test_core.py b/tests/unit/grains/test_core.py
+index 47c9cdd35b..c604df6c57 100644
+--- a/tests/unit/grains/test_core.py
++++ b/tests/unit/grains/test_core.py
+@@ -784,3 +784,35 @@ SwapTotal: 4789244 kB'''
+ []}}
+ with patch.object(salt.utils.dns, 'parse_resolv', MagicMock(return_value=resolv_mock)):
+ assert core.dns() == ret
++
++ def _run_dns_test(self, resolv_mock, ret):
++ with patch.object(salt.utils, 'is_windows',
++ MagicMock(return_value=False)):
++ with patch.dict(core.__opts__, {'ipv6': False}):
++ with patch.object(salt.utils.dns, 'parse_resolv',
++ MagicMock(return_value=resolv_mock)):
++ get_dns = core.dns()
++ self.assertEqual(get_dns, ret)
++
++ @skipIf(not salt.utils.platform.is_linux(), 'System is not Linux')
++ @patch.object(salt.utils, 'is_windows', MagicMock(return_value=False))
++ @patch('salt.utils.network.ip_addrs', MagicMock(return_value=['1.2.3.4', '5.6.7.8']))
++ @patch('salt.utils.network.ip_addrs6',
++ MagicMock(return_value=['fe80::a8b2:93ff:fe00:0', 'fe80::a8b2:93ff:dead:beef']))
++ @patch('salt.utils.network.socket.getfqdn', MagicMock(side_effect=lambda v: v)) # Just pass-through
++ def test_fqdns_return(self):
++ '''
++ test the return for a dns grain. test for issue:
++ https://github.com/saltstack/salt/issues/41230
++ '''
++ reverse_resolv_mock = [('foo.bar.baz', [], ['1.2.3.4']),
++ ('rinzler.evil-corp.com', [], ['5.6.7.8']),
++ ('foo.bar.baz', [], ['fe80::a8b2:93ff:fe00:0']),
++ ('bluesniff.foo.bar', [], ['fe80::a8b2:93ff:dead:beef'])]
++ ret = {'fqdns': ['bluesniff.foo.bar', 'foo.bar.baz', 'rinzler.evil-corp.com']}
++ with patch.object(socket, 'gethostbyaddr', side_effect=reverse_resolv_mock):
++ fqdns = core.fqdns()
++ self.assertIn('fqdns', fqdns)
++ self.assertEqual(len(fqdns['fqdns']), len(ret['fqdns']))
++ self.assertEqual(set(fqdns['fqdns']), set(ret['fqdns']))
++
+--
+2.13.6
+
+
diff --git a/initialize-__context__-retcode-for-functions-handled.patch b/initialize-__context__-retcode-for-functions-handled.patch
new file mode 100644
index 0000000..b326f74
--- /dev/null
+++ b/initialize-__context__-retcode-for-functions-handled.patch
@@ -0,0 +1,27 @@
+From c374feb62af75dfe18e8c81fb9cb556d678487ce Mon Sep 17 00:00:00 2001
+From: =?UTF-8?q?Pablo=20Su=C3=A1rez=20Hern=C3=A1ndez?=
+
+Date: Tue, 24 Apr 2018 13:50:49 +0100
+Subject: [PATCH] Initialize __context__ retcode for functions handled
+ via schedule util module
+
+---
+ salt/utils/schedule.py | 1 +
+ 1 file changed, 1 insertion(+)
+
+diff --git a/salt/utils/schedule.py b/salt/utils/schedule.py
+index de057477a3..6cb3ce0ef8 100644
+--- a/salt/utils/schedule.py
++++ b/salt/utils/schedule.py
+@@ -701,6 +701,7 @@ class Schedule(object):
+ for global_key, value in six.iteritems(func_globals):
+ self.functions[mod_name].__globals__[global_key] = value
+
++ self.functions.pack['__context__']['retcode'] = 0
+ ret['return'] = self.functions[func](*args, **kwargs)
+
+ if not self.standalone:
+--
+2.15.1
+
+
diff --git a/provide-kwargs-to-pkg_resource.parse_targets-require.patch b/provide-kwargs-to-pkg_resource.parse_targets-require.patch
new file mode 100644
index 0000000..139352d
--- /dev/null
+++ b/provide-kwargs-to-pkg_resource.parse_targets-require.patch
@@ -0,0 +1,37 @@
+From f7af1739a5795de6f98cfe2856372c755711e6dc Mon Sep 17 00:00:00 2001
+From: Michael Calmer
+Date: Wed, 18 Apr 2018 17:19:18 +0200
+Subject: [PATCH] provide kwargs to pkg_resource.parse_targets required
+ to detect advisory type
+
+fix invalid string compare
+---
+ salt/modules/yumpkg.py | 4 ++--
+ 1 file changed, 2 insertions(+), 2 deletions(-)
+
+diff --git a/salt/modules/yumpkg.py b/salt/modules/yumpkg.py
+index 39abb77fbc..9eb27e7701 100644
+--- a/salt/modules/yumpkg.py
++++ b/salt/modules/yumpkg.py
+@@ -1322,7 +1322,7 @@ def install(name=None,
+
+ try:
+ pkg_params, pkg_type = __salt__['pkg_resource.parse_targets'](
+- name, pkgs, sources, saltenv=saltenv, normalize=normalize
++ name, pkgs, sources, saltenv=saltenv, normalize=normalize, **kwargs
+ )
+ except MinionError as exc:
+ raise CommandExecutionError(exc)
+@@ -1620,7 +1620,7 @@ def install(name=None,
+ if _yum() == 'dnf':
+ cmd.extend(['--best', '--allowerasing'])
+ _add_common_args(cmd)
+- cmd.append('install' if pkg_type is not 'advisory' else 'update')
++ cmd.append('install' if pkg_type != 'advisory' else 'update')
+ cmd.extend(targets)
+ out = __salt__['cmd.run_all'](
+ cmd,
+--
+2.15.1
+
+
diff --git a/salt.changes b/salt.changes
index 8928018..088cd92 100644
--- a/salt.changes
+++ b/salt.changes
@@ -1,3 +1,44 @@
+-------------------------------------------------------------------
+Wed Apr 25 14:50:36 UTC 2018 - Pablo Suárez Hernández
+
+- Fix minion scheduler to return a 'retcode' attribute (bsc#1089112)
+- Fix for logging during network interface querying (bsc#1087581)
+- Fix rhel packages requires both net-tools and iproute (bsc#1087055)
+
+- Added:
+ * initialize-__context__-retcode-for-functions-handled.patch
+
+- Modified:
+ * fix-for-errno-0-resolver-error-0-no-error-bsc-108758.patch
+
+-------------------------------------------------------------------
+Wed Apr 18 17:09:41 UTC 2018 - Pablo Suárez Hernández
+
+- Fix patchinstall on yum module. Bad comparison (bsc#1087278)
+
+- Added:
+ * provide-kwargs-to-pkg_resource.parse_targets-require.patch
+
+-------------------------------------------------------------------
+Wed Apr 18 16:55:28 UTC 2018 - Pablo Suárez Hernández
+
+- Strip trailing commas on Linux user's GECOS fields (bsc#1089362)
+- Fallback to PyMySQL (bsc#1087891)
+- Improved test for fqdns
+- Update SaltSSH patch
+- Fix for [Errno 0] Resolver Error 0 (no error) (bsc#1087581)
+ * Lintfix: PEP8 ident
+ * Use proper levels of the error handling, use proper log formatting.
+ * Fix unit test for reversed fqdns return data
+
+- Added:
+ * strip-trailing-commas-on-linux-user-gecos-fields.patch
+ * fall-back-to-pymysql.patch
+ * fix-for-errno-0-resolver-error-0-no-error-bsc-108758.patch
+
+- Modified:
+ * add-saltssh-multi-version-support-across-python-inte.patch
+
-------------------------------------------------------------------
Fri Apr 6 16:58:59 UTC 2018 - Mihai Dinca
diff --git a/salt.spec b/salt.spec
index 2d67b34..48178f9 100644
--- a/salt.spec
+++ b/salt.spec
@@ -86,6 +86,17 @@ Patch12: make-it-possible-to-use-login-pull-and-push-from-mod.patch
Patch13: explore-module.run-response-to-catch-the-result-in-d.patch
# PATCH-FIX_UPSTREAM https://github.com/saltstack/salt/pull/46684
Patch14: add-saltssh-multi-version-support-across-python-inte.patch
+# PATCH-FIX_UPSTREAM https://github.com/saltstack/salt/pull/46635
+Patch15: fix-for-errno-0-resolver-error-0-no-error-bsc-108758.patch
+# PATCH-FIX_UPSTREAM https://github.com/saltstack/salt/pull/46890
+Patch16: fall-back-to-pymysql.patch
+# PATCH-FIX_UPSTREAM https://github.com/saltstack/salt/pull/47149
+Patch17: strip-trailing-commas-on-linux-user-gecos-fields.patch
+# PATCH-FIX_UPSTREAM https://github.com/saltstack/salt/pull/47155
+Patch18: provide-kwargs-to-pkg_resource.parse_targets-require.patch
+# PATCH-FIX_UPSTREAM https://github.com/saltstack/salt/pull/47270
+Patch19: initialize-__context__-retcode-for-functions-handled.patch
+
# BuildRoot: %{_tmppath}/%{name}-%{version}-build
BuildRoot: %{_tmppath}/%{name}-%{version}-build
@@ -116,12 +127,12 @@ Requires: procps
%if 0%{?suse_version} >= 1500
Requires: iproute2
%else
+%if 0%{?suse_version}
Requires: net-tools
-%endif
-
-%if 0%{?rhel}
+%else
Requires: iproute
%endif
+%endif
%if %{with systemd}
BuildRequires: systemd
@@ -558,6 +569,11 @@ cp %{S:5} ./.travis.yml
%patch12 -p1
%patch13 -p1
%patch14 -p1
+%patch15 -p1
+%patch16 -p1
+%patch17 -p1
+%patch18 -p1
+%patch19 -p1
%build
%if 0%{?build_py2}
diff --git a/strip-trailing-commas-on-linux-user-gecos-fields.patch b/strip-trailing-commas-on-linux-user-gecos-fields.patch
new file mode 100644
index 0000000..e82879b
--- /dev/null
+++ b/strip-trailing-commas-on-linux-user-gecos-fields.patch
@@ -0,0 +1,55 @@
+From f9fb3639bb3c44babd92d9499bdde83a0a81d6ab Mon Sep 17 00:00:00 2001
+From: =?UTF-8?q?Pablo=20Su=C3=A1rez=20Hern=C3=A1ndez?=
+
+Date: Wed, 18 Apr 2018 12:05:35 +0100
+Subject: [PATCH] Strip trailing commas on Linux user GECOS fields
+
+Add unit tests for GECOS fields
+---
+ salt/modules/useradd.py | 2 +-
+ tests/unit/modules/test_useradd.py | 18 ++++++++++++++++++
+ 2 files changed, 19 insertions(+), 1 deletion(-)
+
+diff --git a/salt/modules/useradd.py b/salt/modules/useradd.py
+index 545fe2a6f1..a61ba0e960 100644
+--- a/salt/modules/useradd.py
++++ b/salt/modules/useradd.py
+@@ -81,7 +81,7 @@ def _build_gecos(gecos_dict):
+ return '{0},{1},{2},{3}'.format(gecos_dict.get('fullname', ''),
+ gecos_dict.get('roomnumber', ''),
+ gecos_dict.get('workphone', ''),
+- gecos_dict.get('homephone', ''))
++ gecos_dict.get('homephone', '')).rstrip(',')
+
+
+ def _update_gecos(name, key, value, root=None):
+diff --git a/tests/unit/modules/test_useradd.py b/tests/unit/modules/test_useradd.py
+index eb983685bb..fa30a0df71 100644
+--- a/tests/unit/modules/test_useradd.py
++++ b/tests/unit/modules/test_useradd.py
+@@ -393,3 +393,21 @@ class UserAddTestCase(TestCase, LoaderModuleMockMixin):
+ mock = MagicMock(side_effect=[{'name': ''}, False, {'name': ''}])
+ with patch.object(useradd, 'info', mock):
+ self.assertFalse(useradd.rename('salt', 'salt'))
++
++ def test_build_gecos_field(self):
++ '''
++ Test if gecos fields are built correctly (removing trailing commas)
++ '''
++ test_gecos = {'fullname': 'Testing',
++ 'roomnumber': 1234,
++ 'workphone': 22222,
++ 'homephone': 99999}
++ expected_gecos_fields = 'Testing,1234,22222,99999'
++ self.assertEqual(useradd._build_gecos(test_gecos), expected_gecos_fields)
++ test_gecos.pop('roomnumber')
++ test_gecos.pop('workphone')
++ expected_gecos_fields = 'Testing,,,99999'
++ self.assertEqual(useradd._build_gecos(test_gecos), expected_gecos_fields)
++ test_gecos.pop('homephone')
++ expected_gecos_fields = 'Testing'
++ self.assertEqual(useradd._build_gecos(test_gecos), expected_gecos_fields)
+--
+2.15.1
+
+