Skip to content
Snippets Groups Projects
Commit 624e0d34 authored by Thomas Woerner's avatar Thomas Woerner
Browse files

ipaclient: No kinit on controller for deployment using OTP

The generation of the OTP for client deployment is now completely
happening on the first of the given or detected servers with delegate_to.
The module ipaclient_get_otp has been replaced by a new module using code
from ipahost module and module_utils ansible_freeipa_module.

The action plugin ipaclient_get_otp has been removed and with this also
ipaclient_get_facts.

If an admin keytab is used instead of an admin password, it is copied to
the server as a temporary file to enable the OTP generation. The temporary
file is removed again after using the ipaclient_get_otp module.

The utils script build-galaxy-release.sh has been updated to not copy the
ipaclient action plugin to the global plugins folder of the collection.

This change is import for the use of the ipaclient role with AAP as only
the base environment is sufficient now.

The ipaclient README and also the global README have been updated as
kinit is not needed anymore on the controller for OTP.

Fixes #903 (Allow the use of principals other than admin when using
            ipaadmin_keytab)
parent 9423eb81
No related branches found
No related tags found
No related merge requests found
......@@ -69,7 +69,6 @@ Requirements
**Controller**
* 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**
* Supported FreeIPA version (see above)
......@@ -289,7 +288,7 @@ ipaserver_domain=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:
```yaml
......
......@@ -32,7 +32,6 @@ Requirements
**Controller**
* Ansible version: 2.8+
* /usr/bin/kinit is required on the controller if a one time password (OTP) is used
**Node**
* Supported FreeIPA version (see above)
......
# 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)
# -*- 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()
# -*- coding: utf-8 -*-
# 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
#
# 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
ANSIBLE_METADATA = {'metadata_version': '1.0',
'status': ['preview'],
'supported_by': 'community'}
ANSIBLE_METADATA = {
"metadata_version": "1.0",
"supported_by": "community",
"status": ["preview"],
}
DOCUMENTATION = '''
DOCUMENTATION = """
---
module: ipaclient_get_otp
short_description: Manage IPA hosts
description:
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.
short_description: Get OTP for host enrollment
description: Get OTP for host enrollment
options:
principal:
description:
User Principal allowed to promote replicas and join IPA realm
type: str
required: no
ipaadmin_principal:
description: The admin principal.
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
required: yes
certificates:
description: A list of host certificates
type: list
elements: str
required: no
sshpubkey:
description: The SSH public key for the host
ipaadmin_password:
description: |
The admin password. Either ipaadmin_password or ipaadmin_keytab needs
to be given.
required: false
type: str
required: no
ipaddress:
description: The IP address for the host
ipaadmin_keytab:
description: |
The admin keytab. Either ipaadmin_password or ipaadmin_keytab needs
to be given.
type: str
required: no
random:
description: Generate a random password to be used in bulk enrollment
type: bool
required: no
default: no
state:
description: The desired host state
required: false
hostname:
description: The FQDN hostname.
type: str
choices: ['present', 'absent']
default: present
required: no
required: true
author:
- Florence Blanc-Renaud (@flo-renaud)
'''
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 = '''
- Thomas Woerner (@t-woerner)
"""
EXAMPLES = """
"""
RETURN = """
host:
description: the host structure as returned from IPA API
description: Host dict with random password
returned: always
type: complex
type: dict
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:
description: the OneTimePassword generated for this host
description: The generated random password
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 tempfile
import shutil
from contextlib import contextmanager
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.ansible_ipa_client import (
check_imports, api, errors, paths, run
)
try:
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:
unicode = str
def get_host_diff(ipa_host, module_host):
"""
Build a dict with the differences from two host dicts.
def temp_kinit(principal, password, keytab):
"""Kinit with password or keytab using a temporary ccache."""
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
try:
if password:
kinit_password(principal, password, ccache_name)
else:
kinit_keytab(principal, keytab, ccache_name)
except RuntimeError as e:
raise RuntimeError("Kerberos authentication failed: %s" % str(e))
: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.
os.environ["KRB5CCNAME"] = ccache_name
return ccache_dir, ccache_name
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):
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):
"""
Ensure host exists in IPA and has the same attributes.
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.
: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 password and not keytab:
module.fail_json(msg="One of password and keytab is required.")
if not principal:
principal = "admin"
if not diffs:
# Same attributes, success
module.exit_json(changed=False, host=ipahost)
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))
# Need to modify the host - only if not in check_mode
if module.check_mode:
module.exit_json(changed=True)
api.bootstrap(context="server", debug=env.debug, log=None)
api.finalize()
# 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)
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)
# 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):
def ipa_command(command, name, args):
"""
Ensure host does not exist in IPA.
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.
: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)
return api.Command[command](name, **args)
# 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'))
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:
_api.Command.host_del(fqdn)
except Exception as e:
module.fail_json(msg="Failed to remove host: %s" % e)
_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)
module.exit_json(changed=True)
return _result["result"]
def main():
module = AnsibleModule(
argument_spec=dict(
principal=dict(required=False, type='str', default='admin'),
ccache=dict(required=False, type='path'),
fqdn=dict(required=True, type='str'),
certificates=dict(required=False, type='list', elements='str'),
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'),
ipaadmin_principal=dict(type="str", default="admin"),
ipaadmin_password=dict(type="str", required=False, no_log=True),
ipaadmin_keytab=dict(type="str", required=False, no_log=False),
hostname=dict(type="str", required=True),
),
mutually_exclusive=[["ipaadmin_password", "ipaadmin_keytab"]],
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')
fqdn = unicode(module.params.get('fqdn'))
state = module.params.get('state')
# In check mode always return changed.
if module.check_mode:
module.exit_json(changed=True)
try:
os.environ['KRB5CCNAME'] = ccache
cfg = dict(
context='ansible_module',
confdir=paths.ETC_IPA,
in_server=False,
debug=False,
verbose=0,
)
api.bootstrap(**cfg)
api.finalize()
api.Backend.rpcclient.connect()
ipaadmin_principal = module_params_get(module, "ipaadmin_principal")
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)
try:
result = api.Command.host_show(fqdn, all=True)
host = result['result']
except errors.NotFound:
host = None
hostname = module_params_get(module, "hostname")
if state in ['present', 'disabled']:
ensure_host_present(module, api, host)
elif state == 'absent':
ensure_host_absent(module, api, host)
exit_args = {}
except Exception as e:
module.fail_json(msg="ipaclient_get_otp module failed : %s" % str(e))
finally:
run([paths.KDESTROY], raiseonerr=False, env=os.environ)
# Connect to IPA API
with ipa_connect(module, ipaadmin_principal, ipaadmin_password,
ipaadmin_keytab):
res_show = host_show(module, hostname)
args = {"random": True}
if res_show is None:
# Create new host, force is needed to create the host without
# 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)
exit_args["randompassword"] = result['result']['randompassword']
module.exit_json(changed=False, host=host)
module.exit_json(changed=True, host=exit_args)
if __name__ == '__main__':
if __name__ == "__main__":
main()
......@@ -18,9 +18,9 @@
when: ipaclient_no_dns_lookup | bool and groups.ipaserver is defined and
ipaclient_servers is not defined
- name: Install - Check that either principal or keytab is set
fail: msg="ipaadmin_principal and ipaadmin_keytab cannot be used together"
when: ipaadmin_keytab is defined and ipaadmin_principal is defined
- name: Install - Check that either password or keytab is set
fail: msg="ipaadmin_password and ipaadmin_keytab cannot be used together"
when: ipaadmin_keytab is defined and ipaadmin_password is defined
- name: Install - Set default principal if no keytab is given
set_fact:
......@@ -99,35 +99,44 @@
not ipaclient_force_join | bool
# 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
# is not set.
# the OTP isn't predefined, ie when ipaclient_use_otp is set and
# ipaclient_otp is not set.
# It connects to ipaserver and add the host with --random option in order
# to create a OneTime Password
# If a keytab is specified in the hostent, then the hostent will be disabled
# if ipaclient_use_otp is set.
- block:
- 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
- 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
no_log: yes
ipaclient_get_otp:
state: present
principal: "{{ ipaadmin_principal | default(omit) }}"
password: "{{ ipaadmin_password | default(omit) }}"
keytab: "{{ ipaadmin_keytab | default(omit) }}"
fqdn: "{{ result_ipaclient_test.hostname }}"
lifetime: "{{ ipaclient_lifetime | default(omit) }}"
random: True
ipaadmin_principal: "{{ ipaadmin_principal | default(omit) }}"
ipaadmin_password: "{{ ipaadmin_password | default(omit) }}"
ipaadmin_keytab: "{{ keytab_temp.path | default(omit) }}"
hostname: "{{ result_ipaclient_test.hostname }}"
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] }}"
ignore_errors: yes
- name: Install - Report error for OTP generation
debug:
......@@ -144,6 +153,14 @@
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
no_log: yes
set_fact:
......@@ -161,7 +178,7 @@
# result_ipaclient_join.already_joined)))
- 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
- 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 .
})
[ ! -x plugins/action ] && mkdir plugins/action
(cd plugins/action && {
ln -sf ../../roles/*/action_plugins/*.py .
})
# There are no action plugins anymore in the roles, therefore this section
# is commneted out.
#[ ! -x plugins/action ] && mkdir plugins/action
#(cd plugins/action && {
# ln -sf ../../roles/*/action_plugins/*.py .
#})
for doc_fragment in plugins/doc_fragments/*.py; do
fragment=$(basename -s .py "$doc_fragment")
......
0% Loading or .
You are about to add 0 people to the discussion. Proceed with caution.
Please register or to comment