Merge "Revert "Fallback added for: pillar.galera.(master|slave).admin.user pillar.galera.(master|slave).admin.password""
diff --git a/README.rst b/README.rst
index 5cfa7ae..b89cb51 100644
--- a/README.rst
+++ b/README.rst
@@ -896,6 +896,86 @@
 .. literalinclude:: tests/pillar/control_virt_custom.sls
    :language: yaml
 
+Salt shared library
+-------------------
+
+This formula includes 'sharedlib' execution module which is a kind
+of 'library' of function and / or classes to be used in Jinja templates
+or directly as execution module.
+
+'sharedlib' implements a loader that is able to scan nested directories
+and import Python classes / functions from nested modules. Salt doesn't
+allow this as it only imports top-level modules:
+
+https://github.com/saltstack/salt/issues/37273
+
+'sharedlib' implements 4 main functions:
+
+* 'sharedlib.list' - search and print functions / classes found in nested directories
+* 'sharedlib.info' - print docstring of a function (if it exists)
+* 'sharedlib.get' - get function / class object, but not execute it immediately
+* 'sharedlib.call' - get function / class and execute / initialize it with
+  arguments given.
+
+Each of the commands above also have it's own docstring so it's possible to
+use them on a system:
+
+.. code-block:: text
+
+    # salt-call sys.doc sharedlib.list
+    local:
+      ----------
+      sharedlib.list:
+
+        List available functions.
+
+          .. code-block::
+
+            salt-call sharedlib.list
+
+Usage examples:
+
+.. code-block:: text
+
+    # salt-call sharedlib.list
+    local:
+      ----------
+      sharedlib.list:
+        ----------
+        classes:
+          - misc.Test
+          - misc2.Test
+        functions:
+          - misc.cast_dict_keys_to_int
+
+.. code-block:: text
+
+    # salt-call sharedlib.info misc.cast_dict_keys_to_int
+    local:
+      ----------
+      sharedlib.info:
+        ----------
+        misc.cast_dict_keys_to_int:
+
+          Return a dictionary with keys casted to int.
+          This usually is required when you want sort the dict later.
+
+          Jinja example:
+
+          .. code-block: jinja
+
+            {%- set ruleset = salt['sharedlib.call']('misc.cast_dict_keys_to_int', c.get('ruleset', {})) %}
+
+          .. code-block:: jinja
+
+            {%- set func = salt['sharedlib.get']('misc.cast_dict_keys_to_int') %}
+            {%- for c_name, c in t.chains.items() %}
+              {%- set ruleset = func(c.get('ruleset', {})) %}
+              {%- for rule_id, r in ruleset | dictsort %}
+              ...
+            {%- endfor %}
+
+
 Usage
 =====
 
diff --git a/_modules/sharedlib/__init__.py b/_modules/sharedlib/__init__.py
new file mode 100644
index 0000000..609d13c
--- /dev/null
+++ b/_modules/sharedlib/__init__.py
@@ -0,0 +1,110 @@
+from inspect import getmembers, isclass, isfunction
+from functools import wraps
+
+import importlib
+import pkgutil
+import sys
+
+
+class Loader(object):
+    def __init__(self, name):
+        self.name = name
+        self.loader = pkgutil.get_loader(self.name)
+        self.path = self.loader.filename
+        sys.path.append(self.path)
+
+    def get(self, name, *args, **kwargs):
+        parts = name.split('.')
+        module_name = '.'.join(parts[:-1])
+        module = importlib.import_module(module_name)
+        module.__salt__ = __salt__
+        return getattr(module, parts[-1])
+
+    def info(self, name, *args, **kwargs):
+        return {name: getattr(self.get(name), '__doc__')}
+
+    def call(self, name, *args, **kwargs):
+        return self.get(name)(*args, **kwargs)
+
+    def list(self, *args, **kwargs):
+        classes = set()
+        functions = set()
+        for _,name,ispkg in pkgutil.walk_packages([self.path,]):
+            if ispkg:
+                continue
+            try:
+                module = importlib.import_module(name)
+                for member_name,_ in getmembers(module, isfunction):
+                    functions.add('.'.join((name, member_name)))
+                for member_name,_ in getmembers(module, isclass):
+                    classes.add('.'.join((name, member_name)))
+            except:
+                pass
+        return {'functions': sorted(functions), 'classes': sorted(classes)}
+
+
+LOADER = Loader('sharedlib')
+
+
+def loader_attr(name):
+    def wrapper(f):
+        @wraps(f)
+        def func_wrapper(*args, **kwargs):
+            return getattr(LOADER, name)(*args, **kwargs)
+        return func_wrapper
+    return wrapper
+
+
+def wrap_output(f):
+    @wraps(f)
+    def wrapper(*args, **kwargs):
+        return {kwargs.get('__pub_fun'): f(*args, **kwargs)}
+    return wrapper
+
+
+@wrap_output
+@loader_attr('list')
+def list(*args, **kwargs):
+    '''
+    List available functions.
+
+    .. code-block:: text
+
+        salt-call sharedlib.list
+    '''
+
+@wrap_output
+@loader_attr('info')
+def info(name, *args, **kwargs):
+    '''
+    Returns info about function.
+
+    .. code-block:: text
+
+        salt-call sharedlib.info function.name
+    '''
+
+@loader_attr('get')
+def get(name):
+    '''
+    Returns function / class object (but not calls it) for later use.
+    This is mostly useful in jinja templates.
+
+    .. code-block:: jinja
+
+        {%- set func = salt['sharedlib.get']('function.name') %}
+        {%- func(*args, **kwargs) %}
+    '''
+
+@loader_attr('call')
+def call(name, *args, **kwargs):
+    '''
+    Calls a function from library.
+
+    For more information about a function being called use 'sharedlib.info'.
+
+    .. code-block:: jinja
+
+        {%- set x = salt['sharedlib.call']('function.name', *args, **kwargs) %}
+    '''
+
diff --git a/_modules/sharedlib/misc.py b/_modules/sharedlib/misc.py
new file mode 100644
index 0000000..5a6f5c4
--- /dev/null
+++ b/_modules/sharedlib/misc.py
@@ -0,0 +1,22 @@
+def cast_dict_keys_to_int(x, *args, **kwargs):
+    '''
+    Return a dictionary with keys casted to int.
+    This usually is required when you want sort the dict later.
+
+    Jinja examples:
+
+    .. code-block: jinja
+
+        {%- set ruleset = salt['sharedlib.call']('misc.cast_dict_keys_to_int', c.get('ruleset', {})) %}
+
+    .. code-block:: jinja
+
+      {%- set func = salt['sharedlib.get']('misc.cast_dict_keys_to_int') %}
+      {%- for c_name, c in t.chains.items() %}
+        {%- set ruleset = func(c.get('ruleset', {})) %}
+        {%- for rule_id, r in ruleset | dictsort %}
+        ...
+      {%- endfor %}
+    '''
+    return dict([(int(key),value) for key,value in x.items()])
+