mirror of
https://github.com/freeipa/ansible-freeipa.git
synced 2026-08-05 14:24:48 +00:00
Merge pull request #987 from t-woerner/ipaclient_no_kinit_on_controller_for_OTP
ipaclient: No kinit on controller for deployment using OTP
This commit is contained in:
@@ -69,7 +69,6 @@ Requirements
|
|||||||
|
|
||||||
**Controller**
|
**Controller**
|
||||||
* Ansible version: 2.8+ (ansible-freeipa is an Ansible Collection)
|
* Ansible version: 2.8+ (ansible-freeipa is an Ansible Collection)
|
||||||
* /usr/bin/kinit is required on the controller if a one time password (OTP) is used
|
|
||||||
|
|
||||||
**Node**
|
**Node**
|
||||||
* Supported FreeIPA version (see above)
|
* Supported FreeIPA version (see above)
|
||||||
@@ -289,7 +288,7 @@ ipaserver_domain=test.local
|
|||||||
ipaserver_realm=TEST.LOCAL
|
ipaserver_realm=TEST.LOCAL
|
||||||
```
|
```
|
||||||
|
|
||||||
For enhanced security it is possible to use a auto-generated one-time-password (OTP). This will be generated on the controller using the (first) server.
|
For enhanced security it is possible to use a auto-generated one-time-password (OTP). This will be generated on the (first) server.
|
||||||
|
|
||||||
To enable the generation of the one-time-password:
|
To enable the generation of the one-time-password:
|
||||||
```yaml
|
```yaml
|
||||||
|
|||||||
@@ -32,7 +32,6 @@ Requirements
|
|||||||
|
|
||||||
**Controller**
|
**Controller**
|
||||||
* Ansible version: 2.8+
|
* Ansible version: 2.8+
|
||||||
* /usr/bin/kinit is required on the controller if a one time password (OTP) is used
|
|
||||||
|
|
||||||
**Node**
|
**Node**
|
||||||
* Supported FreeIPA version (see above)
|
* Supported FreeIPA version (see above)
|
||||||
|
|||||||
@@ -1,247 +0,0 @@
|
|||||||
# Authors:
|
|
||||||
# Florence Blanc-Renaud <frenaud@redhat.com>
|
|
||||||
#
|
|
||||||
# Copyright (C) 2017 Red Hat
|
|
||||||
# see file 'COPYING' for use and warranty information
|
|
||||||
#
|
|
||||||
# This program is free software; you can redistribute it and/or modify
|
|
||||||
# it under the terms of the GNU General Public License as published by
|
|
||||||
# the Free Software Foundation, either version 3 of the License, or
|
|
||||||
# (at your option) any later version.
|
|
||||||
#
|
|
||||||
# This program is distributed in the hope that it will be useful,
|
|
||||||
# but WITHOUT ANY WARRANTY; without even the implied warranty of
|
|
||||||
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
|
||||||
# GNU General Public License for more details.
|
|
||||||
#
|
|
||||||
# You should have received a copy of the GNU General Public License
|
|
||||||
# along with this program. If not, see <http://www.gnu.org/licenses/>.
|
|
||||||
|
|
||||||
from __future__ import (absolute_import, division, print_function)
|
|
||||||
|
|
||||||
__metaclass__ = type
|
|
||||||
|
|
||||||
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
|
|
||||||
|
|
||||||
|
|
||||||
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
|
|
||||||
|
|
||||||
# pylint: disable=invalid-name
|
|
||||||
with subprocess.Popen(
|
|
||||||
args, stdin=p_in, stdout=p_out, stderr=p_err, close_fds=True
|
|
||||||
) as p:
|
|
||||||
__temp, stderr = p.communicate(stdin)
|
|
||||||
|
|
||||||
if p.returncode != 0:
|
|
||||||
raise RuntimeError(stderr)
|
|
||||||
|
|
||||||
|
|
||||||
def kinit_password(principal, password, ccache_name, config):
|
|
||||||
"""
|
|
||||||
Perform kinit using principal/password.
|
|
||||||
|
|
||||||
It uses the specified config file to kinit and stores 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:
|
|
||||||
return run_cmd(args, stdin=password.encode())
|
|
||||||
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.
|
|
||||||
|
|
||||||
It uses the specified config file to kinit and stores the TGT
|
|
||||||
in ccache_name.
|
|
||||||
"""
|
|
||||||
args = ["/usr/bin/kinit", "-kt", keytab, "-c", ccache_name, principal]
|
|
||||||
old_config = os.environ.get('KRB5_CONFIG')
|
|
||||||
os.environ["KRB5_CONFIG"] = config
|
|
||||||
|
|
||||||
try:
|
|
||||||
return run_cmd(args)
|
|
||||||
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): # pylint: disable=too-few-public-methods
|
|
||||||
|
|
||||||
# pylint: disable=too-many-return-statements
|
|
||||||
def run(self, tmp=None, task_vars=None):
|
|
||||||
"""
|
|
||||||
Handle credential cache transfer.
|
|
||||||
|
|
||||||
ipa* commands can either provide a password or a keytab file
|
|
||||||
in order to authenticate on the managed node with Kerberos.
|
|
||||||
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 = {}
|
|
||||||
|
|
||||||
# pylint: disable=super-with-arguments
|
|
||||||
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 (not keytab and not password):
|
|
||||||
result['failed'] = True
|
|
||||||
result['msg'] = "keytab or password is required"
|
|
||||||
return result
|
|
||||||
|
|
||||||
if not principal:
|
|
||||||
result['failed'] = True
|
|
||||||
result['msg'] = "principal is required"
|
|
||||||
return result
|
|
||||||
|
|
||||||
data = self._execute_module(module_name='ipaclient_get_facts',
|
|
||||||
module_args={}, task_vars=task_vars)
|
|
||||||
|
|
||||||
try:
|
|
||||||
domain = data['ansible_facts']['ipa']['domain']
|
|
||||||
realm = data['ansible_facts']['ipa']['realm']
|
|
||||||
except KeyError:
|
|
||||||
result['failed'] = True
|
|
||||||
result['msg'] = "The host is not an IPA server"
|
|
||||||
return result
|
|
||||||
|
|
||||||
items = principal.split('@')
|
|
||||||
if len(items) < 2:
|
|
||||||
principal = str('%s@%s' % (principal, realm))
|
|
||||||
|
|
||||||
# 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')
|
|
||||||
|
|
||||||
# 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: # pylint: disable=invalid-name
|
|
||||||
f.write(content)
|
|
||||||
|
|
||||||
if password:
|
|
||||||
try:
|
|
||||||
# perform kinit -c ccache_name -l 1h principal
|
|
||||||
kinit_password(principal, password, ccache_name,
|
|
||||||
krb5conf_name)
|
|
||||||
except Exception as e:
|
|
||||||
result['failed'] = True
|
|
||||||
result['msg'] = 'kinit %s with password failed: %s' % \
|
|
||||||
(principal, to_native(e))
|
|
||||||
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: %s' % \
|
|
||||||
(principal, keytab, str(e))
|
|
||||||
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)
|
|
||||||
@@ -1,282 +0,0 @@
|
|||||||
# -*- coding: utf-8 -*-
|
|
||||||
|
|
||||||
# Authors:
|
|
||||||
# Thomas Woerner <twoerner@redhat.com>
|
|
||||||
#
|
|
||||||
# Based on ipa-client-install code
|
|
||||||
#
|
|
||||||
# Copyright (C) 2018-2022 Red Hat
|
|
||||||
# see file 'COPYING' for use and warranty information
|
|
||||||
#
|
|
||||||
# This program is free software; you can redistribute it and/or modify
|
|
||||||
# it under the terms of the GNU General Public License as published by
|
|
||||||
# the Free Software Foundation, either version 3 of the License, or
|
|
||||||
# (at your option) any later version.
|
|
||||||
#
|
|
||||||
# This program is distributed in the hope that it will be useful,
|
|
||||||
# but WITHOUT ANY WARRANTY; without even the implied warranty of
|
|
||||||
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
|
||||||
# GNU General Public License for more details.
|
|
||||||
#
|
|
||||||
# You should have received a copy of the GNU General Public License
|
|
||||||
# along with this program. If not, see <http://www.gnu.org/licenses/>.
|
|
||||||
|
|
||||||
from __future__ import (absolute_import, division, print_function)
|
|
||||||
|
|
||||||
__metaclass__ = type
|
|
||||||
|
|
||||||
DOCUMENTATION = """
|
|
||||||
---
|
|
||||||
module: ipaclient_get_facts
|
|
||||||
short_description: Get facts about IPA client and server configuration.
|
|
||||||
description: Get facts about IPA client and server configuration.
|
|
||||||
author:
|
|
||||||
- Thomas Woerner (@t-woerner)
|
|
||||||
"""
|
|
||||||
|
|
||||||
EXAMPLES = """
|
|
||||||
"""
|
|
||||||
|
|
||||||
RETURN = """
|
|
||||||
ipa:
|
|
||||||
description: IPA configuration
|
|
||||||
returned: always
|
|
||||||
type: complex
|
|
||||||
contains:
|
|
||||||
packages:
|
|
||||||
description: IPA lib and server bindings
|
|
||||||
type: dict
|
|
||||||
returned: always
|
|
||||||
contains:
|
|
||||||
ipalib:
|
|
||||||
description: Whether ipalib.api binding could be imported.
|
|
||||||
type: bool
|
|
||||||
returned: always
|
|
||||||
ipaserver:
|
|
||||||
description: Whether ipaserver binding could be imported.
|
|
||||||
type: bool
|
|
||||||
returned: always
|
|
||||||
configured:
|
|
||||||
description: IPA components
|
|
||||||
type: dict
|
|
||||||
returned: always
|
|
||||||
contains:
|
|
||||||
client:
|
|
||||||
description: Whether client is configured
|
|
||||||
type: bool
|
|
||||||
returned: always
|
|
||||||
server:
|
|
||||||
description: Whether server is configured
|
|
||||||
type: bool
|
|
||||||
returned: always
|
|
||||||
dns:
|
|
||||||
description: Whether dns is configured
|
|
||||||
type: bool
|
|
||||||
returned: always
|
|
||||||
ca:
|
|
||||||
description: Whether ca is configured
|
|
||||||
type: bool
|
|
||||||
returned: always
|
|
||||||
kra:
|
|
||||||
description: Whether kra is configured
|
|
||||||
type: bool
|
|
||||||
returned: always
|
|
||||||
ntpd:
|
|
||||||
description: Whether ntpd is configured
|
|
||||||
type: bool
|
|
||||||
returned: always
|
|
||||||
"""
|
|
||||||
|
|
||||||
import os
|
|
||||||
import re
|
|
||||||
from ansible.module_utils import six
|
|
||||||
try:
|
|
||||||
from ansible.module_utils.six.moves.configparser import RawConfigParser
|
|
||||||
except ImportError:
|
|
||||||
from ConfigParser import RawConfigParser
|
|
||||||
|
|
||||||
from ansible.module_utils.basic import AnsibleModule
|
|
||||||
|
|
||||||
# pylint: disable=unused-import
|
|
||||||
try:
|
|
||||||
from ipalib import api # noqa: F401
|
|
||||||
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 # noqa: F401
|
|
||||||
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(r'^\s*\[ntpd\]\s*$')
|
|
||||||
|
|
||||||
try:
|
|
||||||
# pylint: disable=invalid-name
|
|
||||||
with open(SERVER_SYSRESTORE_STATE) as f:
|
|
||||||
for line in f.readlines():
|
|
||||||
if ntpd_conf_section.match(line):
|
|
||||||
return True
|
|
||||||
# pylint: enable=invalid-name
|
|
||||||
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(r'^\s*dyndb\s+"ipa"\s+"[^"]+"\s+{$')
|
|
||||||
|
|
||||||
try:
|
|
||||||
with open(NAMED_CONF) as f: # pylint: disable=invalid-name
|
|
||||||
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'}
|
|
||||||
if subsystem not in available_subsystems:
|
|
||||||
raise AssertionError("Subsystem '%s' not available" % subsystem)
|
|
||||||
|
|
||||||
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:
|
|
||||||
# pylint: disable=import-outside-toplevel
|
|
||||||
from ipapython import version
|
|
||||||
# pylint: enable=import-outside-toplevel
|
|
||||||
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
|
|
||||||
# 4.6.90.pre2
|
|
||||||
if part.startswith('dev') or part.startswith('pre') 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={},
|
|
||||||
supports_check_mode=True
|
|
||||||
)
|
|
||||||
|
|
||||||
# The module does not change anything, meaning that
|
|
||||||
# check mode is supported
|
|
||||||
|
|
||||||
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():
|
|
||||||
facts['configured']['client'] = True
|
|
||||||
|
|
||||||
facts['version'] = get_ipa_version()
|
|
||||||
for key, value in six.iteritems(get_ipa_conf()):
|
|
||||||
facts[key] = value
|
|
||||||
|
|
||||||
if HAS_IPASERVER:
|
|
||||||
if is_server_configured():
|
|
||||||
facts['configured']['server'] = True
|
|
||||||
facts['configured']['dns'] = is_dns_configured()
|
|
||||||
facts['configured']['ca'] = is_ca_configured()
|
|
||||||
facts['configured']['kra'] = is_kra_configured()
|
|
||||||
facts['configured']['ntpd'] = is_ntpd_configured()
|
|
||||||
|
|
||||||
module.exit_json(
|
|
||||||
changed=False,
|
|
||||||
ansible_facts=dict(ipa=facts)
|
|
||||||
)
|
|
||||||
|
|
||||||
|
|
||||||
if __name__ == '__main__':
|
|
||||||
main()
|
|
||||||
@@ -1,9 +1,9 @@
|
|||||||
# -*- coding: utf-8 -*-
|
# -*- coding: utf-8 -*-
|
||||||
|
|
||||||
# Authors:
|
# Authors:
|
||||||
# Florence Blanc-Renaud <frenaud@redhat.com>
|
# Thomas Woerner <twoerner@redhat.com>
|
||||||
#
|
#
|
||||||
# Copyright (C) 2017-2022 Red Hat
|
# Copyright (C) 2019-2022 Red Hat
|
||||||
# see file 'COPYING' for use and warranty information
|
# see file 'COPYING' for use and warranty information
|
||||||
#
|
#
|
||||||
# This program is free software; you can redistribute it and/or modify
|
# This program is free software; you can redistribute it and/or modify
|
||||||
@@ -23,324 +23,267 @@ from __future__ import (absolute_import, division, print_function)
|
|||||||
|
|
||||||
__metaclass__ = type
|
__metaclass__ = type
|
||||||
|
|
||||||
ANSIBLE_METADATA = {'metadata_version': '1.0',
|
ANSIBLE_METADATA = {
|
||||||
'status': ['preview'],
|
"metadata_version": "1.0",
|
||||||
'supported_by': 'community'}
|
"supported_by": "community",
|
||||||
|
"status": ["preview"],
|
||||||
|
}
|
||||||
|
|
||||||
DOCUMENTATION = '''
|
DOCUMENTATION = """
|
||||||
---
|
---
|
||||||
module: ipaclient_get_otp
|
module: ipaclient_get_otp
|
||||||
short_description: Manage IPA hosts
|
short_description: Get OTP for host enrollment
|
||||||
description:
|
description: Get OTP for host enrollment
|
||||||
Manage hosts in a IPA domain.
|
|
||||||
The operation needs to be authenticated with Kerberos either by providing
|
|
||||||
a password or a keytab corresponding to a principal allowed to perform
|
|
||||||
host operations.
|
|
||||||
options:
|
options:
|
||||||
principal:
|
ipaadmin_principal:
|
||||||
description:
|
description: The admin principal.
|
||||||
User Principal allowed to promote replicas and join IPA realm
|
|
||||||
type: str
|
|
||||||
required: no
|
|
||||||
default: admin
|
default: admin
|
||||||
ccache:
|
|
||||||
description: The local ccache
|
|
||||||
type: path
|
|
||||||
required: no
|
|
||||||
fqdn:
|
|
||||||
description:
|
|
||||||
The fully-qualified hostname of the host to add/modify/remove
|
|
||||||
type: str
|
type: str
|
||||||
required: yes
|
ipaadmin_password:
|
||||||
certificates:
|
description: |
|
||||||
description: A list of host certificates
|
The admin password. Either ipaadmin_password or ipaadmin_keytab needs
|
||||||
type: list
|
to be given.
|
||||||
elements: str
|
required: false
|
||||||
required: no
|
|
||||||
sshpubkey:
|
|
||||||
description: The SSH public key for the host
|
|
||||||
type: str
|
type: str
|
||||||
required: no
|
ipaadmin_keytab:
|
||||||
ipaddress:
|
description: |
|
||||||
description: The IP address for the host
|
The admin keytab. Either ipaadmin_password or ipaadmin_keytab needs
|
||||||
|
to be given.
|
||||||
type: str
|
type: str
|
||||||
required: no
|
required: false
|
||||||
random:
|
hostname:
|
||||||
description: Generate a random password to be used in bulk enrollment
|
description: The FQDN hostname.
|
||||||
type: bool
|
|
||||||
required: no
|
|
||||||
default: no
|
|
||||||
state:
|
|
||||||
description: The desired host state
|
|
||||||
type: str
|
type: str
|
||||||
choices: ['present', 'absent']
|
required: true
|
||||||
default: present
|
|
||||||
required: no
|
|
||||||
author:
|
author:
|
||||||
- Florence Blanc-Renaud (@flo-renaud)
|
- Thomas Woerner (@t-woerner)
|
||||||
'''
|
"""
|
||||||
|
|
||||||
EXAMPLES = '''
|
EXAMPLES = """
|
||||||
# Example from Ansible Playbooks
|
"""
|
||||||
# Add a new host with a random OTP, authenticate using principal/password
|
|
||||||
- ipaclient_get_otp:
|
|
||||||
principal: admin
|
|
||||||
password: MySecretPassword
|
|
||||||
fqdn: ipaclient.ipa.domain.com
|
|
||||||
ipaddress: 192.168.100.23
|
|
||||||
random: True
|
|
||||||
register: result_ipaclient_get_otp
|
|
||||||
'''
|
|
||||||
|
|
||||||
RETURN = '''
|
RETURN = """
|
||||||
host:
|
host:
|
||||||
description: the host structure as returned from IPA API
|
description: Host dict with random password
|
||||||
returned: always
|
returned: always
|
||||||
type: complex
|
type: dict
|
||||||
contains:
|
contains:
|
||||||
dn:
|
|
||||||
description: the DN of the host entry
|
|
||||||
type: str
|
|
||||||
returned: always
|
|
||||||
fqdn:
|
|
||||||
description: the fully qualified host name
|
|
||||||
type: str
|
|
||||||
returned: always
|
|
||||||
has_keytab:
|
|
||||||
description: whether the host entry contains a keytab
|
|
||||||
type: bool
|
|
||||||
returned: always
|
|
||||||
has_password:
|
|
||||||
description: whether the host entry contains a password
|
|
||||||
type: bool
|
|
||||||
returned: always
|
|
||||||
managedby_host:
|
|
||||||
description: the list of hosts managing the host
|
|
||||||
type: list
|
|
||||||
returned: always
|
|
||||||
randompassword:
|
randompassword:
|
||||||
description: the OneTimePassword generated for this host
|
description: The generated random password
|
||||||
type: str
|
type: str
|
||||||
returned: changed
|
"""
|
||||||
certificates:
|
|
||||||
description: the list of host certificates
|
|
||||||
type: list
|
|
||||||
elements: str
|
|
||||||
returned: when present
|
|
||||||
sshpubkey:
|
|
||||||
description: the SSH public key for the host
|
|
||||||
type: str
|
|
||||||
returned: when present
|
|
||||||
ipaddress:
|
|
||||||
description: the IP address for the host
|
|
||||||
type: str
|
|
||||||
returned: when present
|
|
||||||
'''
|
|
||||||
|
|
||||||
import os
|
import os
|
||||||
|
import tempfile
|
||||||
|
import shutil
|
||||||
|
from contextlib import contextmanager
|
||||||
from ansible.module_utils.basic import AnsibleModule
|
from ansible.module_utils.basic import AnsibleModule
|
||||||
|
from ansible.module_utils._text import to_text
|
||||||
from ansible.module_utils import six
|
from ansible.module_utils import six
|
||||||
|
|
||||||
from ansible.module_utils.ansible_ipa_client import (
|
try:
|
||||||
check_imports, api, errors, paths, run
|
from ipalib import api
|
||||||
)
|
from ipalib import errors as ipalib_errors # noqa
|
||||||
|
from ipalib.config import Env
|
||||||
|
from ipaplatform.paths import paths
|
||||||
|
from ipapython.ipautil import run
|
||||||
|
from ipalib.constants import DEFAULT_CONFIG
|
||||||
|
try:
|
||||||
|
from ipalib.install.kinit import kinit_password, kinit_keytab
|
||||||
|
except ImportError:
|
||||||
|
from ipapython.ipautil import kinit_password, kinit_keytab
|
||||||
|
except ImportError as _err:
|
||||||
|
MODULE_IMPORT_ERROR = str(_err)
|
||||||
|
else:
|
||||||
|
MODULE_IMPORT_ERROR = None
|
||||||
|
|
||||||
|
|
||||||
if six.PY3:
|
if six.PY3:
|
||||||
unicode = str
|
unicode = str
|
||||||
|
|
||||||
|
|
||||||
def get_host_diff(ipa_host, module_host):
|
def temp_kinit(principal, password, keytab):
|
||||||
"""
|
"""Kinit with password or keytab using a temporary ccache."""
|
||||||
Build a dict with the differences from two host dicts.
|
ccache_dir = tempfile.mkdtemp(prefix='krbcc')
|
||||||
|
ccache_name = os.path.join(ccache_dir, 'ccache')
|
||||||
|
|
||||||
:param ipa_host: the host structure seen from IPA
|
|
||||||
:param module_host: the target host structure seen from the module params
|
|
||||||
|
|
||||||
:return: a dict representing the host attributes to apply
|
|
||||||
"""
|
|
||||||
non_updateable_keys = ['ip_address']
|
|
||||||
data = {}
|
|
||||||
for key in non_updateable_keys:
|
|
||||||
if key in module_host:
|
|
||||||
del module_host[key]
|
|
||||||
|
|
||||||
for key in module_host.keys():
|
|
||||||
ipa_value = ipa_host.get(key, None)
|
|
||||||
module_value = module_host.get(key, None)
|
|
||||||
if isinstance(ipa_value, list) and not isinstance(module_value, list):
|
|
||||||
module_value = [module_value]
|
|
||||||
if isinstance(ipa_value, list) and isinstance(module_value, list):
|
|
||||||
ipa_value = sorted(ipa_value)
|
|
||||||
module_value = sorted(module_value)
|
|
||||||
if ipa_value != module_value:
|
|
||||||
data[key] = unicode(module_value)
|
|
||||||
return data
|
|
||||||
|
|
||||||
|
|
||||||
def get_module_host(module):
|
|
||||||
"""
|
|
||||||
Create a structure representing the host information.
|
|
||||||
|
|
||||||
Reads the module parameters and builds the host structure as expected from
|
|
||||||
the module
|
|
||||||
:param module: the ansible module
|
|
||||||
:returns: a dict representing the host attributes
|
|
||||||
"""
|
|
||||||
data = {}
|
|
||||||
certificates = module.params.get('certificates')
|
|
||||||
if certificates:
|
|
||||||
data['usercertificate'] = certificates
|
|
||||||
sshpubkey = module.params.get('sshpubkey')
|
|
||||||
if sshpubkey:
|
|
||||||
data['ipasshpubkey'] = unicode(sshpubkey)
|
|
||||||
ipaddress = module.params.get('ipaddress')
|
|
||||||
if ipaddress:
|
|
||||||
data['ip_address'] = unicode(ipaddress)
|
|
||||||
random = module.params.get('random')
|
|
||||||
if random:
|
|
||||||
data['random'] = random
|
|
||||||
return data
|
|
||||||
|
|
||||||
|
|
||||||
def ensure_host_present(module, _api, ipahost):
|
|
||||||
"""
|
|
||||||
Ensure host exists in IPA and has the same attributes.
|
|
||||||
|
|
||||||
:param module: the ansible module
|
|
||||||
:param api: IPA api handle
|
|
||||||
:param ipahost: the host information present in IPA, can be none if the
|
|
||||||
host does not exist
|
|
||||||
"""
|
|
||||||
fqdn = unicode(module.params.get('fqdn'))
|
|
||||||
if ipahost:
|
|
||||||
# Host already present, need to compare the attributes
|
|
||||||
module_host = get_module_host(module)
|
|
||||||
diffs = get_host_diff(ipahost, module_host)
|
|
||||||
|
|
||||||
if not diffs:
|
|
||||||
# Same attributes, success
|
|
||||||
module.exit_json(changed=False, host=ipahost)
|
|
||||||
|
|
||||||
# Need to modify the host - only if not in check_mode
|
|
||||||
if module.check_mode:
|
|
||||||
module.exit_json(changed=True)
|
|
||||||
|
|
||||||
# If we want to create a random password, and the host
|
|
||||||
# already has Keytab: true, then we need first to run
|
|
||||||
# ipa host-disable in order to remove OTP and keytab
|
|
||||||
if module.params.get('random') and ipahost['has_keytab'] is True:
|
|
||||||
_api.Command.host_disable(fqdn)
|
|
||||||
|
|
||||||
result = _api.Command.host_mod(fqdn, **diffs)
|
|
||||||
# Save random password as it is not displayed by host-show
|
|
||||||
if module.params.get('random'):
|
|
||||||
randompassword = result['result']['randompassword']
|
|
||||||
result = _api.Command.host_show(fqdn)
|
|
||||||
if module.params.get('random'):
|
|
||||||
result['result']['randompassword'] = randompassword
|
|
||||||
module.exit_json(changed=True, host=result['result'])
|
|
||||||
|
|
||||||
if not ipahost:
|
|
||||||
# Need to add the user, only if not in check_mode
|
|
||||||
if module.check_mode:
|
|
||||||
module.exit_json(changed=True)
|
|
||||||
|
|
||||||
# Must add the user
|
|
||||||
module_host = get_module_host(module)
|
|
||||||
# force creation of host even if there is no DNS record
|
|
||||||
module_host["force"] = True
|
|
||||||
result = _api.Command.host_add(fqdn, **module_host)
|
|
||||||
# Save random password as it is not displayed by host-show
|
|
||||||
if module.params.get('random'):
|
|
||||||
randompassword = result['result']['randompassword']
|
|
||||||
result = _api.Command.host_show(fqdn)
|
|
||||||
if module.params.get('random'):
|
|
||||||
result['result']['randompassword'] = randompassword
|
|
||||||
module.exit_json(changed=True, host=result['result'])
|
|
||||||
|
|
||||||
|
|
||||||
def ensure_host_absent(module, _api, host):
|
|
||||||
"""
|
|
||||||
Ensure host does not exist in IPA.
|
|
||||||
|
|
||||||
:param module: the ansible module
|
|
||||||
:param api: the IPA API handle
|
|
||||||
:param host: the host information present in IPA, can be none if the
|
|
||||||
host does not exist
|
|
||||||
"""
|
|
||||||
if not host:
|
|
||||||
# Nothing to do, host already removed
|
|
||||||
module.exit_json(changed=False)
|
|
||||||
|
|
||||||
# Need to remove the host - only if not in check_mode
|
|
||||||
if module.check_mode:
|
|
||||||
module.exit_json(changed=True, host=host)
|
|
||||||
|
|
||||||
fqdn = unicode(module.params.get('fqdn'))
|
|
||||||
try:
|
try:
|
||||||
_api.Command.host_del(fqdn)
|
if password:
|
||||||
except Exception as e:
|
kinit_password(principal, password, ccache_name)
|
||||||
module.fail_json(msg="Failed to remove host: %s" % e)
|
else:
|
||||||
|
kinit_keytab(principal, keytab, ccache_name)
|
||||||
|
except RuntimeError as e:
|
||||||
|
raise RuntimeError("Kerberos authentication failed: %s" % str(e))
|
||||||
|
|
||||||
module.exit_json(changed=True)
|
os.environ["KRB5CCNAME"] = ccache_name
|
||||||
|
return ccache_dir, ccache_name
|
||||||
|
|
||||||
|
|
||||||
|
def temp_kdestroy(ccache_dir, ccache_name):
|
||||||
|
"""Destroy temporary ticket and remove temporary ccache."""
|
||||||
|
if ccache_name is not None:
|
||||||
|
run([paths.KDESTROY, '-c', ccache_name], raiseonerr=False)
|
||||||
|
del os.environ['KRB5CCNAME']
|
||||||
|
if ccache_dir is not None:
|
||||||
|
shutil.rmtree(ccache_dir, ignore_errors=True)
|
||||||
|
|
||||||
|
|
||||||
|
@contextmanager
|
||||||
|
def ipa_connect(module, principal=None, password=None, keytab=None):
|
||||||
|
"""
|
||||||
|
Create a context with a connection to IPA API.
|
||||||
|
|
||||||
|
Parameters
|
||||||
|
----------
|
||||||
|
module: AnsibleModule
|
||||||
|
The AnsibleModule to use
|
||||||
|
principal: string
|
||||||
|
The optional principal name
|
||||||
|
password: string
|
||||||
|
The optional password. Either password or keytab needs to be given.
|
||||||
|
keytab: string
|
||||||
|
The optional keytab. Either password or keytab needs to be given.
|
||||||
|
|
||||||
|
"""
|
||||||
|
if not password and not keytab:
|
||||||
|
module.fail_json(msg="One of password and keytab is required.")
|
||||||
|
if not principal:
|
||||||
|
principal = "admin"
|
||||||
|
|
||||||
|
ccache_dir = None
|
||||||
|
ccache_name = None
|
||||||
|
try:
|
||||||
|
ccache_dir, ccache_name = temp_kinit(principal, password, keytab)
|
||||||
|
# api_connect start
|
||||||
|
env = Env()
|
||||||
|
env._bootstrap()
|
||||||
|
env._finalize_core(**dict(DEFAULT_CONFIG))
|
||||||
|
|
||||||
|
api.bootstrap(context="server", debug=env.debug, log=None)
|
||||||
|
api.finalize()
|
||||||
|
|
||||||
|
if api.env.in_server:
|
||||||
|
backend = api.Backend.ldap2
|
||||||
|
else:
|
||||||
|
backend = api.Backend.rpcclient
|
||||||
|
|
||||||
|
if not backend.isconnected():
|
||||||
|
backend.connect(ccache=ccache_name)
|
||||||
|
# api_connect end
|
||||||
|
except Exception as e:
|
||||||
|
module.fail_json(msg=str(e))
|
||||||
|
else:
|
||||||
|
try:
|
||||||
|
yield ccache_name
|
||||||
|
except Exception as e:
|
||||||
|
module.fail_json(msg=str(e))
|
||||||
|
finally:
|
||||||
|
temp_kdestroy(ccache_dir, ccache_name)
|
||||||
|
|
||||||
|
|
||||||
|
def ipa_command(command, name, args):
|
||||||
|
"""
|
||||||
|
Execute an IPA API command with a required `name` argument.
|
||||||
|
|
||||||
|
Parameters
|
||||||
|
----------
|
||||||
|
command: string
|
||||||
|
The IPA API command to execute.
|
||||||
|
name: string
|
||||||
|
The name parameter to pass to the command.
|
||||||
|
args: dict
|
||||||
|
The parameters to pass to the command.
|
||||||
|
|
||||||
|
"""
|
||||||
|
return api.Command[command](name, **args)
|
||||||
|
|
||||||
|
|
||||||
|
def _afm_convert(value):
|
||||||
|
if value is not None:
|
||||||
|
if isinstance(value, list):
|
||||||
|
return [_afm_convert(x) for x in value]
|
||||||
|
if isinstance(value, dict):
|
||||||
|
return {_afm_convert(k): _afm_convert(v)
|
||||||
|
for k, v in value.items()}
|
||||||
|
if isinstance(value, str):
|
||||||
|
return to_text(value)
|
||||||
|
|
||||||
|
return value
|
||||||
|
|
||||||
|
|
||||||
|
def module_params_get(module, name):
|
||||||
|
return _afm_convert(module.params.get(name))
|
||||||
|
|
||||||
|
|
||||||
|
def host_show(module, name):
|
||||||
|
_args = {
|
||||||
|
"all": True,
|
||||||
|
}
|
||||||
|
|
||||||
|
try:
|
||||||
|
_result = ipa_command("host_show", name, _args)
|
||||||
|
except ipalib_errors.NotFound as e:
|
||||||
|
msg = str(e)
|
||||||
|
if "host not found" in msg:
|
||||||
|
return None
|
||||||
|
module.fail_json(msg="host_show failed: %s" % msg)
|
||||||
|
|
||||||
|
return _result["result"]
|
||||||
|
|
||||||
|
|
||||||
def main():
|
def main():
|
||||||
|
|
||||||
module = AnsibleModule(
|
module = AnsibleModule(
|
||||||
argument_spec=dict(
|
argument_spec=dict(
|
||||||
principal=dict(required=False, type='str', default='admin'),
|
ipaadmin_principal=dict(type="str", default="admin"),
|
||||||
ccache=dict(required=False, type='path'),
|
ipaadmin_password=dict(type="str", required=False, no_log=True),
|
||||||
fqdn=dict(required=True, type='str'),
|
ipaadmin_keytab=dict(type="str", required=False, no_log=False),
|
||||||
certificates=dict(required=False, type='list', elements='str'),
|
hostname=dict(type="str", required=True),
|
||||||
sshpubkey=dict(required=False, type='str'),
|
|
||||||
ipaddress=dict(required=False, type='str'),
|
|
||||||
random=dict(required=False, type='bool', default=False),
|
|
||||||
state=dict(required=False, type='str',
|
|
||||||
choices=['present', 'absent'], default='present'),
|
|
||||||
),
|
),
|
||||||
|
mutually_exclusive=[["ipaadmin_password", "ipaadmin_keytab"]],
|
||||||
supports_check_mode=True,
|
supports_check_mode=True,
|
||||||
)
|
)
|
||||||
|
|
||||||
check_imports(module)
|
if MODULE_IMPORT_ERROR is not None:
|
||||||
|
module.fail_json(msg=MODULE_IMPORT_ERROR)
|
||||||
|
|
||||||
ccache = module.params.get('ccache')
|
# In check mode always return changed.
|
||||||
fqdn = unicode(module.params.get('fqdn'))
|
if module.check_mode:
|
||||||
state = module.params.get('state')
|
module.exit_json(changed=True)
|
||||||
|
|
||||||
try:
|
ipaadmin_principal = module_params_get(module, "ipaadmin_principal")
|
||||||
os.environ['KRB5CCNAME'] = ccache
|
ipaadmin_password = module_params_get(module, "ipaadmin_password")
|
||||||
|
ipaadmin_keytab = module_params_get(module, "ipaadmin_keytab")
|
||||||
|
if ipaadmin_keytab:
|
||||||
|
if not os.path.exists(ipaadmin_keytab):
|
||||||
|
module.fail_json(msg="Unable to open ipaadmin_keytab '%s'" %
|
||||||
|
ipaadmin_keytab)
|
||||||
|
|
||||||
cfg = dict(
|
hostname = module_params_get(module, "hostname")
|
||||||
context='ansible_module',
|
|
||||||
confdir=paths.ETC_IPA,
|
|
||||||
in_server=False,
|
|
||||||
debug=False,
|
|
||||||
verbose=0,
|
|
||||||
)
|
|
||||||
api.bootstrap(**cfg)
|
|
||||||
api.finalize()
|
|
||||||
api.Backend.rpcclient.connect()
|
|
||||||
|
|
||||||
try:
|
exit_args = {}
|
||||||
result = api.Command.host_show(fqdn, all=True)
|
|
||||||
host = result['result']
|
|
||||||
except errors.NotFound:
|
|
||||||
host = None
|
|
||||||
|
|
||||||
if state in ['present', 'disabled']:
|
# Connect to IPA API
|
||||||
ensure_host_present(module, api, host)
|
with ipa_connect(module, ipaadmin_principal, ipaadmin_password,
|
||||||
elif state == 'absent':
|
ipaadmin_keytab):
|
||||||
ensure_host_absent(module, api, host)
|
res_show = host_show(module, hostname)
|
||||||
|
|
||||||
except Exception as e:
|
args = {"random": True}
|
||||||
module.fail_json(msg="ipaclient_get_otp module failed : %s" % str(e))
|
if res_show is None:
|
||||||
finally:
|
# Create new host, force is needed to create the host without
|
||||||
run([paths.KDESTROY], raiseonerr=False, env=os.environ)
|
# IP address.
|
||||||
|
args["force"] = True
|
||||||
|
result = ipa_command("host_add", hostname, args)
|
||||||
|
else:
|
||||||
|
# If host exists and has a keytab (is enrolled) then disable the
|
||||||
|
# host to be able to create a new OTP.
|
||||||
|
if res_show["has_keytab"]:
|
||||||
|
ipa_command("host_disable", hostname, {})
|
||||||
|
result = ipa_command("host_mod", hostname, args)
|
||||||
|
|
||||||
module.exit_json(changed=False, host=host)
|
exit_args["randompassword"] = result['result']['randompassword']
|
||||||
|
|
||||||
|
module.exit_json(changed=True, host=exit_args)
|
||||||
|
|
||||||
|
|
||||||
if __name__ == '__main__':
|
if __name__ == "__main__":
|
||||||
main()
|
main()
|
||||||
|
|||||||
@@ -18,9 +18,9 @@
|
|||||||
when: ipaclient_no_dns_lookup | bool and groups.ipaserver is defined and
|
when: ipaclient_no_dns_lookup | bool and groups.ipaserver is defined and
|
||||||
ipaclient_servers is not defined
|
ipaclient_servers is not defined
|
||||||
|
|
||||||
- name: Install - Check that either principal or keytab is set
|
- name: Install - Check that either password or keytab is set
|
||||||
fail: msg="ipaadmin_principal and ipaadmin_keytab cannot be used together"
|
fail: msg="ipaadmin_password and ipaadmin_keytab cannot be used together"
|
||||||
when: ipaadmin_keytab is defined and ipaadmin_principal is defined
|
when: ipaadmin_keytab is defined and ipaadmin_password is defined
|
||||||
|
|
||||||
- name: Install - Set default principal if no keytab is given
|
- name: Install - Set default principal if no keytab is given
|
||||||
set_fact:
|
set_fact:
|
||||||
@@ -99,35 +99,44 @@
|
|||||||
not ipaclient_force_join | bool
|
not ipaclient_force_join | bool
|
||||||
|
|
||||||
# The following block is executed when using OTP to enroll IPA client and
|
# The following block is executed when using OTP to enroll IPA client and
|
||||||
# the OTP isn't predefined, ie when ipaclient_use_otp is set and ipaclient_otp
|
# the OTP isn't predefined, ie when ipaclient_use_otp is set and
|
||||||
# is not set.
|
# ipaclient_otp is not set.
|
||||||
# It connects to ipaserver and add the host with --random option in order
|
# It connects to ipaserver and add the host with --random option in order
|
||||||
# to create a OneTime Password
|
# to create a OneTime Password
|
||||||
# If a keytab is specified in the hostent, then the hostent will be disabled
|
# If a keytab is specified in the hostent, then the hostent will be disabled
|
||||||
# if ipaclient_use_otp is set.
|
# if ipaclient_use_otp is set.
|
||||||
- block:
|
- block:
|
||||||
- name: Install - Keytab or password is required for getting otp
|
- name: Install - Keytab or password is required for getting otp
|
||||||
fail: msg="Keytab or password is required for getting otp"
|
ansible.builtin.fail:
|
||||||
|
msg: Keytab or password is required for getting otp
|
||||||
when: ipaadmin_keytab is undefined and ipaadmin_password is undefined
|
when: ipaadmin_keytab is undefined and ipaadmin_password is undefined
|
||||||
|
|
||||||
|
- name: Install - Create temporary file for keytab
|
||||||
|
ansible.builtin.tempfile:
|
||||||
|
state: file
|
||||||
|
prefix: ipaclient_temp_
|
||||||
|
path: /root
|
||||||
|
register: keytab_temp
|
||||||
|
delegate_to: "{{ result_ipaclient_test.servers[0] }}"
|
||||||
|
when: ipaadmin_keytab is defined
|
||||||
|
|
||||||
|
- name: Install - Copy keytab to server temporary file
|
||||||
|
ansible.builtin.copy:
|
||||||
|
src: "{{ ipaadmin_keytab }}"
|
||||||
|
dest: "{{ keytab_temp.path }}"
|
||||||
|
mode: 0600
|
||||||
|
delegate_to: "{{ result_ipaclient_test.servers[0] }}"
|
||||||
|
when: ipaadmin_keytab is defined
|
||||||
|
|
||||||
- name: Install - Get One-Time Password for client enrollment
|
- name: Install - Get One-Time Password for client enrollment
|
||||||
no_log: yes
|
no_log: yes
|
||||||
ipaclient_get_otp:
|
ipaclient_get_otp:
|
||||||
state: present
|
ipaadmin_principal: "{{ ipaadmin_principal | default(omit) }}"
|
||||||
principal: "{{ ipaadmin_principal | default(omit) }}"
|
ipaadmin_password: "{{ ipaadmin_password | default(omit) }}"
|
||||||
password: "{{ ipaadmin_password | default(omit) }}"
|
ipaadmin_keytab: "{{ keytab_temp.path | default(omit) }}"
|
||||||
keytab: "{{ ipaadmin_keytab | default(omit) }}"
|
hostname: "{{ result_ipaclient_test.hostname }}"
|
||||||
fqdn: "{{ result_ipaclient_test.hostname }}"
|
|
||||||
lifetime: "{{ ipaclient_lifetime | default(omit) }}"
|
|
||||||
random: True
|
|
||||||
register: result_ipaclient_get_otp
|
register: result_ipaclient_get_otp
|
||||||
# If the host is already enrolled, this command will exit on error
|
|
||||||
# The error can be ignored
|
|
||||||
failed_when: result_ipaclient_get_otp is failed and
|
|
||||||
"Password cannot be set on enrolled host" not
|
|
||||||
in result_ipaclient_get_otp.msg
|
|
||||||
delegate_to: "{{ result_ipaclient_test.servers[0] }}"
|
delegate_to: "{{ result_ipaclient_test.servers[0] }}"
|
||||||
ignore_errors: yes
|
|
||||||
|
|
||||||
- name: Install - Report error for OTP generation
|
- name: Install - Report error for OTP generation
|
||||||
debug:
|
debug:
|
||||||
@@ -144,6 +153,14 @@
|
|||||||
|
|
||||||
when: ipaclient_use_otp | bool and ipaclient_otp is not defined
|
when: ipaclient_use_otp | bool and ipaclient_otp is not defined
|
||||||
|
|
||||||
|
always:
|
||||||
|
- name: Install - Remove keytab temporary file
|
||||||
|
ansible.builtin.file:
|
||||||
|
path: "{{ keytab_temp.path }}"
|
||||||
|
state: absent
|
||||||
|
delegate_to: "{{ result_ipaclient_test.servers[0] }}"
|
||||||
|
when: keytab_temp.path is defined
|
||||||
|
|
||||||
- name: Store predefined OTP in admin_password
|
- name: Store predefined OTP in admin_password
|
||||||
no_log: yes
|
no_log: yes
|
||||||
set_fact:
|
set_fact:
|
||||||
@@ -161,7 +178,7 @@
|
|||||||
# result_ipaclient_join.already_joined)))
|
# result_ipaclient_join.already_joined)))
|
||||||
|
|
||||||
- name: Install - Check if principal and keytab are set
|
- name: Install - Check if principal and keytab are set
|
||||||
fail: msg="Principal and keytab cannot be used together"
|
fail: msg="Admin principal and client keytab cannot be used together"
|
||||||
when: ipaadmin_principal is defined and ipaclient_keytab is defined
|
when: ipaadmin_principal is defined and ipaclient_keytab is defined
|
||||||
|
|
||||||
- name: Install - Check if one of password or keytabs are set
|
- name: Install - Check if one of password or keytabs are set
|
||||||
|
|||||||
@@ -123,10 +123,12 @@ sed -i -e "s/ansible.module_utils.ansible_freeipa_module/ansible_collections.${c
|
|||||||
ln -sf ../../roles/*/library/*.py .
|
ln -sf ../../roles/*/library/*.py .
|
||||||
})
|
})
|
||||||
|
|
||||||
[ ! -x plugins/action ] && mkdir plugins/action
|
# There are no action plugins anymore in the roles, therefore this section
|
||||||
(cd plugins/action && {
|
# is commneted out.
|
||||||
ln -sf ../../roles/*/action_plugins/*.py .
|
#[ ! -x plugins/action ] && mkdir plugins/action
|
||||||
})
|
#(cd plugins/action && {
|
||||||
|
# ln -sf ../../roles/*/action_plugins/*.py .
|
||||||
|
#})
|
||||||
|
|
||||||
for doc_fragment in plugins/doc_fragments/*.py; do
|
for doc_fragment in plugins/doc_fragments/*.py; do
|
||||||
fragment=$(basename -s .py "$doc_fragment")
|
fragment=$(basename -s .py "$doc_fragment")
|
||||||
|
|||||||
Reference in New Issue
Block a user