From 38d7223376efe03ffd46317bf37b03b7266fe1b1 Mon Sep 17 00:00:00 2001
From: Florence Blanc-Renaud <flo@redhat.com>
Date: Thu, 10 Aug 2017 16:54:44 +0200
Subject: [PATCH] Modify ipahost module: the authentication is done locally on
 the controller node and the credential cache is copied to the managed node

ipahost module is also using facts gathered from the server to find the
domain and realm.
---
 action_plugins/ipahost.py         | 215 ++++++++++++++++++++++++++----
 action_plugins/ipahost.pyc        | Bin 2096 -> 0 bytes
 inventory/hosts                   |  12 +-
 library/ipa_facts.py              | 175 ++++++++++++++++++++++++
 library/ipaclient.py              |   6 +-
 library/ipahost.py                |  25 ++--
 roles/ipaclient/tasks/install.yml |  42 ++++--
 roles/ipaclient/vars/default.yml  |   2 +-
 roles/ipaclient/vars/rhel.yml     |   2 +-
 site.yml                          |  11 --
 10 files changed, 427 insertions(+), 63 deletions(-)
 delete mode 100644 action_plugins/ipahost.pyc
 create mode 100644 library/ipa_facts.py

diff --git a/action_plugins/ipahost.py b/action_plugins/ipahost.py
index 3854dcec..d4bd4b02 100644
--- a/action_plugins/ipahost.py
+++ b/action_plugins/ipahost.py
@@ -17,61 +17,226 @@
 # You should have received a copy of the GNU General Public License
 # along with this program.  If not, see <http://www.gnu.org/licenses/>.
 
+import gssapi
 import os
+import shutil
+import subprocess
+import tempfile
+from jinja2 import Template
 
 from ansible.errors import AnsibleError
 from ansible.module_utils._text import to_native
 from ansible.plugins.action import ActionBase
 
+try:
+    from __main__ import display
+except ImportError:
+    from ansible.utils.display import Display
+    display = Display()
+
+def run_cmd(args, stdin=None):
+    """
+    Execute an external command.
+    """
+    p_in = None
+    p_out = subprocess.PIPE
+    p_err = subprocess.PIPE
+
+    if stdin:
+        p_in = subprocess.PIPE
+
+    p = subprocess.Popen(args, stdin=p_in, stdout=p_out, stderr=p_err,
+                         close_fds=True)
+    stdout, stderr = p.communicate(stdin)
+
+    return p.returncode
+
+
+def kinit_password(principal, password, ccache_name, config):
+    """
+    Perform kinit using principal/password, with the specified config file
+    and store the TGT in ccache_name.
+    """
+    args = [ "/usr/bin/kinit", principal, '-c', ccache_name]
+    old_config = os.environ.get('KRB5_CONFIG')
+    os.environ['KRB5_CONFIG'] = config
+
+    try:
+        result = run_cmd(args, stdin=password)
+        return result
+    finally:
+        if old_config is not None:
+            os.environ['KRB5_CONFIG'] = old_config
+        else:
+            os.environ.pop('KRB5_CONFIG', None)
+
+
+def kinit_keytab(principal, keytab, ccache_name, config):
+    """
+    Perform kinit using principal/keytab, with the specified config file
+    and store the TGT in ccache_name.
+    """
+    old_config = os.environ.get('KRB5_CONFIG')
+    os.environ['KRB5_CONFIG'] = config
+    try:
+        name = gssapi.Name(principal, gssapi.NameType.kerberos_principal)
+        store = {'ccache': ccache_name,
+                 'client_keytab': keytab}
+        cred = gssapi.Credentials(name=name, store=store, usage='initiate')
+        return cred
+    finally:
+        if old_config is not None:
+            os.environ['KRB5_CONFIG'] = old_config
+        else:
+            os.environ.pop('KRB5_CONFIG', None)
+
+
+KRB5CONF_TEMPLATE = """
+[logging]
+ default = FILE:/var/log/krb5libs.log
+ kdc = FILE:/var/log/krb5kdc.log
+ admin_server = FILE:/var/log/kadmind.log
+
+[libdefaults]
+ default_realm = {{ ipa_realm }}
+ dns_lookup_realm = false
+ dns_lookup_kdc = true
+ rdns = false
+ ticket_lifetime = {{ ipa_lifetime }}
+ forwardable = true
+ udp_preference_limit = 0
+ default_ccache_name = KEYRING:persistent:%{uid}
+
+[realms]
+ {{ ipa_realm }} = {
+  kdc = {{ ipa_server }}:88
+  master_kdc = {{ ipa_server }}:88
+  admin_server = {{ ipa_server }}:749
+  default_domain = {{ ipa_domain }}
+}
+
+[domain_realm]
+ .{{ ipa_domain }} = {{ ipa_realm }}
+ {{ ipa_domain }} = {{ ipa_realm}}
+"""
+
 class ActionModule(ActionBase):
+
     def run(self, tmp=None, task_vars=None):
         """
-        handler for file transfer operations
+        handler for credential cache transfer
 
         ipa* commands can either provide a password or a keytab file
         in order to authenticate on the managed node with Kerberos.
-        When a keytab is provided, it needs to be copied from the control
-        node to the managed node.
-        This Action Module performs the copy when needed.
+        The module is using these credentials to obtain a TGT locally on the
+        control node:
+        - need to create a krb5.conf Kerberos client configuration that is
+        using IPA server
+        - set the environment variable KRB5_CONFIG to point to this conf file
+        - set the environment variable KRB5CCNAME to use a specific cache
+        - perform kinit on the control node
+        This command creates the credential cache file
+        - copy the credential cache file on the managed node
+
+        Then the IPA commands can use this credential cache file.
         """
 
         if task_vars is None:
             task_vars = dict()
 
         result = super(ActionModule, self).run(tmp, task_vars)
+        principal = self._task.args.get('principal', None)
         keytab = self._task.args.get('keytab', None)
         password = self._task.args.get('password', None)
+        lifetime = self._task.args.get('lifetime', '1h')
 
-        if (keytab is None and password is None):
+        if (not keytab and not password):
             result['failed'] = True
             result['msg'] = "keytab or password is required"
             return result
 
-        # If password is supplied, just need to execute the module
-        if password:
-            result.update(self._execute_module(task_vars=task_vars))
+        if not principal:
+            result['failed'] = True
+            result['msg'] = "principal is required"
             return result
 
-        # Password not supplied, need to transfer the keytab file
-        # Check if the source keytab exists
+        data = self._execute_module(module_name='ipa_facts', module_args=dict(),
+                                    task_vars=None)
         try:
-            keytab = self._find_needle('files', keytab)
-        except AnsibleError as e:
+            domain = data['ansible_facts']['ipa']['domain']
+            realm = data['ansible_facts']['ipa']['realm']
+        except KeyError:
             result['failed'] = True
-            result['msg'] = to_native(e)
+            result['msg'] = "The host is not an IPA server"
             return result
 
-        # Create the remote tmp dir
-        tmp = self._make_tmp_path()
-        tmp_keytab = self._connection._shell.join_path(
-            tmp, os.path.basename(keytab))
-        self._transfer_file(keytab, tmp_keytab)
-        self._fixup_perms2((tmp, tmp_keytab))
+        items = principal.split('@')
+        if len(items) < 2:
+            principal = str('%s@%s' % (principal, realm))
 
-        new_module_args = self._task.args.copy()
-        new_module_args.update(dict(keytab=tmp_keytab))
+        # Locally create a temp directory to store krb5.conf and ccache
+        local_temp_dir = tempfile.mkdtemp()
+        krb5conf_name = os.path.join(local_temp_dir, 'krb5.conf')
+        ccache_name = os.path.join(local_temp_dir, 'ccache')
 
-        # Execute module
-        result.update(self._execute_module(module_args=new_module_args, task_vars=task_vars))
-        self._remove_tmp_path(tmp)
-        return result
+        # Create the krb5.conf from the template
+        template = Template(KRB5CONF_TEMPLATE)
+        content = template.render(dict(
+            ipa_server=task_vars['ansible_host'],
+            ipa_domain=domain,
+            ipa_realm=realm,
+            ipa_lifetime=lifetime))
+
+        with open(krb5conf_name, 'w') as f:
+            f.write(content)
+
+        if password:
+            # perform kinit -c ccache_name -l 1h principal
+            res = kinit_password(principal, password, ccache_name,
+                                 krb5conf_name)
+            if res:
+                result['failed'] = True
+                result['msg'] = 'kinit %s with password failed' % principal
+                return result
+
+        else:
+            # Password not supplied, need to use the keytab file
+            # Check if the source keytab exists
+            try:
+                keytab = self._find_needle('files', keytab)
+            except AnsibleError as e:
+                result['failed'] = True
+                result['msg'] = to_native(e)
+                return result
+            # perform kinit -kt keytab
+            try:
+                kinit_keytab(principal, keytab, ccache_name, krb5conf_name)
+            except Exception as e:
+                result['failed'] = True
+                result['msg'] = 'kinit %s with keytab %s failed' % (principal, keytab)
+                return result
+
+        try:
+            # Create the remote tmp dir
+            tmp = self._make_tmp_path()
+            tmp_ccache = self._connection._shell.join_path(
+                tmp, os.path.basename(ccache_name))
+
+            # Copy the ccache to the remote tmp dir
+            self._transfer_file(ccache_name, tmp_ccache)
+            self._fixup_perms2((tmp, tmp_ccache))
+
+            new_module_args = self._task.args.copy()
+            new_module_args.pop('password', None)
+            new_module_args.pop('keytab', None)
+            new_module_args.pop('lifetime', None)
+            new_module_args.update(ccache=tmp_ccache)
+
+            # Execute module
+            result.update(self._execute_module(module_args=new_module_args,
+                                               task_vars=task_vars))
+            return result
+        finally:
+            # delete the local temp directory
+            shutil.rmtree(local_temp_dir, ignore_errors=True)
+            run_cmd(['/usr/bin/kdestroy', '-c', tmp_ccache])
diff --git a/action_plugins/ipahost.pyc b/action_plugins/ipahost.pyc
deleted file mode 100644
index 5ef86f653d89033f779da0dc8329b5e900761cf5..0000000000000000000000000000000000000000
GIT binary patch
literal 0
HcmV?d00001

literal 2096
zcmZSn%**wXFDx>d0ScHI7#JKF7#NDf7#J8*7#LC*8FCmHq9AN0h8#wQTqcGnCI&{3
z95X`>Gea&5Llg^y&%%(y%8<*(5XHv8n8M1C%Epk!$dJOu(89pb%)k&C#SStqSc9E`
zfg$t%|NsB}G#D8e7)p2;7#JM$iZhdPQeBIR@{1s%oFJ+E_`JlD%(7I75En>ca!F=>
zo>O9RDnuWc>zkianv;s+2`7*z#2FYEQW+Sc7(o`NFoHbP!o(28%n%f;!NkD8kPPxJ
z$Raid1_o!4jungy3^hy)DVz-191KMg3?+;pr`Iwu<S{UmFfo)cGt@9JG&3<|u`tvy
zf>bs$G1P(uvsf9j*ch_FeoWy6si|cFDMpBcjRmP^WvF3+SuvTRh84js;b5p?W=LUX
zut{M7vsf8yYM2><YgieIwt)n)IKgIefr7P$iJ_T+v6+#95u~J23@pRVkirf!9%K&-
zLwKGZ14{`HLpB#f(H{oJS~dnogcT*c3^i;F&5R&(Q=k?_)G#n)@qzu!2sV$OAxnTE
zOAzGS5+Q~xVTLRbu<?vwAF_khh=N3F*g+zUApK$>)ivx4X-pu~!9tM05eFO0!BE4&
zki`iK+Z09ykUqFC5Z(hTN?`*BF#{9WlMD=55+IYo9A<`K4N!V0PGe+X;8K8sjKsW@
zoYW$PwEQB4w9K4Tg_5GgyyCRfB8B{d)S^UC(kbSGtIjM))KW;!&&^HDODR@JPRvtC
z%`C}CEm9~b$}h`INmWQxC`c?WF3&GYQOGY+NL0v9tt?4Q0-KC5Bu^o~C?&N>p(I}+
zv9u&3HLoNyIk6;FAwN%{BqLQJH!&|UJvBukFFz$!p**uBL%}<>C@HllzgQ1$MtDYQ
z9>Vy{VweL`bQCg66!KD2Q;I>xC#5PR=NDwArYNKp<>!J;PtMOPDay}*n*_F{Bp)n?
zY%{{Hkc`Y?1xIkUQGnzdg@V+gwEUvnVz92{{DMk_a*%^S_NJyFbe4c}2_y(gI2afh
zU||CmPfN_qNlhu1V_;x_C;^2SOoc*bu|iR5VQFSjYDx(x&*c`UmvDkIcw%vOd|6^q
zaS1B}0|O|=ic7c|7#MQF89F|(D81NELlUHrg@J*=FF!A}1jI<mOfCWIC@w8XEeZme
z2sXB;G!HBsUy@jy4OR&<0W6iCS^^dbDJo3`JGitUC9x#6gr9+dAwIPtHMz7TH6CIu
zxL}D-%gjrO2L(Y+Y7nS!3<8%`d?4kyiP@>~CAkIh1&JjYVE4u+=jY|6f&&$7czkh2
zYEBN=i&^=Zc@Tx55+}bH><X|(P#7j97N_PV=BAc_4Th!Gcu+``@Ph10%d99Zh%ZPj
z$}KhmYX!wmi2z8dC^a{~4B-$BaDak>r#Lky4eaTX+=3vGEnqhlr52awlmvkk1c5@k
z1mvOA5>O@r`2`Yd;PA*xEr$jLI0iI8-Uk!KpfXZFBR@A)KdmS=FR?TQM5ksJB<dFz
zCF>_b3US@EqExV0A~-<e3vx=+GxLh|GYb+k@{3FK3Mzv@zLf@*w-z9?xp^3Qn4}pc
z7$uk_7`Yg^7+ILv7$q55SeO~b82K5w7zG$vnRys_7$q157=@XH7{!?78F?A~G(dg<
zC&l=9P$0&~gMAYZb9{UdC@q0413Mo<P|YKtq$dUP2q<#d7&Snl9Rv~pM<_Hv^io09
zS}`~om4GB+;*j(fUs{rxQ>+(Xl3Gy$No8UT3=A+e(9qL^1ZEJ(9&mz3cm~X(mXE-}
XX#;VXoe0QhATRMS2`~yU3NQfxf(r2!

diff --git a/inventory/hosts b/inventory/hosts
index a3a3ccf4..552db14b 100644
--- a/inventory/hosts
+++ b/inventory/hosts
@@ -9,7 +9,13 @@ ipaclient_domain=ipadomain.com
 ipaclient_realm=IPADOMAIN.COM
 ipaclient_server=ipaserver.ipadomain.com
 ipaclient_extraargs=[ '--kinit-attempts=3', '--mkhomedir']
+# if neither ipaclient_password nor ipaclient_keytab is defined,
+# the enrollement will create a OneTime Password and enroll with this OTP
+# In this case ipaserver_password or ipaserver_keytab is required
+#ipaclient_principal=admin
+#ipaclient_password=SecretPassword123
+#ipaclient_keytab=/tmp/krb5.keytab
+ipaserver_principal=admin
+#ipaserver_password=SecretPassword123
+ipaserver_keytab=files/admin.keytab
 
-[ipaservers:vars]
-ipa_admin=admin
-ipa_password=MySecretPassword123
diff --git a/library/ipa_facts.py b/library/ipa_facts.py
new file mode 100644
index 00000000..625387fd
--- /dev/null
+++ b/library/ipa_facts.py
@@ -0,0 +1,175 @@
+#!/usr/bin/python
+# -*- coding: utf-8 -*-
+
+import os
+import re
+import six
+from six.moves.configparser import RawConfigParser
+
+from ansible.module_utils.basic import AnsibleModule
+
+try:
+    from ipalib import api
+except ImportError:
+    HAS_IPALIB = False
+else:
+    HAS_IPALIB = True
+    from ipaplatform.paths import paths
+    try:
+        # FreeIPA >= 4.5
+        from ipalib.install import sysrestore
+    except ImportError:
+        # FreeIPA 4.4 and older
+        from ipapython import sysrestore
+
+try:
+    import ipaserver
+except ImportError:
+    HAS_IPASERVER = False
+else:
+    HAS_IPASERVER = True
+
+SERVER_SYSRESTORE_STATE = "/var/lib/ipa/sysrestore/sysrestore.state"
+NAMED_CONF = "/etc/named.conf"
+VAR_LIB_PKI_TOMCAT = "/var/lib/pki/pki-tomcat"
+
+
+def is_ntpd_configured():
+    # ntpd is configured when sysrestore.state contains the line
+    # [ntpd]
+    ntpd_conf_section = re.compile('^\s*\[ntpd\]\s*$')
+
+    try:
+        with open(SERVER_SYSRESTORE_STATE) as f:
+            for line in f.readlines():
+                if ntpd_conf_section.match(line):
+                    return True
+        return False
+    except IOError:
+        return False
+
+def is_dns_configured():
+    # dns is configured when /etc/named.conf contains the line
+    # dyndb "ipa" "/usr/lib64/bind/ldap.so" {
+    bind_conf_section = re.compile('^\s*dyndb\s+"ipa"\s+"[^"]+"\s+{$')
+
+    try:
+        with open(NAMED_CONF) as f:
+            for line in f.readlines():
+                if bind_conf_section.match(line):
+                    return True
+        return False
+    except IOError:
+        return False
+
+def is_dogtag_configured(subsystem):
+    # ca / kra is configured when the directory /var/lib/pki/pki-tomcat/[ca|kra]
+    # exists
+    available_subsystems = { 'ca', 'kra' }
+    assert subsystem in available_subsystems
+
+    return os.path.isdir(os.path.join(VAR_LIB_PKI_TOMCAT, subsystem))
+
+def is_ca_configured():
+    return is_dogtag_configured('ca')
+
+def is_kra_configured():
+    return is_dogtag_configured('kra')
+
+def is_client_configured():
+    # IPA Client is configured when /etc/ipa/default.conf exists
+    # and /var/lib/ipa-client/sysrestore/sysrestore.state exists
+
+    fstore = sysrestore.FileStore(paths.IPA_CLIENT_SYSRESTORE)
+    return (os.path.isfile(paths.IPA_DEFAULT_CONF) and fstore.has_files())
+
+def is_server_configured():
+    # IPA server is configured when /etc/ipa/default.conf exists
+    # and /var/lib/ipa/sysrestore/sysrestore.state exists
+    return (os.path.isfile(paths.IPA_DEFAULT_CONF) and
+            os.path.isfile(SERVER_SYSRESTORE_STATE))
+
+def get_ipa_conf():
+    # Extract basedn, realm and domain from /etc/ipa/default.conf
+    parser = RawConfigParser()
+    parser.read(paths.IPA_DEFAULT_CONF)
+    basedn = parser.get('global', 'basedn')
+    realm = parser.get('global', 'realm')
+    domain = parser.get('global', 'domain')
+    return dict(
+        basedn=basedn,
+        realm=realm,
+        domain=domain
+        )
+
+def get_ipa_version():
+    try:
+        from ipapython import version
+    except ImportError:
+        return None
+    else:
+        version_info = []
+        for part in version.VERSION.split('.'):
+            # DEV versions look like:
+            # 4.4.90.201610191151GITd852c00
+            # 4.4.90.dev201701071308+git2e43db1
+            if part.startswith('dev') or 'GIT' in part:
+                version_info.append(part)
+            else:
+                version_info.append(int(part))
+
+        return dict(
+            api_version=version.API_VERSION,
+            num_version=version.NUM_VERSION,
+            vendor_version=version.VENDOR_VERSION,
+            version=version.VERSION,
+            version_info=version_info
+            )
+        
+def main():
+    module = AnsibleModule(
+        argument_spec = dict(),
+        supports_check_mode=True
+    )
+
+    # The module does not change anything, meaning that
+    # check mode is supported
+
+    ipa_facts = dict(
+        packages= dict(
+            ipalib=HAS_IPALIB,
+            ipaserver=HAS_IPASERVER,
+        ),
+        configured=dict(
+            client=False,
+            server=False,
+            dns=False,
+            ca=False,
+            kra=False,
+            ntpd=False
+        )
+    )
+
+    if HAS_IPALIB:
+        if is_client_configured():
+            ipa_facts['configured']['client'] = True
+
+            ipa_facts['version'] = get_ipa_version()
+            for key,value in six.iteritems(get_ipa_conf()):
+                ipa_facts[key] = value
+
+    if HAS_IPASERVER:
+        if is_server_configured():
+            ipa_facts['configured']['server'] = True
+            ipa_facts['configured']['dns'] = is_dns_configured()
+            ipa_facts['configured']['ca'] = is_ca_configured()
+            ipa_facts['configured']['kra'] = is_kra_configured()
+            ipa_facts['configured']['ntpd'] = is_ntpd_configured()
+
+    module.exit_json(
+        changed=False,
+        ansible_facts=dict(ipa=ipa_facts)
+        )
+
+if __name__ == '__main__':
+    main()
diff --git a/library/ipaclient.py b/library/ipaclient.py
index 37bd9c5b..a1840669 100644
--- a/library/ipaclient.py
+++ b/library/ipaclient.py
@@ -151,7 +151,10 @@ def get_ipa_conf():
     parser.read(paths.IPA_DEFAULT_CONF)
     result = dict()
     for item in ['basedn', 'realm', 'domain', 'server', 'host', 'xmlrpc_uri']:
-        value = parser.get('global', item)
+        if parser.has_option('global', item):
+	    value = parser.get('global', item)
+        else:
+            value = None
         if value:
             result[item] = value
 
@@ -251,6 +254,7 @@ def ensure_ipa_client(module):
     if keytab:
         cmd.append("--keytab")
         cmd.append(keytab)
+        cmd.append("-d")
     if otp:
         cmd.append("--password")
         cmd.append(otp)
diff --git a/library/ipahost.py b/library/ipahost.py
index c4914d61..08305fc1 100644
--- a/library/ipahost.py
+++ b/library/ipahost.py
@@ -36,7 +36,7 @@ description:
 options:
   principal:
     description: Kerberos principal used to manage the host
-    required: false
+    required: true
     default: admin
   password:
     description: Password for the kerberos principal
@@ -44,6 +44,10 @@ options:
   keytab:
     description: Keytab file containing the Kerberos principal and encrypted key
     required: false
+  lifetime:
+    description: Sets the default lifetime for initial ticket requests
+    required: false
+    default: 1h
   fqdn:
     description: the fully-qualified hostname of the host to add/modify/remove
     required: true
@@ -251,9 +255,10 @@ def main():
     """
     module = AnsibleModule(
         argument_spec=dict(
-            keytab = dict(required=False, type='path'),
+            #keytab = dict(required=False, type='path'),
             principal = dict(default='admin'),
-            password = dict(required=False, no_log=True),
+            #password = dict(required=False, no_log=True),
+            ccache = dict(required=False, type='path'),
             fqdn = dict(required=True),
             certificates = dict(required=False, type='list'),
             sshpubkey= dict(required=False),
@@ -261,27 +266,21 @@ def main():
             random = dict(default=False, type='bool'),
             state = dict(default='present', choices=[ 'present', 'absent' ]),
         ),
-        required_one_of=[ [ 'password', 'keytab'], ],
-        mutually_exclusive=[ [ 'password', 'keytab' ], ],
+        #mutually_exclusive=[['password','keytab']],
+        #required_one_of=[['[password','keytab']],
         supports_check_mode=True,
     )
 
     principal = module.params.get('principal', 'admin')
     password = module.params.get('password')
     keytab = module.params.get('keytab')
+    ccache = module.params.get('ccache')
     fqdn = unicode(module.params.get('fqdn'))
     state = module.params.get('state')
 
     try:
-        ccache_dir = tempfile.mkdtemp(prefix='krbcc')
-        ccache_name = os.path.join(ccache_dir, 'ccache')
-
-        if keytab:
-            kinit_keytab(principal, keytab, ccache_name)
-        elif password:
-            kinit_password(principal, password, ccache_name)
+        os.environ['KRB5CCNAME']=ccache
 
-        os.environ['KRB5CCNAME'] = ccache_name
         cfg = dict(
             context='ansible_module',
             confdir=paths.ETC_IPA,
diff --git a/roles/ipaclient/tasks/install.yml b/roles/ipaclient/tasks/install.yml
index cc6ce0b0..a05aee3b 100644
--- a/roles/ipaclient/tasks/install.yml
+++ b/roles/ipaclient/tasks/install.yml
@@ -1,6 +1,32 @@
 ---
 # tasks file for ipaclient
 
+# The following block is executed when using OTP to enroll IPA client
+# ie when neither ipaclient_password not ipaclient_keytab is set
+# It connects to ipaserver and add the host with --random option in order
+# to create a OneTime Password
+- block:
+  - name: Install - Get a One-Time Password for client enrollment
+    ipahost:
+      state: present
+      principal: "{{ ipaserver_principal | default('admin') }}"
+      password: "{{ ipaserver_password | default(omit) }}"
+      keytab: "{{ ipaserver_keytab | default(omit) }}"
+      fqdn: "{{ ansible_fqdn }}"
+      lifetime: "{{ ipaserver_lifetime | default(omit) }}"
+      random: True
+    register: ipahost_output
+    # If the host is already enrolled, this command will exit on error
+    # The error can be ignored
+    failed_when: ipahost_output|failed and "Password cannot be set on enrolled host" not in ipahost_output.msg
+    delegate_to: "{{ groups.ipaservers[0] }}"
+
+  - name: Install - Store the previously obtained OTP
+    set_fact:
+      ipaclient_otp: "{{ipahost_output.host.randompassword if ipahost_output.host is defined else 'dummyotp' }}"
+
+  when: ipaclient_password is not defined and ipaclient_keytab is not defined
+
 - name: Install - Install IPA client package
   package:
     name: "{{ ipaclient_package }}"
@@ -9,11 +35,11 @@
 - name: Install - Configure IPA client
   ipaclient:
     state: present
-    domain: "{{ ipaclient_domain }}"
-    realm: "{{ ipaclient_realm }}"
-    server: "{{ ipaclient_server }}"
-    principal: "{{ ipaclient_principal }}"
-    password: "{{ ipaclient_password }}"
-    keytab: "{{ ipaclient_keytab }}"
-    otp: "{{ ipaclient_otp }}"
-    extra_args: "{{ ipaclient_extraargs }}"
+    domain: "{{ ipaclient_domain | default(omit) }}"
+    realm: "{{ ipaclient_realm | default(omit) }}"
+    server: "{{ ipaclient_server | default(omit) }}"
+    principal: "{{ ipaclient_principal | default(omit) }}"
+    password: "{{ ipaclient_password | default(omit) }}"
+    keytab: "{{ ipaclient_keytab | default(omit) }}"
+    otp: "{{ ipaclient_otp | default(omit) }}"
+    extra_args: "{{ ipaclient_extraargs | default(omit) }}"
diff --git a/roles/ipaclient/vars/default.yml b/roles/ipaclient/vars/default.yml
index a0e63eaf..eb675d4b 100644
--- a/roles/ipaclient/vars/default.yml
+++ b/roles/ipaclient/vars/default.yml
@@ -1,3 +1,3 @@
 # defaults file for ipaclient
-# defaults/fedora.yml
+# vars/default.yml
 ipaclient_package: freeipa-client
diff --git a/roles/ipaclient/vars/rhel.yml b/roles/ipaclient/vars/rhel.yml
index c36d57dd..76c7a343 100644
--- a/roles/ipaclient/vars/rhel.yml
+++ b/roles/ipaclient/vars/rhel.yml
@@ -1,4 +1,4 @@
 # defaults file for ipaclient
-# defaults/rhel.yml
+# vars/rhel.yml
 ipaclient_package: ipa-client
 
diff --git a/site.yml b/site.yml
index 43d5bcc4..18a4255a 100644
--- a/site.yml
+++ b/site.yml
@@ -3,17 +3,6 @@
   hosts: ipaclients
   become: true
 
-  pre_tasks:
-
-  - name: For OTP client registration, add client and get OTP
-    ipahost:
-      keytab: files/admin.keytab
-      fqdn: "{{ ansible_fqdn }}"
-      random: True
-    register: ipahost
-    delegate_to: "{{ groups.ipaservers[0] }}"
-
   roles:
   - role: ipaclient
     state: present
-    ipaclient_otp: "{{ ipahost.host.randompassword }}"
-- 
GitLab