mirror of
https://github.com/freeipa/ansible-freeipa.git
synced 2026-06-23 09:14:43 +00:00
ipclient: Move library and action_plugins into ipaclient role directory
The directories library and action_plugins do only contain ipaclient specific modules and plugins. Therefore these directories should be located in the ipaclient role directory.
This commit is contained in:
242
roles/ipaclient/action_plugins/ipahost.py
Normal file
242
roles/ipaclient/action_plugins/ipahost.py
Normal file
@@ -0,0 +1,242 @@
|
||||
# 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/>.
|
||||
|
||||
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 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 = 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 (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='ipa_facts', module_args=dict(),
|
||||
task_vars=None)
|
||||
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:
|
||||
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])
|
||||
175
roles/ipaclient/library/ipa_facts.py
Normal file
175
roles/ipaclient/library/ipa_facts.py
Normal file
@@ -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()
|
||||
208
roles/ipaclient/library/ipaapi.py
Normal file
208
roles/ipaclient/library/ipaapi.py
Normal file
@@ -0,0 +1,208 @@
|
||||
#!/usr/bin/python
|
||||
# -*- coding: utf-8 -*-
|
||||
|
||||
# Authors:
|
||||
# Thomas Woerner <twoerner@redhat.com>
|
||||
#
|
||||
# Based on ipa-client-install code
|
||||
#
|
||||
# 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/>.
|
||||
|
||||
ANSIBLE_METADATA = {'metadata_version': '1.0',
|
||||
'status': ['preview'],
|
||||
'supported_by': 'community'}
|
||||
|
||||
DOCUMENTATION = '''
|
||||
---
|
||||
module: ipaapi
|
||||
short description: Create temporary NSS database, call IPA API for remaining enrollment parts
|
||||
description:
|
||||
Create temporary NSS database, call IPA API for remaining enrollment parts
|
||||
options:
|
||||
realm:
|
||||
description: The Kerberos realm of an existing IPA deployment.
|
||||
required: true
|
||||
hostname:
|
||||
description: The hostname of the machine to join (FQDN).
|
||||
required: true
|
||||
debug:
|
||||
description: Turn on extra debugging
|
||||
required: false
|
||||
type: bool
|
||||
default: no
|
||||
author:
|
||||
- Thomas Woerner
|
||||
'''
|
||||
|
||||
EXAMPLES = '''
|
||||
- name: IPA API calls for remaining enrollment parts
|
||||
ipaapi:
|
||||
servers: ["server1.example.com","server2.example.com"]
|
||||
domain: example.com
|
||||
hostname: client1.example.com
|
||||
register: ipaapi
|
||||
'''
|
||||
|
||||
RETURN = '''
|
||||
ca_enabled:
|
||||
description: Wheter the Certificate Authority is enabled or not.
|
||||
returned: always
|
||||
type: bool
|
||||
subject_base:
|
||||
description: The subject base, needed for certmonger
|
||||
returned: always
|
||||
type: string
|
||||
sample: O=EXAMPLE.COM
|
||||
'''
|
||||
|
||||
import os
|
||||
import sys
|
||||
import time
|
||||
import tempfile
|
||||
import inspect
|
||||
|
||||
from ansible.module_utils.basic import AnsibleModule
|
||||
from ansible.module_utils.ansible_ipa_client import *
|
||||
|
||||
def main():
|
||||
module = AnsibleModule(
|
||||
argument_spec = dict(
|
||||
servers=dict(required=True, type='list'),
|
||||
realm=dict(required=True),
|
||||
hostname=dict(required=True),
|
||||
debug=dict(required=False, type='bool', default="false")
|
||||
),
|
||||
supports_check_mode = True,
|
||||
)
|
||||
|
||||
module._ansible_debug = True
|
||||
realm = module.params.get('realm')
|
||||
hostname = module.params.get('hostname')
|
||||
servers = module.params.get('servers')
|
||||
debug = module.params.get('debug')
|
||||
|
||||
host_principal = 'host/%s@%s' % (hostname, realm)
|
||||
os.environ['KRB5CCNAME'] = paths.IPA_DNS_CCACHE
|
||||
|
||||
ca_certs = x509.load_certificate_list_from_file(paths.IPA_CA_CRT)
|
||||
if NUM_VERSION >= 40500 and NUM_VERSION < 40590:
|
||||
ca_certs = [ cert.public_bytes(serialization.Encoding.DER)
|
||||
for cert in ca_certs ]
|
||||
elif NUM_VERSION < 40500:
|
||||
ca_certs = [ cert.der_data for cert in ca_certs ]
|
||||
|
||||
with certdb.NSSDatabase() as tmp_db:
|
||||
api.bootstrap(context='cli_installer',
|
||||
confdir=paths.ETC_IPA,
|
||||
debug=debug,
|
||||
delegate=False,
|
||||
nss_dir=tmp_db.secdir)
|
||||
|
||||
if 'config_loaded' not in api.env:
|
||||
module.fail_json(msg="Failed to initialize IPA API.")
|
||||
|
||||
# Clear out any current session keyring information
|
||||
try:
|
||||
delete_persistent_client_session_data(host_principal)
|
||||
except ValueError:
|
||||
pass
|
||||
|
||||
# Add CA certs to a temporary NSS database
|
||||
try:
|
||||
if NUM_VERSION > 40404:
|
||||
tmp_db.create_db()
|
||||
|
||||
for i, cert in enumerate(ca_certs):
|
||||
tmp_db.add_cert(cert,
|
||||
'CA certificate %d' % (i + 1),
|
||||
certdb.EXTERNAL_CA_TRUST_FLAGS)
|
||||
else:
|
||||
pwd_file = write_tmp_file(ipa_generate_password())
|
||||
tmp_db.create_db(pwd_file.name)
|
||||
|
||||
for i, cert in enumerate(ca_certs):
|
||||
tmp_db.add_cert(cert, 'CA certificate %d' % (i + 1), 'C,,')
|
||||
except CalledProcessError as e:
|
||||
module.fail_json(msg="Failed to add CA to temporary NSS database.")
|
||||
|
||||
api.finalize()
|
||||
|
||||
# Now, let's try to connect to the server's RPC interface
|
||||
connected = False
|
||||
try:
|
||||
api.Backend.rpcclient.connect()
|
||||
connected = True
|
||||
module.debug("Try RPC connection")
|
||||
api.Backend.rpcclient.forward('ping')
|
||||
except errors.KerberosError as e:
|
||||
if connected:
|
||||
api.Backend.rpcclient.disconnect()
|
||||
module.log(
|
||||
"Cannot connect to the server due to Kerberos error: %s. "
|
||||
"Trying with delegate=True" % e)
|
||||
try:
|
||||
api.Backend.rpcclient.connect(delegate=True)
|
||||
module.debug("Try RPC connection")
|
||||
api.Backend.rpcclient.forward('ping')
|
||||
|
||||
module.log("Connection with delegate=True successful")
|
||||
|
||||
# The remote server is not capable of Kerberos S4U2Proxy
|
||||
# delegation. This features is implemented in IPA server
|
||||
# version 2.2 and higher
|
||||
module.warn(
|
||||
"Target IPA server has a lower version than the enrolled "
|
||||
"client")
|
||||
module.warn(
|
||||
"Some capabilities including the ipa command capability "
|
||||
"may not be available")
|
||||
except errors.PublicError as e2:
|
||||
module.fail_json(
|
||||
msg="Cannot connect to the IPA server RPC interface: %s" % e2)
|
||||
except errors.PublicError as e:
|
||||
module.fail_json(
|
||||
msg="Cannot connect to the server due to generic error: %s" % e)
|
||||
# Use the RPC directly so older servers are supported
|
||||
try:
|
||||
result = api.Backend.rpcclient.forward(
|
||||
'ca_is_enabled',
|
||||
version=u'2.107',
|
||||
)
|
||||
ca_enabled = result['result']
|
||||
except (errors.CommandError, errors.NetworkError):
|
||||
result = api.Backend.rpcclient.forward(
|
||||
'env',
|
||||
server=True,
|
||||
version=u'2.0',
|
||||
)
|
||||
ca_enabled = result['result']['enable_ra']
|
||||
if not ca_enabled:
|
||||
disable_ra()
|
||||
|
||||
# Get subject base from ipa server
|
||||
try:
|
||||
config = api.Command['config_show']()['result']
|
||||
subject_base = str(DN(config['ipacertificatesubjectbase'][0]))
|
||||
except errors.PublicError as e:
|
||||
module.fail_json(msg="Cannot get subject base from server: %s" % e)
|
||||
|
||||
module.exit_json(changed=True,
|
||||
ca_enabled=ca_enabled,
|
||||
subject_base=subject_base)
|
||||
|
||||
if __name__ == '__main__':
|
||||
main()
|
||||
487
roles/ipaclient/library/ipadiscovery.py
Normal file
487
roles/ipaclient/library/ipadiscovery.py
Normal file
@@ -0,0 +1,487 @@
|
||||
#!/usr/bin/python
|
||||
# -*- coding: utf-8 -*-
|
||||
|
||||
# Authors:
|
||||
# Thomas Woerner <twoerner@redhat.com>
|
||||
#
|
||||
# Based on ipa-client-install code
|
||||
#
|
||||
# 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/>.
|
||||
|
||||
ANSIBLE_METADATA = {
|
||||
'metadata_version': '1.0',
|
||||
'supported_by': 'community',
|
||||
'status': ['preview'],
|
||||
}
|
||||
|
||||
DOCUMENTATION = '''
|
||||
---
|
||||
module: ipadiscovery
|
||||
short description: Tries to discover IPA server
|
||||
description:
|
||||
Tries to discover IPA server using DNS or host name
|
||||
options:
|
||||
servers:
|
||||
description: The FQDN of the IPA servers to connect to.
|
||||
required: false
|
||||
type: list
|
||||
default: []
|
||||
domain:
|
||||
description: The primary DNS domain of an existing IPA deployment.
|
||||
required: false
|
||||
realm:
|
||||
description: The Kerberos realm of an existing IPA deployment.
|
||||
required: false
|
||||
hostname:
|
||||
description: The hostname of the machine to join (FQDN).
|
||||
required: false
|
||||
ca_cert_file:
|
||||
description: A CA certificate to use.
|
||||
required: false
|
||||
on_master:
|
||||
description: IPA client installation on IPA server
|
||||
required: false
|
||||
default: false
|
||||
type: bool
|
||||
default: no
|
||||
ntp_servers:
|
||||
description: List of NTP servers to use
|
||||
required: false
|
||||
type: list
|
||||
default: []
|
||||
no_ntp:
|
||||
description: Do not sync time and do not detect time servers
|
||||
required: false
|
||||
default: false
|
||||
type: bool
|
||||
default: no
|
||||
author:
|
||||
- Thomas Woerner
|
||||
'''
|
||||
|
||||
EXAMPLES = '''
|
||||
# Complete autodiscovery, register return values as ipadiscovery
|
||||
- name: IPA discovery
|
||||
ipadiscovery:
|
||||
register: ipadiscovery
|
||||
|
||||
# Discovery using servers, register return values as ipadiscovery
|
||||
- name: IPA discovery
|
||||
ipadiscovery:
|
||||
servers: server1.domain.com,server2.domain.com
|
||||
register: ipadiscovery
|
||||
|
||||
# Discovery using domain name, register return values as ipadiscovery
|
||||
- name: IPA discovery
|
||||
ipadiscovery:
|
||||
domain: domain.com
|
||||
register: ipadiscovery
|
||||
|
||||
# Discovery using realm, register return values as ipadiscovery
|
||||
- name: IPA discovery
|
||||
ipadiscovery:
|
||||
realm: DOMAIN.COM
|
||||
register: ipadiscovery
|
||||
|
||||
# Discovery using hostname, register return values as ipadiscovery
|
||||
- name: IPA discovery
|
||||
ipadiscovery:
|
||||
hostname: host.domain.com
|
||||
register: ipadiscovery
|
||||
'''
|
||||
|
||||
RETURN = '''
|
||||
servers:
|
||||
description: The list of detected or passed in IPA servers.
|
||||
returned: always
|
||||
type: list
|
||||
sample: ["server1.example.com","server2.example.com"]
|
||||
domain:
|
||||
description: The DNS domain of the detected or passed in IPA deployment.
|
||||
returned: always
|
||||
type: string
|
||||
sample: example.com
|
||||
realm:
|
||||
description: The Kerberos realm of the detected or passed in IPA deployment.
|
||||
returned: always
|
||||
type: string
|
||||
sample: EXAMPLE.COM
|
||||
kdc:
|
||||
description: The detected KDC server name.
|
||||
returned: always
|
||||
type: string
|
||||
sample: server1.example.com
|
||||
basedn:
|
||||
description: The basedn of the detected IPA server.
|
||||
returned: always
|
||||
type: string
|
||||
sample: dc=example,dc=com
|
||||
hostname:
|
||||
description: The detected or passed in FQDN hostname of the client.
|
||||
returned: always
|
||||
type: string
|
||||
sample: client1.example.com
|
||||
client_domain:
|
||||
description: The domain name of the client.
|
||||
returned: always
|
||||
type: string
|
||||
sample: example.com
|
||||
dnsok:
|
||||
description: True if DNS discovery worked and not passed in any servers.
|
||||
returned: always
|
||||
type: bool
|
||||
ntp_servers:
|
||||
description: The list of detected NTP servers.
|
||||
returned: always
|
||||
type: list
|
||||
sample: ["ntp.example.com"]
|
||||
ipa_python_version:
|
||||
description: The IPA python version as a number: <major version>*10000+<minor version>*100+<release>
|
||||
returned: always
|
||||
type: int
|
||||
sample: 040400
|
||||
'''
|
||||
|
||||
import os
|
||||
import socket
|
||||
|
||||
from six.moves.configparser import RawConfigParser
|
||||
from ansible.module_utils.basic import AnsibleModule
|
||||
from ansible.module_utils.ansible_ipa_client import *
|
||||
|
||||
def get_cert_path(cert_path):
|
||||
"""
|
||||
If a CA certificate is passed in on the command line, use that.
|
||||
|
||||
Else if a CA file exists in paths.IPA_CA_CRT then use that.
|
||||
|
||||
Otherwise return None.
|
||||
"""
|
||||
if cert_path is not None:
|
||||
return cert_path
|
||||
|
||||
if os.path.exists(paths.IPA_CA_CRT):
|
||||
return paths.IPA_CA_CRT
|
||||
|
||||
return None
|
||||
|
||||
def is_client_configured():
|
||||
"""
|
||||
Check if ipa client is configured.
|
||||
|
||||
IPA client is configured when /etc/ipa/default.conf exists and
|
||||
/var/lib/ipa-client/sysrestore/sysrestore.state exists.
|
||||
|
||||
:returns: boolean
|
||||
"""
|
||||
|
||||
return (os.path.isfile(paths.IPA_DEFAULT_CONF) and
|
||||
os.path.isfile(os.path.join(paths.IPA_CLIENT_SYSRESTORE,
|
||||
sysrestore.SYSRESTORE_STATEFILE)))
|
||||
|
||||
def get_ipa_conf():
|
||||
"""
|
||||
Return IPA configuration read from /etc/ipa/default.conf
|
||||
|
||||
:returns: dict containing key,value
|
||||
"""
|
||||
|
||||
parser = RawConfigParser()
|
||||
parser.read(paths.IPA_DEFAULT_CONF)
|
||||
result = dict()
|
||||
for item in ['basedn', 'realm', 'domain', 'server', 'host', 'xmlrpc_uri']:
|
||||
if parser.has_option('global', item):
|
||||
value = parser.get('global', item)
|
||||
else:
|
||||
value = None
|
||||
if value:
|
||||
result[item] = value
|
||||
|
||||
return result
|
||||
|
||||
def main():
|
||||
module = AnsibleModule(
|
||||
argument_spec = dict(
|
||||
servers=dict(required=False, type='list', default=[]),
|
||||
domain=dict(required=False),
|
||||
realm=dict(required=False),
|
||||
hostname=dict(required=False),
|
||||
ca_cert_file=dict(required=False),
|
||||
on_master=dict(required=False, type='bool', default=False),
|
||||
ntp_servers=dict(required=False, type='list', default=[]),
|
||||
no_ntp=dict(required=False, type='bool', default=False),
|
||||
),
|
||||
supports_check_mode = True,
|
||||
)
|
||||
|
||||
module._ansible_debug = True
|
||||
opt_domain = module.params.get('domain')
|
||||
opt_servers = module.params.get('servers')
|
||||
opt_realm = module.params.get('realm')
|
||||
opt_hostname = module.params.get('hostname')
|
||||
opt_ca_cert_file = module.params.get('ca_cert_file')
|
||||
opt_on_master = module.params.get('on_master')
|
||||
opt_ntp_servers = module.params.get('ntp_servers')
|
||||
opt_no_ntp = module.params.get('no_ntp')
|
||||
|
||||
hostname = None
|
||||
hostname_source = None
|
||||
dnsok = False
|
||||
cli_domain = None
|
||||
cli_server = None
|
||||
cli_realm = None
|
||||
cli_kdc = None
|
||||
client_domain = None
|
||||
cli_basedn = None
|
||||
|
||||
if opt_hostname:
|
||||
hostname = opt_hostname
|
||||
hostname_source = 'Provided as option'
|
||||
else:
|
||||
hostname = socket.getfqdn()
|
||||
hostname_source = "Machine's FQDN"
|
||||
if hostname != hostname.lower():
|
||||
module.fail_json(
|
||||
msg="Invalid hostname '%s', must be lower-case." % hostname)
|
||||
|
||||
if (hostname == 'localhost') or (hostname == 'localhost.localdomain'):
|
||||
module.fail_json(
|
||||
msg="Invalid hostname, '%s' must not be used." % hostname)
|
||||
|
||||
# Get domain from first server if domain is not set, but there are servers
|
||||
if opt_domain is None and len(opt_servers) > 0:
|
||||
opt_domain = opt_servers[0][opt_servers[0].find(".")+1:]
|
||||
|
||||
# Create the discovery instance
|
||||
ds = ipadiscovery.IPADiscovery()
|
||||
|
||||
ret = ds.search(
|
||||
domain=opt_domain,
|
||||
servers=opt_servers,
|
||||
realm=opt_realm,
|
||||
hostname=hostname,
|
||||
ca_cert_path=get_cert_path(opt_ca_cert_file))
|
||||
|
||||
if opt_servers and ret != 0:
|
||||
# There is no point to continue with installation as server list was
|
||||
# passed as a fixed list of server and thus we cannot discover any
|
||||
# better result
|
||||
module.fail_json(msg="Failed to verify that %s is an IPA Server." % \
|
||||
', '.join(opt_servers))
|
||||
|
||||
if ret == ipadiscovery.BAD_HOST_CONFIG:
|
||||
module.fail_json(msg="Can't get the fully qualified name of this host")
|
||||
if ret == ipadiscovery.NOT_FQDN:
|
||||
module.fail_json(msg="%s is not a fully-qualified hostname" % hostname)
|
||||
if ret in (ipadiscovery.NO_LDAP_SERVER, ipadiscovery.NOT_IPA_SERVER) \
|
||||
or not ds.domain:
|
||||
if ret == ipadiscovery.NO_LDAP_SERVER:
|
||||
if ds.server:
|
||||
module.log("%s is not an LDAP server" % ds.server)
|
||||
else:
|
||||
module.log("No LDAP server found")
|
||||
elif ret == ipadiscovery.NOT_IPA_SERVER:
|
||||
if ds.server:
|
||||
module.log("%s is not an IPA server" % ds.server)
|
||||
else:
|
||||
module.log("No IPA server found")
|
||||
else:
|
||||
module.log("Domain not found")
|
||||
if opt_domain:
|
||||
cli_domain = opt_domain
|
||||
cli_domain_source = 'Provided as option'
|
||||
else:
|
||||
module.fail_json(
|
||||
msg="Unable to discover domain, not provided")
|
||||
|
||||
ret = ds.search(
|
||||
domain=cli_domain,
|
||||
servers=opt_servers,
|
||||
hostname=hostname,
|
||||
ca_cert_path=get_cert_path(opt_ca_cert_file))
|
||||
|
||||
if not cli_domain:
|
||||
if ds.domain:
|
||||
cli_domain = ds.domain
|
||||
cli_domain_source = ds.domain_source
|
||||
module.debug("will use discovered domain: %s" % cli_domain)
|
||||
|
||||
client_domain = hostname[hostname.find(".")+1:]
|
||||
|
||||
if ret in (ipadiscovery.NO_LDAP_SERVER, ipadiscovery.NOT_IPA_SERVER) \
|
||||
or not ds.server:
|
||||
module.debug("IPA Server not found")
|
||||
if opt_servers:
|
||||
cli_server = opt_servers
|
||||
cli_server_source = 'Provided as option'
|
||||
else:
|
||||
module.fail_json(msg="Unable to find IPA Server to join")
|
||||
|
||||
ret = ds.search(
|
||||
domain=cli_domain,
|
||||
servers=cli_server,
|
||||
hostname=hostname,
|
||||
ca_cert_path=get_cert_path(opt_ca_cert_file))
|
||||
|
||||
else:
|
||||
# Only set dnsok to True if we were not passed in one or more servers
|
||||
# and if DNS discovery actually worked.
|
||||
if not opt_servers:
|
||||
(server, domain) = ds.check_domain(
|
||||
ds.domain, set(), "Validating DNS Discovery")
|
||||
if server and domain:
|
||||
module.debug("DNS validated, enabling discovery")
|
||||
dnsok = True
|
||||
else:
|
||||
module.debug("DNS discovery failed, disabling discovery")
|
||||
else:
|
||||
module.debug(
|
||||
"Using servers from command line, disabling DNS discovery")
|
||||
|
||||
if not cli_server:
|
||||
if opt_servers:
|
||||
cli_server = ds.servers
|
||||
cli_server_source = 'Provided as option'
|
||||
module.debug(
|
||||
"will use provided server: %s" % ', '.join(opt_servers))
|
||||
elif ds.server:
|
||||
cli_server = ds.servers
|
||||
cli_server_source = ds.server_source
|
||||
module.debug("will use discovered server: %s" % cli_server[0])
|
||||
|
||||
if ret == ipadiscovery.NOT_IPA_SERVER:
|
||||
module.fail_json(msg="%s is not an IPA v2 Server." % cli_server[0])
|
||||
|
||||
if ret == ipadiscovery.NO_ACCESS_TO_LDAP:
|
||||
module.warn("Anonymous access to the LDAP server is disabled.")
|
||||
ret = 0
|
||||
|
||||
if ret == ipadiscovery.NO_TLS_LDAP:
|
||||
module.warn(
|
||||
"The LDAP server requires TLS is but we do not have the CA.")
|
||||
ret = 0
|
||||
|
||||
if ret != 0:
|
||||
module.fail_json(
|
||||
msg="Failed to verify that %s is an IPA Server." % cli_server[0])
|
||||
|
||||
cli_kdc = ds.kdc
|
||||
if dnsok and not cli_kdc:
|
||||
module.fail_json(
|
||||
msg="DNS domain '%s' is not configured for automatic "
|
||||
"KDC address lookup." % ds.realm.lower())
|
||||
|
||||
if dnsok:
|
||||
module.log("Discovery was successful!")
|
||||
|
||||
cli_realm = ds.realm
|
||||
cli_realm_source = ds.realm_source
|
||||
module.debug("will use discovered realm: %s" % cli_realm)
|
||||
|
||||
if opt_realm and opt_realm != cli_realm:
|
||||
module.fail_json(
|
||||
msg=
|
||||
"The provided realm name [%s] does not match discovered one [%s]" %
|
||||
(opt_realm, cli_realm))
|
||||
|
||||
cli_basedn = str(ds.basedn)
|
||||
cli_basedn_source = ds.basedn_source
|
||||
module.debug("will use discovered basedn: %s" % cli_basedn)
|
||||
|
||||
module.log("Client hostname: %s" % hostname)
|
||||
module.debug("Hostname source: %s" % hostname_source)
|
||||
module.log("Realm: %s" % cli_realm)
|
||||
module.debug("Realm source: %s" % cli_realm_source)
|
||||
module.log("DNS Domain: %s" % cli_domain)
|
||||
module.debug("DNS Domain source: %s" % cli_domain_source)
|
||||
module.log("IPA Server: %s" % ', '.join(cli_server))
|
||||
module.debug("IPA Server source: %s" % cli_server_source)
|
||||
module.log("BaseDN: %s" % cli_basedn)
|
||||
module.debug("BaseDN source: %s" % cli_basedn_source)
|
||||
|
||||
# ipa-join would fail with IP address instead of a FQDN
|
||||
for srv in cli_server:
|
||||
try:
|
||||
socket.inet_pton(socket.AF_INET, srv)
|
||||
is_ipaddr = True
|
||||
except socket.error:
|
||||
try:
|
||||
socket.inet_pton(socket.AF_INET6, srv)
|
||||
is_ipaddr = True
|
||||
except socket.error:
|
||||
is_ipaddr = False
|
||||
|
||||
if is_ipaddr:
|
||||
module.warn(
|
||||
"It seems that you are using an IP address "
|
||||
"instead of FQDN as an argument to --server. The "
|
||||
"installation may fail.")
|
||||
break
|
||||
|
||||
if not opt_on_master and not opt_no_ntp:
|
||||
if len(opt_ntp_servers) < 1:
|
||||
# Detect NTP servers
|
||||
ds = ipadiscovery.IPADiscovery()
|
||||
ntp_servers = ds.ipadns_search_srv(cli_domain, '_ntp._udp',
|
||||
None, break_on_first=False)
|
||||
else:
|
||||
ntp_servers = opt_ntp_servers
|
||||
|
||||
# Attempt to sync time:
|
||||
# At first with given or dicovered time servers. If no ntp
|
||||
# servers have been given or discovered, then with the ipa
|
||||
# server.
|
||||
module.log('Synchronizing time ...')
|
||||
synced_ntp = False
|
||||
# use user specified NTP servers if there are any
|
||||
for s in ntp_servers:
|
||||
synced_ntp = ntpconf.synconce_ntp(s, False)
|
||||
if synced_ntp:
|
||||
break
|
||||
if not synced_ntp and not ntp_servers:
|
||||
synced_ntp = ntpconf.synconce_ntp(cli_server[0], False)
|
||||
if not synced_ntp:
|
||||
module.warn("Unable to sync time with NTP server")
|
||||
else:
|
||||
ntp_servers = [ ]
|
||||
|
||||
# Check if ipa client is already configured
|
||||
if is_client_configured():
|
||||
# Check that realm and domain match
|
||||
current_config = get_ipa_conf()
|
||||
if cli_domain != current_config.get('domain'):
|
||||
return module.fail_json(msg="IPA client already installed "
|
||||
"with a conflicting domain")
|
||||
if cli_realm != current_config.get('realm'):
|
||||
return module.fail_json(msg="IPA client already installed "
|
||||
"with a conflicting realm")
|
||||
|
||||
# Done
|
||||
module.exit_json(changed=True,
|
||||
servers=cli_server,
|
||||
domain=cli_domain,
|
||||
realm=cli_realm,
|
||||
kdc=cli_kdc,
|
||||
basedn=cli_basedn,
|
||||
hostname=hostname,
|
||||
client_domain=client_domain,
|
||||
dnsok=dnsok,
|
||||
ntp_servers=ntp_servers,
|
||||
ipa_python_version=IPA_PYTHON_VERSION)
|
||||
|
||||
if __name__ == '__main__':
|
||||
main()
|
||||
209
roles/ipaclient/library/ipaextras.py
Normal file
209
roles/ipaclient/library/ipaextras.py
Normal file
@@ -0,0 +1,209 @@
|
||||
#!/usr/bin/python
|
||||
# -*- coding: utf-8 -*-
|
||||
|
||||
# Authors:
|
||||
# Thomas Woerner <twoerner@redhat.com>
|
||||
#
|
||||
# Based on ipa-client-install code
|
||||
#
|
||||
# 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/>.
|
||||
|
||||
ANSIBLE_METADATA = {
|
||||
'metadata_version': '1.0',
|
||||
'supported_by': 'community',
|
||||
'status': ['preview'],
|
||||
}
|
||||
|
||||
DOCUMENTATION = '''
|
||||
---
|
||||
module: ipaextras
|
||||
short description: Configure IPA extras
|
||||
description:
|
||||
Configure IPA extras
|
||||
options:
|
||||
servers:
|
||||
description: The FQDN of the IPA servers to connect to.
|
||||
required: true
|
||||
type: list
|
||||
domain:
|
||||
description: The primary DNS domain of an existing IPA deployment.
|
||||
required: true
|
||||
ntp:
|
||||
description: Set to no to not configure and enable NTP
|
||||
required: false
|
||||
type: bool
|
||||
default: no
|
||||
force_ntpd:
|
||||
description: Stop and disable any time&date synchronization services besides ntpd.
|
||||
required: false
|
||||
type: bool
|
||||
default: no
|
||||
ntp_servers:
|
||||
description: The ntp servers to configure if ntp is enabled.
|
||||
required: false
|
||||
type: list
|
||||
ssh:
|
||||
description: Configure OpenSSH client
|
||||
required: false
|
||||
type: bool
|
||||
default: yes
|
||||
sssd:
|
||||
description: Configure the client to use SSSD for authentication
|
||||
required: false
|
||||
type: bool
|
||||
default: yes
|
||||
trust_sshfp:
|
||||
description: Configure OpenSSH client to trust DNS SSHFP records
|
||||
required: false
|
||||
type: bool
|
||||
default: yes
|
||||
sshd:
|
||||
description: Configure OpenSSH server
|
||||
required: false
|
||||
type: bool
|
||||
default: yes
|
||||
automount_location:
|
||||
description: Automount location
|
||||
required: false
|
||||
firefox:
|
||||
description: Configure Firefox to use IPA domain credentials
|
||||
required: false
|
||||
type: bool
|
||||
default: no
|
||||
firefox_dir:
|
||||
description: Specify directory where Firefox is installed (for example: '/usr/lib/firefox')
|
||||
required: false
|
||||
no_nisdomain:
|
||||
description: Do not configure NIS domain name
|
||||
required: false
|
||||
type: bool
|
||||
default: no
|
||||
nisdomain:
|
||||
description: NIS domain name
|
||||
required: false
|
||||
on_master:
|
||||
description: Whether the configuration is done on the master or not.
|
||||
required: false
|
||||
type: bool
|
||||
default: no
|
||||
author:
|
||||
- Thomas Woerner
|
||||
'''
|
||||
|
||||
EXAMPLES = '''
|
||||
- name: IPA extras configurations
|
||||
ipaextras:
|
||||
servers: ["server1.example.com","server2.example.com"]
|
||||
domain: example.com
|
||||
'''
|
||||
|
||||
RETURN = '''
|
||||
'''
|
||||
|
||||
import os
|
||||
|
||||
from ansible.module_utils.basic import AnsibleModule
|
||||
from ansible.module_utils.ansible_ipa_client import *
|
||||
|
||||
def main():
|
||||
module = AnsibleModule(
|
||||
argument_spec = dict(
|
||||
servers=dict(required=True, type='list'),
|
||||
domain=dict(required=True),
|
||||
ntp=dict(required=False, type='bool', default='no'),
|
||||
force_ntpd=dict(required=False, type='bool', default='no'),
|
||||
ntp_servers=dict(required=False, type='list'),
|
||||
ssh=dict(required=False, type='bool', default='yes'),
|
||||
sssd=dict(required=False, type='bool', default='yes'),
|
||||
trust_sshfp=dict(required=False, type='bool', default='yes'),
|
||||
sshd=dict(required=False, type='bool', default='yes'),
|
||||
automount_location=dict(required=False),
|
||||
firefox=dict(required=False, type='bool', default='no'),
|
||||
firefox_dir=dict(required=False),
|
||||
no_nisdomain=dict(required=False, type='bool', default='no'),
|
||||
nisdomain=dict(required=False),
|
||||
on_master=dict(required=False, type='bool', default='no'),
|
||||
),
|
||||
supports_check_mode = True,
|
||||
)
|
||||
|
||||
module._ansible_debug = True
|
||||
servers = module.params.get('servers')
|
||||
domain = module.params.get('domain')
|
||||
ntp = module.params.get('ntp')
|
||||
force_ntpd = module.params.get('force_ntpd')
|
||||
ntp_servers = module.params.get('ntp_servers')
|
||||
ssh = module.params.get('ssh')
|
||||
sssd = module.params.get('sssd')
|
||||
trust_sshfp = module.params.get('trust_sshfp')
|
||||
sshd = module.params.get('sshd')
|
||||
automount_location = module.params.get('automount_location')
|
||||
firefox = module.params.get('firefox')
|
||||
firefox_dir = module.params.get('firefox_dir')
|
||||
no_nisdomain = module.params.get('no_nisdomain')
|
||||
nisdomain = module.params.get('nisdomain')
|
||||
on_master = module.params.get('on_master')
|
||||
|
||||
fstore = sysrestore.FileStore(paths.IPA_CLIENT_SYSRESTORE)
|
||||
statestore = sysrestore.StateFile(paths.IPA_CLIENT_SYSRESTORE)
|
||||
|
||||
os.environ['KRB5CCNAME'] = paths.IPA_DNS_CCACHE
|
||||
|
||||
options.sssd = sssd
|
||||
options.trust_sshfp = trust_sshfp
|
||||
options.location = automount_location
|
||||
options.server = servers
|
||||
options.firefox_dir = firefox_dir
|
||||
options.nisdomain = nisdomain
|
||||
|
||||
if ntp and not on_master:
|
||||
# disable other time&date services first
|
||||
if force_ntpd:
|
||||
ntpconf.force_ntpd(statestore)
|
||||
|
||||
ntpconf.config_ntp(ntp_servers, fstore, statestore)
|
||||
module.log("NTP enabled")
|
||||
|
||||
if ssh:
|
||||
configure_ssh_config(fstore, options)
|
||||
|
||||
if sshd:
|
||||
configure_sshd_config(fstore, options)
|
||||
|
||||
if automount_location:
|
||||
configure_automount(options)
|
||||
|
||||
if firefox:
|
||||
configure_firefox(options, statestore, domain)
|
||||
|
||||
if not no_nisdomain:
|
||||
if NUM_VERSION < 40500:
|
||||
configure_nisdomain(options=options, domain=domain)
|
||||
else:
|
||||
configure_nisdomain(options=options, domain=domain,
|
||||
statestore=statestore)
|
||||
|
||||
# Cleanup: Remove CCACHE_FILE
|
||||
try:
|
||||
os.remove(paths.IPA_DNS_CCACHE)
|
||||
except Exception:
|
||||
pass
|
||||
|
||||
module.exit_json(changed=True)
|
||||
|
||||
if __name__ == '__main__':
|
||||
main()
|
||||
123
roles/ipaclient/library/ipafixca.py
Normal file
123
roles/ipaclient/library/ipafixca.py
Normal file
@@ -0,0 +1,123 @@
|
||||
#!/usr/bin/python
|
||||
# -*- coding: utf-8 -*-
|
||||
|
||||
# Authors:
|
||||
# Thomas Woerner <twoerner@redhat.com>
|
||||
#
|
||||
# Based on ipa-client-install code
|
||||
#
|
||||
# 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/>.
|
||||
|
||||
ANSIBLE_METADATA = {'metadata_version': '1.0',
|
||||
'status': ['preview'],
|
||||
'supported_by': 'community'}
|
||||
|
||||
DOCUMENTATION = '''
|
||||
---
|
||||
module: ipafixca
|
||||
short description: Fix IPA ca certificate
|
||||
description:
|
||||
Repair Fix IPA ca certificate
|
||||
options:
|
||||
servers:
|
||||
description: The FQDN of the IPA servers to connect to.
|
||||
required: true
|
||||
type: list
|
||||
realm:
|
||||
description: The Kerberos realm of an existing IPA deployment.
|
||||
required: true
|
||||
basedn:
|
||||
description: The basedn of the IPA server (of the form dc=example,dc=com).
|
||||
required: true
|
||||
allow_repair:
|
||||
description: Allow repair of already joined hosts. Contrary to ipaclient_force_join the host entry will not be changed on the server.
|
||||
required: true
|
||||
type: bool
|
||||
default: no
|
||||
author:
|
||||
- Thomas Woerner
|
||||
'''
|
||||
|
||||
EXAMPLES = '''
|
||||
- name: Fix IPA ca certificate
|
||||
ipafixca:
|
||||
servers: ["server1.example.com","server2.example.com"]
|
||||
realm: EXAMPLE.COM
|
||||
basedn: dc=example,dc=com
|
||||
allow_repair: yes
|
||||
'''
|
||||
|
||||
RETURN = '''
|
||||
'''
|
||||
|
||||
import os
|
||||
|
||||
from ansible.module_utils.basic import AnsibleModule
|
||||
from ansible.module_utils.ansible_ipa_client import *
|
||||
|
||||
def main():
|
||||
module = AnsibleModule(
|
||||
argument_spec = dict(
|
||||
servers=dict(required=True, type='list'),
|
||||
realm=dict(required=True),
|
||||
basedn=dict(required=True),
|
||||
allow_repair=dict(required=True, type='bool'),
|
||||
),
|
||||
)
|
||||
|
||||
module._ansible_debug = True
|
||||
servers = module.params.get('servers')
|
||||
realm = module.params.get('realm')
|
||||
basedn = module.params.get('basedn')
|
||||
allow_repair = module.params.get('allow_repair')
|
||||
|
||||
env = {'PATH': SECURE_PATH}
|
||||
fstore = sysrestore.FileStore(paths.IPA_CLIENT_SYSRESTORE)
|
||||
os.environ['KRB5CCNAME'] = paths.IPA_DNS_CCACHE
|
||||
|
||||
options.ca_cert_file = None
|
||||
options.unattended = True
|
||||
options.principal = None
|
||||
options.force = False
|
||||
options.password = None
|
||||
|
||||
changed = False
|
||||
if not os.path.exists(paths.IPA_CA_CRT):
|
||||
if not allow_repair:
|
||||
module.fail_json(
|
||||
msg="%s missing, enable allow_repair to fix it." % \
|
||||
paths.IPA_CA_CRT)
|
||||
|
||||
# Repair missing ca.crt file
|
||||
try:
|
||||
os.environ['KRB5_CONFIG'] = env['KRB5_CONFIG'] = "/etc/krb5.conf"
|
||||
env['KRB5CCNAME'] = os.environ['KRB5CCNAME']
|
||||
if NUM_VERSION < 40100:
|
||||
get_ca_cert(fstore, options, servers[0], basedn)
|
||||
else:
|
||||
get_ca_certs(fstore, options, servers[0], basedn, realm)
|
||||
changed = True
|
||||
del os.environ['KRB5_CONFIG']
|
||||
except errors.FileError as e:
|
||||
module.fail_json(msg='%s' % e)
|
||||
except Exception as e:
|
||||
module.fail_json(msg="Cannot obtain CA certificate\n%s" % e)
|
||||
|
||||
module.exit_json(changed=changed)
|
||||
|
||||
if __name__ == '__main__':
|
||||
main()
|
||||
75
roles/ipaclient/library/ipafstore.py
Normal file
75
roles/ipaclient/library/ipafstore.py
Normal file
@@ -0,0 +1,75 @@
|
||||
#!/usr/bin/python
|
||||
# -*- coding: utf-8 -*-
|
||||
|
||||
# Authors:
|
||||
# Thomas Woerner <twoerner@redhat.com>
|
||||
#
|
||||
# Based on ipa-client-install code
|
||||
#
|
||||
# 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/>.
|
||||
|
||||
ANSIBLE_METADATA = {
|
||||
'metadata_version': '1.0',
|
||||
'supported_by': 'community',
|
||||
'status': ['preview'],
|
||||
}
|
||||
|
||||
DOCUMENTATION = '''
|
||||
---
|
||||
module: fstore
|
||||
short description: Backup files using IPA client sysrestore
|
||||
description:
|
||||
Backup files using IPA client sysrestore
|
||||
options:
|
||||
backup:
|
||||
description: File to backup
|
||||
required: true
|
||||
author:
|
||||
- Thomas Woerner
|
||||
'''
|
||||
|
||||
EXAMPLES = '''
|
||||
- name: Backup /etc/krb5.conf
|
||||
ipafstore:
|
||||
backup: "/etc/krb5.conf"
|
||||
'''
|
||||
|
||||
RETURN = '''
|
||||
'''
|
||||
|
||||
from ansible.module_utils.basic import AnsibleModule
|
||||
from ansible.module_utils.ansible_ipa_client import *
|
||||
|
||||
def main():
|
||||
module = AnsibleModule(
|
||||
argument_spec = dict(
|
||||
backup=dict(required=True),
|
||||
),
|
||||
)
|
||||
|
||||
module._ansible_debug = True
|
||||
backup = module.params.get('backup')
|
||||
|
||||
fstore = sysrestore.FileStore(paths.IPA_CLIENT_SYSRESTORE)
|
||||
if not fstore.has_file(backup):
|
||||
fstore.backup_file(backup)
|
||||
module.exit_json(changed=True)
|
||||
|
||||
module.exit_json(changed=False)
|
||||
|
||||
if __name__ == '__main__':
|
||||
main()
|
||||
361
roles/ipaclient/library/ipahost.py
Normal file
361
roles/ipaclient/library/ipahost.py
Normal file
@@ -0,0 +1,361 @@
|
||||
#!/usr/bin/python
|
||||
# -*- coding: utf-8 -*-
|
||||
|
||||
# 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/>.
|
||||
|
||||
ANSIBLE_METADATA = {'metadata_version': '1.0',
|
||||
'status': ['preview'],
|
||||
'supported_by': 'community'}
|
||||
|
||||
DOCUMENTATION = '''
|
||||
---
|
||||
module: ipahost
|
||||
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.
|
||||
options:
|
||||
principal:
|
||||
description: Kerberos principal used to manage the host
|
||||
required: true
|
||||
default: admin
|
||||
password:
|
||||
description: Password for the kerberos principal
|
||||
required: false
|
||||
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
|
||||
random:
|
||||
description: generate a random password to be used in bulk enrollment
|
||||
required: false
|
||||
type: bool
|
||||
default: no
|
||||
state:
|
||||
description: the host state
|
||||
required: false
|
||||
default: present
|
||||
choices: [ "present", "absent" ]
|
||||
certificates:
|
||||
description: a list of host certificates
|
||||
required: false
|
||||
type: list
|
||||
sshpubkey:
|
||||
description: the SSH public key for the host
|
||||
required: false
|
||||
ipaddress:
|
||||
description: the IP address for the host
|
||||
required: false
|
||||
|
||||
requirements:
|
||||
- gssapi on the Ansible controller
|
||||
author:
|
||||
- "Florence Blanc-Renaud"
|
||||
'''
|
||||
|
||||
EXAMPLES = '''
|
||||
# Example from Ansible Playbooks
|
||||
# Add a new host with a random OTP, authenticate using principal/password
|
||||
- ipahost:
|
||||
principal: admin
|
||||
password: MySecretPassword
|
||||
fqdn: ipaclient.ipa.domain.com
|
||||
ipaddress: 192.168.100.23
|
||||
random: True
|
||||
register: ipahost
|
||||
|
||||
# Add a new host, authenticate with a keytab stored on the controller node
|
||||
- ipahost:
|
||||
keytab: admin.keytab
|
||||
fqdn: ipaclient.ipa.domain.com
|
||||
|
||||
# Remove a host, authenticate using principal/password
|
||||
- ipahost:
|
||||
principal: admin
|
||||
password: MySecretPassword
|
||||
fqdn: ipaclient.ipa.domain.com
|
||||
state: absent
|
||||
|
||||
# Modify a host, add ssh public key:
|
||||
- ipahost:
|
||||
principal: admin
|
||||
password: MySecretPassword
|
||||
fqdn: ipaclient.ipa.domain.com
|
||||
sshpubkey: ssh-rsa AAAA...
|
||||
|
||||
'''
|
||||
|
||||
RETURN = '''
|
||||
host:
|
||||
description: the host structure as returned from IPA API
|
||||
returned: always
|
||||
type: complex
|
||||
contains:
|
||||
dn:
|
||||
description: the DN of the host entry
|
||||
type: string
|
||||
returned: always
|
||||
fqdn:
|
||||
description: the fully qualified host name
|
||||
type: string
|
||||
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
|
||||
type: string
|
||||
returned: changed
|
||||
certificates:
|
||||
description: the list of host certificates
|
||||
type: list
|
||||
returned: when present
|
||||
sshpubkey:
|
||||
description: the SSH public key for the host
|
||||
type: string
|
||||
returned: when present
|
||||
ipaddress:
|
||||
description: the IP address for the host
|
||||
type: string
|
||||
returned: when present
|
||||
'''
|
||||
|
||||
import os
|
||||
import six
|
||||
|
||||
from ansible.module_utils.basic import AnsibleModule
|
||||
|
||||
from ipalib import api, errors
|
||||
from ipaplatform.paths import paths
|
||||
from ipapython.ipautil import run
|
||||
|
||||
if six.PY3:
|
||||
unicode = str
|
||||
|
||||
def get_host_diff(ipa_host, module_host):
|
||||
"""
|
||||
Compares two dictionaries containing host attributes and builds a dict
|
||||
of differences.
|
||||
|
||||
: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 = dict()
|
||||
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):
|
||||
"""
|
||||
Creates 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 = dict()
|
||||
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):
|
||||
"""
|
||||
Ensures that the 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'] == 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)
|
||||
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):
|
||||
"""
|
||||
Ensures that the 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:
|
||||
api.Command.host_del(fqdn)
|
||||
except Exception as e:
|
||||
module.fail_json(msg="Failed to remove host: %s" % e)
|
||||
|
||||
module.exit_json(changed=True)
|
||||
|
||||
|
||||
def main():
|
||||
"""
|
||||
Main routine for the ansible module.
|
||||
"""
|
||||
module = AnsibleModule(
|
||||
argument_spec=dict(
|
||||
principal = dict(default='admin'),
|
||||
ccache = dict(required=False, type='path'),
|
||||
fqdn = dict(required=True),
|
||||
certificates = dict(required=False, type='list'),
|
||||
sshpubkey= dict(required=False),
|
||||
ipaddress = dict(required=False),
|
||||
random = dict(default=False, type='bool'),
|
||||
state = dict(default='present', choices=[ 'present', 'absent' ]),
|
||||
),
|
||||
supports_check_mode=True,
|
||||
)
|
||||
|
||||
principal = module.params.get('principal', 'admin')
|
||||
ccache = module.params.get('ccache')
|
||||
fqdn = unicode(module.params.get('fqdn'))
|
||||
state = module.params.get('state')
|
||||
|
||||
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()
|
||||
|
||||
changed = False
|
||||
try:
|
||||
result = api.Command.host_show(fqdn, all=True)
|
||||
host = result['result']
|
||||
except errors.NotFound:
|
||||
host = None
|
||||
|
||||
if state == 'present' or state == 'disabled':
|
||||
changed = ensure_host_present(module, api, host)
|
||||
elif state == 'absent':
|
||||
changed = ensure_host_absent(module, api, host)
|
||||
|
||||
except Exception as e:
|
||||
module.fail_json(msg="ipahost module failed : %s" % str(e))
|
||||
finally:
|
||||
run(["kdestroy"], raiseonerr=False, env=os.environ)
|
||||
|
||||
module.exit_json(changed=changed, host=host)
|
||||
|
||||
if __name__ == '__main__':
|
||||
main()
|
||||
314
roles/ipaclient/library/ipajoin.py
Normal file
314
roles/ipaclient/library/ipajoin.py
Normal file
@@ -0,0 +1,314 @@
|
||||
#!/usr/bin/python
|
||||
# -*- coding: utf-8 -*-
|
||||
|
||||
# Authors:
|
||||
# Thomas Woerner <twoerner@redhat.com>
|
||||
#
|
||||
# Based on ipa-client-install code
|
||||
#
|
||||
# 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/>.
|
||||
|
||||
ANSIBLE_METADATA = {
|
||||
'metadata_version': '1.0',
|
||||
'supported_by': 'community',
|
||||
'status': ['preview'],
|
||||
}
|
||||
|
||||
DOCUMENTATION = '''
|
||||
---
|
||||
module: ipajoin
|
||||
short description: Join a machine to an IPA realm and get a keytab for the host service principal
|
||||
description:
|
||||
Join a machine to an IPA realm and get a keytab for the host service principal
|
||||
options:
|
||||
servers:
|
||||
description: The FQDN of the IPA servers to connect to.
|
||||
required: true
|
||||
type: list
|
||||
domain:
|
||||
description: The primary DNS domain of an existing IPA deployment.
|
||||
required: true
|
||||
realm:
|
||||
description: The Kerberos realm of an existing IPA deployment.
|
||||
required: true
|
||||
hostname:
|
||||
description: The hostname of the machine to join (FQDN).
|
||||
required: true
|
||||
kdc:
|
||||
description: The name or address of the host running the KDC.
|
||||
required: true
|
||||
basedn:
|
||||
description: The basedn of the IPA server (of the form dc=example,dc=com).
|
||||
required: true
|
||||
principal:
|
||||
description: The authorized kerberos principal used to join the IPA realm.
|
||||
required: false
|
||||
password:
|
||||
description: The password to use if not using Kerberos to authenticate.
|
||||
required: false
|
||||
keytab:
|
||||
description: The path to a backed-up host keytab from previous enrollment.
|
||||
required: false
|
||||
ca_cert_file:
|
||||
description: A CA certificate to use. Do not acquire the IPA CA certificate via automated means.
|
||||
required: false
|
||||
force_join:
|
||||
description: Force enrolling the host even if host entry exists.
|
||||
required: false
|
||||
type: bool
|
||||
default: no
|
||||
kinit_attempts:
|
||||
description: Repeat the request for host Kerberos ticket X times.
|
||||
required: false
|
||||
type: int
|
||||
default: 5
|
||||
debug:
|
||||
description: Enable debug mode.
|
||||
required: false
|
||||
type: bool
|
||||
default: no
|
||||
author:
|
||||
- Thomas Woerner
|
||||
'''
|
||||
|
||||
EXAMPLES = '''
|
||||
# Join IPA to get the keytab
|
||||
- name: Join IPA in force mode with maximum 5 kinit attempts
|
||||
ipajoin:
|
||||
servers: ["server1.example.com","server2.example.com"]
|
||||
domain: example.com
|
||||
realm: EXAMPLE.COM
|
||||
kdc: server1.example.com
|
||||
basedn: dc=example,dc=com
|
||||
hostname: client1.example.com
|
||||
principal: admin
|
||||
password: MySecretPassword
|
||||
force_join: yes
|
||||
kinit_attempts: 5
|
||||
|
||||
# Join IPA to get the keytab using ipadiscovery return values
|
||||
- name: Join IPA
|
||||
ipajoin:
|
||||
servers: "{{ ipadiscovery.servers }}"
|
||||
domain: "{{ ipadiscovery.domain }}"
|
||||
realm: "{{ ipadiscovery.realm }}"
|
||||
kdc: "{{ ipadiscovery.kdc }}"
|
||||
basedn: "{{ ipadiscovery.basedn }}"
|
||||
hostname: "{{ ipadiscovery.hostname }}"
|
||||
principal: admin
|
||||
password: MySecretPassword
|
||||
'''
|
||||
|
||||
RETURN = '''
|
||||
already_joined:
|
||||
description: The flag describes if the host is arelady joined.
|
||||
returned: always
|
||||
type: bool
|
||||
'''
|
||||
|
||||
import os
|
||||
import tempfile
|
||||
|
||||
from ansible.module_utils.basic import AnsibleModule
|
||||
from ansible.module_utils.ansible_ipa_client import *
|
||||
|
||||
def main():
|
||||
module = AnsibleModule(
|
||||
argument_spec = dict(
|
||||
servers=dict(required=True, type='list'),
|
||||
domain=dict(required=True),
|
||||
realm=dict(required=True),
|
||||
hostname=dict(required=True),
|
||||
kdc=dict(required=True),
|
||||
basedn=dict(required=True),
|
||||
principal=dict(required=False),
|
||||
password=dict(required=False, no_log=True),
|
||||
keytab=dict(required=False),
|
||||
ca_cert_file=dict(required=False),
|
||||
force_join=dict(required=False, type='bool'),
|
||||
kinit_attempts=dict(required=False, type='int', default=5),
|
||||
debug=dict(required=False, type='bool'),
|
||||
),
|
||||
supports_check_mode = True,
|
||||
)
|
||||
|
||||
module._ansible_debug = True
|
||||
servers = module.params.get('servers')
|
||||
domain = module.params.get('domain')
|
||||
realm = module.params.get('realm')
|
||||
hostname = module.params.get('hostname')
|
||||
basedn = module.params.get('basedn')
|
||||
kdc = module.params.get('kdc')
|
||||
force_join = module.params.get('force_join')
|
||||
principal = module.params.get('principal')
|
||||
password = module.params.get('password')
|
||||
keytab = module.params.get('keytab')
|
||||
ca_cert_file = module.params.get('ca_cert_file')
|
||||
kinit_attempts = module.params.get('kinit_attempts')
|
||||
debug = module.params.get('debug')
|
||||
|
||||
if password is not None and password != "" and \
|
||||
keytab is not None and keytab != "":
|
||||
module.fail_json(msg="Password and keytab cannot be used together")
|
||||
|
||||
client_domain = hostname[hostname.find(".")+1:]
|
||||
nolog = tuple()
|
||||
env = {'PATH': SECURE_PATH}
|
||||
fstore = sysrestore.FileStore(paths.IPA_CLIENT_SYSRESTORE)
|
||||
host_principal = 'host/%s@%s' % (hostname, realm)
|
||||
sssd = True
|
||||
|
||||
options.ca_cert_file = ca_cert_file
|
||||
options.unattended = True
|
||||
options.principal = principal if principal != "" else None
|
||||
options.force = False
|
||||
options.password = password
|
||||
|
||||
ccache_dir = None
|
||||
changed = False
|
||||
already_joined = False
|
||||
try:
|
||||
(krb_fd, krb_name) = tempfile.mkstemp()
|
||||
os.close(krb_fd)
|
||||
configure_krb5_conf(
|
||||
cli_realm=realm,
|
||||
cli_domain=domain,
|
||||
cli_server=servers,
|
||||
cli_kdc=kdc,
|
||||
dnsok=False,
|
||||
filename=krb_name,
|
||||
client_domain=client_domain,
|
||||
client_hostname=hostname,
|
||||
configure_sssd=sssd,
|
||||
force=False)
|
||||
env['KRB5_CONFIG'] = krb_name
|
||||
ccache_dir = tempfile.mkdtemp(prefix='krbcc')
|
||||
ccache_name = os.path.join(ccache_dir, 'ccache')
|
||||
join_args = [paths.SBIN_IPA_JOIN,
|
||||
"-s", servers[0],
|
||||
"-b", str(realm_to_suffix(realm)),
|
||||
"-h", hostname]
|
||||
if debug:
|
||||
join_args.append("-d")
|
||||
env['XMLRPC_TRACE_CURL'] = 'yes'
|
||||
if force_join:
|
||||
join_args.append("-f")
|
||||
if principal:
|
||||
if principal.find('@') == -1:
|
||||
principal = '%s@%s' % (principal, realm)
|
||||
try:
|
||||
kinit_password(principal, password, ccache_name,
|
||||
config=krb_name)
|
||||
except RuntimeError as e:
|
||||
module.fail_json(
|
||||
msg="Kerberos authentication failed: {}".format(e))
|
||||
elif keytab:
|
||||
join_args.append("-f")
|
||||
if os.path.exists(keytab):
|
||||
try:
|
||||
kinit_keytab(host_principal,
|
||||
keytab,
|
||||
ccache_name,
|
||||
config=krb_name,
|
||||
attempts=kinit_attempts)
|
||||
except GSSError as e:
|
||||
module.fail_json(
|
||||
msg="Kerberos authentication failed: {}".format(e))
|
||||
else:
|
||||
module.fail_json(
|
||||
msg="Keytab file could not be found: {}".format(keytab))
|
||||
|
||||
elif password:
|
||||
join_args.append("-w")
|
||||
join_args.append(password)
|
||||
nolog = (password,)
|
||||
|
||||
env['KRB5CCNAME'] = os.environ['KRB5CCNAME'] = ccache_name
|
||||
# Get the CA certificate
|
||||
try:
|
||||
os.environ['KRB5_CONFIG'] = env['KRB5_CONFIG']
|
||||
if NUM_VERSION < 40100:
|
||||
get_ca_cert(fstore, options, servers[0], basedn)
|
||||
else:
|
||||
get_ca_certs(fstore, options, servers[0], basedn, realm)
|
||||
del os.environ['KRB5_CONFIG']
|
||||
except errors.FileError as e:
|
||||
module.fail_json(msg='%s' % e)
|
||||
except Exception as e:
|
||||
module.fail_json(msg="Cannot obtain CA certificate\n%s" % e)
|
||||
|
||||
# Now join the domain
|
||||
result = run(
|
||||
join_args, raiseonerr=False, env=env, nolog=nolog,
|
||||
capture_error=True)
|
||||
stderr = result.error_output
|
||||
|
||||
if result.returncode != 0:
|
||||
if result.returncode == 13:
|
||||
already_joined = True
|
||||
module.log("Host is already joined")
|
||||
else:
|
||||
if principal:
|
||||
run(["kdestroy"], raiseonerr=False, env=env)
|
||||
module.fail_json(msg="Joining realm failed: %s" % stderr)
|
||||
else:
|
||||
changed = True
|
||||
module.log("Enrolled in IPA realm %s" % realm)
|
||||
|
||||
# Fail for missing krb5.keytab on already joined host
|
||||
if already_joined and not os.path.exists(paths.KRB5_KEYTAB):
|
||||
module.fail_json(msg="krb5.keytab missing! Retry with ipaclient_force_join=yes to generate a new one.")
|
||||
|
||||
if principal:
|
||||
run(["kdestroy"], raiseonerr=False, env=env)
|
||||
|
||||
# Obtain the TGT. We do it with the temporary krb5.conf, sot
|
||||
# tha only the KDC we're installing under is contacted.
|
||||
# Other KDCs might not have replicated the principal yet.
|
||||
# Once we have the TGT, it's usable on any server.
|
||||
try:
|
||||
kinit_keytab(host_principal, paths.KRB5_KEYTAB,
|
||||
paths.IPA_DNS_CCACHE,
|
||||
config=krb_name,
|
||||
attempts=kinit_attempts)
|
||||
env['KRB5CCNAME'] = os.environ['KRB5CCNAME'] = paths.IPA_DNS_CCACHE
|
||||
except GSSError as e:
|
||||
# failure to get ticket makes it impossible to login and
|
||||
# bind from sssd to LDAP, abort installation
|
||||
module.fail_json(msg="Failed to obtain host TGT: %s" % e)
|
||||
|
||||
finally:
|
||||
try:
|
||||
os.remove(krb_name)
|
||||
except OSError:
|
||||
module.fail_json(msg="Could not remove %s" % krb_name)
|
||||
if ccache_dir is not None:
|
||||
try:
|
||||
os.rmdir(ccache_dir)
|
||||
except OSError:
|
||||
pass
|
||||
if os.path.exists(krb_name + ".ipabkp"):
|
||||
try:
|
||||
os.remove(krb_name + ".ipabkp")
|
||||
except OSError:
|
||||
module.fail_json(msg="Could not remove %s.ipabkp" % krb_name)
|
||||
|
||||
module.exit_json(changed=changed,
|
||||
already_joined=already_joined)
|
||||
|
||||
if __name__ == '__main__':
|
||||
main()
|
||||
319
roles/ipaclient/library/ipanss.py
Normal file
319
roles/ipaclient/library/ipanss.py
Normal file
@@ -0,0 +1,319 @@
|
||||
#!/usr/bin/python
|
||||
# -*- coding: utf-8 -*-
|
||||
|
||||
# Authors:
|
||||
# Thomas Woerner <twoerner@redhat.com>
|
||||
#
|
||||
# Based on ipa-client-install code
|
||||
#
|
||||
# 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/>.
|
||||
|
||||
ANSIBLE_METADATA = {
|
||||
'metadata_version': '1.0',
|
||||
'supported_by': 'community',
|
||||
'status': ['preview'],
|
||||
}
|
||||
|
||||
DOCUMENTATION = '''
|
||||
---
|
||||
module: ipanss
|
||||
short description: Create IPA NSS database
|
||||
description:
|
||||
Create IPA NSS database
|
||||
options:
|
||||
servers:
|
||||
description: The FQDN of the IPA servers to connect to.
|
||||
required: true
|
||||
type: list
|
||||
domain:
|
||||
description: The primary DNS domain of an existing IPA deployment.
|
||||
required: true
|
||||
realm:
|
||||
description: The Kerberos realm of an existing IPA deployment.
|
||||
required: true
|
||||
hostname:
|
||||
description: The hostname of the machine to join (FQDN).
|
||||
required: true
|
||||
basedn:
|
||||
description: The basedn of the IPA server (of the form dc=example,dc=com).
|
||||
required: true
|
||||
principal:
|
||||
description: The authorized kerberos principal used to join the IPA realm.
|
||||
required: false
|
||||
subject_base:
|
||||
description: The subject base, needed for certmonger
|
||||
required: true
|
||||
ca_enabled:
|
||||
description: Whether the Certificate Authority is enabled or not.
|
||||
required: true
|
||||
type: bool
|
||||
default: no
|
||||
mkhomedir:
|
||||
description: Whether to create home directories for users on their first login.
|
||||
required: false
|
||||
type: bool
|
||||
default: no
|
||||
on_master:
|
||||
description: Whether the configuration is done on the master or not.
|
||||
required: false
|
||||
type: bool
|
||||
default: no
|
||||
author:
|
||||
- Thomas Woerner
|
||||
'''
|
||||
|
||||
EXAMPLES = '''
|
||||
- name: Create IPA NSS database
|
||||
ipanss:
|
||||
servers: ["server1.example.com","server2.example.com"]
|
||||
domain: example.com
|
||||
realm: EXAMPLE.COM
|
||||
basedn: dc=example,dc=com
|
||||
hostname: client1.example.com
|
||||
subject_base: O=EXAMPLE.COM
|
||||
principal: admin
|
||||
ca_enabled: yes
|
||||
'''
|
||||
|
||||
RETURN = '''
|
||||
'''
|
||||
|
||||
import os
|
||||
import time
|
||||
|
||||
from ansible.module_utils.basic import AnsibleModule
|
||||
from ansible.module_utils.ansible_ipa_client import *
|
||||
|
||||
def main():
|
||||
module = AnsibleModule(
|
||||
argument_spec = dict(
|
||||
servers=dict(required=True, type='list'),
|
||||
domain=dict(required=True),
|
||||
realm=dict(required=True),
|
||||
hostname=dict(required=True),
|
||||
basedn=dict(required=True),
|
||||
principal=dict(required=False),
|
||||
subject_base=dict(required=True),
|
||||
ca_enabled=dict(required=True, type='bool'),
|
||||
mkhomedir=dict(required=False, type='bool'),
|
||||
on_master=dict(required=False, type='bool'),
|
||||
),
|
||||
supports_check_mode = True,
|
||||
)
|
||||
|
||||
module._ansible_debug = True
|
||||
servers = module.params.get('servers')
|
||||
realm = module.params.get('realm')
|
||||
hostname = module.params.get('hostname')
|
||||
basedn = module.params.get('basedn')
|
||||
domain = module.params.get('domain')
|
||||
principal = module.params.get('principal')
|
||||
subject_base = module.params.get('subject_base')
|
||||
ca_enabled = module.params.get('ca_enabled')
|
||||
mkhomedir = module.params.get('mkhomedir')
|
||||
on_master = module.params.get('on_master')
|
||||
|
||||
fstore = sysrestore.FileStore(paths.IPA_CLIENT_SYSRESTORE)
|
||||
statestore = sysrestore.StateFile(paths.IPA_CLIENT_SYSRESTORE)
|
||||
standard_logging_setup(
|
||||
paths.IPACLIENT_INSTALL_LOG, verbose=True, debug=False,
|
||||
filemode='a', console_format='%(message)s')
|
||||
|
||||
os.environ['KRB5CCNAME'] = paths.IPA_DNS_CCACHE
|
||||
|
||||
options.dns_updates = False
|
||||
options.all_ip_addresses = False
|
||||
options.ip_addresses = None
|
||||
options.request_cert = False
|
||||
options.hostname = hostname
|
||||
options.preserve_sssd = False
|
||||
options.on_master = False
|
||||
options.conf_ssh = True
|
||||
options.conf_sshd = True
|
||||
options.conf_sudo = True
|
||||
options.primary = False
|
||||
options.permit = False
|
||||
options.krb5_offline_passwords = False
|
||||
options.create_sshfp = True
|
||||
|
||||
##########################################################################
|
||||
|
||||
# Create IPA NSS database
|
||||
try:
|
||||
create_ipa_nssdb()
|
||||
except ipautil.CalledProcessError as e:
|
||||
module.fail_json(msg="Failed to create IPA NSS database: %s" % e)
|
||||
|
||||
# Get CA certificates from the certificate store
|
||||
try:
|
||||
ca_certs = get_certs_from_ldap(servers[0], basedn, realm,
|
||||
ca_enabled)
|
||||
except errors.NoCertificateError:
|
||||
if ca_enabled:
|
||||
ca_subject = DN(('CN', 'Certificate Authority'), subject_base)
|
||||
else:
|
||||
ca_subject = None
|
||||
ca_certs = certstore.make_compat_ca_certs(ca_certs, realm,
|
||||
ca_subject)
|
||||
ca_certs_trust = [(c, n, certstore.key_policy_to_trust_flags(t, True, u))
|
||||
for (c, n, t, u) in ca_certs]
|
||||
|
||||
if hasattr(paths, "KDC_CA_BUNDLE_PEM"):
|
||||
x509.write_certificate_list(
|
||||
[c for c, n, t, u in ca_certs if t is not False],
|
||||
paths.KDC_CA_BUNDLE_PEM)
|
||||
if hasattr(paths, "CA_BUNDLE_PEM"):
|
||||
x509.write_certificate_list(
|
||||
[c for c, n, t, u in ca_certs if t is not False],
|
||||
paths.CA_BUNDLE_PEM)
|
||||
|
||||
# Add the CA certificates to the IPA NSS database
|
||||
module.debug("Adding CA certificates to the IPA NSS database.")
|
||||
ipa_db = certdb.NSSDatabase(paths.IPA_NSSDB_DIR)
|
||||
for cert, nickname, trust_flags in ca_certs_trust:
|
||||
try:
|
||||
ipa_db.add_cert(cert, nickname, trust_flags)
|
||||
except CalledProcessError as e:
|
||||
module.fail_json(msg="Failed to add %s to the IPA NSS database." % nickname)
|
||||
|
||||
# Add the CA certificates to the platform-dependant systemwide CA store
|
||||
tasks.insert_ca_certs_into_systemwide_ca_store(ca_certs)
|
||||
|
||||
if not on_master:
|
||||
client_dns(servers[0], hostname, options)
|
||||
configure_certmonger(fstore, subject_base, realm, hostname,
|
||||
options, ca_enabled)
|
||||
|
||||
if hasattr(paths, "SSH_CONFIG_DIR"):
|
||||
ssh_config_dir = paths.SSH_CONFIG_DIR
|
||||
else:
|
||||
ssh_config_dir = services.knownservices.sshd.get_config_dir()
|
||||
update_ssh_keys(hostname, ssh_config_dir, options.create_sshfp)
|
||||
|
||||
try:
|
||||
os.remove(paths.IPA_DNS_CCACHE)
|
||||
except Exception:
|
||||
pass
|
||||
|
||||
##########################################################################
|
||||
|
||||
# Name Server Caching Daemon. Disable for SSSD, use otherwise
|
||||
# (if installed)
|
||||
nscd = services.knownservices.nscd
|
||||
if nscd.is_installed():
|
||||
if NUM_VERSION < 40500:
|
||||
save_state(nscd)
|
||||
else:
|
||||
save_state(nscd, statestore)
|
||||
|
||||
try:
|
||||
nscd_service_action = 'stop'
|
||||
nscd.stop()
|
||||
except Exception:
|
||||
module.warn("Failed to %s the %s daemon" %
|
||||
(nscd_service_action, nscd.service_name))
|
||||
|
||||
try:
|
||||
nscd.disable()
|
||||
except Exception:
|
||||
module.warn("Failed to disable %s daemon. Disable it manually." %
|
||||
nscd.service_name)
|
||||
|
||||
nslcd = services.knownservices.nslcd
|
||||
if nslcd.is_installed():
|
||||
if NUM_VERSION < 40500:
|
||||
save_state(nslcd)
|
||||
else:
|
||||
save_state(nslcd, statestore)
|
||||
|
||||
##########################################################################
|
||||
|
||||
# Modify nsswitch/pam stack
|
||||
tasks.modify_nsswitch_pam_stack(sssd=True,
|
||||
mkhomedir=mkhomedir,
|
||||
statestore=statestore)
|
||||
|
||||
module.log("SSSD enabled")
|
||||
|
||||
argspec = inspect.getargspec(services.service)
|
||||
if len(argspec.args) > 1:
|
||||
sssd = services.service('sssd', api)
|
||||
else:
|
||||
sssd = services.service('sssd')
|
||||
try:
|
||||
sssd.restart()
|
||||
except CalledProcessError:
|
||||
module.warn("SSSD service restart was unsuccessful.")
|
||||
|
||||
try:
|
||||
sssd.enable()
|
||||
except CalledProcessError as e:
|
||||
module.warn(
|
||||
"Failed to enable automatic startup of the SSSD daemon: "
|
||||
"%s", e)
|
||||
|
||||
if configure_openldap_conf(fstore, basedn, servers):
|
||||
module.log("Configured /etc/openldap/ldap.conf")
|
||||
else:
|
||||
module.log("Failed to configure /etc/openldap/ldap.conf")
|
||||
|
||||
# Check that nss is working properly
|
||||
if not on_master:
|
||||
user = principal
|
||||
if user is None or user == "":
|
||||
user = "admin@%s" % domain
|
||||
module.log("Principal is not set when enrolling with OTP"
|
||||
"; using principal '%s' for 'getent passwd'" % user)
|
||||
elif '@' not in user:
|
||||
user = "%s@%s" % (user, domain)
|
||||
n = 0
|
||||
found = False
|
||||
# Loop for up to 10 seconds to see if nss is working properly.
|
||||
# It can sometimes take a few seconds to connect to the remote
|
||||
# provider.
|
||||
# Particulary, SSSD might take longer than 6-8 seconds.
|
||||
while n < 10 and not found:
|
||||
try:
|
||||
ipautil.run(["getent", "passwd", user])
|
||||
found = True
|
||||
except Exception as e:
|
||||
time.sleep(1)
|
||||
n = n + 1
|
||||
|
||||
if not found:
|
||||
module.fail_json(msg="Unable to find '%s' user with 'getent "
|
||||
"passwd %s'!" % (user.split("@")[0], user))
|
||||
if conf:
|
||||
module.log("Recognized configuration: %s" % conf)
|
||||
else:
|
||||
module.fail_json(msg=
|
||||
"Unable to reliably detect "
|
||||
"configuration. Check NSS setup manually.")
|
||||
|
||||
try:
|
||||
hardcode_ldap_server(servers)
|
||||
except Exception as e:
|
||||
module.fail_json(msg="Adding hardcoded server name to "
|
||||
"/etc/ldap.conf failed: %s" % str(e))
|
||||
|
||||
##########################################################################
|
||||
|
||||
module.exit_json(changed=True,
|
||||
ca_enabled_ra=ca_enabled)
|
||||
|
||||
if __name__ == '__main__':
|
||||
main()
|
||||
281
roles/ipaclient/library/ipasssd.py
Normal file
281
roles/ipaclient/library/ipasssd.py
Normal file
@@ -0,0 +1,281 @@
|
||||
#!/usr/bin/python
|
||||
# -*- coding: utf-8 -*-
|
||||
|
||||
# Authors:
|
||||
# Thomas Woerner <twoerner@redhat.com>
|
||||
#
|
||||
# Based on ipa-client-install code
|
||||
#
|
||||
# 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/>.
|
||||
|
||||
ANSIBLE_METADATA = {
|
||||
'metadata_version': '1.0',
|
||||
'supported_by': 'community',
|
||||
'status': ['preview'],
|
||||
}
|
||||
|
||||
DOCUMENTATION = '''
|
||||
---
|
||||
module: sssd_conf
|
||||
short description: Configure sssd
|
||||
description:
|
||||
Configure sssd
|
||||
options:
|
||||
servers:
|
||||
description: The FQDN of the IPA servers to connect to.
|
||||
required: true
|
||||
type: list
|
||||
domain:
|
||||
description: The primary DNS domain of an existing IPA deployment.
|
||||
required: true
|
||||
realm:
|
||||
description: The Kerberos realm of an existing IPA deployment.
|
||||
required: true
|
||||
hostname:
|
||||
description: The hostname of the machine to join (FQDN).
|
||||
required: true
|
||||
services:
|
||||
description: The services that should be enabled in the ssd configuration.
|
||||
required: true
|
||||
type: list
|
||||
krb5_offline_passwords:
|
||||
description: Whether user passwords are stored when the server is offline.
|
||||
required: false
|
||||
type: bool
|
||||
default: no
|
||||
on_master:
|
||||
description: Whether the configuration is done on the master or not.
|
||||
required: false
|
||||
type: bool
|
||||
default: no
|
||||
primary:
|
||||
description: Whether to use fixed server as primary IPA server.
|
||||
required: false
|
||||
type: bool
|
||||
default: no
|
||||
preserve_sssd:
|
||||
description: Preserve old SSSD configuration if possible.
|
||||
required: false
|
||||
type: bool
|
||||
default: no
|
||||
permit:
|
||||
description: Disable access rules by default, permit all access.
|
||||
required: false
|
||||
type: bool
|
||||
default: no
|
||||
dns_updates:
|
||||
description: Configures the machine to attempt dns updates when the ip address changes.
|
||||
required: false
|
||||
type: bool
|
||||
default: no
|
||||
all_ip_addresses:
|
||||
description: All routable IP addresses configured on any interface will be added to DNS.
|
||||
required: false
|
||||
type: bool
|
||||
default: no
|
||||
author:
|
||||
- Thomas Woerner
|
||||
'''
|
||||
|
||||
EXAMPLES = '''
|
||||
- name: Configure SSSD
|
||||
sssd:
|
||||
servers: ["server1.example.com","server2.example.com"]
|
||||
domain: example.com
|
||||
realm: EXAMPLE.COM
|
||||
hostname: client1.example.com
|
||||
services: ["ssh", "sudo"]
|
||||
cache_credentials: yes
|
||||
krb5_offline_passwords: yes
|
||||
'''
|
||||
|
||||
RETURN = '''
|
||||
'''
|
||||
|
||||
import os
|
||||
import sys
|
||||
import tempfile
|
||||
import SSSDConfig
|
||||
|
||||
from ansible.module_utils.basic import AnsibleModule
|
||||
from ansible.module_utils.ansible_ipa_client import *
|
||||
|
||||
def sssd_enable_service(module, sssdconfig, service):
|
||||
try:
|
||||
sssdconfig.new_service(service)
|
||||
except SSSDConfig.ServiceAlreadyExists:
|
||||
pass
|
||||
except SSSDConfig.ServiceNotRecognizedError:
|
||||
module.fail_json(
|
||||
msg="Unable to activate the %s service in SSSD config." % service)
|
||||
sssdconfig.activate_service(service)
|
||||
|
||||
def main():
|
||||
module = AnsibleModule(
|
||||
argument_spec = dict(
|
||||
servers=dict(required=True, type='list'),
|
||||
domain=dict(required=True),
|
||||
realm=dict(required=True),
|
||||
hostname=dict(required=True),
|
||||
services=dict(required=True, type='list'),
|
||||
krb5_offline_passwords=dict(required=False, type='bool'),
|
||||
on_master=dict(required=False, type='bool'),
|
||||
primary=dict(required=False, type='bool'),
|
||||
preserve_sssd=dict(required=False, type='bool'),
|
||||
permit=dict(required=False, type='bool'),
|
||||
dns_updates=dict(required=False, type='bool'),
|
||||
all_ip_addresses=dict(required=False, type='bool'),
|
||||
),
|
||||
supports_check_mode = True,
|
||||
)
|
||||
|
||||
module._ansible_debug = True
|
||||
cli_servers = module.params.get('servers')
|
||||
cli_domain = module.params.get('domain')
|
||||
cli_realm = module.params.get('realm')
|
||||
client_hostname = module.params.get('hostname')
|
||||
services = module.params.get('services')
|
||||
krb5_offline_passwords = module.params.get('krb5_offline_passwords')
|
||||
on_master = module.params.get('on_master')
|
||||
primary = module.params.get('primary')
|
||||
preserve_sssd = module.params.get('preserve_sssd')
|
||||
permit = module.params.get('permit')
|
||||
dns_updates = module.params.get('dns_updates')
|
||||
all_ip_addresses = module.params.get('all_ip_addresses')
|
||||
|
||||
fstore = sysrestore.FileStore(paths.IPA_CLIENT_SYSRESTORE)
|
||||
client_domain = client_hostname[client_hostname.find(".")+1:]
|
||||
|
||||
try:
|
||||
sssdconfig = SSSDConfig.SSSDConfig()
|
||||
sssdconfig.import_config()
|
||||
except Exception as e:
|
||||
if os.path.exists(paths.SSSD_CONF) and preserve_sssd:
|
||||
# SSSD config is in place but we are unable to read it
|
||||
# In addition, we are instructed to preserve it
|
||||
# This all means we can't use it and have to bail out
|
||||
module.fail_json(
|
||||
msg="SSSD config exists but cannot be parsed: %s" % str(e))
|
||||
|
||||
# SSSD configuration does not exist or we are not asked to preserve it,
|
||||
# create new one
|
||||
# We do make new SSSDConfig instance because IPAChangeConf-derived
|
||||
# classes have no means to reset their state and ParseError exception
|
||||
# could come due to parsing error from older version which cannot be
|
||||
# upgraded anymore, leaving sssdconfig instance practically unusable
|
||||
# Note that we already backed up sssd.conf before going into this
|
||||
# routine
|
||||
if isinstance(e, IOError):
|
||||
pass
|
||||
else:
|
||||
# It was not IOError so it must have been parsing error
|
||||
module.fail_json(msg="Unable to parse existing SSSD config.")
|
||||
|
||||
module.log("New SSSD config will be created")
|
||||
sssdconfig = SSSDConfig.SSSDConfig()
|
||||
sssdconfig.new_config()
|
||||
|
||||
try:
|
||||
domain = sssdconfig.new_domain(cli_domain)
|
||||
except SSSDConfig.DomainAlreadyExistsError:
|
||||
module.log("Domain %s is already configured in existing SSSD "
|
||||
"config, creating a new one." % cli_domain)
|
||||
sssdconfig = SSSDConfig.SSSDConfig()
|
||||
sssdconfig.new_config()
|
||||
domain = sssdconfig.new_domain(cli_domain)
|
||||
|
||||
if on_master:
|
||||
sssd_enable_service(module, sssdconfig, 'ifp')
|
||||
|
||||
if (("ssh" in services and os.path.isfile(paths.SSH_CONFIG)) or
|
||||
("sshd" in services and os.path.isfile(paths.SSHD_CONFIG))):
|
||||
sssd_enable_service(module, sssdconfig, 'ssh')
|
||||
|
||||
if "sudo" in services:
|
||||
sssd_enable_service(module, sssdconfig, 'sudo')
|
||||
configure_nsswitch_database(fstore, 'sudoers', ['sss'],
|
||||
default_value=['files'])
|
||||
|
||||
domain.add_provider('ipa', 'id')
|
||||
|
||||
# add discovery domain if client domain different from server domain
|
||||
# do not set this config in server mode (#3947)
|
||||
if not on_master and cli_domain != client_domain:
|
||||
domain.set_option('dns_discovery_domain', cli_domain)
|
||||
|
||||
if not on_master:
|
||||
if primary:
|
||||
domain.set_option('ipa_server', ', '.join(cli_servers))
|
||||
else:
|
||||
domain.set_option('ipa_server',
|
||||
'_srv_, %s' % ', '.join(cli_servers))
|
||||
else:
|
||||
domain.set_option('ipa_server_mode', 'True')
|
||||
# the master should only use itself for Kerberos
|
||||
domain.set_option('ipa_server', cli_servers[0])
|
||||
|
||||
# increase memcache timeout to 10 minutes when in server mode
|
||||
try:
|
||||
nss_service = sssdconfig.get_service('nss')
|
||||
except SSSDConfig.NoServiceError:
|
||||
nss_service = sssdconfig.new_service('nss')
|
||||
|
||||
nss_service.set_option('memcache_timeout', 600)
|
||||
sssdconfig.save_service(nss_service)
|
||||
|
||||
domain.set_option('ipa_domain', cli_domain)
|
||||
domain.set_option('ipa_hostname', client_hostname)
|
||||
if cli_domain.lower() != cli_realm.lower():
|
||||
domain.set_option('krb5_realm', cli_realm)
|
||||
|
||||
# Might need this if /bin/hostname doesn't return a FQDN
|
||||
# domain.set_option('ipa_hostname', 'client.example.com')
|
||||
|
||||
domain.add_provider('ipa', 'auth')
|
||||
domain.add_provider('ipa', 'chpass')
|
||||
if not permit:
|
||||
domain.add_provider('ipa', 'access')
|
||||
else:
|
||||
domain.add_provider('permit', 'access')
|
||||
|
||||
domain.set_option('cache_credentials', True)
|
||||
|
||||
# SSSD will need TLS for checking if ipaMigrationEnabled attribute is set
|
||||
# Note that SSSD will force StartTLS because the channel is later used for
|
||||
# authentication as well if password migration is enabled. Thus set
|
||||
# the option unconditionally.
|
||||
domain.set_option('ldap_tls_cacert', paths.IPA_CA_CRT)
|
||||
|
||||
if dns_updates:
|
||||
domain.set_option('dyndns_update', True)
|
||||
if all_ip_addresses:
|
||||
domain.set_option('dyndns_iface', '*')
|
||||
else:
|
||||
iface = get_server_connection_interface(cli_servers[0])
|
||||
domain.set_option('dyndns_iface', iface)
|
||||
if krb5_offline_passwords:
|
||||
domain.set_option('krb5_store_password_if_offline', True)
|
||||
|
||||
domain.set_active(True)
|
||||
|
||||
sssdconfig.save_domain(domain)
|
||||
sssdconfig.write(paths.SSSD_CONF)
|
||||
|
||||
module.exit_json(changed=True)
|
||||
|
||||
if __name__ == '__main__':
|
||||
main()
|
||||
220
roles/ipaclient/library/ipatest.py
Normal file
220
roles/ipaclient/library/ipatest.py
Normal file
@@ -0,0 +1,220 @@
|
||||
#!/usr/bin/python
|
||||
# -*- coding: utf-8 -*-
|
||||
|
||||
# Authors:
|
||||
# Thomas Woerner <twoerner@redhat.com>
|
||||
#
|
||||
# Based on ipa-client-install code
|
||||
#
|
||||
# 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/>.
|
||||
|
||||
ANSIBLE_METADATA = {
|
||||
'metadata_version': '1.0',
|
||||
'supported_by': 'community',
|
||||
'status': ['preview'],
|
||||
}
|
||||
|
||||
DOCUMENTATION = '''
|
||||
---
|
||||
module: ipatest
|
||||
short description: Test if the krb5.keytab on the machine is valid and can be used.
|
||||
description:
|
||||
Test if the krb5.keytab on the machine is valid and can be used.
|
||||
A temporary krb5.conf file will be generated to not fail on an invalid one.
|
||||
options:
|
||||
servers:
|
||||
description: The FQDN of the IPA servers to connect to.
|
||||
required: true
|
||||
type: list
|
||||
domain:
|
||||
description: The primary DNS domain of an existing IPA deployment.
|
||||
required: true
|
||||
realm:
|
||||
description: The Kerberos realm of an existing IPA deployment.
|
||||
required: true
|
||||
hostname:
|
||||
description: The hostname of the machine to join (FQDN).
|
||||
required: true
|
||||
kdc:
|
||||
description: The name or address of the host running the KDC.
|
||||
required: true
|
||||
kinit_attempts:
|
||||
description: Repeat the request for host Kerberos ticket X times.
|
||||
required: false
|
||||
type: int
|
||||
default: 5
|
||||
author:
|
||||
- Thomas Woerner
|
||||
'''
|
||||
|
||||
EXAMPLES = '''
|
||||
# Test IPA with local keytab
|
||||
- name: Test IPA in force mode with maximum 5 kinit attempts
|
||||
ipatest:
|
||||
servers: ["server1.example.com","server2.example.com"]
|
||||
domain: example.com
|
||||
realm: EXAMPLE.COM
|
||||
kdc: server1.example.com
|
||||
hostname: client1.example.com
|
||||
kinit_attempts: 5
|
||||
|
||||
# Test IPA with ipadiscovery return values
|
||||
- name: Join IPA
|
||||
ipajoin:
|
||||
servers: "{{ ipadiscovery.servers }}"
|
||||
domain: "{{ ipadiscovery.domain }}"
|
||||
realm: "{{ ipadiscovery.realm }}"
|
||||
kdc: "{{ ipadiscovery.kdc }}"
|
||||
hostname: "{{ ipadiscovery.hostname }}"
|
||||
'''
|
||||
|
||||
RETURN = '''
|
||||
krb5_keytab_ok:
|
||||
description: The flag describes if krb5.keytab on the host is usable.
|
||||
returned: always
|
||||
type: bool
|
||||
ca_crt_exists:
|
||||
description: The flag describes if ca.crt exists.
|
||||
returned: always
|
||||
krb5_conf_ok:
|
||||
description: The flag describes if krb5.conf on the host is usable.
|
||||
returned: always
|
||||
type: bool
|
||||
ipa_test_ok:
|
||||
description: The flag describes if ipa ping test succeded.
|
||||
returned: always
|
||||
type: bool
|
||||
'''
|
||||
|
||||
class Object(object):
|
||||
pass
|
||||
options = Object()
|
||||
|
||||
import os
|
||||
import tempfile
|
||||
|
||||
from ansible.module_utils.basic import AnsibleModule
|
||||
from ansible.module_utils.ansible_ipa_client import *
|
||||
|
||||
def main():
|
||||
module = AnsibleModule(
|
||||
argument_spec = dict(
|
||||
servers=dict(required=True, type='list'),
|
||||
domain=dict(required=True),
|
||||
realm=dict(required=True),
|
||||
hostname=dict(required=True),
|
||||
kdc=dict(required=True),
|
||||
kinit_attempts=dict(required=False, type='int', default=5),
|
||||
),
|
||||
supports_check_mode = True,
|
||||
)
|
||||
|
||||
module._ansible_debug = True
|
||||
servers = module.params.get('servers')
|
||||
domain = module.params.get('domain')
|
||||
realm = module.params.get('realm')
|
||||
hostname = module.params.get('hostname')
|
||||
kdc = module.params.get('kdc')
|
||||
kinit_attempts = module.params.get('kinit_attempts')
|
||||
|
||||
client_domain = hostname[hostname.find(".")+1:]
|
||||
host_principal = 'host/%s@%s' % (hostname, realm)
|
||||
sssd = True
|
||||
|
||||
# Remove IPA_DNS_CCACHE remain if it exists
|
||||
try:
|
||||
os.remove(paths.IPA_DNS_CCACHE)
|
||||
except OSError:
|
||||
pass
|
||||
|
||||
krb5_keytab_ok = False
|
||||
krb5_conf_ok = False
|
||||
ipa_test_ok = False
|
||||
ca_crt_exists = os.path.exists(paths.IPA_CA_CRT)
|
||||
env = {'PATH': SECURE_PATH, 'KRB5CCNAME': paths.IPA_DNS_CCACHE}
|
||||
|
||||
# First try: Validate krb5 keytab with system krb5 configuraiton
|
||||
try:
|
||||
kinit_keytab(host_principal, paths.KRB5_KEYTAB,
|
||||
paths.IPA_DNS_CCACHE,
|
||||
config=paths.KRB5_CONF,
|
||||
attempts=kinit_attempts)
|
||||
krb5_keytab_ok = True
|
||||
krb5_conf_ok = True
|
||||
|
||||
# Test IPA
|
||||
try:
|
||||
result = run(["/usr/bin/ipa", "ping"], raiseonerr=False, env=env)
|
||||
if result.returncode == 0:
|
||||
ipa_test_ok = True
|
||||
except OSError:
|
||||
pass
|
||||
except GSSError as e:
|
||||
pass
|
||||
|
||||
# Second try: Validate krb5 keytab with temporary krb5
|
||||
# configuration
|
||||
if not krb5_conf_ok:
|
||||
try:
|
||||
(krb_fd, krb_name) = tempfile.mkstemp()
|
||||
os.close(krb_fd)
|
||||
configure_krb5_conf(
|
||||
cli_realm=realm,
|
||||
cli_domain=domain,
|
||||
cli_server=servers,
|
||||
cli_kdc=kdc,
|
||||
dnsok=False,
|
||||
filename=krb_name,
|
||||
client_domain=client_domain,
|
||||
client_hostname=hostname,
|
||||
configure_sssd=sssd,
|
||||
force=False)
|
||||
|
||||
try:
|
||||
kinit_keytab(host_principal, paths.KRB5_KEYTAB,
|
||||
paths.IPA_DNS_CCACHE,
|
||||
config=krb_name,
|
||||
attempts=kinit_attempts)
|
||||
krb5_keytab_ok = True
|
||||
|
||||
# Test IPA
|
||||
env['KRB5_CONFIG'] = krb_name
|
||||
try:
|
||||
result = run(["/usr/bin/ipa", "ping"], raiseonerr=False,
|
||||
env=env)
|
||||
if result.returncode == 0:
|
||||
ipa_test_ok = True
|
||||
except OSError:
|
||||
pass
|
||||
|
||||
except GSSError as e:
|
||||
pass
|
||||
|
||||
finally:
|
||||
try:
|
||||
os.remove(krb_name)
|
||||
except OSError:
|
||||
module.fail_json(msg="Could not remove %s" % krb_name)
|
||||
|
||||
module.exit_json(changed=False,
|
||||
krb5_keytab_ok=krb5_keytab_ok,
|
||||
krb5_conf_ok=krb5_conf_ok,
|
||||
ca_crt_exists=ca_crt_exists,
|
||||
ipa_test_ok=ipa_test_ok)
|
||||
|
||||
if __name__ == '__main__':
|
||||
main()
|
||||
Reference in New Issue
Block a user