diff --git a/plugins/module_utils/ansible_freeipa_module.py b/plugins/module_utils/ansible_freeipa_module.py
index b8df38d4f6bc6fe21dc4af8bedab93cd728cbdd2..9c10135f4c86a842e098f23393a0adbbcbd27b97 100644
--- a/plugins/module_utils/ansible_freeipa_module.py
+++ b/plugins/module_utils/ansible_freeipa_module.py
@@ -2,6 +2,7 @@
 # -*- coding: utf-8 -*-
 
 # Authors:
+#   Sergio Oliveira Campos <seocam@redhat.com>
 #   Thomas Woerner <twoerner@redhat.com>
 #
 # Copyright (C) 2019  Red Hat
@@ -27,10 +28,12 @@ import tempfile
 import shutil
 import gssapi
 from datetime import datetime
+from pprint import pformat
 from ipalib import api
-from ipalib import errors as ipalib_errors
+from ipalib import errors as ipalib_errors  # noqa
 from ipalib.config import Env
 from ipalib.constants import DEFAULT_CONFIG, LDAP_GENERALIZED_TIME_FORMAT
+
 try:
     from ipalib.install.kinit import kinit_password, kinit_keytab
 except ImportError:
@@ -38,7 +41,9 @@ except ImportError:
 from ipapython.ipautil import run
 from ipaplatform.paths import paths
 from ipalib.krb_utils import get_credentials_if_valid
+from ansible.module_utils.basic import AnsibleModule
 from ansible.module_utils._text import to_text
+
 try:
     from ipalib.x509 import Encoding
 except ImportError:
@@ -52,7 +57,7 @@ if six.PY3:
     unicode = str
 
 
-def valid_creds(module, principal):
+def valid_creds(module, principal):  # noqa
     """
     Get valid credintials matching the princial, try GSSAPI first
     """
@@ -205,9 +210,24 @@ def date_format(value):
     raise ValueError("Invalid date '%s'" % value)
 
 
-def compare_args_ipa(module, args, ipa):
+def compare_args_ipa(module, args, ipa):  # noqa
+    """Compare IPA obj attrs with the command args.
+
+    This function compares IPA objects attributes with the args the
+    module is intending to use to call a command. This is useful to know
+    if call to IPA server will be needed or not.
+    In other to compare we have to prepare the perform slight changes in
+    data formats.
+
+    Returns True if they are the same and False otherwise.
+    """
+    base_debug_msg = "Ansible arguments and IPA commands differed. "
+
     for key in args.keys():
         if key not in ipa:
+            module.debug(
+                base_debug_msg + "Command key not present in IPA: %s" % key
+            )
             return False
         else:
             arg = args[key]
@@ -220,25 +240,35 @@ def compare_args_ipa(module, args, ipa):
             if isinstance(ipa_arg, list):
                 if not isinstance(arg, list):
                     arg = [arg]
+                if len(ipa_arg) != len(arg):
+                    module.debug(
+                        base_debug_msg
+                        + "List length doesn't match for key %s: %d %d"
+                        % (key, len(arg), len(ipa_arg),)
+                    )
+                    return False
                 if isinstance(ipa_arg[0], str) and isinstance(arg[0], int):
                     arg = [to_text(_arg) for _arg in arg]
                 if isinstance(ipa_arg[0], unicode) and isinstance(arg[0], int):
                     arg = [to_text(_arg) for _arg in arg]
-            # module.warn("%s <=> %s" % (repr(arg), repr(ipa_arg)))
             try:
                 arg_set = set(arg)
                 ipa_arg_set = set(ipa_arg)
             except TypeError:
                 if arg != ipa_arg:
-                    # module.warn("%s != %s" % (repr(arg), repr(ipa_arg)))
+                    module.debug(
+                        base_debug_msg
+                        + "Different values: %s %s" % (arg, ipa_arg)
+                    )
                     return False
             else:
                 if arg_set != ipa_arg_set:
-                    # module.warn("%s != %s" % (repr(arg), repr(ipa_arg)))
+                    module.debug(
+                        base_debug_msg
+                        + "Different set content: %s %s"
+                        % (arg_set, ipa_arg_set,)
+                    )
                     return False
-
-        # module.warn("%s == %s" % (repr(arg), repr(ipa_arg)))
-
     return True
 
 
@@ -289,6 +319,16 @@ def encode_certificate(cert):
     return encoded
 
 
+def is_valid_port(port):
+    if not isinstance(port, int):
+        return False
+
+    if 1 <= port <= 65535:
+        return True
+
+    return False
+
+
 def is_ipv4_addr(ipaddr):
     """
     Test if figen IP address is a valid IPv4 address
@@ -309,3 +349,294 @@ def is_ipv6_addr(ipaddr):
     except socket.error:
         return False
     return True
+
+
+class AnsibleFreeIPAParams(dict):
+    def __init__(self, ansible_module):
+        self.update(ansible_module.params)
+        self.ansible_module = ansible_module
+
+    @property
+    def names(self):
+        return self.name
+
+    def __getattr__(self, name):
+        param = self.get(name)
+        if param is not None:
+            return _afm_convert(param)
+
+
+class FreeIPABaseModule(AnsibleModule):
+    """
+    Base class for FreeIPA Ansible modules.
+
+    Provides methods useful methods to be used by our modules.
+
+    This class should be overriten and instantiated for the module.
+    A basic implementation of an Ansible FreeIPA module expects its
+    class to:
+
+    1. Define a class attribute ``ipa_param_mapping``
+    2. Implement the method ``define_ipa_commands()``
+    3. Implement the method ``check_ipa_params()`` (optional)
+
+    After instantiating the class the method ``ipa_run()`` should be called.
+
+    Example (ansible-freeipa/plugins/modules/ipasomemodule.py):
+
+    class SomeIPAModule(FreeIPABaseModule):
+        ipa_param_mapping = {
+            "arg_to_be_passed_to_ipa_command": "module_param",
+            "another_arg": "get_another_module_param",
+        }
+
+        def get_another_module_param(self):
+            another_module_param = self.ipa_params.another_module_param
+            # Validate or modify another_module_param
+            # ...
+            return another_module_param
+
+        def check_ipa_params(self):
+            # Validate your params here
+            # Example:
+            if not self.ipa_params.module_param in VALID_OPTIONS:
+                self.fail_json(msg="Invalid value for argument module_param")
+
+        def define_ipa_commands(self):
+            args = self.get_ipa_command_args()
+
+            self.add_ipa_command(
+                "some_ipa_command",
+                name="obj-name",
+                args=args,
+            )
+
+    def main():
+        ipa_module = SomeIPAModule(argument_spec=dict(
+            module_param=dict(
+                type="str",
+                default=None,
+                required=False,
+            ),
+            another_module_param=dict(
+                type="str",
+                default=None,
+                required=False,
+            ),
+        ))
+        ipa_module.ipa_run()
+
+    if __name__ == "__main__":
+        main()
+
+    """
+
+    ipa_param_mapping = None
+
+    def __init__(self, *args, **kwargs):
+        super(FreeIPABaseModule, self).__init__(*args, **kwargs)
+
+        # Attributes to store kerberos credentials (if needed)
+        self.ccache_dir = None
+        self.ccache_name = None
+
+        # Status of an execution. Will be changed to True
+        #   if something is actually peformed.
+        self.changed = False
+
+        # Status of the connection with the IPA server.
+        # We need to know if the connection was actually stablished
+        #   before we start sending commands.
+        self.ipa_connected = False
+
+        # Commands to be executed
+        self.ipa_commands = []
+
+        # Module exit arguments.
+        self.exit_args = {}
+
+        # Wrapper around the AnsibleModule.params.
+        # Return the actual params but performing transformations
+        #   when needed.
+        self.ipa_params = AnsibleFreeIPAParams(self)
+
+    def get_ipa_command_args(self):
+        """
+        Return a dict to be passed to an IPA command.
+
+        The keys of ``ipa_param_mapping`` are also the keys of the return dict.
+
+        The values of ``ipa_param_mapping`` needs to be either:
+            * A str with the name of a defined method; or
+            * A key of ``AnsibleModule.param``.
+
+        In case of a method the return of the method will be set as value
+        for the return dict.
+
+        In case of a AnsibleModule.param the value of the param will be
+        set in the return dict. In addition to that boolean values will be
+        automaticaly converted to uppercase strings (as required by FreeIPA
+        server).
+
+        """
+        args = {}
+        for ipa_param_name, param_name in self.ipa_param_mapping.items():
+
+            # Check if param_name is actually a param
+            if param_name in self.ipa_params:
+                value = self.ipa_params.get(param_name)
+                if isinstance(value, bool):
+                    value = "TRUE" if value else "FALSE"
+
+            # Since param wasn't a param check if it's a method name
+            elif hasattr(self, param_name):
+                method = getattr(self, param_name)
+                if callable(method):
+                    value = method()
+
+            # We don't have a way to guess the value so fail.
+            else:
+                self.fail_json(
+                    msg=(
+                        "Couldn't get a value for '%s'. Option '%s' is not "
+                        "a module argument neither a defined method."
+                    )
+                    % (ipa_param_name, param_name)
+                )
+
+            if value is not None:
+                args[ipa_param_name] = value
+
+        return args
+
+    def check_ipa_params(self):
+        """Validate ipa_params before command is called."""
+        pass
+
+    def define_ipa_commands(self):
+        """Define commands that will be run in IPA server."""
+        raise NotImplementedError
+
+    def api_command(self, command, name=None, args=None):
+        """Execute a single command in IPA server."""
+        if args is None:
+            args = {}
+
+        if name is None:
+            return api_command_no_name(self, command, args)
+
+        return api_command(self, command, name, args)
+
+    def __enter__(self):
+        """
+        Connect to IPA server.
+
+        Check the there are working Kerberos credentials to connect to
+        IPA server. If there are not we perform a temporary kinit
+        that will be terminated when exiting the context.
+
+        If the connection fails ``ipa_connected`` attribute will be set
+        to False.
+        """
+        principal = self.ipa_params.ipaadmin_principal
+        password = self.ipa_params.ipaadmin_password
+
+        try:
+            if not valid_creds(self, principal):
+                self.ccache_dir, self.ccache_name = temp_kinit(
+                    principal, password,
+                )
+
+            api_connect()
+
+        except Exception as excpt:
+            self.fail_json(msg=str(excpt))
+        else:
+            self.ipa_connected = True
+
+        return self
+
+    def __exit__(self, exc_type, exc_val, exc_tb):
+        """
+        Terminate a connection with the IPA server.
+
+        Deal with exceptions, destroy temporary kinit credentials and
+        exit the module with proper arguments.
+
+        """
+        if exc_val:
+            self.fail_json(msg=str(exc_val))
+
+        # TODO: shouldn't we also disconnect from api backend?
+        temp_kdestroy(self.ccache_dir, self.ccache_name)
+
+        self.exit_json(changed=self.changed, user=self.exit_args)
+
+    def get_command_errors(self, command, result):
+        """Look for erros into command results."""
+        # Get all errors
+        # All "already a member" and "not a member" failures in the
+        # result are ignored. All others are reported.
+        errors = []
+        for item in result.get("failed", tuple()):
+            failed_item = result["failed"][item]
+            for member_type in failed_item:
+                for member, failure in failed_item[member_type]:
+                    if (
+                        "already a member" in failure
+                        or "not a member" in failure
+                    ):
+                        continue
+                    errors.append(
+                        "%s: %s %s: %s"
+                        % (command, member_type, member, failure)
+                    )
+
+        if len(errors) > 0:
+            self.fail_json(", ".join("errors"))
+
+    def add_ipa_command(self, command, name=None, args=None):
+        """Add a command to the list of commands to be executed."""
+        self.ipa_commands.append((name, command, args or {}))
+
+    def _run_ipa_commands(self):
+        """Execute commands in self.ipa_commands."""
+        result = None
+
+        for name, command, args in self.ipa_commands:
+            try:
+                result = self.api_command(command, name, args)
+            except Exception as excpt:
+                self.fail_json(msg="%s: %s: %s" % (command, name, str(excpt)))
+            else:
+                if "completed" in result:
+                    if result["completed"] > 0:
+                        self.changed = True
+                else:
+                    self.changed = True
+
+            self.get_command_errors(command, result)
+
+    def require_ipa_attrs_change(self, command_args, ipa_attrs):
+        """
+        Compare given args with current object attributes.
+
+        Returns True in case current IPA object attributes differ from
+        args passed to the module.
+        """
+        equal = compare_args_ipa(self, command_args, ipa_attrs)
+        return not equal
+
+    def pdebug(self, value):
+        """Debug with pretty formatting."""
+        self.debug(pformat(value))
+
+    def ipa_run(self):
+        """Execute module actions."""
+        with self:
+            if not self.ipa_connected:
+                return
+
+            self.check_ipa_params()
+            self.define_ipa_commands()
+            self._run_ipa_commands()