Shared lib of small but usefull functions and other

This commit adds 'sharedlib' loader that allows to organize
functions into tree structure. Salt doesn't allow this as it
only imports top-level modules

https://github.com/saltstack/salt/issues/37273

See README for more details.

Change-Id: I7827c42f8f0d4caef56eff6352a49fe1a95a50cc
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()])
+