diff --git a/roles/ipaclient/defaults/main.yml b/roles/ipaclient/defaults/main.yml index 199485a0ee3da995f1f5e155ac84b199575b8396..890f35fde53889f7534488182e8391e4bfc1c52f 100644 --- a/roles/ipaclient/defaults/main.yml +++ b/roles/ipaclient/defaults/main.yml @@ -12,8 +12,12 @@ ipaclient_no_dns_lookup: no ipaclient_ssh_trust_dns: no ipaclient_no_ssh: no ipaclient_no_sshd: no +#ipaclient_no_dns_sshfp: no +#ipaclient_force: no +ipaclient_force_ntpd: no ipaclient_no_nisdomain: no ipaclient_configure_firefox: no +ipahost_all_ip_addresses: no ### packages ### ipaclient_install_packages: yes diff --git a/roles/ipaclient/library/ipaclient_test.py b/roles/ipaclient/library/ipaclient_test.py index 6a0d1e64c50878b590294503847885a04dad615a..a3dead2a57b521d9f82711c557bb4f726b1d592d 100644 --- a/roles/ipaclient/library/ipaclient_test.py +++ b/roles/ipaclient/library/ipaclient_test.py @@ -49,15 +49,6 @@ options: hostname: description: The hostname of the machine to join (FQDN). required: false - ca_cert_file: - description: A CA certificate to use. - required: false - on_master: - description: IPA client installation on IPA server - required: false - default: false - type: bool - default: no ntp_servers: description: List of NTP servers to use required: false @@ -72,14 +63,56 @@ options: default: false type: bool default: no + force_ntpd: + description: Stop and disable any time&date synchronization services besides ntpd. Deprecated since 4.7. + requried: false + type: bool + default: no + nisdomain: + description: NIS domain name + required: false no_nisdomain: description: Do not configure NIS domain name required: false type: bool default: no - nisdomain: - description: NIS domain name + kinit_attempts: + description: Repeat the request for host Kerberos ticket X times. + required: false + type: int + default: 5 + ca_cert_files: + description: CA certificates to use. + required: false + configure_firefox: + description: Configure Firefox to use IPA domain credentials + required: false + type: bool + default: no + firefox_dir: + description: Specify directory where Firefox is installed (for example: '/usr/lib/firefox') + required: false + ip_addresses: + description: All routable IP addresses configured on any interface will be added to DNS. required: false + type: bool + default: no + all_ip_addresses: + description: All routable IP addresses configured on any interface will be added to DNS. + required: false + type: bool + default: no + on_master: + description: IPA client installation on IPA server + required: false + default: false + type: bool + default: no + enable_dns_updates: + description: Configures the machine to attempt dns updates when the ip address changes. + required: false + type: bool + default: no author: - Thomas Woerner ''' @@ -227,301 +260,591 @@ def get_ipa_conf(): def main(): module = AnsibleModule( argument_spec = dict( - servers=dict(required=False, type='list', default=[]), - domain=dict(required=False), - realm=dict(required=False), - hostname=dict(required=False), - ca_cert_file=dict(required=False), - on_master=dict(required=False, type='bool', default=False), - ntp_servers=dict(required=False, type='list', default=[]), - ntp_pool=dict(required=False), + ### basic ### + domain=dict(required=False, default=None), + servers=dict(required=False, type='list', default=None), + realm=dict(required=False, default=None), + hostname=dict(required=False, default=None), + ntp_servers=dict(required=False, type='list', default=None), + ntp_pool=dict(required=False, default=None), no_ntp=dict(required=False, type='bool', default=False), - #no_nisdomain=dict(required=False, type='bool', default='no'), - #nisdomain=dict(required=False), + force_ntpd=dict(required=False, type='bool', default=False), + nisdomain=dict(required=False, default=None), + no_nisdomain=dict(required=False, type='bool', default='no'), + kinit_attempts=dict(required=False, type='int'), + ca_cert_files=dict(required=False, type='list', default=None), + configure_firefox=dict(required=False, type='bool', default=False), + firefox_dir=dict(required=False), + ip_addresses=dict(required=False, type='list', default=None), + all_ip_addresses=dict(required=False, type='bool', default=False), + on_master=dict(required=False, type='bool', default=False), + ### sssd ### + enable_dns_updates=dict(required=False, type='bool', default=False), ), supports_check_mode = True, ) - module._ansible_debug = True - options.domain = module.params.get('domain') + #module._ansible_debug = True + options.domain_name = module.params.get('domain') options.servers = module.params.get('servers') - options.realm = module.params.get('realm') - options.hostname = module.params.get('hostname') - options.ca_cert_file = module.params.get('ca_cert_file') - options.on_master = module.params.get('on_master') + options.realm_name = module.params.get('realm') + options.host_name = module.params.get('hostname') options.ntp_servers = module.params.get('ntp_servers') options.ntp_pool = module.params.get('ntp_pool') options.no_ntp = module.params.get('no_ntp') - options.conf_ntp = not options.no_ntp - #options.no_nisdomain = module.params.get('no_nisdomain') - #options.nisdomain = module.params.get('nisdomain') - #options.ip_addresses - #options.all_ip_addresses - #options.enable_dns_updates - - hostname = None - hostname_source = None - dnsok = False - cli_domain = None - cli_server = None - cli_realm = None - cli_kdc = None - client_domain = None - cli_basedn = None - - fstore = sysrestore.FileStore(paths.IPA_CLIENT_SYSRESTORE) - statestore = sysrestore.StateFile(paths.IPA_CLIENT_SYSRESTORE) + options.force_ntpd = module.params.get('force_ntpd') + options.nisdomain = module.params.get('nisdomain') + options.no_nisdomain = module.params.get('no_nisdomain') + options.kinit_attempts = module.params.get('kinit_attempts') + options.ca_cert_files = module.params.get('ca_cert_files') + options.configure_firefox = module.params.get('configure_firefox') + options.firefox_dir = module.params.get('firefox_dir') + options.ip_addresses = module.params.get('ip_addresses') + options.all_ip_addresses = module.params.get('all_ip_addresses') + options.on_master = module.params.get('on_master') + options.enable_dns_updates = module.params.get('enable_dns_updates') + + # Get domain from first server if domain is not set, but if there are + # servers + if options.domain_name is None and len(options.servers) > 0: + options.domain_name = options.servers[0][options.servers[0].find(".")+1:] + + try: + self = options + + ### HostNameInstallInterface ### + + if options.ip_addresses is not None: + for value in options.ip_addresses: + try: + CheckedIPAddress(value) + except Exception as e: + raise ValueError("invalid IP address {0}: {1}".format( + value, e)) + + ### ServiceInstallInterface ### + + validate_domain_name(options.domain_name) + + if options.realm_name: + validate_domain_name(options.realm_name, entity="realm") + + ### ClientInstallInterface ### + + if options.kinit_attempts < 1: + raise ValueError("expects an integer greater than 0.") + + ### ClientInstallInterface.__init__ ### + + if self.servers and not self.domain_name: + raise RuntimeError( + "--server cannot be used without providing --domain") - if options.ntp_servers and options.no_ntp: - module.fail_json( - msg="--ntp-server cannot be used together with --no-ntp") + if self.force_ntpd: + logger.warning("Option --force-ntpd has been deprecated") - if options.ntp_pool and options.no_ntp: - module.fail_json( - msg="--ntp-pool cannot be used together with --no-ntp") + if self.ntp_servers and self.no_ntp: + raise RuntimeError( + "--ntp-server cannot be used together with --no-ntp") - #if options.no_nisdomain and options.nisdomain: - # module.fail_json( - # "--no-nisdomain cannot be used together with --nisdomain") + if self.ntp_pool and self.no_ntp: + raise RuntimeError( + "--ntp-pool cannot be used together with --no-ntp") - #if options.ip_addresses: - # if options.enable_dns_updates: - # module.fail_json( - # "--ip-addresses cannot be used together with" - # " --enable-dns-updates") + if self.no_nisdomain and self.nisdomain: + raise RuntimeError( + "--no-nisdomain cannot be used together with --nisdomain") - # if options.all_ip_addresses: - # module.fail_json( - # "--ip-address cannot be used together with" - # "--all-ip-addresses") + if self.ip_addresses: + if self.enable_dns_updates: + raise RuntimeError( + "--ip-address cannot be used together with" + " --enable-dns-updates") - if options.hostname: - hostname = options.hostname - hostname_source = 'Provided as option' + if self.all_ip_addresses: + raise RuntimeError( + "--ip-address cannot be used together with" + "--all-ip-addresses") + + ### SSSDInstallInterface ### + + self.no_sssd = False + + ### ClientInstall ### + + if options.ca_cert_files is not None: + for value in options.ca_cert_files: + if not isinstance(value, list): + raise ValueError("Expected list, got {!r}".format(value)) + # this is what init() does + value = value[-1] + if not os.path.exists(value): + raise ValueError("'%s' does not exist" % value) + if not os.path.isfile(value): + raise ValueError("'%s' is not a file" % value) + if not os.path.isabs(value): + raise ValueError("'%s' is not an absolute file path" % value) + + try: + x509.load_certificate_from_file(value) + except Exception: + raise ValueError("'%s' is not a valid certificate file" % value) + + #self.prompt_password = self.interactive + + self.no_ac = False + + ### ClientInstall.__init__ ### + + if self.firefox_dir and not self.configure_firefox: + raise RuntimeError( + "--firefox-dir cannot be used without --configure-firefox " + "option") + + except (RuntimeError, ValueError) as e: + module.fail_json(msg=str(e)) + + ### ipaclient.install.client.init ### + + # root_logger + options.debug = False + options.unattended = not installer.interactive + if options.domain_name: + options.domain = normalize_hostname(installer.domain_name) else: - hostname = socket.getfqdn() - hostname_source = "Machine's FQDN" - if hostname != hostname.lower(): - module.fail_json( - msg="Invalid hostname '%s', must be lower-case." % hostname) - - if (hostname == 'localhost') or (hostname == 'localhost.localdomain'): - module.fail_json( - msg="Invalid hostname, '%s' must not be used." % hostname) - - # Get domain from first server if domain is not set, but there are servers - if options.domain is None and len(options.servers) > 0: - options.domain = options.servers[0][options.servers[0].find(".")+1:] - - # Create the discovery instance - ds = ipadiscovery.IPADiscovery() - - ret = ds.search( - domain=options.domain, - servers=options.servers, - realm=options.realm, - hostname=hostname, - ca_cert_path=get_cert_path(options.ca_cert_file)) - - if options.servers and ret != 0: - # There is no point to continue with installation as server list was - # passed as a fixed list of server and thus we cannot discover any - # better result - module.fail_json(msg="Failed to verify that %s is an IPA Server." % \ - ', '.join(options.servers)) - - if ret == ipadiscovery.BAD_HOST_CONFIG: - module.fail_json(msg="Can't get the fully qualified name of this host") - if ret == ipadiscovery.NOT_FQDN: - module.fail_json(msg="%s is not a fully-qualified hostname" % hostname) - if ret in (ipadiscovery.NO_LDAP_SERVER, ipadiscovery.NOT_IPA_SERVER) \ - or not ds.domain: - if ret == ipadiscovery.NO_LDAP_SERVER: - if ds.server: - module.log("%s is not an LDAP server" % ds.server) - else: - module.log("No LDAP server found") - elif ret == ipadiscovery.NOT_IPA_SERVER: - if ds.server: - module.log("%s is not an IPA server" % ds.server) - else: - module.log("No IPA server found") - else: - module.log("Domain not found") - if options.domain: - cli_domain = options.domain - cli_domain_source = 'Provided as option' - else: - module.fail_json( - msg="Unable to discover domain, not provided") + options.domain = None + options.server = options.servers + options.realm = options.realm_name + #installer.primary = installer.fixed_primary + #if installer.principal: + # installer.password = installer.admin_password + #else: + # installer.password = installer.host_password + installer.hostname = installer.host_name + options.conf_ntp = not options.no_ntp + #installer.trust_sshfp = installer.ssh_trust_dns + #installer.conf_ssh = not installer.no_ssh + #installer.conf_sshd = not installer.no_sshd + #installer.conf_sudo = not installer.no_sudo + #installer.create_sshfp = not installer.no_dns_sshfp + if installer.ca_cert_files: + installer.ca_cert_file = installer.ca_cert_files[-1] + else: + installer.ca_cert_file = None + #installer.location = installer.automount_location + installer.dns_updates = installer.enable_dns_updates + #installer.krb5_offline_passwords = not installer.no_krb5_offline_passwords + installer.sssd = not installer.no_sssd - ret = ds.search( - domain=cli_domain, - servers=options.servers, - 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 - module.debug("will use discovered domain: %s" % cli_domain) - - client_domain = hostname[hostname.find(".")+1:] - - if ret in (ipadiscovery.NO_LDAP_SERVER, ipadiscovery.NOT_IPA_SERVER) \ - or not ds.server: - module.debug("IPA Server not found") - if options.servers: - cli_server = options.servers - cli_server_source = 'Provided as option' + try: + + ### client ### + + # global variables + hostname = None + hostname_source = None + nosssd_files = None + dnsok = False + cli_domain = None + cli_server = None + subject_base = None + cli_realm = None + cli_kdc = None + client_domain = None + cli_basedn = None + # end of global variables + + ### client.install_check ### + + logger.info("This program will set up FreeIPA client.") + logger.info("Version {}".format(version.VERSION)) + logger.info("") + + cli_domain_source = 'Unknown source' + cli_server_source = 'Unknown source' + + fstore = sysrestore.FileStore(paths.IPA_CLIENT_SYSRESTORE) + + if not os.getegid() == 0: + raise ScriptError( + "You must be root to run ipa-client-install.", + rval=CLIENT_INSTALL_ERROR) + + tasks.check_selinux_status() + + #if is_ipa_client_installed(fstore, on_master=options.on_master): + # logger.error("IPA client is already configured on this system.") + # logger.info( + # "If you want to reinstall the IPA client, uninstall it first " + # "using 'ipa-client-install --uninstall'.") + # raise ScriptError(rval=CLIENT_ALREADY_CONFIGURED) + + check_ldap_conf() + + if options.conf_ntp: + try: + timeconf.check_timedate_services() + except timeconf.NTPConflictingService as e: + logger.info("WARNING: conflicting time&date synchronization service '{}'" + " will be disabled".format(e.conflicting_service)) + logger.info("in favor of chronyd") + logger.info("") + except timeconf.NTPConfigurationError: + pass + + # password, principal and keytab are checked in tasks/install.yml + #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 + #): + # raise ScriptError( + # "One of password / principal / keytab is required.", + # rval=CLIENT_INSTALL_ERROR) + + if options.hostname: + hostname = options.hostname + hostname_source = 'Provided as option' else: - module.fail_json(msg="Unable to find IPA Server to join") + hostname = socket.getfqdn() + hostname_source = "Machine's FQDN" + if hostname != hostname.lower(): + raise ScriptError( + "Invalid hostname '{}', must be lower-case.".format(hostname), + rval=CLIENT_INSTALL_ERROR + ) + + if hostname in ('localhost', 'localhost.localdomain'): + raise ScriptError( + "Invalid hostname, '{}' must not be used.".format(hostname), + rval=CLIENT_INSTALL_ERROR) + + # --no-sssd is not supported any more for rhel-based distros + if not tasks.is_nosssd_supported() and not options.sssd: + raise ScriptError( + "Option '--no-sssd' is incompatible with the 'authselect' tool " + "provided by this distribution for configuring system " + "authentication resources", + rval=CLIENT_INSTALL_ERROR) + + # --noac is not supported any more for rhel-based distros + if not tasks.is_nosssd_supported() and options.no_ac: + raise ScriptError( + "Option '--noac' is incompatible with the 'authselect' tool " + "provided by this distribution for configuring system " + "authentication resources", + rval=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): + raise ScriptError( + "The pam_krb5 package must be installed", + rval=CLIENT_INSTALL_ERROR) + + (nssldap_installed, nosssd_files) = nssldap_exists() + if not nssldap_installed: + raise ScriptError( + "One of these packages must be installed: nss_ldap or " + "nss-pam-ldapd", + rval=CLIENT_INSTALL_ERROR) + + # principal and keytab are checked in tasks/install.yml + #if options.keytab and options.principal: + # raise ScriptError( + # "Options 'principal' and 'keytab' cannot be used together.", + # rval=CLIENT_INSTALL_ERROR) + + # keytab and force_join are checked in tasks/install.yml + #if options.keytab and options.force_join: + # logger.warning("Option 'force-join' has no additional effect " + # "when used with together with option 'keytab'.") + + # Added with freeipa-4.7.1 >>> + # Remove invalid keytab file + try: + gssapi.Credentials( + store={'keytab': paths.KRB5_KEYTAB}, + usage='accept', + ) + except gssapi.exceptions.GSSError: + logger.debug("Deleting invalid keytab: '%s'.", paths.KRB5_KEYTAB) + remove_file(paths.KRB5_KEYTAB) + # Added with freeipa-4.7.1 <<< + + # Check if old certificate exist and show warning + if ( + not options.ca_cert_file and + get_cert_path(options.ca_cert_file) == paths.IPA_CA_CRT + ): + logger.warning("Using existing certificate '%s'.", paths.IPA_CA_CRT) + + if not check_ip_addresses(options): + module.warn("Failed to check ip addresses, check installation log") + raise ScriptError(rval=CLIENT_INSTALL_ERROR) + + # Create the discovery instance + ds = ipadiscovery.IPADiscovery() ret = ds.search( - domain=cli_domain, - servers=cli_server, + domain=options.domain, + servers=options.server, + realm=options.realm_name, 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.servers: - (server, domain) = ds.check_domain( - ds.domain, set(), "Validating DNS Discovery") - if server and domain: - module.debug("DNS validated, enabling discovery") - dnsok = True + 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 + logger.error( + "Failed to verify that %s is an IPA Server.", + ', '.join(options.server)) + 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() + module.warn("Failed to verify that %s is an IPA Server." % + ', '.join(options.server)) + raise ScriptError(rval=CLIENT_INSTALL_ERROR) + + if ret == ipadiscovery.BAD_HOST_CONFIG: + logger.error("Can't get the fully qualified name of this host") + logger.info("Check that the client is properly configured") + module.warn("Can't get the fully qualified name of this host") + raise ScriptError(rval=CLIENT_INSTALL_ERROR) + if ret == ipadiscovery.NOT_FQDN: + raise ScriptError( + "{} is not a fully-qualified hostname".format(hostname), + rval=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: + logger.debug("%s is not an LDAP server", ds.server) + else: + logger.debug("No LDAP server found") + elif ret == ipadiscovery.NOT_IPA_SERVER: + if ds.server: + logger.debug("%s is not an IPA server", ds.server) + else: + logger.debug("No IPA server found") else: - module.debug("DNS discovery failed, disabling discovery") + logger.debug("Domain not found") + if options.domain: + cli_domain = options.domain + cli_domain_source = 'Provided as option' + elif options.unattended: + raise ScriptError( + "Unable to discover domain, not provided on command line", + rval=CLIENT_INSTALL_ERROR) + else: + raise ScriptError("No interactive installation") + # 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' + # 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 + 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: + logger.debug("IPA Server not found") + if options.server: + cli_server = options.server + cli_server_source = 'Provided as option' + elif options.unattended: + raise ScriptError( + "Unable to find IPA Server to join", + rval=CLIENT_INSTALL_ERROR) + else: + raise ScriptError("No interactive installation") + # 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' + # 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: - module.debug( - "Using servers from command line, disabling DNS discovery") - - if not cli_server: - if options.servers: - cli_server = ds.servers - cli_server_source = 'Provided as option' - module.debug( - "will use provided server: %s" % ', '.join(options.servers)) - elif ds.server: - cli_server = ds.servers - cli_server_source = ds.server_source - module.debug("will use discovered server: %s" % cli_server[0]) - - if ret == ipadiscovery.NOT_IPA_SERVER: - module.fail_json(msg="%s is not an IPA v2 Server." % cli_server[0]) - - if ret == ipadiscovery.NO_ACCESS_TO_LDAP: - module.warn("Anonymous access to the LDAP server is disabled.") - ret = 0 - - if ret == ipadiscovery.NO_TLS_LDAP: - module.warn( - "The LDAP server requires TLS is but we do not have the CA.") - ret = 0 - - if ret != 0: - module.fail_json( - msg="Failed to verify that %s is an IPA Server." % cli_server[0]) - - cli_kdc = ds.kdc - if dnsok and not cli_kdc: - module.fail_json( - msg="DNS domain '%s' is not configured for automatic " - "KDC address lookup." % ds.realm.lower()) - - if dnsok: - module.log("Discovery was successful!") - - cli_realm = ds.realm - cli_realm_source = ds.realm_source - module.debug("will use discovered realm: %s" % cli_realm) - - if options.realm and options.realm != cli_realm: - module.fail_json( - msg= - "The provided realm name [%s] does not match discovered one [%s]" % - (options.realm, cli_realm)) - - cli_basedn = str(ds.basedn) - cli_basedn_source = ds.basedn_source - module.debug("will use discovered basedn: %s" % cli_basedn) - - module.log("Client hostname: %s" % hostname) - module.debug("Hostname source: %s" % hostname_source) - module.log("Realm: %s" % cli_realm) - module.debug("Realm source: %s" % cli_realm_source) - module.log("DNS Domain: %s" % cli_domain) - module.debug("DNS Domain source: %s" % cli_domain_source) - module.log("IPA Server: %s" % ', '.join(cli_server)) - module.debug("IPA Server source: %s" % cli_server_source) - module.log("BaseDN: %s" % cli_basedn) - module.debug("BaseDN source: %s" % cli_basedn_source) - - # ipa-join would fail with IP address instead of a FQDN - for srv in cli_server: - try: - socket.inet_pton(socket.AF_INET, srv) - is_ipaddr = True - except socket.error: + # 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: + logger.debug("DNS validated, enabling discovery") + dnsok = True + else: + logger.debug("DNS discovery failed, disabling discovery") + else: + 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' + logger.debug( + "will use provided server: %s", ', '.join(options.server)) + elif ds.server: + cli_server = ds.servers + cli_server_source = ds.server_source + logger.debug("will use discovered server: %s", cli_server[0]) + + if ret == ipadiscovery.NOT_IPA_SERVER: + logger.error("%s is not an IPA v2 Server.", cli_server[0]) + print_port_conf_info() + logger.debug("(%s: %s)", cli_server[0], cli_server_source) + raise ScriptError(rval=CLIENT_INSTALL_ERROR) + + if ret == ipadiscovery.NO_ACCESS_TO_LDAP: + logger.warning("Anonymous access to the LDAP server is disabled.") + logger.info("Proceeding without strict verification.") + logger.info( + "Note: This is not an error if anonymous access " + "has been explicitly restricted.") + ret = 0 + + if ret == ipadiscovery.NO_TLS_LDAP: + logger.warning( + "The LDAP server requires TLS is but we do not have the CA.") + logger.info("Proceeding without strict verification.") + ret = 0 + + if ret != 0: + logger.error( + "Failed to verify that %s is an IPA Server.", + cli_server[0]) + 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() + logger.debug("(%s: %s)", cli_server[0], cli_server_source) + raise ScriptError(rval=CLIENT_INSTALL_ERROR) + + cli_kdc = ds.kdc + if dnsok and not cli_kdc: + logger.error( + "DNS domain '%s' is not configured for automatic " + "KDC address lookup.", ds.realm.lower()) + logger.debug("(%s: %s)", ds.realm, ds.realm_source) + logger.error("KDC address will be set to fixed value.") + + if dnsok: + logger.info("Discovery was successful!") + elif not options.unattended: + raise ScriptError("No interactive installation") + # if not options.server: + # logger.warning( + # "The failure to use DNS to find your IPA " + # "server indicates that your resolv.conf file is not properly " + # "configured.") + # logger.info( + # "Autodiscovery of servers for failover cannot work " + # "with this configuration.") + # 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): + # raise ScriptError(rval=CLIENT_INSTALL_ERROR) + + cli_realm = ds.realm + cli_realm_source = ds.realm_source + logger.debug("will use discovered realm: %s", cli_realm) + + if options.realm_name and options.realm_name != cli_realm: + logger.error( + "The provided realm name [%s] does not match discovered one [%s]", + options.realm_name, cli_realm) + logger.debug("(%s: %s)", cli_realm, cli_realm_source) + raise ScriptError(rval=CLIENT_INSTALL_ERROR) + + cli_basedn = ds.basedn + cli_basedn_source = ds.basedn_source + logger.debug("will use discovered basedn: %s", cli_basedn) + subject_base = DN(('O', cli_realm)) + + logger.info("Client hostname: %s", hostname) + logger.debug("Hostname source: %s", hostname_source) + logger.info("Realm: %s", cli_realm) + logger.debug("Realm source: %s", cli_realm_source) + logger.info("DNS Domain: %s", cli_domain) + logger.debug("DNS Domain source: %s", cli_domain_source) + logger.info("IPA Server: %s", ', '.join(cli_server)) + logger.debug("IPA Server source: %s", cli_server_source) + logger.info("BaseDN: %s", cli_basedn) + 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_INET6, srv) + socket.inet_pton(socket.AF_INET, srv) is_ipaddr = True except socket.error: - is_ipaddr = False + try: + socket.inet_pton(socket.AF_INET6, srv) + is_ipaddr = True + except socket.error: + is_ipaddr = False + + if is_ipaddr: + logger.info() + logger.warning( + "It seems that you are using an IP address " + "instead of FQDN as an argument to --server. The " + "installation may fail.") + break - if is_ipaddr: - module.warn( - "It seems that you are using an IP address " - "instead of FQDN as an argument to --server. The " - "installation may fail.") - break + #logger.info() + #if not options.unattended and not user_input( + # "Continue to configure the system with these values?", False): + # raise ScriptError(rval=CLIENT_INSTALL_ERROR) - ntp_servers = [ ] - if sync_time is not None: - if options.conf_ntp: - # Attempt to configure and sync time with NTP server (chrony). - sync_time(options, fstore, statestore) - elif options.on_master: - # If we're on master skipping the time sync here because it was done - # in ipa-server-install - logger.info("Skipping attempt to configure and synchronize time with" - " chrony server as it has been already done on master.") - else: - logger.info("Skipping chrony configuration") - - elif 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 - if len(options.ntp_servers) < 1: - # Detect NTP servers - ds = ipadiscovery.IPADiscovery() - ntp_servers = ds.ipadns_search_srv(cli_domain, '_ntp._udp', - None, break_on_first=False) - else: - ntp_servers = options.ntp_servers - - # Attempt to sync time: - # At first with given or dicovered time servers. If no ntp - # servers have been given or discovered, then with the ipa - # server. - module.log('Synchronizing time ...') - synced_ntp = False - # use user specified NTP servers if there are any - for s in ntp_servers: - synced_ntp = timeconf.synconce_ntp(s, False) - if synced_ntp: - break - if not synced_ntp and not ntp_servers: - synced_ntp = timeconf.synconce_ntp(cli_server[0], False) - if not synced_ntp: - module.warn("Unable to sync time with NTP server") + except ScriptError as e: + module.warn("2nd part: %s" % e) + module.fail_json(msg=str(e)) + + ######################################################################### + + ### client._install ### + + statestore = sysrestore.StateFile(paths.IPA_CLIENT_SYSRESTORE) + + # May not happen in here at this time + #if not options.on_master: + # # Try removing old principals from the keytab + # purge_host_keytab(cli_realm) # Check if ipa client is already configured if is_client_configured(): @@ -535,16 +858,16 @@ def main(): "with a conflicting realm") # Done - module.exit_json(changed=True, + module.exit_json(changed=False, servers=cli_server, domain=cli_domain, realm=cli_realm, kdc=cli_kdc, - basedn=cli_basedn, + basedn=str(cli_basedn), hostname=hostname, client_domain=client_domain, dnsok=dnsok, - ntp_servers=ntp_servers, + sssd=options.sssd, ipa_python_version=IPA_PYTHON_VERSION) if __name__ == '__main__': diff --git a/roles/ipaclient/module_utils/ansible_ipa_client.py b/roles/ipaclient/module_utils/ansible_ipa_client.py index 44371792e328f724682f7e614596d8da8caebf92..efc7f690eeff454b8c87466f62809ad5737f7058 100644 --- a/roles/ipaclient/module_utils/ansible_ipa_client.py +++ b/roles/ipaclient/module_utils/ansible_ipa_client.py @@ -31,19 +31,51 @@ if NUM_VERSION < 30201: else: IPA_PYTHON_VERSION = NUM_VERSION -class options_obj(object): - pass -options = options_obj() +class installer_obj(object): + def __init__(self): + pass + + def set_logger(self, logger): + self.logger = logger + + #def __getattribute__(self, attr): + # value = super(installer_obj, self).__getattribute__(attr) + # if not attr.startswith("--") and not attr.endswith("--"): + # logger.debug( + # " <-- Accessing installer.%s (%s)" % (attr, repr(value))) + # return value + + def __getattr__(self, attr): + #logger.info(" --> ADDING missing installer.%s" % attr) + self.logger.warn(" --> ADDING missing installer.%s" % attr) + setattr(self, attr, None) + return getattr(self, attr) + + #def __setattr__(self, attr, value): + # logger.debug(" --> Setting installer.%s to %s" % (attr, repr(value))) + # return super(installer_obj, self).__setattr__(attr, value) + + def knobs(self): + for name in self.__dict__: + yield self, name + +# Initialize installer settings +installer = installer_obj() +# Create options +options = installer +options.interactive = False if NUM_VERSION >= 40400: # IPA version >= 4.4 import sys import inspect + import gssapi import logging import six + from ipapython import version try: from ipaclient.install import ipadiscovery except ImportError: @@ -63,6 +95,9 @@ if NUM_VERSION >= 40400: from ipalib import certstore from ipalib.rpc import delete_persistent_client_session_data from ipapython import certdb, ipautil + from ipapython.admintool import ScriptError + from ipapython.ipautil import CheckedIPAddress + from ipalib.util import validate_domain_name, normalize_hostname from ipaplatform import services from ipaplatform.paths import paths from ipaplatform.tasks import tasks @@ -84,7 +119,11 @@ if NUM_VERSION >= 40400: configure_certmonger, update_ssh_keys, configure_openldap_conf, \ hardcode_ldap_server, get_certs_from_ldap, save_state, \ create_ipa_nssdb, configure_ssh_config, configure_sshd_config, \ - configure_automount, configure_firefox, configure_nisdomain + configure_automount, configure_firefox, configure_nisdomain, \ + CLIENT_INSTALL_ERROR, is_ipa_client_installed, \ + CLIENT_ALREADY_CONFIGURED, nssldap_exists, remove_file, \ + check_ip_addresses, print_port_conf_info, configure_ipa_conf, \ + purge_host_keytab, configure_sssd_conf except ImportError: # Create temporary copy of ipa-client-install script (as # ipa_client_install.py) to be able to import the script easily @@ -125,6 +164,7 @@ if NUM_VERSION >= 40400: configure_krb5_conf = ipa_client_install.configure_krb5_conf if NUM_VERSION < 40100: get_ca_cert = ipa_client_install.get_ca_cert + get_ca_certs = None else: get_ca_certs = ipa_client_install.get_ca_certs SECURE_PATH = ("/bin:/sbin:/usr/kerberos/bin:/usr/kerberos/sbin:/usr/bin:/usr/sbin") @@ -179,9 +219,16 @@ if NUM_VERSION >= 40400: except ImportError: check_ldap_conf = None + try: + from ipaclient.install.client import sssd_enable_ifp + except ImportError: + sssd_enable_ifp = None + logger = logging.getLogger("ipa-client-install") + root_logger = logger else: # IPA version < 4.4 raise Exception("freeipa version '%s' is too old" % VERSION) + diff --git a/roles/ipaclient/tasks/install.yml b/roles/ipaclient/tasks/install.yml index b770195a70dddbc12cd7ba44c0bc9eb2458eb203..ba16a9a72b952568999077341d17b81ba09a1c29 100644 --- a/roles/ipaclient/tasks/install.yml +++ b/roles/ipaclient/tasks/install.yml @@ -21,17 +21,36 @@ ipaclient_servers: "{{ groups['ipaserver'] | list }}" when: ipaclient_no_dns_lookup | bool and groups.ipaserver is defined and ipaclient_servers is not defined -- name: Install - IPA discovery +- fail: msg="ipaadmin_principal and ipaadmin_keytab cannot be used together" + when: ipaadmin_keytab is defined and ipaadmin_principal is defined + +- name: Install - Set default principal if no keytab is given + set_fact: + ipaadmin_principal: admin + when: ipaadmin_principal is undefined and ipaclient_keytab is undefined + +- name: Install - IPA client test ipaclient_test: + ### basic ### domain: "{{ ipaserver_domain | default(ipaclient_domain) | default(omit) }}" servers: "{{ ipaclient_servers | default(omit) }}" realm: "{{ ipaserver_realm | default(ipaclient_realm) | default(omit) }}" hostname: "{{ ipaclient_hostname | default(ansible_fqdn) }}" - ca_cert_file: "{{ ipaclient_ca_cert_file | default(omit) }}" - on_master: "{{ ipaclient_on_master }}" - ntp_servers: "{{ ipaclient_ntp_servers | default([]) }}" + ntp_servers: "{{ ipaclient_ntp_servers | default(omit) }}" ntp_pool: "{{ ipaclient_ntp_pool | default(omit) }}" no_ntp: "{{ ipaclient_no_ntp }}" + force_ntpd: "{{ ipaclient_force_ntpd }}" + nisdomain: "{{ ipaclient_nisdomain | default(omit) }}" + no_nisdomain: "{{ ipaclient_no_nisdomain }}" + kinit_attempts: "{{ ipaclient_kinit_attempts }}" + ca_cert_files: "{{ ipaclient_ca_cert_file | default(omit) }}" + configure_firefox: "{{ ipaclient_configure_firefox }}" + firefox_dir: "{{ ipaclient_firefox_dir | default(omit) }}" + ip_addresses: "{{ ipaclient_ip_addresses | default(omit) }}" + all_ip_addresses: "{{ ipahost_all_ip_addresses }}" + on_master: "{{ ipaclient_on_master }}" + ### sssd ### + enable_dns_updates: "{{ ipassd_enable_dns_updates }}" register: result_ipaclient_test - name: Install - Set default principal if no keytab is given