#! /usr/bin/python -E # Authors: Simo Sorce # Karl MacMillan # # Copyright (C) 2007 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 . # try: import sys import os import stat import time import socket import logging import tempfile import getpass import re from ipaclient import ipadiscovery import ipaclient.ipachangeconf import ipaclient.ntpconf from ipapython.ipautil import run, user_input, CalledProcessError, file_exists, install_file from ipapython import ipautil from ipapython import dnsclient from ipapython import sysrestore from ipapython import version from ipapython import certmonger from ipapython.config import IPAOptionParser import SSSDConfig from ConfigParser import RawConfigParser from optparse import SUPPRESS_HELP except ImportError: print >> sys.stderr, """\ There was a problem importing one of the required Python modules. The error was: %s """ % sys.exc_value sys.exit(1) CLIENT_INSTALL_ERROR = 1 CLIENT_NOT_CONFIGURED = 2 CLIENT_ALREADY_CONFIGURED = 3 CLIENT_UNINSTALL_ERROR = 4 # error after restoring files/state client_nss_nickname_format = 'IPA Machine Certificate - %s' def parse_options(): parser = IPAOptionParser(version=version.VERSION) parser.add_option("--domain", dest="domain", help="domain name") parser.add_option("--server", dest="server", help="IPA server") parser.add_option("--realm", dest="realm_name", help="realm name") parser.add_option("-f", "--force", dest="force", action="store_true", default=False, help="force setting of LDAP/Kerberos conf") parser.add_option("-d", "--debug", dest="debug", action="store_true", default=False, help="print debugging information") parser.add_option("-U", "--unattended", dest="unattended", action="store_true", help="unattended installation never prompts the user") parser.add_option("--ntp-server", dest="ntp_server", help="ntp server to use") parser.add_option("-S", "--no-sssd", action="store_false", help="Do not configure the client to use SSSD for authentication", default=True, dest="sssd") parser.add_option("-N", "--no-ntp", action="store_false", help="do not configure ntp", default=True, dest="conf_ntp") parser.add_option("-w", "--password", dest="password", sensitive=True, help="password to join the IPA realm (assumes bulk password unless principal is also set)"), parser.add_option("-W", dest="prompt_password", action="store_true", default=False, help="Prompt for a password to join the IPA realm"), parser.add_option("-p", "--principal", dest="principal", help="principal to use to join the IPA realm"), # --on-master is used in ipa-server-install and ipa-replica-install # only, it isn't meant to be used on clients. parser.add_option("--on-master", dest="on_master", action="store_true", help=SUPPRESS_HELP, default=False) parser.add_option("--permit", dest="permit", action="store_true", help="disable access rules by default, permit all access.", default=False) parser.add_option("--mkhomedir", dest="mkhomedir", action="store_true", help="create home directories for users on their first login", default=False) parser.add_option("", "--uninstall", dest="uninstall", action="store_true", default=False, help="uninstall an existing installation") parser.add_option("", "--hostname", dest="hostname", help="The hostname of this server (FQDN). If specified, the hostname will be set and " "the system configuration will be updated to persist over reboot. " "By default a nodename result from uname(2) is used.") parser.add_option("", "--enable-dns-updates", dest="dns_updates", action="store_true", default=False, help="Configures the machine to attempt dns updates when the ip address changes.") parser.add_option("--no-krb5-offline-passwords", dest="krb5_offline_passwords", action="store_false", help="Configure SSSD not to store user password when the server is offline", default=True) options, args = parser.parse_args() safe_opts = parser.get_safe_opts(options) if (options.server and not options.domain): parser.error("--server cannot be used without providing --domain") return safe_opts, options def logging_setup(options): # Always log everything (i.e., DEBUG) to the log # file. log_file = "/var/log/ipaclient-install.log" if options.uninstall: log_file = "/var/log/ipaclient-uninstall.log" old_umask = os.umask(077) logging.basicConfig(level=logging.DEBUG, format='%(asctime)s %(levelname)s %(message)s', filename=log_file, filemode='w') os.umask(old_umask) console = logging.StreamHandler() # If the debug option is set, also log debug messages to the console if options.debug: console.setLevel(logging.DEBUG) else: # Otherwise, log critical and error messages console.setLevel(logging.ERROR) formatter = logging.Formatter('%(name)-12s: %(levelname)-8s %(message)s') console.setFormatter(formatter) logging.getLogger('').addHandler(console) def nickname_exists(nickname): (sout, serr, returncode) = run(["/usr/bin/certutil", "-L", "-d", "/etc/pki/nssdb", "-n", nickname], raiseonerr=False) if returncode == 0: return True else: return False def emit_quiet(quiet, message): if not quiet: print message def uninstall(options, env, quiet=False): if not fstore.has_files(): print "IPA client is not configured on this system." return CLIENT_NOT_CONFIGURED server_fstore = sysrestore.FileStore('/var/lib/ipa/sysrestore') if server_fstore.has_files() and not options.on_master: print "IPA client is configured as a part of IPA server on this system." print "Refer to ipa-server-install for uninstallation." return CLIENT_NOT_CONFIGURED sssdconfig = SSSDConfig.SSSDConfig() sssdconfig.import_config() domains = sssdconfig.list_active_domains() hostname = None for name in domains: domain = sssdconfig.get_domain(name) try: provider = domain.get_option('id_provider') except SSSDConfig.NoOptionError: continue if provider == "ipa": try: hostname = domain.get_option('ipa_hostname') except SSSDConfig.NoOptionError: continue if hostname is None: hostname = socket.getfqdn() client_nss_nickname = client_nss_nickname_format % hostname # Remove our host cert and CA cert if nickname_exists("IPA CA"): try: run(["/usr/bin/certutil", "-D", "-d", "/etc/pki/nssdb", "-n", "IPA CA"]) except Exception, e: emit_quiet(quiet, "Failed to remove IPA CA from /etc/pki/nssdb: %s" % str(e)) # Always start certmonger. We can't untrack something if it isn't # running try: ipautil.service_start('messagebus') except Exception, e: logging.error("messagebus failed to start: %s" % str(e)) try: ipautil.service_start('certmonger') except Exception, e: logging.error("certmonger failed to start: %s" % str(e)) try: certmonger.stop_tracking('/etc/pki/nssdb', nickname=client_nss_nickname) except (CalledProcessError, RuntimeError), e: logging.error("certmonger failed to stop tracking certificate: %s" % str(e)) if nickname_exists(client_nss_nickname): try: run(["/usr/bin/certutil", "-D", "-d", "/etc/pki/nssdb", "-n", client_nss_nickname]) except Exception, e: emit_quiet(quiet, "Failed to remove %s from /etc/pki/nssdb: %s" % (client_nss_nickname, str(e))) try: ipautil.service_stop('certmonger') except Exception, e: logging.error("certmonger failed to stop: %s" % str(e)) # Remove any special principal names we added to the IPA CA helper certmonger.remove_principal_from_cas() try: ipautil.chkconfig_off('certmonger') except Exception, e: emit_quiet(quiet, "Failed to disable automatic startup of the certmonger daemon") logging.error("Failed to disable automatic startup of the certmonger daemon: %s" % str(e)) if not options.on_master and os.path.exists('/etc/ipa/default.conf'): emit_quiet(quiet, "Unenrolling client from IPA server") join_args = ["/usr/sbin/ipa-join", "--unenroll", "-h", hostname] (stdout, stderr, returncode) = run(join_args, raiseonerr=False, env=env) if returncode != 0: emit_quiet(quiet, "Unenrolling host failed: %s" % stderr) if os.path.exists('/etc/ipa/default.conf'): emit_quiet(quiet, "Removing Kerberos service principals from /etc/krb5.keytab") try: parser = RawConfigParser() fp = open('/etc/ipa/default.conf', 'r') parser.readfp(fp) fp.close() realm = parser.get('global', 'realm') run(["/usr/sbin/ipa-rmkeytab", "-k", "/etc/krb5.keytab", "-r", realm]) except Exception, e: emit_quiet(quiet, "Failed to clean up /etc/krb5.keytab") logging.debug("Failed to remove Kerberos service principals: %s" % str(e)) emit_quiet(quiet, "Disabling client Kerberos and LDAP configurations") try: run(["/usr/sbin/authconfig", "--disableldap", "--disablekrb5", "--disablesssd", "--disablesssdauth", "--disablemkhomedir", "--update"]) except Exception, e: emit_quiet(quiet, "Failed to remove krb5/LDAP configuration. " +str(e)) return CLIENT_INSTALL_ERROR if fstore.has_files(): emit_quiet(quiet, "Restoring client configuration files") fstore.restore_all_files() old_hostname = statestore.restore_state('network','hostname') if old_hostname is not None and old_hostname != hostname: try: ipautil.run(['/bin/hostname', old_hostname]) except CalledProcessError, e: print >>sys.stderr, "Failed to set this machine hostname to %s (%s)." % (old_hostname, str(e)) if ipautil.service_is_installed('nscd'): try: ipautil.service_restart('nscd') except: emit_quiet(quiet, "Failed to restart start the NSCD daemon") try: ipautil.chkconfig_on('nscd') except: emit_quiet(quiet, "Failed to configure automatic startup of the NSCD daemon") else: # this is optional service, just log logging.info("NSCD daemon is not installed, skip configuration") if ipautil.service_is_installed('nslcd'): try: ipautil.service_stop('nslcd') except: emit_quiet(quiet, "Failed to stop the NSLCD daemon") try: ipautil.chkconfig_off('nslcd') except: emit_quiet(quiet, "Failed to disable automatic startup of the NSLCD daemon") else: # this is optional service, just log logging.info("NSLCD daemon is not installed, skip configuration") if not options.unattended: emit_quiet(quiet, "The original nsswitch.conf configuration has been restored.") emit_quiet(quiet, "You may need to restart services or reboot the machine.") if not options.on_master: if user_input("Do you want to reboot the machine?", False): try: run(["/usr/bin/reboot"]) except Exception, e: emit_quiet(quiet, "Reboot command failed to exceute. " + str(e)) return CLIENT_UNINSTALL_ERROR # Remove the IPA configuration file try: os.remove("/etc/ipa/default.conf") except: pass return 0 def configure_ipa_conf(fstore, cli_basedn, cli_realm, cli_domain, cli_server): ipaconf = ipaclient.ipachangeconf.IPAChangeConf("IPA Installer") ipaconf.setOptionAssignment(" = ") ipaconf.setSectionNameDelimiters(("[","]")) opts = [{'name':'comment', 'type':'comment', 'value':'File modified by ipa-client-install'}, {'name':'empty', 'type':'empty'}] #[global] defopts = [{'name':'basedn', 'type':'option', 'value':cli_basedn}, {'name':'realm', 'type':'option', 'value':cli_realm}, {'name':'domain', 'type':'option', 'value':cli_domain}, {'name':'server', 'type':'option', 'value':cli_server}, {'name':'xmlrpc_uri', 'type':'option', 'value':'https://%s/ipa/xml' % cli_server}, {'name':'enable_ra', 'type':'option', 'value':'True'}] opts.append({'name':'global', 'type':'section', 'value':defopts}) opts.append({'name':'empty', 'type':'empty'}) fstore.backup_file("/etc/ipa/default.conf") ipaconf.newConf("/etc/ipa/default.conf", opts) return 0 def configure_ldap_conf(fstore, cli_basedn, cli_realm, cli_domain, cli_server, dnsok, options): ldapconf = ipaclient.ipachangeconf.IPAChangeConf("IPA Installer") ldapconf.setOptionAssignment(" ") opts = [{'name':'comment', 'type':'comment', 'value':'File modified by ipa-client-install'}, {'name':'empty', 'type':'empty'}, {'name':'ldap_version', 'type':'option', 'value':'3'}, {'name':'base', 'type':'option', 'value':cli_basedn}, {'name':'empty', 'type':'empty'}, {'name':'nss_base_passwd', 'type':'option', 'value':'cn=users,cn=accounts,'+cli_basedn+'?sub'}, {'name':'nss_base_group', 'type':'option', 'value':'cn=groups,cn=accounts,'+cli_basedn+'?sub'}, {'name':'nss_schema', 'type':'option', 'value':'rfc2307bis'}, {'name':'nss_map_attribute', 'type':'option', 'value':'uniqueMember member'}, {'name':'nss_initgroups_ignoreusers', 'type':'option', 'value':'root,dirsrv'}, {'name':'empty', 'type':'empty'}, {'name':'nss_reconnect_maxsleeptime', 'type':'option', 'value':'8'}, {'name':'nss_reconnect_sleeptime', 'type':'option', 'value':'1'}, {'name':'bind_timelimit', 'type':'option', 'value':'5'}, {'name':'timelimit', 'type':'option', 'value':'15'}, {'name':'empty', 'type':'empty'}] if not dnsok or options.force or options.on_master: if options.on_master: opts.append({'name':'uri', 'type':'option', 'value':'ldap://localhost'}) else: opts.append({'name':'uri', 'type':'option', 'value':'ldap://'+cli_server}) else: opts.append({'name':'nss_srv_domain', 'type':'option', 'value':cli_domain}) opts.append({'name':'empty', 'type':'empty'}) ret = (0, None, None) # Depending on the release and distribution this may exist in any # number of different file names, update what we find for filename in ['/etc/ldap.conf', '/etc/nss_ldap.conf', '/etc/libnss-ldap.conf', '/etc/pam_ldap.conf']: if file_exists(filename): try: fstore.backup_file(filename) ldapconf.newConf(filename, opts) return (0, 'LDAP', filename) except Exception, e: print "Creation of %s: %s" % (filename, str(e)) return (1, 'LDAP', filename) return ret def configure_nslcd_conf(fstore, cli_basedn, cli_realm, cli_domain, cli_server, dnsok, options): nslcdconf = ipaclient.ipachangeconf.IPAChangeConf("IPA Installer") nslcdconf.setOptionAssignment(" ") opts = [{'name':'comment', 'type':'comment', 'value':'File modified by ipa-client-install'}, {'name':'empty', 'type':'empty'}, {'name':'ldap_version', 'type':'option', 'value':'3'}, {'name':'base', 'type':'option', 'value':cli_basedn}, {'name':'empty', 'type':'empty'}, {'name':'base passwd', 'type':'option', 'value':'cn=users,cn=accounts,'+cli_basedn}, {'name':'base group', 'type':'option', 'value':'cn=groups,cn=accounts,'+cli_basedn}, {'name':'map group', 'type':'option', 'value':'uniqueMember member'}, {'name':'timelimit', 'type':'option', 'value':'15'}, {'name':'empty', 'type':'empty'}] if not dnsok or options.force or options.on_master: if options.on_master: opts.append({'name':'uri', 'type':'option', 'value':'ldap://localhost'}) else: opts.append({'name':'uri', 'type':'option', 'value':'ldap://'+cli_server}) else: opts.append({'name':'uri', 'type':'option', 'value':'DNS'}) opts.append({'name':'empty', 'type':'empty'}) if file_exists('/etc/nslcd.conf'): try: fstore.backup_file('/etc/nslcd.conf') nslcdconf.newConf('/etc/nslcd.conf', opts) except Exception, e: print "Creation of %s: %s" % ('/etc/nslcd.conf', str(e)) return (1, None, None) if ipautil.service_is_installed('nslcd'): try: ipautil.service_restart('nslcd') except Exception, e: logging.error("nslcd failed to restart: %s" % str(e)) try: ipautil.chkconfig_on('nslcd') except Exception, e: print "Failed to configure automatic startup of the NSLCD daemon" logging.error("Failed to enable automatic startup of the NSLCD daemon: %s" % str(e)) else: logging.debug("NSLCD daemon is not installed, skip configuration") return (0, None, None) return (0, 'NSLCD', '/etc/nslcd.conf') def hardcode_ldap_server(cli_server): """ DNS Discovery didn't return a valid IPA server, hardcode a value into the file instead. """ if not file_exists('/etc/ldap.conf'): return ldapconf = ipaclient.ipachangeconf.IPAChangeConf("IPA Installer") ldapconf.setOptionAssignment(" ") opts = [{'name':'uri', 'type':'option', 'action':'set', 'value':'ldap://'+cli_server}, {'name':'empty', 'type':'empty'}] # Errors raised by this should be caught by the caller ldapconf.changeConf("/etc/ldap.conf", opts) print "Changed configuration of /etc/ldap.conf to use hardcoded server name: " +cli_server return def configure_krb5_conf(fstore, cli_basedn, cli_realm, cli_domain, cli_server, cli_kdc, dnsok, options, filename): krbconf = ipaclient.ipachangeconf.IPAChangeConf("IPA Installer") krbconf.setOptionAssignment(" = ") krbconf.setSectionNameDelimiters(("[","]")) krbconf.setSubSectionDelimiters(("{","}")) krbconf.setIndent((""," "," ")) opts = [{'name':'comment', 'type':'comment', 'value':'File modified by ipa-client-install'}, {'name':'empty', 'type':'empty'}] #[libdefaults] libopts = [{'name':'default_realm', 'type':'option', 'value':cli_realm}] if not dnsok or not cli_kdc or options.force: libopts.append({'name':'dns_lookup_realm', 'type':'option', 'value':'false'}) libopts.append({'name':'dns_lookup_kdc', 'type':'option', 'value':'false'}) else: libopts.append({'name':'dns_lookup_realm', 'type':'option', 'value':'true'}) libopts.append({'name':'dns_lookup_kdc', 'type':'option', 'value':'true'}) libopts.append({'name':'rdns', 'type':'option', 'value':'false'}) libopts.append({'name':'ticket_lifetime', 'type':'option', 'value':'24h'}) libopts.append({'name':'forwardable', 'type':'option', 'value':'yes'}) opts.append({'name':'libdefaults', 'type':'section', 'value':libopts}) opts.append({'name':'empty', 'type':'empty'}) #the following are necessary only if DNS discovery does not work if not dnsok or not cli_kdc or options.force: #[realms] kropts =[{'name':'kdc', 'type':'option', 'value':cli_server+':88'}, {'name':'admin_server', 'type':'option', 'value':cli_server+':749'}, {'name':'default_domain', 'type':'option', 'value':cli_domain}] else: kropts = [] kropts.append({'name':'pkinit_anchors', 'type':'option', 'value':'FILE:/etc/ipa/ca.crt'}) ropts = [{'name':cli_realm, 'type':'subsection', 'value':kropts}] opts.append({'name':'realms', 'type':'section', 'value':ropts}) opts.append({'name':'empty', 'type':'empty'}) #[domain_realm] dropts = [{'name':'.'+cli_domain, 'type':'option', 'value':cli_realm}, {'name':cli_domain, 'type':'option', 'value':cli_realm}] opts.append({'name':'domain_realm', 'type':'section', 'value':dropts}) opts.append({'name':'empty', 'type':'empty'}) #[appdefaults] pamopts = [{'name':'debug', 'type':'option', 'value':'false'}, {'name':'krb4_convert', 'type':'option', 'value':'false'}] appopts = [{'name':'pam', 'type':'subsection', 'value':pamopts}] opts.append({'name':'appdefaults', 'type':'section', 'value':appopts}) logging.debug("Writing Kerberos configuration to %s:\n%s" % (filename, krbconf.dump(opts))) krbconf.newConf(filename, opts); return 0 def configure_certmonger(fstore, subject_base, cli_realm, hostname, options): started = True principal = 'host/%s@%s' % (hostname, cli_realm) try: ipautil.service_start('messagebus') except Exception, e: logging.error("messagebus failed to start: %s" % str(e)) # Ensure that certmonger has been started at least once to generate the # cas files in /var/lib/certmonger/cas. try: ipautil.service_restart('certmonger') except Exception, e: logging.error("certmonger failed to restart: %s" % str(e)) if options.hostname: # It needs to be stopped if we touch them try: ipautil.service_stop('certmonger') except Exception, e: logging.error("certmonger failed to stop: %s" % str(e)) # If the hostname is explicitly set then we need to tell certmonger # which principal name to use when requesting certs. certmonger.add_principal_to_cas(principal) try: ipautil.service_restart('certmonger') except Exception, e: print "Failed to start the certmonger daemon" print "Automatic certificate management will not be available" logging.error("certmonger failed to restart: %s" % str(e)) started = False try: ipautil.chkconfig_on('certmonger') except Exception, e: print "Failed to configure automatic startup of the certmonger daemon" print "Automatic certificate management will not be available" logging.error("Failed to disable automatic startup of the certmonger daemon: %s" % str(e)) # Request our host cert if started: client_nss_nickname = client_nss_nickname_format % hostname subject = 'CN=%s,%s' % (hostname, subject_base) try: run(["ipa-getcert", "request", "-d", "/etc/pki/nssdb", "-n", client_nss_nickname, "-N", subject, "-K", principal]) except: print "certmonger request for host certificate failed" def backup_and_replace_hostname(fstore, statestore, hostname): # TODO: this code is for Red Hat-based systems # it need to be rewritten for cross-paltform support # so that different configuration backends would be possible # (GNU/Debian stores this information in a different place) network_filename = "/etc/sysconfig/network" # Backup original /etc/sysconfig/network fstore.backup_file(network_filename) hostname_pattern = re.compile(''' (^ \s* (?P