From 12cafe49be52deca889c6a909f6451a464085b06 Mon Sep 17 00:00:00 2001 From: David Kupka Date: Sun, 4 Jan 2015 15:04:18 -0500 Subject: [PATCH] client: Add support for multiple IP addresses during installation. https://fedorahosted.org/freeipa/ticket/4249 Reviewed-By: Martin Basti --- ipa-client/ipa-install/ipa-client-install | 289 +++++++++++++++++++++++------- 1 file changed, 223 insertions(+), 66 deletions(-) diff --git a/ipa-client/ipa-install/ipa-client-install b/ipa-client/ipa-install/ipa-client-install index 91323ae115a27d221bcbc43fee887c56d99c8635..793de4fc950ad73b1d88f9ab4bd5178afc8b813d 100755 --- a/ipa-client/ipa-install/ipa-client-install +++ b/ipa-client/ipa-install/ipa-client-install @@ -32,6 +32,7 @@ try: from optparse import SUPPRESS_HELP, OptionGroup, OptionValueError import shutil from krbV import Krb5Error + import dns import nss.nss as nss import SSSDConfig @@ -180,9 +181,15 @@ def parse_options(): basic_group.add_option("--configure-firefox", dest="configure_firefox", action="store_true", default=False, help="configure Firefox") - parser.add_option_group(basic_group) basic_group.add_option("--firefox-dir", dest="firefox_dir", default=None, help="specify directory where Firefox is installed (for example: '/usr/lib/firefox')") + basic_group.add_option("--ip-address", dest="ip_addresses", default=[], + action="append", help="Specify IP address that should be added to DNS." + " This option can be used multiple times") + basic_group.add_option("--all-ip-addresses", dest="all_ip_addresses", + default=False, action="store_true", help="All routable IP" + " addresses configured on any inteface will be added to DNS") + parser.add_option_group(basic_group) sssd_group = OptionGroup(parser, "SSSD options") sssd_group.add_option("--permit", dest="permit", @@ -223,6 +230,15 @@ def parse_options(): if options.no_nisdomain and options.nisdomain: parser.error("--no-nisdomain cannot be used together with --nisdomain") + if options.ip_addresses: + if options.dns_updates: + parser.error("--ip-address cannot be used together with" + " --enable-dns-updates") + + if options.all_ip_addresses: + parser.error("--ip-address cannot be used together with" + " --all-ip-addresses") + return safe_opts, options def logging_setup(options): @@ -1285,6 +1301,11 @@ def configure_sssd_conf(fstore, cli_realm, cli_domain, cli_server, options, clie 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) @@ -1501,39 +1522,41 @@ def unconfigure_nisdomain(): services.knownservices.domainname.disable() -def resolve_ipaddress(server): - """ Connect to the server's LDAP port in order to determine what ip - address this machine uses as "public" ip (relative to the server). - - Returns a tuple with the IP address and address family when - connection was successful. Socket error is raised otherwise. - """ - last_socket_error = None - - 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, e: - last_socket_error = e - s = None +def get_iface_from_ip(ip_addr): + ipresult = ipautil.run([paths.IP, '-oneline', 'address', 'show']) + for line in ipresult[0].split('\n'): + fields = line.split() + if len(fields) < 6: continue - + if fields[2] not in ['inet', 'inet6']: + continue + (ip, mask) = fields[3].rsplit('/', 1) + if ip == ip_addr: + return fields[1] + else: + raise RuntimeError("IP %s not assigned to any interface." % ip_addr) + + +def get_local_ipaddresses(iface=None): + args = [paths.IP, '-oneline', 'address', 'show'] + if iface: + args += ['dev', iface] + ipresult = ipautil.run(args) + lines = ipresult[0].split('\n') + ips = [] + for line in lines: + fields = line.split() + if len(fields) < 6: + continue + if fields[2] not in ['inet', 'inet6']: + continue + (ip, mask) = fields[3].rsplit('/', 1) try: - s.connect(sa) - sockname = s.getsockname() - - # For both IPv4 and IPv6 own IP address is always the first item - return (sockname[0], af) - except socket.error, e: - last_socket_error = e - finally: - if s: - s.close() + ips.append(ipautil.CheckedIPAddress(ip)) + except ValueError: + continue + return ips - if last_socket_error is not None: - raise last_socket_error # pylint: disable=E0702 def do_nsupdate(update_txt): root_logger.debug("Writing nsupdate commands to %s:", UPDATE_FILE) @@ -1558,21 +1581,24 @@ def do_nsupdate(update_txt): return result -UPDATE_TEMPLATE_A = """ -debug +DELETE_TEMPLATE_A = """ update delete $HOSTNAME. IN A show send -update add $HOSTNAME. $TTL IN A $IPADDRESS -show -send """ -UPDATE_TEMPLATE_AAAA = """ -debug +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 @@ -1581,46 +1607,174 @@ send UPDATE_FILE = paths.IPA_DNS_UPDATE_TXT CCACHE_FILE = paths.IPA_DNS_CCACHE -def update_dns(server, hostname): +def update_dns(server, hostname, options): try: - (ip, af) = resolve_ipaddress(server) - except socket.gaierror, e: - root_logger.debug("update_dns: could not connect to server: %s", e) - root_logger.error("Cannot update DNS records! " - "Failed to connect to server '%s'.", server) - return - - sub_dict = dict(HOSTNAME=hostname, - IPADDRESS=ip, - TTL=1200 - ) - - if af == socket.AF_INET: - template = UPDATE_TEMPLATE_A - elif af == socket.AF_INET6: - template = UPDATE_TEMPLATE_AAAA + 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: - root_logger.info("Failed to determine this machine's ip address.") - root_logger.warning("Failed to update DNS A record.") + 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 - update_txt = ipautil.template_str(template, sub_dict) + 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) + - if do_nsupdate(update_txt): - root_logger.info("DNS server record set to: %s -> %s", hostname, ip) +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: - root_logger.error("Failed to update DNS records.") + msg = "Cannot get server connection interface" + if last_error: + msg += ": %s" % (last_error) + raise RuntimeError(msg) -def client_dns(server, hostname, dns_updates=False): + +def client_dns(server, hostname, options): dns_ok = ipautil.is_host_resolvable(hostname) if not dns_ok: - root_logger.warning("Hostname (%s) not found in DNS", hostname) + root_logger.warning("Hostname (%s) does not have A/AAAA record.", + hostname) + + if (options.dns_updates or options.all_ip_addresses or options.ip_addresses + or not dns_ok): + update_dns(server, hostname, options) - if dns_updates or not dns_ok: - update_dns(server, hostname) + +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(server, hostname, ssh_dir, create_sshfp): if not os.path.isdir(ssh_dir): @@ -2127,6 +2281,9 @@ def install(options, env, fstore, statestore): 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() @@ -2717,7 +2874,7 @@ def install(options, env, fstore, statestore): root_logger.info("Added CA certificates to the default NSS database.") if not options.on_master: - client_dns(cli_server[0], hostname, options.dns_updates) + client_dns(cli_server[0], hostname, options) configure_certmonger(fstore, subject_base, cli_realm, hostname, options, ca_enabled) -- 2.4.3