diff options
author | Martin Basti <mbasti@redhat.com> | 2016-10-19 10:52:37 +0200 |
---|---|---|
committer | Jan Cholasta <jcholast@redhat.com> | 2016-11-11 12:13:56 +0100 |
commit | f98faec47847022879b8bceb63839fcfd6e45402 (patch) | |
tree | 72b29472123d88ac07e01690af7831d71224fbb9 /client | |
parent | 0933e080aa9635bba12efc53d904d524b309027f (diff) | |
download | freeipa-f98faec47847022879b8bceb63839fcfd6e45402.tar.gz freeipa-f98faec47847022879b8bceb63839fcfd6e45402.tar.xz freeipa-f98faec47847022879b8bceb63839fcfd6e45402.zip |
ipa-client-install: move client install to module
This commit only moves the code from ipa-client-install to module
ipaclient/install/client.py and fixes PEP8.
https://fedorahosted.org/freeipa/ticket/6392
Reviewed-By: Stanislav Laznicka <slaznick@redhat.com>
Diffstat (limited to 'client')
-rwxr-xr-x | client/ipa-client-install | 2905 |
1 files changed, 20 insertions, 2885 deletions
diff --git a/client/ipa-client-install b/client/ipa-client-install index c228ea3ce..a30fee8b9 100755 --- a/client/ipa-client-install +++ b/client/ipa-client-install @@ -21,62 +21,20 @@ from __future__ import print_function -try: - import sys - - import os - import time - import socket - import tempfile - import getpass - from six.moves.configparser import RawConfigParser - from optparse import SUPPRESS_HELP, OptionGroup, OptionValueError - import dns - import gssapi - import netifaces - - import SSSDConfig - from six.moves.urllib.parse import urlparse, urlunparse - from cryptography.hazmat.primitives import serialization +import sys +import os - from ipapython.ipa_log_manager import standard_logging_setup, root_logger - from ipaclient import ipadiscovery - import ipaclient.ipachangeconf - import ipaclient.ntpconf - from ipapython.ipautil import ( - run, user_input, CalledProcessError, file_exists, dir_exists, - realm_to_suffix, is_fips_enabled) - from ipaplatform.tasks import tasks - from ipaplatform import services - from ipaplatform.paths import paths - from ipapython import ipautil, sysrestore, version, certmonger, ipaldap - from ipapython import kernel_keyring, certdb - from ipapython.config import IPAOptionParser - from ipalib import api, errors - from ipalib import x509, certstore - from ipalib.util import ( - normalize_hostname, validate_domain_name, verify_host_resolvable, - network_ip_address_warning, broadcast_ip_address_warning - ) - from ipalib.constants import CACERT - from ipapython.dn import DN - from ipapython.ssh import SSHPublicKey - from ipalib.rpc import delete_persistent_client_session_data +from optparse import SUPPRESS_HELP, OptionGroup, OptionValueError -except ImportError as e: - print("""\ -There was a problem importing one of the required Python modules. The -error was: - - %s -""" % e, file=sys.stderr) - sys.exit(1) - -SUCCESS = 0 -CLIENT_INSTALL_ERROR = 1 -CLIENT_NOT_CONFIGURED = 2 -CLIENT_ALREADY_CONFIGURED = 3 -CLIENT_UNINSTALL_ERROR = 4 # error after restoring files/state +from ipaclient.install import client +from ipapython.ipa_log_manager import standard_logging_setup, root_logger +from ipapython.ipautil import is_fips_enabled +from ipaplatform.tasks import tasks +from ipaplatform.paths import paths +from ipapython import version, sysrestore +from ipapython.config import IPAOptionParser +from ipalib import x509 +from ipalib.util import normalize_hostname, validate_domain_name fstore = sysrestore.FileStore(paths.IPA_CLIENT_SYSRESTORE) @@ -256,6 +214,7 @@ def parse_options(): return safe_opts, options + def logging_setup(options): log_file = paths.IPACLIENT_INSTALL_LOG @@ -267,2830 +226,6 @@ def logging_setup(options): console_format='%(message)s') -def remove_file(filename): - """ - Deletes a file. If the file does not exist (OSError 2) does nothing. - Otherwise logs an error message and instructs the user to remove the - offending file manually - :param filename: name of the file to be removed - """ - - try: - os.remove(filename) - except OSError as e: - if e.errno == 2: - return - - root_logger.error("Failed to remove file %s: %s", filename, e) - root_logger.error('Please remove %s manually, as it can cause ' - 'subsequent installation to fail.', filename) - - -def log_service_error(name, action, error): - root_logger.error("%s failed to %s: %s", name, action, str(error)) - -def cert_summary(msg, certs, indent=' '): - if msg: - s = '%s\n' % msg - else: - s = '' - for cert in certs: - s += '%sSubject: %s\n' % (indent, DN(cert.subject)) - s += '%sIssuer: %s\n' % (indent, DN(cert.issuer)) - s += '%sValid From: %s\n' % (indent, cert.not_valid_before) - s += '%sValid Until: %s\n' % (indent, cert.not_valid_after) - s += '\n' - s = s[:-1] - - return s - -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 CACERT then use that. - - Otherwise return None. - """ - if cert_path is not None: - return cert_path - - if os.path.exists(CACERT): - return CACERT - - return None - - -def save_state(service): - enabled = service.is_enabled() - running = service.is_running() - - if enabled or running: - statestore.backup_state(service.service_name, 'enabled', enabled) - statestore.backup_state(service.service_name, 'running', running) - - -def restore_state(service): - enabled = statestore.restore_state(service.service_name, 'enabled') - running = statestore.restore_state(service.service_name, 'running') - - if enabled: - try: - service.enable() - except Exception: - root_logger.warning( - "Failed to configure automatic startup of the %s daemon", - service.service_name - ) - if running: - try: - service.start() - except Exception: - root_logger.warning( - "Failed to restart the %s daemon", - service.service_name - ) - - -# Checks whether nss_ldap or nss-pam-ldapd is installed. If anyone of mandatory files was found returns True and list of all files found. -def nssldap_exists(): - files_to_check = [{'function':'configure_ldap_conf', 'mandatory':[paths.LDAP_CONF,paths.NSS_LDAP_CONF,paths.LIBNSS_LDAP_CONF], 'optional':[paths.PAM_LDAP_CONF]}, - {'function':'configure_nslcd_conf', 'mandatory':[paths.NSLCD_CONF]}] - files_found = {} - retval = False - - for function in files_to_check: - files_found[function['function']]=[] - for file_type in ['mandatory','optional']: - try: - for filename in function[file_type]: - if file_exists(filename): - files_found[function['function']].append(filename) - if file_type == 'mandatory': - retval = True - except KeyError: - pass - - return (retval, files_found) - -# helper function for uninstall -# deletes IPA domain from sssd.conf -def delete_ipa_domain(): - try: - sssdconfig = SSSDConfig.SSSDConfig() - sssdconfig.import_config() - domains = sssdconfig.list_active_domains() - - ipa_domain_name = None - - for name in domains: - domain = sssdconfig.get_domain(name) - try: - provider = domain.get_option('id_provider') - if provider == "ipa": - ipa_domain_name = name - break - except SSSDConfig.NoOptionError: - continue - - if ipa_domain_name is not None: - sssdconfig.delete_domain(ipa_domain_name) - sssdconfig.write() - else: - root_logger.warning("IPA domain could not be found in " - "/etc/sssd/sssd.conf and therefore not deleted") - except IOError: - root_logger.warning("IPA domain could not be deleted. " - "No access to the /etc/sssd/sssd.conf file.") - -def is_ipa_client_installed(on_master=False): - """ - Consider IPA client not installed if nothing is backed up - and default.conf file does not exist. If on_master is set to True, - the existence of default.conf file is not taken into consideration, - since it has been already created by ipa-server-install. - """ - - installed = fstore.has_files() or \ - (not on_master and os.path.exists(paths.IPA_DEFAULT_CONF)) - - return installed - -def configure_nsswitch_database(fstore, database, services, preserve=True, - append=True, default_value=()): - """ - Edits the specified nsswitch.conf database (e.g. passwd, group, sudoers) - to use the specified service(s). - - Arguments: - fstore - FileStore to backup the nsswitch.conf - database - database configuration that should be ammended, e.g 'sudoers' - service - list of services that should be added, e.g. ['sss'] - preserve - if True, the already configured services will be preserved - - The next arguments modify the behaviour if preserve=True: - append - if True, the services will be appended, if False, prepended - default_value - list of services that are considered as default (if - the database is not mentioned in nsswitch.conf), e.g. - ['files'] - """ - - # Backup the original version of nsswitch.conf, we're going to edit it now - if not fstore.has_file(paths.NSSWITCH_CONF): - fstore.backup_file(paths.NSSWITCH_CONF) - - conf = ipaclient.ipachangeconf.IPAChangeConf("IPA Installer") - conf.setOptionAssignment(':') - - if preserve: - # Read the existing configuration - with open(paths.NSSWITCH_CONF, 'r') as f: - opts = conf.parse(f) - raw_database_entry = conf.findOpts(opts, 'option', database)[1] - - # Detect the list of already configured services - if not raw_database_entry: - # If there is no database entry, database is not present in - # the nsswitch.conf. Set the list of services to the - # default list, if passed. - configured_services = list(default_value) - else: - configured_services = raw_database_entry['value'].strip().split() - - # Make sure no service is added if already mentioned in the list - added_services = [s for s in services - if s not in configured_services] - - # Prepend / append the list of new services - if append: - new_value = ' ' + ' '.join(configured_services + added_services) - else: - new_value = ' ' + ' '.join(added_services + configured_services) - - else: - # Preserve not set, let's rewrite existing configuration - new_value = ' ' + ' '.join(services) - - # Set new services as sources for database - opts = [{'name': database, - 'type':'option', - 'action':'set', - 'value': new_value - }, - {'name':'empty', - 'type':'empty' - }] - - conf.changeConf(paths.NSSWITCH_CONF, opts) - root_logger.info("Configured %s in %s" % (database, paths.NSSWITCH_CONF)) - - -def uninstall(options, env): - - if not is_ipa_client_installed(): - root_logger.error("IPA client is not configured on this system.") - return CLIENT_NOT_CONFIGURED - - server_fstore = sysrestore.FileStore(paths.SYSRESTORE) - if server_fstore.has_files() and not options.on_master: - root_logger.error( - "IPA client is configured as a part of IPA server on this system.") - root_logger.info("Refer to ipa-server-install for uninstallation.") - return CLIENT_NOT_CONFIGURED - - try: - run(["ipa-client-automount", "--uninstall", "--debug"]) - except Exception as e: - root_logger.error( - "Unconfigured automount client failed: %s", str(e)) - - # Reload the state as automount unconfigure may have modified it - fstore._load() - statestore._load() - - hostname = None - ipa_domain = None - was_sssd_configured = False - try: - sssdconfig = SSSDConfig.SSSDConfig() - sssdconfig.import_config() - domains = sssdconfig.list_active_domains() - all_domains = sssdconfig.list_domains() - - # we consider all the domains, because handling sssd.conf - # during uninstall is dependant on was_sssd_configured flag - # so the user does not lose info about inactive domains - if len(all_domains) > 1: - # There was more than IPA domain configured - was_sssd_configured = True - 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 - try: - ipa_domain = domain.get_option('ipa_domain') - except SSSDConfig.NoOptionError: - pass - except Exception as e: - # We were unable to read existing SSSD config. This might mean few things: - # - sssd wasn't installed - # - sssd was removed after install and before uninstall - # - there are no active domains - # in both cases we cannot continue with SSSD - pass - - if hostname is None: - hostname = socket.getfqdn() - - ipa_db = certdb.NSSDatabase(paths.IPA_NSSDB_DIR) - sys_db = certdb.NSSDatabase(paths.NSS_DB_DIR) - - cmonger = services.knownservices.certmonger - if ipa_db.has_nickname('Local IPA host'): - try: - certmonger.stop_tracking(paths.IPA_NSSDB_DIR, - nickname='Local IPA host') - except RuntimeError as e: - root_logger.error("%s failed to stop tracking certificate: %s", - cmonger.service_name, e) - - client_nss_nickname = 'IPA Machine Certificate - %s' % hostname - if sys_db.has_nickname(client_nss_nickname): - try: - certmonger.stop_tracking(paths.NSS_DB_DIR, - nickname=client_nss_nickname) - except RuntimeError as e: - root_logger.error("%s failed to stop tracking certificate: %s", - cmonger.service_name, e) - - for filename in (os.path.join(ipa_db.secdir, 'cert8.db'), - os.path.join(ipa_db.secdir, 'key3.db'), - os.path.join(ipa_db.secdir, 'secmod.db'), - os.path.join(ipa_db.secdir, 'pwdfile.txt')): - remove_file(filename) - - # Remove any special principal names we added to the IPA CA helper - certmonger.remove_principal_from_cas() - - try: - cmonger.stop() - except Exception as e: - log_service_error(cmonger.service_name, 'stop', e) - - try: - cmonger.disable() - except Exception as e: - root_logger.error( - "Failed to disable automatic startup of the %s service: %s", - cmonger.service_name, str(e)) - - if not options.on_master and os.path.exists(paths.IPA_DEFAULT_CONF): - root_logger.info("Unenrolling client from IPA server") - join_args = [paths.SBIN_IPA_JOIN, "--unenroll", "-h", hostname] - if options.debug: - join_args.append("-d") - env['XMLRPC_TRACE_CURL'] = 'yes' - result = run(join_args, raiseonerr=False, env=env) - if result.returncode != 0: - root_logger.error("Unenrolling host failed: %s", result.error_log) - - if os.path.exists(paths.IPA_DEFAULT_CONF): - root_logger.info( - "Removing Kerberos service principals from /etc/krb5.keytab") - try: - parser = RawConfigParser() - fp = open(paths.IPA_DEFAULT_CONF, 'r') - parser.readfp(fp) - fp.close() - realm = parser.get('global', 'realm') - run([paths.IPA_RMKEYTAB, "-k", paths.KRB5_KEYTAB, "-r", realm]) - except CalledProcessError as err: - if err.returncode != 5: - # 5 means Principal name or realm not found in keytab - # and can be ignored - root_logger.error( - "Failed to remove Kerberos service principals: %s", - str(err)) - except Exception as e: - root_logger.error( - "Failed to remove Kerberos service principals: %s", str(e)) - - root_logger.info("Disabling client Kerberos and LDAP configurations") - was_sssd_installed = False - was_sshd_configured = False - if fstore.has_files(): - was_sssd_installed = fstore.has_file(paths.SSSD_CONF) - - sshd_config = os.path.join(services.knownservices.sshd.get_config_dir(), "sshd_config") - was_sshd_configured = fstore.has_file(sshd_config) - try: - tasks.restore_pre_ipa_client_configuration(fstore, - statestore, - was_sssd_installed, - was_sssd_configured) - except Exception as e: - root_logger.error( - "Failed to remove krb5/LDAP configuration: %s", str(e)) - return CLIENT_INSTALL_ERROR - - # Clean up the SSSD cache before SSSD service is stopped or restarted - remove_file(paths.SSSD_MC_GROUP) - remove_file(paths.SSSD_MC_PASSWD) - - if ipa_domain: - sssd_domain_ldb = "cache_" + ipa_domain + ".ldb" - sssd_ldb_file = os.path.join(paths.SSSD_DB, sssd_domain_ldb) - remove_file(sssd_ldb_file) - - sssd_domain_ccache = "ccache_" + ipa_domain.upper() - sssd_ccache_file = os.path.join(paths.SSSD_DB, sssd_domain_ccache) - remove_file(sssd_ccache_file) - - # Next if-elif-elif construction deals with sssd.conf file. - # Old pre-IPA domains are preserved due merging the old sssd.conf - # during the installation of ipa-client but any new domains are - # only present in sssd.conf now, so we don't want to delete them - # by rewriting sssd.conf file. IPA domain is removed gracefully. - - # SSSD was installed before our installation and other non-IPA domains - # found, restore backed up sssd.conf to sssd.conf.bkp and remove IPA - # domain from the current sssd.conf - if was_sssd_installed and was_sssd_configured: - root_logger.info( - "The original configuration of SSSD included other domains than " + - "the IPA-based one.") - - delete_ipa_domain() - - - restored = False - try: - restored = fstore.restore_file(paths.SSSD_CONF,paths.SSSD_CONF_BKP) - except OSError: - root_logger.debug("Error while restoring pre-IPA /etc/sssd/sssd.conf.") - - if restored: - root_logger.info("Original pre-IPA SSSD configuration file was " - "restored to /etc/sssd/sssd.conf.bkp.") - - root_logger.info("IPA domain removed from current one, " + - "restarting SSSD service") - sssd = services.service('sssd') - try: - sssd.restart() - except CalledProcessError: - root_logger.warning("SSSD service restart was unsuccessful.") - - # SSSD was not installed before our installation, but other domains found, - # delete IPA domain, but leave other domains intact - elif not was_sssd_installed and was_sssd_configured: - delete_ipa_domain() - root_logger.info("Other domains than IPA domain found, " + - "IPA domain was removed from /etc/sssd/sssd.conf.") - - sssd = services.service('sssd') - try: - sssd.restart() - except CalledProcessError: - root_logger.warning("SSSD service restart was unsuccessful.") - - # SSSD was not installed before our installation, and no other domains - # than IPA are configured in sssd.conf - make sure config file is removed - elif not was_sssd_installed and not was_sssd_configured: - try: - os.rename(paths.SSSD_CONF,paths.SSSD_CONF_DELETED) - except OSError: - root_logger.debug("Error while moving /etc/sssd/sssd.conf to %s" % - paths.SSSD_CONF_DELETED) - - root_logger.info("Redundant SSSD configuration file " + - "/etc/sssd/sssd.conf was moved to /etc/sssd/sssd.conf.deleted") - - sssd = services.service('sssd') - try: - sssd.stop() - except CalledProcessError: - root_logger.warning("SSSD service could not be stopped") - - try: - sssd.disable() - except CalledProcessError as e: - root_logger.warning( - "Failed to disable automatic startup of the SSSD daemon: %s", e) - - tasks.restore_hostname(fstore, statestore) - - if fstore.has_files(): - root_logger.info("Restoring client configuration files") - fstore.restore_all_files() - - unconfigure_nisdomain() - - nscd = services.knownservices.nscd - nslcd = services.knownservices.nslcd - - for service in (nscd, nslcd): - if service.is_installed(): - restore_state(service) - else: - # this is an optional service, just log - root_logger.info( - "%s daemon is not installed, skip configuration", - service.service_name - ) - - ntp_configured = statestore.has_state('ntp') - if ntp_configured: - ntp_enabled = statestore.restore_state('ntp', 'enabled') - ntp_step_tickers = statestore.restore_state('ntp', 'step-tickers') - restored = False - - try: - # Restore might fail due to file missing in backup - # the reason for it might be that freeipa-client was updated - # to this version but not unenrolled/enrolled again - # In such case it is OK to fail - restored = fstore.restore_file(paths.NTP_CONF) - restored |= fstore.restore_file(paths.SYSCONFIG_NTPD) - if ntp_step_tickers: - restored |= fstore.restore_file(paths.NTP_STEP_TICKERS) - except Exception: - pass - - if not ntp_enabled: - services.knownservices.ntpd.stop() - services.knownservices.ntpd.disable() - else: - if restored: - services.knownservices.ntpd.restart() - - try: - ipaclient.ntpconf.restore_forced_ntpd(statestore) - except CalledProcessError as e: - root_logger.error('Failed to start chronyd: %s', e) - - if was_sshd_configured and services.knownservices.sshd.is_running(): - services.knownservices.sshd.restart() - - # Remove the Firefox configuration - if statestore.has_state('firefox'): - root_logger.info("Removing Firefox configuration.") - preferences_fname = statestore.restore_state('firefox', 'preferences_fname') - if preferences_fname is not None: - if file_exists(preferences_fname): - try: - os.remove(preferences_fname) - except Exception as e: - root_logger.warning("'%s' could not be removed: %s." % preferences_fname, str(e)) - root_logger.warning("Please remove file '%s' manually." % preferences_fname) - - rv = 0 - - if fstore.has_files(): - root_logger.error('Some files have not been restored, see %s' % - paths.SYSRESTORE_INDEX) - has_state = False - for module in statestore.modules: - root_logger.error('Some installation state for %s has not been ' - 'restored, see /var/lib/ipa/sysrestore/sysrestore.state', - module) - has_state = True - rv = 1 - - if has_state: - root_logger.warning( - 'Some installation state has not been restored.\n' - 'This may cause re-installation to fail.\n' - 'It should be safe to remove /var/lib/ipa-client/sysrestore.state ' - 'but it may\n mean your system hasn\'t been restored ' - 'to its pre-installation state.') - - # Remove the IPA configuration file - remove_file(paths.IPA_DEFAULT_CONF) - - # Remove the CA cert from the systemwide certificate store - tasks.remove_ca_certs_from_systemwide_ca_store() - - # Remove the CA cert - remove_file(CACERT) - - root_logger.info("Client uninstall complete.") - - # The next block of code prompts for reboot, therefore all uninstall - # logic has to be done before - - if not options.unattended: - root_logger.info( - "The original nsswitch.conf configuration has been restored.") - root_logger.info( - "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([paths.SBIN_REBOOT]) - except Exception as e: - root_logger.error( - "Reboot command failed to exceute: %s", str(e)) - return CLIENT_UNINSTALL_ERROR - - # IMPORTANT: Do not put any client uninstall logic after the block above - - return rv - -def configure_ipa_conf(fstore, cli_basedn, cli_realm, cli_domain, cli_server, hostname): - 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[0]}, - {'name':'host', 'type':'option', 'value':hostname}, - {'name':'xmlrpc_uri', 'type':'option', 'value':'https://%s/ipa/xml' % ipautil.format_netloc(cli_server[0])}, - {'name':'enable_ra', 'type':'option', 'value':'True'}] - - opts.append({'name':'global', 'type':'section', 'value':defopts}) - opts.append({'name':'empty', 'type':'empty'}) - - target_fname = paths.IPA_DEFAULT_CONF - fstore.backup_file(target_fname) - ipaconf.newConf(target_fname, opts) - os.chmod(target_fname, 0o644) - - return 0 - - -def disable_ra(): - """Set the enable_ra option in /etc/ipa/default.conf to False - - Note that api.env will retain the old value (it is readonly). - """ - parser = RawConfigParser() - parser.read(paths.IPA_DEFAULT_CONF) - parser.set('global', 'enable_ra', 'False') - fp = open(paths.IPA_DEFAULT_CONF, 'w') - parser.write(fp) - fp.close() - - -def configure_ldap_conf(fstore, cli_basedn, cli_realm, cli_domain, cli_server, dnsok, options, files): - 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':str(DN(('cn', 'users'), ('cn', 'accounts'), cli_basedn))+'?sub'}, - {'name':'nss_base_group', 'type':'option', 'value':str(DN(('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://'+ipautil.format_netloc(cli_server[0])}) - else: - opts.append({'name':'nss_srv_domain', 'type':'option', 'value':cli_domain}) - - opts.append({'name':'empty', 'type':'empty'}) - - # Depending on the release and distribution this may exist in any - # number of different file names, update what we find - for filename in files: - try: - fstore.backup_file(filename) - ldapconf.newConf(filename, opts) - except Exception as e: - root_logger.error("Creation of %s failed: %s", filename, str(e)) - return (1, 'LDAP', filename) - - if files: - return (0, 'LDAP', ', '.join(files)) - - return 0, None, None - -def configure_nslcd_conf(fstore, cli_basedn, cli_realm, cli_domain, cli_server, dnsok, options, files): - 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':str(DN(('cn', 'users'), ('cn', 'accounts'), cli_basedn))}, - {'name':'base group', 'type':'option', 'value':str(DN(('cn', 'groups'), ('cn', 'accounts'), cli_basedn))}, - {'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://'+ipautil.format_netloc(cli_server[0])}) - else: - opts.append({'name':'uri', 'type':'option', 'value':'DNS'}) - - opts.append({'name':'empty', 'type':'empty'}) - - for filename in files: - try: - fstore.backup_file(filename) - nslcdconf.newConf(filename, opts) - except Exception as e: - root_logger.error("Creation of %s failed: %s", filename, str(e)) - return (1, None, None) - - nslcd = services.knownservices.nslcd - if nslcd.is_installed(): - try: - nslcd.restart() - except Exception as e: - log_service_error(nslcd.service_name, 'restart', e) - - try: - nslcd.enable() - except Exception as e: - root_logger.error( - "Failed to enable automatic startup of the %s daemon: %s", - nslcd.service_name, str(e)) - else: - root_logger.debug("%s daemon is not installed, skip configuration", - nslcd.service_name) - return (0, None, None) - - return (0, 'NSLCD', ', '.join(files)) - -def configure_openldap_conf(fstore, cli_basedn, cli_server): - ldapconf = ipaclient.ipachangeconf.IPAChangeConf("IPA Installer") - ldapconf.setOptionAssignment((" ", "\t")) - - opts = [{'name':'comment', 'type':'comment', - 'value':' File modified by ipa-client-install'}, - {'name':'empty', 'type':'empty'}, - {'name':'comment', 'type':'comment', - 'value':' We do not want to break your existing configuration, ' - 'hence:'}, - # this needs to be kept updated if we change more options - {'name':'comment', 'type':'comment', - 'value':' URI, BASE and TLS_CACERT have been added if they ' - 'were not set.'}, - {'name':'comment', 'type':'comment', - 'value':' In case any of them were set, a comment with ' - 'trailing note'}, - {'name':'comment', 'type':'comment', - 'value':' "# modified by IPA" note has been inserted.'}, - {'name':'comment', 'type':'comment', - 'value':' To use IPA server with openLDAP tools, please comment ' - 'out your'}, - {'name':'comment', 'type':'comment', - 'value':' existing configuration for these options and ' - 'uncomment the'}, - {'name':'comment', 'type':'comment', - 'value':' corresponding lines generated by IPA.'}, - {'name':'empty', 'type':'empty'}, - {'name':'empty', 'type':'empty'}, - {'action':'addifnotset', 'name':'URI', 'type':'option', - 'value':'ldaps://'+ cli_server[0]}, - {'action':'addifnotset', 'name':'BASE', 'type':'option', - 'value':str(cli_basedn)}, - {'action':'addifnotset', 'name':'TLS_CACERT', 'type':'option', - 'value':CACERT},] - - target_fname = paths.OPENLDAP_LDAP_CONF - fstore.backup_file(target_fname) - - error_msg = "Configuring {path} failed with: {err}" - - try: - ldapconf.changeConf(target_fname, opts) - except SyntaxError as e: - root_logger.info("Could not parse {path}".format(path=target_fname)) - root_logger.debug(error_msg.format(path=target_fname, err=str(e))) - return False - except IOError as e : - root_logger.info("{path} does not exist.".format(path=target_fname)) - root_logger.debug(error_msg.format(path=target_fname, err=str(e))) - return False - except Exception as e: # we do not want to fail in an optional step - root_logger.debug(error_msg.format(path=target_fname, err=str(e))) - return False - - os.chmod(target_fname, 0o644) - return True - -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(paths.LDAP_CONF): - return - - ldapconf = ipaclient.ipachangeconf.IPAChangeConf("IPA Installer") - ldapconf.setOptionAssignment(" ") - - opts = [{'name':'uri', 'type':'option', 'action':'set', 'value':'ldap://'+ipautil.format_netloc(cli_server[0])}, - {'name':'empty', 'type':'empty'}] - - # Errors raised by this should be caught by the caller - ldapconf.changeConf(paths.LDAP_CONF, opts) - root_logger.info("Changed configuration of /etc/ldap.conf to use " + - "hardcoded server name: %s", cli_server[0]) - - return - -def configure_krb5_conf(cli_realm, cli_domain, cli_server, cli_kdc, dnsok, - options, filename, client_domain, client_hostname): - - 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'}, - {'name':'includedir', 'type':'option', 'value':paths.COMMON_KRB5_CONF_DIR, 'delim':' '}] - - # SSSD include dir - if options.sssd: - opts.append({'name':'includedir', 'type':'option', 'value':paths.SSSD_PUBCONF_KRB5_INCLUDE_D_DIR, 'delim':' '}) - opts.append({'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':'true'}) - libopts.append({'name':'udp_preference_limit', 'type':'option', 'value':'0'}) - - # Configure KEYRING CCACHE if supported - if kernel_keyring.is_persistent_keyring_supported(): - root_logger.debug("Enabling persistent keyring CCACHE") - libopts.append({'name':'default_ccache_name', 'type':'option', - 'value':'KEYRING:persistent:%{uid}'}) - - 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 - kropts = [] - if not dnsok or not cli_kdc or options.force: - #[realms] - for server in cli_server: - kropts.append({'name':'kdc', 'type':'option', 'value':ipautil.format_netloc(server, 88)}) - kropts.append({'name':'master_kdc', 'type':'option', 'value':ipautil.format_netloc(server, 88)}) - kropts.append({'name':'admin_server', 'type':'option', 'value':ipautil.format_netloc(server, 749)}) - kropts.append({'name': 'kpasswd_server', - 'type': 'option', - 'value': ipautil.format_netloc(server, 464) - }) - kropts.append({'name':'default_domain', 'type':'option', 'value':cli_domain}) - kropts.append({'name':'pkinit_anchors', 'type':'option', 'value':'FILE:%s' % CACERT}) - 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}, - {'name':client_hostname, 'type':'option', 'value':cli_realm}] - - #add client domain mapping if different from server domain - if cli_domain != client_domain: - dropts.append({'name':'.'+client_domain, 'type':'option', 'value':cli_realm}) - dropts.append({'name':client_domain, 'type':'option', 'value':cli_realm}) - - opts.append({'name':'domain_realm', 'type':'section', 'value':dropts}) - opts.append({'name':'empty', 'type':'empty'}) - - root_logger.debug("Writing Kerberos configuration to %s:", filename) - root_logger.debug("%s", krbconf.dump(opts)) - - krbconf.newConf(filename, opts) - os.chmod(filename, 0o644) - - return 0 - -def configure_certmonger(fstore, subject_base, cli_realm, hostname, options, - ca_enabled): - if not options.request_cert: - return - - if not ca_enabled: - root_logger.warning( - "An RA is not configured on the server. " - "Not requesting host certificate.") - return - - principal = 'host/%s@%s' % (hostname, cli_realm) - - if options.hostname: - # 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) - - cmonger = services.knownservices.certmonger - try: - cmonger.enable() - except Exception as e: - root_logger.error( - "Failed to configure automatic startup of the %s daemon: %s", - cmonger.service_name, str(e)) - root_logger.warning( - "Automatic certificate management will not be available") - - # Request our host cert - subject = str(DN(('CN', hostname), subject_base)) - passwd_fname = os.path.join(paths.IPA_NSSDB_DIR, 'pwdfile.txt') - try: - certmonger.request_cert(nssdb=paths.IPA_NSSDB_DIR, - nickname='Local IPA host', - subject=subject, dns=[hostname], - principal=principal, - passwd_fname=passwd_fname) - except Exception as ex: - root_logger.error("%s request for host certificate failed: %s", - cmonger.service_name, ex) - -def configure_sssd_conf(fstore, cli_realm, cli_domain, cli_server, options, client_domain, client_hostname): - try: - sssdconfig = SSSDConfig.SSSDConfig() - sssdconfig.import_config() - except Exception as e: - if os.path.exists(paths.SSSD_CONF) and options.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 - root_logger.error( - "SSSD config exists but cannot be parsed: %s", str(e)) - root_logger.error( - "Was instructed to preserve existing SSSD config") - root_logger.info("Correct errors in /etc/sssd/sssd.conf and " + - "re-run installation") - return 1 - - # 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 - root_logger.error("Unable to parse existing SSSD config. " + - "As option --preserve-sssd was not specified, new config " + - "will override the old one.") - root_logger.info("The old /etc/sssd/sssd.conf is backed up and " + - "will be restored during uninstall.") - root_logger.info("New SSSD config will be created") - sssdconfig = SSSDConfig.SSSDConfig() - sssdconfig.new_config() - - try: - domain = sssdconfig.new_domain(cli_domain) - except SSSDConfig.DomainAlreadyExistsError: - root_logger.info("Domain %s is already configured in existing SSSD " + - "config, creating a new one.", cli_domain) - root_logger.info("The old /etc/sssd/sssd.conf is backed up and will " + - "be restored during uninstall.") - sssdconfig = SSSDConfig.SSSDConfig() - sssdconfig.new_config() - domain = sssdconfig.new_domain(cli_domain) - - ssh_dir = services.knownservices.sshd.get_config_dir() - ssh_config = os.path.join(ssh_dir, 'ssh_config') - sshd_config = os.path.join(ssh_dir, 'sshd_config') - - if (options.conf_ssh and file_exists(ssh_config)) or (options.conf_sshd and file_exists(sshd_config)): - try: - sssdconfig.new_service('ssh') - except SSSDConfig.ServiceAlreadyExists: - pass - except SSSDConfig.ServiceNotRecognizedError: - root_logger.error("Unable to activate the SSH service in SSSD config.") - root_logger.info( - "Please make sure you have SSSD built with SSH support installed.") - root_logger.info( - "Configure SSH support manually in /etc/sssd/sssd.conf.") - - sssdconfig.activate_service('ssh') - - if options.conf_sudo: - # Activate the service in the SSSD config - try: - sssdconfig.new_service('sudo') - except SSSDConfig.ServiceAlreadyExists: - pass - except SSSDConfig.ServiceNotRecognizedError: - root_logger.error("Unable to activate the SUDO service in " - "SSSD config.") - - sssdconfig.activate_service('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 options.on_master and cli_domain != client_domain: - domain.set_option('dns_discovery_domain', cli_domain) - - if not options.on_master: - if options.primary: - domain.set_option('ipa_server', ', '.join(cli_server)) - else: - domain.set_option('ipa_server', '_srv_, %s' % ', '.join(cli_server)) - else: - domain.set_option('ipa_server_mode', 'True') - # the master should only use itself for Kerberos - domain.set_option('ipa_server', cli_server[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 options.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', CACERT) - - if options.dns_updates: - domain.set_option('dyndns_update', True) - if options.all_ip_addresses: - domain.set_option('dyndns_iface', '*') - else: - iface = get_server_connection_interface(cli_server[0]) - domain.set_option('dyndns_iface', iface) - if options.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) - - return 0 - -def change_ssh_config(filename, changes, sections): - if not changes: - return True - - try: - f = open(filename, 'r') - except IOError as e: - root_logger.error("Failed to open '%s': %s", filename, str(e)) - return False - - change_keys = tuple(key.lower() for key in changes) - section_keys = tuple(key.lower() for key in sections) - - lines = [] - in_section = False - for line in f: - line = line.rstrip('\n') - pline = line.strip() - if not pline or pline.startswith('#'): - lines.append(line) - continue - option = pline.split()[0].lower() - if option in section_keys: - in_section = True - break - if option in change_keys: - line = '#' + line - lines.append(line) - for option, value in changes.items(): - if value is not None: - lines.append('%s %s' % (option, value)) - if in_section: - lines.append('') - lines.append(line) - for line in f: - line = line.rstrip('\n') - lines.append(line) - lines.append('') - - f.close() - - try: - f = open(filename, 'w') - except IOError as e: - root_logger.error("Failed to open '%s': %s", filename, str(e)) - return False - - f.write('\n'.join(lines)) - - f.close() - - return True - -def configure_ssh_config(fstore, options): - ssh_dir = services.knownservices.sshd.get_config_dir() - ssh_config = os.path.join(ssh_dir, 'ssh_config') - - if not file_exists(ssh_config): - root_logger.info("%s not found, skipping configuration" % ssh_config) - return - - fstore.backup_file(ssh_config) - - changes = { - 'PubkeyAuthentication': 'yes', - } - - if options.sssd and file_exists(paths.SSS_SSH_KNOWNHOSTSPROXY): - changes['ProxyCommand'] = '%s -p %%p %%h' % paths.SSS_SSH_KNOWNHOSTSPROXY - changes['GlobalKnownHostsFile'] = paths.SSSD_PUBCONF_KNOWN_HOSTS - if options.trust_sshfp: - changes['VerifyHostKeyDNS'] = 'yes' - changes['HostKeyAlgorithms'] = 'ssh-rsa,ssh-dss' - - change_ssh_config(ssh_config, changes, ['Host', 'Match']) - root_logger.info('Configured %s', ssh_config) - -def configure_sshd_config(fstore, options): - sshd = services.knownservices.sshd - ssh_dir = sshd.get_config_dir() - sshd_config = os.path.join(ssh_dir, 'sshd_config') - - if not file_exists(sshd_config): - root_logger.info("%s not found, skipping configuration" % sshd_config) - return - - fstore.backup_file(sshd_config) - - changes = { - 'PubkeyAuthentication': 'yes', - 'KerberosAuthentication': 'no', - 'GSSAPIAuthentication': 'yes', - 'UsePAM': 'yes', - 'ChallengeResponseAuthentication': 'yes', - } - - if options.sssd and file_exists(paths.SSS_SSH_AUTHORIZEDKEYS): - authorized_keys_changes = None - - candidates = ( - { - 'AuthorizedKeysCommand': paths.SSS_SSH_AUTHORIZEDKEYS, - 'AuthorizedKeysCommandUser': 'nobody', - }, - { - 'AuthorizedKeysCommand': paths.SSS_SSH_AUTHORIZEDKEYS, - 'AuthorizedKeysCommandRunAs': 'nobody', - }, - { - 'PubKeyAgent': '%s %%u' % paths.SSS_SSH_AUTHORIZEDKEYS, - 'PubKeyAgentRunAs': 'nobody', - }, - ) - - for candidate in candidates: - args = ['sshd', '-t', '-f', paths.DEV_NULL] - for item in candidate.items(): - args.append('-o') - args.append('%s=%s' % item) - - result = ipautil.run(args, raiseonerr=False) - if result.returncode == 0: - authorized_keys_changes = candidate - break - - if authorized_keys_changes is not None: - changes.update(authorized_keys_changes) - else: - root_logger.warning("Installed OpenSSH server does not " - "support dynamically loading authorized user keys. " - "Public key authentication of IPA users will not be " - "available.") - - change_ssh_config(sshd_config, changes, ['Match']) - root_logger.info('Configured %s', sshd_config) - - if sshd.is_running(): - try: - sshd.restart() - except Exception as e: - log_service_error(sshd.service_name, 'restart', e) - - -def configure_automount(options): - root_logger.info('\nConfiguring automount:') - - args = [ - 'ipa-client-automount', '--debug', '-U', - '--location', options.location - ] - - if options.server: - args.extend(['--server', options.server[0]]) - if not options.sssd: - args.append('--no-sssd') - - try: - result = run(args) - except Exception as e: - root_logger.error('Automount configuration failed: %s', str(e)) - else: - root_logger.info(result.output_log) - - -def configure_nisdomain(options, domain): - domain = options.nisdomain or domain - root_logger.info('Configuring %s as NIS domain.' % domain) - - nis_domain_name = '' - - # First backup the old NIS domain name - if os.path.exists(paths.BIN_NISDOMAINNAME): - try: - result = ipautil.run([paths.BIN_NISDOMAINNAME], - capture_output=True) - except CalledProcessError: - pass - else: - nis_domain_name = result.output - - statestore.backup_state('network', 'nisdomain', nis_domain_name) - - # Backup the state of the domainname service - statestore.backup_state("domainname", "enabled", - services.knownservices.domainname.is_enabled()) - - # Set the new NIS domain name - tasks.set_nisdomain(domain) - - # Enable and start the domainname service - services.knownservices.domainname.enable() - # Restart rather than start so that new NIS domain name is loaded - # if the service is already running - services.knownservices.domainname.restart() - - -def unconfigure_nisdomain(): - # Set the nisdomain permanent and current nisdomain configuration as it was - if statestore.has_state('network'): - old_nisdomain = statestore.restore_state('network','nisdomain') or '' - - if old_nisdomain: - root_logger.info('Restoring %s as NIS domain.' % old_nisdomain) - else: - root_logger.info('Unconfiguring the NIS domain.') - - tasks.set_nisdomain(old_nisdomain) - - # Restore the configuration of the domainname service - enabled = statestore.restore_state('domainname', 'enabled') - if not enabled: - services.knownservices.domainname.disable() - - -def get_iface_from_ip(ip_addr): - for interface in netifaces.interfaces(): - if_addrs = netifaces.ifaddresses(interface) - for family in [netifaces.AF_INET, netifaces.AF_INET6]: - for ip in if_addrs.get(family, []): - if ip['addr'] == ip_addr: - return interface - else: - raise RuntimeError("IP %s not assigned to any interface." % ip_addr) - - -def get_local_ipaddresses(iface=None): - if iface: - interfaces = [iface] - else: - interfaces = netifaces.interfaces() - - ips = [] - for interface in interfaces: - if_addrs = netifaces.ifaddresses(interface) - for family in [netifaces.AF_INET, netifaces.AF_INET6]: - for ip in if_addrs.get(family, []): - try: - ips.append(ipautil.CheckedIPAddress(ip['addr'])) - root_logger.debug('IP check successful: %s' % ip['addr']) - except ValueError as e: - root_logger.debug('IP check failed: %s' % e) - return ips - - -def do_nsupdate(update_txt): - root_logger.debug("Writing nsupdate commands to %s:", UPDATE_FILE) - root_logger.debug("%s", update_txt) - - update_fd = open(UPDATE_FILE, "w") - update_fd.write(update_txt) - update_fd.flush() - update_fd.close() - - result = False - try: - ipautil.run([paths.NSUPDATE, '-g', UPDATE_FILE]) - result = True - except CalledProcessError as e: - root_logger.debug('nsupdate failed: %s', str(e)) - - try: - os.remove(UPDATE_FILE) - except Exception: - pass - - return result - -DELETE_TEMPLATE_A = """ -update delete $HOSTNAME. IN A -show -send -""" - -DELETE_TEMPLATE_AAAA = """ -update delete $HOSTNAME. IN AAAA -show -send -""" -ADD_TEMPLATE_A = """ -update add $HOSTNAME. $TTL IN A $IPADDRESS -show -send -""" - -ADD_TEMPLATE_AAAA = """ -update add $HOSTNAME. $TTL IN AAAA $IPADDRESS -show -send -""" - -UPDATE_FILE = paths.IPA_DNS_UPDATE_TXT -CCACHE_FILE = paths.IPA_DNS_CCACHE - -def update_dns(server, hostname, options): - - try: - ips = get_local_ipaddresses() - except CalledProcessError as e: - root_logger.error("Cannot update DNS records. %s" % e) - root_logger.debug("Unable to get local IP addresses.") - - if options.all_ip_addresses: - update_ips = ips - elif options.ip_addresses: - update_ips = [] - for ip in options.ip_addresses: - update_ips.append(ipautil.CheckedIPAddress(ip)) - else: - try: - iface = get_server_connection_interface(server) - except RuntimeError as e: - root_logger.error("Cannot update DNS records. %s" % e) - return - try: - update_ips = get_local_ipaddresses(iface) - except CalledProcessError as e: - root_logger.error("Cannot update DNS records. %s" % e) - return - - if not update_ips: - root_logger.info("Failed to determine this machine's ip address(es).") - return - - network_ip_address_warning(update_ips) - broadcast_ip_address_warning(update_ips) - - update_txt = "debug\n" - update_txt += ipautil.template_str(DELETE_TEMPLATE_A, - dict(HOSTNAME=hostname)) - update_txt += ipautil.template_str(DELETE_TEMPLATE_AAAA, - dict(HOSTNAME=hostname)) - - for ip in update_ips: - sub_dict = dict(HOSTNAME=hostname, IPADDRESS=ip, TTL=1200) - if ip.version == 4: - template = ADD_TEMPLATE_A - elif ip.version == 6: - template = ADD_TEMPLATE_AAAA - update_txt += ipautil.template_str(template, sub_dict) - - if not do_nsupdate(update_txt): - root_logger.error("Failed to update DNS records.") - verify_dns_update(hostname, update_ips) - - -def verify_dns_update(fqdn, ips): - """ - Verify that the fqdn resolves to all IP addresses and - that there's matching PTR record for every IP address. - """ - # verify A/AAAA records - missing_ips = [str(ip) for ip in ips] - extra_ips = [] - for record_type in [dns.rdatatype.A, dns.rdatatype.AAAA]: - root_logger.debug('DNS resolver: Query: %s IN %s' % - (fqdn, dns.rdatatype.to_text(record_type))) - try: - answers = dns.resolver.query(fqdn, record_type) - except (dns.resolver.NoAnswer, dns.resolver.NXDOMAIN): - root_logger.debug('DNS resolver: No record.') - except dns.resolver.NoNameservers: - root_logger.debug('DNS resolver: No nameservers answered the' - 'query.') - except dns.exception.DNSException: - root_logger.debug('DNS resolver error.') - else: - for rdata in answers: - try: - missing_ips.remove(rdata.address) - except ValueError: - extra_ips.append(rdata.address) - - # verify PTR records - fqdn_name = dns.name.from_text(fqdn) - wrong_reverse = {} - missing_reverse = [str(ip) for ip in ips] - for ip in ips: - ip_str = str(ip) - addr = dns.reversename.from_address(ip_str) - root_logger.debug('DNS resolver: Query: %s IN PTR' % addr) - try: - answers = dns.resolver.query(addr, dns.rdatatype.PTR) - except (dns.resolver.NoAnswer, dns.resolver.NXDOMAIN): - root_logger.debug('DNS resolver: No record.') - except dns.resolver.NoNameservers: - root_logger.debug('DNS resolver: No nameservers answered the' - 'query.') - except dns.exception.DNSException: - root_logger.debug('DNS resolver error.') - else: - missing_reverse.remove(ip_str) - for rdata in answers: - if not rdata.target == fqdn_name: - wrong_reverse.setdefault(ip_str, []).append(rdata.target) - - if missing_ips: - root_logger.warning('Missing A/AAAA record(s) for host %s: %s.' % - (fqdn, ', '.join(missing_ips))) - if extra_ips: - root_logger.warning('Extra A/AAAA record(s) for host %s: %s.' % - (fqdn, ', '.join(extra_ips))) - if missing_reverse: - root_logger.warning('Missing reverse record(s) for address(es): %s.' % - ', '.join(missing_reverse)) - if wrong_reverse: - root_logger.warning('Incorrect reverse record(s):') - for ip in wrong_reverse: - for target in wrong_reverse[ip]: - root_logger.warning('%s is pointing to %s instead of %s' % - (ip, target, fqdn_name)) - -def get_server_connection_interface(server): - # connect to IPA server, get all ip addresses of inteface used to connect - for res in socket.getaddrinfo(server, 389, socket.AF_UNSPEC, socket.SOCK_STREAM): - af, socktype, proto, _canonname, sa = res - try: - s = socket.socket(af, socktype, proto) - except socket.error as e: - last_error = e - s = None - continue - try: - s.connect(sa) - sockname = s.getsockname() - ip = sockname[0] - except socket.error as e: - last_error = e - continue - finally: - if s: - s.close() - try: - return get_iface_from_ip(ip) - except (CalledProcessError, RuntimeError) as e: - last_error = e - else: - msg = "Cannot get server connection interface" - if last_error: - msg += ": %s" % (last_error) - raise RuntimeError(msg) - - -def client_dns(server, hostname, options): - - try: - verify_host_resolvable(hostname) - dns_ok = True - except errors.DNSNotARecordError: - root_logger.warning("Hostname (%s) does not have A/AAAA record.", - hostname) - dns_ok = False - except errors.DNSResolverError as ex: - root_logger.warning("DNS resolution for hostname %s failed: %s", - hostname, ex) - dns_ok = False - - if (options.dns_updates or options.all_ip_addresses or options.ip_addresses - or not dns_ok): - update_dns(server, hostname, options) - - -def check_ip_addresses(options): - if options.ip_addresses: - for ip in options.ip_addresses: - try: - ipautil.CheckedIPAddress(ip, match_local=True) - except ValueError as e: - root_logger.error(e) - return False - return True - - -def update_ssh_keys(hostname, ssh_dir, create_sshfp): - if not os.path.isdir(ssh_dir): - return - - pubkeys = [] - for basename in os.listdir(ssh_dir): - if not basename.endswith('.pub'): - continue - filename = os.path.join(ssh_dir, basename) - - try: - f = open(filename, 'r') - except IOError as e: - root_logger.warning("Failed to open '%s': %s", filename, str(e)) - continue - - for line in f: - line = line[:-1].lstrip() - if not line or line.startswith('#'): - continue - try: - pubkey = SSHPublicKey(line) - except (ValueError, UnicodeDecodeError): - continue - root_logger.info("Adding SSH public key from %s", filename) - pubkeys.append(pubkey) - - f.close() - - try: - # Use the RPC directly so older servers are supported - api.Backend.rpcclient.forward( - 'host_mod', - ipautil.fsdecode(hostname), - ipasshpubkey=[pk.openssh() for pk in pubkeys], - updatedns=False, - version=u'2.26', # this version adds support for SSH public keys - ) - except errors.EmptyModlist: - pass - except Exception as e: - root_logger.info("host_mod: %s", str(e)) - root_logger.warning("Failed to upload host SSH public keys.") - return - - if create_sshfp: - ttl = 1200 - - update_txt = 'debug\n' - update_txt += 'update delete %s. IN SSHFP\nshow\nsend\n' % hostname - for pubkey in pubkeys: - sshfp = pubkey.fingerprint_dns_sha1() - if sshfp is not None: - update_txt += 'update add %s. %s IN SSHFP %s\n' % (hostname, ttl, sshfp) - sshfp = pubkey.fingerprint_dns_sha256() - if sshfp is not None: - update_txt += 'update add %s. %s IN SSHFP %s\n' % (hostname, ttl, sshfp) - update_txt += 'show\nsend\n' - - if not do_nsupdate(update_txt): - root_logger.warning("Could not update DNS SSHFP records.") - -def print_port_conf_info(): - root_logger.info( - "Please make sure the following ports are opened " - "in the firewall settings:\n" - " TCP: 80, 88, 389\n" - " UDP: 88 (at least one of TCP/UDP ports 88 has to be open)\n" - "Also note that following ports are necessary for ipa-client " - "working properly after enrollment:\n" - " TCP: 464\n" - " UDP: 464, 123 (if NTP enabled)") - -def get_certs_from_ldap(server, base_dn, realm, ca_enabled): - ldap_uri = ipaldap.get_ldap_uri(server) - conn = ipaldap.LDAPClient(ldap_uri, sasl_nocanon=True) - try: - conn.gssapi_bind() - certs = certstore.get_ca_certs(conn, base_dn, realm, ca_enabled) - except errors.NotFound: - raise errors.NoCertificateError(entry=server) - except errors.NetworkError as e: - raise errors.NetworkError(uri=conn.ldap_uri, error=str(e)) - except Exception as e: - raise errors.LDAPError(str(e)) - finally: - conn.unbind() - - return certs - -def get_ca_certs_from_file(url): - ''' - Get the CA cert from a user supplied file and write it into the - CACERT file. - - Raises errors.NoCertificateError if unable to read cert. - Raises errors.FileError if unable to write cert. - ''' - - try: - parsed = urlparse(url, 'file') - except Exception: - raise errors.FileError(reason="unable to parse file url '%s'" % url) - - if parsed.scheme != 'file': - raise errors.FileError(reason="url is not a file scheme '%s'" % url) - - filename = parsed.path - - if not os.path.exists(filename): - raise errors.FileError(reason="file '%s' does not exist" % filename) - - if not os.path.isfile(filename): - raise errors.FileError(reason="file '%s' is not a file" % filename) - - root_logger.debug("trying to retrieve CA cert from file %s", filename) - try: - certs = x509.load_certificate_list_from_file(filename) - except Exception: - raise errors.NoCertificateError(entry=filename) - - return certs - -def get_ca_certs_from_http(url, warn=True): - ''' - Use HTTP to retrieve the CA cert and write it into the CACERT file. - This is insecure and should be avoided. - - Raises errors.NoCertificateError if unable to retrieve and write cert. - ''' - - if warn: - root_logger.warning("Downloading the CA certificate via HTTP, " + - "this is INSECURE") - - root_logger.debug("trying to retrieve CA cert via HTTP from %s", url) - try: - - result = run([paths.BIN_CURL, "-o", "-", url], capture_output=True) - except CalledProcessError: - raise errors.NoCertificateError(entry=url) - stdout = result.output - - try: - certs = x509.load_certificate_list(stdout) - except Exception: - raise errors.NoCertificateError(entry=url) - - return certs - -def get_ca_certs_from_ldap(server, basedn, realm): - ''' - Retrieve th CA cert from the LDAP server by binding to the - server with GSSAPI using the current Kerberos credentials. - Write the retrieved cert into the CACERT file. - - Raises errors.NoCertificateError if cert is not found. - Raises errors.NetworkError if LDAP connection can't be established. - Raises errors.LDAPError for any other generic LDAP error. - Raises errors.OnlyOneValueAllowed if more than one cert is found. - Raises errors.FileError if unable to write cert. - ''' - - root_logger.debug("trying to retrieve CA cert via LDAP from %s", server) - - try: - certs = get_certs_from_ldap(server, basedn, realm, False) - except Exception as e: - root_logger.debug("get_ca_certs_from_ldap() error: %s", e) - raise - - certs = [x509.load_certificate(c[0], x509.DER) for c in certs - if c[2] is not False] - - return certs - -def validate_new_ca_certs(existing_ca_certs, new_ca_certs, ask, - override=False): - if existing_ca_certs is None: - root_logger.info( - cert_summary("Successfully retrieved CA cert", new_ca_certs)) - return - - existing_ca_certs = set(existing_ca_certs) - new_ca_certs = set(new_ca_certs) - if existing_ca_certs > new_ca_certs: - root_logger.warning( - "The CA cert available from the IPA server does not match the\n" - "local certificate available at %s" % CACERT) - root_logger.warning( - cert_summary("Existing CA cert:", existing_ca_certs)) - root_logger.warning( - cert_summary("Retrieved CA cert:", new_ca_certs)) - if override: - root_logger.warning("Overriding existing CA cert\n") - elif not ask or not user_input( - "Do you want to replace the local certificate with the CA\n" - "certificate retrieved from the IPA server?", True): - raise errors.CertificateInvalidError(name='Retrieved CA') - else: - root_logger.debug( - "Existing CA cert and Retrieved CA cert are identical") - -def get_ca_certs(fstore, options, server, basedn, realm): - ''' - Examine the different options and determine a method for obtaining - the CA cert. - - If successful the CA cert will have been written into CACERT. - - Raises errors.NoCertificateError if not successful. - - The logic for determining how to load the CA cert is as follow: - - In the OTP case (not -p and -w): - - 1. load from user supplied cert file - 2. else load from HTTP - - In the 'user_auth' case ((-p and -w) or interactive): - - 1. load from user supplied cert file - 2. load from LDAP using SASL/GSS/Krb5 auth - (provides mutual authentication, integrity and security) - 3. if LDAP failed and interactive ask for permission to - use insecure HTTP (default: No) - - In the unattended case: - - 1. load from user supplied cert file - 2. load from HTTP if --force specified else fail - - In all cases if HTTP is used emit warning message - ''' - - ca_file = CACERT + ".new" - - def ldap_url(): - return urlunparse(('ldap', ipautil.format_netloc(server), - '', '', '', '')) - - def file_url(): - return urlunparse(('file', '', options.ca_cert_file, - '', '', '')) - - def http_url(): - return urlunparse(('http', ipautil.format_netloc(server), - '/ipa/config/ca.crt', '', '', '')) - - - interactive = not options.unattended - otp_auth = options.principal is None and options.password is not None - existing_ca_certs = None - ca_certs = None - - if options.ca_cert_file: - url = file_url() - try: - ca_certs = get_ca_certs_from_file(url) - except errors.FileError as e: - root_logger.debug(e) - raise - except Exception as e: - root_logger.debug(e) - raise errors.NoCertificateError(entry=url) - root_logger.debug("CA cert provided by user, use it!") - else: - if os.path.exists(CACERT): - if os.path.isfile(CACERT): - try: - existing_ca_certs = x509.load_certificate_list_from_file( - CACERT) - except Exception as e: - raise errors.FileError(reason=u"Unable to load existing" + - " CA cert '%s': %s" % (CACERT, e)) - else: - raise errors.FileError(reason=u"Existing ca cert '%s' is " + - "not a plain file" % (CACERT)) - - if otp_auth: - if existing_ca_certs: - root_logger.info("OTP case, CA cert preexisted, use it") - else: - url = http_url() - override = not interactive - if interactive and not user_input( - "Do you want to download the CA cert from " + url + " ?\n" - "(this is INSECURE)", False): - raise errors.NoCertificateError(message=u"HTTP certificate" - " download declined by user") - try: - ca_certs = get_ca_certs_from_http(url, override) - except Exception as e: - root_logger.debug(e) - raise errors.NoCertificateError(entry=url) - - validate_new_ca_certs(existing_ca_certs, ca_certs, False, - override) - else: - # Auth with user credentials - try: - url = ldap_url() - ca_certs = get_ca_certs_from_ldap(server, basedn, realm) - validate_new_ca_certs(existing_ca_certs, ca_certs, interactive) - except errors.FileError as e: - root_logger.debug(e) - raise - except (errors.NoCertificateError, errors.LDAPError) as e: - root_logger.debug(str(e)) - url = http_url() - if existing_ca_certs: - root_logger.warning( - "Unable to download CA cert from LDAP\n" - "but found preexisting cert, using it.\n") - elif interactive and not user_input( - "Unable to download CA cert from LDAP.\n" - "Do you want to download the CA cert from " + url + "?\n" - "(this is INSECURE)", False): - raise errors.NoCertificateError(message=u"HTTP " - "certificate download declined by user") - elif not interactive and not options.force: - root_logger.error( - "In unattended mode without a One Time Password " - "(OTP) or without --ca-cert-file\nYou must specify" - " --force to retrieve the CA cert using HTTP") - raise errors.NoCertificateError(message=u"HTTP " - "certificate download requires --force") - else: - try: - ca_certs = get_ca_certs_from_http(url) - except Exception as e: - root_logger.debug(e) - raise errors.NoCertificateError(entry=url) - validate_new_ca_certs(existing_ca_certs, ca_certs, - interactive) - except Exception as e: - root_logger.debug(str(e)) - raise errors.NoCertificateError(entry=url) - - if ca_certs is None and existing_ca_certs is None: - raise errors.InternalError(u"expected CA cert file '%s' to " - u"exist, but it's absent" % (ca_file)) - - if ca_certs is not None: - try: - ca_certs = [ - cert.public_bytes(serialization.Encoding.DER) - for cert in ca_certs - ] - x509.write_certificate_list(ca_certs, ca_file) - except Exception as e: - if os.path.exists(ca_file): - try: - os.unlink(ca_file) - except OSError as e: - root_logger.error( - "Failed to remove '%s': %s", ca_file, e) - raise errors.FileError(reason = - u"cannot write certificate file '%s': %s" % (ca_file, e)) - - os.rename(ca_file, CACERT) - - # Make sure the file permissions are correct - try: - os.chmod(CACERT, 0o644) - except Exception as e: - raise errors.FileError(reason=u"Unable set permissions on ca " - u"cert '%s': %s" % (CACERT, e)) - -#IMPORTANT First line of FF config file is ignored -FIREFOX_CONFIG_TEMPLATE = """ - -/* Kerberos SSO configuration */ -pref("network.negotiate-auth.trusted-uris", ".$DOMAIN"); - -/* These are the defaults */ -pref("network.negotiate-auth.gsslib", ""); -pref("network.negotiate-auth.using-native-gsslib", true); -pref("network.negotiate-auth.allow-proxies", true); -""" - -FIREFOX_PREFERENCES_FILENAME = "all-ipa.js" -FIREFOX_PREFERENCES_REL_PATH = "browser/defaults/preferences" - -def configure_firefox(options, statestore, domain): - try: - root_logger.debug("Setting up Firefox configuration.") - - preferences_dir = None - - # Check user specified location of firefox install directory - if options.firefox_dir is not None: - pref_path = os.path.join(options.firefox_dir, - FIREFOX_PREFERENCES_REL_PATH) - if dir_exists(pref_path): - preferences_dir = pref_path - else: - root_logger.error("Directory '%s' does not exists." % pref_path) - else: - # test if firefox is installed - if file_exists(paths.FIREFOX): - - # find valid preferences path - for path in [paths.LIB_FIREFOX, paths.LIB64_FIREFOX]: - pref_path = os.path.join(path, - FIREFOX_PREFERENCES_REL_PATH) - if dir_exists(pref_path): - preferences_dir = pref_path - break - else: - root_logger.error("Firefox configuration skipped (Firefox not found).") - return - - # setting up firefox - if preferences_dir is not None: - - # user could specify relative path, we need to store absolute - preferences_dir = os.path.abspath(preferences_dir) - root_logger.debug("Firefox preferences directory found '%s'." % preferences_dir) - preferences_fname = os.path.join(preferences_dir, FIREFOX_PREFERENCES_FILENAME) - update_txt = ipautil.template_str(FIREFOX_CONFIG_TEMPLATE, dict(DOMAIN=domain)) - root_logger.debug("Firefox trusted uris will be set as '.%s' domain." % domain) - root_logger.debug("Firefox configuration will be stored in '%s' file." % preferences_fname) - - try: - with open(preferences_fname, 'w') as f: - f.write(update_txt) - root_logger.info("Firefox sucessfully configured.") - statestore.backup_state('firefox', 'preferences_fname', preferences_fname) - except Exception as e: - root_logger.debug("An error occured during creating preferences file: %s." % str(e)) - root_logger.error("Firefox configuration failed.") - else: - root_logger.debug("Firefox preferences directory not found.") - root_logger.error("Firefox configuration failed.") - - except Exception as e: - root_logger.debug(str(e)) - root_logger.error("Firefox configuration failed.") - - -def install(options, env, fstore, statestore): - dnsok = False - - cli_domain = None - cli_server = None - subject_base = None - - cli_domain_source = 'Unknown source' - cli_server_source = 'Unknown source' - - if options.conf_ntp and not options.on_master and not options.force_ntpd: - try: - ipaclient.ntpconf.check_timedate_services() - except ipaclient.ntpconf.NTPConflictingService as e: - print("WARNING: ntpd time&date synchronization service will not" \ - " be configured as") - print("conflicting service (%s) is enabled" % e.conflicting_service) - print("Use --force-ntpd option to disable it and force configuration" \ - " of ntpd") - print("") - - # configuration of ntpd is disabled in this case - options.conf_ntp = False - except ipaclient.ntpconf.NTPConfigurationError: - pass - - if options.unattended and (options.password is None and - options.principal is None and - options.keytab is None and - options.prompt_password is False and - not options.on_master): - root_logger.error("One of password / principal / keytab is required.") - return CLIENT_INSTALL_ERROR - - if options.hostname: - hostname = options.hostname - hostname_source = 'Provided as option' - else: - hostname = socket.getfqdn() - hostname_source = "Machine's FQDN" - if hostname != hostname.lower(): - root_logger.error( - "Invalid hostname '%s', must be lower-case.", hostname) - return CLIENT_INSTALL_ERROR - if (hostname == 'localhost') or (hostname == 'localhost.localdomain'): - root_logger.error("Invalid hostname, '%s' must not be used.", hostname) - return CLIENT_INSTALL_ERROR - - # when installing with '--no-sssd' option, check whether nss-ldap is installed - if not options.sssd: - if not os.path.exists(paths.PAM_KRB5_SO): - root_logger.error("The pam_krb5 package must be installed") - return CLIENT_INSTALL_ERROR - - (nssldap_installed, nosssd_files) = nssldap_exists() - if not nssldap_installed: - root_logger.error("One of these packages must be installed: " + - "nss_ldap or nss-pam-ldapd") - return CLIENT_INSTALL_ERROR - - if options.keytab and options.principal: - root_logger.error("Options 'principal' and 'keytab' cannot be used " - "together.") - return CLIENT_INSTALL_ERROR - - if options.keytab and options.force_join: - root_logger.warning("Option 'force-join' has no additional effect " - "when used with together with option 'keytab'.") - - # Check if old certificate exist and show warning - if not options.ca_cert_file and get_cert_path(options.ca_cert_file) == CACERT: - root_logger.warning("Using existing certificate '%s'.", CACERT) - - if not check_ip_addresses(options): - return CLIENT_INSTALL_ERROR - - # Create the discovery instance - ds = ipadiscovery.IPADiscovery() - - ret = ds.search(domain=options.domain, servers=options.server, realm=options.realm_name, hostname=hostname, ca_cert_path=get_cert_path(options.ca_cert_file)) - - if options.server 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 - root_logger.error("Failed to verify that %s is an IPA Server.", - ', '.join(options.server)) - root_logger.error("This may mean that the remote server is not up " - "or is not reachable due to network or firewall settings.") - print_port_conf_info() - return CLIENT_INSTALL_ERROR - - if ret == ipadiscovery.BAD_HOST_CONFIG: - root_logger.error("Can't get the fully qualified name of this host") - root_logger.info("Check that the client is properly configured") - return CLIENT_INSTALL_ERROR - if ret == ipadiscovery.NOT_FQDN: - root_logger.error("%s is not a fully-qualified hostname", hostname) - return CLIENT_INSTALL_ERROR - if ret in (ipadiscovery.NO_LDAP_SERVER, ipadiscovery.NOT_IPA_SERVER) \ - or not ds.domain: - if ret == ipadiscovery.NO_LDAP_SERVER: - if ds.server: - root_logger.debug("%s is not an LDAP server" % ds.server) - else: - root_logger.debug("No LDAP server found") - elif ret == ipadiscovery.NOT_IPA_SERVER: - if ds.server: - root_logger.debug("%s is not an IPA server" % ds.server) - else: - root_logger.debug("No IPA server found") - else: - root_logger.debug("Domain not found") - if options.domain: - cli_domain = options.domain - cli_domain_source = 'Provided as option' - elif options.unattended: - root_logger.error( - "Unable to discover domain, not provided on command line") - return CLIENT_INSTALL_ERROR - else: - root_logger.info( - "DNS discovery failed to determine your DNS domain") - cli_domain = user_input("Provide the domain name of your IPA server (ex: example.com)", allow_empty = False) - cli_domain_source = 'Provided interactively' - root_logger.debug( - "will use interactively provided domain: %s", cli_domain) - ret = ds.search(domain=cli_domain, servers=options.server, hostname=hostname, ca_cert_path=get_cert_path(options.ca_cert_file)) - - if not cli_domain: - if ds.domain: - cli_domain = ds.domain - cli_domain_source = ds.domain_source - root_logger.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: - root_logger.debug("IPA Server not found") - if options.server: - cli_server = options.server - cli_server_source = 'Provided as option' - elif options.unattended: - root_logger.error("Unable to find IPA Server to join") - return CLIENT_INSTALL_ERROR - else: - root_logger.debug("DNS discovery failed to find the IPA Server") - cli_server = [user_input("Provide your IPA server name (ex: ipa.example.com)", allow_empty = False)] - cli_server_source = 'Provided interactively' - root_logger.debug("will use interactively provided server: %s", cli_server[0]) - ret = ds.search(domain=cli_domain, servers=cli_server, hostname=hostname, ca_cert_path=get_cert_path(options.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 options.server: - (server, domain) = ds.check_domain(ds.domain, set(), "Validating DNS Discovery") - if server and domain: - root_logger.debug("DNS validated, enabling discovery") - dnsok = True - else: - root_logger.debug("DNS discovery failed, disabling discovery") - else: - root_logger.debug("Using servers from command line, disabling DNS discovery") - - if not cli_server: - if options.server: - cli_server = ds.servers - cli_server_source = 'Provided as option' - root_logger.debug("will use provided server: %s", ', '.join(options.server)) - elif ds.server: - cli_server = ds.servers - cli_server_source = ds.server_source - root_logger.debug("will use discovered server: %s", cli_server[0]) - - if ret == ipadiscovery.NOT_IPA_SERVER: - root_logger.error("%s is not an IPA v2 Server.", cli_server[0]) - print_port_conf_info() - root_logger.debug("(%s: %s)", cli_server[0], cli_server_source) - return CLIENT_INSTALL_ERROR - - if ret == ipadiscovery.NO_ACCESS_TO_LDAP: - root_logger.warning("Anonymous access to the LDAP server is disabled.") - root_logger.info("Proceeding without strict verification.") - root_logger.info("Note: This is not an error if anonymous access " + - "has been explicitly restricted.") - ret = 0 - - if ret == ipadiscovery.NO_TLS_LDAP: - root_logger.warning("The LDAP server requires TLS is but we do not " + - "have the CA.") - root_logger.info("Proceeding without strict verification.") - ret = 0 - - if ret != 0: - root_logger.error("Failed to verify that %s is an IPA Server.", - cli_server[0]) - root_logger.error("This may mean that the remote server is not up " - "or is not reachable due to network or firewall settings.") - print_port_conf_info() - root_logger.debug("(%s: %s)", cli_server[0], cli_server_source) - return CLIENT_INSTALL_ERROR - - cli_kdc = ds.kdc - if dnsok and not cli_kdc: - root_logger.error("DNS domain '%s' is not configured for automatic " + - "KDC address lookup.", ds.realm.lower()) - root_logger.debug("(%s: %s)", ds.realm, ds.realm_source) - root_logger.error("KDC address will be set to fixed value.") - - if dnsok: - root_logger.info("Discovery was successful!") - elif not options.unattended: - if not options.server: - root_logger.warning("The failure to use DNS to find your IPA" + - " server indicates that your resolv.conf file is not properly" + - " configured.") - root_logger.info("Autodiscovery of servers for failover cannot work " + - "with this configuration.") - root_logger.info("If you proceed with the installation, services " + - "will be configured to always access the discovered server for " + - "all operations and will not fail over to other servers in case " + - "of failure.") - if not user_input("Proceed with fixed values and no DNS discovery?", False): - return CLIENT_INSTALL_ERROR - - cli_realm = ds.realm - cli_realm_source = ds.realm_source - root_logger.debug("will use discovered realm: %s", cli_realm) - - if options.realm_name and options.realm_name != cli_realm: - root_logger.error( - "The provided realm name [%s] does not match discovered one [%s]", - options.realm_name, cli_realm) - root_logger.debug("(%s: %s)", cli_realm, cli_realm_source) - return CLIENT_INSTALL_ERROR - - cli_basedn = ds.basedn - cli_basedn_source = ds.basedn_source - root_logger.debug("will use discovered basedn: %s", cli_basedn) - subject_base = DN(('O', cli_realm)) - - root_logger.info("Client hostname: %s", hostname) - root_logger.debug("Hostname source: %s", hostname_source) - root_logger.info("Realm: %s", cli_realm) - root_logger.debug("Realm source: %s", cli_realm_source) - root_logger.info("DNS Domain: %s", cli_domain) - root_logger.debug("DNS Domain source: %s", cli_domain_source) - root_logger.info("IPA Server: %s", ', '.join(cli_server)) - root_logger.debug("IPA Server source: %s", cli_server_source) - root_logger.info("BaseDN: %s", cli_basedn) - root_logger.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: - print() - root_logger.warning("It seems that you are using an IP address " - "instead of FQDN as an argument to --server. The " - "installation may fail.") - break - - print() - if not options.unattended and not user_input("Continue to configure the system with these values?", False): - return CLIENT_INSTALL_ERROR - - if not options.on_master: - # Try removing old principals from the keytab - try: - ipautil.run([paths.IPA_RMKEYTAB, - '-k', paths.KRB5_KEYTAB, '-r', cli_realm]) - except CalledProcessError as e: - if e.returncode not in (3, 5): - # 3 - Unable to open keytab - # 5 - Principal name or realm not found in keytab - root_logger.error("Error trying to clean keytab: " + - "/usr/sbin/ipa-rmkeytab returned %s" % e.returncode) - else: - root_logger.info("Removed old keys for realm %s from %s" % ( - cli_realm, paths.KRB5_KEYTAB)) - - if options.hostname and not options.on_master: - # skip this step when run by ipa-server-install as it always configures - # hostname - tasks.backup_hostname(fstore, statestore) - tasks.set_hostname(options.hostname) - - ntp_srv_servers = [] - if not options.on_master and options.conf_ntp: - # Attempt to sync time with IPA server. - # If we're skipping NTP configuration, we also skip the time sync here. - # We assume that NTP servers are discoverable through SRV records in the DNS - # If that fails, we try to sync directly with IPA server, assuming it runs NTP - root_logger.info('Synchronizing time with KDC...') - ntp_srv_servers = ds.ipadns_search_srv(cli_domain, '_ntp._udp', - None, break_on_first=False) - synced_ntp = False - ntp_servers = ntp_srv_servers - - # use user specified NTP servers if there are any - if options.ntp_servers: - ntp_servers = options.ntp_servers - - for s in ntp_servers: - synced_ntp = ipaclient.ntpconf.synconce_ntp(s, options.debug) - if synced_ntp: - break - - if not synced_ntp and not options.ntp_servers: - synced_ntp = ipaclient.ntpconf.synconce_ntp(cli_server[0], - options.debug) - if not synced_ntp: - root_logger.warning("Unable to sync time with NTP " + - "server, assuming the time is in sync. Please check " + - "that 123 UDP port is opened.") - else: - root_logger.info('Skipping synchronizing time with NTP server.') - - if not options.unattended: - if (options.principal is None and options.password is None and - options.prompt_password is False and options.keytab is None): - options.principal = user_input("User authorized to enroll " - "computers", allow_empty=False) - root_logger.debug( - "will use principal provided as option: %s", options.principal) - - host_principal = 'host/%s@%s' % (hostname, cli_realm) - if not options.on_master: - nolog = tuple() - # First test out the kerberos configuration - try: - (krb_fd, krb_name) = tempfile.mkstemp() - os.close(krb_fd) - if configure_krb5_conf( - cli_realm=cli_realm, - cli_domain=cli_domain, - cli_server=cli_server, - cli_kdc=cli_kdc, - dnsok=False, - options=options, - filename=krb_name, - client_domain=client_domain, - client_hostname=hostname): - root_logger.error("Test kerberos configuration failed") - return CLIENT_INSTALL_ERROR - 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", cli_server[0], - "-b", str(realm_to_suffix(cli_realm)), - "-h", hostname] - if options.debug: - join_args.append("-d") - env['XMLRPC_TRACE_CURL'] = 'yes' - if options.force_join: - join_args.append("-f") - if options.principal is not None: - stdin = None - principal = options.principal - if principal.find('@') == -1: - principal = '%s@%s' % (principal, cli_realm) - if options.password is not None: - stdin = options.password - else: - if not options.unattended: - try: - stdin = getpass.getpass("Password for %s: " % principal) - except EOFError: - stdin = None - if not stdin: - root_logger.error( - "Password must be provided for %s.", principal) - return CLIENT_INSTALL_ERROR - else: - if sys.stdin.isatty(): - root_logger.error("Password must be provided in " + - "non-interactive mode.") - root_logger.info("This can be done via " + - "echo password | ipa-client-install ... " + - "or with the -w option.") - return CLIENT_INSTALL_ERROR - else: - stdin = sys.stdin.readline() - - try: - ipautil.kinit_password(principal, stdin, ccache_name, - config=krb_name) - except RuntimeError as e: - print_port_conf_info() - root_logger.error("Kerberos authentication failed: %s" % e) - return CLIENT_INSTALL_ERROR - elif options.keytab: - join_args.append("-f") - if os.path.exists(options.keytab): - try: - ipautil.kinit_keytab(host_principal, options.keytab, - ccache_name, - config=krb_name, - attempts=options.kinit_attempts) - except gssapi.exceptions.GSSError as e: - print_port_conf_info() - root_logger.error("Kerberos authentication failed: %s" - % e) - return CLIENT_INSTALL_ERROR - else: - root_logger.error("Keytab file could not be found: %s" - % options.keytab) - return CLIENT_INSTALL_ERROR - elif options.password: - nolog = (options.password,) - join_args.append("-w") - join_args.append(options.password) - elif options.prompt_password: - if options.unattended: - root_logger.error( - "Password must be provided in non-interactive mode") - return CLIENT_INSTALL_ERROR - try: - password = getpass.getpass("Password: ") - except EOFError: - password = None - if not password: - root_logger.error("Password must be provided.") - return CLIENT_INSTALL_ERROR - 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'] - get_ca_certs(fstore, options, cli_server[0], cli_basedn, - cli_realm) - del os.environ['KRB5_CONFIG'] - except errors.FileError as e: - root_logger.error(e) - return CLIENT_INSTALL_ERROR - except Exception as e: - root_logger.error("Cannot obtain CA certificate\n%s", e) - return CLIENT_INSTALL_ERROR - - # 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: - root_logger.error("Joining realm failed: %s", stderr) - if not options.force: - if result.returncode == 13: - root_logger.info("Use --force-join option to override " - "the host entry on the server " - "and force client enrollment.") - return CLIENT_INSTALL_ERROR - root_logger.info("Use ipa-getkeytab to obtain a host " + - "principal for this server.") - else: - root_logger.info("Enrolled in IPA realm %s", cli_realm) - - start = stderr.find('Certificate subject base is: ') - if start >= 0: - start = start + 29 - subject_base = stderr[start:] - subject_base = subject_base.strip() - subject_base = DN(subject_base) - - if options.principal is not None: - run(["kdestroy"], raiseonerr=False, env=env) - - # Obtain the TGT. We do it with the temporary krb5.conf, so that - # 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: - ipautil.kinit_keytab(host_principal, paths.KRB5_KEYTAB, - CCACHE_FILE, - config=krb_name, - attempts=options.kinit_attempts) - env['KRB5CCNAME'] = os.environ['KRB5CCNAME'] = CCACHE_FILE - except gssapi.exceptions.GSSError as e: - print_port_conf_info() - root_logger.error("Failed to obtain host TGT: %s" % e) - # failure to get ticket makes it impossible to login and bind - # from sssd to LDAP, abort installation and rollback changes - return CLIENT_INSTALL_ERROR - - finally: - try: - os.remove(krb_name) - except OSError: - root_logger.error("Could not remove %s", krb_name) - try: - os.rmdir(ccache_dir) - except OSError: - pass - try: - os.remove(krb_name + ".ipabkp") - except OSError: - root_logger.error("Could not remove %s.ipabkp", krb_name) - - # Configure ipa.conf - if not options.on_master: - configure_ipa_conf(fstore, cli_basedn, cli_realm, cli_domain, - cli_server, hostname) - root_logger.info("Created /etc/ipa/default.conf") - - with certdb.NSSDatabase() as tmp_db: - api.bootstrap(context='cli_installer', - debug=options.debug, - delegate=False, - nss_dir=tmp_db.secdir) - if 'config_loaded' not in api.env: - root_logger.error("Failed to initialize IPA API.") - return CLIENT_INSTALL_ERROR - - # Always back up sssd.conf. It gets updated by authconfig --enablekrb5. - fstore.backup_file(paths.SSSD_CONF) - if options.sssd: - if configure_sssd_conf(fstore, cli_realm, cli_domain, cli_server, - options, client_domain, hostname): - return CLIENT_INSTALL_ERROR - root_logger.info("Configured /etc/sssd/sssd.conf") - - if options.on_master: - # If on master assume kerberos is already configured properly. - # Get the host TGT. - try: - ipautil.kinit_keytab(host_principal, paths.KRB5_KEYTAB, - CCACHE_FILE, - attempts=options.kinit_attempts) - os.environ['KRB5CCNAME'] = CCACHE_FILE - except gssapi.exceptions.GSSError as e: - root_logger.error("Failed to obtain host TGT: %s" % e) - return CLIENT_INSTALL_ERROR - else: - # Configure krb5.conf - fstore.backup_file(paths.KRB5_CONF) - if configure_krb5_conf( - cli_realm=cli_realm, - cli_domain=cli_domain, - cli_server=cli_server, - cli_kdc=cli_kdc, - dnsok=dnsok, - options=options, - filename=paths.KRB5_CONF, - client_domain=client_domain, - client_hostname=hostname): - return CLIENT_INSTALL_ERROR - - root_logger.info( - "Configured /etc/krb5.conf for IPA realm %s", cli_realm) - - # 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 - ca_certs = x509.load_certificate_list_from_file(CACERT) - ca_certs = [ - cert.public_bytes(serialization.Encoding.DER) - for cert in ca_certs - ] - try: - pwd_file = ipautil.write_tmp_file(ipautil.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: - root_logger.info("Failed to add CA to temporary NSS database.") - return CLIENT_INSTALL_ERROR - - api.finalize() - - # Now, let's try to connect to the server's RPC interface - connected = False - try: - api.Backend.rpcclient.connect() - connected = True - root_logger.debug("Try RPC connection") - api.Backend.rpcclient.forward('ping') - except errors.KerberosError as e: - if connected: - api.Backend.rpcclient.disconnect() - root_logger.info( - "Cannot connect to the server due to Kerberos error: %s. " - "Trying with delegate=True", e) - try: - api.Backend.rpcclient.connect(delegate=True) - root_logger.debug("Try RPC connection") - api.Backend.rpcclient.forward('ping') - - root_logger.info("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 - root_logger.warning( - "Target IPA server has a lower version than the enrolled " - "client") - root_logger.warning( - "Some capabilities including the ipa command capability " - "may not be available") - except errors.PublicError as e2: - root_logger.warning( - "Second connect with delegate=True also failed: %s", e2) - root_logger.error( - "Cannot connect to the IPA server RPC interface: %s", e2) - return CLIENT_INSTALL_ERROR - except errors.PublicError as e: - root_logger.error( - "Cannot connect to the server due to generic error: %s", e) - return CLIENT_INSTALL_ERROR - - # 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() - - # Create IPA NSS database - try: - certdb.create_ipa_nssdb() - except ipautil.CalledProcessError as e: - root_logger.error("Failed to create IPA NSS database: %s", e) - return CLIENT_INSTALL_ERROR - - # Get CA certificates from the certificate store - try: - ca_certs = get_certs_from_ldap(cli_server[0], cli_basedn, cli_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, cli_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] - - # Add the CA certificates to the IPA NSS database - root_logger.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: - root_logger.error( - "Failed to add %s to the IPA NSS database.", nickname) - return CLIENT_INSTALL_ERROR - - # Add the CA certificates to the platform-dependant systemwide CA store - tasks.insert_ca_certs_into_systemwide_ca_store(ca_certs) - - if not options.on_master: - client_dns(cli_server[0], hostname, options) - configure_certmonger(fstore, subject_base, cli_realm, hostname, - options, ca_enabled) - - update_ssh_keys(hostname, services.knownservices.sshd.get_config_dir(), - options.create_sshfp) - - try: - os.remove(CCACHE_FILE) - except Exception: - pass - - #Name Server Caching Daemon. Disable for SSSD, use otherwise (if installed) - nscd = services.knownservices.nscd - if nscd.is_installed(): - save_state(nscd) - - try: - if options.sssd: - nscd_service_action = 'stop' - nscd.stop() - else: - nscd_service_action = 'restart' - nscd.restart() - except Exception: - root_logger.warning("Failed to %s the %s daemon", - nscd_service_action, nscd.service_name) - if not options.sssd: - root_logger.warning( - "Caching of users/groups will not be available") - - try: - if options.sssd: - nscd.disable() - else: - nscd.enable() - except Exception: - if not options.sssd: - root_logger.warning( - "Failed to configure automatic startup of the %s daemon", - nscd.service_name) - root_logger.info("Caching of users/groups will not be " + - "available after reboot") - else: - root_logger.warning( - "Failed to disable %s daemon. Disable it manually.", - nscd.service_name) - - else: - # this is optional service, just log - if not options.sssd: - root_logger.info("%s daemon is not installed, skip configuration", - nscd.service_name) - - nslcd = services.knownservices.nslcd - if nslcd.is_installed(): - save_state(nslcd) - - retcode, conf = (0, None) - - if not options.no_ac: - # Modify nsswitch/pam stack - tasks.modify_nsswitch_pam_stack(sssd=options.sssd, - mkhomedir=options.mkhomedir, - statestore=statestore) - - root_logger.info("%s enabled", "SSSD" if options.sssd else "LDAP") - - if options.sssd: - sssd = services.service('sssd') - try: - sssd.restart() - except CalledProcessError: - root_logger.warning("SSSD service restart was unsuccessful.") - - try: - sssd.enable() - except CalledProcessError as e: - root_logger.warning( - "Failed to enable automatic startup of the SSSD daemon: %s", e) - - if not options.sssd: - tasks.modify_pam_to_use_krb5(statestore) - root_logger.info("Kerberos 5 enabled") - - # Update non-SSSD LDAP configuration after authconfig calls as it would - # change its configuration otherways - if not options.sssd: - for configurer in [configure_ldap_conf, configure_nslcd_conf]: - (retcode, conf, filenames) = configurer(fstore, cli_basedn, cli_realm, cli_domain, cli_server, dnsok, options, nosssd_files[configurer.__name__]) - if retcode: - return CLIENT_INSTALL_ERROR - if conf: - root_logger.info( - "%s configured using configuration file(s) %s", - conf, filenames) - - if configure_openldap_conf(fstore, cli_basedn, cli_server): - root_logger.info("Configured /etc/openldap/ldap.conf") - else: - root_logger.info("Failed to configure /etc/openldap/ldap.conf") - - #Check that nss is working properly - if not options.on_master: - 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", "admin@%s" % cli_domain]) - found = True - except Exception as e: - time.sleep(1) - n = n + 1 - - if not found: - root_logger.error( - "Unable to find 'admin' user with " - "'getent passwd admin@%s'!" % cli_domain) - if conf: - root_logger.info("Recognized configuration: %s", conf) - else: - root_logger.error("Unable to reliably detect " + - "configuration. Check NSS setup manually.") - - try: - hardcode_ldap_server(cli_server) - except Exception as e: - root_logger.error("Adding hardcoded server name to " + - "/etc/ldap.conf failed: %s", str(e)) - - if options.conf_ntp and not options.on_master: - # disable other time&date services first - if options.force_ntpd: - ipaclient.ntpconf.force_ntpd(statestore) - - if options.ntp_servers: - ntp_servers = options.ntp_servers - elif ntp_srv_servers: - ntp_servers = ntp_srv_servers - else: - root_logger.warning("No SRV records of NTP servers found. IPA " - "server address will be used") - ntp_servers = cli_server - - ipaclient.ntpconf.config_ntp(ntp_servers, fstore, statestore) - root_logger.info("NTP enabled") - - if options.conf_ssh: - configure_ssh_config(fstore, options) - - if options.conf_sshd: - configure_sshd_config(fstore, options) - - if options.location: - configure_automount(options) - - if options.configure_firefox: - configure_firefox(options, statestore, cli_domain) - - if not options.no_nisdomain: - configure_nisdomain(options=options, domain=cli_domain) - - root_logger.info('Client configuration complete.') - - return 0 - def main(): safe_options, options = parse_options() @@ -3109,17 +244,17 @@ def main(): env={"PATH":"/bin:/sbin:/usr/kerberos/bin:/usr/kerberos/sbin:/usr/bin:/usr/sbin"} if options.uninstall: - return uninstall(options, env) + return client.uninstall(options, env) - if is_ipa_client_installed(on_master=options.on_master): + if client.is_ipa_client_installed(on_master=options.on_master): root_logger.error("IPA client is already configured on this system.") root_logger.info( "If you want to reinstall the IPA client, uninstall it first " + "using 'ipa-client-install --uninstall'.") - return CLIENT_ALREADY_CONFIGURED + return client.CLIENT_ALREADY_CONFIGURED - rval = install(options, env, fstore, statestore) - if rval == CLIENT_INSTALL_ERROR: + rval = client.install(options, env, fstore, statestore) + if rval == client.CLIENT_INSTALL_ERROR: if options.force: root_logger.warning( "Installation failed. Force set so not rolling back changes.") @@ -3131,7 +266,7 @@ def main(): else: root_logger.error("Installation failed. Rolling back changes.") options.unattended = True - uninstall(options, env) + client.uninstall(options, env) return rval @@ -3146,6 +281,6 @@ except RuntimeError as e: sys.exit(e) finally: try: - os.remove(CCACHE_FILE) + os.remove(client.CCACHE_FILE) except Exception: pass |